Native path geometry
Inkfinite stores arbitrary vector geometry as native path shapes. SVG import and vector
editing use this representation instead of keeping an SVG-specific document model.
Representation
A path shape has kind: "path". Its kind-specific properties store subpaths and fill_rule:
{
"subpaths": [
{
"segments": [
{ "type": "move", "to": { "x": 0, "y": 0 } },
{ "type": "line", "to": { "x": 40, "y": 0 } },
{
"type": "quadratic",
"control": { "x": 50, "y": 10 },
"to": { "x": 40, "y": 20 }
},
{
"type": "cubic",
"control_1": { "x": 40, "y": 30 },
"control_2": { "x": 0, "y": 30 },
"to": { "x": 0, "y": 20 }
}
],
"closed": true
}
],
"fill_rule": "nonzero"
} Coordinates belong to the path’s local coordinate system. The shape transform places that local geometry relative to its layer or parent container. Painting properties can live alongside the geometry in the shape’s properties.
Segments and subpaths
Every subpath has at least one segment and starts with exactly one move segment. The move gives
the subpath its starting point. Later segments continue from the previous destination:
linestores a destination point.quadraticstores one control point and a destination.cubicstores two control points and a destination.
A closed subpath connects its final destination back to its initial move point. Closure is a
property of the subpath, not a separate segment. Separate subpaths represent compound geometry
but do not use additional move segments inside one subpath.
Compound fills
fill_rule is either nonzero or evenodd, matching the SVG fill-rule values. The rule applies
to all subpaths in the path when a renderer determines which regions are inside the compound path.
Validation and bindings
Rust validates path properties before they enter a shape record. It rejects empty paths,
empty subpaths, subpaths without an initial move, later move segments, and non-finite coordinates. validate_shape_properties applies this check whenever the shape kind is path. Create and patch
operations reserialize valid path geometry through the Rust representation before committing it.
Freehand shapes keep their input points and brush settings as properties. Rust validates those
properties at the same transaction boundary, writes their canonical field representation, and
computes the committed outline for bounds and invalidated regions. TypeScript can use perfect-freehand for pointer previews and hit testing. It doesn’t decide whether a stroke enters
the document.
The binding generator exports PathFillRule, PathSegment, PathSubpath, and PathGeometry to @inkfinite/bindings. Its registry also exposes validatePathGeometry and applies the same
structural checks to serialized values.
Editing ownership
inkfinite-core owns deterministic operations that produce committed path geometry or hierarchy
changes. This includes splitting curves to add anchors, deleting anchors, converting segment types,
opening and closing subpaths, joining endpoints, and computing parent-relative transforms when
reparenting a shape without changing its world-space appearance. These operations are exposed to
interactive clients through generated bindings and WASM where needed. CLI, MCP, desktop, and web
edits therefore use the same implementations rather than replacing complete paths with geometry
constructed independently by each interface.
TypeScript owns direct-selection tool state, selected anchors and handles, pointer interpretation, overlays, and frame-by-frame gesture previews. A preview may mirror a Rust calculation to avoid a round trip during pointer movement, but it does not define committed geometry. Shared fixtures must compare preview results with the canonical Rust operation for curve splitting, topology changes, and nested transforms.
Direct selection uses Alt-click on a rendered segment to add an anchor. Delete removes selected anchors. Q and C convert their incoming segments to quadratic and cubic curves, L converts them to lines, O opens selected subpaths, Z closes them, and B breaks selected cubic handles. J joins two selected endpoints from separate open subpaths, or joins cubic handles for another selection. These commands remain editor interactions. The resulting topology operations are committed through the Rust reconciliation API.
A completed gesture submits one operation in one transaction through the Rust document session. Rust applies the operation, validates the resulting path and hierarchy, computes committed bounds and invalidated regions, and returns the updated editor projection. One completed gesture therefore produces one undo step.
Geometry and rendering
Path bounds use segment endpoints and the interior extrema of quadratic and cubic Bézier curves. A closed subpath contributes its implicit closing line. Shape transforms compose from the path to its parent and through the containing hierarchy.
The Canvas renderer draws move, line, quadratic, and cubic segments and applies the stored compound fill rule. Fill hit testing uses the same rule. Stroke hit testing follows the flattened curve segments with the stored stroke width and selection tolerance. Headless SVG output serializes normalized commands with fixed numeric formatting, fill rules, painting properties, and composed transforms.
Rust and TypeScript consume shared valid and invalid path fixtures. The fixtures cover curve bounds, compound geometry, nested transforms, and validation errors for malformed paths. SVG parsing and direct path editing build on these services.
Path metrics
inkfinite-core measures paths with adaptive Bézier flattening. A geometric tolerance controls
maximum flattening error; it does not select a fixed sample count. The metrics service provides
path length, point and tangent queries by distance, nearest-point queries with source segment
locations, and trimming that keeps line, quadratic, and cubic commands.
The browser editor mirrors these operations in packages/core/src/path-metrics.ts for pointer
interaction. It transforms path control points before measuring them, so hit testing uses world
geometry under rotation and non-uniform scale. Native paths and resolved arrows use the same
nearest-point service instead of separate curve sample counts.