Color, Alignment Guides, Anchor/Path Labels, Measurement Labels,
Transform Tools, Object Highlighting and Snapping Tolerance are read:
- tools: `guides::GuideStyle` on `Targets` (`styled(cx)`) colours the
lines and labels and leaves them out when their option is off, the
snap itself unchanged; `ToolContext` gains the six fields and
`snap_tol()`, replacing the 5 px literal for every smart guide snap
(drawn points, moves, bounding-box handles, ruler guides, artboards,
reference points). The tools gate their `Overlay::Measure` readouts on
Measurement Labels (drawing, moving, ruler guides, artboards) or
Transform Tools (scaling, rotating, Free Transform, transform tools).
- engine: `with_tool_cx` fills them from `Prefs` (the colour parsed, magenta
as fallback); the colour's default is `#ff3dfc`, what the tools drew.
- ui-egui: the hover outline needs Object Highlighting and Smart Guides.
- Tests in tools (guides, select), engine (tests_prefs) and the canvas;
docs/mcp.md and ROADMAP.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Guides dragged out of a ruler did not snap at all (only Shift to the ruler's
ticks): they now go through the engine (`Session::ruler_guide`, a
`NewGuide` previewing `guide.add`, one undo step) and snap as a moved guide
does: with Smart Guides to the edges, centres (so the sides' midpoints) and
anchors of the art and of the artboards, labelled; with Smart Guides off,
View > Snap to Point pulls new and moved guides onto anchors. Moving art
with the Selection tool already snapped flush to artboard edges and centres
with Smart Guides on; a test now covers it.
Artboard guides: `Guide.artboard` holds an artboard id (written only when
set, so canvas guides save as before and older files open unchanged).
`guide.add {artboard}` makes one, `guide.list` reports it; a guide dragged
out of a ruler with the Artboard tool belongs to the active artboard. It is
drawn, picked and snapped to across its artboard only, moves with it
(`artboard.move`, a pure move in `artboard.setProps`, `artboard.rearrange`),
is copied with it (`artboard.duplicate`, Alt-drag copy) and deleted with it
(`artboard.delete`, the guide selection kept in step).
Guides as Layers rows with their own lock and hide (part d) stay open: ruler
guides are not layer nodes; Lock Guides locks them all, and Make Guides
paths are Layers rows already. Listed in the ROADMAP parity row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Artboard tool chosen, Copy and Cut take its artboard together with
the art fully inside it (when Move/Copy Artwork with Artboard is on), and
Paste brings both back in one undo step: Paste right of the last artboard,
Paste in Place / in Front / in Back where it was, into this or another
document, the art going back into the layers it came from (Paste on All
Artboards pastes the art alone). The clipboard carries the artboard
(Clipboard::artboard); a pasted artboard keeps its name when it is free.
Duplicate Artboards (the Artboards panel menu, and now a row dragged onto
New Artboard) copies the art too: artboard.duplicate {index?, art?} defaults
to the Artboard tool's artboard and moveArt option and reports the copies.
It shares copy_artboard with the Alt-drag copy (artboard.move {copy}).
New commands reachable over MCP: artboard.copy and artboard.cut
{index?, art?}; edit.copy / edit.cut run them while the Artboard tool is
chosen (and are enabled then with nothing selected). Docs (mcp.md), the
tool and panel docs, translations of the new labels and the ROADMAP follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live rectangles drew elliptical corners after an uneven scale in v0.4.0;
inconsistent on a rectangle that isn't square:
- doc: LiveShape::fold_scale moves an uneven scale left in `xf` (files
saved before #291) into the size and radii; `transform` uses it, and
`folded()`/`corner_style` give Live Corners document units.
- engine: object.setLiveShape folds that scale first, so setting a radius
or kind (fields, dialog, widgets, MCP) gives circular corners on such
shapes instead of keeping the ellipses.
- geom: max_corner_radius/fitted_corner_radius are the one limit every
corner shares (half the shorter side).
- tools: corner widgets sit at the centres of the corners as drawn (a
radius past the limit put them off the arc, further in along the long
side), drag in document units on stretched shapes, and outline the
edited corners in red at the limit, as the reference app does.
- svg: CSS export writes the drawn radii (CSS would draw a radius past the
limit when its neighbours leave room).
- docs: mcp.md Live Corners, ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alt already deleted regions and pieces of open lines, but erase mode had
no edges of closed shapes, no distinct pointer, highlighted regions in the
merge-mode grey, only noticed Alt on a mouse move, and an erase drag could
flip mode at release. Like the reference app's erase mode:
- pathops::shape_builder(shapes, edges) returns a BuilderArrangement; with
`edges` it also cuts the outlines of closed (and filled open) paths
wherever other paths meet them. pathops::cut_out removes such a piece
from a path: a closed subpath opens there, an open one splits or
shortens, a piece all the way round takes its subpath, and a filled open
path's invisible closing edge never comes back as a visible segment.
Specks where paths only touch cut nothing; a zero-length input is no
longer the source of edges running through it. encloses_area is shared
with the tool's closing of filled open paths.
- BuilderMap (with_edges) hit-tests edges like line pieces (nearest wins);
an erase gesture that crosses an edge into a region deletes the region,
not the edge (erased_edges).
- shapeBuilder.merge {erase}: deletes the edges touched within tolerance
(from every shape along them), after the regions; returns `edges`.
shapeBuilder.regions lists `edges` (bounds, source). One undo step.
- Shape Builder tool: Alt held switches to erase mode and back on release:
red mesh/edge highlights of what will be deleted, a minus pointer
(Cursor::ShapeBuilder / ShapeBuilderErase, a plus in merge mode); a drag
keeps the mode it started in. The canvas re-hovers the tool when a
modifier changes, so the highlight follows Alt without a mouse move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Type on a path could only be repositioned by Type on a Path Options, whose menu item sent
`{start: 0}`, so OK threw the text to the start of its path; nothing showed or dragged its
brackets. Now, as in Illustrator:
- Type on a path flows between a start and an end bracket (`TextKind::OnPath { end }`, None:
the end of the path or once round a closed one; `TextKind::path_span`). Flipping
(`TextObject::flip_on_path`) reverses the path keeping a closed path's first anchor and swaps
the brackets, so the type keeps its stretch of the path.
- Type on a Path Options gains Align to Path (ascender, descender, center, baseline) and
Spacing (glyphs spaced as if set that far above the path: closed up round the outside of
curves, opened up round the inside), laid out by the text engine.
- `type.pathOptions {start?, end?, flip?, effect?, alignToPath?, spacing?, ids?}` sets them in
one undo step; with none of them it queries. The menu item opens the dialog with the selected
type's Effect, Flip, Align to Path and Spacing (dropdowns for the choices) and leaves the
brackets alone, so OK as opened changes nothing.
- The Selection and Direct Selection tools show selected type on a path's path and its start,
centre and end brackets (`tools::pathtype`, with a bracket cursor): dragging the start or end
bracket sets where the type runs, dragging the centre bracket slides it along the path and,
across the path, flips it (Cmd/Ctrl held: it only slides). Each drag is one undo step.
- Arc-length measuring moved to `vectorcraft_geom::ArcPath` (layout, the tools and
`text.createInPath` share it). SVG export outlines path type whose end bracket, alignment or
spacing `<textPath>` can't express.
- docs/mcp.md documents the command and the drags; ROADMAP updated; dialog strings translated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stroke panel weight dropdown converted one pt-based preset list into
whatever Units > Stroke was set to, which produced ugly rounded values like
0.088 mm, 0.265 mm, 10.583 mm from the pt ladder. Each unit now has its own
native ladder (observed from Illustrator's running dropdowns and typed in),
converted to pt once at use. Points, Millimeters, Centimeters, Inches,
Pixels and Picas each get Illustrator's actual values; the exotic Unit
variants (Feet & Inches, Meters, Yards, Feet) fall back to the pt ladder.
Picas round-trip to exact pt weights (1 pt, 12 pt, 60 pt …) but display as
decimal picas until Unit::number learns 0p1 / 1p3 notation; that is a
system-wide formatter change left for a separate PR.
Also bumps Unit::number precision from 3 to 4 decimals so Illustrator's
sub-eighth-inch strokes (0.0078, 0.0156, 0.0313, 0.0625 in) show their
authored values instead of rounding to 0.008 / 0.016 / 0.031 / 0.063.
Trailing zeros are still trimmed, so 1 pt stays "1".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dragging a numeric field's label left or right steps the field's value,
as Photoshop's scrubby labels do (beyond Illustrator): one unit every
2 points, ten with Shift, a tenth with Ctrl/Cmd (counts stay whole), in
the field's unit and precision. The panels apply each value as they
apply a typed one, so the art follows live and the caller's limits
hold (dragging back from a limit changes the value at once).
- scrub.rs: the label zone over the label noted just before a field
(dim_label, label_row, widgets::field_label, the Character and
Paragraph panels' icons, the Transform and Properties rotate/shear
icons), the ↔ cursor on hover, a click on the label focuses the
field. The text box itself never scrubs; a field without a label
doesn't. num_field, plain_field, mixed_field (and the spin fields)
and opt_field take part.
- Engine: undo groups (Session::begin_undo_group / end_undo_group). The
edits made in a group, commands and committed live drags alike, are
one undo step, journaled one by one; a cancelled group is undone and
dropped from the journal. The app opens one while a label is dragged,
so a scrub is one undo step whichever path the panel applies values
by (app.run, session.execute, live_run), and Escape cancels it
(shortcuts leave Escape to the drag, so it doesn't close a dialog or
deselect).
- Preferences > General > Scrub Numeric Fields by Dragging
(`scrubNumericFields`, on by default), translated in ja, es, zh-hans,
zh-hant and pt-br.
- Tests: label drags with Shift/Ctrl, limits, counts and units, Escape,
the preference, the cursor, click-to-focus, no scrub on the box or
without a label, and in the app one undo step for a W drag.
- ROADMAP and docs/mcp.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Switching to CMYK separates RGB greys through the CMYK profile into
four-colour greys and a rich black, as Illustrator does; only Gray
colours stay on the black plate. The command catalogue said "greys stay
greys", which read as if R = G = B colours went to K only.
- file.documentColorMode (and its alias) and document.open {colorMode}
take grays: "profile" (default, unchanged) | "black". With "black",
RGB greys with R = G = B convert to K only, K = their grey value (the
inks Edit Colors > Convert to Grayscale before the switch gives);
other colours convert as before. Swatches, tints, gradient stops and
pattern tiles follow the same rule.
- Pattern tiles are now converted with the rest of the document (they
kept RGB colours in a CMYK document before), as the command said.
- The command catalogue, docs/mcp.md and the ROADMAP say what happens to
Gray colours and RGB greys.
- Tests: the profile path (four-colour greys, rich black, pattern tiles,
Gray kept), grays: "black" over art, swatches, tints, gradients and
patterns, document.open, and the issue's file over MCP through a PDF
export and back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fourteen more Preferences options that were stored but read by nothing now
act, for the mouse and for agents (prefs.set, then pointer_gesture,
press_key or the commands):
- Selection & Anchor Display: Snap to Point and its distance (with Smart
Guides off, View > Snap to Point lands a dragged selection's grabbed
point, drawn points and reference points on anchors and ruler guides);
Move Locked and Hidden Artwork with Artboard (artboard.move, the Artboard
tool and Rearrange; off, hidden art now stays too); Enable Rubber Band for
the Pen and Curvature tools; Handles (solid, hollow, large);
Show handles when multiple anchors are selected (canvas and Direct
Selection's handle picking); Hide Corner Widget for angles greater than.
- General: Transform Pattern Tiles is the default of a new `patterns`
param on object.transform/move/rotate/scale/reflect/shear/transformEach
(journalled when the art uses a pattern), and the transform dialogs get a
Transform Patterns checkbox; Select Same Tint % (off, Select > Same
Fill/Stroke Color takes every tint of a global or spot swatch).
- Type: new type.step {attribute, by} steps size, leading, tracking, kerning
and baseline shift by the Size/Leading, Tracking and Baseline Shift
increments; Increase/Decrease Font Size (Cmd+Shift+. and Cmd+Shift+,);
the Type tool's Alt+arrows (kerning at a caret or tracking, leading,
Shift for baseline shift, Cmd/Ctrl for five steps). Enable Missing Glyph
Protection keeps characters a new font lacks in the font that had them
(text.setStyle, text.setRangeStyle). Fill New Type Objects With
Placeholder Text: type the Type tools place starts with placeholder text,
selected (text.create / text.createInPath take `placeholder`).
Units > General, Stroke and Type were already the single source of truth
(units.rs). Auto Size New Area Type, Show Font Names in English and
Highlight Alternate Glyphs need subsystems the app doesn't have yet and
stay open with the rest of #394.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The inspect_document tool (and the control channel's document.inspect)
take {depth?, childLimit?}, so MCP clients get the skeleton without
going through run_command; the tool description points at the
drill-down path.
- document.find rejects a filter of the wrong type instead of dropping
it (which widened the search); one opt_u64 helper parses limit, depth
and childLimit.
- docs/mcp.md spells out find's matching rules; control-protocol.md
and ROADMAP updated; MCP and engine tests cover the new paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.inspect takes the same {depth?, childLimit?} slicing as
document.node (a depth-0 read is the layer skeleton), and document.find
{name?|kind?|text?, limit?} locates nodes across the tree with their
ancestor paths and a total. docs/mcp.md documents the drill-down recipe;
the CLI stdio sweep covers both.
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>
Under Wayland the window is native but arboard was built without its
data-control backend, so it read the X11 clipboard through XWayland,
which only holds what X11 apps copied: Paste found nothing and reported
an empty clipboard (egui's text paste into fields uses smithay-clipboard,
which is why that worked). arboard now has `wayland-data-control`, the
same as PhotoCraft; it falls back to X11 where the compositor lacks the
protocol (GNOME's Mutter).
The reporter's clipboard also held text/uri-list: with a file copied in a
file manager, Paste would have taken the path as text before the bitmap.
Copied files now paste first, as the first one that is art (SVG, PDF,
EMF/WMF or a bitmap), told from its first 4 KiB by the engine's format
detection (`clipboard::file_flavour`): text/uri-list through arboard on
Linux and macOS (a trailing CR from CRLF lists trimmed), CF_HDROP on
Windows, read with the clipboard closed again.
Dropping files on the window can't work under Wayland yet: winit 0.30,
which eframe 0.36 runs on, has no Wayland drag and drop (winit#1881,
added in 0.31), so no dropped_files arrive. docs/development.md documents
it and the workarounds (copy and paste the files, File > Place, or run
under XWayland with `WAYLAND_DISPLAY=`).
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>
text.setRangeStyle takes charAlign, so the panel menu works on the
characters selected with the Type tool (it routes there while text is
edited, and charAlign alone was answered with "nothing to change").
One char_align_param parses it for both commands. The menu shows with
Preferences › Type › Show East Asian Options, like Mojikumi Set and
Top-to-Top Leading. Rows in the Simplified Chinese, Spanish and
Brazilian Portuguese catalogs; the Japanese Em Box Center names the em
box. ROADMAP: the gating, the range command, and ICF top/bottom still
missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Characters of different sizes on one line all sat on the Roman baseline.
Japanese layout usually lines them up by their ideographic em boxes
instead (a 16 pt 第一回 next to 32 pt 雅楽 centred on it, not sitting on
its baseline). The Character panel menu now has Character Alignment:
- `CharStyle.char_align`: Roman Baseline (as before, the default), or the
em box Top/Right, Center or Bottom/Left: a character smaller than the
line's largest moves so that its em box top, centre or bottom lines up
with the largest's (the em box from the font's vertical metrics, else
0.12 em below the baseline to 0.88 em above it). Vertical type the
same across the column (top → right). Written only when not Roman
Baseline.
- `text.setFormat {charAlign: "romanBaseline"|"emBoxTop"|"emBoxCenter"|
"emBoxBottom"}`; the menu shows the selection's setting; catalog rows
in ja and zh-hant.
- Not yet: aligning on the ICF (average character face) top and bottom.
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>
When the browser dropped the page's WebGPU device or WebGL context, the
editor kept running but nothing it drew reached the screen: Chrome and
WebKit showed a frozen, stale canvas and Firefox a blank page, while
edits and Save went on.
- ui-egui: new `graphics` module. `GraphicsLoss` carries a loss report
from any thread (wgpu's device-lost callback, the canvas's
`webglcontextlost` event) to the UI thread. `VectorcraftApp::
graphics_lost` writes Data Recovery copies of the modified documents at
once. The app now notices when the host hands it a new egui context and
uploads everything again (fonts and theme, the canvas, place cursor
thumbnails); every per-thread texture cache (layer, symbol, swatch,
style, asset and glyph thumbnails, previews, font samples, Navigator,
Flattener Preview) is a `TexCache` that empties for a new context.
Without this, the moved app drew stale or wrong textures and egui
panicked on its missing font families.
- web: the shell watches both loss signals; on a loss it puts a fresh
canvas in place of the dead one and starts the same `WebRunner` again,
handing over the same app (documents, undo, panels), with services made
for the new context and a status-bar note. If no new graphics can be had
(or after three losses) it removes the stale canvas, lets go of the
recovery copies and says to reload.
- engine: `recovery::leave` lets go of a session's area but keeps its
copies, so the reloaded page offers them at once instead of after the
heartbeat goes stale.
- desktop: eframe can't give a window a new device, so a lost device
(driver reset) is logged, the copies are written and the status bar says
to save and restart; closing then skips the Save Changes question it
can't show.
- Tests: loss reporting across threads, `TexCache` emptying, the app
uploading the canvas, a cached preview, theme and fonts again in a new
context (fails without the fix), copies written on loss, and
`recovery::leave` offering copies at once with both lock and heartbeat
stores. docs/development.md (web section) and ROADMAP updated.
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>
The Shape Builder only used the filled areas of the selected paths, so a
stroke-only line or open path across a shape did nothing. Like the
reference app's Shape Builder, open paths now act as cutting edges:
- pathops::shape_builder: with an open path in the selection, the
regions are the Live Paint planar map's faces (every path an edge), so a
line across a shape splits it and lines can enclose a region of their
own; it also returns each open path cut where other paths meet it. Closed
shapes alone, more than SHAPE_BUILDER_MAX_SEGMENTS segments, or a map
that can't be built fall back to the filled-area arrangement.
- Filled open paths count as closed (an invisible closing edge) where that
encloses an area; unfilled open paths and straight lines stay edges.
- shapeBuilder.merge: merging or deleting regions on both sides of a line
piece removes that piece (the line is split, its other pieces kept); an
area only lines enclose takes the current fill and the front-most
object's look. Alt (erase) within `tolerance` of a line piece deletes it.
Returns `lines` (pieces deleted).
- shapeBuilder.regions also lists the line pieces.
- The tool shares BuilderMap/Touched with the command, highlights the line
piece under the pointer while Alt is held and passes its tolerance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`command.batch` rolls a failed batch back through each document's interaction, which restored the
art, the selection, the current layer and isolation but not the highlighted Layers panel rows. A
batch whose `layer.setCurrent` step ran before a later step failed left that layer's row
highlighted, so a following Collect in New Layer collected the layer instead of the selected art.
The failed batch isn't journaled, so replaying the journal gave a different document: the
`journal_replay_reproduces_document` property test failed whenever it drew that sequence.
The interaction now keeps the highlighted rows too, which also puts them back when a drag is
cancelled.
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>
- The em box top that Top-to-Top Leading asks of every glyph's line metrics parsed the font's
cmap and vertical metrics per glyph on every layout: each face now reads it once.
- The Paragraph panel menu shows Top-to-Top / Bottom-to-Bottom Leading with the East Asian
options (Preferences › Type), as the Mojikumi Set does, and Reset Panel goes back to
Bottom-to-Bottom Leading, the default.
- Saved as `leading_model`, like the other paragraph attributes in files.
- Simplified Chinese (landed since) translates the two menu items.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japanese layout measures leading from the top of one line's ideographic
em box to the next (the right side, in vertical type), not from baseline
to baseline. The Paragraph panel menu now offers both:
- `ParaStyle.leading_model`: RomanBaseline (as before, the default) or
EmBoxTop. With EmBoxTop a line's leading is the space below it — the
next line's em box top is this line's plus its leading, and the next
baseline hangs from there by that line's tallest em box — and area
type's first line touches the top of the frame (its em box top on the
frame's top, at least Area Type Options' minimum). With baseline
leading a line's leading stays the space above it.
- Lines of one size are exactly as far apart either way; the models
differ for mixed sizes and for the first line of area type (fonts
whose ascent isn't the em box top: Hiragino's nearly is).
- The em box top comes from the font's vertical metrics (#343), else
0.88 em. Written only when EmBoxTop.
- `text.setFormat {leadingModel: "romanBaseline"|"emBoxTop"}`; menu items
Top-to-Top Leading / Bottom-to-Bottom Leading (checked as set); catalog
rows in ja and zh-hant.
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>
Any hit on a path's stroke took the "select the whole path" branch, so
dragging a segment moved the whole path. Now a press on the stroke of
a path that isn't selected as a whole selects the segment's two
anchors (unless they already are), and the drag bends the segment
with path.reshapeSegment when it's curved, else moves the selected
anchors (a straight edge moves, the rest stays), as in the reference
app. The fill, Shift-click and a path selected as a whole keep their
behaviour.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
New type's Auto alignment is saved as the alignment it has in the
paragraph direction (Right for right to left, else Left) plus an
optional `justify_auto` flag, so builds without Auto parse the file
and lay it out the same; this build reads Auto back. Paragraph styles
keep Auto as one `justify` attribute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Illustrator sets a paragraph's direction (Left-to-Right / Right-to-Left
Paragraph Direction in the Paragraph panel, with the Middle Eastern
options) and keeps its alignment buttons physical; it has no automatic
alignment toggle. So:
- ParaStyle.direction (None: from each paragraph's first strong
character, as before; written only when set), the Paragraph panel's two
direction buttons with Show Indic Options (original icons), and
text.setFormat {direction: "auto"|"leftToRight"|"rightToLeft"}. The
layout uses it as the base direction.
- The Automatic alignment check box is gone. Justify::Auto stays as the
start of the paragraph's direction, set on new type only (text.create,
the Type tools, with the mojikumi default in one new_type_para()):
TextObject::point is Left again, so importers (PDF, SVG, DXF, EMF) keep
their own alignment. The alignment buttons show what Auto resolves to.
- Bidi only where there is something right to left: plain paragraphs skip
the algorithm (no per-layout allocation), it runs once per paragraph
(shaping takes the paragraph's levels instead of recomputing them; path
type computed it four times), and the shaper keeps Han, Hiragana,
Katakana and Bopomofo in one segment (splitting at every change of
script cost Japanese text a shaper call per kana-kanji switch).
- Vertical type keeps its logical order, so its glyphs and lines are not
marked right to left (the caret no longer runs backwards down a column).
- PDF import puts Hebrew and Arabic, which PDFs (and VectorCraft's own
export) draw in visual order, back in logical order with the direction
that shows it as drawn; with bidi layout it came back mirrored.
- SVG import pins the direction SVG implies (left to right unless
direction: rtl); SVG export also outlines right-to-left paragraphs.
- Docs (development.md), ROADMAP (folded into the Type entry, Type core
row) and README updated; catalog rows for the new labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unicode bidi (unicode-bidi, unicode-script, unicode-general-category):
each paragraph's direction from its first strong character, Hebrew and
Arabic shaped right to left, embedded Latin and numbers keep theirs,
lines wrapped in logical order and reordered per line by whole
clusters; source text, undo and style ranges stay logical.
- Caret, hit testing, selection and the arrow keys follow the visual
order.
- New text aligns automatically: right-to-left paragraphs right, others
left (Justify::Auto, text.setStyle {justify: "auto"}); the Paragraph
panel highlights the resolved alignment and has an Automatic alignment
toggle. Explicit alignments, and older documents', are kept.
- SVG export outlines bidirectional text (with a warning) until it can
be written as directional live text.
Laying out type (show, stringwidth, cshow) costs about 300 operations plus
64 a character, measured, and now counts that much against MAX_OPS: a
300-byte file running cshow over a 4 MB string took 60 s, and
{ (a) stringwidth pop pop } loop ran for over 5 minutes (already the case on
main); both now stop with "the program runs too long" within seconds.
Tests: executable filters run inside themselves stop at the nesting limit
(directly, mutually and under stopped), currentfile cvx exec runs nothing
twice, cshow over a long string is refused; the hostile PostScript fuzz
tokens gain self-recursive filter execution, currentfile filters, cvlit and
cshow.
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>
Add Snapping to the Artboard Tool
Merged with the Artboard tool's Alt-drag duplicate (#348): a copy snaps too, and to the artboard
and art it leaves behind (a move skips them).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file can hold any number of saved selections; the duplicate-name check and the name lookup were quadratic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9bcb695ad3904565ed84b7562aa99dd818604cec)
Choosing a perspective tool shows the grid, but the canvas also re-selected the tool after a
Cmd-drag with the borrowed Selection tool, so a grid hidden with the tool chosen came back after
any Cmd-drag. `Session::switch_tool` (and the app's `restore_tool`) switch without choosing; the
temporary-tool return uses it.
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>
path.smartRemoveAnchor stays. Object › Path, the Control bar, the contextual task bar, Properties, and the canvas context menu run it on direct-selected anchors. The Pen no longer uses Alt/Option for this, so that key stays free for Convert Anchor Point.
- A dragged handle no longer snaps to its own path's bounds and centre, which move with the
handle; the path's anchors are point and alignment targets instead.
- Shift steps 45° from the Constrain Angle preference, like the cut and gradient tools.
- snap_draw and snap_handle share one pixel/grid/smart-guides switch.
- A test that a smooth anchor's opposite handle turns with a Shift-dragged handle and that Alt
still breaks it, and the tools' module docs (both from #338).
Co-authored-by: theironchef <6278036+theironchef@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>