From 54fb48cec71668a8fe7fee082439c9d59facbace Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Wed, 27 May 2026 06:34:43 +0300 Subject: [PATCH] Fixed conic verb bug in eraser --- src/CanvasComponents/BrushComponentCode.cpp | 106 +++++++++++++++++++ src/CanvasComponents/BrushComponentCode.hpp | 8 ++ src/CanvasComponents/CanvasComponent.cpp | 3 + src/CanvasComponents/CanvasComponent.hpp | 1 + src/CanvasComponents/MeshCanvasComponent.cpp | 74 ++----------- src/CanvasComponents/MeshCanvasComponent.hpp | 1 + src/DrawingProgram/Tools/EraserTool.cpp | 3 + 7 files changed, 133 insertions(+), 63 deletions(-) diff --git a/src/CanvasComponents/BrushComponentCode.cpp b/src/CanvasComponents/BrushComponentCode.cpp index 92ce695..d1c3e9b 100644 --- a/src/CanvasComponents/BrushComponentCode.cpp +++ b/src/CanvasComponents/BrushComponentCode.cpp @@ -19,6 +19,8 @@ #include "BrushComponentCode.hpp" #include #include +#include +#include #include "../TimePoint.hpp" #include "../DrawingProgram/DrawingProgram.hpp" #include "../World.hpp" @@ -31,6 +33,110 @@ namespace BrushComponentCode { +void skpath_to_clipper2_pathsd(Clipper2Lib::PathsD& clipperPath, const SkPath& skPath) { + SkPath::Iter iter(skPath, false); + + for(;;) { + std::optional rec = iter.next(); + if(!rec.has_value()) + break; + + switch(rec->fVerb) { + case SkPathVerb::kClose: + break; + case SkPathVerb::kLine: + clipperPath.back().emplace_back(rec->fPoints[1].x(), rec->fPoints[1].y()); + break; + case SkPathVerb::kMove: + clipperPath.emplace_back(); + clipperPath.back().emplace_back(rec->fPoints[0].x(), rec->fPoints[0].y()); + break; + default: + throw std::runtime_error("[skpath_to_clipper2_pathsd] Illegal verb " + std::to_string(static_cast(rec->fVerb))); + break; + } + } +} + +bool clipper2_polygon_to_skpath_builder(SkPathBuilder& skPathBuilder, const Clipper2Lib::PathD& clipperPath) { + if(clipperPath.empty()) + return false; + skPathBuilder.moveTo(clipperPath.front().x, clipperPath.front().y); + for(size_t i = 1; i < clipperPath.size(); i++) + skPathBuilder.lineTo(clipperPath[i].x, clipperPath[i].y); + skPathBuilder.close(); + return true; +} + +void sort_clipper_polytreed_into_skpaths(std::vector& paths, const Clipper2Lib::PolyTreeD& tree) { + using namespace Clipper2Lib; + for(auto& treeChild : tree) { + SkPathBuilder newPath; + if(!clipper2_polygon_to_skpath_builder(newPath, treeChild->Polygon())) + continue; + for(auto& child : *treeChild) { + if(child->IsHole()) { + clipper2_polygon_to_skpath_builder(newPath, child->Polygon()); + for(auto& childChild : *child) { + if(childChild->IsHole()) + continue; + else + sort_clipper_polytreed_into_skpaths(paths, *child); + } + } + else + sort_clipper_polytreed_into_skpaths(paths, *child); + } + paths.emplace_back(newPath.detach()); + } +} + +void sort_clipper_polytreed_into_skpath_builder(SkPathBuilder& skPathBuilder, const Clipper2Lib::PolyTreeD& tree) { + using namespace Clipper2Lib; + for(auto& treeChild : tree) { + if(!clipper2_polygon_to_skpath_builder(skPathBuilder, treeChild->Polygon())) + continue; + for(auto& child : *treeChild) { + if(child->IsHole()) { + clipper2_polygon_to_skpath_builder(skPathBuilder, child->Polygon()); + for(auto& childChild : *child) { + if(childChild->IsHole()) + continue; + else + sort_clipper_polytreed_into_skpath_builder(skPathBuilder, *child); + } + } + else + sort_clipper_polytreed_into_skpath_builder(skPathBuilder, *child); + } + } +} + +void clipper2_polygons_to_skpath_builder(SkPathBuilder& skPathBuilder, const Clipper2Lib::PathsD& clipperPath) { + for(auto& p : clipperPath) + clipper2_polygon_to_skpath_builder(skPathBuilder, p); +} + +std::optional skpath_simplify_only_lines(const SkPath& skPath) { + using namespace Clipper2Lib; + PathsD clippingSubjects; + skpath_to_clipper2_pathsd(clippingSubjects, skPath); + ClipperD clipper; + PathsD solutionPaths; + + clipper.AddSubject(clippingSubjects); + clipper.AddClip(clippingSubjects); + if(!clipper.Execute(ClipType::Union, skPath.getFillType() == SkPathFillType::kEvenOdd ? FillRule::EvenOdd : FillRule::NonZero, solutionPaths)) + return std::nullopt; + + SimplifyPaths(solutionPaths, 0.01); + + SkPathBuilder newPath; + newPath.setFillType(skPath.getFillType()); + clipper2_polygons_to_skpath_builder(newPath, solutionPaths); + return newPath.detach(); +} + SkPath brush_stroke_to_skpath(const std::vector& brushPoints, bool hasRoundCaps) { std::vector points = smooth_points(brushPoints, 0, brushPoints.size() - 1, DEFAULT_SMOOTHNESS); return create_triangles(brushPoints, points, hasRoundCaps); diff --git a/src/CanvasComponents/BrushComponentCode.hpp b/src/CanvasComponents/BrushComponentCode.hpp index c738c53..d47df67 100644 --- a/src/CanvasComponents/BrushComponentCode.hpp +++ b/src/CanvasComponents/BrushComponentCode.hpp @@ -20,6 +20,7 @@ #include "../CoordSpaceHelper.hpp" #include #include "../InputManager.hpp" +#include class DrawingProgram; @@ -31,6 +32,7 @@ struct MeshShapeData { }; namespace BrushComponentCode { + constexpr float CLIPPER_SIMPLIFY_EPSILON = 0.1; struct BrushPoint { Vector2f pos; float width; @@ -47,6 +49,12 @@ namespace BrushComponentCode { CoordSpaceHelper coords; }; + void skpath_to_clipper2_pathsd(Clipper2Lib::PathsD& clipperPath, const SkPath& skPath); + bool clipper2_polygon_to_skpath_builder(SkPathBuilder& skPathBuilder, const Clipper2Lib::PathD& clipperPath); + void sort_clipper_polytreed_into_skpaths(std::vector& paths, const Clipper2Lib::PolyTreeD& tree); + void clipper2_polygons_to_skpath_builder(SkPathBuilder& skPathBuilder, const Clipper2Lib::PathsD& clipperPath); + std::optional skpath_simplify_only_lines(const SkPath& skPath); + SkPath brush_stroke_to_skpath(const std::vector& brushPoints, bool hasRoundCaps); SkPath create_triangles(const std::vector& regularPoints, const std::vector& smoothedPoints, bool hasRoundCaps); std::vector get_wedge_indices(const std::vector& points); diff --git a/src/CanvasComponents/CanvasComponent.cpp b/src/CanvasComponents/CanvasComponent.cpp index 5f26794..d448201 100644 --- a/src/CanvasComponents/CanvasComponent.cpp +++ b/src/CanvasComponents/CanvasComponent.cpp @@ -75,6 +75,9 @@ bool CanvasComponent::can_erase_detail() const { return false; } +void CanvasComponent::simplify_paths() { +} + std::vector CanvasComponent::attempt_split(DrawingProgram& drawP) const { return {}; } CanvasComponentEraseDetailResult CanvasComponent::erase_detail(const SkPath& eraseAgainst) { diff --git a/src/CanvasComponents/CanvasComponent.hpp b/src/CanvasComponents/CanvasComponent.hpp index e92bbd2..64021e8 100644 --- a/src/CanvasComponents/CanvasComponent.hpp +++ b/src/CanvasComponents/CanvasComponent.hpp @@ -42,6 +42,7 @@ class CanvasComponent { 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 void simplify_paths(); 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 339c20e..887c7cd 100644 --- a/src/CanvasComponents/MeshCanvasComponent.cpp +++ b/src/CanvasComponents/MeshCanvasComponent.cpp @@ -272,43 +272,6 @@ 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; @@ -316,32 +279,9 @@ std::vector MeshCanvasComponent::attempt_split(Drawin PathsD clippingSubjects; - SkPath::Iter iter(d.meshPath, false); + BrushComponentCode::skpath_to_clipper2_pathsd(clippingSubjects, d.meshPath); - 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) + if(clippingSubjects.size() <= 1) return {}; ClipperD clipper; @@ -353,7 +293,7 @@ std::vector MeshCanvasComponent::attempt_split(Drawin std::vector splitPaths; - sort_tree_into_sk_paths(splitPaths, solutionTree); + BrushComponentCode::sort_clipper_polytreed_into_skpaths(splitPaths, solutionTree); if(splitPaths.size() <= 1) return toRet; @@ -364,6 +304,7 @@ std::vector MeshCanvasComponent::attempt_split(Drawin mesh.d.color = d.color; mesh.d.meshPath = p; newComp->coords = compContainer->coords; + mesh.simplify_paths(); toRet.emplace_back(newComp); } @@ -376,6 +317,7 @@ CanvasComponentEraseDetailResult MeshCanvasComponent::erase_detail(const SkPath& return CanvasComponentEraseDetailResult::NO_CHANGE; std::optional newPath = Op(d.meshPath, eraseAgainst, SkPathOp::kDifference_SkPathOp); if(newPath.has_value()) { + auto oldPath = d.meshPath; d.meshPath = newPath.value(); if(d.meshPath.isEmpty()) return CanvasComponentEraseDetailResult::REMOVED; @@ -384,6 +326,12 @@ CanvasComponentEraseDetailResult MeshCanvasComponent::erase_detail(const SkPath& return CanvasComponentEraseDetailResult::NO_CHANGE; } +void MeshCanvasComponent::simplify_paths() { + auto newPath = Simplify(d.meshPath); + if(newPath.has_value()) + d.meshPath = newPath.value(); +} + SCollision::AABB MeshCanvasComponent::get_obj_coord_bounds() const { return d.meshPath.getBounds(); } diff --git a/src/CanvasComponents/MeshCanvasComponent.hpp b/src/CanvasComponents/MeshCanvasComponent.hpp index 90ac47e..91171eb 100644 --- a/src/CanvasComponents/MeshCanvasComponent.hpp +++ b/src/CanvasComponents/MeshCanvasComponent.hpp @@ -36,6 +36,7 @@ class MeshCanvasComponent : public CanvasComponent { 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; + virtual void simplify_paths() override; struct Data { SkPath meshPath; diff --git a/src/DrawingProgram/Tools/EraserTool.cpp b/src/DrawingProgram/Tools/EraserTool.cpp index 83ff71d..4c6607e 100644 --- a/src/DrawingProgram/Tools/EraserTool.cpp +++ b/src/DrawingProgram/Tools/EraserTool.cpp @@ -86,6 +86,8 @@ void EraserTool::input_mouse_button_on_canvas_callback(const InputManager::Mouse } BrushComponentCode::mouse_button(drawP, genData, drawP.world.drawData.cam.c, button, relativeWidthResult.first.value()); + // NOTE: Must set erase path when isErasing is set to true to make sure that the Circle path from not erasing part doesn't reach the erase_on_path code + erasePath = BrushComponentCode::brush_stroke_to_skpath(genData.brushPoints, drawP.world.main.toolConfig.brush.hasRoundCaps); isErasing = true; } else if(!button.down && isErasing) { @@ -232,6 +234,7 @@ void EraserTool::commit_erase() { drawP.layerMan.erase_component_container(erasedComponents, !placedNewObject); bool first = !objectsToErase && !placedNewObject; for(auto& [comp, oldData] : updatedComponents) { + comp->obj->get_comp().simplify_paths(); comp->obj->commit_update_dont_invalidate_cache(drawP); comp->obj->send_comp_update(drawP, true); if(first) {