- Type left unfinished when a layer's marked content ends, and a clip that closes in a later layer,
stay in their own layer (no stray "Page N" layer, no art moved to the next layer).
- Top-level layers stack as the pages mark them, so a layer whose art starts on a later page keeps its
place; layers marked with no art come in empty.
- A stroke written as each glyph's outline stroked, right after (or before) the type, becomes the type
object's stroke above (or below) its characters: the type stays one editable object.
- Spaces read from gaps wider than a space get their own tracking where even tracking would put the
glyphs after them more than a tenth of an em off (barcode digit groups); lines are measured to their
last visible glyph, so a wrapped line's trailing space no longer spreads into the tracking.
- Lines wrapped in a frame (each wrapped line ending in a space, one left edge, font, size and leading,
with an optional paragraph spacing) come back as one area type object, left aligned or justified,
only where laying it out breaks the lines exactly where the file does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The worker reads the clipboard only while nothing is copied in the app
and a document is open (as the in-line check did before), and idles on
a channel otherwise; it sends a verdict and wakes the UI only when the
answer changes.
- The in-line check (Windows, macOS, web) keeps its short-circuit: it
doesn't open the system clipboard after an in-app copy or without a
document, as before the PR.
- A dead worker hands the check back to the UI thread instead of
freezing the last verdict.
- Dropping the app wakes the worker and waits (250 ms at most) until it
has let go of its clipboard handle: on X11 the last arboard handle
alive hands what the app copied to the clipboard manager, and a
detached worker holding it would lose that handover at exit.
- The factory stays in `Services` until the first frame takes it (no
extra app fields); main.rs installs it with `cfg!(target_os = "linux")`.
- Tests trimmed from five to three that share tests_sysclip's helpers:
the frame never waits on a slow clipboard and only the probe thread
reads it (nothing read without a document), the probe rests after an
in-app copy and lets go of its handle when the app drops, and a dead
probe falls back to the in-line check. The in-line case is already
covered by tests_clipboard.
- ROADMAP (Last updated, Platforms, Shipped so far) and
docs/development.md (Linux: clipboard owners that never answer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`has()` on an X11 clipboard owner that never answers can block for
seconds; the app called it every 0.25 s on the UI thread to decide
whether Paste is enabled, so a stuck owner froze the frame loop (the
window stopped drawing and could not be closed).
`clipboard_probe` owns a second clipboard handle on a worker thread,
publishes its verdict over a channel and wakes the UI only when it
changes; the frame reads the latest verdict without ever blocking.
Without a factory the check stays in line, throttled, exactly as before
(the web shows SVG text only; hosts that don't install one keep the old
path).
The factory is installed on Linux only for now (the environment the bug
reproduces in); other platforms keep the in-line check, so this cannot
change their behaviour.
Tests: a fake clipboard whose `has()` blocks on a gate, a probe factory
sharing a recorder, and a headless frame whose elapsed time is measured.
They assert the frame does not wait on the clipboard and that the check
runs on the probe thread, that the probe's verdict reaches `system_paste`
and the Paste menu, that dropping the app stops the probe, and that the
in-line check (no factory: the web, older hosts) still works.
Reported: Alt-click with the Pen should toggle an anchor between corner and smooth, and the
Control bar had no way to make a corner anchor smooth.
v0.5.0 already has Pen + Alt as the Anchor Point tool (#473) and the Control bar's and the
Properties panel's Convert buttons for direct-selected anchors, the path being drawn included (its
last anchor is selected). The remaining gap: with Direct Selection, the Pen, the Curvature or the
anchor tools, a path selected as a whole shows all its anchors selected, yet the Convert buttons
only appeared once anchors were direct-selected.
- chrome::anchor_controls decides what the Control bar and the Properties panel show: Convert and
Anchors for direct-selected anchors (as before), Convert alone for paths selected as a whole
with a tool that edits anchors (path.convertAnchors already converts every anchor of those);
Remove and Cut still wait for anchors direct-selected, as they act on those only.
- Tests: the Control bar and the Properties panel offer Convert for a whole rectangle with Direct
Selection and the Pen, not with the Selection tool, and smooth converts every corner; Pen + Alt
on the path being drawn makes a smooth anchor a corner (click) and a corner smooth (drag), and
drawing goes on. ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported: closing a shape by pressing on the first anchor and dragging out a curve flickered, and
releasing at the wrong moment left the shape open.
The press decided to close (path.close preview), but every move asked again which path was being
drawn: the closing preview leaves the path closed, so no longer one the Pen extends, and the move
fell through to "first anchor of a new path" (a path.create preview), the next move back to a
close of a path that no longer existed in the preview... The release committed whichever was last.
- The press now records what it placed (Place: the anchor, the path it goes on, none for a new
path, and whether it closes it), and the drag only shapes that anchor's handles: a close stays a
close, symmetric by default and Alt breaking the handles, and the release closes the path wherever
it ends. The first anchor of a new path no longer depends on the selection either.
- Tests: tools (the drag sees the closed path), engine (closed throughout a 12-move drag, symmetric
handles at the release), MCP headless pointer_gesture. ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported: with the Pen there was no way to edit curves in place as Ctrl does in Illustrator on
Windows (Cmd on the Mac), segments couldn't be grabbed, and the curve-editing tools felt slow and
sometimes didn't take a handle.
- Cmd/Ctrl held at a press with any tool but a selection tool now borrows the selection tool in the
engine (Session::pointer), not in the canvas: the selection tool chosen last, or Direct Selection
for the Pen, Curvature and anchor tools until one is chosen. The tool lent to isn't deactivated,
so the Pen goes on drawing its path after the release while that path stays selected; it no
longer draws on another open path selected meanwhile. Agents reach it with pointer_gesture
mods.cmd. With Cmd held the pointer is the borrowed tool's. Choosing a tool or switching
documents mid-gesture gives the tool back first.
- Pen + Alt over a segment of a selected path reshapes it, as the Anchor Point tool does.
- The handles shown are the handles that drag (one helper for Direct Selection, the Anchor Point
tool and Pen + Alt): Direct Selection now drags the handles of a path selected as a whole (as
the Pen leaves it, so Cmd + drag of a handle while drawing works), the Anchor Point tool no
longer grabs handles that Show handles when multiple anchors are selected hides, the nearest
handle wins, and a handle shorter than the tolerance leaves its anchor to be picked. The anchor
tools pick within Selection & Anchor Display > Tolerance instead of a fixed 4 px.
- path.reshapeSegment keeps a smooth anchor at either end smooth (its other handle turns with the
moved one) instead of kinking it.
- A dragged handle gathered its smart guide targets from the whole document on every pointer move
(21 ms per move on 20,000 paths): HandleSnap now gathers them once per drag (1 ms).
vectorcraft-cli perf measures handle and segment drags on a 5,000-anchor path.
- catalog::is_selection_tool and catalog::edits_anchors replace the UI's own lists.
- Tests in tools, engine and MCP (headless pointer_gesture); docs/mcp.md, control-protocol.md,
ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.5.0 asked wgpu for the low-power adapter everywhere (#306). On a Linux
desktop whose KDE Wayland compositor runs on an NVIDIA GPU, wgpu sorted the
Ryzen's integrated RADV GPU first: it reports that it can present to the
window, but KWin can't import the buffers it allocates ("importing the
supplied dmabufs failed"), which is a fatal Wayland protocol error. The next
surface configuration then got ERROR_SURFACE_LOST_KHR and egui-wgpu panicked
("The surface isn't supported by this adapter", winit.rs:129).
- Preferences > Performance > Graphics Processor gains Automatic, the new
default: power saving on Windows and macOS as #306 wanted, and on Linux the
system's order, where Mesa's device-select layer puts the GPU the desktop
runs on first. Values are now automatic / lowPower / highPerformance; 0.5.0
saved powerSaving for everyone, which reads as automatic.
- The app picks the window's adapter itself (apps/vectorcraft/src/gpu.rs,
eframe's native_adapter_selector): only adapters that present to the
surface, WGPU_ADAPTER_NAME first, hardware before software, native backends
before OpenGL, then the power preference. Every adapter and the pick are
logged.
- run_native runs under the engine guard. When the graphics fail during the
first frames (a wgpu error, or a panic in wgpu, egui-wgpu or naga), the app
starts again without that adapter (exec on Unix, so an AppImage stays
mounted; a new process on Windows), passing the failed adapters in
VECTORCRAFT_GPU_SKIP, and says so in the status bar. When none is left it
logs why and exits with an error, never a panic.
- docs/development.md, README and ROADMAP: the new default, the restart and
the escape hatches (WGPU_POWER_PREF, WGPU_ADAPTER_NAME, WGPU_BACKEND,
MESA_VK_DEVICE_SELECT, DRI_PRIME).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing could be undocked: the dock's tabs only switched panels, the icon
column only popped its panels out next to it, and only tool flyouts
(#437) could float.
- A dock tab, a panel icon or a popped-out panel's title dragged out of
the dock floats that panel inside the app window; the strip right of
the dock's tabs floats the whole Properties | Layers | Libraries group.
- A floating group moves by its title bar (or the strip right of its
tabs); a tab dragged out of it floats on its own; dropped on another
group's title bar or tabs it stacks with that group; dropped on the
dock, which lights up under the pointer, its panels go back where they
live in the dock (tabbed group or icon column). Its x does the same.
- The Tools panel floats by its title bar or grip and docks again on the
window's left edge, which lights up the same way.
- window.panel.float {panel, x?, y?, onto?, group?} and window.panel.dock
{panel, group?} (panel "tools" for the Tools panel) for the control
channel and MCP; every panel menu ends with Float Panel / Dock Panel;
window.panel on a floating panel shows its tab.
- Floating groups and the Tools panel's position are kept with the
preferences and in user workspaces (built-ins dock everything),
sanitized on load, and clamped into a smaller window when drawn.
- One move state in egui temp memory drives every drag, so a tab torn off
keeps following the pointer; per frame the work is a lookup per panel.
- Icon buttons can be drawn for a caller's response (paint_icon_button),
so the column's icons keep their ids when one above them floats.
- Translations (ja, es, zh-hans, zh-hant; cs and pt-br for the commands),
es @msg rows, docs/control-protocol.md and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grip at the bar's left end now drags the bar anywhere on the canvas
(grab cursor), which always holds it whole, also when the window shrinks.
Unpinned, the bar keeps that offset as it follows the selection (in the
document it was moved in); the "…" button opens a More Options menu with
Hide Bar, Pin Bar Position (checked while pinned: the bar stays where it
is across selection changes and the handle still moves it; unpinned it
follows the selection again from there), Reset Bar Position (unpinned,
back under the selection) and Show Properties Panel, as in Illustrator.
As there, the position and the pin last for the session only.
- UI commands window.taskBar.pin {pinned?} and window.taskBar.reset, so
the control channel and MCP reach them; ui.inspect reports taskBar
{pinned, rect}.
- One opt_bool helper reads the {key?: bool} params of the toggling window
commands (toolbarColumns, floatTools, collapseDock, taskBar.pin).
- New strings in ja, es, zh-hans, zh-hant, pt-br and cs; the dropped
"Hide Contextual Task Bar" tooltip and per-key es messages removed.
- Headless egui test: dragging the grip moves the bar, the offset follows
the selection, pin holds it across selection changes and a shrinking
window, unpin follows from there, reset restores.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A press inside a dialog the popped-out panel opened (Swatch Options from its
menu, say), or on a modal dialog's backdrop, put the panel away under the
dialog. The press now counts as away only when it lands on no foreground
layer (the flyout itself, dialogs, palettes) and not on the modal backdrop
(`dialogs::modal::backdrop`), as the popovers decide a click elsewhere.
- A press in the expanded dock's panel group left the flyout open: only the
icon column is spared now (its icons swap or close the flyout themselves),
so a click in another docked panel puts it away, as a click anywhere else in
the workspace does in the reference app.
- The test covers a click in a dialog, on its backdrop and in the docked group.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preferences › User Interface › Auto-Collapse Iconic Panels was stored
and never read: a panel popped out of the icon column stayed until its
» or its icon. On, a press away from the flyout now puts it away. A
press on the icon column is left to the icons (they swap or close the
flyout on release, so acting on the press would reopen the one being
closed), and a press in a popup the flyout opened (a panel menu, a
dropdown) keeps it. Off by default, as in Illustrator.
Developed with AI assistance (Claude Code); reviewed and tested locally.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The option tells points from picas, where a bare `12` could be either: the
reference app dims it unless a unit is Picas, and it starts on. A number typed
with no unit into a field in picas is now read in points with it on (the
default again, as before #486), and in picas with it off; fields in any other
unit read it in their unit either way, as there. The Preferences dialog dims
the checkbox unless one of the Units is Picas.
The fields learn the preference when it changes, through
`prefs_dialog::apply_runtime` as Scrub Numeric Fields does, rather than from a
write into the egui context every frame. Tests for picas and other fields, the
wiring and the dimming; docs/mcp.md and ROADMAP say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preferences › Units › Numbers Without Units Are Points was stored and
never read. On, a number typed with no unit into any length field
(`num_field`, `opt_field`, and so the spin fields built on them) is read
in points whatever the field's unit; a typed unit (`12 mm`, `1in`)
still wins, and the field keeps showing its value in its own unit. The
app sets the frame's rule from the preference in `logic`, the fields
read it through the egui context (`widgets::typed_unit`), so none of
the 45 callers change.
The default flips from true to false, which is Illustrator's default
and the behaviour the fields had while the option was unread.
Developed with AI assistance (Claude Code); reviewed and tested locally.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Preferences › General › Show The Home Screen When No Documents Are Open
was stored and never read: the Home screen always replaced the canvas
while no document was open. Off, the window is now empty while no
document is open (as when Illustrator's option is off), and the Home
button (`app.home`) still shows the screen; closing the last document
returns to the empty window. `menus::home_showing` is the one place
that decides, for the canvas and the Home button's lit state.
Developed with AI assistance (Claude Code); reviewed and tested locally.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wheel did nothing over numeric fields. As in Illustrator, once a numeric
field has the focus, turning the wheel over it steps its value like Up/Down:
a notch a step, Shift ten, Ctrl/Cmd a tenth, applied at once with the new value
selected.
- widgets::step_value (was step_with_arrows) reads the arrows and the wheel
through one path for num_field, plain/mixed fields and the spin fields built
on them (panels, the Control bar, dialogs, the Font Size and Leading combo
boxes), and now opt_field (Stroke dash/gap) too. Trackpad and fine-wheel
points add up to notches; the rest carries to the next turn.
- While the pointer is over the focused field the panel under it doesn't
scroll; an unfocused field, or the pointer anywhere else, leaves the wheel to
the panel and the canvas as before.
- The dialogs' and preferences' egui drag values become the same recessed
number fields (widgets::range_field: clamped to their range, rounded to their
precision), so they type, step and scroll like every other numeric field.
- Headless tests for the wheel (units, modifiers, points, focus and pointer),
and for the panel scrolling or not; docs/control-protocol.md and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Shift-drag marquee only ever added to the selection, so there was no way to
take some of the selected objects out with it. As in Illustrator, Shift-drag
now toggles what the marquee reaches: the selected ones are deselected, the
others selected, the rest of the selection stays.
- Selection tool: the Shift marquee runs select.toggle, which now also takes
{ids} (each one toggles).
- Direct Selection and Group Selection: the Shift marquee toggles the anchors
inside it (select.anchorsMany mode "toggle"); a path left with none leaves
the selection, one with all of them is selected whole again.
- select.anchorsMany takes mode "set"|"add"|"toggle"|"subtract" (add: true is
still mode add). Adding anchors of a path selected whole keeps it whole, and
anchor references outside the path are ignored.
- Lasso: Shift adds and Alt subtracts through the same command, so an Alt lasso
no longer drops selected type, groups and other non-path objects.
- Tests at the tool, engine (Session::pointer) and MCP (pointer_gesture) levels,
command sweep cases for the new modes; docs/mcp.md and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bounding-box resizes with the Selection and Free Transform tools already lined
their edges up with the other objects (#355, after v0.4.0), but a handle never
landed on another object's anchor point, nothing was labelled, and the Rotate,
Scale, Shear and Reflect tools didn't snap their drags at all.
- Targets::snap_scale: the dragged corner (free, or along its diagonal with
Shift) or side lands on the nearest anchor or centre within the snapping
tolerance, labelled "anchor"/"center"; alignment snaps keep their line and
now say "align", as an artboard's resize handle does. Alt (from the centre)
and rotated boxes work the same way.
- The transform tools start a drag from the anchor or centre they grabbed (the
point a click would set) and snap the pointer as drawing tools do (Smart
Guides, the grid, pixels or Snap to Point), so a corner dragged onto another
object's anchor scales exactly onto it.
- Factor the nearest anchor/centre lookup (Targets::nearest_point) and the
transform tools' drag teardown (end_drag).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pens on KDE Plasma 6.3+ under Wayland don't reach the app: winit 0.30 binds no
Wayland tablet protocol (zwp_tablet_v2), and since Plasma 6.3 KWin no longer
emulates pointer input for such clients (kwin!6336), so the pen moves KWin's own
cursor while the window gets no motion or presses. The fix is upstream (winit
0.31's tablet input, winit#4318, once eframe moves to it). docs/development.md
and the README give the workarounds: run under XWayland (WAYLAND_DISPLAY=), or
set KWIN_WAYLAND_EMULATE_TABLET=1 for the session.
On our side, a pen or finger (winit Touch, emulated as a mouse by egui-winit)
lifts with an egui PointerGone in the same frame. A tap whose press and lift
arrived in one frame had no hover position, so the canvas ignored it, and a
drag ending without a pointer position ended at the canvas's centre. The press
and double-tap now fall back to the interact position, and the release to the
last position seen during the drag. Headless frame tests replay the events a
pen produces: a stroke with pressure reaching Use Pressure Pen, and a one-frame
tap that selects without moving (fails without the fix).
ROADMAP: Last updated, the Platforms row, the hardening gap and the graphics
processor/input bullet (pen pressure reaches the tools on Windows only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pixel grid draws through the document grid's `grid()`, which assumed an
unrotated view: with Rotate View its lines stayed upright on screen, in the
wrong places, or vanished. It now spans the box round the canvas's corners in
document space and draws each line through the view, so the pixel grid and
the document grid (lines and dots) turn with the art. The lines are iterated
rather than collected, capped, and computed by multiplication instead of
accumulating a step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preferences › Guides & Grid › Show Pixel Grid (Above 600% Zoom) was stored and
never read. On (the default), in View › Pixel Preview at 600% zoom and above the
canvas draws a translucent line at every document pixel over the art, so the
pixels the art rasterizes to can be seen; nothing below 600%, out of Pixel
Preview, or with the option off. Drawn by the same `grid()` as the document
grid, at a 1 pt spacing.
A headless frame test counts the pixel grid's lines at 800%, 500% and 600%, in
and out of Pixel Preview and with the option off, and checks they are drawn
over the art.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Preferences › General › Anti-aliased Artwork was stored and never read. Off, the
canvas renders the art with `AntiAlias::None` — a pixel is painted when the art
covers at least half of it, raster effects and pattern tiles staying smooth — and
on (the default) with `AntiAlias::Art`, as before. The preference is part of the
canvas's cache key, so changing it re-renders. Exports keep their own
Anti-aliasing option.
A headless frame test reads the art's raster back out of the frame's texture
upload: edge pixels partly covered with the option on, none off, and smooth
again once it is back on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Type is picked on the baseline of any of its lines (point type and area
type, besides area type's frame), as in Illustrator, not only the first
one: the layout's baselines are cached with its bounds
(`TextObject::cached_baselines`, from `TextLayout::baselines`), with the
first one across the bounds as the fallback. Vertical type's runs down
each column's centre line.
- Type in perspective is picked where it is drawn (through its projection).
- The bounds rule type out before its path is built, so hovering stays
free of allocations away from type.
- The Type tools still edit type clicked among its characters (they made
new type there with the preference on), and the Eyedropper still samples
it there, as it does inside fills with Object Selection by Path Only.
- Tests for all of it; docs/mcp.md and ROADMAP say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preferences › Type › Type Object Selection by Path Only was stored and never read.
On, the Selection tools pick type on its type path only — point type's baseline
(the line the selection marks under it, across its layout's width), area type's
frame, type on a path's path — within the selection tolerance; a click among the
glyphs or inside the frame selects nothing. Off (the default), anywhere in the
type's bounds selects it, as before.
`HitOptions` gains `type_path_only`, set by the tools' context (`hit_options()`)
and the canvas's `hit_at` from the preference, so the Selection tools, Cmd-click
behind, object highlighting and the context menu agree. Tests at the hit-test
level (point, area and on-path type) and through the Selection tool with
`prefs.set`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
egui reports a modifier's own key press (ShiftLeft, ControlLeft…) as a key event, and the
recorder took the first key press it saw, so pressing Shift+A saved `ShiftLeft` as the chord: a
shortcut that can never fire. The recorder now skips modifier key presses, so the chord completes
on the first other key with the modifiers held (Esc still cancels). A modifier is never a chord's
key in the shortcut parser either, so `shortcuts.set` refuses one, and overrides saved that way by
older versions are dropped on start, giving the default back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stock EPS files saved in the Illustrator 8 format failed with "typecheck in `a
dictionary`": its gradient procset builds shading dictionaries with `bd` …
`ed`, where `ed` is `(>>) cvx` (or a Level 1 dictionary builder as a
string), and `cvx` left a string literal, so `shfill` got a string. Its
`discard` procedure also `load`s procedures by integer keys, which `load`
refused.
- Executable strings: `cvx` makes a string executable (`xcheck` true,
`cvlit` back, still a `stringtype` for `length`/`get`/`forall`/
`getinterval`); run by name or met in a procedure, it runs its text as a
program, every time (an executable file shares the same runner).
- `load` takes any key (integers, booleans), as `where` already did.
- Test: an Illustrator 8-style gradient procset with both `ed` forms and
`discard`; the import fuzz vocabulary has executable strings and integer
keys. An Illustrator 2026 file saved in the Illustrator 8 format (made
here, not committed) now opens as vectors with its radial gradient.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discord feedback: "EPS files are always rasterized: they open as bitmaps, in
other programs as vectors". Every EPS saved by Illustrator 2026 fell back to
its TIFF preview (`gcheck` was unknown), and cairo/Inkscape files stopped at
their first image (`status`), dropped masked images (ImageType 3) and failed
on mesh gradients; the warning didn't say where the program failed.
Fallback never silent:
- The warning names the PostScript error, the operator that raised it and
the named procedures it ran in, innermost first ("a PostScript error
(typecheck in `add`), in `inner` in `outer`"); errors caught by `stopped`
aren't reported. It reaches the status bar, document.open, app.open and
file.open (which now return document.open's result, so MCP's open_file over
the control channel gets it), and `vectorcraft-cli info` ("warnings").
- A file whose PostScript can't be read opens as its TIFF preview, else its
Windows metafile preview (as vectors, scaled to the bounding box), else its
EPSI bitmap. es @msg rows for the new messages.
Interpreter:
- Operators programs probe or use: gcheck/scheck, rootfont,
currentcolorrendering, currenthsbcolor, status, token, read,
bytesavailable, fileposition/setfileposition, resetfile, write*, pdfmark
(marks dropped, as Ghostscript does), execform, nulldevice, strokepath,
pathforall, user paths (ufill/ueofill/ustroke/uappend/upath/setbbox, the
encoded form too), setcachedevice2, findcolorrendering, cache, device and
VM queries, revision; (%stdout) (w) file gives a sink.
- `//name` is replaced by its value when read, inside procedures too.
- Tiling patterns become pattern swatches (their PaintProc drawn in pattern
space, one cell; uncoloured ones in the colour under them; makepattern
copies share one swatch); nested patterns stop at 8 levels.
- Type 3 fonts draw their glyphs (BuildGlyph/BuildChar, widths from
setcachedevice/setcharwidth) as outlines grouped per string, spaced by
ashow/widthshow/awidthshow/xshow/yshow/xyshow/kshow; glyphshow and
stringwidth/cshow use them.
- Shadings of types 1 and 4-7 become gradient meshes (data from arrays,
strings or files, packed or not, flags sharing edges), as fills through a
shading pattern too; sampled functions (type 0) are evaluated;
GradientMesh::coons/triangle are shared with PDF import.
- ImageType 3 masked images, interleaved by sample, by row or in separate
sources; ReusableStreamDecode reads its data when made; Flate/LZW PNG and
TIFF predictors.
- Bounded: DeviceN at 32 inks, sampled functions at 2^24 samples and 32
outputs, 4096 patches a shading, strokepath ignores dashes finer than 1e5
pieces, Type 3 glyphs nest 8 deep (a glyph that shows itself overflowed the
stack before the limit), 64-bit index maths for 32-bit targets.
Tests: a synthetic program per generator (cairo/Inkscape, its Type 3 fonts,
matplotlib, CorelDRAW, Affinity, LibreOffice, Ghostscript eps2write,
Illustrator procsets), meshes and function shadings, masked images, fault
messages, probing operators, metafile/EPSI previews, `//name`, user paths,
hostile patterns/glyphs/shadings (run against main, 12 of the 14 generator
and feature tests fail; CorelDRAW- and LibreOffice-style programs already
read there). The import fuzz vocabulary covers the new operators. Checked
against real files made here with Inkscape 1.4 (cairo) and Illustrator 2026
(its own format and the CS-to-CC compatibility formats; all fell back or
stopped early on main and open as vectors now; its Illustrator 8 format
needs the next commit); those files aren't committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two tools had hand-drawn nibs that didn't match the Pen tool's icon and read as distorted
next to it. They now use Lucide's pen-tool (the Pen tool's own icon) with a plus or minus badge,
so the Pen group reads as one family; the Remove Anchor Points buttons pick up the minus one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Presentation Mode hides the menus and panels, had no Escape and was saved with the
preferences, so it came back on every restart with no way out. Escape now leaves it (as in
Illustrator), F from it goes back to Normal Screen Mode, and the screen mode is no longer
saved: the app always starts in Normal Screen Mode, and preferences saved in Presentation
Mode by older versions open normally. ui.inspect reports `screenMode` on its own.
Desktop file drags carry no pointer position or modifiers (winit 0.30 on Windows, macOS
and X11), so every drop on a window with a document open was taken for the middle of the
canvas and placed. Where the platform doesn't say, a dropped document (native, SVG, PDF,
.ai, EPS, DXF, EMF/WMF) now opens as a tab of its own and a picture or text is placed in
the middle of the view; pictures dropped together with documents are placed first, so the
last document opened is shown. The web, whose drag events give the position, keeps
Illustrator's choice: placed on the canvas, opened off it (the tab bar).
Illustrator's PDF-compatible part leaves out art outside the artboards (it lives in the
private data alone, which isn't read), so it can't open; importing a file with that
private data now says so in a note. Off-page art a PDF does draw already opens on the
pasteboard; a test keeps it so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Paragraph panel menu's Burasagari submenu reads None / Regular / Force, as the reference
app's help names them (`text.setFormat` keeps "none"/"standard"/"forced", its scripting names);
Reset Panel goes back to new type's Regular.
- The full-width ,.and half-width 、。 hang too; Latin commas and full stops never do, so Latin
text keeps its breaks and composer with new type's default (tested). The spaces after a hanging
mark stay on its line.
- docs/mcp.md: a Burasagari section. ROADMAP: wording, Last updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New type (the Type tools, `text.create`, area type and type on a path) takes Standard
burasagari, Illustrator's default (observed in its Paragraph panel menu). Documents from before
it and imported text keep None.
- Only a paragraph with a Japanese comma or full stop is composed line by line, so Latin text keeps
the every-line composer with the new default (tested).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paragraph panel menu › Burasagari › None / Standard / Forced (with the East Asian options) and
`text.setFormat {burasagari}`:
- None: a comma or full stop that doesn't fit goes to the next line with the character before it.
- Standard: one that doesn't fit hangs outside the line; one that fits stays inside.
- Forced: one ending an area type line always hangs, and the rest of the line fills the measure.
Only 、 and 。 hang (closing brackets don't), in the width their mojikumi gives them. Horizontal
and vertical. Saved only when not None; new type keeps None. Paragraphs with burasagari are
composed line by line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discord feedback: Alt with the Pen didn't move a direction handle on its own, and there was no
way to break a path at an anchor and pull the ends apart.
- Pen: Alt held over a handle end or an anchor of a selected path (the one being drawn too) hands
the gesture to the Anchor Point tool, with the convert cursor: dragging a handle moves it alone,
clicking a smooth anchor makes it a corner, dragging an anchor pulls out symmetric handles.
Drawing goes on afterwards. Handles hidden by a partial anchor selection aren't grabbed.
- path.cutAtAnchors cuts at the direct-selected anchors in one undo step: a closed path opens
there, an open one becomes one path per piece, and one anchor of each cut stays selected so a
drag pulls the path apart. It answers {ids} and refuses (no undo step) when only open ends are
selected. path.convertAnchors checks `to`, skips stale anchor refs instead of indexing past
them, and keeps a smooth anchor's handles when converting to smooth.
- SubPath::cut_at and SubPath::smooth_anchor (geom) now back path.split, path.cutAtAnchors,
path.convertAnchor and path.convertAnchors, replacing three copies of that logic.
- Control bar and Properties panel: "Convert:" corner/smooth and "Anchors:" remove, connect
(Join), cut buttons when anchors are selected; Cut Path on the task bar and the context menu.
New original icon dc-anchor-smooth.svg (ASSETS.md row).
- ja, zh-hans, zh-hant and es strings (cs for the context menu item), es @msg rows; docs/mcp.md
section; ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discord feedback: Image Trace and the other image options were hidden when an image was
selected; copy, paste, Image Trace should work from the Control bar as in Illustrator.
- Control bar, one image selected: Linked File/Embedded, file name, colour mode and PPI, then
Embed (+ Edit Original) or Unembed…, an Image Trace split button (click: Default preset;
arrow: trace with any preset), Mask and Crop Image, in place of Fill and Stroke.
- Control bar, one Image Trace object selected: "Image Tracing", its Preset (choosing another
traces it again), the Image Trace panel and Expand.
- Properties panel: Image Trace (split), Crop Image and Mask in Quick Actions for an image; an
Image Trace section (Preset, Expand, Release) for an Image Trace object. Embed/Unembed buttons
are shared with the Control bar (place::link_buttons).
- New engine command object.maskImage ("Mask"): clips the selected image with a clipping path
on its outline (rotated with it) and selects that path so its handles crop; one undo step.
- widgets::split_button (flat button with a menu arrow).
- Translations for the new strings (es, ja, zh-hans, zh-hant; "Mask" also pt-br, cs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legacy PostScript .ai files (Illustrator 3-8, and their EPS) open through
the EPS interpreter, which only recorded what was drawn: every group came
in flattened. In a file in Illustrator's format (its %AI comments, or
Illustrator named as the creator) the `u` ... `U` it writes at the top
level now open and close groups, whatever its prolog defines them as, so
groups come in nested as they were, between and inside clipping groups.
clipnest's Clip carries an optional region: without one it is a plain
group, nested in the same chain as the clips (ids given in opening order,
so a chain of clips and groups sorts by id).
Clips nested without end used to nest clipping groups past the stack
(a stack overflow in EPS and EMF/WMF import): clips and groups now nest
at most 128 deep with a warning, and each object's chain counts against
the EPS reader's drawing budget.
PDF-compatible .ai (Illustrator 9+) can't get the same: its PDF content
draws each object on its own and doesn't mark plain groups (only layers,
clipping groups and groups with opacity, blending or a mask, which
already come back); the groups live only in the private data, which is
out of scope. Documented in ROADMAP and docs/mcp.md.
Addresses Discord feedback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discord feedback: type in a frame dragged with the Type tool could not be resized or switched
between area and point type the way the reference app allows.
- The Selection tool shows the type widget beside a single selected point or area type object:
a small circle outside the middle of its bounding box's right side, hollow on point type and
filled on area type (vectorcraft_tools::typewidget, drawn by the canvas from the same geometry).
Hovering it shows the arrow with a type badge; its clicks neither drag nor deselect, and a
double-click runs type.convertToAreaType / type.convertToPointType (the Type menu's commands),
keeping the text and its styles in place, one undo step, reachable over MCP.
- Resizing area type made by dragging already works on main (#252): the bounding box resizes the
type area and the text reflows. A regression test now covers the reported flow.
- docs/mcp.md and ROADMAP.md describe the widget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discord feedback.
- Properties panel: the Appearance section's Fill and Stroke rows now use the
Control bar's widgets: the chips with their chevron and swatch/mixer
popover, the underlined Stroke link that opens the Stroke panel as a
popover (stroke::link, shared with the Control bar) and the weight spinner
with its presets (stroke::weight_field, which also follows the Type tool's
selected characters). With nothing selected the Document view shows them
too, as the Control bar does: they set up the next object drawn.
- Window menu panel keys as in Illustrator: Brushes F5, Layers F7, Align
Shift+F7, Transform Shift+F8, Info Cmd+F8, Pathfinder Cmd+Shift+F9,
Symbols Cmd+Shift+F11, Character Cmd+T, Paragraph Cmd+Alt+T, Tabs
Cmd+Shift+T, OpenType Cmd+Alt+Shift+T (beside the existing Color, Color
Guide, Appearance, Graphic Styles, Stroke, Gradient, Transparency and
Attributes keys). The dock's tabs (Properties, Layers, Libraries) join the
panel shortcut table and the Keyboard Shortcuts editor through
state::all_panels, which window.panel's name lookup and the catalog's
label list use too.
- Alt-wheel (Option on the Mac) zooms about the pointer whatever General >
Zoom with Mouse Wheel says: egui spreads a notch over a few frames, and the
rest of an Alt turn scrolled once Alt was up (also over the control
channel's ui.wheel); the turn's own modifiers now decide. Pinch zoom is
covered by the test too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A screenshot on the clipboard didn't paste everywhere:
- macOS: the Paste menu item (which takes Cmd+V there) was enabled for text
only, so with nothing copied in the app Cmd+V did nothing; and arboard
reads the pasteboard's TIFF only. The pasteboard's types now enable Paste,
and its PNG, else TIFF, pastes as it is.
- Windows: bitmaps came through CF_BITMAP and GetDIBits, which drops alpha.
Paste now reads PNG, else CF_DIBV5, else CF_DIB, decoded with their colour
masks and tables (a BMP file header in front, which image's headerless
decoder gets wrong for masks after a BITMAPINFOHEADER), opaque when the
alpha is zero throughout as screenshots leave it, capped at 32768 px a side.
- Web: only SVG text pasted. The page's paste event now hands the first
picture or file that is art to the app (VectorcraftApp::paste_from_host),
pasted with the chord held, like the desktop's paste keys.
- Linux reads image/png already; its Paste menu item still waits for text
(no cheap way to ask arboard for a bitmap), the paste keys take one.
Bitmaps paste as embedded images at the view centre, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>