From c3fa1deee9b839396962312347979700783f7768 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Tue, 26 May 2026 17:08:43 +0300 Subject: [PATCH] Split mesh on erase --- src/CanvasComponents/CanvasComponent.cpp | 2 + src/CanvasComponents/CanvasComponent.hpp | 1 + src/CanvasComponents/MeshCanvasComponent.cpp | 106 +++++++++++++++++- src/CanvasComponents/MeshCanvasComponent.hpp | 1 + .../Layers/DrawingProgramLayerManager.cpp | 25 ++++- .../Layers/DrawingProgramLayerManager.hpp | 11 +- src/DrawingProgram/Tools/BrushTool.cpp | 7 +- src/DrawingProgram/Tools/EraserTool.cpp | 31 ++++- 8 files changed, 168 insertions(+), 16 deletions(-) diff --git a/src/CanvasComponents/CanvasComponent.cpp b/src/CanvasComponents/CanvasComponent.cpp index 6fd2fdd..5f26794 100644 --- a/src/CanvasComponents/CanvasComponent.cpp +++ b/src/CanvasComponents/CanvasComponent.cpp @@ -75,6 +75,8 @@ bool CanvasComponent::can_erase_detail() const { return false; } +std::vector CanvasComponent::attempt_split(DrawingProgram& drawP) const { return {}; } + CanvasComponentEraseDetailResult CanvasComponent::erase_detail(const SkPath& eraseAgainst) { return CanvasComponentEraseDetailResult::NO_CHANGE; } diff --git a/src/CanvasComponents/CanvasComponent.hpp b/src/CanvasComponents/CanvasComponent.hpp index d3649c6..e92bbd2 100644 --- a/src/CanvasComponents/CanvasComponent.hpp +++ b/src/CanvasComponents/CanvasComponent.hpp @@ -41,6 +41,7 @@ class CanvasComponent { virtual void get_used_resources(std::unordered_set& resourceSet) const; virtual void remap_resource_ids(const std::unordered_map& resourceOldToNewMap); virtual void change_stroke_color(const Vector4f& newStrokeColor); + virtual std::vector attempt_split(DrawingProgram& drawP) const; virtual std::optional get_stroke_color() const; virtual void set_data_from(const CanvasComponent& other) = 0; diff --git a/src/CanvasComponents/MeshCanvasComponent.cpp b/src/CanvasComponents/MeshCanvasComponent.cpp index 07a0626..339c20e 100644 --- a/src/CanvasComponents/MeshCanvasComponent.cpp +++ b/src/CanvasComponents/MeshCanvasComponent.cpp @@ -33,9 +33,10 @@ #include "../DrawCollision.hpp" #include "CanvasComponentContainer.hpp" #include "Helpers/SCollision.hpp" +#include "../DrawingProgram/DrawingProgram.hpp" +#include "../World.hpp" #include - -#define BVH_LEVELS 3 +#include template void skpath_write(const SkPath& p, Archive& a) { std::vector contourSizes; @@ -106,6 +107,9 @@ void MeshCanvasComponent::load_file(cereal::PortableBinaryInputArchive& a, Versi bool hasRoundCaps; a(brushPoints, d.color, hasRoundCaps); d.meshPath = BrushComponentCode::brush_stroke_to_skpath(brushPoints, hasRoundCaps); + std::optional simplified = Simplify(d.meshPath); + if(simplified.has_value()) + d.meshPath = simplified.value(); } } @@ -268,6 +272,104 @@ bool MeshCanvasComponent::can_erase_detail() const { return true; } +void sort_tree_into_sk_paths(std::vector& paths, const Clipper2Lib::PolyTreeD& tree) { + using namespace Clipper2Lib; + for(auto& treeChild : tree) { + SkPathBuilder newPath; + { + const PathD& poly = treeChild->Polygon(); + if(poly.empty()) { + continue; + } + newPath.moveTo(poly.front().x, poly.front().y); + for(size_t i = 1; i < poly.size(); i++) + newPath.lineTo(poly[i].x, poly[i].y); + newPath.close(); + } + for(auto& child : *treeChild) { + if(child->IsHole()) { + const PathD& poly = child->Polygon(); + newPath.moveTo(poly.front().x, poly.front().y); + for(size_t i = 1; i < poly.size(); i++) + newPath.lineTo(poly[i].x, poly[i].y); + newPath.close(); + for(auto& childChild : *child) { + if(childChild->IsHole()) + continue; + else { + sort_tree_into_sk_paths(paths, *child); + } + } + } + else { + sort_tree_into_sk_paths(paths, *child); + } + } + paths.emplace_back(newPath.detach()); + } +} + +std::vector MeshCanvasComponent::attempt_split(DrawingProgram& drawP) const { + using namespace Clipper2Lib; + + std::vector toRet; + + PathsD clippingSubjects; + + SkPath::Iter iter(d.meshPath, false); + + size_t moveCount = 0; + for(;;) { + std::optional rec = iter.next(); + if(!rec.has_value()) + break; + + switch(rec->fVerb) { + case SkPathVerb::kClose: + break; + case SkPathVerb::kLine: + clippingSubjects.back().emplace_back(rec->fPoints[1].x(), rec->fPoints[1].y()); + break; + case SkPathVerb::kMove: + clippingSubjects.emplace_back(); + clippingSubjects.back().emplace_back(rec->fPoints[0].x(), rec->fPoints[0].y()); + moveCount++; + break; + default: + throw std::runtime_error("[attempt_split] Illegal verb " + std::to_string(static_cast(rec->fVerb))); + break; + } + } + + if(moveCount <= 1) + return {}; + + ClipperD clipper; + PolyTreeD solutionTree; + + clipper.AddSubject(clippingSubjects); + clipper.AddClip(clippingSubjects); + clipper.Execute(ClipType::Union, FillRule::EvenOdd, solutionTree); + + std::vector splitPaths; + + sort_tree_into_sk_paths(splitPaths, solutionTree); + + if(splitPaths.size() <= 1) + return toRet; + + for(const SkPath& p : splitPaths) { + CanvasComponentContainer* newComp = new CanvasComponentContainer(drawP.world.netObjMan, CanvasComponentType::MESH); + MeshCanvasComponent& mesh = static_cast(newComp->get_comp()); + mesh.d.color = d.color; + mesh.d.meshPath = p; + newComp->coords = compContainer->coords; + toRet.emplace_back(newComp); + } + + return toRet; +} + CanvasComponentEraseDetailResult MeshCanvasComponent::erase_detail(const SkPath& eraseAgainst) { bool intersectsAABB = d.meshPath.getBounds().intersects(eraseAgainst.getBounds()); if(!intersectsAABB) diff --git a/src/CanvasComponents/MeshCanvasComponent.hpp b/src/CanvasComponents/MeshCanvasComponent.hpp index adda9ef..90ac47e 100644 --- a/src/CanvasComponents/MeshCanvasComponent.hpp +++ b/src/CanvasComponents/MeshCanvasComponent.hpp @@ -33,6 +33,7 @@ class MeshCanvasComponent : public CanvasComponent { virtual void load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version) override; virtual std::unique_ptr get_data_copy() const override; virtual void change_stroke_color(const Vector4f& newStrokeColor) override; + virtual std::vector attempt_split(DrawingProgram& drawP) const override; virtual std::optional get_stroke_color() const override; virtual void set_data_from(const CanvasComponent& other) override; diff --git a/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp b/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp index cb88def..77d7bf5 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayerManager.cpp @@ -250,6 +250,12 @@ std::vector DrawingProgramLayerManage return {}; } +std::vector DrawingProgramLayerManager::add_many_components_to_layer(DrawingProgramLayerListItem* layer, const std::vector>& newObjs, bool newUndo) { + auto toRet = layer->get_layer().components->insert_sorted_list_and_send_create(layer->get_layer().components, newObjs); + add_undo_place_components(layer, toRet, newUndo); + return toRet; +} + void DrawingProgramLayerManager::disable_add_to_cache_block(const std::function& toRun) { addToCacheOnComponentInsert = false; toRun(); @@ -368,8 +374,8 @@ void DrawingProgramLayerManager::add_undo_place_component(CanvasComponentContain drawP.world.undo.push(std::make_unique(objInfo->pos, drawP.world.undo.get_undoid_from_netid(objInfo->obj.get_net_id()), drawP.world.undo.get_undoid_from_netid(objInfo->obj->parentLayer->get_layer().components.get_net_id()))); } -void DrawingProgramLayerManager::erase_component_map(const std::unordered_map>& eraseMap) { - add_undo_erase_components(eraseMap); +void DrawingProgramLayerManager::erase_component_map(const std::unordered_map>& eraseMap, bool newUndo) { + add_undo_erase_components(eraseMap, newUndo); drawP.world.netObjMan.send_multi_update_messsage([&]() { for(auto& [layerListItem, netObjSetToErase] : eraseMap) { auto& layerComponentList = layerListItem->get_layer().components; @@ -378,7 +384,7 @@ void DrawingProgramLayerManager::erase_component_map(const std::unordered_map>& eraseMap) { +void DrawingProgramLayerManager::add_undo_erase_components(const std::unordered_map>& eraseMap, bool newUndo) { using namespace NetworkingObjects; struct EraseListData { @@ -485,10 +491,14 @@ void DrawingProgramLayerManager::add_undo_erase_components(const std::unordered_ ++i; } } - undoMan.push(std::make_unique(std::move(undoMap))); + + if(newUndo) + undoMan.push(std::make_unique(std::move(undoMap))); + else + undoMan.push_on_last(std::make_unique(std::move(undoMap))); } -void DrawingProgramLayerManager::add_undo_place_components(DrawingProgramLayerListItem* parent, const std::vector& placeList) { +void DrawingProgramLayerManager::add_undo_place_components(DrawingProgramLayerListItem* parent, const std::vector& placeList, bool newUndo) { using namespace NetworkingObjects; struct PlaceListData { @@ -575,5 +585,8 @@ void DrawingProgramLayerManager::add_undo_place_components(DrawingProgramLayerLi undoData.pos.emplace_back(comp->pos - i); ++i; } - undoMan.push(std::make_unique(undoMan.get_undoid_from_netid(parent->get_layer().components.get_net_id()), std::move(undoData))); + if(newUndo) + undoMan.push(std::make_unique(undoMan.get_undoid_from_netid(parent->get_layer().components.get_net_id()), std::move(undoData))); + else + undoMan.push_on_last(std::make_unique(undoMan.get_undoid_from_netid(parent->get_layer().components.get_net_id()), std::move(undoData))); } diff --git a/src/DrawingProgram/Layers/DrawingProgramLayerManager.hpp b/src/DrawingProgram/Layers/DrawingProgramLayerManager.hpp index 391d8f5..f69b746 100644 --- a/src/DrawingProgram/Layers/DrawingProgramLayerManager.hpp +++ b/src/DrawingProgram/Layers/DrawingProgramLayerManager.hpp @@ -31,9 +31,9 @@ class DrawingProgramLayerManager { NetworkingObjects::NetObjOwnerPtr layerTreeRoot; NetworkingObjects::NetObjWeakPtr editingLayer; void set_initial_editing_layer(); - void add_undo_erase_components(const std::unordered_map>& eraseMap); - void add_undo_place_components(DrawingProgramLayerListItem* parent, const std::vector& placeList); - void erase_component_map(const std::unordered_map>& eraseMap); + void add_undo_erase_components(const std::unordered_map>& eraseMap, bool newUndo = true); + void add_undo_place_components(DrawingProgramLayerListItem* parent, const std::vector& placeList, bool newUndo = true); + void erase_component_map(const std::unordered_map>& eraseMap, bool newUndo = true); bool commitUpdateOnComponentInsert = true; bool addToCacheOnComponentInsert = true; public: @@ -49,7 +49,7 @@ class DrawingProgramLayerManager { void read_components_client(cereal::PortableBinaryInputArchive& a); bool is_a_layer_being_edited(); void scale_up(const WorldScalar& scaleUpAmount); - template void erase_component_container(const List& compsToErase) { + template void erase_component_container(const List& compsToErase, bool newUndo = true) { if(!compsToErase.empty()) { std::unordered_map> idsToEraseInSpecificLayers; for(auto& c : compsToErase) @@ -59,7 +59,7 @@ class DrawingProgramLayerManager { return a->pos < b->pos; }); } - erase_component_map(idsToEraseInSpecificLayers); + erase_component_map(idsToEraseInSpecificLayers, newUndo); } } uint32_t edited_layer_component_count(); @@ -80,6 +80,7 @@ class DrawingProgramLayerManager { CanvasComponentContainer::ObjInfo* add_component_to_layer_being_edited(CanvasComponentContainer* newObj); std::vector add_many_components_to_layer_being_edited(const std::vector>& newObjs); + std::vector add_many_components_to_layer(DrawingProgramLayerListItem* layer, const std::vector>& newObjs, bool newUndo = true); std::vector get_flattened_component_list() const; std::vector get_flattened_layer_list(); void add_undo_place_component(CanvasComponentContainer::ObjInfo* objInfo); diff --git a/src/DrawingProgram/Tools/BrushTool.cpp b/src/DrawingProgram/Tools/BrushTool.cpp index fafc134..7db7b4c 100644 --- a/src/DrawingProgram/Tools/BrushTool.cpp +++ b/src/DrawingProgram/Tools/BrushTool.cpp @@ -25,9 +25,9 @@ #include "../../CanvasComponents/MeshCanvasComponent.hpp" #include "Helpers/NetworkingObjects/NetObjTemporaryPtr.decl.hpp" #include "../../CanvasComponents/CanvasComponentContainer.hpp" - #include "../../GUIStuff/ElementHelpers/TextLabelHelpers.hpp" #include "../../GUIStuff/ElementHelpers/CheckBoxHelpers.hpp" +#include #define MINIMUM_DISTANCE_FROM_FIRST_POINT 3.0f #define MINIMUM_DISTANCE_TO_NEXT_POINT 0.002f @@ -83,6 +83,11 @@ void BrushTool::commit_data(bool final) { NetworkingObjects::NetObjOwnerPtr& containerPtr = objInfoBeingEdited->obj; MeshCanvasComponent& newMesh = static_cast(containerPtr->get_comp()); newMesh.d.meshPath = BrushComponentCode::brush_stroke_to_skpath(genData.brushPoints, drawP.world.main.toolConfig.brush.hasRoundCaps); + if(final) { + std::optional simplified = Simplify(newMesh.d.meshPath); + if(simplified.has_value()) + newMesh.d.meshPath = simplified.value(); + } containerPtr->commit_update(drawP); containerPtr->send_comp_update(drawP, final); } diff --git a/src/DrawingProgram/Tools/EraserTool.cpp b/src/DrawingProgram/Tools/EraserTool.cpp index 30de75a..83ff71d 100644 --- a/src/DrawingProgram/Tools/EraserTool.cpp +++ b/src/DrawingProgram/Tools/EraserTool.cpp @@ -24,6 +24,7 @@ #include #include "../../CanvasComponents/BrushComponentCode.hpp" #include "../EditCanvasComponentWorldUndoAction.hpp" +#include #include "../../GUIStuff/ElementHelpers/TextLabelHelpers.hpp" #include "../../GUIStuff/ElementHelpers/RadioButtonHelpers.hpp" @@ -88,6 +89,10 @@ void EraserTool::input_mouse_button_on_canvas_callback(const InputManager::Mouse isErasing = true; } else if(!button.down && isErasing) { + // If not real time eraser, we can benefit from simplifying the path + std::optional simplified = Simplify(erasePath); + if(simplified.has_value()) + erasePath = simplified.value(); erase_on_path(); reset_erasing_stroke(); commit_erase(); @@ -202,8 +207,30 @@ void EraserTool::erase_component(CanvasComponentContainer::ObjInfo* erasedComp) } void EraserTool::commit_erase() { - bool first = erasedComponents.empty(); // erase_component_container will call EraserTool::erase_component, which will clear erasedComponents eventually - drawP.layerMan.erase_component_container(erasedComponents); + bool placedNewObject = false; + std::unordered_map>> newObjectsToPlace; + std::erase_if(updatedComponents, [&] (auto& p) { + auto& [comp, oldData] = p; + std::vector splitComps = comp->obj->get_comp().attempt_split(drawP); + if(!splitComps.empty()) { + for(CanvasComponentContainer* c : splitComps) + newObjectsToPlace[comp->obj->parentLayer].emplace_back(std::next(comp->obj->objInfo), c); + placedNewObject = true; + erasedComponents.emplace(comp); + comp->obj->get_comp().set_data_from(*oldData); + return true; + } + return false; + }); + bool firstPlacement = true; + for(auto& [layer, compMap] : newObjectsToPlace) { + drawP.layerMan.add_many_components_to_layer(layer, compMap, firstPlacement); + firstPlacement = false; + } + bool objectsToErase = !erasedComponents.empty(); + if(objectsToErase) + drawP.layerMan.erase_component_container(erasedComponents, !placedNewObject); + bool first = !objectsToErase && !placedNewObject; for(auto& [comp, oldData] : updatedComponents) { comp->obj->commit_update_dont_invalidate_cache(drawP); comp->obj->send_comp_update(drawP, true);