Paths and compound paths now measure their visual bounds along their shape:
the body (inside/outside alignment, the width profile's widest point), the
tips of the miter spikes the limit keeps, projecting caps' corners and the
arrowheads' reach. A rectangle's right-angle miters stay within half the
weight (Fit to Selected Art, Rasterize and all-art SVG export get tighter,
exact boxes); Appearance::outset is the conservative reach for anything else.
Clicks hit what the strokes paint: outside strokes outside the path (0.8 x the
weight out), inside ones inside, the profile's width where clicked, caps,
spikes and heads. Click culling keeps a cheap box (hit test 0.34 ms per click
on the 50k-path budget). The subpath/tangent helpers move from the stroke
geometry into vectorcraft_geom::bez so both share them; a cross-check test
holds every painted point of the real stroke geometry inside the bounds and
hittable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Freeform gradients carry their own points (colour, opacity, spread) and lines
(Catmull-Rom curves through points), blended by inverse-distance weighting
from each disc of pure colour. paint.editGradient {kind: "freeform", mode?}
places 4+ points inside the shape, coloured along the stops (which follow the
points' colours). The renderer samples the field on a cached colour grid
drawn as an image paint clipped by the fill or stroke. Moves, transforms,
box rebases and warps move the points; old files load unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A group's or layer's own fills and strokes paint every member's geometry, below or above the
members as the new contents slot (Appearance.contents_index) says; its geometry effects reshape
the members as one piece and its raster effects apply to the composite (one combined shadow),
respecting knockout groups. Type's own fills and strokes paint under or over its characters.
Render, SVG/PDF (via bake) and Expand Appearance evaluate them alike.
Commands: appearance.* / effect.* take target: "object"|"contents" and layer ids;
appearance.moveItem moves the Contents slot (from: "contents") or an item across it
(contents); appearance.targetContents selects a group's members. Graphic styles applied to a
group now style the group itself. The Appearance panel shows Contents/Characters rows that
drag, and double-click to target the members or edit the characters' paint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text import turns the gap between an absolutely positioned <tspan y> and the previous line into
that many line breaks, with two problems:
- At least one break was added even when the tspan sits on the current baseline. Styled or
kerned runs written with x and y on every tspan (as Illustrator writes them) were split into one
line per run: "Hello <tspan y=same>world</tspan>" came back as "Hello \nworld".
- The count had no upper bound. A tspan with y="1e15" (or any y far below its text) asked for
~10^14 '\n' characters, and opening the file aborted the app, the MCP server or
`vectorcraft-cli` with "memory allocation failed" after eating all memory.
A tspan less than half a line from the current baseline now adds no break, the gap is capped at
10 000 line breaks, and a non-finite gap counts as one; other gaps are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VectorCraft writes and reads SVG at 1 user unit (CSS px) = 1 pt, but a root width/height in
absolute units was taken in CSS pixels (96 per inch). An A4 document from Inkscape
(width="210mm" height="297mm" viewBox="0 0 210 297") opened on a 793.7 × 1122.5 pt artboard,
i.e. 280 × 396 mm, with all art 4/3 too large, and its PDF export came out on that page size;
width="8.5in" gave an 11.3 inch artboard and width="300pt" a 400 pt one.
When the root width or height has an absolute unit (in, cm, mm, pt, pc) the import now scales by
72/96 along that axis, so the artboard and the art keep their physical size (595.3 × 841.9 pt for
A4); geometry, stroke widths and text follow through the same transform. Unitless, px, % and
viewBox-only files are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Area type was written as one <tspan> per paragraph at the frame's top-left: SVG text never wraps,
so every SVG viewer (and VectorCraft's own import) showed each paragraph as a single line running
out of the frame. Centred and right-aligned area type also got text-anchor="middle"/"end" at the
frame's left edge, so it was centred on (or ended at) that edge instead of inside the frame.
The exporter now asks the text layout for the lines: each line is a <tspan x y> at its baseline,
at the line's left edge, centre or right edge to match the text-anchor, with hyphenated breaks
written with their hyphen and the spaces at soft breaks dropped. The <text> carries the first
line's position too, so importers that place text by its own x/y (ours included) start at the
first line. Re-importing an exported file now reproduces the area type pixel for pixel as point
type with the same line breaks (justified lines lose their extra word spacing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VectorCraft exports type on a path as <text><textPath href="#text-path-N"
startOffset="…%"> with the path in <defs>, but the XML text fallback read
every <text> as point type at the text element's x/y. Text on a path
therefore re-imported as a straight line at (0, 0), above the artboard
and mostly out of view, and lost its path.
Resolve the textPath's href (SVG 2 href or xlink:href) to the <path>,
bake the text's user-space transform (and the path's own transform) into
it, and read startOffset (a percentage, or a length along the path) into
TextKind::OnPath.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>