Commit Graph
248 Commits
Author SHA1 Message Date
imachusandClaude Opus 5.5 2c8b8b0cc1 Type: Proportional Metrics in vertical type — upright glyphs on the font's proportional heights, vpal (#966)
The second half of #966. With Proportional Metrics on, a vertical segment is
also shaped top to bottom, with and without `vpal`: the difference gives
each glyph's change of advance down the column and its x and y placement.
An upright glyph (not tate-chu-yoko) in a font with vertical metrics takes
them on top of its cell, so it moves with its shorter cell; a font without
vertical metrics keeps `upright_cell`'s cell, and with the switch off
nothing is shaped twice and the column is as before.

As in horizontal type, Line-end Punctuation Half Width takes nothing more
off the punctuation `vpal` re-spaced and trims the punctuation it leaves
full height as before. Docs: mcp.md, and Proportional Metrics leaves the
missing lists in gaps.md, type-parity.md and target-app-parity.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:24:45 +07:00
EmlembowandClaude Opus 5.5 7ff0269a1d Fix #867: macOS file dialogs are sheets, and no dialog runs inside the event handler
The crash: rfd's blocking dialog ran -[NSSavePanel runModal] inside
winit's event handler; when a resize of the panel ends, AppKit runs the
closures winit queued meanwhile, and winit's re-entrancy panic fires
inside an Objective-C block, where it can't unwind, so the app aborts.
As PhotoCraft (#673) and PdfCraft (#145) ship it:

- macOS: picks::ask shows an rfd::AsyncFileDialog as a sheet on the
  window (beginSheetModalForWindow returns at once); a waiter thread
  sends the paths and wakes the window. A pick asked outside an entry is
  refused and logged instead of running a modal. Linux keeps its thread
  (#592) and now also wakes the window; Windows is unchanged.
- Nine paths that asked outside an entry now run as entries: Load
  Profile, Swatches and Graphic Styles Other Library, the shortcut
  editor's Import and Export, and the presets dialogs' Export.
- While a dialog is open: the macOS menu bar is inert except Hide, Hide
  Others and Minimize; Quit, Close, Close All and the close button are
  refused; Finder/Dock opens and the IME discard wait; another dialog
  is refused with a status message. The answer re-activates the
  document that asked (by uid) and Save Changes finds its document by
  uid; a frame guard clears a stale picks entry after a recovered panic.
- ui.inspect reports fileDialog.

Squashed onto current main by the maintainer (with main's automation
sandbox wrappers and font-name menus kept).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:14:17 +07:00
bflatasticandClaude Opus 5.5 525b79a51a #1111: Ctrl+Shift+L, C, R, J and F align paragraphs
The paragraph alignment shortcuts were missing. Cmd/Ctrl+Shift+L, C and
R align the selected type's paragraphs left, centred and right, J
justifies them with the last line aligned left and F justifies all
lines; while the Type tool edits text they act on the paragraphs its
selection touches, through the Paragraph panel's own path. They are UI
commands (type.alignLeft, …), listed in Keyboard Shortcuts, and enabled
while type is selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:58:51 +07:00
dotCooCoo 8e4158b364 Align: Align to Artboard uses the active artboard
Align to Artboard aligned the selection to the artboard under its
center, or to the first artboard when no artboard was under the
center. A press with a selection tool inside an artboard makes that
artboard the active one (#693). With the Selection tool, a
Shift-click on an empty spot inside an artboard also makes it active
and keeps the selection. Align to Artboard did not use the active
artboard.

object.align takes artboard, a 0-based artboard index, and uses it
when its to param is "artboard". The app passes its active artboard,
the one the Artboards panel highlights, to each object.align it runs
that names none, as it does for Paste in Place, Paste in Front, Paste
in Back (#693) and Select › All on Active Artboard (#1006). MCP calls
in remote mode also get it. Without artboard (headless MCP and the
CLI), or with an index the document doesn't have, object.align aligns
to the artboard under the center of the selection, or to the first
artboard when no artboard is under the center.

command.batch takes artboard too, and the app passes it the active
artboard. The batch passes it on to each step of these five commands
that names none, when the step runs. When the document a step runs in
has no artboard at that index (for example, after an earlier step
deleted the active artboard when it was the last one), the step gets
that document's last artboard. A step in a document that an earlier
step opened, created or switched to gets the same index, not that
document's own active artboard.

With Align to Selection, one selected object aligned from the Align
panel, the Control bar or the Properties panel is aligned to the
active artboard too, unless it is the key object.

Recorded actions leave out the artboard that the app passes to these
commands and to the batch. The Actions panel plays an action as a
batch. Its Align to Artboard, All on Active Artboard and in-place
paste steps that name no artboard use the artboard that is active
when it plays, and the default Center on Artboard action now centers
on the active artboard. In-place paste steps recorded since #693 and
before this change still paste onto the artboard that was active when
they were recorded.

Tests cover object.align with and without artboard on three artboards;
a Shift-click that makes an artboard active, after which Align to
Artboard aligns to that artboard but keeps an artboard the call names;
a batch that passes its artboard to each step when the step runs,
after a step deletes the last artboard and in a new document; recorded
actions that leave the artboard out; and recorded Align to Artboard
and Paste in Place steps and the default Center on Artboard action,
which play on the artboard that is active when they play.

Docs: docs/mcp.md (Align to Artboard), docs/target-app-parity.md.
2026-10-11 11:33:01 +07:00
dotCooCoo a8eb9024da Align: a key object needs two or more selected objects
Selecting two objects, clicking one to make it the key object and then
Shift-clicking the other left the remaining object as the key. The
canvas still drew that object with the thick key-object outline. The
Align panel aligned the object to itself, so the object did not move.

The key is now cleared after any change that leaves no other selected
object outside it, such as a Shift-click, or hiding or deleting the
other object's row in the Layers panel. On a single selected object,
or on a group selected with only its own members, select.key sets no
key.

With one object selected and Align to Key Object chosen, the Align
panel, the Control bar and the Properties panel ran object.align with
to: "key", which fails with "no key object" when the selection has no
key. All three now align a single selected object to the artboard
whatever Align To is set to.

Docs: docs/target-app-parity.md.
2026-10-11 11:32:59 +07:00
bflatasticandClaude Opus 5.5 c4e139af19 #885: every checkbox keeps a visible box at rest
The theme leaves egui's own checkbox without an outline while inactive,
so an unchecked box was invisible and a checked one a bare tick. #1080
moved Preferences to widgets::check; the remaining ui.checkbox uses
follow: the Properties panel's Snap to Point, Snap to Grid and Scale
Strokes & Effects, Revolve's Keep visible surfaces and Shading, Save
options, Export for Screens, the Variables dialog and plug-in forms.

widgets::check_name shows a document's name (an artboard's, an asset's)
as it is, never translated; the check and radio rows translate their
labels in the public functions, so choice_row draws text as given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:27:46 +07:00
skylar taylor-barrick 0c57b1d5d2 UI.6: reconcile shared docking with native panel sets 2026-10-10 19:28:31 -06:00
imachusandClaude Opus 5.5 621e1a6640 Type: Proportional Metrics — full-width glyphs on the font's proportional widths, palt (#966)
A character attribute (Character panel menu with the East Asian options;
`text.setFormat` / `text.setRangeStyle {proportionalMetrics}`), saved only
when on. In horizontal type it applies `palt`; vertical type sets as before
until the vertical shaping change (`vpal`) follows.

Line-end Punctuation Half Width takes nothing more off the punctuation
`palt` re-spaced (the flush opening bracket, consecutive punctuation, the
line-end half width): the segment is shaped once more without `palt`, and
the glyphs whose advance differs are marked. Punctuation the font leaves
full width is trimmed as before. SVG writes `"palt" 1` with the
character's other features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:00:35 +07:00
lollinoo c5274beb23 #1034: scaled type shows and takes its scaled sizes
After scaling 12 pt type 2x, the Character panel still showed 12 pt and
typing 10 pt gave 20 pt type. Character controls and text commands now
work in document points: the size, leading and baseline shift shown and
set are the stored ones times the text's scale along its em height, and
horizontal scale includes the ratio of the transformed em axes. Rotation,
reflection and shear stay in the transform, so text remains editable.
Groups' text counts, keyboard increments use the same units, Shrink
Text to Fit works in document tenths, and a collapsed transform rejects
dimensional edits without touching the document.

text.getRange takes effective: true for the document-space values.
2026-10-11 07:00:34 +07:00
bflatasticandClaude Opus 5.5 c183c47df3 Review: guides on hidden or locked layers, and through layer commands
- Guides on a hidden or locked layer leave the selection (Selection::prune,
  run after every edit and selection change), their Layers rows neither
  select nor drag them, and guide.move / guide.remove / guide.setLayer
  refuse them; guide.setLayer refuses a locked target layer, as dragged
  rows are refused there.
- A new guide goes where new art goes (the current layer, else the top
  shown and unlocked one; refused when there is none), so it never lands
  on a hidden layer and vanishes.
- Layer commands keep guides with their layers: Duplicate (and Alt-drag
  copies of rows) copy a layer's guides onto the copy, Merge Selected and
  Flatten Artwork put the merged layers' guides on the merged layer and
  Flatten discards a hidden layer's. One shared doc predicate for "the
  layer a guide is on"; the deleted-layer cleanup moves into the doc.
- Tests: engine tests for the above and their undo, a doc check that an
  old guide reads with no layer and none is written, and a headless
  Layers panel test of guide rows (select, drag to another layer, locked).
- Translations for the new message; docs and a ROADMAP progress line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:03:48 +07:00
redrun6767andClaude Opus 5.5 2972e2e6bf Guides: drag guide rows to another layer; put older guides on a layer
Guide rows in the Layers panel drag (the selected guides when the row is
one of them) onto any row of another layer, which lights up, and go to
that layer in one undo step (guide.setLayer takes indexes). Guides on no
layer, from files saved before guides had layers, go on the top visible,
unlocked layer when a document opens or is reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CC5DiGJEZ5ADaQMBEQk4W1
2026-10-11 06:03:47 +07:00
redrun6767andClaude Opus 5.5 02e93902b3 Guides: ruler guides as layer objects
A ruler guide goes on the current layer (guide.add {layer}), shows,
hides, locks and is deleted with its layer, is listed in the Layers panel
as a row that selects it, and moves to another layer (guide.setLayer).
Guides on a hidden layer neither show nor snap; those on a locked layer
can't be picked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CC5DiGJEZ5ADaQMBEQk4W1
2026-10-11 06:03:47 +07:00
mateususaandClaude Opus 5.5 580ada65d5 #1026: Properties › Transform reaches Scale Corners and Scale Strokes & Effects
The Transform panel has both options; the Properties panel's Transform
section had no way to them. A More Options button after the flip buttons
now opens a popup with the two (the scaleCorners / scaleStrokes prefs),
open while they are toggled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:30:24 +07:00
mateususaandClaude Opus 5.5 0eb1e463b9 #1026: the flip buttons' icons match their flips
Lucide names its flip icons for their mirror line: flip-horizontal-2
draws a horizontal line with triangles above and below (the top-bottom
flip). The Properties and Transform panels showed it on the left-right
flip, and the other on the top-bottom one. Swapped back, with a comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:30:24 +07:00
bflatastic cacc9e4f36 #1006: Select > All on Active Artboard selects on the active artboard
The menu item sends no artboard, so the engine fell back to the first one even with another
active in the Artboards panel.

The frontend fills in the active artboard (the view's, as the Artboards panel and navigator
show it) for select.allOnArtboard, as it already did for Paste in Place / in Front / in Back
(#693); the two now share one block, and the index is kept within the artboards. An explicit
artboard still wins; the engine is unchanged.

The test makes artboard 2 active, then 1, and invokes the menu item each time, then passes
artboard 1 explicitly with artboard 1 active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	ROADMAP.md
2026-10-11 03:51:27 +07:00
skylar taylor-barrick ed14640756 UI.9: integrate shared docking and preserve panel workflows 2026-10-10 14:49:35 -06:00
bflatasticandClaude Opus 5.5 9e09d53fd9 Review #1013: the stepper test from #1015 compiles and hits the spinner
The headless test taken from #1015 set `RawInput::modifiers`, which egui
0.36 doesn't have, and clicked left of the spinner (the right-aligned
label's text ends before its box). Modifiers now come as
`ModifiersChanged` events, as the Appearance panel tests send them, and
the click is placed from the field's text. It also checks Ctrl/Cmd steps
a tenth. It fails with the spinner ignoring modifiers (15 pt for 24).
rustfmt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:14:00 +07:00
Nigel Ihza Firdausy 6d5fdc8a2a test(ui): Shift-click on the Stroke weight stepper steps 10 pt (#991)
The headless panel test of #1015, kept on top of #1013's fix (#1015's
own change to the spinner is superseded by it).
2026-10-11 02:06:41 +07:00
bflatasticandClaude Opus 5.5 d0015a496b variables: Window › Variables opens the panel
As Window's other panel items do, and as the reference's Window menu has
it, Window › Variables is the panel itself instead of a submenu of
eighteen commands. The panel menu gains Make Text Dynamic, Make
Visibility Dynamic, Unbind Variable and Delete Variable beside the
bottom bar's buttons, which share one reading of what the selection
allows; every command stays in the command palette. The labels only
the submenu used leave the catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:34:21 +07:00
bflatasticandClaude Opus 5.5 500e19680d variables: review fixes for the panel, dialogs and translations
- Dataset and variable names are the user's: the panel's Data Set dropdown and
  the dialogs' name dropdowns show them as they are instead of translating
  them; the dialog's Kind dropdown shows translated kind names.
- Choosing the empty option in a dialog dropdown picked the first name.
- The panel reads the art once per frame for every row's italics, the dialog
  no longer clones the document's variables every frame, and slices are
  matched instead of indexed.
- "No document" and "Define a variable first" go through tl!, and the dialog
  headings Variable Options / Edit Data Set, Text and Visibility have rows in
  every complete catalog; the unused "{name} · {kind} · {count}" row is gone.
- German rows for every Variables string (the catalog predates this branch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:34:21 +07:00
Fahri Meral f2252b0d9c variables: highlighted row owns Delete, Options and Select Bound Object
Select Bound Object ran variable.unbind on the selection, so choosing it silently
removed the bindings; Delete Variable and Delete Data Set deleted every row. Now
the panel keeps a highlighted variable row (a click highlights and selects what
it drives, as the reference panel does): Select Bound Object selects what the
highlighted variable drives via select.set, Delete Variable deletes only it, and
Delete Data Set deletes only the current set. Variable Options and Rename Data
Set open their dialogs preselected with the highlighted variable / active set
(the double-click used to invoke variable.rename with a bare name, which only
errored). Unbind enables only over a bound selection; Make Text Dynamic only for
exactly one type object.

The highlight is per-document session state with its own command
(variable.highlight, no undo step, like layer.highlight): rename carries it,
delete clears it when stale. New engine tests (18) and panel tests cover the
reviewer's regression case, verified to fail on the old wiring.
2026-10-10 23:34:21 +07:00
Fahri Meral 06dbe5b1e8 variables: put the panel in the collapsed icon column
Opening the panel from the Window menu worked, but collapsing the dock did not
show it: `ICON_PANELS` only registers a panel (so `window.panel` finds it) while
`ICON_PANEL_GROUPS` is what the collapsed column actually draws, and `variables`
was in the first list only. Sixteen panels are registered without a group; for
the ones meant to live in the column, the entry in `ICON_PANELS` alone is not
enough.

`variables` gets a row of its own at the bottom of the column, where the
reference app keeps it. Both lists now say which is which, so the next panel
does not repeat this, and a test asserts a registered panel reaches the column
(it fails without the group entry) and that every grouped id names a registered
panel with an icon.

`floating.rs`'s icon test counted the column's icons, which only means
"one left" while the column is tall enough to show them all — past that, one
scrolling into view replaces the one that left. It runs on a window tall enough
for every icon now, and checks the count is the number of registered groups'
icons rather than a number that happens to pass.

Dragging a panel into the icon column still doesn't work, and is not something
this PR changes: tearing a panel off makes it float, for every panel. Docking it
back into the column would be a dock feature of its own.

cargo xtask ci: all 7 steps pass on Rust 1.99.
2026-10-10 23:34:21 +07:00
Fahri Meral 5a638b0a80 variables: the panel does the workflow itself, and rows can be captured
The engine side was called a solid base, but the panel could only list and
apply: variables and datasets were made through commands, which most people
won't find. The reference app's Variables workflow is now what the panel does,
and every button runs a `variable.*` / `dataset.*` command, so the panel and an
agent drive the document the same way.

New commands (all also in Window › Variables, the palette and over MCP):

- `variable.makeTextDynamic` / `variable.makeVisibilityDynamic`: define a
  variable of that kind over the selection, named after what it drives, and
  bind it — one step, as the panel's two buttons.
- `dataset.capture`: record what the bound objects hold right now as a new row
  ("Data Set 1"… ) and make it active. This, rather than typing values, is how
  rows are made: change the art, then capture.
- `dataset.update`: write what the art holds now into the active row. Without
  an active row it says so rather than picking one.
- `variable.rename` / `dataset.rename` (Variable Options…, Rename Data Set…):
  a name is how a variable is referred to, so renaming carries its bindings and
  its values in every row with it.
- `dataset.list` reports `matches` per row: whether the art still holds what
  that row says.

`Document::captured_values` is the one place that reads a bound object's value,
so capture, update and the drift check can't disagree about what the art says.

Panel: the current data set across the top as a dropdown with ◀ ▶; variable
rows with their kind's icon and the name of the object they drive ("Multiple
objects" when it is more than one), a click selecting it and a double click
opening Variable Options; a dataset row in italics once an edit has moved the
art away from what it says; Make Text/Visibility Dynamic, New, Unbind and Delete
below, Capture and Update on a second line; and a (≡) menu with the same
commands for the ones that act on a row rather than the selection.

Checked on Rust 1.99 (the current stable), which is what the review asked for:
cargo xtask ci, all 7 steps pass.
2026-10-10 23:34:21 +07:00
Fahri Meral 202894372a variables: fix binding lifetime, dataset editing and the missing UI to define
Review of #761 turned up three real defects and a feature that was visible
but not usable. All of it is fixed here, each defect with a regression test
that fails without the fix.

- A binding outlived the object it named. Deleting art left its entry in
  `variables.bindings` forever: it counted as a skip in every dataset
  application, `variable.list` counted it, and the saved file grew with every
  delete. `Document::prune_variable_bindings` now lets them go after each
  edit, as `prune_assets` does for Asset Export's.
- `Variables::prune` cleared `active_dataset` by comparing it against the
  *variable* names being deleted. A dataset named like a variable lost its
  active state when the variable went. The two are separate namespaces; the
  block is gone (dataset.delete already did it right).
- `variable.delete` reported the number of names asked for, not the number
  deleted, so deleting a name that was not there still said `deleted: 1`.

- `dataset.set` (new): a dataset's values could only be set when it was
  created, so changing one meant deleting and rebuilding the row — losing its
  place in the list and the active one. This replaces the row's values and
  reports what it dropped.
- Window › Variables, and dialogs for the "…" commands. Every command had an
  empty menu path and the command palette runs those with `{}`, so nothing
  could be defined from the UI at all. The commands now carry their menu
  path, the "…" ones open a Variables dialog (a name and a kind; a name and a
  field per variable, its kind's editor), and `dataset.set` opens with the
  row's own values.
- Locked art is neither bound nor rewritten by a dataset, and says so.
- `set_plain_text` told a node that existed but was not type that it was
  missing.
- The panel had Document Info's icon, so the two were indistinguishable in
  the collapsed column; it gets its own, plus New/Delete buttons, and its
  rows say what their count is.

Format round trips now cover variables (the testkit fixture defines both
kinds, binds them and applies a row), and ROADMAP/README no longer list the
feature as missing.

cargo xtask ci: all 7 steps pass.
2026-10-10 23:34:21 +07:00
Fahri Meral ba2069c057 variables: panel, window menu entry and translations 2026-10-10 23:32:59 +07:00
seldoms f0349fb2f8 i18n: wrap hardcoded UI strings for Chinese localization
- Wrap remaining hardcoded UI labels and buttons with tr()/tl!() calls
- Add missing Simplified and Traditional Chinese translations
- Add entries to other language catalogs for consistency
2026-10-10 18:27:56 +07:00
bflatasticandClaude Opus 5.5 2bc6c66c94 Part of gap 5: Scatter, Art and Pattern Brush Options
Double-clicking a Scatter, Art or Pattern brush in the Brushes panel (or the
panel menu's Brush Options...) now opens its options dialog, as Calligraphic
and Bristle brushes already did. OK runs brush.options and every stroke
painted with the brush redraws.

- Scatter Brush Options: Name; Size (10-1000 %), Spacing (10-10000 %),
  Scatter (-1000-1000 %) and Rotation (-180-180 deg), each Fixed (the first
  value), Random (between the two values) or Pressure (the first value at
  the lightest pen pressure, the second at the heaviest); Rotation relative
  to Page or Path. Scatter brushes now keep these modes (`modes`); brushes
  saved before keep their look (Random where the two values differ).
- Art Brush Options: Name; Width (1-1000 %); Scale Proportionately, Stretch
  to Fit Stroke Length or Stretch Between Guides with the guides' Start and
  End (% of the art length); the four Directions; Flip Along and Across.
- Pattern Brush Options: Name; Scale (1-10000 %), Spacing (0-10000 %); the
  five tiles (Outer Corner, Side, Inner Corner, Start, End) drawn with their
  art or None; Flip Along and Across; Stretch to fit, Add space to fit or
  Approximate path.
- All three: Colorization (None, Tints, Tints and Shades, Hue Shift with its
  Key Color picked from the art's colours, the most used one by default) and
  a live preview of a stroke painted with the edited brush.

Brush definitions from files and commands are now kept within these ranges
(Brush::sanitize, also for Calligraphic and Bristle values); unknown modes,
directions, fits or colorization methods are refused. The brush.options and
ui.brushOptions docs list the new params and fields. Tests: the brush model
(modes, pressure, sanitizing, art colours), engine params reaching the
brush and strokes redrawing, junk params in command_sweep, and headless
dialog tests for each dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:48:05 +07:00
bflatasticandClaude Opus 5.5 d62ecfe33a Fix #926: the Libraries panel's search, groups and library export/import
Search: a search field at the top of the Libraries panel filters the
current library's colours, character and paragraph styles and graphics
by name, ignoring case; groups without a match are hidden, groups with
one open, and a search nothing matches says so ("No results").

Groups: items can be put in user-named groups, kept in the library file
(`groups: [{name, items: [{kind, item}]}]`, left out when there are
none, so files written before groups load as they are). New commands:
library.createGroup {library?, name?, items?}, library.renameGroup
{library?, group, name}, library.deleteGroup {library?, group} (its
items stay, ungrouped) and library.moveItem {library?, kind, item,
group?}. An item is in one group at most; deleting an item takes it out
of its group. The panel shows each group as a collapsible section with
its item count after the ungrouped items (rename and delete on its
context menu), each item's context menu has Move to Group (No Group,
the groups, New Group...), and a New Group button sits in the footer.
library.get lists the groups, library.list counts them. The generic
command dialog takes fixed params in `__params` for this.

Export / import: library.export {library?, path?} writes a library
(colours, styles, graphics with art and thumbnails, groups) as a
.vclibrary file, the JSON the library folder keeps (without a path:
{data}); library.import {path | data | dataBase64, name?} adds one as a
new library, made current, numbered on a name clash ("Brand (2)").
The panel menu has Import Library... and Export Library..., through the
app's file dialogs (the web downloads, and picks through File > Open,
which now lists Libraries files). Imported and folder files are made
safe: at most 10,000 items of a kind and 1,000 groups, names clipped and
made unique, sizes finite, thumbnails kept only when their PNG header
says at most 1024 px a side, group entries naming missing items or
unknown kinds dropped; a file over 512 MB or that isn't a library is an
error. Paths go through the engine's file helpers (automation roots).
The importer is fuzzed (import_fuzz: garbage and mutated libraries).

Also fixed: the third graphic added to a library got the second's id
(`graphic-2` twice).

Docs: the commands' params docs, docs/mcp.md, ROADMAP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:17:01 +07:00
bflatasticandClaude Opus 5.5 b3ed7df2a4 Fix #921: the Properties panel's Gradient section with Type and Draw
With the Gradient tool active, the Properties panel shows a Gradient
section: the type buttons (Linear, Radial, Freeform) and, for a
freeform gradient, Draw: Points or Lines as radio buttons. The Gradient
panel and the Properties panel share the type row and the draw mode
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:50:23 +07:00
bflatasticandClaude Opus 5.5 2ef32bb1a2 Fix #924: the Tabs panel's Leader and Align On keep what is typed
Both fields read their text from the tab stop again every frame, so each
keystroke was lost before Enter or a click away could apply it: the
field seemed to clear itself while typing. They now use the panels' text
field, which keeps the text being typed until it is committed. The
leader keeps spaces (". " for spaced dots), up to eight characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:49:49 +07:00
Rick 934b65b42a M11: honor Move Artwork in artboard coordinate edits 2026-10-10 09:49:24 +02:00
bflatasticandClaude Opus 5.5 8d2a3f48a1 Fix #909: a Width tool profile's preview fits the Profile menu
The Profile menu's preview drew a stroke's width profile assuming its
widths stay within the stroke weight. A profile made with the Width tool
can be many times wider, so its silhouette spilled over the Stroke panel.
It is now scaled to the preview by its widest point and clipped to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:52:41 +07:00
BFlat bd439727dd Merge pull request #914 from storytold/feat/812-tool-options
Part of #812: polygon properties, Flare Tool Options, more tool double-clicks
2026-10-10 13:23:32 +07:00
bflatasticandClaude Opus 5.5 7c0ce8a382 Part of #812: polygon properties in the Properties and Transform panels
A selected live polygon now shows Illustrator's Polygon Properties in the
Transform panel and the Properties panel's Transform section: side count,
angle, corner type and radius, radius, side length and Make Sides Equal
(enabled once an uneven scale has made its sides differ). Before, only the
side count and corner radius were there.

object.setLiveShape takes polygonAngle (degrees counterclockwise),
polygonRadius and sideLength (points, centre to vertex / side) and
makeSidesEqual, which drops the uneven scale and shear and keeps the
polygon's centre, angle and mean radius. Each call is one undo step; a
radius or length that isn't a number above 0 is an error. A star stays a
plain path, as in Illustrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:00:21 +07:00
bflatasticandClaude Opus 5.5 980cc4fe44 Fix #901: Shift-click a target circle in the Layers panel to add or remove it
Clicking an object's target circle in the Layers panel always replaced
the selection, so several objects couldn't be picked there. Shift-click
now adds the object (a layer: its art) to the selection, or takes it out
when it is selected already, as in Illustrator; nothing stays targeted
then. layer.target takes add: true for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:55:38 +07:00
bflatasticandClaude Opus 5.5 5cf9233d26 Part of #852: Brush Options for Calligraphic and Bristle brushes, pen pressure
Double-clicking a brush in the Brushes panel (or the panel menu's Brush
Options…) opens its options. Calligraphic Brush Options: Angle,
Roundness and Size, each Fixed, Random or Pressure with its Variation,
over live previews of the nib and of a stroke. Bristle Brush Options:
Shape and the bristle sliders. OK runs brush.options; strokes painted
with the brush update.

The Paintbrush records the pen pressure along a stroke (path.freehand
takes [x, y, pressure] points), and a Calligraphic brush with Pressure
variation changes its nib along the stroke, from the value less the
variation at the lightest pressure to the value plus it at the heaviest.
Brushes saved before keep their Random variation. Pen pressure doesn't
reach the app on macOS yet (winit 0.30 reports none there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:15:18 +07:00
bflatasticandClaude Opus 5.5 61d372bb7f Fix #775: the Properties panel's Paragraph section justifies and hyphenates
With type selected, the Properties panel's Paragraph section had only
Align Left, Center and Right. It now has the Paragraph panel's seven
alignments (the justified ones too), its Hyphenate checkbox and a "…"
button that opens the Paragraph panel, as Illustrator's does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:27:56 +07:00
bflatasticandClaude Opus 5.5 82dc5f8182 Fix #789: every align button aligns to the Align To choice
Align to Key Object and Align to Artboard worked only from the Align
panel's own buttons: the Control bar's and the Properties panel's align
buttons always aligned to the selection (one object to the artboard).
They are now one set of buttons that follow Align To (and the key
object), and the Properties panel's Align section gets the "…" button
that opens the Align panel for Distribute and Align To.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:27:54 +07:00
bflatasticandClaude Opus 5.5 79c81d754c Fix #835: a recent colour recolours the selected gradient stop
In the gradient stop popover (and the Color panel), clicking a recent
colour replaced the whole gradient with that colour, while the sliders,
spectrum and Black and White chips recoloured the selected stop. The
recent colours now go through the Color panel's target like the chips.
The Swatches panel's recent colours still paint the fill or stroke, as
its swatches do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:27:54 +07:00
dotCooCoo 5709e636a5 Fonts: missing fonts are listed when a document opens, with Find Fonts and a folder search
When a document opens with fonts that aren't available, fonts used
only in its symbols included, VectorCraft lists them in a Missing
Fonts dialog after the missing linked file questions, one document at
a time, with Find Fonts and Close. Find Fonts opens Type › Find Font
on the first missing font of the layers' type, to replace it. After
Find Fonts or Close the dialog doesn't open again for that open; when
another dialog replaces it, it opens again once that dialog closes.

Search Fonts Folder (the Fonts folder File › Package writes next to
the document) and Find in Folder… search a folder and its subfolders
for the fonts' files, and Add Fonts copies the files the user chooses
into a Fonts folder next to the preferences. The desktop app and
vectorcraft-cli read that folder with the system's font folders. Type
› Find Font has a Find in Folder… button too when fonts the document
uses are missing. The web app shows no dialog; a status message says
how many fonts aren't available.

- text.missingFonts lists each missing family and style once,
  whatever version the document's type names, and leaves out CSS
  generic families such as sans-serif. text.findFontFiles searches a
  folder on up to 8 threads and returns at once; text.findFontFiles {}
  reports the search, and {stop: true} stops it. A search reads only
  what the font scan reads of font files (their table directories and
  name, fvar and OS/2 tables) and, on macOS, of suitcase fonts, and
  lists a file for a font only when the file makes the font resolve
  exactly. It does not enter app packages, the Program Files and
  ProgramData folders, the system's folders at the root of a volume,
  the Shared and Public folders in Users, a home folder's Library,
  AppData, Applications, snap and dot folders, a Library folder's app
  data folders or the folders the environment names for apps, and it
  skips other hidden folders and does not follow links below the
  picked folder. It ends after 60 s by default, after 10,000,000
  entries listed, with more than 1,000,000 folders waiting, after
  10,000 font files read or 1,000 kept, or once every font has a file;
  Stop and Close end it too.
- text.addFontFiles copies only files the last search found. Of
  those, it copies only regular .ttf, .otf, .ttc, .otc, .woff and
  .woff2 files and suitcase fonts with their resource fork, at most
  256 MB a file and 1 GB in all. It never replaces a file: a file with
  the same contents is kept, and when another file has that name, the
  copy gets a number (Name 2.otf). The fonts are then scanned again,
  and type set in them redraws.
- ui.missingFontsDialog and ui.findFontsInFolder open the dialog for
  agents; docs/control-protocol.md lists its fields.
- File › Package gives a font file whose name has no font extension
  the extension that matches its data (.ttf, .otf or .ttc).
- vectorcraft-text adds set_app_font_dir, which system_font_dirs
  reads, WantedFonts, which reads a font file's names as the font scan
  does, is_font_file and is_suitcase. #762 split its face reader, which
  now takes read budgets: file_face_names_within tries the suitcase
  reader first, and reader_face_names_within applies the budgets to
  each font. vectorcraft-format adds write_new_with.

Docs: docs/mcp.md, docs/control-protocol.md, ROADMAP.
2026-10-10 08:42:32 +07:00
bflatasticandClaude Opus 5.5 78f0297127 Part of #812: an ellipse's Pie Start and End Angle and Invert Pie
An ellipse kept pie angles in its model, but nothing drew or set them. Now
the Transform and Properties panels show Ellipse Properties with Pie Start
Angle, Pie End Angle and Invert Pie, as in Illustrator, and
object.setLiveShape takes {pieStart, pieEnd, invertPie}. The pie runs
counterclockwise on screen from the start angle (0 at 3 o'clock) to the
end angle and closes through the centre; 0 to 360 is the whole ellipse,
an end of 0 means 360, and Invert Pie swaps the angles to show the other
part. One undo step each; a bad angle is an error. The labels are
translated in every complete language. The on-canvas pie widget is still
to come.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:00:15 +07:00
BFlat cbacb47fdb Merge pull request #770 from dotCooCoo/feat/swatch-libraries
Swatches: Save Swatch Library writes swatch exchange (.ase) files
2026-10-10 01:53:32 +07:00
bflatasticandClaude Opus 5.5 4b44265965 Fix #734: Crop Image shows a crop box on the image
Object › Crop Image and the image's Crop Image buttons (Control bar,
Properties) now show a crop box on the selected image, as Illustrator
does, instead of cropping straight to the artboard (which did nothing for
an image inside its artboard). The box starts on the image's part over its
artboard; its handles and its inside drag it, always within the image, and
the image outside it is dimmed. The Control bar shows its X/Y/W/H with
Apply and Cancel; Enter applies and Escape cancels, both back to the
Selection tool. Apply runs object.cropImage {rect}, one undo step.

- tools: the Crop Image tool (cropimage.rs), with crop_box shared by the
  tool and the Control bar.
- ui: ui.cropImage (enabled with an image selected), the Control bar's
  crop fields and buttons, "Apply" translated (es, fr, it, ru, uk, ja,
  zh-hans, zh-hant).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:40:12 +07:00
dotCooCoo a61d058f1b Swatches: Save Swatch Library writes swatch exchange (.ase) files
The Save Swatch Library dialog lists Swatch Exchange (.ase) as a
format, and swatch.library.save takes format: "ase" or a path ending
in .ase. Each solid color keeps its model (RGB, CMYK, Lab or Gray) and
its type (spot, global or process), and color groups stay groups.
Gradients and patterns are left out, and a tint swatch is written as
the color it shows. Without a path or user: true, the command returns
the file as dataBase64.

A swatch exchange library opens under the name of its file. When Save
Swatch Library or Save Graphic Style Library saves to a file, the save
panel suggests the Name from the dialog as the file name, as a save to
User Defined does, and the web app downloads the file under that name.
A name made only of dots and spaces, such as "...", gives the file name
Library (Library.vcswatches) in place of a hidden file.

palette_io::write returns the file's bytes. A property test writes
generated libraries and reads them back, and an import fuzz test loads
mutated copies of a file the writer wrote.
2026-10-09 08:15:09 -07:00
bflatasticandClaude Opus 5.5 7255ac394d Fix #745: a working Libraries panel of graphics, colours and text styles
The Libraries panel was a placeholder: it invited dragging art onto it,
but nothing happened. It now keeps local libraries (no account, no
cloud), as Illustrator's Libraries panel does:

- drag art off the canvas (or a swatch) onto the panel to add it, or use
  + to add the selection's graphic, fill or stroke colour, character
  style or paragraph style;
- drag a graphic onto the canvas (or double-click it) to place a copy,
  click a colour to paint the selection through the active Fill/Stroke
  proxy, click a style to apply it to the selected type;
- the library picker and the panel menu create, rename and delete
  libraries; each is a .vclibrary file in a Libraries folder next to the
  preferences (session-only in the web app, headless sessions and runs
  without preferences).

Engine: library.list/get/setCurrent/create/rename/delete/add/use/
removeItem. A graphic is stored as a native document with a thumbnail:
adding copies as Copy does and placing pastes as Paste does, resources
and swatch-name conflicts included. Using a style adds it to the
document and applies it in one undo step.

Clipboard::to_document gave its layer the id u64::MAX, which made a
saved copy overflow Document::fix_next_id when read back; the layer now
takes an id above the objects', and fix_next_id saturates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:12:59 +07:00
HTandClaude Opus 5.5 f2b35fc039 Fix #602: Scale Artwork with Artboard scales the art with a resized artboard
`artboard.setProps {scaleArt: true}` maps the art fully inside a resized
artboard (as Move Artwork with Artboard gathers it) and its artboard guides
from the old rectangle onto the new one, each side by its own ratio, in one
undo step, and returns the ids scaled. Strokes, effects and corners follow
Scale Strokes & Effects and Scale Corners (`strokes`, `corners`), pattern tiles
Transform Pattern Tiles (`patterns`); locked and hidden art follows
`lockedAndHidden` (default: prefs moveLockedWithArtboard). The resolved choices
are journaled, so a replay scales the same objects the same way. A move that
keeps the size scales nothing.

The Artboard tool's `scaleArt` option (saved with `moveArt`) makes handle
drags proportional, as the public help describes, keeps them proportional at
the 1 pt minimum, and leaves the scaled art out of the Smart Guide targets.
Move and Scale Artwork with Artboard are check boxes in the Control bar and
Properties' Edit Artboards; the artboard size fields there and in the
Transform panel pass the option. Translated in the eight catalogs that have
Move Artwork with Artboard. docs/mcp.md and the ROADMAP updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:50:35 +01:00
bflatasticandClaude Opus 5.5 ee16d10fd4 Fix #724: Type > Bold and Italic, Cmd+Shift+B / Cmd+Shift+I while typing
Designers coming from page layout apps expect Cmd+Shift+B to bold the
text they are editing. Type > Bold and Type > Italic switch the selected
type (or the Type tool's selection) to its family's own Bold or Italic
face, and back to the regular weight or upright face when it already is,
keeping the face's other words (a Narrow face stays Narrow). There is no
fake bold or slant: a family without the face is left as it is and the
status bar says so. While the Type tool edits text, Cmd+Shift+B and
Cmd+Shift+I apply them; elsewhere Cmd+Shift+B still hides the bounding
box. Both can be given other shortcuts in Edit > Keyboard Shortcuts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:20:58 +07:00
bflatasticandClaude Opus 5.5 c16804237e Fix #729: the Tabs panel's Above Text puts its ruler over the text
The Tabs panel's ruler was a fixed strip in the panel, at no relation to
the text on the canvas, so stops couldn't be placed by eye. Above Text
(Position Panel Above Text) floats the panel just above the selected
text, as wide as its ruler needs to put 0 on the text's left edge and
its marks at the canvas zoom: each stop sits over the column it sets.
Floating panel groups keep a width they are given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:38:49 +07:00
BFlat 9f7526aee6 Merge pull request #721 from imachus/pr/ja-kinsoku-set
Type: Kinsoku Set — Hard (today's set), Soft and None (#633)
2026-10-09 15:51:18 +07:00
imachusandClaude Opus 5.5 806e543fd1 Type: Kinsoku Set — Hard (today's set), Soft and None (#633)
Paragraph panel › Kinsoku Set (with the East Asian options) and `text.setFormat {kinsoku}`, as
decided in #633:

- Hard: today's set, so existing documents don't change; the default, and not saved.
- Soft: as Hard, except that 々, the prolonged sound mark ー (JLREQ cl-10) and small kana (cl-11)
  may start a line: JLREQ's level 3 line-breaking rules (Appendix C.3). Half-width forms aren't in
  those classes and stay off the line start.
- None: no kinsoku.

The setting reaches every break the composers consider (greedy, every-line and hyphenation
points). Reset Panel returns to Hard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:26:37 +09:00