The Properties panel's Font Size and Leading were plain typed fields and the
Control bar had no type controls. All three places now use the shared
editable combo (`widgets::spin_field`: typing, arrow and spinner stepping,
label scrubbing, a preset dropdown in the type unit):
- Character panel: the size and leading grid is shared with the Properties
panel (`character::metrics_grid`), which now shows the font style, Font
Size, Leading, Kerning and Tracking as combos and applies them to the
Type tool's selected characters as the Character panel does.
- Control bar: with type selected or edited, a Character link (the Character
panel in a popover), the font family and style menus and the Font Size combo.
- Presets as Illustrator lists them: sizes 6-72 pt; Leading has Auto first
(`widgets::spin_field_auto`, `text.setStyle {leading: "auto"}`) and then
the size ladder instead of 120% values; Kerning and Tracking -100...200.
- Font Size, Leading, Kerning and Tracking show blank where the selected
text's values differ (`character::shared`); the spinner does nothing on a
blank field instead of jumping to the step size.
Each pick is one `text.setStyle` / `text.setRangeStyle` step, so MCP and the
control channel are unchanged. Translations for the two new strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `Targets::align_rects` tells `snap_point_with` an axis lined up even when
nothing moved or Alignment Guides hide the line, so the "align" label stays.
- Free Transform's move offset is a measurement label (as the Selection
tool's), its scale/rotate readouts Transform Tools; slice move/resize
readouts follow Measurement Labels.
- Snap to Point (Smart Guides off) keeps the default look for moves too, as
for drawn points and guides.
- Test: guides dragged out of a ruler (#451) follow Snapping Tolerance,
Anchor/Path Labels and Measurement Labels; docs/mcp.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The toolbar went to two columns whenever the window was shorter than the
single column (about 990 pt for Basic, more for Advanced), so on most
screens it was always double and the double arrow and the Window menu item
flipped a flag nothing read. The column count is now the user's choice
alone; a window too short for the tools scrolls them (#84).
- window.toolbarColumns takes {double?: bool} (set or toggle, returns the
new state) and reports `checked`; the double arrow runs it.
- The Window › Toolbars item reads "Double Column" with a check mark;
catalogs updated (cs, es, ja, pt-br, zh-hans, zh-hant) and an es @msg row
for the new error.
- Headless tests: the arrow and the command toggle the column count in a
700 pt window (both fail on main).
Co-Authored-By: Claude Opus 5.5 <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>
Over the canvas the window hid the system cursor and painted the tool's
cursor glyph into its own frame. That glyph reached the screen only after
the frame was rendered, queued (two frames by default) and composited, so
on Windows it visibly trailed the real pointer, on any GPU: the "sluggish
mouse" of the report. Frame times were not the cause (0.4 ms empty,
1.5 ms median on a 20 000-path document, and no repaints while idle).
- cursors: the glyphs are collected as shapes, tessellated by egui and
rasterized into straight-alpha RGBA bitmaps (anti-aliased, hotspot on a
pixel corner), cached per cursor and display scale so the same Arc is
handed over every frame and egui-winit keeps its OS cursor.
- canvas: on Windows and Linux the tool cursor goes to the OS through
ctx.set_cursor_image; the system moves it itself. macOS (bitmaps sized
in points) and the web (no bitmap cursors in eframe) keep painting it.
- app: the wgpu surface queues one frame instead of two (LOW_LATENCY):
the GPU only composites the CPU-rendered canvas, so everything drawn in
the window follows the pointer a frame sooner.
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>
The scan read only the platform's font folders, once per session (or on
Character panel menu > Refresh Font List). Fonts Windows registers outside
its font folders (installed as shortcuts, or by programs into their own
folders) never listed, nor did fonts installed while the app was open.
- Windows: the font files registered under the local machine's and the
user's Fonts registry keys outside the scanned folders are scanned too
(windows-registry, the windows-rs project's safe registry API).
- Linux/BSD: the XDG data folders' fonts (XDG_DATA_HOME, XDG_DATA_DIRS:
Flatpak, Snap, NixOS, Guix) and a Flatpak sandbox's host fonts
(/run/host/fonts, local-fonts, user-fonts); macOS: /Network/Library/Fonts.
- The global database looks the platform's paths up again at each scan,
and a scan's roots may be font files as well as folders.
- FontDb::installed_fonts_changed compares the folders the last scan read
(their modification times, and the paths it started from); the app
checks it when its window comes back to the front and rescans
(text.rescanFonts) only when fonts were installed or removed meanwhile.
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>
- A click on a flyout's tear-off bar, or the release of the long press
that opened it over the bar, tears the flyout off too, as in
Illustrator; dragging it still does.
- window.floatTools {tool, floating?} floats or puts back the group
holding a tool, for the control channel and MCP's invoke_menu.
- Floating strips are part of user workspaces; the built-in ones float
none, so Reset puts them back.
- Loaded preferences keep one strip per group, of known tools.
- No per-frame clone of the strips or copy of the active tool's id.
- docs/control-protocol.md, ROADMAP, Float Tool Group translations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A grab bar runs down the right side of a tool group's flyout. Dragging it
tears the flyout off: the group floats as a strip of its tools' buttons
(the active one lit, each tool's name and shortcut in its tool tip), with
the bar on its right. The same bar moves the strip, and the × at the top
of the bar puts the group back in the toolbar. While a group floats, the
presses that would open its flyout (right, long or corner press) raise
the strip and flash its border instead, so a group is never both a
flyout and a floating strip. Floating strips are kept with the UI
preferences and kept on screen.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A press on a single tool's button no longer schedules the repaint that
would open a flyout it doesn't have. The ROADMAP's UI list and Last
updated line name the right and corner press.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A right press opens the flyout on the press instead of on the release,
including on the active tool's button, and the release leaves it open. A
primary press on the button's corner triangle opens it at once without
picking the tool; a plain click in the middle still picks it. The long
press keeps scheduling the frame where it becomes long.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Illustrator documents two drop targets in the Layers panel's bottom bar:
dragging items to Create New Layer duplicates them, to the Delete icon
deletes them. Dropping rows on New Sublayer or Make/Release Clipping
Mask no longer creates a sublayer or clips the row's layer; those
buttons only act on a click, and the new test checks a drop on New
Sublayer changes nothing. The trash and New Layer share one drop loop,
and the drag ghost paints its colour bar and thumbnail with the row's
own helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a row shows a dimmed copy of it (colour bar, thumbnail, name)
under the pointer. A row dropped on Delete, New Layer, New Sublayer or
Make/Release Clipping Mask runs that button on the row's layer, as a
click would: Delete removes the row itself and New Layer duplicates it,
as in Illustrator.
Rows dragged onto the trash weren't deleted. The trash took the drop as
a target-circle payload first, and egui's take discards a payload of
another type, so the rows reached the second check as nothing. It now
peeks at the type before taking. The trash is also outlined while rows
or a target circle are over it, like the other drop buttons.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each preset in the Color dropdown has a chip of its colour before its
name; Custom has none, its name aligned with the others.
Co-Authored-By: Claude Opus 5.5 (1M context) <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 popover kept its open flag while its anchor was hidden (panel closed, selection changed), so
it came back open the next time the row was drawn. It now remembers the last frame it was open in
and counts as closed after a frame without it, as egui's remembered popups do. ROADMAP: the
Appearance panel's Opacity popup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
egui remembers one open popup at a time, so opening the blend-mode
dropdown or the opacity slider inside the Opacity popup closed the popup,
and the dropdown or slider with it. A new widgets::popover keeps its own
open state and closes only on Escape, its anchor, or a click outside it
and the popups it opened. The Opacity popup and the Stroke popover (which
holds dropdowns too) use it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The pt ladder stays 0.25 … 10, 20, 30 … 100 pt (the dropdown's documented 20/30/40 pt steps).
- Feet, yards and meters returned the pt ladder converted as if it were in that unit (a 100 m
stroke); they now get the pt ladder in points.
- weight_presets returns a fixed array (no allocation per frame); the pica ladder is stored in
points; 0.325 in is 0.375 in (the eighths ladder).
- Fields keep three decimals in points, pixels and picas (595.276 pt) and show four in mm, cm,
inches and larger units (8.2677 in, 0.0078 in).
- ROADMAP: the Stroke panel's per-unit presets.
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>
The modal dialogs were anchored to the window's centre, so none of them
could be moved. They now share one modal frame (dialogs/modal.rs): the
band from the window's top edge down to its heading moves it, as a title
bar would, with a grab cursor; it stays on screen and keeps where it was
moved (per dialog, for the session). The shared dialog frame, New
Document (and More Settings), Preferences, Keyboard Shortcuts,
Workspaces and Find Font all use it; About moves by its title bar.
The form dialogs drew every string field as a text box, so Offset
Path's Joins had to be typed. Their choice fields are now dropdowns
(Joins: Miter, Round, Bevel; Average's axis; Shear's and Reflect's
axis), the fields follow the dialog's order (Offset, Joins, Miter Limit;
Rows, Columns, Gutter) instead of alphabetical, and Offset Path has a
Preview that Cancel rolls back.
The axis labels are translated in a new "axis" catalog context: the
plain Horizontal and Vertical rows mean writing direction in Japanese.
ja, es, zh-hans and zh-hant get the axis rows (Horizontal, Vertical,
Both) and Joins, Miter, Bevel, Offset and Axis.
Headless tests drag the heading of four dialog windows (moves, a drag
below the heading doesn't, kept on screen, reopens where it was left),
open the Joins dropdown and choose Round (same result as the command
with round joins), and check the axis dropdowns and the preview.
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>
The import walks each page's content to learn which optional content
group each marked-content sequence belongs to, and stopped after 2,000,000
operators. Past that, art went to the page's "Page N" layer, visible and
printable, so a layer that was off (or didn't print) showed and printed;
an Illustrator drawing of about a million paths lost 6 of its 15 layers.
- The walk now reads up to 64 million operators a page (it reads about
15 million a second, twice as fast as the interpreter draws them; the
issue's 2.1-million-operator file imports in 3 s, a 10-million one in
15 s). Marked-content tags are kept as 64-bit hashes rather than byte
vectors, and at most 4 million a page, so the walk's memory stays
bounded on hostile input.
- When the walk is cut short (or the interpreter and the walk otherwise
part) and some group is off or doesn't print, art in marked content
whose group can't be told goes to a hidden, non-printing "Page N
(unsorted)" layer instead of the page's layer, and the warning says
so (and that the page was too large to read, when it was). Art drawn
past the limit inside a group already open stays in its group; art
outside any group stays on the page's layer. Files whose groups all
show and print keep the old fallback.
Tests (with the limit lowered): a hidden, non-printing group past the
limit lands on the hidden unsorted layer while the open group keeps its
art and the page's art stays shown; with every group shown, art goes to
the page's layer as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A top-level <g id> becomes a layer only when it was plain, so an Inkscape
layer (or our own export of a layer with a clipping mask) carrying a
clip-path came in as a clip group, and a file whose layers were all
clipped to one frame opened as one "Layer 1" of clip groups with the
layer names lost.
A top-level group that does nothing but clip its art (no mask, filter,
opacity or blend) is now a layer too: its clip path becomes the layer's
clipping mask (the clipping path first, as Make Clipping Mask in the
Layers panel leaves it), a clip path clipped in turn nesting a clip
group inside the layer as before. The clip building is shared with clip
groups. Top-level text that joins a clipping layer (before or after it)
isn't in its clip: the mask then becomes a clip group inside the layer
and the text sits beside it. A clip path with no shape leaves the layer
empty.
Tests: the issue's Inkscape layers (names kept, each a clipping layer,
drawn as resvg draws the SVG), a nested clip on a layer, the export ->
import round trip of clipping layers, and text joining clipping layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging direct-selected anchors never snapped: the anchor drag sent the raw
pointer delta. Now the anchor pressed on (or the pointer on a straight segment)
snaps and the other selected anchors follow by the same delta:
- Smart Guides: onto other anchors (including the anchors of its own path that
stay put), onto path segments ("path", new segment targets gathered only for
anchor drags, lines exactly on straight segments), else into line with
anchors, edges, centres and artboards ("align"). The anchors that move, their
segments and the reshaped paths' bounds are no targets, so an anchor can't
snap to itself. Targets are gathered once when the drag begins, capped.
- Smart Guides off: Snap to Point pulls it onto anchors and ruler guides within
its distance; Snap to Grid / Snap to Pixel apply as for drawn points.
- Shift keeps the 45-degree constraint unsnapped.
Group Selection and Direct Selection's whole-object moves now snap like the
Selection tool: its move snapping moved into a shared `MoveSnap`, and
`snap_with` into a reusable `PointSnap` that caches its targets per drag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Line-end Punctuation Half Width, an opening bracket starting a
wrapped line now loses the half-em space before it (JLREQ 3.1.5), so its
face lines up with the line's start and the line has that much more room.
A paragraph's first line keeps the bracket full width.
Also fixes upright glyphs in vertical type that lost their leading space
(consecutive opening brackets, and now the line start): they moved up the
column but also half an em across it, because the turning point moved
instead of the glyph. The tests now check the position across the line.
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>
Follow-up to #370: Hangul Compatibility Jamo (U+3130–318F, ㄱ ㅏ) and
half-width Hangul (U+FFA0–FFDC) counted as Japanese letters, so a Latin
letter next to them took the quarter em. They are left out like Hangul
syllables; the test covers both.
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>
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
Help > About VectorCraft gains Contributors and Models tabs next to the existing About
content, following the craftrules standard (standards/contributors.md):
contributors/contributors.json is generated by craftrules/scripts/contributors.py and baked
into the binary by crates/ui-egui/build.rs as static tables (credits.rs). Each contributor
line: @username, opt-in display/real name, PRs, commits, lines +/-/delta, binary assets +/-,
first/last commit. Sortable by every column; alphabetical ignores case and the leading @.
Labels are translated (ja, zh-hant, pt-br catalog rows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors docs: craftrules is two levels up (../../craftrules)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section
build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: credits labels for catalogs added on main since this branch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate after rebase onto main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
Find Font and the Glyphs panel were widgets::font_dropdown's last
callers; the font menu's own tests cover searching and Enter. ROADMAP:
the Glyphs panel and Find Font's Replace With use the font menu, without
its live preview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both switch from widgets::font_dropdown to font_menu::font_menu, so they
get the same samples, favourites and filters as the Character and
Properties panels. They only pick a font: the new font_menu::picked
returns the choice and toggles stars, and never previews on the document.
Follow-up to #339 as suggested in #364.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BRE8ZD3p8QgYctENhnJDz3
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>
The Fidelity slider names its ends, Accurate and Smooth, through a
shared form::slider_ends that the Color Guide Options' Less/More now use
too. The dialog's strings get Spanish rows (complete catalog), and
Accurate rows in every catalog. ROADMAP: the Drawing tools row lists
what the freehand Tool Options still lack (Keep selected, Alt to the
Smooth tool, Blob Brush Merge Only with Selection, brush angle and
roundness).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The five freehand tools keep options (Fidelity, fill, the two
tolerances, the brush size) that last across tool switches and are
saved with the preferences, but nothing in the window showed or changed
them: a double-click on the tool's button answered "the pencil tool has
no options", which the toolbar drops. It now opens a Tool Options dialog
with the options the tool has, and OK sets them with tool.setOption. The
dialog's strings are in the Japanese, Chinese (both scripts) and
Brazilian Portuguese catalogs.
Co-Authored-By: Claude Sonnet 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>
SVG import kept a <text>'s nominal font-size and left the scale of the
transforms and viewBox above it in the type's transform, so the Character
panel showed one size and the canvas drew another (7 pt under scale(3)
drew at 21 pt).
The importer now moves that scale into the type: the scale across the
baseline (the mean scale for type on a path) multiplies the size,
leading, baseline shift, character stroke weights and dashes and the
text path, and the transform keeps only the rest (a stretch along the
baseline, a skew, a rotation or a reflection). The type draws exactly as
before. Paints resolve in the grown text space, non-scaling strokes and
the stroke under the characters are worked out before the fold, the
factor is rounded to 6 significant digits (usvg's transforms are single
precision) and never pushes sizes past the Character panel's
0.1–1296 pt (the rest stays in the transform).
Tests: a group's, the text's own and a viewBox scale; rotated and scaled
text equal to the same text written at twice the size; stretch and skew;
type on a scaled path; the size limits; gradients on scaled text; SVG
export → import of scaled type (stable over two round trips).
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>