Add ru.tsv with the full interface coverage: every menu string, every
tl! literal, the plurals (one/few/many) and the status/error messages
(@msg). Registered as complete_menus with the Russian plural rule;
every ru-* locale resolves to it. Keeps the product, workspace and
perspective preset names in English like the other complete catalogs.
Tests: russian_reads_as_russian, russian_plurals_have_three_forms,
russian_glyphs_are_available_without_system_fonts; ru added to
KEEPS_MENU_NAMES and COMPLETE_MESSAGES.
Clean-room: written from the meaning of the English labels in the
vocabulary Russian-speaking users of vector illustration software know
(Слой, Образцы, Обработка контуров, Монтажная область, Обводка,
Заливка), matching the Russian catalog of the sibling app PhotoCraft.
No other application's localisation was copied.
An SVG used as an animation rig lost its hidden alternates and their
rotation pivots: the exporter left hidden objects out (only hidden layers
could be kept), and the importer dropped data-* attributes.
- SVG export: hiddenLayers (on by default for document.save, as before)
now keeps hidden objects too, each written display="none" on its own
element (path, image, text or group). Exports still leave them out
unless asked; the CLI's convert asks, as a conversion keeps the
document.
- An object's own data: ObjectAttributes.data, the data-* attributes in
order (ours, data-name and data-vc-*, aside). The importer keeps them on
the objects and layers they belong to (at most 64 per element), the
exporter writes them back, the native format saves them (missing in
older files), and object.setProps {data: {key: value|null}} sets or
removes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
file.saveAs and document.save wrote a PDF-compatible .ai, but
document.export (and with it the CLI's run --export x.ai and convert
in.svg out.ai, and MCP's export) refused it as a format that can only be
opened, and --help didn't list it as writable.
The .ai encoder is now shared: save.rs's ai_params and encode_ai (a PDF
of every artboard with the PDF options, carrying the native document,
blank pages with pdfCompatible: false) serve Save As and the exports
alike. The .ai format is writable, so document.formats, --help and the
MCP export tool list it; an export leaves the document's own path alone.
Export As and Export for Screens keep leaving .ai to Save As.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exporting one artboard of many to SVG wrote the whole document's art into
that artboard's file, the other artboards' art outside its viewBox: an
editor opening it showed every artboard's art. A file for an artboard
named in the export (Use Artboards with a range or All, Export for
Screens) now holds the objects over that artboard only, at any depth of
layers, as DXF's crop leaves them; objects reaching into it stay whole
and a clipping layer keeps all of its objects. An SVG with no artboard
named (Save As) still holds the whole document.
Part of #546: Export As with a range writes the chosen artboard alone in
SVG, DXF, PDF and PNG, each file with that artboard's art (tested from
the dialogs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows lists each GPU under both backends, and Vulkan came first. On an
Intel GPU its driver made the whole window flicker black while the
pointer moved; the same machine was fine with WGPU_BACKEND=dx12 or gl.
adapter_order now ranks a GPU's DX12 adapter before its Vulkan one, after
the power preference, so the GPU chosen doesn't change, only how it is
driven. Vulkan stays the fallback (a failed DX12 start restarts on it)
and WGPU_BACKEND=vulkan still picks it. Elsewhere there is no DX12
adapter, so nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the menus are in the system menu bar, laid out as a Mac app's (Illustrator on the Mac as
the reference), and the app settings sit in the VectorCraft menu: About, Settings ▸ (one item per
Preferences page, General… ⌘K), Language ▸, Appearance ▸ (UI brightness), Services, Hide
VectorCraft ⌃⌘H (⌘H stays View › Hide Edges), Hide Others ⌥⌘H, Show All and Quit ⌘Q (the app's own
quit, so unsaved documents are asked about); they leave Edit and Help. Window gets Minimize ⌃⌘M,
Zoom and Bring All to Front; Help is the system's Help menu. Windows, Linux and the web keep the
in-window menu bar unchanged; --in-window-menus or VECTORCRAFT_IN_WINDOW_MENUS keeps it on macOS
too, and so does a menu bar AppKit refuses to build.
One menu model: the in-window menus and the native bar draw the same rows (menus::entry), and the
platform-free native_menu module lays them out, decides when to update (a cheap state hash; labels,
enabled and checked in place, a rebuild only when the structure changes) and hands key equivalents
back to egui as the key presses they were, so shortcuts, user overrides and focused text fields
behave as in the window and nothing fires twice. The muda backend (pinned =0.21.1, macOS only, no
unsafe) is apps/vectorcraft/src/mac_menu.rs, mirroring PhotoCraft's. ui.inspect reports
nativeMenuBar. Translations for the new labels in ja, es, zh-hans, zh-hant, cs and pt-br.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every drawing tool now asks one snapper (guides::DrawSnap) for the points it
places: the Pen's and Curvature tool's anchors, the Pencil's ends, the shape
tools' start and dragged corner or radius (Rectangle, Rounded Rectangle,
Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's
click and frame and a new artboard's corners. Points land on other objects'
anchors, centres and paths ("anchor", "center", "path") or line up with their
anchors, edges and centres ("align"); hovering shows it before the press, the
Pen's guides end its rubber band, and the Pen lands on the path being drawn
too. Shift keeps a point to its 45-degree step (from Constrain Angle) or a
box's diagonal and slides it along that way into line with a target.
Preferences > Smart Guides > Construction Guides and Angles now act: a drawn
point near a guide through the point it leaves from lands on it.
Before, the Pen snapped only its press and showed nothing, and the shape tools
re-gathered targets on every drag step from the document showing their own
preview, so the dragged corner stuck to where the last step left it.
Performance: targets are gathered once per document state (keyed on the
document's uid and revision, now in ToolContext), from the art in the window,
and sorted, so the anchors, centres and lines near the pointer are found by
bisection. Leaving objects out of the targets no longer looks up each node's
ancestry (quadratic in the document's size): one pruned walk does it, which
also speeds up the Selection tool's moves and resizes.
The direction handles' HandleSnap (#494) is now a DrawSnap holding its own
targets (Targets::for_handle) for the drag: one cache, one lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Corner widgets only ever showed on live rectangles: CornerWidgets::of bailed
out unless the selection's live shape was LiveShape::Rectangle, and
object.setLiveShape only edited rectangle radii. A star is a plain path, so it
had no widgets with either selection tool.
- geom::corners: the corners of any path (anchors without handles between two
straight sides) and the path with them cut round, inverted round or
chamfered, each corner a circle tangent to both sides, its radius no larger
than takes the cut halfway along its shorter side. Rectangles now cut their
corners with it (bit-identical anchors and layout).
- doc::corners::LiveCorners/set_corners: one model for a live rectangle's,
a live polygon's (new radii/kinds) and any other path's corners (new
LiveShape::Path keeping the uncut outline, plain again once nothing is cut);
radii scale with transforms (Scale Corners) and stay circular.
- object.setLiveShape takes anchor indices for `corners` on any path; polygons
keep a shared radius as their sides change.
- Widgets: Direct Selection on any path, Selection on live rectangles and
polygons; placed on each corner's bisector, the drag follows it, the red
limit outline marks corners at their own limit; the angle preference and
View > Hide Corner Widget apply. Corners dialog and the Properties and
Transform panels' Corner Radius (now on polygons too) work on them.
- Tests for the star, the polygon and a pen path in geom, doc, tools, engine
and ui-egui; docs and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds an Italian catalog (crates/ui-egui/src/i18n/it.tsv, 3,549 rows in
es.tsv's order) covering every menu string, every tl! literal, the
plurals and every status/error message, at the same scope as Spanish.
Standard Italian in sentence case, written clean-room from the meaning
of the English labels; the product, workspace, perspective preset and
language names stay in English as in es and ja.
Registered as "Italiano" with the one/other plural rule and
complete_menus; every it-* locale resolves to it. Tests: it joins
KEEPS_MENU_NAMES and COMPLETE_MESSAGES, italian_reads_as_italian checks
vocabulary and plurals, tags_map_to_languages the locale tags, and the
bundled-font glyph test covers Italian accents and guillemets. README,
ROADMAP and docs/development.md list the language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011iayep93edeimiAE8oSfaT
- 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>
As decided in #432 (option 2): while the interface is in Japanese, new type starts with em box
top-to-top leading and em box centre character alignment; other languages keep the Roman
baseline, so Latin area type doesn't move.
- The UI tells the engine the language it is drawn in each frame (`Session::ui_language`); without
a UI, an explicit `interfaceLanguage` preference counts (`Session::japanese_interface`).
- `text.create` and `text.createInPath` (the Type tools' clicks and drags) apply it, and take
`leadingModel` / `charAlign` to choose; the values used are journaled, so a replay does the same
in any language.
- `paraStyle.new` / `charStyle.new` made from nothing (no `attrs`, no text selected) carry them.
- Imported text (PDF, SVG, DXF…) keeps the Roman baseline. Saved documents keep what they say.
- docs/mcp.md (New type in a Japanese interface) and the ROADMAP note it.
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>
* Release: Flatpak x86_64/aarch64 bundles, AppImage zsync updates, README Downloads
- New `flatpak` matrix job (ubuntu-24.04 and ubuntu-24.04-arm) repackages each Linux tarball
into vectorcraft-<ver>-linux-<arch>.flatpak via packaging/linux/flatpak-bundle.sh and the
new ai.storyteller.vectorcraft.bundle.yml; packaging-lint keeps its runtime and finish-args
identical to the from-source Flathub manifest.
- AppImages embed gh-releases-zsync update information and ship a .zsync beside them; the
Linux job checks both.
- Jobs that sign nothing (version, linux, flatpak, freebsd, web) leave the `release`
environment, so a branch workflow_dispatch is a dry run; the draft-release job still needs
the signed macOS/Windows jobs.
- README: a Downloads section listing every build; docs/releasing.md updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release: read AppImage update info without extract-and-run
With APPIMAGE_EXTRACT_AND_RUN=1 the runtime forwards --appimage-updateinformation to the app,
which then tries to open a window and fails on the headless runner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- packaging/macos/dmg/generate.py writes background.tiff and DS_Store on any OS (resvg with
the repo's fonts; ds_store and mac_alias, as dmgbuild uses). Both are byte-reproducible and
carry nothing from the machine that made them.
- DS_Store: same window, icon size, positions and background alias as before, without the
bookmark (pBBk/pBB0) and the alias's disk-image record, which held the author's boot volume
UUID and name, its creation date and temporary paths.
- background.tiff: the same pixels as before (checked), now opaque RGB with Deflate (1.8 MB to
1.0 MB) and LittleCMS's built-in sRGB profile instead of an HP profile with no open licence;
no file names or software tags.
- background.svg: drop an unused pattern and the xlink namespace (renders the same).
- ASSETS.md rows for the three files; README, docs/releasing.md (volume name) 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>
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>
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>
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>
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>
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>
The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: the graphics adapter in use, a lost graphics
device, the engine guard's report of a recovered panic. A launch from a
desktop menu has no terminal either, so there was nothing to attach to a
bug report.
apps/vectorcraft/src/logging.rs (no new dependency):
- standard error plus logs/vectorcraft.log next to the preferences
(~/.config/vectorcraft/logs, or under $XDG_CONFIG_HOME); each launch
rotates the previous log to vectorcraft.1.log and .2.log, so the log of
a run that crashed survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
(bounded) and written once the file is attached
- default levels: info for vectorcraft*, warn for everything else;
RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
error as the only sink
- runs with VECTORCRAFT_NO_PREFS (agents' test runs) log to standard
error only, so they don't rotate away the user's own logs
The logger is installed first in main and the file is opened after
argument parsing (--version leaves no file). Log messages of this crate
drop their "vectorcraft: " prefix, which the record's target now shows.
vectorcraft-cli keeps its own MCP logger (vectorcraft_mcp::logging).
Documented in docs/development.md (Logs, Environment variables).
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>
- `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>
Guides dragged out of a ruler did not snap at all (only Shift to the ruler's
ticks): they now go through the engine (`Session::ruler_guide`, a
`NewGuide` previewing `guide.add`, one undo step) and snap as a moved guide
does: with Smart Guides to the edges, centres (so the sides' midpoints) and
anchors of the art and of the artboards, labelled; with Smart Guides off,
View > Snap to Point pulls new and moved guides onto anchors. Moving art
with the Selection tool already snapped flush to artboard edges and centres
with Smart Guides on; a test now covers it.
Artboard guides: `Guide.artboard` holds an artboard id (written only when
set, so canvas guides save as before and older files open unchanged).
`guide.add {artboard}` makes one, `guide.list` reports it; a guide dragged
out of a ruler with the Artboard tool belongs to the active artboard. It is
drawn, picked and snapped to across its artboard only, moves with it
(`artboard.move`, a pure move in `artboard.setProps`, `artboard.rearrange`),
is copied with it (`artboard.duplicate`, Alt-drag copy) and deleted with it
(`artboard.delete`, the guide selection kept in step).
Guides as Layers rows with their own lock and hide (part d) stay open: ruler
guides are not layer nodes; Lock Guides locks them all, and Make Guides
paths are Layers rows already. Listed in the ROADMAP parity row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Artboard tool chosen, Copy and Cut take its artboard together with
the art fully inside it (when Move/Copy Artwork with Artboard is on), and
Paste brings both back in one undo step: Paste right of the last artboard,
Paste in Place / in Front / in Back where it was, into this or another
document, the art going back into the layers it came from (Paste on All
Artboards pastes the art alone). The clipboard carries the artboard
(Clipboard::artboard); a pasted artboard keeps its name when it is free.
Duplicate Artboards (the Artboards panel menu, and now a row dragged onto
New Artboard) copies the art too: artboard.duplicate {index?, art?} defaults
to the Artboard tool's artboard and moveArt option and reports the copies.
It shares copy_artboard with the Alt-drag copy (artboard.move {copy}).
New commands reachable over MCP: artboard.copy and artboard.cut
{index?, art?}; edit.copy / edit.cut run them while the Artboard tool is
chosen (and are enabled then with nothing selected). Docs (mcp.md), the
tool and panel docs, translations of the new labels and the ROADMAP follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live rectangles drew elliptical corners after an uneven scale in v0.4.0;
inconsistent on a rectangle that isn't square:
- doc: LiveShape::fold_scale moves an uneven scale left in `xf` (files
saved before #291) into the size and radii; `transform` uses it, and
`folded()`/`corner_style` give Live Corners document units.
- engine: object.setLiveShape folds that scale first, so setting a radius
or kind (fields, dialog, widgets, MCP) gives circular corners on such
shapes instead of keeping the ellipses.
- geom: max_corner_radius/fitted_corner_radius are the one limit every
corner shares (half the shorter side).
- tools: corner widgets sit at the centres of the corners as drawn (a
radius past the limit put them off the arc, further in along the long
side), drag in document units on stretched shapes, and outline the
edited corners in red at the limit, as the reference app does.
- svg: CSS export writes the drawn radii (CSS would draw a radius past the
limit when its neighbours leave room).
- docs: mcp.md Live Corners, ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>