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>
File › Place and File › Open read PSD and PSB files: the merged image
every such document carries after its layers, which is what a linked PSD
shows. A placed PSD links like any image, so the Links panel updates and
relinks it, and it is sized by its ResolutionInfo resource.
The reader is written from the public file format specification. It
reads Bitmap, Grayscale, Duotone, Indexed, RGB, CMYK (through the colour
settings' CMYK) and Lab, at 1, 8, 16 and 32 bits per channel (32-bit
values are linear), raw, PackBits or zipped with or without prediction.
A document with transparency stores its merged image on white with the
alpha in the first extra channel. It comes back with straight colours,
also for 16- and 32-bit files, which say so in their Lr16/Lr32 block.
Multichannel files are refused with a message. The file is untrusted:
every read is bounds-checked and the image's size is checked against
the decode budget before anything is allocated.
PSB is a read-only format of its own, so exports never write a PSD under
a .psb name. The desktop packaging still doesn't associate either
extension with VectorCraft (UNASSOCIATED_EXTS): they belong to raster
editors. Layers (import options) are left for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preferences › File Handling › Update Links only acted when a document
opened, so a linked image edited in another app while VectorCraft stayed
open kept its old pixels until Update Link in the Links panel, even with
Automatically chosen.
When the window comes back to the front, the active document's links
are now checked. With Automatically, changed files are read again; with
Ask When Modified, they are offered for update, once per change of a
file (keyed by path, size and modification time, since two saves can
share a time); with Manually, the Links panel shows them as modified.
Nothing is asked while a dialog is open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect › Distort & Transform › Transform… was a plain list of fields.
It now has Transform Each's layout: Scale and Move with sliders, Rotate
with an angle dial, then Copies, Reflect X and Y, the reference point and
Random, kept with the effect so editing it from the Appearance panel
shows them again.
- The effect scales, rotates and reflects about the chosen point of the
9-point grid (`reference`, the centre by default).
- Random takes a share of each value, drawn from the object's id: each
object varies its own way and keeps its result on every redraw; a
group's members move together.
- Copies stop before one reaches past 1e9 pt, and the scale and move are
capped, so large values can't overflow.
- Transform Each and the effect share the sections and options code; the
angle dial is a new widget.
- A plain number field now takes the focus when it is a dialog's first
field, so both dialogs open with Scale › Horizontal ready for typing.
Transform Objects, Transform Patterns and Scale Strokes & Effects are not
in the effect yet: the geometry effects reshape paths only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vectorcraft-cli mcp and vectorcraft --control take the flags PhotoCraft
takes. While roots are in force, every path a command reads (open, place,
relink, SVG image links, library and profile loads, plug-ins, preset
imports, font searches) must lie inside the read root and every path it
writes (save, export, package, Export for Screens and for Web, print to
file, captures, library saves) inside the write root. Paths are made
absolute, links are followed (a missing file's nearest folder's), and the
result is compared by whole components, ignoring case on Windows; `..`
below a missing folder, dangling links and device names are refused. A
root left out grants none of its access; without the flags nothing
changes.
The checks live in one place, vectorcraft_engine::file_access, consulted
by the engine's file helpers and the app's file services. The headless
server confines its whole process; the app confines control requests,
the frames carrying input they inject and the background saves they
start, leaving the person at the keyboard and the app's own folders
alone. The screenshot tool now has the backend write its file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Display Print Size at 100% Zoom divided an assumed 96 pixels per inch by
the display's scale factor, so on a scaled display (150%, Retina) a
document inch came out a fraction of an inch. Nothing measures the
screen's real density, so an inch is now 96 screen points, the reference
density display scaling is set against, at any scale factor. The unused
screen_ppi and pixels_per_point fields go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: register the MCP server from an installed release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: drop the third-party link; the Windows path is the default install folder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: drop the third-party link; the Windows path is the default install folder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: drop the third-party link; the Windows path is the default install folder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicking a tool's button (or tool.options {tool}) now also opens:
- Artboard tool: Artboard Options of the active artboard (the Artboard
tool's while it is in use, else the window's);
- the graph tools: Graph Type for the selected graph (an error without
one), the dialog Object > Graph > Type opens;
- the Symbolism tools: Symbolism Tools Options with the brush the eight
tools share (Diameter, Intensity, Symbol Set Density);
- Magic Wand: the Magic Wand panel.
The Object > Graph dialogs' opener is shared with the toolbar. The new
labels are translated in the eight complete catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Flare tool keeps its options (Center: Diameter, Opacity, Brightness;
Halo: Growth, Fuzziness; Rays: on/off, Number, Longest, Fuzziness; Rings:
on/off, Path, Number, Largest, Direction) as persistent tool options,
saved with the preferences. The next flare dragged out uses them (the drag
still sets the centre and diameter, and Up/Down the ray count), a click
with the tool opens them filled in, and double-clicking the tool (or
tool.options {tool: "flare"}) opens them to edit: OK keeps them and draws
nothing. Before, the click dialog was the generic parameter form with
fixed defaults and the drag always used the command's defaults.
The new Flare Tool Options dialog lays the four groups out in two
columns; Rays and Rings have their checkboxes, and every value is kept
within its range. Its labels are translated in the eight complete
catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The Curvature tool only edited the path it was drawing. It now edits any
selected path, whatever drew it, keeping its shape except where edited:
a click on a point makes it the current (direct-selected) point and a
drag moves it, re-curving only its two segments (the neighbours keep
their handle directions); Alt-click or double-click toggles it smooth or
corner and Delete removes it; a click on a segment adds a smooth point
without changing the shape, and a drag moves it in the same undo step.
With an end of an open path current, clicks go on from it (the old
segments keep their shape) and a click on the other end closes it.
The edits go through a new path.curvatureEdit command (move, insert,
extend, close), path.convertAnchor and path.removeAnchor, one undo step
each. The tool now claims Delete/Backspace while it has a point to
remove, so the key no longer deletes the whole path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run (vectorcraft-cli run, the MCP command_batch tool) had no way to use
an id an earlier step returned. A parameter string that is exactly
$N.path or $last.path now takes that value from step N's result
(0-based), e.g. {"id":"$1.id"}; $$ starts a literal $, and a
reference to nothing fails the step. Objects given to a command are now
used as given: an ids element or id that isn't an object id (or an
unresolved reference) fails instead of the command acting on the
selection. text.setRangeStyle styles the selected type object when no
id is given, as text.setStyle does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Color books such as the Pantone books a user owns (.acb) couldn't be
opened. They are now read (never written), from public descriptions of
the format: their RGB, CMYK or Lab colors, named with the book's prefix
and suffix ("PANTONE 185 C"), as spot colors unless the book marks
them as process colors, leaving out the pages' unnamed padding. They
open from Window › Swatch Libraries › Other Library…, File › Open and
the user library folder like .ase files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Creating a Vulkan instance loads every installed Vulkan driver into the
process, and a faulty one (Intel's igvk64.dll) crashed VectorCraft
before its window appeared. On Windows the window now starts with
Direct3D 12, falling back to OpenGL, as PdfCraft does; WGPU_BACKEND
(e.g. vulkan) still chooses others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Illustrator EPS and .ai type is made whole from the file's text
document, shown or hidden: point type, area type (a story in several
frames as threaded type that flows when the file opens) and type on a
path, with its fonts, size, leading, tracking, scaling, baseline shift,
fill and stroke colours (grey, RGB or CMYK), alignment, indents and
paragraph spacing. The page's drawing of that type, often in pieces, is
left out; where the text document can't say, type that shows still
comes from the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report kept a placed document's LINKED FILES and FONTS inside its parent's list. Each document
now gets its own sections, the packaged one first and unchanged (no Document: line), then each
placed document's, headed by its file. Drops a debug_assert and an unneeded mut, runs rustfmt, and
documents the nested packaging in docs/mcp.md and the ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add Fonts adds only files a folder search found.
- macOS declares .suit as the extension of a font suitcase
(com.apple.font-suitcase), and the font scan reads a suitcase
whatever its name. A search considered only suitcases without an
extension. It now also considers files whose extension is .suit, in
any letter case, and reads one only when it is a suitcase font.
- Add Fonts copies at most 256 MB a file. A search also read larger
font files, and a search that found one for a font could end before
it found a smaller copy. A search now skips font files over 256 MB.
Docs: docs/mcp.md.
A folder search reads at most 8 MB of each font file. The suitcase
reader reads a suitcase font's resource fork whole, up to 256 MB.
Before this change, the search applied its limit to each font only
after that read. One search thread could read 256 MB from a single
suitcase and hold a copy of its fonts as well. The search now reads a
suitcase font only when its resource fork is 8 MB or less. The font
scan still reads each suitcase font's resource fork whole, up to
256 MB.
Docs: docs/mcp.md.
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.
A letterhead saved by Illustrator 28 without PDF compatibility wouldn't
open: "its editing data can't be read (it ends inside a layer)". Its
layer starts with a /ForeignObject (non-native art: the content of a
placed PDF), whose /Data entry is the PDF as ASCII85 in comment lines.
ASCII85 includes `_`, so about one line in 85 starts with `%_`, which
the lexer reads as hidden tokens; a `(` in such a line opened a string
that swallowed the dictionary's end, the rest of the art and the
layer's LB.
The reader now skips to the data's `~>` after `/Data ,` in a
/ForeignObject, as it already did for a /Binary dictionary's ASCII85,
and leaves the non-native art out with a note (nothing draws its PDF
from the editing data yet).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZwZjADUZP4VzvrFGpiZch
- Fit to Artwork Bounds fits the art exports draw: hidden objects and
layers, guides and template layers don't count, and the bounds stop at
a layer's clip.
- The comparison with the page catches an object one draws that the
other doesn't (pixels whose neighbours all differ, away from type), a
light colour against white, and, for an EPS whose page is its art's
box, art the layers print outside it.
- textAs: outlines opens a .ai whose type shows from its PDF part, which
has the outlines.
- document.open takes editingData: false to open an Illustrator EPS or
.ai as its page or PDF part only, for print pipelines.
Part of #637.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a desktop with a Ryzen integrated GPU without a monitor and an NVIDIA
card driving both monitors, Automatic (power saving on Windows) picked
the integrated GPU; Windows presented every frame across adapters and
the integrated GPU's driver reset, losing the graphics device or taking
every monitor down for minutes (same bug as pdfcraft#378).
Automatic now asks the system which GPU drives which display (Windows:
EnumDisplayDevices through winsafe; Linux: /sys/class/drm) and ranks the
GPU driving a display, and among those the primary display's, ahead of
the power preference. Where the system can't say, or no adapter's PCI
ids match (OpenGL, Metal), the order is the power preference's as
before, so nothing gets worse. Power Saving and High Performance chosen
by the user, and WGPU_POWER_PREF, keep ordering by kind alone.
The log now lists the display GPUs, which adapters drive a display, and
the adapter drawn on with the reason, the first question in every
black-window report. Tests cover the device-id parser, the rankings
(desktop with all monitors on the discrete GPU, hybrid laptop, a monitor
on each GPU, no match), the sysfs reader and a live EnumDisplayDevices
call.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZwZjADUZP4VzvrFGpiZch
swatch.library.copyToUser {library} copies a library loaded from
another folder into the user library folder of the desktop app. A
.vcswatches, .gpl or .ase file is copied as it is; the swatches of a
document and a library loaded from the data or dataBase64 parameter are
written as a .vcswatches file. The copy gets a name that no file in the
folder has, and when a file with the same extension and bytes is
already there, that file is used. The library is then listed as User
Defined only. Until the app quits, commands given its loaded id use the
copy, unless another library is loaded under that id. Without a user
library folder (the web and headless sessions) the command is disabled.
A file with the same extension and bytes as a User Defined library
opens as that User Defined library. The unchanged original of a copied
.ase file is one such file. Graphic style libraries open the same way.
Japanese now translates the status-bar and error messages (`@msg`
templates) like Spanish, French, Italian, Russian and Ukrainian do:
556 rows at the end of ja.tsv, the 498 message literals the test scan
finds plus the 58 that the other catalogs also carry (web start-up,
relinking, export notes, PostScript read errors).
`ja` joins COMPLETE_MESSAGES, so complete_languages_translate_every_message
keeps it complete. The registry comment, docs/development.md and the
ROADMAP line now list Japanese with the other five. The messages
themselves stay English for agents and MCP.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
As in Illustrator, the Pen shows its continue cursor over an end of any
open path, selected or not, and a click there selects the path and draws
on from that end (reversing it from its first end). While drawing, it
shows a join cursor over an end of another open path, and a click joins
the two into one path, in one undo step, which finishes the path.
path.join takes {ids: [a, b], ends: ["first"|"last", "first"|"last"]}
to join the ends asked for (the Pen's join); without them it joins the
selected paths' nearest ends as before. Joining a path with itself, or
anything that isn't a path, is an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The source audit linked commercial template listings and other pages it
didn't use, and described the environment it ran in. Keep the two sources
the corpus uses, say in a sentence what kinds of sources were left out and
why, and state plainly that reopening our output in Affinity is still to do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The font scan read only the macOS font folders and the Additional
Fonts Folder, so fonts that apps and font managers register with
CoreText from their own folders were missing from the font menus, and
exports set such type in the fallback font. CoreText's font manager
lists them with the installed fonts.
- `mac_fonts.rs` lists the files of the fonts the font manager has
available (`CTFontManagerCopyAvailableFontURLs`, through
objc2-core-text), keeping only file URLs as `open_documents.rs` does
(#433). The desktop app and `vectorcraft-cli` install it as they
install the DirectWrite lister on Windows, and the scan reads the
files outside the font folders in place.
- The scan leaves out faces with no outlines the font engine draws (no
glyf, CFF, CFF2 or VARC table), on every platform. CoreText lists
Apple's PingFang UI interface font (macOS 27), whose outlines are in
an hvgl table; if the scan cataloged it, PingFang SC would count as
installed, and Chinese characters in fallback text, Japanese kanji
included, would draw blank.
- Tests: the list holds the installed fonts by full path, a font
registered from another folder is listed while it is registered, and
a face without outlines the engine draws isn't cataloged.
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.
Clippy 1.99.0 added `chunks_exact_to_as_chunks`, which fails `cargo xtask ci`
at its clippy step. Eight uses of `chunks_exact(_mut)` with a constant size now
use `as_chunks(_mut)` (stable since 1.88, within the 1.95 minimum): four in
crates/affinity, three in crates/effects and crates/engine tests, and two in
xtask's sha256. docs/development.md says the gate targets the current stable.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace the Trunk post_build hook packaging/web/copy-fonts.sh with `cargo xtask web-fonts` (xtask/src/web_fonts.rs), so the web build only needs trunk, the wasm32 target and cargo, with no POSIX shell, and works the same on Windows.
The new task reuses crates/text/build/web_fonts.rs for list parsing and URLs, so build.rs and the copier can't disagree on fonts/<sha16>/<file>. It still does nothing without CRAFT_FONTS_DIR, checks each file's SHA-256 against the manifest, and copies the licence next to each face. A listed face that an older craft-fonts checkout lacks is now skipped with a warning, as build.rs does, instead of failing the build. A relative CRAFT_FONTS_DIR is resolved from the workspace root.
Update the references in Trunk.toml, fonts.rs, build.rs, craft_fonts.rs and web-fonts.txt, and document the web build's requirements in docs/development.md. Add tests for the copy, a hash mismatch and the no-op case.
Signed-off-by: Ahmed Moussa <moussa.ahmed95@gmail.com>
Double-clicking the MSI used to flash a progress bar and close, so users couldn't tell it had
installed. It now opens a wizard: Welcome, install folder (remembered across upgrades), Ready,
progress, and a Finish page that says where to find the app with a ticked "Launch VectorCraft"
box. Every page names the full version, pre-release tag included. The wizard art is drawn from
the app icon at package time; msiexec /qn still installs silently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Desktop builds made with CRAFT_FONTS_DIR now load craft-fonts' 12 Arabic families into the document font database. Noto Sans Arabic comes first and is the fallback for Arabic text. Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar can be picked by name from the font menus.
The web build doesn't embed these fonts because they would make the wasm too large. It fetches them from beside the wasm instead:
- crates/text/web-fonts.txt lists each face as startup or background.
- build.rs turns that list into vectorcraft_text::WEB_FONTS, with each face's URL (fonts/<sha16>/<file>) and SRI hash taken from the craft-fonts manifest.
- packaging/web/copy-fonts.sh, a Trunk post_build hook, copies the font files and their OFL licences there.
- apps/vectorcraft-web/src/fonts.rs fetches Noto Sans Arabic before the app starts, so Arabic text never shows as boxes. It loads the other faces in the background and logs `web font …` for any that fail.
Because fonts can now arrive after startup, the Type tool's layout cache also checks the font database's generation, so text lays out again when new fonts arrive.
_headers and .htaccess serve fonts/* as font/ttf and cache it as immutable. packaging/web/README.md and docs/development.md explain the fonts folder, and ROADMAP.md lists the Arabic typefaces under Type. Also adds Ahmed Moussa to contributors/people.toml.
Signed-off-by: Ahmed Moussa <moussa.ahmed95@gmail.com>
The PDF writer defaulted to Convert Text to Outlines, so every glyph of
every page became a path: a text-heavy PDF grew several times larger on
export (a 7-page document went from 27 KB to 226 KB) and its text could
no longer be selected or searched. Illustrator's PDF presets embed font
subsets and keep live text; that is now the default too
(advanced.outlineText: false). Outlining stays available as an option,
fonts that can't be embedded still become outlines with a warning, and
printer's marks (page information) are drawn as shapes as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`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>
Such a file's PDF part is a placeholder page, which was refused. When
the file carries editing data, it now opens from that alone: its
structure as a PDF-compatible file's, its point type made from the text
document, and type it can't make (area type, type on a path) left out
with a note that marks the import as lossy. Without editing data, or
with pages picked or layers off, the placeholder is refused as before;
damaged editing data says so.
The page-order note is no longer given when the page has no type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layers reader of the editing copy (operators translated into
PostScript for the interpreter, any file with RGB colours, gradients,
transparency or images falling back to its page) is replaced by a reader
of the whole structure (import/ai): layers and sublayers with their
options, groups, compound paths, clipping groups and layer clipping,
names, hidden and locked objects, RGB, CMYK, grey, named and spot
colours, gradients, transparency, fill rule, overprint, embedded images
with their alpha channel, guides and every artboard, and objects drawn
as their look when they have several fills or strokes, effects or a
brush. Containers: Zstandard, zlib or plain data in EPS and .ai.
The text placement, the comparison with the page and the overwrite guard
stay around it. The page's type takes its text object's place without a
group; the comparison is by lightness, on nothing, without layers that
don't print. Symbols, pattern fills, placed files and unknown operators
make the file come in as its page where they show, and are left out of
layers that don't.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Laser and cutter software skips HATCH entities, so shapes that were only
filled (type, traced art) reached it as nothing. Each filled area is now
written as its outlines (polylines and splines, in the fill's colour)
followed by the solid hatch; R12 keeps writing the outlines alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Metal reports no PCI ids, so both GPUs of a dual-GPU Mac had the adapter key
Metal:0000:0000. When the integrated GPU failed at start-up, the restart left
out both and the app quit, although the discrete GPU works. An adapter without
ids is now keyed by its name, so the restart leaves out only the one that
failed.
The failure itself was wgpu's indirect-call validation compute shader, which
OCLP-patched Metal on an Iris Pro can't compile. Nothing in VectorCraft or
egui draws indirectly, so that validation is off (WGPU_VALIDATION_INDIRECT_CALL=1
turns it back on).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel laid out a row (with its thumbnail and several whole-document
lookups) for every open row each frame. A layer of 5,000 groups of 10 paths,
all open, took about 29 s a frame headless. Now the rows shown are listed once
per document change (with their layer colour and dimming worked out on the
way down) and ScrollArea::show_rows lays out only those in view: 0.4 ms a
frame. A row highlighted out of view is scrolled to.
The open rows move from UI state into the document's state
(DocState::layers_open, OpenRows): a document opens with only its top-level
layers open, and native files save the open rows (Document::layers_open,
written only when not the default, at save time like the view, so opening or
closing rows never marks the document modified).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
French now sits between Spanish and Italian (es, fr, it, ru): the
plural rule, the LANGUAGES registry entry (so the Language menu lists
Français before Italiano), the locale tests, the fr() helper and
french_reads_as_french. README, ROADMAP and docs/development.md list
the languages in the same order.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add fr.tsv with the full interface coverage: every menu string, every
tl! literal, the plurals and the status/error messages (@msg).
Registered as complete_menus with the French plural rule (0 and 1 are
singular); every fr-* locale (fr-FR, fr-BE, fr-CA, fr-CH …) resolves
to it. Keeps the product, workspace and perspective preset names in
English like the other complete catalogs.
Tests: french_reads_as_french; fr added to KEEPS_MENU_NAMES and
COMPLETE_MESSAGES; the Latin-script glyph test covers French letters
and the no-break space; locale tests use de-DE as the unsupported case.
Clean-room: written from the meaning of the English labels in the
vocabulary French-speaking users of vector illustration software know
(Plan de travail, Nuancier, Contour, Fond, Tracé, Calque). No other
application's localisation was copied.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Character panel menu › Character Alignment gains ICF Top/Right and ICF Bottom/Left, and
`charAlign` takes `icfTop` / `icfBottom`: a character smaller than the largest on its line lines
up its ideographic character face's top (right, vertical) or bottom (left) with the big one's.
`FontFace::icf_margins` places the ICF inside the em box as the OpenType baseline tags define it
(https://learn.microsoft.com/en-us/typography/opentype/spec/baselinetags): `icfb` / `icft` of the
BASE table's horizontal and vertical axes against `ideo` / `idtp` (else the OS/2 typographic
descender and one em), for Han ideographs (else kana, else the first script). Without them, as that
page allows, the average ink box of some ideographs and kana; a face without ideographs uses its
em box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Document symbols placed inline in a run of type (beyond Illustrator; like
InDesign inline anchored objects): TextRun.inline holds the symbol, scale and
shift; the run is one U+FFFC laid out as a glyph sized to the art, centred on
the cap height. text.insertInline and Type > Insert Inline Symbol; editing
keeps the graphic whole; canvas, SVG (<use>) and PDF draw the art; Create
Outlines turns it into a symbol instance.
The DirectWrite font lister (#594) was installed in the desktop app only:
`vectorcraft-cli` (convert, run, MCP) didn't see fonts that font services
such as Adobe Fonts load on Windows, so its exports set such type in a
fallback font.
- The CLI shares the app's `system_fonts.rs` and installs it first thing
on Windows (not the win7 build). The text crate stays free of unsafe
code; the CLI denies unsafe code instead of forbidding it, so the
shared module's own allowance applies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the Free Transform tool (E) is active, a small box at the canvas's
top left holds Constrain over the tool's three modes: Free Transform,
Perspective Distort and Free Distort. Before, the modes were reachable
only by modifier keys or `tool.setOption`.
- The buttons set the tool's `constrain` and `mode` options through
`tool.setOption`, as agents do. Constrain acts as Shift held and is
greyed in the distort modes, where it does nothing. The modifier keys
keep working while dragging.
- Theme tokens; Constrain shows the link icon, Free Transform the tool's
own icon, the distort modes are drawn in code. Tooltips translated
(cs, es, it, ja, pt-br, ru, zh-hans, zh-hant).
- `ui.inspect` reports where it shows (`freeTransformWidget`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File › Open, Other Library… and the User Defined library folder read
.ase files. Colors keep their model, global and spot colors stay global
and spot, and color groups stay groups. The library readers take a
file's bytes. On the web, File › Open passes a swatch library file to
swatch.library.load as dataBase64 instead of as lossy UTF-8 text.
Swatch and group names in a library are now unique, in .gpl files too.
Add to Swatches puts each color group of a library into one document
group when the document already has a swatch with the group's name.
The library fuzz helpers fail when a command panics.
Each paragraph has its own alignment, indents, spacing, hyphenation, tab
stops and paragraph style (TextObject.paras; para stays paragraph 0's, so
old files and readers work). Layout, editing (split/merge), threads, the
Paragraph/Tabs/Paragraph Styles panels, ranged text.setStyle/setFormat/
tabs/paraStyle.apply, SVG export/import and the fuzzers follow.
Area Type Options gain a fit: none, autoHeight (Illustrator's Auto Size:
the frame's height follows the text after every edit, resolved in
refresh_bounds inside the edit so it shares the edit's undo step) and
shrinkText {minPercent} (beyond Illustrator: overflowing text scales its
sizes, leading and baseline shifts down at layout time by the largest
factor, in 0.1 pt steps of the first run, that fits). The Auto Size New
Area Type preference now applies. text.areaOptions and document.inspect
report overflow and the shrink factor; the Area Type Options dialog
offers fit and first baseline as dropdowns.
A `.ai` file's PDF part is what apps that don't read VectorCraft's native
document open. It had no PDF layers: hidden layers and sublayers were left
out of it and everything else was flat page content.
- `.ai` files (Save As, exports, the CLI) write PDF layers unless
`createLayers` is false (a PDF standard still decides for itself).
- createLayers writes sublayers too, as optional content groups listed
under their layer's in `/Order`, hidden ones off with their art.
- A layer's form gets a box round the whole page instead of its art's
bounds, which readers made a clipping group of, gathering a layer's art
and its sublayers' in one group.
- PDF import: art inside a group, clip or form that dissolves into its
parent keeps the layer it was drawn in, so sublayers nested in their
layer's content import as sublayers.
- Save PDF: "Create PDF layers from layers and sublayers" (ja, es, it,
pt-br, ru, zh-hans, zh-hant).
Hidden objects (not layers) still aren't in the PDF part.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Area Type Options > Align: Top / Center / Bottom / Justify (AreaOptions.verticalAlign,
LayoutOptions.vertical_align). Each row/column cell is aligned on its own; rectangular
cells shift their lines, other frames and text wrap reflow until settled without
losing text. Justify shares the leftover space equally between line gaps. Area Type Options
shows Align and First Baseline as dropdowns (the parameter dialog's choices table).
Font services load their fonts with GDI at run time, from files in their
own folders, without installing them: neither the font folders nor the
registry list them, so the font scan never saw them. DirectWrite's system
font collection holds them since Windows 10 1607.
- vectorcraft-text: `set_platform_font_files` lets the app name font files
the platform knows; each scan and each check for installed fonts asks
again and reads those outside the font folders. A file named by itself
is read whatever its extension (font services keep fonts in files
without one).
- The desktop app lists the local files of DirectWrite's system font
collection on Windows (`system_fonts.rs`, through the windows crate
already in the build), read in place.
- The check for fonts installed while the app was in the background runs
on a thread: DirectWrite's check for updates takes about 50 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Illustrator's Every-line Composer balances the breaks of the whole
paragraph for left/centre/right aligned text too, not only justified
text. VectorCraft only composed justified area text; ragged text was
always broken greedily.
- Model: ParaStyle.composer (vectorcraft_doc::Composer, re-exported by
vectorcraft-text), Every-line by default like Illustrator, saved only
when Single-line. layout() reads it from the object;
LayoutOptions.composer is now an Option override for tests.
- Composer: the justified gate is gone. Ragged lines never shrink (a
line that doesn't fit ends the node), badness is 100*r^3 with r = the
line's leftover space over a rag zone of 1/6 of the width (finite for
lines without spaces); the last line is free unless shorter than 1/3
of the width (measured in zones of 1/3), so a lone short word on the
last line pulls a word down. Hyphens cost 25 in ragged text (50 in
justified). Knuth-Plass fitness classes with adjacency demerits
(justified 10000, ragged 1000). Lines past the last width change
share one node key, active nodes are capped (4096) and paragraphs
over 200k glyphs break single-line, so work stays bounded; ragged
tries a tolerance of one rag zone, then any fitting breaks, then
falls back to greedy.
- text.setFormat {composer: "singleLine"|"everyLine"}; the Paragraph
panel menu's composer items work with a check mark (Reset Panel goes
back to Every-line).
- Tests: ragged Every-line moves "or" down in "Destroy all creatures
with toughness X or less." where greedy leaves "less." alone; the rag
of long paragraphs is no worse than greedy (sum of squared leftover
and of neighbour differences); Single-line is greedy; the 10k-char
layout budget covers ragged text; command test with undo; native
format round trip (rich_session's text is now Single-line).
No existing test needed changing: the justified tests and
hyphenated_layout_adds_hyphens (ragged, now Every-line) still pass.
Auto alignment (start of the paragraph direction) is ragged; a wrapped
line's Mojikumi flush opening bracket gives its half em back to the
composer, as in the line loop; tests cover Auto in both directions.
Layout of 10k chars, release: justified Every-line 5.6 ms, ragged
Every-line 5.8 ms, ragged Single-line 4.8 ms.
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>
The scan read only the platform's font folders, once per session (or on
Character panel menu > Refresh Font List). Fonts Windows registers outside
its font folders (installed as shortcuts, or by programs into their own
folders) never listed, nor did fonts installed while the app was open.
- Windows: the font files registered under the local machine's and the
user's Fonts registry keys outside the scanned folders are scanned too
(windows-registry, the windows-rs project's safe registry API).
- Linux/BSD: the XDG data folders' fonts (XDG_DATA_HOME, XDG_DATA_DIRS:
Flatpak, Snap, NixOS, Guix) and a Flatpak sandbox's host fonts
(/run/host/fonts, local-fonts, user-fonts); macOS: /Network/Library/Fonts.
- The global database looks the platform's paths up again at each scan,
and a scan's roots may be font files as well as folders.
- FontDb::installed_fonts_changed compares the folders the last scan read
(their modification times, and the paths it started from); the app
checks it when its window comes back to the front and rescans
(text.rescanFonts) only when fonts were installed or removed meanwhile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A click on a flyout's tear-off bar, or the release of the long press
that opened it over the bar, tears the flyout off too, as in
Illustrator; dragging it still does.
- window.floatTools {tool, floating?} floats or puts back the group
holding a tool, for the control channel and MCP's invoke_menu.
- Floating strips are part of user workspaces; the built-in ones float
none, so Reset puts them back.
- Loaded preferences keep one strip per group, of known tools.
- No per-frame clone of the strips or copy of the active tool's id.
- docs/control-protocol.md, ROADMAP, Float Tool Group translations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Type on a path could only be repositioned by Type on a Path Options, whose menu item sent
`{start: 0}`, so OK threw the text to the start of its path; nothing showed or dragged its
brackets. Now, as in Illustrator:
- Type on a path flows between a start and an end bracket (`TextKind::OnPath { end }`, None:
the end of the path or once round a closed one; `TextKind::path_span`). Flipping
(`TextObject::flip_on_path`) reverses the path keeping a closed path's first anchor and swaps
the brackets, so the type keeps its stretch of the path.
- Type on a Path Options gains Align to Path (ascender, descender, center, baseline) and
Spacing (glyphs spaced as if set that far above the path: closed up round the outside of
curves, opened up round the inside), laid out by the text engine.
- `type.pathOptions {start?, end?, flip?, effect?, alignToPath?, spacing?, ids?}` sets them in
one undo step; with none of them it queries. The menu item opens the dialog with the selected
type's Effect, Flip, Align to Path and Spacing (dropdowns for the choices) and leaves the
brackets alone, so OK as opened changes nothing.
- The Selection and Direct Selection tools show selected type on a path's path and its start,
centre and end brackets (`tools::pathtype`, with a bracket cursor): dragging the start or end
bracket sets where the type runs, dragging the centre bracket slides it along the path and,
across the path, flips it (Cmd/Ctrl held: it only slides). Each drag is one undo step.
- Arc-length measuring moved to `vectorcraft_geom::ArcPath` (layout, the tools and
`text.createInPath` share it). SVG export outlines path type whose end bracket, alignment or
spacing `<textPath>` can't express.
- docs/mcp.md documents the command and the drags; ROADMAP updated; dialog strings translated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a numeric field's label left or right steps the field's value,
as Photoshop's scrubby labels do (beyond Illustrator): one unit every
2 points, ten with Shift, a tenth with Ctrl/Cmd (counts stay whole), in
the field's unit and precision. The panels apply each value as they
apply a typed one, so the art follows live and the caller's limits
hold (dragging back from a limit changes the value at once).
- scrub.rs: the label zone over the label noted just before a field
(dim_label, label_row, widgets::field_label, the Character and
Paragraph panels' icons, the Transform and Properties rotate/shear
icons), the ↔ cursor on hover, a click on the label focuses the
field. The text box itself never scrubs; a field without a label
doesn't. num_field, plain_field, mixed_field (and the spin fields)
and opt_field take part.
- Engine: undo groups (Session::begin_undo_group / end_undo_group). The
edits made in a group, commands and committed live drags alike, are
one undo step, journaled one by one; a cancelled group is undone and
dropped from the journal. The app opens one while a label is dragged,
so a scrub is one undo step whichever path the panel applies values
by (app.run, session.execute, live_run), and Escape cancels it
(shortcuts leave Escape to the drag, so it doesn't close a dialog or
deselect).
- Preferences > General > Scrub Numeric Fields by Dragging
(`scrubNumericFields`, on by default), translated in ja, es, zh-hans,
zh-hant and pt-br.
- Tests: label drags with Shift/Ctrl, limits, counts and units, Escape,
the preference, the cursor, click-to-focus, no scrub on the box or
without a label, and in the app one undo step for a W drag.
- ROADMAP and docs/mcp.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Switching to CMYK separates RGB greys through the CMYK profile into
four-colour greys and a rich black, as Illustrator does; only Gray
colours stay on the black plate. The command catalogue said "greys stay
greys", which read as if R = G = B colours went to K only.
- file.documentColorMode (and its alias) and document.open {colorMode}
take grays: "profile" (default, unchanged) | "black". With "black",
RGB greys with R = G = B convert to K only, K = their grey value (the
inks Edit Colors > Convert to Grayscale before the switch gives);
other colours convert as before. Swatches, tints, gradient stops and
pattern tiles follow the same rule.
- Pattern tiles are now converted with the rest of the document (they
kept RGB colours in a CMYK document before), as the command said.
- The command catalogue, docs/mcp.md and the ROADMAP say what happens to
Gray colours and RGB greys.
- Tests: the profile path (four-colour greys, rich black, pattern tiles,
Gray kept), grays: "black" over art, swatches, tints, gradients and
patterns, document.open, and the issue's file over MCP through a PDF
export and back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fourteen more Preferences options that were stored but read by nothing now
act, for the mouse and for agents (prefs.set, then pointer_gesture,
press_key or the commands):
- Selection & Anchor Display: Snap to Point and its distance (with Smart
Guides off, View > Snap to Point lands a dragged selection's grabbed
point, drawn points and reference points on anchors and ruler guides);
Move Locked and Hidden Artwork with Artboard (artboard.move, the Artboard
tool and Rearrange; off, hidden art now stays too); Enable Rubber Band for
the Pen and Curvature tools; Handles (solid, hollow, large);
Show handles when multiple anchors are selected (canvas and Direct
Selection's handle picking); Hide Corner Widget for angles greater than.
- General: Transform Pattern Tiles is the default of a new `patterns`
param on object.transform/move/rotate/scale/reflect/shear/transformEach
(journalled when the art uses a pattern), and the transform dialogs get a
Transform Patterns checkbox; Select Same Tint % (off, Select > Same
Fill/Stroke Color takes every tint of a global or spot swatch).
- Type: new type.step {attribute, by} steps size, leading, tracking, kerning
and baseline shift by the Size/Leading, Tracking and Baseline Shift
increments; Increase/Decrease Font Size (Cmd+Shift+. and Cmd+Shift+,);
the Type tool's Alt+arrows (kerning at a caret or tracking, leading,
Shift for baseline shift, Cmd/Ctrl for five steps). Enable Missing Glyph
Protection keeps characters a new font lacks in the font that had them
(text.setStyle, text.setRangeStyle). Fill New Type Objects With
Placeholder Text: type the Type tools place starts with placeholder text,
selected (text.create / text.createInPath take `placeholder`).
Units > General, Stroke and Type were already the single source of truth
(units.rs). Auto Size New Area Type, Show Font Names in English and
Highlight Alternate Glyphs need subsystems the app doesn't have yet and
stay open with the rest of #394.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The inspect_document tool (and the control channel's document.inspect)
take {depth?, childLimit?}, so MCP clients get the skeleton without
going through run_command; the tool description points at the
drill-down path.
- document.find rejects a filter of the wrong type instead of dropping
it (which widened the search); one opt_u64 helper parses limit, depth
and childLimit.
- docs/mcp.md spells out find's matching rules; control-protocol.md
and ROADMAP updated; MCP and engine tests cover the new paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.inspect takes the same {depth?, childLimit?} slicing as
document.node (a depth-0 read is the layer skeleton), and document.find
{name?|kind?|text?, limit?} locates nodes across the tree with their
ancestor paths and a total. docs/mcp.md documents the drill-down recipe;
the CLI stdio sweep covers both.
Guides dragged out of the rulers could not be touched again: no tool hit-tested
them, so a click on a guide went to the art under it.
- The Selection, Direct Selection and Group Selection tools pick a ruler guide
within the selection tolerance (over the art; Direct Selection takes an
anchor on it first) through a shared tools::rulerguide::GuideEdit. A drag
moves the selected guides in one undo step (Alt copies them, Shift snaps the
dragged one to the ruler's ticks, otherwise it snaps to pixels, the grid or,
with Smart Guides, the art's edges, centres and anchors); dropped off the
canvas onto its ruler, the guide is deleted. Hidden or locked guides can't be
picked; the pointer shows which way a guide moves.
- Selection::guides holds the selected guides (on their own, like slices); the
canvas draws them in the selection colour. Delete/Backspace (edit.clear)
delete them, the arrow keys (object.nudge) nudge them, Lock Guides and Hide
Guides deselect them.
- Commands for agents: guide.list, guide.select {indexes, toggle?},
guide.move {dx?, dy?, copy?} (besides {index, pos}) and guide.remove {index?};
guide positions out of range are refused. Documented in docs/mcp.md.
- Shift while dragging a new guide out of a ruler snaps it to the ticks too
(Unit::ruler_step, shared with the rulers' drawing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fifteen Preferences options were stored but read by nothing. They now act,
through the existing plumbing (ToolContext for the tools, the prefs read by the
canvas for the view), and reach agents through prefs.set:
- General: Show Tool Tips (off: no widget's tool tip comes due), Double Click
To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer,
Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways), Use Precise
Cursors (Pen, Eyedropper, Slice and Blend pointers become a crosshair).
- Selection & Anchor Display: Tolerance (objects, anchors and handles, in
place of the fixed 3 and 4 px), Object Selection by Path Only (compound
paths too), Command Click to Select Objects Behind (cycles down the objects
under the pointer; not when Cmd only borrows the selection tool), Zoom to
Selection (Zoom In/Out centre the selection), anchor Size, Highlight anchors
on mouse over (Direct Selection).
- Type: Number of Recent Fonts (up to 15 Recent Fonts slots).
- Guides & Grid: guide and grid Color and Style (lines or dots), Grids In Back.
The canvas's context menu, panel drops and hover highlight pick objects like
the selection tools. The control channel gets ui.wheel (with modifiers held
like ui.click) so agents can turn the wheel. The rest of #394 stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
Help > About VectorCraft gains Contributors and Models tabs next to the existing About
content, following the craftrules standard (standards/contributors.md):
contributors/contributors.json is generated by craftrules/scripts/contributors.py and baked
into the binary by crates/ui-egui/build.rs as static tables (credits.rs). Each contributor
line: @username, opt-in display/real name, PRs, commits, lines +/-/delta, binary assets +/-,
first/last commit. Sortable by every column; alphabetical ignores case and the leading @.
Labels are translated (ja, zh-hant, pt-br catalog rows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors docs: craftrules is two levels up (../../craftrules)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section
build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: credits labels for catalogs added on main since this branch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate after rebase onto main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Under Wayland the window is native but arboard was built without its
data-control backend, so it read the X11 clipboard through XWayland,
which only holds what X11 apps copied: Paste found nothing and reported
an empty clipboard (egui's text paste into fields uses smithay-clipboard,
which is why that worked). arboard now has `wayland-data-control`, the
same as PhotoCraft; it falls back to X11 where the compositor lacks the
protocol (GNOME's Mutter).
The reporter's clipboard also held text/uri-list: with a file copied in a
file manager, Paste would have taken the path as text before the bitmap.
Copied files now paste first, as the first one that is art (SVG, PDF,
EMF/WMF or a bitmap), told from its first 4 KiB by the engine's format
detection (`clipboard::file_flavour`): text/uri-list through arboard on
Linux and macOS (a trailing CR from CRLF lists trimmed), CF_HDROP on
Windows, read with the clipboard closed again.
Dropping files on the window can't work under Wayland yet: winit 0.30,
which eframe 0.36 runs on, has no Wayland drag and drop (winit#1881,
added in 0.31), so no dropped_files arrive. docs/development.md documents
it and the workarounds (copy and paste the files, File > Place, or run
under XWayland with `WAYLAND_DISPLAY=`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`depth` / `childLimit` without `summary: true` used to return the whole
full-JSON subtree silently; it is now an invalid-params error. docs/mcp.md
describes the slice parameters and childCount, the engine test covers
fractional values, unsliced reads and limits past the tree, and the CLI
stdio sweep reads a sliced group through run_command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The five freehand tools keep options (Fidelity, fill, the two
tolerances, the brush size) that last across tool switches and are
saved with the preferences, but nothing in the window showed or changed
them: a double-click on the tool's button answered "the pencil tool has
no options", which the toolbar drops. It now opens a Tool Options dialog
with the options the tool has, and OK sets them with tool.setOption. The
dialog's strings are in the Japanese, Chinese (both scripts) and
Brazilian Portuguese catalogs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Select All (Cmd/Ctrl+A) now follows the keyboard, as in Illustrator:
- macOS menu bar: the system menu takes Cmd+A/X/C/V before the window
sees them, so a focused panel, dialog or search field got the document
command. `menus::invoke_from_system_menu` (used by `NativeMenu::poll`)
hands Select All, Cut, Copy and Paste to a focused text field as the
events its keys would send; clicks on those menu items follow the same
rule. Other commands, and these with no field focused, run as before.
- Type tool: `select.all` itself selects all of the text being edited
while the Type tool edits (returns {editing, start, end}), so the menu,
the shortcut, the control channel and headless MCP (`press_key`) agree.
The UI's own Select All interception and the Type tool's hard-wired
Cmd+A are gone; Cmd+A reaches it through the Select All shortcut.
- Windows, Linux and the web already left Cmd+A to a focused field; now
covered by headless app-frame tests (field, Type tool, canvas).
Docs: docs/mcp.md (Select All while editing type), ROADMAP. Not yet:
Select All across threaded frames (one frame's text is selected).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the browser dropped the page's WebGPU device or WebGL context, the
editor kept running but nothing it drew reached the screen: Chrome and
WebKit showed a frozen, stale canvas and Firefox a blank page, while
edits and Save went on.
- ui-egui: new `graphics` module. `GraphicsLoss` carries a loss report
from any thread (wgpu's device-lost callback, the canvas's
`webglcontextlost` event) to the UI thread. `VectorcraftApp::
graphics_lost` writes Data Recovery copies of the modified documents at
once. The app now notices when the host hands it a new egui context and
uploads everything again (fonts and theme, the canvas, place cursor
thumbnails); every per-thread texture cache (layer, symbol, swatch,
style, asset and glyph thumbnails, previews, font samples, Navigator,
Flattener Preview) is a `TexCache` that empties for a new context.
Without this, the moved app drew stale or wrong textures and egui
panicked on its missing font families.
- web: the shell watches both loss signals; on a loss it puts a fresh
canvas in place of the dead one and starts the same `WebRunner` again,
handing over the same app (documents, undo, panels), with services made
for the new context and a status-bar note. If no new graphics can be had
(or after three losses) it removes the stale canvas, lets go of the
recovery copies and says to reload.
- engine: `recovery::leave` lets go of a session's area but keeps its
copies, so the reloaded page offers them at once instead of after the
heartbeat goes stale.
- desktop: eframe can't give a window a new device, so a lost device
(driver reset) is logged, the copies are written and the status bar says
to save and restart; closing then skips the Save Changes question it
can't show.
- Tests: loss reporting across threads, `TexCache` emptying, the app
uploading the canvas, a cached preview, theme and fonts again in a new
context (fails without the fix), copies written on loss, and
`recovery::leave` offering copies at once with both lock and heartbeat
stores. docs/development.md (web section) and ROADMAP updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live rectangle stored four corner radii but drew them all with the largest,
and the corner widgets always rounded all four corners, so one corner could not
be rounded on its own.
- geom: rectangle_with_corners draws each corner with its own radius and kind
(Round, Inverted Round, Chamfer); a sharp corner is one anchor, a cut one two.
Uniform corners keep the exact rounded-rectangle anchors.
- doc: LiveShape::Rectangle gains `kinds` (saved only when a corner isn't round;
older files read as round), plus helpers mapping anchors to corners.
- engine: object.setLiveShape takes `corners` and `kind`; without `corners` it
edits the Direct-Selected corners, else all four, and keeps the selected
corners selected as their anchors change.
- tools: with Direct Selection, only the selected corners show widgets and a
drag rounds just those; Alt-click cycles the corner kind; double-click opens
the new Corners dialog (also `ui.corners`).
- ui: Properties/Transform radius fields show and set the selected corners;
CSS export keeps per-corner radii and skips cut-in corners.
- docs: mcp.md (Live Corners), control-protocol.md (Corners dialog), ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object Illustrator writes as a fill of its outline left open and then a
stroke of it closed imported as two objects: a filled path and a separate
stroke-only path. A fill and a stroke of the same outline (equal once every
subpath is closed) now become one path with the fill and a live stroke
(width, cap, join, miter limit, dash, paint), taking the stroke's outline.
Layers: a group whose view state is off (/Usage /View /ViewState /OFF, with or
without an /AS view event) comes in as a hidden layer with its art, as groups
off in the default configuration already did. The configuration's /Order
nesting (inline or as its own object, labels skipped) makes sublayers, which
take their top-level layer's colour; nesting depth, entries and groups are
capped. Splitting a page's art by group is no longer quadratic, so thousands
of layers import quickly.
The 6D art pen (rotation-aware brushes) is a separate feature, now on the
ROADMAP gap list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A heartbeat alone can't tell a crashed tab from one whose timers stopped in
the background, so another tab could discard (or restore) the copies of a
still-live document once its heartbeat went stale.
- Recovery stores without locks can now announce an area for as long as the
app runs (RecoveryStore::announce/announced). The web shell implements it
with the browser's Web Locks (navigator.locks through js_sys::Reflect): a
tab holds a lock named after its area, which the browser releases only when
the tab is gone. Lock snapshots are refreshed every 10 s (once before the
first frame); a snapshot over a minute old counts every area as held.
- An area is gone only when no announcement holds it and its heartbeat is
stale. Where the store can't tell (no Web Locks), the owner repairs: each
heartbeat (also its first frame after a pause) and each recovery save write
again the copies missing from its area, and a heartbeat newer than the one
another tab wrote when taking the area over marks it running again.
- MemoryStore::announcing models Web Locks in tests; engine and UI tests cover
the issue's sequence. Native folder locks are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Rotate, Scale, Reflect and Shear tools answer Enter with their
dialog, but the shortcut handler only applied what a tool asks of the
UI for the keys it claims, so the request was dropped and Enter did
nothing. It now applies it for Enter, and consumes the key when a
dialog opened, since a dialog takes Enter as its OK in the same frame.
The Selection, Direct Selection and Group Selection tools open the
Move dialog the same way, on Enter and on a double-click on their
toolbar button (`tool.options`), as Object > Transform > Move does.
With nothing selected neither opens anything.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Opening a PDF whose fonts aren't installed keeps the text live in the fallback font (as
Illustrator substitutes and highlights missing fonts), and opening said so, but every export
then wrote the fallback font silently ("warnings": []), so a headless convert re-set the type
without a word.
- Exports that draw type now say so in their warnings, naming the missing families and the
fallback font: PDF (outlined or real text, also .ai and Save As), EPS, EMF/WMF, raster images
and SVG with outlined or embedded fonts. Live SVG text, DXF, text and native files name the
font itself and don't warn. Replacing the font (Find Font) clears it.
- The fonts a document uses (text.fonts' scope, Package's font list) come from one helper.
- textAs: outlines already keeps the glyphs the PDF draws (its embedded font program, installed
or not): now covered by a test with an embedded serif font that isn't installed, checked
against the installed font's and the fallback's outlines.
- docs/mcp.md and ROADMAP describe the warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PDF's DeviceCMYK image opened as RGB (the interpreter's pixels as a PNG),
so every export wrote it as DeviceRGB or ICC RGB, silently. Images now keep
their CMYK samples, as CMYK colours already did:
- Model: CMYK images are blobs holding CMYK samples, a four-component JPEG
(Adobe's inverted samples) or a Deflate CMYK TIFF. vectorcraft-doc's cmyk
module reads their ink amounts (ImageBlob::is_cmyk, ::cmyk, capped at 64
megapixels) and writes the TIFF (ImageBlob::cmyk_tiff).
- PDF import: DeviceCMYK, CalCMYK and four-component ICC images keep their
samples. A JPEG with Decode [1 0 1 0 1 0 1 0] is kept byte for byte; any
other is decoded (8 or 16 bits, its Decode array applied) and stored as a
CMYK TIFF. Masked CMYK images and 1/2/4-bit ones open in RGB with a warning.
Placing a CMYK TIFF keeps it CMYK too.
- PDF export: CMYK images are written in CMYK (DeviceCMYK, or ICC-based with
the CMYK profile when colours are tagged: PDF/X-3, PDF/X-4, PDF/A). A CMYK
JPEG neither resampled nor recompressed passes through unchanged; others are
resampled in CMYK and compressed as ZIP or as a CMYK JPEG (jpeg-encoder,
which also replaces the ZIP fallback and its warning for images converted to
CMYK). Output conversion treats them like CMYK colours: Preserve Numbers and
PDF/X-1a keep their numbers, Convert to Destination converts them to another
CMYK profile or to RGB through a 17^4 lookup table (cms::CmykLut, now also
behind the renderer's ink table). A CMYK image that can't be decoded is
reported in the warnings.
- Canvas: CMYK images show through the working CMYK profile, as CMYK fills
do, and paint their own inks in ink planes (CMYK raster exports, CMYK
blending). SVG export writes TIFF images as PNG.
Illustrator's Output › No Conversion keeps colour data as it is
(https://helpx.adobe.com/illustrator/using/pdf-options.html).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PDF-compatible .ai paints the editor's page (an opaque white rectangle the
size of the page) before its first optional-content layer and outside every
layer. It came in as art: an extra "Page 1" layer with a page-sized white path
behind the artwork, which hid white art and gave SVG/PNG/PDF exports an
opaque background.
For pages with the editor's private data (the way the importer already tells
a .ai from a PDF), the first art outside every layer, painted before any
layer, is dropped when it is a plain fill-only rectangle in an opaque,
non-spot white, with normal blending, equal to the crop box (0.5 pt
tolerance). Plain PDFs keep such a rectangle as art (as the reference app
does when opening a PDF), and so does anything else: grey, smaller or larger
rectangles, strokes, translucency, white inks, fills over the layers.
The white-cover test the soft-mask reader used is now a shared helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a Spanish catalog (crates/ui-egui/src/i18n/es.tsv) covering the whole
interface like Japanese and Traditional Chinese: every menu string, every
tl! literal, the hint-bar fragments, plurals and the runtime strings the
other complete catalogs carry. It is written from the meaning of the
English labels in neutral, international Spanish (Latin American usage)
with the vocabulary users of vector illustration software know (Mesa de
trabajo, Muestras, Trazo, Relleno, Trazado, Pluma, Selección directa,
Buscatrazos, Motivo, Preajuste); no other application's localisation was
copied. Like Japanese it keeps the product, OpenType, the built-in
workspace names and the perspective grid preset names in English.
The language is registered with complete_menus (so the completeness
tests enforce it) and the one/other plural rule; every es-* system locale
(es-ES, es-MX, es-AR, es-419 ...) resolves to it.
Tests: es locale tags, Spanish vocabulary and plurals, Spanish letters in
the bundled UI fonts, and a new check that catalogs written with spaces
keep the leading/trailing spaces of joined fragments. ROADMAP, README and
docs/development.md list the language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop the workspace [patch.crates-io] and restore windows-link's crates.io
source and checksum in Cargo.lock, so every normal build keeps the
checksum-verified crate. windows7.ps1 now applies vendor/windows-link with
--config, checks that only windows-link's lock entry moved, builds and tests
with --locked, checks the patched crate is the one in use, and restores
Cargo.lock afterwards.
- windows7.yml runs on demand and weekly instead of on every PR, pins every
action by commit SHA, and leaves the dependency check and host tests to the
script (-Test) so the Cargo arguments live in one place.
- ROADMAP, README, NOTICE and docs: experimental, unsupported, not verified by
maintainers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finder, Open With, a drop on the Dock icon and `open -a` failed with "VectorCraft cannot open
files in the PDF document format":
- The release Info.plist declared no document types, so Launch Services refused the files before
the app saw them. Info.plist.in now declares every format File › Open reads (the engine's
OPEN_EXTS; a test keeps them in step): VectorCraft owns its own documents and templates and is an
alternate for the others, so it takes over no other app's files. `cargo xtask bundle` now fills
in the same template instead of its own, shorter plist.
- macOS delivers such files as open-document events, not arguments. winit 0.30's application
delegate has no `application:openURLs:`, so AppKit fell back to NSDocumentController, which
refuses every file of a non-NSDocument app (the error in both issues). The new
`open_documents` module adds that method to the delegate's class when the app is about to finish
launching (before AppKit delivers launch files), queues the paths and wakes the UI, which opens
them with `io::open_path` like command-line files; errors go to the status bar too.
New direct macOS dependencies `objc2-foundation` 0.3 and `block2` 0.6 were already in Cargo.lock
(through winit, muda and rfd). The module allows `unsafe_code` for the Objective-C runtime call.
Fixes#354
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web reads dropped files asynchronously, and the place inbox only kept
the drop point, so a file dropped on document A was placed into whatever
document was active when its bytes arrived. A drop on a canvas now carries
place::DropAt (the document's uid, the point, embed), and drop_files makes
that document active again before placing; when it has closed meanwhile,
nothing is placed and the status bar says so. The native drop path is
synchronous and goes through the same code, so it is unchanged in effect.
Test: a drop pending while another document opens lands in the first one
at the drop point; one whose document closes is not placed anywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Taiwan vocabulary left over from the zh-hant rows #303 started from is
replaced with the mainland's, about 560 rows: 档案 → 文件, a Taiwan 文件
(document) → 文档, 工作区域 → 画板, 笔画 → 描边, 色票 → 色板, 遮色片 → 蒙版,
渐层 → 渐变, 偏好设定 → 首选项, 快速键 → 快捷键, 按一下 → 单击, 描述档 → 配置文件,
品质 → 质量, 汇出/汇入 → 导出/导入, 连结 → 链接, 点阵 → 栅格, a row 列 → 行,
「」 → “”, half-width parentheses; tool names that collided or were wrong
are fixed (Selection Tool 选择工具, Scale Tool 比例缩放工具, Warp 变形,
Puppet Warp 操控变形, Divide 分割, Exclude 差集, the 直排 type tools).
- 42 rows for strings that landed after #303 (Layers panel, graphics
processor preference, panel collapse, the font menu's filters).
- A catalog header documenting the format and the vocabulary, and a test
that zh-hans has no Traditional characters or Taiwan terms.
rows were the better base (hand-adjusted mainland terms in the menus and
dialogs), so no rows were taken from #316.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Illustrator sets a paragraph's direction (Left-to-Right / Right-to-Left
Paragraph Direction in the Paragraph panel, with the Middle Eastern
options) and keeps its alignment buttons physical; it has no automatic
alignment toggle. So:
- ParaStyle.direction (None: from each paragraph's first strong
character, as before; written only when set), the Paragraph panel's two
direction buttons with Show Indic Options (original icons), and
text.setFormat {direction: "auto"|"leftToRight"|"rightToLeft"}. The
layout uses it as the base direction.
- The Automatic alignment check box is gone. Justify::Auto stays as the
start of the paragraph's direction, set on new type only (text.create,
the Type tools, with the mojikumi default in one new_type_para()):
TextObject::point is Left again, so importers (PDF, SVG, DXF, EMF) keep
their own alignment. The alignment buttons show what Auto resolves to.
- Bidi only where there is something right to left: plain paragraphs skip
the algorithm (no per-layout allocation), it runs once per paragraph
(shaping takes the paragraph's levels instead of recomputing them; path
type computed it four times), and the shaper keeps Han, Hiragana,
Katakana and Bopomofo in one segment (splitting at every change of
script cost Japanese text a shaper call per kana-kanji switch).
- Vertical type keeps its logical order, so its glyphs and lines are not
marked right to left (the caret no longer runs backwards down a column).
- PDF import puts Hebrew and Arabic, which PDFs (and VectorCraft's own
export) draw in visual order, back in logical order with the direction
that shows it as drawn; with bidi layout it came back mirrored.
- SVG import pins the direction SVG implies (left to right unless
direction: rtl); SVG export also outlines right-to-left paragraphs.
- Docs (development.md), ROADMAP (folded into the Type entry, Type core
row) and README updated; catalog rows for the new labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unicode bidi (unicode-bidi, unicode-script, unicode-general-category):
each paragraph's direction from its first strong character, Hebrew and
Arabic shaped right to left, embedded Latin and numbers keep theirs,
lines wrapped in logical order and reordered per line by whole
clusters; source text, undo and style ranges stay logical.
- Caret, hit testing, selection and the arrow keys follow the visual
order.
- New text aligns automatically: right-to-left paragraphs right, others
left (Justify::Auto, text.setStyle {justify: "auto"}); the Paragraph
panel highlights the resolved alignment and has an Automatic alignment
toggle. Explicit alignments, and older documents', are kept.
- SVG export outlines bidirectional text (with a warning) until it can
be written as directional live text.
A file's saved selections are untrusted: on load at most 25 are kept, names
are trimmed, capped at 255 characters and unique, and ids the document
doesn't have are dropped (after fix_next_id such an id would select whatever
object is drawn next). Save and Edit Selection cap names the same way. The
limit, the name cleanup and the default "Selection N" live on
SavedSelection, shared by the engine, the Save Selection dialog and the
Select menu's slot table. docs/mcp.md documents the select.save /
savedList / recall / editSaved commands; ROADMAP counts 13 stubbed menu items
and names what's still missing (Update Selection, the Layers panel menu).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
normalize_panel walks DockTab::ALL and ICON_PANELS instead of repeating the
dock tab ids. vectorcraft://object/{id} turns only a missing object into
-32602; no document or an unreachable app stay backend errors, as before.
docs/mcp.md documents document.node's summary flag and the panel names
open_panel takes; control-protocol.md the window.panel names. The ROADMAP
states the measured speed-up (a leaf read on 2,000 objects: ~20 ms to ~0.06 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 28 rows for strings that landed on main after #317 was written (Layers
panel menu and options, Release to Layers, the Graphics Processor
preference), so the Japanese catalog stays complete.
- Fixes: Live Paint Bucket is ライブペイントツール (was the selection
tool's name), the File Info rating stars read {n} つ星 (were "rays"),
Flare Tool Options and Symbol Sprayer match their tool names, Rectangular
Grid matches its tool (長方形グリッド), recovery data is 復帰用データ
throughout, (今後対応予定) for every roadmap note.
- Consistent spelling: フォルダー, 切り替え, Web サイト, 英語 (米国), and
a half-width colon after every field label, as in the other 200 rows.
- tests: KEEPS_MENU_NAMES gets its own doc comment instead of taking
MENU_KEEP_AS_IS's.
- ROADMAP and docs/development.md: Japanese ships complete, Czech covers
the menus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the crates/ui-egui/src/i18n/pt-br.tsv catalog (Brazilian Portuguese):
every menu label, every tl! literal of the shell and the plural messages
(2533 strings + 13 plurals), written clean-room in ordinary Brazilian
Portuguese computing vocabulary, placeholders and trailing ellipses kept
in lockstep with the English source.
Register the language in i18n::LANGUAGES (code pt-br, native name
"Português (Brasil)", one/other plural rule) so the Language menu, the
preference and the system-locale match (pt_BR → pt-br) pick it up.
Extend the menu-label test to pt-br: it covers every menu label like cs
and ja, and keeps the MENU_KEEP_AS_IS strings (product name, language
names, workspace names, perspective grid presets) in English.
Update docs/development.md and ROADMAP.md accordingly.
The grid was drawn whenever its `visible` flag was set or a perspective
tool was active, and choosing the Perspective Grid tool (Shift+P) didn't set
the flag. So with the tool chosen, View > Perspective Grid > Hide Grid
(perspective.grid.show) turned the hidden flag on instead of off and the
grid stayed on screen however it was toggled.
Choosing a perspective tool now shows the grid (sets `visible`, view state,
no undo step), and the grid, its widget and the Perspective Grid tool's
handles are drawn only while it is visible. Hide Grid hides it with the
tool still chosen; choosing the tool again shows it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Parity: Object › Path › Remove Anchor Points now runs the new command (`path.removeAnchors`,
the menu's feature name) instead of `path.deleteAnchors`, which deleted the anchors with their
segments and opened closed paths; that stays the Delete key's command ("Delete Anchor Points",
off the menu). No second "Smart" item.
- Geometry: the refit moves to `vectorcraft_pathops::remove_anchor` on the existing least-squares
fitter (fixed end tangents, a golden-section search for where the curve passes the removed
point) instead of a ~1,000-candidate grid search in geom (about 4 ms per anchor, now well under
0.1 ms). Two straight sides stay one straight side (a rectangle loses a corner and becomes a
triangle), as Illustrator does; non-finite input leaves the handles alone.
- The Delete Anchor Point tool (`path.removeAnchor`) uses the same function, replacing its
heuristic handle stretch; the explicit id/anchor and `fit` params of the new command are dropped
as duplicates of it. The command is enabled by direct-selected anchors (`has_anchors`), not a
UI override.
- The Control bar shows an icon button (the Delete Anchor Point icon, tooltip "Remove Anchor
Points") instead of a 204 pt text button; the labels reuse the translated "Remove Anchor
Points"; zh-hant gains "Delete Anchor Points". The Pen test that pinned Alt-click as plain Pen
(Illustrator's Alt over an anchor is Convert Anchor Point) is dropped.
- Tests for the refit, closed paths, straight sides and bad input; docs/mcp.md and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Illustrator's Alt-click on a layer's eye hides the other layers and, once they are
hidden, shows every layer again; the lock column does the same with Lock Others and
Unlock All Layers. Reuse the panel menu's commands (one undo step each, named as the
menu items) instead of toggling the row's siblings, so a sublayer's or an object's row
stands for its top-level layer. Document it in the ROADMAP and docs/mcp.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Five app screenshots retaken from the current build at 2240x1400 (UI at
1.6x): the hero shows the Outer Glow settings in the Appearance panel, the
Live Blends shot shows the new Layers panel rows, and a new shot shows a
two-point Perspective Grid scene (examples/perspective-city.vectorcraft,
built through the command API).
- "A look around" is a 2x2 grid; captions and alt texts follow the shots.
- The "Made in VectorCraft" art is re-rendered with vectorcraft-cli (the
radial Repeat's gradients now turn with each instance).
- Every image is losslessly optimised with oxipng (7.2 MB -> 4.4 MB,
pixel-identical).
- ASSETS.md rows for the new screenshot and example.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The » at the top of the right dock was only painted. Its header strip is
now a click target (hover highlight, "Collapse to Icons" tooltip) that
runs the new window.collapseDock UI command: the Properties | Layers |
Libraries group hides and its three panels become icons at the top of
the icon column, under a « strip ("Expand Panels") that expands them
again. While collapsed, those icons and window.panel pop the panel out
next to the column like the other icon panels (dock-width flyout, Layers
and Properties given a height that keeps them on screen); expanding with
one popped out shows its tab.
The flyout now follows the icon column's real left edge (recorded each
frame) instead of assuming a 300-point dock, so it also tracks a resized
or collapsed dock. The collapsed state is kept with the UI preferences
and in user workspaces; the built-in workspaces expand the dock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On hybrid-graphics laptops (an integrated GPU driving the display plus a
discrete one), egui-wgpu's default power preference is HighPerformance, so
VectorCraft rendered on the discrete GPU and its frames were copied to the
display through the integrated one. On an Asus ZenBook with Intel Iris + Arc
the window flickered whenever the mouse moved over it (#306, #305).
The canvas is rasterized on the CPU (vello_cpu) and the GPU only composites
the canvas texture and draws the UI, so the integrated GPU is plenty. The
desktop app now asks wgpu for the power-saving adapter by default.
Single-GPU machines are unaffected.
- Preferences > Performance > Graphics Processor (`gpuPreference`:
powerSaving, the default, or highPerformance), with a note that it
applies after a restart. The app reads it from the saved preferences
before the window opens. WGPU_POWER_PREF still overrides it.
- Help > About and `ui.inspect` (`graphicsAdapter`) name the adapter in
use, and it is logged at startup, for future GPU bug reports.
- Docs (README, docs/development.md) and ROADMAP updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#249 made the reduced repros of #234 open, but a real file saved by the
reference app (version 30) still fell back to its screen-resolution preview
with "typecheck in `a dictionary`". Running its prolog to the end needed:
- The standard dictionaries (systemdict, userdict, globaldict, statusdict,
errordict, $error) as values in systemdict, so `/globaldict where
{ /globaldict get begin } if` gets a dictionary, not an operator.
SharedFontDirectory names the font directory too. errordict has a
handleerror for prologs that wrap it.
- `dictstack`, `internaldict`, `currentsystemparams` and
`currentuserparams` (with the cache sizes programs `get`), and `cshow`.
- Executable files: `cvx` and `cvlit` set a file's attribute, `xcheck`
reads it, and executing one runs its data as a program, as the prolog
does with `(...) 0 () /SubFileDecode filter cvx exec`.
- SubFileDecode with a count of 0 and an empty end string passes its
whole source through instead of nothing.
- `flushfile` on a filter over the program's data skips that data (the
file's XMP packet). A filter chained from a pending one leaves it
nothing to skip, so the writer's own `VCsrc flushfile` is unchanged.
The interpreter empties its standard dictionaries when it finishes, since
systemdict now holds itself. Both real files tested now import as 15
vector paths in CMYK with no warnings, and round-trip through SVG and PDF
pixel for pixel. The EPS fuzzer exercises the new operators.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a bounding-box handle of area type with the Selection tool now
resizes the type area: the text reflows at its own size (threads too)
instead of being scaled, and History says "Resize Type Area". With other
objects selected they scale as before while the area type resizes its
frame. Direct Selection picks the frame's corners and edges and reshapes
the area ("Reshape Type Area", new command text.reshapeArea), and the
selected frame's anchors and outline show. Area Type Options gains Width
and Height. Point type, rotation, Object > Transform > Scale, the Scale
tool and the Transform panel still transform the type.
Root cause: every handle drag previewed object.transform, which folds the
matrix into the text's xf (TextObject::transform), so area type scaled its
glyphs; and Direct Selection's anchor hit test only looked at paths, so the
frame's corners could not be picked. object.transform now takes
typeAreas, which reshapes the frame in text space (xf^-1 * m * xf), and
the tools use it and text.reshapeArea.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A double-click on a tool button runs tool.options, which only knew a few
tools, so the common double-clicks did nothing: the Hand tool now fits
the artboard in the window, the Zoom tool shows 100%, and Rotate, Scale,
Reflect and Shear open their Object > Transform dialog (when something
is selected, like the menu items).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The export tool's description, the docs' open_file and export rows and the
CLI's run/convert usage listed only some formats. The CLI usage now prints
the writable extensions next to the readable ones, both generated from the
format table (export_extensions), the export tool points to its generated
format enum, and the docs name EPS, DXF, EMF, WMF, SVGZ and templates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paint.proxies gives swatch-linked paint as {type, color, swatch, tint};
document.inspect gives it as a label such as "G1 40% (#ff9999)". The tints
section and the inspect_document row now say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A type node's fill, stroke and strokeWidth now come from its first run, as
paint.proxies and its strokeOptions already did; fills and strokes of the
type object itself are reported apart as objectFill, objectStroke and
objectStrokeWidth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.exportSelection (the MCP export tool's `selection: true`) now
replies with `format` and the encoder's `warnings`, the same list a
whole-document export gives: lossy WebP written lossless, blend modes left
out of SVG Tiny 1.2, PDF layers at 1.4 and the rest. A selection export
that approximates nothing replies with an empty list.
Root cause: export_selection encoded with `encode`, which drops the
warnings encode_with_warnings returns, and its reply carried only path,
bytes and bounds. The params doc and docs/mcp.md now describe the reply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japanese covered 36 menu labels after #145; every other label stayed
English. ja.tsv now holds all 556 interface menu labels, in menu order,
including both readings of every toggle, plus the labels added since: the
Language menu's Automatic and the canvas context menu's. #145's 36 entries
are unchanged. Kept in English, as for Czech: the product name, language
names, OpenType, the built-in workspace names and the bracketed perspective
grid presets.
The Czech coverage test becomes menu_catalogs_translate_every_menu_label,
run for Czech and Japanese. ROADMAP and docs/development.md say both cover
every menu label.
Ported by the maintainers from the Rust table of the original PR onto the
catalog framework (#205): the placeholder labels of recent-file, recent-font,
user-preset and user-library slots are left out (the menus show the names).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A line that is not a JSON object with a string `method` (an HTTP request
line, other non-JSON text, a JSON array, invalid UTF-8, a line over 4 MiB)
now gets one error reply and the connection is closed, so a browser's
cross-origin fetch can't smuggle a command in its body. Connections are
capped at 16. Same transport as LightCraft (storytold/lightcraft#129) and
FilmCraft (storytold/filmcraft#123).
EPS files the reference app saves (its procsets in the prolog, a binary header with a TIFF
preview) now open as editable art instead of failing with "undefinedresource in Generic". When
their PostScript still can't be read, the palette preview they carry is placed with the usual
warning instead of the file being refused.
Root cause: five interpreter gaps and one preview format. The Generic and Category resource
categories didn't exist, so categories couldn't be made from Generic (now Category holds the
standard categories, defineresource in Category makes new ones, undefineresource is added);
clipsave/cliprestore were unknown (now a clip stack in the graphics state); systemdict's
languagelevel was the operator, not the integer 3; getinterval copied arrays and strings
instead of sharing their storage, so astore into a subarray was lost (arrays and strings are
now views into shared storage, and readstring/readline return the part they fill); and
resourceforall was unknown (now enumerates matching names with * ? \ templates). The preview
is an 8-bit palette TIFF with an alpha sample, which the image crate can't decode: a small
bounds-checked reader handles that layout (either byte order, any strips).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release.yml's comments sent readers to docs/releasing.md, which never
existed: the workflow was ported from a sibling app without its document.
docs/releasing.md now covers how a release is cut (version bump with
cargo xtask version, the release branch, the draft, publishing, test runs
with a version override), what each job builds and how (macOS universal
signing and notarization, Windows x64/x86/arm64 with the PE checks and the
two signing options, Linux packages and the glibc 2.35 floor behind the
ubuntu-22.04 runners, FreeBSD in a VM, the web build, craft-fonts), the
draft release job and the signing secrets. Every detail is checked against
this repository's workflow and packaging scripts. The README links it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Strings in code stay English and are the lookup keys; `crates/ui-egui/src/i18n`
maps them to display text at render time from one `*.tsv` catalog per language
(`LANGUAGES` registry, `tl!`/`t`/`tr`/`tr_ctx`/`tr_id`/`tn`/`fmt`). Command ids,
menu paths, `ui.menu.list`, the control channel, the CLI, MCP, status messages
and undo labels never see translated text, so agents, scripts and the existing
tests are unaffected (under `cargo test` the UI is always English).
Wired: in-window menus and the macOS native menu (rebuilt on a language change),
dynamic labels (Undo X, Reset X, Last Effect: X as templates), the command
palette (matches English and translated text), dock tabs, panel tooltips, the
toolbar (category names now cut by characters, not bytes), the dialog frame, the
shared widgets, the Preferences dialog, the shortcut editor, the hint and status
bars, the Home screen, and the literals of every panel and dialog.
Preference `interfaceLanguage` (User Interface › Language): `auto` or a code.
`auto` follows `VECTORCRAFT_LOCALE`, `LC_ALL`/`LC_MESSAGES`/`LANG`/`LANGUAGE`,
the macOS preferred languages and, on Windows (which sets no `LANG`), the user
locale from `HKCU\Control Panel\International`. `zh-TW`/`zh-HK`/`zh-MO`/
`zh-Hant-*` resolve to `zh-hant`; Simplified locales stay English until a
`zh-hans` catalog exists.
`zh-hant.tsv` (~2,850 rows) is clean-room, in the vocabulary used in Taiwan;
terms shared with PhotoCraft reuse its wording, vector terms use everyday
vector-drawing vocabulary. Tests: catalog well-formedness (placeholders,
ellipses, command ids, no Simplified characters), locale resolution, and two
coverage tests that fail when a menu string, panel/tool/preference label,
dialog button, hint-bar fragment or `tl!` literal has no row in a language
marked `complete_menus`. `VECTORCRAFT_I18N_DUMP=file cargo test -p
vectorcraft-ui-egui dump_source_strings` lists the strings for translators.
Also: two clippy findings under rustc 1.97 fixed in passing.
Integrated with main (squashed onto it, 42 commits on): the Czech interface
(#213) moves from its Rust table to `i18n/cs.tsv` on the registry (all 556
rows, Czech plural rule, its menu-coverage and glyph tests kept); Find Font's
resolution notes and the canvas context menu's labels are translated and
covered by the catalog tests (`menus::CONTEXT_LABELS`); the glyph test checks
CJK text only when the craft-fonts build input is embedded (#220 moved the
Japanese fonts out of this repository); a language saved by an older version
carries over to `interfaceLanguage`; Windows' locale lookup runs reg.exe by its
full path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server answered tools/* and two fixed resources and nothing else, and
resources/templates/list returned an empty array. This adds the rest of the surface a
client can actually use, against 2025-06-18.
- prompts/list and prompts/get: five workflows (poster, icon-set, recolor,
trace-and-style, export-set) with templated arguments. A missing required argument
is -32602 and names itself.
- resources/templates/list, and resources/read for the templates: object/{id},
command/{id}, effect/{id}, swatch/{name}. Reading one thing at a time instead of
vectorcraft://document/json, which on a real document is thousands of lines an agent
pays for again on every change.
- completion/complete for prompt arguments and template variables, ranked prefix
first, capped at 100 with total and hasMore. The values are read live, so a suggested
format, effect id, command id, trace preset, swatch or object id is one the engine
has.
- logging/setLevel and notifications/message. Nothing installed a log::Log before, so
the engine's log::debug! calls went nowhere; this is the sink, silent until asked.
Installing it is vectorcraft-cli's job rather than the library's, so an embedder
that already has a logger keeps it.
docs/mcp.md gains a Protocol section with the capability table and a Not implemented
section: subscriptions, progress, cancellation, sampling, elicitation and Streamable
HTTP all need the transport to interleave reads, which this one does not — handle_line
answers a line with a reply, so it neither reads an incoming cancellation nor can write
progress before the reply it belongs to.
62 tests (50 unit, 12 property/fuzz). protocol_props.rs gained five: prompts survive
junk arguments, completion/complete never panics and always answers at most 100
string values with a consistent hasMore, every template reads or fails cleanly, and
prompts/completions/templates are swept with junk params while asserting the document
comes back byte-identical.
ROADMAP: one bullet under Automation in Shipped so far and the changelog line. The
parity table and the gap list are untouched — MCP is agent automation, so feature
parity does not move.
macOS keeps faces for its own interface under family names that start with
"." (".SF NS", ".LastResort", ".Aqua Kana"…); Mac apps don't list them. Since
the font menus list every installed family (#36), they showed 74 of them on a
stock macOS, at the top of every list.
- FontDb::menu_family_list: family_list without names that start with ".",
cached like it. Only a leading dot hides a family.
- Type › Font, the family dropdowns (Character, Properties), text.fontList and
Refresh Font List's count use it.
- Lookups by name still see every family: documents and fallbacks that name
a hidden family still find it (face, has_family, find_family, families,
text.fontList {family}, Fonts in Document's missing check).
- Test: tests_sysfonts::font_lists_leave_out_hidden_system_families_which_still_resolve
(a temporary font folder); the engine and UI font-list tests compare with
the listed families.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-clicking the canvas now opens a context menu, as the reference app
does. The object under the pointer is selected first unless it already is.
The menu then lists what applies to the selection: Undo and Redo, Cut, Copy,
Paste, Isolate Selected Group or Exit Isolation Mode, Group and Ungroup,
Join and Average, Make or Release Clipping Mask and Compound Path, Make
Guides, the Transform, Arrange and Select submenus, and Export Selection.
On empty canvas it lists Paste, the zoom commands, the ruler, grid and guide
toggles and Select All. Commands that can't run now are left out (Undo and
Redo stay, greyed out), and a press on the canvas only closes the menu. Agents list the same menu with the
`ui.contextMenu.list` control method.
Root cause: the canvas ignored the secondary button except in the toolbar's
tool flyouts; there was no context menu at all. The menu reuses the menu
bar's items, rendering and dispatch; Object > Transform and Arrange are now
shared lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>