Files
vectorcraft/crates/pathops
Brandon ThomasandClaude Opus 5.5 3e323bfa2a Robustness: VectorCraft never crashes — no panics in shipped code
Shipped code no longer unwraps, expects or panics: every one of the 124
unwrap()/expect()/panic!/unreachable! sites outside tests now handles the
failure (Result/Option, ?, let-else, defaults) and reports errors to the user.

- Lints: workspace clippy denies unwrap_used, expect_used, panic, todo,
  unimplemented and unreachable; clippy.toml allows them in tests, and
  integration tests, examples and testkit opt out explicitly.
- Safety net (vectorcraft_engine::guard): a panic in a top-level command rolls
  the document back and returns EngineError::Internal; a panicking tool is
  reset; the MCP server answers with an internal error and keeps serving; the
  UI frame and each control request are guarded.
- APIs that hid failures now return them: Rendered::to_png/to_jpeg/to_webp
  return Result (JPEG/WebP errors used to write empty files),
  FontDb::face returns Option, Region::top returns Option.
- Fuzzing: engine/tests/import_fuzz.rs feeds garbage, hostile and mutated SVG
  and PDF through import → render → export (30k cases locally, no panics).
- Docs: AGENTS.md/CLAUDE.md rule, docs/development.md "Robustness" section,
  README pointer and a PR template checklist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 06:28:59 +07:00
..

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.