Guides dragged out of the rulers could not be touched again: no tool hit-tested
them, so a click on a guide went to the art under it.
- The Selection, Direct Selection and Group Selection tools pick a ruler guide
within the selection tolerance (over the art; Direct Selection takes an
anchor on it first) through a shared tools::rulerguide::GuideEdit. A drag
moves the selected guides in one undo step (Alt copies them, Shift snaps the
dragged one to the ruler's ticks, otherwise it snaps to pixels, the grid or,
with Smart Guides, the art's edges, centres and anchors); dropped off the
canvas onto its ruler, the guide is deleted. Hidden or locked guides can't be
picked; the pointer shows which way a guide moves.
- Selection::guides holds the selected guides (on their own, like slices); the
canvas draws them in the selection colour. Delete/Backspace (edit.clear)
delete them, the arrow keys (object.nudge) nudge them, Lock Guides and Hide
Guides deselect them.
- Commands for agents: guide.list, guide.select {indexes, toggle?},
guide.move {dx?, dy?, copy?} (besides {index, pos}) and guide.remove {index?};
guide positions out of range are refused. Documented in docs/mcp.md.
- Shift while dragging a new guide out of a ruler snaps it to the ticks too
(Unit::ruler_step, shared with the rulers' drawing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fifteen Preferences options were stored but read by nothing. They now act,
through the existing plumbing (ToolContext for the tools, the prefs read by the
canvas for the view), and reach agents through prefs.set:
- General: Show Tool Tips (off: no widget's tool tip comes due), Double Click
To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer,
Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways), Use Precise
Cursors (Pen, Eyedropper, Slice and Blend pointers become a crosshair).
- Selection & Anchor Display: Tolerance (objects, anchors and handles, in
place of the fixed 3 and 4 px), Object Selection by Path Only (compound
paths too), Command Click to Select Objects Behind (cycles down the objects
under the pointer; not when Cmd only borrows the selection tool), Zoom to
Selection (Zoom In/Out centre the selection), anchor Size, Highlight anchors
on mouse over (Direct Selection).
- Type: Number of Recent Fonts (up to 15 Recent Fonts slots).
- Guides & Grid: guide and grid Color and Style (lines or dots), Grids In Back.
The canvas's context menu, panel drops and hover highlight pick objects like
the selection tools. The control channel gets ui.wheel (with modifiers held
like ui.click) so agents can turn the wheel. The rest of #394 stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`depth` / `childLimit` without `summary: true` used to return the whole
full-JSON subtree silently; it is now an invalid-params error. docs/mcp.md
describes the slice parameters and childCount, the engine test covers
fractional values, unsliced reads and limits past the tree, and the CLI
stdio sweep reads a sliced group through run_command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select All (Cmd/Ctrl+A) now follows the keyboard, as in Illustrator:
- macOS menu bar: the system menu takes Cmd+A/X/C/V before the window
sees them, so a focused panel, dialog or search field got the document
command. `menus::invoke_from_system_menu` (used by `NativeMenu::poll`)
hands Select All, Cut, Copy and Paste to a focused text field as the
events its keys would send; clicks on those menu items follow the same
rule. Other commands, and these with no field focused, run as before.
- Type tool: `select.all` itself selects all of the text being edited
while the Type tool edits (returns {editing, start, end}), so the menu,
the shortcut, the control channel and headless MCP (`press_key`) agree.
The UI's own Select All interception and the Type tool's hard-wired
Cmd+A are gone; Cmd+A reaches it through the Select All shortcut.
- Windows, Linux and the web already left Cmd+A to a focused field; now
covered by headless app-frame tests (field, Type tool, canvas).
Docs: docs/mcp.md (Select All while editing type), ROADMAP. Not yet:
Select All across threaded frames (one frame's text is selected).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live rectangle stored four corner radii but drew them all with the largest,
and the corner widgets always rounded all four corners, so one corner could not
be rounded on its own.
- geom: rectangle_with_corners draws each corner with its own radius and kind
(Round, Inverted Round, Chamfer); a sharp corner is one anchor, a cut one two.
Uniform corners keep the exact rounded-rectangle anchors.
- doc: LiveShape::Rectangle gains `kinds` (saved only when a corner isn't round;
older files read as round), plus helpers mapping anchors to corners.
- engine: object.setLiveShape takes `corners` and `kind`; without `corners` it
edits the Direct-Selected corners, else all four, and keeps the selected
corners selected as their anchors change.
- tools: with Direct Selection, only the selected corners show widgets and a
drag rounds just those; Alt-click cycles the corner kind; double-click opens
the new Corners dialog (also `ui.corners`).
- ui: Properties/Transform radius fields show and set the selected corners;
CSS export keeps per-corner radii and skips cut-in corners.
- docs: mcp.md (Live Corners), control-protocol.md (Corners dialog), ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object Illustrator writes as a fill of its outline left open and then a
stroke of it closed imported as two objects: a filled path and a separate
stroke-only path. A fill and a stroke of the same outline (equal once every
subpath is closed) now become one path with the fill and a live stroke
(width, cap, join, miter limit, dash, paint), taking the stroke's outline.
Layers: a group whose view state is off (/Usage /View /ViewState /OFF, with or
without an /AS view event) comes in as a hidden layer with its art, as groups
off in the default configuration already did. The configuration's /Order
nesting (inline or as its own object, labels skipped) makes sublayers, which
take their top-level layer's colour; nesting depth, entries and groups are
capped. Splitting a page's art by group is no longer quadratic, so thousands
of layers import quickly.
The 6D art pen (rotation-aware brushes) is a separate feature, now on the
ROADMAP gap list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A heartbeat alone can't tell a crashed tab from one whose timers stopped in
the background, so another tab could discard (or restore) the copies of a
still-live document once its heartbeat went stale.
- Recovery stores without locks can now announce an area for as long as the
app runs (RecoveryStore::announce/announced). The web shell implements it
with the browser's Web Locks (navigator.locks through js_sys::Reflect): a
tab holds a lock named after its area, which the browser releases only when
the tab is gone. Lock snapshots are refreshed every 10 s (once before the
first frame); a snapshot over a minute old counts every area as held.
- An area is gone only when no announcement holds it and its heartbeat is
stale. Where the store can't tell (no Web Locks), the owner repairs: each
heartbeat (also its first frame after a pause) and each recovery save write
again the copies missing from its area, and a heartbeat newer than the one
another tab wrote when taking the area over marks it running again.
- MemoryStore::announcing models Web Locks in tests; engine and UI tests cover
the issue's sequence. Native folder locks are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening a PDF whose fonts aren't installed keeps the text live in the fallback font (as
Illustrator substitutes and highlights missing fonts), and opening said so, but every export
then wrote the fallback font silently ("warnings": []), so a headless convert re-set the type
without a word.
- Exports that draw type now say so in their warnings, naming the missing families and the
fallback font: PDF (outlined or real text, also .ai and Save As), EPS, EMF/WMF, raster images
and SVG with outlined or embedded fonts. Live SVG text, DXF, text and native files name the
font itself and don't warn. Replacing the font (Find Font) clears it.
- The fonts a document uses (text.fonts' scope, Package's font list) come from one helper.
- textAs: outlines already keeps the glyphs the PDF draws (its embedded font program, installed
or not): now covered by a test with an embedded serif font that isn't installed, checked
against the installed font's and the fallback's outlines.
- docs/mcp.md and ROADMAP describe the warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PDF's DeviceCMYK image opened as RGB (the interpreter's pixels as a PNG),
so every export wrote it as DeviceRGB or ICC RGB, silently. Images now keep
their CMYK samples, as CMYK colours already did:
- Model: CMYK images are blobs holding CMYK samples, a four-component JPEG
(Adobe's inverted samples) or a Deflate CMYK TIFF. vectorcraft-doc's cmyk
module reads their ink amounts (ImageBlob::is_cmyk, ::cmyk, capped at 64
megapixels) and writes the TIFF (ImageBlob::cmyk_tiff).
- PDF import: DeviceCMYK, CalCMYK and four-component ICC images keep their
samples. A JPEG with Decode [1 0 1 0 1 0 1 0] is kept byte for byte; any
other is decoded (8 or 16 bits, its Decode array applied) and stored as a
CMYK TIFF. Masked CMYK images and 1/2/4-bit ones open in RGB with a warning.
Placing a CMYK TIFF keeps it CMYK too.
- PDF export: CMYK images are written in CMYK (DeviceCMYK, or ICC-based with
the CMYK profile when colours are tagged: PDF/X-3, PDF/X-4, PDF/A). A CMYK
JPEG neither resampled nor recompressed passes through unchanged; others are
resampled in CMYK and compressed as ZIP or as a CMYK JPEG (jpeg-encoder,
which also replaces the ZIP fallback and its warning for images converted to
CMYK). Output conversion treats them like CMYK colours: Preserve Numbers and
PDF/X-1a keep their numbers, Convert to Destination converts them to another
CMYK profile or to RGB through a 17^4 lookup table (cms::CmykLut, now also
behind the renderer's ink table). A CMYK image that can't be decoded is
reported in the warnings.
- Canvas: CMYK images show through the working CMYK profile, as CMYK fills
do, and paint their own inks in ink planes (CMYK raster exports, CMYK
blending). SVG export writes TIFF images as PNG.
Illustrator's Output › No Conversion keeps colour data as it is
(https://helpx.adobe.com/illustrator/using/pdf-options.html).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PDF-compatible .ai paints the editor's page (an opaque white rectangle the
size of the page) before its first optional-content layer and outside every
layer. It came in as art: an extra "Page 1" layer with a page-sized white path
behind the artwork, which hid white art and gave SVG/PNG/PDF exports an
opaque background.
For pages with the editor's private data (the way the importer already tells
a .ai from a PDF), the first art outside every layer, painted before any
layer, is dropped when it is a plain fill-only rectangle in an opaque,
non-spot white, with normal blending, equal to the crop box (0.5 pt
tolerance). Plain PDFs keep such a rectangle as art (as the reference app
does when opening a PDF), and so does anything else: grey, smaller or larger
rectangles, strokes, translucency, white inks, fills over the layers.
The white-cover test the soft-mask reader used is now a shared helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file's saved selections are untrusted: on load at most 25 are kept, names
are trimmed, capped at 255 characters and unique, and ids the document
doesn't have are dropped (after fix_next_id such an id would select whatever
object is drawn next). Save and Edit Selection cap names the same way. The
limit, the name cleanup and the default "Selection N" live on
SavedSelection, shared by the engine, the Save Selection dialog and the
Select menu's slot table. docs/mcp.md documents the select.save /
savedList / recall / editSaved commands; ROADMAP counts 13 stubbed menu items
and names what's still missing (Update Selection, the Layers panel menu).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
normalize_panel walks DockTab::ALL and ICON_PANELS instead of repeating the
dock tab ids. vectorcraft://object/{id} turns only a missing object into
-32602; no document or an unreachable app stay backend errors, as before.
docs/mcp.md documents document.node's summary flag and the panel names
open_panel takes; control-protocol.md the window.panel names. The ROADMAP
states the measured speed-up (a leaf read on 2,000 objects: ~20 ms to ~0.06 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grid was drawn whenever its `visible` flag was set or a perspective
tool was active, and choosing the Perspective Grid tool (Shift+P) didn't set
the flag. So with the tool chosen, View > Perspective Grid > Hide Grid
(perspective.grid.show) turned the hidden flag on instead of off and the
grid stayed on screen however it was toggled.
Choosing a perspective tool now shows the grid (sets `visible`, view state,
no undo step), and the grid, its widget and the Perspective Grid tool's
handles are drawn only while it is visible. Hide Grid hides it with the
tool still chosen; choosing the tool again shows it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Parity: Object › Path › Remove Anchor Points now runs the new command (`path.removeAnchors`,
the menu's feature name) instead of `path.deleteAnchors`, which deleted the anchors with their
segments and opened closed paths; that stays the Delete key's command ("Delete Anchor Points",
off the menu). No second "Smart" item.
- Geometry: the refit moves to `vectorcraft_pathops::remove_anchor` on the existing least-squares
fitter (fixed end tangents, a golden-section search for where the curve passes the removed
point) instead of a ~1,000-candidate grid search in geom (about 4 ms per anchor, now well under
0.1 ms). Two straight sides stay one straight side (a rectangle loses a corner and becomes a
triangle), as Illustrator does; non-finite input leaves the handles alone.
- The Delete Anchor Point tool (`path.removeAnchor`) uses the same function, replacing its
heuristic handle stretch; the explicit id/anchor and `fit` params of the new command are dropped
as duplicates of it. The command is enabled by direct-selected anchors (`has_anchors`), not a
UI override.
- The Control bar shows an icon button (the Delete Anchor Point icon, tooltip "Remove Anchor
Points") instead of a 204 pt text button; the labels reuse the translated "Remove Anchor
Points"; zh-hant gains "Delete Anchor Points". The Pen test that pinned Alt-click as plain Pen
(Illustrator's Alt over an anchor is Convert Anchor Point) is dropped.
- Tests for the refit, closed paths, straight sides and bad input; docs/mcp.md and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Illustrator's Alt-click on a layer's eye hides the other layers and, once they are
hidden, shows every layer again; the lock column does the same with Lock Others and
Unlock All Layers. Reuse the panel menu's commands (one undo step each, named as the
menu items) instead of toggling the row's siblings, so a sublayer's or an object's row
stands for its top-level layer. Document it in the ROADMAP and docs/mcp.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#249 made the reduced repros of #234 open, but a real file saved by the
reference app (version 30) still fell back to its screen-resolution preview
with "typecheck in `a dictionary`". Running its prolog to the end needed:
- The standard dictionaries (systemdict, userdict, globaldict, statusdict,
errordict, $error) as values in systemdict, so `/globaldict where
{ /globaldict get begin } if` gets a dictionary, not an operator.
SharedFontDirectory names the font directory too. errordict has a
handleerror for prologs that wrap it.
- `dictstack`, `internaldict`, `currentsystemparams` and
`currentuserparams` (with the cache sizes programs `get`), and `cshow`.
- Executable files: `cvx` and `cvlit` set a file's attribute, `xcheck`
reads it, and executing one runs its data as a program, as the prolog
does with `(...) 0 () /SubFileDecode filter cvx exec`.
- SubFileDecode with a count of 0 and an empty end string passes its
whole source through instead of nothing.
- `flushfile` on a filter over the program's data skips that data (the
file's XMP packet). A filter chained from a pending one leaves it
nothing to skip, so the writer's own `VCsrc flushfile` is unchanged.
The interpreter empties its standard dictionaries when it finishes, since
systemdict now holds itself. Both real files tested now import as 15
vector paths in CMYK with no warnings, and round-trip through SVG and PDF
pixel for pixel. The EPS fuzzer exercises the new operators.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a bounding-box handle of area type with the Selection tool now
resizes the type area: the text reflows at its own size (threads too)
instead of being scaled, and History says "Resize Type Area". With other
objects selected they scale as before while the area type resizes its
frame. Direct Selection picks the frame's corners and edges and reshapes
the area ("Reshape Type Area", new command text.reshapeArea), and the
selected frame's anchors and outline show. Area Type Options gains Width
and Height. Point type, rotation, Object > Transform > Scale, the Scale
tool and the Transform panel still transform the type.
Root cause: every handle drag previewed object.transform, which folds the
matrix into the text's xf (TextObject::transform), so area type scaled its
glyphs; and Direct Selection's anchor hit test only looked at paths, so the
frame's corners could not be picked. object.transform now takes
typeAreas, which reshapes the frame in text space (xf^-1 * m * xf), and
the tools use it and text.reshapeArea.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The export tool's description, the docs' open_file and export rows and the
CLI's run/convert usage listed only some formats. The CLI usage now prints
the writable extensions next to the readable ones, both generated from the
format table (export_extensions), the export tool points to its generated
format enum, and the docs name EPS, DXF, EMF, WMF, SVGZ and templates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paint.proxies gives swatch-linked paint as {type, color, swatch, tint};
document.inspect gives it as a label such as "G1 40% (#ff9999)". The tints
section and the inspect_document row now say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A type node's fill, stroke and strokeWidth now come from its first run, as
paint.proxies and its strokeOptions already did; fills and strokes of the
type object itself are reported apart as objectFill, objectStroke and
objectStrokeWidth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.exportSelection (the MCP export tool's `selection: true`) now
replies with `format` and the encoder's `warnings`, the same list a
whole-document export gives: lossy WebP written lossless, blend modes left
out of SVG Tiny 1.2, PDF layers at 1.4 and the rest. A selection export
that approximates nothing replies with an empty list.
Root cause: export_selection encoded with `encode`, which drops the
warnings encode_with_warnings returns, and its reply carried only path,
bytes and bounds. The params doc and docs/mcp.md now describe the reply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EPS files the reference app saves (its procsets in the prolog, a binary header with a TIFF
preview) now open as editable art instead of failing with "undefinedresource in Generic". When
their PostScript still can't be read, the palette preview they carry is placed with the usual
warning instead of the file being refused.
Root cause: five interpreter gaps and one preview format. The Generic and Category resource
categories didn't exist, so categories couldn't be made from Generic (now Category holds the
standard categories, defineresource in Category makes new ones, undefineresource is added);
clipsave/cliprestore were unknown (now a clip stack in the graphics state); systemdict's
languagelevel was the operator, not the integer 3; getinterval copied arrays and strings
instead of sharing their storage, so astore into a subarray was lost (arrays and strings are
now views into shared storage, and readstring/readline return the part they fill); and
resourceforall was unknown (now enumerates matching names with * ? \ templates). The preview
is an 8-bit palette TIFF with an alpha sample, which the image crate can't decode: a small
bounds-checked reader handles that layout (either byte order, any strips).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server answered tools/* and two fixed resources and nothing else, and
resources/templates/list returned an empty array. This adds the rest of the surface a
client can actually use, against 2025-06-18.
- prompts/list and prompts/get: five workflows (poster, icon-set, recolor,
trace-and-style, export-set) with templated arguments. A missing required argument
is -32602 and names itself.
- resources/templates/list, and resources/read for the templates: object/{id},
command/{id}, effect/{id}, swatch/{name}. Reading one thing at a time instead of
vectorcraft://document/json, which on a real document is thousands of lines an agent
pays for again on every change.
- completion/complete for prompt arguments and template variables, ranked prefix
first, capped at 100 with total and hasMore. The values are read live, so a suggested
format, effect id, command id, trace preset, swatch or object id is one the engine
has.
- logging/setLevel and notifications/message. Nothing installed a log::Log before, so
the engine's log::debug! calls went nowhere; this is the sink, silent until asked.
Installing it is vectorcraft-cli's job rather than the library's, so an embedder
that already has a logger keeps it.
docs/mcp.md gains a Protocol section with the capability table and a Not implemented
section: subscriptions, progress, cancellation, sampling, elicitation and Streamable
HTTP all need the transport to interleave reads, which this one does not — handle_line
answers a line with a reply, so it neither reads an incoming cancellation nor can write
progress before the reply it belongs to.
62 tests (50 unit, 12 property/fuzz). protocol_props.rs gained five: prompts survive
junk arguments, completion/complete never panics and always answers at most 100
string values with a consistent hasMore, every template reads or fails cleanly, and
prompts/completions/templates are swept with junk params while asserting the document
comes back byte-identical.
ROADMAP: one bullet under Automation in Shipped so far and the changelog line. The
parity table and the gap list are untouched — MCP is agent automation, so feature
parity does not move.
macOS keeps faces for its own interface under family names that start with
"." (".SF NS", ".LastResort", ".Aqua Kana"…); Mac apps don't list them. Since
the font menus list every installed family (#36), they showed 74 of them on a
stock macOS, at the top of every list.
- FontDb::menu_family_list: family_list without names that start with ".",
cached like it. Only a leading dot hides a family.
- Type › Font, the family dropdowns (Character, Properties), text.fontList and
Refresh Font List's count use it.
- Lookups by name still see every family: documents and fallbacks that name
a hidden family still find it (face, has_family, find_family, families,
text.fontList {family}, Fonts in Document's missing check).
- Test: tests_sysfonts::font_lists_leave_out_hidden_system_families_which_still_resolve
(a temporary font folder); the engine and UI font-list tests compare with
the listed families.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing an Effect > Pathfinder item (Add, Intersect, Exclude, Subtract...)
with several loose objects selected now groups them and applies the live
effect to the group, in one undo step, so the result shows at once and stays
editable. A selection that already holds a group, or a single object, works
as before, and agents get `grouped: true` back when it happens.
Root cause: Pathfinder effects combine the contents of a group or layer.
Applied to loose objects, each one got an effect with nothing to combine, so
nothing visibly changed, while the Pathfinder panel's commands (found by the
command search) act on the selection directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Edit Contents: while an envelope's contents are edited, clicks hit the
content where it sits undistorted and the Selection tool selects it; the
menu item reads Edit Envelope then. No default shortcut: the reference
app's (Shift+Cmd+V) is Paste in Place here.
- A selected envelope shows its mesh (a warp's surface grid, a mesh's own,
a top object's outline) instead of its content's outlines.
- Mesh envelope points get bezier handles (append-only; none stored is the
smooth Catmull-Rom mesh, which the first handle edit fills in exactly
along the grid lines). setMeshPoint takes `handle`; object.mesh.movePoint,
addLine and deletePoint edit mesh envelopes too. The Mesh tool and Direct
Selection drag points and the clicked point's handles (shared MeshEdit);
the Mesh tool adds rows and columns inside mesh envelopes. Reset with
Mesh keeps the shape along the grid with handles; Release returns the
exact mesh.
- Control bar: Edit Envelope / Edit Contents, warp style, orientation,
bend and distortions or mesh rows and columns, Reset, Envelope Options.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Perspective Grid tool's left and right ground-level points move the whole
grid; the left and right extents are separate (extentRight; Alt drags both,
Shift goes by whole cells); a cell size widget sits up the line where the planes
meet; Shift keeps a vanishing point on the horizon and the ground points and
origin on one axis. The Plane Switching Widget stays put in a corner of the
document window (ViewInfo/ToolContext carry the window as a ScreenFrame), takes
its clicks with any tool while the grid shows (the Perspective Selection tool
too while it's hidden), and 1-4 pick the left, horizontal, right and no plane.
Double-clicking the Perspective Grid tool opens Perspective Grid Options
(show the widget, its corner; perspective.widget.options, kept with the
preferences).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Type and symbol instances attached to a plane keep their art and carry a projection
(Node.perspective.projection, append-only); the canvas and every export draw their outlines
through it (reshape/bake), so they are foreshortened and stay type and symbol instances. Bounds,
hit testing and transforms follow the projection. Object › Perspective › Edit Text
(perspective.editText, or double-clicking with the Perspective Selection tool) shows the type flat
where it is drawn in isolation mode; exiting isolation projects it again. Release with
Perspective keeps their look. Copies, pastes, duplicates and Selection-tool moves keep the
attachment (the art moves as dragged). Flare, spiral and polar grid draw on the active plane, and
so do the click-to-size shape dialogs (perspective.draw `at`: sizes in plane units).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Images distort as a raster mesh warp: cut by Fidelity into triangles,
each a clipped image under the affine map of its corners (pieces overlap
slightly, no seams; the renderer paints them without clip layers; SVG
writes the pixels once and <use>s them).
- Symbol instances distort as their symbol's art (the renderer's cache
follows edits to the symbol).
- Distort Appearance bakes strokes (filled outlines) and geometry effects
before bending; Distort Linear Gradients bends linear gradients; Distort
Pattern Fills bends the tiles. Opacity masks of the content bend too.
- Bug fix: a rotated, sheared or reflected warp envelope re-warped along
the page axes. Envelopes now keep their own frame (append-only model
field), so the warp turns with the art; move and scale keep it square.
- Live evaluation shares one set of hooks (vectorcraft_doc::live::Hooks,
vectorcraft_effects::{expand_live, expanded_live_group,
expand_live_deep}) between the canvas, Expand and every exporter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closed shapes start where they twist least (unless the Blend tool
picked the anchors); dashes, miter limit, arrowhead scale and width
profiles interpolate (caps, joins and brushes switch halfway);
gradients with different stop counts resample to common stops; groups
with different member counts grow the extra members out of a point;
compound steps stay compound paths; type, symbol instances and images
interpolate their transforms, type its colours and sizes run by run.
New blends are knockout groups and the canvas draws them knocked out
as the exports do. Expand and Release keep the blend's name,
transparency, opacity mask and appearance, and Object > Expand expands
blends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A compound path's stroke lives on the compound: the tool edits it through
any member, and stroke.widthPoint.* / stroke.widthProfile.set given a member
edit the compound's profile.
- Width points show on every subpath the stroke runs along (not just the
first) for the hovered and the selected stroke.
- Cursors: the Width tool's own glyphs off a stroke, over a stroke (a drag
adds a point) and over a width point or handle (a drag moves or widens it).
- Hint-bar text for the tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holding the brush still keeps Twirl, Pucker and Bloat applying, scaled by the
time held: a new Tool::tick (time supplied by the host: canvas frames, or
`holdMs` on ui.pointer / pointer_gesture events) repeats the last point every
0.1 s, one more dab there. Liquify never touches graphs, meshes, type, symbols,
images, or envelopes, repeats and blends with their contents, nor guides in
selection mode; those under the brush come back in `skipped` with a `warning`
that the status bar shows (UiRequest::Status). A drag applies only the dabs
each new sample adds (the stroke's state is kept between previews), giving the
same result as the whole stroke, so journal replay still reproduces it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
View > Perspective Grid gains Show/Hide Rulers, Snap to Grid (on by default),
Lock/Unlock Grid and Lock Station Point, with flipping labels and checkmarks (and
Show/Hide Grid's label). Lock Grid stops the grid widgets from dragging; Lock
Station Point swings the other vanishing point around the station point; Snap to
Grid lands drawn corners and moved edges on gridlines within a quarter cell
(perspective.draw/move take snap?); Show Rulers draws a ruler up the true height
line in the grid's units. Gridlines draw in the Define Grid colours and opacity.
New commands perspective.grid.lock/lockStation/snap/rulers (view state).
Existing tests that measure exact perspective moves (two in tests_distort.rs,
the persp_session helper of tests_persp_select.rs) turn snapping off first (it is on by
default and would move their exact corners onto gridlines).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grid keeps per-plane offsets (leftOffset, rightOffset, groundOffset). Each plane has a widget
(a diamond in its colour) that the Perspective Grid and Perspective Selection tools drag: plain
moves the plane, Shift moves the objects on it too, Alt copies them (perspective.plane.move).
Double-clicking a widget, or ui.perspectivePlane, opens the plane's options dialog (location; Do
Not Move / Move All Objects / Copy All Objects). Object › Perspective › Move Plane to Match Object
(perspective.plane.matchObject). New objects attach where the plane is. Grid edits leave
attached art in place as the reference app does; perspective.grid.set {reproject: true} moves it
with the grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pins show as soon as the tool has art selected: automatic ones at the
centre and the middle of each limb's end (at least three), until pins are
placed. They stay across tool switches while the selection is unchanged and
a new selection starts afresh.
- The pins live in the document (Document.puppet, never saved) with the rest
shape they started from: every warp starts from it, so warps don't stack and
dragging a pin back restores the original; Undo/Redo take the pins back with
the art. Adding and deleting pins are undo steps too.
- object.puppetWarp gains rest: true (the tool's pins); the new query
object.puppetWarp.pins reads them. A click off the mesh adds nothing and
says so (not-allowed cursor, canvas label); the command refuses such pins.
- The mesh grid covers the expanded art, so tiny art with a wide Expand no
longer allocates millions of cells.
- Tests on groups, compound paths and open strokes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A selected blend shows its spine. Direct Selection drags spine points
and handles, the Pen tool adds and deletes points on a selected
blend's spine (object.blend.spine.moveAnchor/addAnchor/removeAnchor).
The straight spine becomes a path on the first edit, its points pinned
to the key objects (append-only BlendSpec.key_anchors): moving a key
moves its end of the spine and the other way round; spines from older
files and Replace Spine still spread the keys evenly. Direct and Group
Selection pick blend keys on the canvas, edits redraw the blend live,
and Release leaves the spine as a path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blend Options is a dedicated dialog (Spacing dropdown with its value,
Align to Page / Align to Path buttons, live preview) opened on the
selected blend's own options, so OK no longer turns Smooth Color into
5 steps; with nothing selected it sets what new blends start with
(object.blend.info reads them). The Blend tool blends from clicked
anchor points (blend spec `starts`), adds keys to an existing blend
instead of nesting it, opens Blend Options on double-click, Alt-click
and its toolbar button, and has its own cursors and hint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Perspective Selection tool shows the selection's bounding box in perspective; its handles
scale in plane space (perspective.transform; Shift proportional, Alt from the centre). Alt-drag
moves copies that stay attached, pressing 5 while dragging moves along the plane's normal
(ToolKey::Digit, delivered by the canvas and MCP press_key), and the arrow keys nudge in
perspective (perspective.nudge). Transform Again repeats the last perspective move or scale in
plane space. Attachments (plane and depth) now live on the objects (Node.perspective), so copies,
duplicates and pastes keep them; grids from older files hand theirs over. Homography moves to
vectorcraft-geom. Hint-bar text for the tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
View > Perspective Grid > One/Two/Three Point Perspective list the built-in views
([1P/2P/3P-Normal View], plus Low and High views generated in code) and the saved
presets of each type; Save Grid as Preset... and Define Grid's Preset menu and Save
Preset... save the fields. Edit > Perspective Grid Presets... (previously a
mislabelled reset to the two-point grid) manages them: New, Edit, Delete, Import,
Export (.vcperspective), stored with the preferences. New commands
perspective.presets.list/save/delete/export/import; perspective.grid.preset takes
a preset name. Preset import is fuzzed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicking a Liquify tool opens its Tool Options (dialog liquifyOptions):
the shared Global Brush Dimensions (Width, Height, Angle, Intensity, Use
Pressure Pen), the tool's options (Detail, Simplify with its checkbox, Twirl
Rate, Complexity, Brush Affects, Horizontal/Vertical) and Show Brush Size.
The canvas captures pen pressure from pen/touch force, ui.pointer and MCP
pointer events take `pressure`, and object.liquify takes [x, y, pressure].
Fixes: LiquifyParams::to_json now sends affectAnchors/In/Out; Alt-drag sizes
the brush from its current size (Shift keeps proportions) instead of
collapsing it. Hint-bar text for the seven tools. Registers the MCP Liquify
tests file added in M8.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Alt-dragging near (not on) a selected pin turns the art around it; the pin
keeps that turn through later warps. arap::Pin gains an angle constraint and
object.puppetWarp takes angles: [deg|null...].
- Shift-click adds pins to the selection or takes them out; dragging a pin
moves every selected pin. Select All Pins selects them all (it used to
deselect). Alt-click no longer deletes a pin; Delete removes the selection.
- Control bar: Expand (0 allowed), Show Mesh (on by default), Select All
Pins. Hint-bar text for the tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every exporter (SVG, PDF, EPS, EMF/WMF, DXF) evaluated live objects with no
text outliner, so type inside an envelope was written undistorted. They now
share vectorcraft_effects::expand_live_deep, which outlines type per run in
each run's paint, as the canvas now does too (it used the first run's paint
for the whole line).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Make with Warp and Make with Mesh open dedicated dialogs (style dropdown,
orientation, bend and distortion sliders; rows and columns) with live
preview; while an envelope is selected the menu shows Reset with Warp /
Reset with Mesh instead, which switch the envelope's kind and keep its
content (Maintain Envelope Shape samples the current surface). Envelope
Options is a full dialog prefilled from the selection (Anti-Alias,
Preserve Shape, Fidelity, Distort Appearance, Distort Linear Gradients,
Distort Pattern Fills); with nothing selected it sets the defaults for new
envelopes. object.envelope.info reads the settings. Release returns the
warp or mesh shape as a gradient mesh and keeps opacity, blend mode and
mask; Expand keeps name, knockout, isolation and mask; Object > Expand
expands envelopes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
View > Perspective Grid > Define Grid... opens a dedicated `perspectiveGrid` dialog
(ui.perspectiveGridDialog) prefilled from the current grid instead of a generic
form with fixed defaults that forced kind 2. The grid model gains (append-only)
name, viewing angle (the station point), units, scale, plane colours and opacity;
grids with a viewing angle foreshorten each axis as a true camera. New commands:
perspective.grid.get (read-only) and perspective.grid.define (one undo step,
only changed fields apply).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Width and Puppet Warp tools now claim Delete and Backspace while they
have width points or pins selected, so the shortcut routing (desktop and MCP
press_key) removes those instead of clearing the artwork. With nothing of the
tool's selected the keys still clear the selected objects. Headless Backspace
clears the selection like the desktop app does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each tool keeps its options (a table in vectorcraft_tools::settings) when another
tool is chosen, across document switches, and in the saved preferences
(Prefs::tool_settings). The 7 Liquify tools share one set of Global Brush
Dimensions; the Symbolism tools share their brush. tool.setOption takes
{values} and a `tool` (another tool's options are stored for later), returns
the options, and works headless (MCP host command).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>