Fixed conic verb bug in eraser

This commit is contained in:
Yousef Khadadeh 2026-05-27 06:34:43 +03:00
parent c3fa1deee9
commit 54fb48cec7
7 changed files with 133 additions and 63 deletions

View file

@ -19,6 +19,8 @@
#include "BrushComponentCode.hpp"
#include <Helpers/ConvertVec.hpp>
#include <Helpers/MathExtras.hpp>
#include <clipper2/clipper.h>
#include <include/core/SkPathTypes.h>
#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<SkPath::IterRec> 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<unsigned>(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<SkPath>& 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> 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<BrushPoint>& brushPoints, bool hasRoundCaps) {
std::vector<BrushPoint> points = smooth_points(brushPoints, 0, brushPoints.size() - 1, DEFAULT_SMOOTHNESS);
return create_triangles(brushPoints, points, hasRoundCaps);

View file

@ -20,6 +20,7 @@
#include "../CoordSpaceHelper.hpp"
#include <include/core/SkPathBuilder.h>
#include "../InputManager.hpp"
#include <clipper2/clipper.h>
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<SkPath>& paths, const Clipper2Lib::PolyTreeD& tree);
void clipper2_polygons_to_skpath_builder(SkPathBuilder& skPathBuilder, const Clipper2Lib::PathsD& clipperPath);
std::optional<SkPath> skpath_simplify_only_lines(const SkPath& skPath);
SkPath brush_stroke_to_skpath(const std::vector<BrushPoint>& brushPoints, bool hasRoundCaps);
SkPath create_triangles(const std::vector<BrushPoint>& regularPoints, const std::vector<BrushPoint>& smoothedPoints, bool hasRoundCaps);
std::vector<size_t> get_wedge_indices(const std::vector<BrushPoint>& points);

View file

@ -75,6 +75,9 @@ bool CanvasComponent::can_erase_detail() const {
return false;
}
void CanvasComponent::simplify_paths() {
}
std::vector<CanvasComponentContainer*> CanvasComponent::attempt_split(DrawingProgram& drawP) const { return {}; }
CanvasComponentEraseDetailResult CanvasComponent::erase_detail(const SkPath& eraseAgainst) {

View file

@ -42,6 +42,7 @@ class CanvasComponent {
virtual void remap_resource_ids(const std::unordered_map<NetworkingObjects::NetObjID, NetworkingObjects::NetObjID>& resourceOldToNewMap);
virtual void change_stroke_color(const Vector4f& newStrokeColor);
virtual std::vector<CanvasComponentContainer*> attempt_split(DrawingProgram& drawP) const;
virtual void simplify_paths();
virtual std::optional<Vector4f> get_stroke_color() const;
virtual void set_data_from(const CanvasComponent& other) = 0;

View file

@ -272,43 +272,6 @@ bool MeshCanvasComponent::can_erase_detail() const {
return true;
}
void sort_tree_into_sk_paths(std::vector<SkPath>& 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<CanvasComponentContainer*> MeshCanvasComponent::attempt_split(DrawingProgram& drawP) const {
using namespace Clipper2Lib;
@ -316,32 +279,9 @@ std::vector<CanvasComponentContainer*> 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<SkPath::IterRec> 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<unsigned>(rec->fVerb)));
break;
}
}
if(moveCount <= 1)
if(clippingSubjects.size() <= 1)
return {};
ClipperD clipper;
@ -353,7 +293,7 @@ std::vector<CanvasComponentContainer*> MeshCanvasComponent::attempt_split(Drawin
std::vector<SkPath> 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<CanvasComponentContainer*> 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<SkPath> 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<float> MeshCanvasComponent::get_obj_coord_bounds() const {
return d.meshPath.getBounds();
}

View file

@ -36,6 +36,7 @@ class MeshCanvasComponent : public CanvasComponent {
virtual std::vector<CanvasComponentContainer*> attempt_split(DrawingProgram& drawP) const override;
virtual std::optional<Vector4f> get_stroke_color() const override;
virtual void set_data_from(const CanvasComponent& other) override;
virtual void simplify_paths() override;
struct Data {
SkPath meshPath;

View file

@ -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) {