vectorcraft/crates/pathops
bflatastic 18d870c7df M3.37: Outline Stroke and the live Outline Stroke effect outline strokes as the canvas paints them
Outline Stroke (and the stroke part of Expand) now builds each stroke from the
shared canvas geometry: dash shapes, arrowheads, width profiles and inside/outside
alignment, united into one clean path (pathops::stroke_region). Every stroke item
is outlined in paint order (runs of fills stay on copies of the path), stroke
opacity, blend mode and effects move to the new fill, and brushed strokes become
their brush art. The live Effect > Path > Outline Stroke outlines the stroke it
sits on (on a fill or the object: the top stroke) with that stroke's cap, join,
dashes, alignment, profile, arrowheads and weight: GeomContext now carries the
stroke and the fill rule, built per item by the renderer, export baking and
Expand Appearance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:53:58 +07:00
..
src M3.37: Outline Stroke and the live Outline Stroke effect outline strokes as the canvas paints them 2026-10-04 22:53:58 +07:00
tests Rename DrawCraft → VectorCraft: crates (vectorcraft-*), apps, binaries, types, env vars, docs, examples (.vectorcraft); old .drawcraft files and preferences still load. Fix: text bounds exact after opening a file; selection no longer marks a document modified (dirty = content differs from the saved document). Screenshots and renders regenerated. 2026-10-01 14:30:09 -04:00
Cargo.toml Rename DrawCraft → VectorCraft: crates (vectorcraft-*), apps, binaries, types, env vars, docs, examples (.vectorcraft); old .drawcraft files and preferences still load. Fix: text bounds exact after opening a file; selection no longer marks a document modified (dirty = content differs from the saved document). Screenshots and renders regenerated. 2026-10-01 14:30:09 -04:00
README.md Rename DrawCraft → VectorCraft: crates (vectorcraft-*), apps, binaries, types, env vars, docs, examples (.vectorcraft); old .drawcraft files and preferences still load. Fix: text bounds exact after opening a file; selection no longer marks a document modified (dirty = content differs from the saved document). Screenshots and renders regenerated. 2026-10-01 14:30:09 -04:00

README.md

vectorcraft-pathops

Path operations for VectorCraft: booleans, the Pathfinder panel, Shape Builder regions, offset path, outline stroke, simplify and the other Object → Path commands. Everything takes and returns vectorcraft_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.