Dragging a direction handle with Direct Selection or the Anchor Point tool
sent the pointer position straight to path.setHandle: Shift did nothing and
the handle never snapped, while the Pen already constrains the handles it
pulls out.
guides::snap_handle places a dragged handle: with Shift at a multiple of
45° from its anchor (the Pen's constraint, keeping the dragged length),
otherwise snapped as a drawn point is (smart guides, the grid, pixels).
Both tools use it and show the smart guides while the handle moves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
artboard.move with copy and artboard.duplicate now make the copy with one helper: a
new id that can't overflow and a name of its own (`<name> copy`, then `<name> copy 2`…,
so two copies of one artboard no longer share a name). `moved` lists the copies a copy
moved rather than the originals it left. Test it with and without moveArt, and note
Alt-drag in the ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alt-dragging an artboard moved it like a plain drag. artboard.move now
takes copy: it leaves the artboard (and the art it carries) where it
is and moves a copy, named "<name> copy", with copies of that art. The
Artboard tool previews it while Alt is held, as "Duplicate Artboard"
(Alt pressed or released mid-drag switches between copy and move),
and makes the copy the active artboard, as in the reference app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine kept its own copy of Document::next_layer_name after #353; the
document's now counts sublayers too, so the engine uses it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new sublayer was named "Layer {node_count}", counting every object, so
an empty document's first sublayer was "Layer 1" like its parent and a
document with five objects got "Layer 6". Top-level layers counted only
top-level names and could repeat a sublayer's. Document::next_layer_name
now counts every layer, sublayers included, and skips any layer's name;
layer.newSublayer uses it too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Between paths, the Pen now adds an anchor where a click lands on a
segment of a selected path and deletes the anchor a click lands on,
with the add and delete cursors while hovering, as in the reference
app. The ends of a selected open path still continue it. Shift held,
or General > Disable Auto Add/Delete (a stored preference nothing read
until now, passed to tools as ToolContext::auto_add_delete), starts a
new path instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Art in a sublayer is now selected as objects (clicks, marquee, Select All, the
Select menu), sublayers select in their own colour, and hidden, locked or
template sublayers block clicks. Layer Options gain Preview (per-layer outline)
and Dim Images. New and fixed layer.* commands for the Layers panel: setCurrent
and highlight (rows at any depth), selectAll, move (reparent with cycle and
nesting checks, Alt copies), delete/duplicate rows, merge, flatten, collect,
release to layers, reverse, template, hide/outline/lock others and their
show/preview/unlock all, locate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On hybrid-graphics laptops (an integrated GPU driving the display plus a
discrete one), egui-wgpu's default power preference is HighPerformance, so
VectorCraft rendered on the discrete GPU and its frames were copied to the
display through the integrated one. On an Asus ZenBook with Intel Iris + Arc
the window flickered whenever the mouse moved over it (#306, #305).
The canvas is rasterized on the CPU (vello_cpu) and the GPU only composites
the canvas texture and draws the UI, so the integrated GPU is plenty. The
desktop app now asks wgpu for the power-saving adapter by default.
Single-GPU machines are unaffected.
- Preferences > Performance > Graphics Processor (`gpuPreference`:
powerSaving, the default, or highPerformance), with a note that it
applies after a restart. The app reads it from the saved preferences
before the window opens. WGPU_POWER_PREF still overrides it.
- Help > About and `ui.inspect` (`graphicsAdapter`) name the adapter in
use, and it is logged at startup, for future GPU bug reports.
- Docs (README, docs/development.md) and ROADMAP updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live rectangle kept every transform in its `xf`, so a non-uniform scale
(a bounding-box drag, Object > Transform > Scale) stretched the rounded
corners built in untransformed space into elliptical arcs. Scale Corners
off divided the radii by the mean scale, which only compensated uniform
scales.
LiveShape::transform now folds a scale along the rectangle's own sides
(any rotation or reflection, no shear) into w/h and keeps only rotation,
reflection and position in `xf`, so the radii are document lengths and the
path is regenerated with circular corners. Radii scale by the mean scale
(sqrt|det|), which keep_corners undoes for Scale Corners off. Shears and
plain moves keep composing into `xf` as before, so files saved with a
scale in `xf` keep their exact geometry until next scaled or rotated.
A corner-widget drag starts from the radius as drawn (clamped to fit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A regression test for non-ASCII paths. It saves to a Japanese file name, with
full-width brackets and a middle dot, inside a Japanese folder. It checks the
document's title, reopens the file with its Japanese text, and exports SVG, PNG
and PDF to Japanese names.
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 tool chose scale, distort, perspective or shear from the modifiers at
the press only, but a Cmd press on the canvas is the temporary Selection
tool, and the documented gesture is to start dragging and then hold Cmd.
A handle drag now follows the modifiers held during the drag; when the
operation changes, the interaction restarts so the undo step is named
after what was done.
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>
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>
The Type tool passed `y + 12` to `text.create` for a dragged frame, and
`text.create` puts the frame's top-left at (x, y), so every frame landed
12 pt below the marquee the user dragged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Strings in code stay English and are the lookup keys; `crates/ui-egui/src/i18n`
maps them to display text at render time from one `*.tsv` catalog per language
(`LANGUAGES` registry, `tl!`/`t`/`tr`/`tr_ctx`/`tr_id`/`tn`/`fmt`). Command ids,
menu paths, `ui.menu.list`, the control channel, the CLI, MCP, status messages
and undo labels never see translated text, so agents, scripts and the existing
tests are unaffected (under `cargo test` the UI is always English).
Wired: in-window menus and the macOS native menu (rebuilt on a language change),
dynamic labels (Undo X, Reset X, Last Effect: X as templates), the command
palette (matches English and translated text), dock tabs, panel tooltips, the
toolbar (category names now cut by characters, not bytes), the dialog frame, the
shared widgets, the Preferences dialog, the shortcut editor, the hint and status
bars, the Home screen, and the literals of every panel and dialog.
Preference `interfaceLanguage` (User Interface › Language): `auto` or a code.
`auto` follows `VECTORCRAFT_LOCALE`, `LC_ALL`/`LC_MESSAGES`/`LANG`/`LANGUAGE`,
the macOS preferred languages and, on Windows (which sets no `LANG`), the user
locale from `HKCU\Control Panel\International`. `zh-TW`/`zh-HK`/`zh-MO`/
`zh-Hant-*` resolve to `zh-hant`; Simplified locales stay English until a
`zh-hans` catalog exists.
`zh-hant.tsv` (~2,850 rows) is clean-room, in the vocabulary used in Taiwan;
terms shared with PhotoCraft reuse its wording, vector terms use everyday
vector-drawing vocabulary. Tests: catalog well-formedness (placeholders,
ellipses, command ids, no Simplified characters), locale resolution, and two
coverage tests that fail when a menu string, panel/tool/preference label,
dialog button, hint-bar fragment or `tl!` literal has no row in a language
marked `complete_menus`. `VECTORCRAFT_I18N_DUMP=file cargo test -p
vectorcraft-ui-egui dump_source_strings` lists the strings for translators.
Also: two clippy findings under rustc 1.97 fixed in passing.
Integrated with main (squashed onto it, 42 commits on): the Czech interface
(#213) moves from its Rust table to `i18n/cs.tsv` on the registry (all 556
rows, Czech plural rule, its menu-coverage and glyph tests kept); Find Font's
resolution notes and the canvas context menu's labels are translated and
covered by the catalog tests (`menus::CONTEXT_LABELS`); the glyph test checks
CJK text only when the craft-fonts build input is embedded (#220 moved the
Japanese fonts out of this repository); a language saved by an older version
carries over to `interfaceLanguage`; Windows' locale lookup runs reg.exe by its
full path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
list_says_how_each_font_resolved expected Source Sans 3 Black to be a
substitute, which holds only while the bundled family is the only Source
Sans 3: on a machine with the full family installed (it ships with many
font packs), Black resolves exactly and the test failed. It now asks for
a style no Source Sans 3 has, so the substitute path is tested anywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts with #214 (fonts by PostScript name): this branch's name index (FaceKeys,
aliases) stays, and #214's `by_postscript_name` (used by PDF import) reads a PostScript
map filled from it, keyed by the same normalized names.
Removed: scanning ~/Library/Application Support/Adobe (CoreSync/plugins/livetype and
Fonts), and the preference that switched it (Type › Include Fonts of Other Design Apps'
Folders). Fonts activated through another vendor's subscription are licensed for use
through that vendor's own sync; reading their files from VectorCraft would go around that
licence, and the project's clean-room rules keep VectorCraft out of other vendors' files.
Fonts the user has installed in the system or user font folders are still found by every
name, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The Live Paint Bucket now fills areas enclosed by any paths. Crossing
lines enclose faces it can fill, a line across a shape splits that shape's
face in two, and an area enclosed by paths becomes a face even when nothing
fills it (it starts unfilled). Edges are the paths cut wherever another path
meets them, keeping their stroke, so Shift-click paints each piece. Closed
shapes give the same faces as before.
Root cause: Live Paint built its faces from the Pathfinder regions, which
divide the filled areas of closed shapes. An open path counted only as the
area it would fill if closed, which for a straight line is nothing, so lines
divided nothing and the bucket found no face to fill. A new planar map in
vectorcraft-pathops (live_paint) sweeps every path as an edge: an open path
goes in traced there and back, with crossing counts that never cancel; faces
are walked on the resulting graph (holes included), and their fills and the
edges' sources are found geometrically. The sweep runs in an exactly sheared
frame, because it mishandles a horizontal line traced there and back.
Make, Merge, the bucket and Live Paint Selection all use it;
interior_point moves to vectorcraft-pathops with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds on the vertical type from #145 (same data, commands and tools).
- Kinsoku (the strict set, horizontal Japanese too): no line starts with a
closing bracket, 、。, a middle dot, colon, !?, the long vowel mark,
iteration marks or small kana; no line ends with an opening bracket.
Hyphenation breaks obey it too.
- Tate-chu-yoko: a run of two or three half-width digits (10月, 第100回)
stands upright across the column as one block one em long, squeezed to
the em when wider; longer numbers stay on their side.
- Upright or on its side by Unicode's Vertical_Orientation: CJK and
symbols upright, marks that need a vertical form on their side when the
font has none; upright glyphs advance at least one em, centred, and turn
about the ideographic em box centre so they sit on the column's centre
line. Point type keeps its anchor on the first column's centre line.
- `vrt2` is no longer requested: its glyphs are already on their side, so
with the layout turning Latin and digits too they came out upside down
(and tate-chu-yoko blocks broke) in fonts that have it, e.g. Hiragino.
- The line→text mapping is one affine (`TextLayout::line_xf`, an exact
quarter turn plus the point-type offset); `logical_point` and
`physical_point` use it. Pixel snapping skips vertical type.
- Tests: kinsoku in both directions, tate-chu-yoko blocks, upright vs
sideways, anchor on the centre line, arrows along columns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documents made elsewhere often name a font differently from the family
name VectorCraft lists: a localized family name (ヒラギノ角ゴシック), a
legacy family/style pair (Hiragino Sans + W3) or a PostScript name
(HiraginoSans-W3). FontDb now indexes every name in a face's name table
and resolves a request to the right face, telling the caller whether it
matched exactly, by another name, by substitution or not at all
(FontMatch).
- text: FaceKeys/aliases, FontDb::resolve, canonical, find_postscript;
fix a race where a face loaded by another thread was reported missing.
- engine: text.fonts reports status, the resolved face and missing
glyphs; Replace Font resolves the same way.
- render: substituted-font highlighting uses resolve, so a font found
by another name is no longer shown as missing.
- Find Font notes how each font was found.
- Preferences › Type › Include Fonts of Other Design Apps' Folders
(macOS, on by default).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typing with an input method (the macOS Japanese IME and the like) used to
insert the raw keys (romaji came in as ASCII): egui's Ime events weren't
handled.
- Tools gain `ime_preedit` / `composing` / `ime_caret`; the Type tool lays
the marked text out in place inside its typing session, underlined (the
clause being converted thicker), and commits it through `text_input`.
Character ranges from the IME convert to byte ranges in one place. A
cancelled composition leaves no empty undo step; an empty clear with
nothing marked never deletes the selection.
- The UI sends Text and Ime events to the tool in order, sends IMEOutput
every frame while text is edited (what lets egui-winit enable the IME
and place its candidate window at the caret, or the converting clause),
and holds back keys, Enter/Escape, clipboard, single-key shortcuts and
menu commands (Undo, Cut, Select All… also from the native menu) while
composing. A click, a tool switch or a style change ends the
composition with the marked text kept as typed; on macOS the IME is
told to discard it (NSTextInputContext.discardMarkedText) so it doesn't
come back in the next input.
- Undo during a typing session also ends the Type tool's session, so its
caret and marked text follow the document.
- Tests: tools (char → byte ranges, underlines and candidate position),
engine (compose → commit is one step, cancel leaves no step, a click
commits, Undo mid-composition is safe), ui-egui (synthetic Ime events:
compose, commit, ⌘Z held back, IME output, click and app switch).
Checked by hand on macOS 27 with the Japanese IME (romaji input, convert,
commit, cancel, Undo/Redo after commit, click away, Latin mode, app
switch). Not checked: Windows and Linux IMEs, the web build.
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>
enveloped_type_exports_distorted_in_every_format compared two exports
byte for byte, and EPS dates its file to the second (%%CreationDate), so
it failed whenever the clock ticked between them. The comparison now drops
the CreationDate/ModDate lines and still checks repeatability and that the
bend shows in every format.
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>