Engine-first Cargo workspace for DrawCraft (clean-room Illustrator): - geom: anchor-based path model, shapes, hit testing, snapping - color: colours, paints, gradients, swatches, blend modes, harmonies - doc: persistent Arc document tree, appearance stacks, text model, selection - render: vello_cpu renderer (appearance, gradients, clip groups, blend modes) - text: font DB, harfrust shaping, point/area/path layout - pathops: curve booleans (linesweeper), Pathfinder, offset, simplify - svg: import (usvg) and export - tools: selection/direct/pen/shape tools emitting commands - engine: Session, history with interactions, ~110 commands Co-Authored-By: Claude Opus 5.5 <[email protected]>
drawcraft-pathops
Path operations for DrawCraft: booleans, the Pathfinder panel, Shape Builder regions, offset path, outline stroke, simplify and the other Object → Path commands. Everything takes and returns drawcraft_geom::PathData.
Booleans (curve-preserving)
pub enum BoolOp { Union, Intersect, Difference, Xor }
pub fn boolean(a: &PathData, a_rule: FillRule, b: &PathData, b_rule: FillRule, op: BoolOp) -> PathData;
pub fn try_boolean(.., precision: f64) -> Result<PathData, PathOpsError>; // errors on NaN/inf
pub fn boolean_n(paths: &[(&PathData, FillRule)], pred: impl Fn(&[bool]) -> bool) -> PathData;
pub fn unite_all(paths: &[(&PathData, FillRule)]) -> PathData; // fast many-shape union
pub fn normalize(path: &PathData, rule: FillRule) -> PathData; // resolve self-overlaps
pub fn area(path: &PathData, rule: FillRule) -> f64; // exact filled area
The engine is linesweeper (a robust sweep line that works directly on cubic Béziers, so there's no polygon flattening). The sweep splits curves at intersections and y-extrema. Afterwards:
- pieces that lie on one input cubic are rebuilt exactly as a single sub-curve of that cubic;
- other smooth joints (for example, in stroker output) are refitted by least squares within
DEFAULT_PRECISION(0.01 pt); - collinear line pieces are merged;
- the inputs' own anchors are always kept;
- slivers narrower than the precision are dropped.
Output contours are simple and consistently oriented: outer contours and holes wind in opposite directions, so the result fills the same under either fill rule. Open subpaths are closed implicitly because they're treated as filled.
Pathfinder / Shape Builder
pub struct Shape { pub path: PathData, pub rule: FillRule, pub key: u64 } // key = paint identity
pub enum PathfinderOp { Unite, MinusFront, Intersect, Exclude, Divide, Trim, Merge, Crop, Outline, MinusBack }
pub fn pathfinder(op: PathfinderOp, shapes: &[Shape] /* back → front */) -> Vec<Shape>;
pub struct Region { pub path: PathData, pub sources: Vec<usize> }
pub fn regions(shapes: &[Shape]) -> Vec<Region>; // all faces of the arrangement
pub fn region_at(shapes: &[Shape], p: Point) -> Option<Region>;
pub fn merge_regions(regions: &[&Region]) -> PathData; // Shape Builder merge = union
Each result takes its key as follows:
| Operation | Result |
|---|---|
| Unite, Intersect, Exclude, Minus Back | Takes the front-most key |
| Minus Front | Takes the back-most key |
| Divide | One shape per face, keyed by the front-most shape that covers the face |
| Trim | One shape for each object that is still visible |
| Merge | Joins touching visible parts that share a key; each connected component becomes one shape |
| Crop | Keeps the visible parts of the lower objects inside the front-most object |
| Outline | Returns open edge paths, split at every junction; coincident edges are de-duplicated, and each keeps the front-most key |
Offset / stroke
pub use kurbo::{Cap, Join};
pub fn offset_path(path: &PathData, delta: f64, join: Join, miter_limit: f64) -> PathData; // delta < 0 insets
pub fn outline_stroke(path: &PathData, width: f64, cap: Cap, join: Join, miter_limit: f64) -> PathData;
outline_stroke passes the path to kurbo::stroke and then normalizes the result, which removes the overlaps at joins. offset_path computes fill ∪ stroke(2|d|) for a positive offset and fill − stroke(2|d|) for a negative one.
Path editing
pub fn simplify(path: &PathData, tolerance: f64) -> PathData;
pub fn simplify_with(path: &PathData, opts: &SimplifyOptions) -> PathData; // tolerance, corner_angle_deg, straight_lines
pub fn smooth(path: &PathData, amount: f64) -> PathData;
pub fn remove_redundant_points(path: &PathData, tolerance: f64) -> PathData;
pub fn add_anchor_points(path: &PathData) -> PathData;
pub fn average(path: &PathData, selection: &[(usize, usize)], axis: AverageAxis) -> PathData;
pub fn join(paths: &[PathData], tolerance: f64) -> PathData;
pub fn split_into_grid(rect: Rect, rows: usize, cols: usize, gutter: f64) -> Vec<PathData>;
simplify fits least-squares cubics with fixed end tangents, then reparameterizes with Newton steps and splits at the point of worst error. Any anchor that turns more than the corner threshold (30° by default) stays a corner.
Caveats
- linesweeper is still in beta. When two identical curves overlap, the sweep can leave a tiny lens between them; the sliver filter removes it.
- The Outline op finds split points by snapping to arrangement junctions with a small tolerance.
- The quality of offsets depends on kurbo's parallel-curve stroker. Its output is refitted, so offset results have more anchors than boolean results.