Seven app screenshots retaken at 2240x1400 (UI at 1.6x), two of them new: a 3D still life with the Revolve dialog (examples/still-life.vectorcraft) and a bracket drawing with linked dimensions (examples/bracket-drawing.vectorcraft). The still life also joins Made in VectorCraft. The older examples are saved again in the current native format; they render the same.
Layers ≡ and row menus collect art via assets.add; a layer id expands to the art under it (including sublayers) so MCP and the panel match Object › Collect for Export.
A corner whose anchor carries a handle (where a curve meets a line, or two
curves meet) was left sharp. It now gets Live Corners' round cut
(geom::corners, #1144), reaching `radius` along each side and trimming a
curved side. Corners between handle-less anchors are rounded exactly as
before, and a closed path still starts where its first corner's rounding
starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Commands that read the selection through edit::selected_roots leave
out an object whose group is also selected (edit::roots_of). Fourteen
places that read `ids` kept an object listed together with its group.
Given ids that list an object and a group that holds it, Make Repeat
failed with "no such object", New Symbol failed or stored the object
twice in the symbol, and object.transform moved it twice (20 pt for a
10 pt move).
These places now pass explicit ids through edit::outermost, which
drops an id whose ancestor is also listed, with the filter that
roots_of runs on the selection (now edit::without_inner). Nothing else
is dropped or replaced: unlike roots_of, it keeps layers and
compound-path members. A list without nesting gives the same result as
before. Commands that take both ids and the selection as listed, such
as paint.setFill without item, are unchanged.
Docs: docs/mcp.md, docs/target-app-parity.md.
Effect › Distort › Glass… offered only its four built-in textures. The
reference app's Texture menu also has Load Texture, which takes an
image file as the glass surface.
The Glass dialog now has a Load Texture… button. It reads a PNG, JPEG,
GIF, WebP, TIFF, BMP, PSD or PSB file within the pixel allowance the
renderer draws images in, and the Texture menu gains Loaded Texture.
The effect stores the image's gray levels in `textureImage` as a
grayscale PNG in base64, so the document saves the texture with the
effect, and copies of the effect carry it. effect.apply and
effect.setParams take `textureImage` as {path}, {name, dataBase64} or
the stored string; any other value is an error.
The texture lies one image pixel per point at Scaling 100%, centered on
the object and repeated. Each pixel's luma is the surface's height,
from 0 for black to 1 pt for white, and a transparent pixel counts as
middle gray. A Glass without `textureImage` draws and saves as before.
Docs: docs/mcp.md (Glass textures), docs/effects-parity.md,
docs/gaps.md, docs/target-app-parity.md.
Unit fields and length strings read a percentage operand as a share of
a value. After * or /, the percentage scales the term before it:
3cm*50% is 1.5 cm and 3cm/50% is 6 cm. After + or -, it adds or takes
away that share of everything before it: 50pt+25% is 62.5 pt and
100pt+20pt-50% is 60 pt. Before this change these expressions read as
nothing, and the field kept its old value.
The parser now splits a sum at its last + or -, so a percentage applies
to the whole sum before it. Text without % gives the same values as
before, apart from float rounding. A percentage on its own (50%) still
reads as nothing, and percent and degree fields still read % as their
suffix.
Command length strings use the same parser: file.new's width and
height, the distance params and the length preferences.
Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
Exports bake live effects into plain art, and a Transform effect on
type or on its group or layer turned the type into glyph outlines, even
when the effect only moved it or made moved copies. A PDF reader could
not select or search that text.
The export bake now keeps such type as type, moved into each place the
effect puts its outlines. It does this for Transform effects without
scale, rotation, reflection or Random, for type whose characters are in
solid colors without underline or strikethrough, and that has no fills
or strokes of its own, inline graphics or perspective, and when the
copies do not overlap. Type under such an effect on its group or layer
also needs no transparency or effects of its own, and the group or
layer needs no fill, stroke or Pathfinder effect of its own. The export
still outlines other type under geometry effects, and the canvas and
Expand Appearance still draw outlines.
Docs: docs/mcp.md, docs/target-app-parity.md.
The PostScript interpreter that reads EPS and PostScript files turned
the samples of a CMYK image into display RGB and stored an RGBA PNG,
so the image lost its ink amounts. The DCTDecode filter decoded a CMYK
JPEG to RGB, so it lost its ink amounts too. The PDF importer keeps
the ink amounts of 8- and 16-bit CMYK images (in a CMYK TIFF, or the
CMYK JPEG itself).
A CMYK image (image or colorimage in DeviceCMYK, CIEBasedDEFG or a
four-component ICCBased space) now keeps its samples in a CMYK TIFF
(ImageBlob::cmyk_tiff), read through its Decode array, at every bit
depth the interpreter reads. A CMYK JPEG gives its samples as stored,
and the image's Decode array reads them: [1 0 1 0 1 0 1 0] undoes the
inverted samples of Adobe's CMYK JPEGs. The pixels that a MaskColor
or an ImageType 3 mask hides become a grayscale opacity mask over the
image, as in PDF import. A mask that hides nothing adds no mask. Gray,
RGB, indexed and spot color images and image masks stay RGBA PNGs.
Docs: docs/file-format-parity.md, docs/target-app-parity.md.
When a graphics driver crashed the process while the window started,
as Intel's Direct3D 12 driver (igd12umd64.dll) did on an Intel UHD
Graphics 605 (#2243), the next start picked the same adapter again. The
restart without a failing adapter and the first-frame watch run inside
the process, so the crash ended them along with it.
Each start now writes graphics-start.json in the log folder before the
graphics start, adds the adapter once one is picked, and clears both
when the first frame reaches the screen or the app ends without
crashing. A start that finds an earlier start still recorded there
leaves that adapter out, as a restart does, logs it and shows a note in
the status bar. When the crash came while the adapters were being
listed (wgpu's Direct3D 12 backend creates a device on each GPU to list
it), the next starts list OpenGL's adapters alone, except on macOS. The
adapters stay left out until the app version or the Graphics Processor
preference changes, a start shows its first frame on one, or no adapter
is left. WGPU_BACKEND and WGPU_ADAPTER_NAME choose regardless of the
record.
Docs: docs/development.md, docs/target-app-parity.md.
A PDF, PDF-compatible .ai or .ait opened from its pages had an empty
File Info. Nothing read the file's document information
dictionary or its XMP packet, so file.info reported no author,
description or keywords, even for a PDF that VectorCraft exported.
vectorcraft_pdf::file_info reads both. The author, description,
keywords, copyright status, notice and URL and the created and modified
dates come from the XMP packet (dc:creator, dc:description, dc:subject
or pdf:Keywords, xmpRights:Marked, dc:rights, xmpRights:WebStatement,
xmp:CreateDate, xmp:ModifyDate), else from the dictionary's Author,
Subject, Keywords, CreationDate and ModDate. document.open,
file.newFromTemplate and file.revert put each value through file.info's
checks and leave out a value that file.info refuses. A packet that is
not well-formed XML leaves the dictionary's values. File Info does not
set the title, and a file restored from the document it carries keeps
that document's File Info. vectorcraft-pdf depends on roxmltree 0.21,
which usvg already brings into Cargo.lock.
Docs: docs/mcp.md (File Info), docs/file-format-parity.md,
docs/target-app-parity.md.
When type had used a font, removing the font while the app ran left
it in the font menus, and the type kept drawing in it until the app
restarted. The font list is the loaded faces plus the scanned catalog,
and a scan replaced the catalog but kept every loaded face.
A scan now drops each loaded face whose file it did not catalog, with
the face's cached outlines. A family with no face left leaves the font
lists, and type set in it lays out in the fallback font and is
reported missing, as when a document opens without the font. When the
font is installed again, the next scan lists it and the type draws in
it again. Bundled fonts and fonts added as bytes have no file and
stay.
Docs: docs/mcp.md, docs/target-app-parity.md.
Adds a Finnish (Suomi, `fi`) catalog with 4,770 entries, written in
sentence case from the English labels with ordinary Finnish
vector-illustration vocabulary. It covers menus, `tl!` literals and the
668 `@msg` status and error messages: `fi` is in KEEPS_MENU_NAMES and
COMPLETE_MESSAGES. Like the other complete catalogs, it leaves the
product, workspace and perspective preset names in English.
Plurals are one/other with the partitive singular after any number but
1 (1 taso, 2 tasoa). Any fi-* locale selects Finnish; sv-FI does not.
Tests: `cargo test -p vectorcraft-ui-egui --lib i18n` passes (47),
including the new finnish_reads_as_finnish and the bundled-font glyph
check.
Clean-room, AI-assisted (Claude Code) translation; a native speaker
checked it in the running app, but it has not been proofread line by
line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds Swedish (Svenska) as an interface language: 4770 strings, written from the English source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1290 (from #1275) declared the glibc floor as `libc.so.6(GLIBC_2.35)(64bit)`. That doesn't stop
RHEL 9: its current glibc, `2.34-283.el9_8`, provides `libc.so.6(GLIBC_2.35)` through backports. On
Rocky Linux 9, dnf upgrades glibc to that build, installs VectorCraft 0.10.0, and `vectorcraft-cli`
then fails with ``/lib64/libm.so.6: version `GLIBC_2.35' not found``. The binaries' only GLIBC_2.35
symbols are `hypot` and `hypotf`, from libm, and RHEL 9 doesn't provide `libm.so.6(GLIBC_2.35)`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spell out dataset.list/capture/update/delete/rename and distinguish the MCP
command_batch tool from the engine command.batch Actions play.
Co-authored-by: Cursor <cursoragent@cursor.com>
Play an action on every file in a folder or every data set, with destination
and Override Open/Save/Export. Actions panel and File › Automate › Batch…
open the dialog; new strings in pt-br.
Co-authored-by: Cursor <cursoragent@cursor.com>
VariableKind::Graph binds a graph root; dataset rows supply CSV as graph.setData.
Make Graph Dynamic, Variables dialog, capture/apply, and pt-br for the new strings.
Co-authored-by: Cursor <cursoragent@cursor.com>
Load and save variable datasets as CSV for data-merge pipelines, and
translate the new Variables panel commands in Brazilian Portuguese.
Co-authored-by: Cursor <cursoragent@cursor.com>
# Conflicts:
# docs/target-app-parity.md
Store the registration point with the symbol and place instances so that point lands on (x, y). Symbol Options exposes the same 9-point control as Transform.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose existing Warp styles, orientation and percentage controls through the shared live effect dialog while preserving preview history and effect targets. Keep ordinary Enter owned by focused non-searchable combo controls.
Co-authored-by: Codex <noreply@openai.com>
assets.export of abutting fills must leave no ghost edge (#983 / #1037); panel thumbnails use the same Art Optimized supersample. Progress docs mark the gap closed.
Keep surviving drawable runs separate when Delete or Backspace removes an open-path anchor. Preserve appearance, selection and one-step Undo.
Co-authored-by: Codex <noreply@openai.com>
Make and Edit choose the Intertwine tool: hovering highlights the overlap
area under the pointer, a click swaps which object is on top there, and a
path drawn around overlaps (Shift: a rectangle) arranges every overlap
inside it; Esc or Enter goes back to the tool chosen before. The overlap
faces move to vectorcraft-tools, shared by the tool and the command.
Clicks inside a raised area pick the member drawn on top there, the
Object menu gets the Intertwine submenu, and the new labels and the
predicate message are translated in the complete catalogs. docs/gaps.md
records what landed under G13.
A step whose id is draw_path or draw_shape runs that tool with its params,
checked as tools/call checks arguments; its result is the tool's, so later
steps can use "$N.id". The reply lists the ids the drawing steps made
under created. The unknown-argument check is shared with tools/call.
The drawing tools style the object they draw in the same call: dash, cap
and join go to the stroke.set that strokeWidth already sends, opacity and
name to object.setProps, and effects are applied in order as apply_effect
does. The options are checked (effect ids against effect.list) before
anything is drawn, so a bad one leaves no half-styled object behind.
A gradient given to several objects fits each one's own bounds. With
span: "selection", a linear or radial gradient without its own vector is
placed once on the union of the targets' bounds, at its angle and aspect,
and every target gets that vector in document coordinates, so the ramp
runs on across them. A solid colour, a swatch, a freeform gradient or an
explicit start/end with it is refused before anything changes; the next
object drawn still fits the gradient to itself.
The nine OFL faces in `assets/fonts` are compiled into every build of
vectorcraft-text: 3.4 MB of the binary, about 1.6 MB of a gzipped wasm.
An embedder that loads every font at run time with `FontDb::add_font`
(a web app serving its own font catalogue, say) ships them twice and
can't turn them off.
- vectorcraft-text: `bundled-fonts`, a default feature, holds the
`include_bytes!` list. Off, a database starts without them (craft-fonts
and system fonts are unaffected) and text has no glyphs until a font
is added, as when the bundled fonts fail to parse.
- vectorcraft-engine: a `bundled-fonts` default feature forwards to it,
so the apps and any embedder using the engine keep the fonts unless
they build it with `default-features = false`.
- The workspace declares vectorcraft-text without default features, so
no library crate turns the fonts back on. The tests of cad, metafile
and tools, which lay type out, ask for the feature in their
dev-dependencies; the other crates' tests get it through testkit.
`cargo test --workspace` builds with the fonts, as before. A new
integration test, `without_bundled_fonts`, covers the configuration
without them (`cargo test -p vectorcraft-text --no-default-features
--test without_bundled_fonts`): the database starts without them, text
in no font lays out with no glyphs, and a font added later is used and
is the fallback for families the database lacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Curvature tool built its curve as a uniform Catmull-Rom: both handles of a
smooth point a sixth of the chord between its neighbours, the ends of an open
path without handles. Uneven spacing overshot or looped, a J or U from three
points bent sharply at the middle with straight legs, and the end segments
looked flat.
- `curvature_spline` (draw2.rs) makes each stretch of three points follow the
circle through them: a smooth point's tangent is that circle's
(`smooth_handles`), each handle `chord / (3 cos²(α/2))` long, α between
handle and chord capped at 90° (`arc_handle`). The ends of an open path
mirror their neighbour's tangent in their chord (`end_handle`). A turn
tighter than 30° (`TIGHT_TURN`) shortens its handles, so a narrow V comes to
a point. Degenerate points become corners. `path.curvature` and the rubber
band use it.
- `catmull_rom` is unchanged for the Pencil and Paintbrush fitting.
- `bend_last` (from: smooth), `curvature_move` on a smooth point, the point
`curvature_insert` adds on a straight segment, and `path.convertAnchor` to
smooth without x/y (`curvature_smooth`) use the same rule.
- `curvature_extend` gives the new end its end handle unless Alt-clicked
(`corner` param on `path.curvatureEdit`).
- Neighbours of a point dragged on an existing path keep their tangents (a
Pen-drawn shape isn't reshaped); the path being drawn re-curves throughout.
When entering text edit mode via double-click from a selection tool
(Selection or Direct Selection), pressing Escape now returns to the specific
tool used before double-clicking (rather than always falling back to
Selection).
Additionally, when editing text while in isolation mode (such as when drilling
into nested groups with the Selection tool), pressing Escape exits text editing
while keeping isolation mode active; a second Escape press is required to exit
isolation mode.
- Record the previous tool in TypeTool on double-click from canvas.
- Return to prev_tool on ToolKey::Escape, consuming the key in shortcuts when claimed.
- Add unit test in vectorcraft-tools verifying Escape returns to prev_tool.
- Add integration tests in vectorcraft-ui-egui verifying Escape returns to Direct
Selection and that isolation mode persists until a second Escape.
- Update docs/target-app-parity.md and ROADMAP.md.
With the Direct Selection tool ("white arrow", shortcut 'a'), double-clicking
a text element (either standalone or nested inside a group) now switches to
the Type tool and immediately positions the caret at the clicked character,
matching the behavior previously introduced for the Selection tool in #284.
- In DirectSelectionTool::pointer, detect double-click hits on NodeKind::Text
when !self.group and emit Action::SwitchTool("type").
- Group Selection (group: true) retains its existing group cycling behavior.
- Add unit test in vectorcraft-tools verifying tool switch on both standalone
and grouped text.
- Add headless egui integration test in vectorcraft-ui-egui verifying tool switch,
caret positioning, and inline editing without triggering shortcuts.
- Update docs/target-app-parity.md and ROADMAP.md.
Document Info › Objects counts the paths but didn't say how long they are,
which sign and print work reads there (an LED strip along the letters, a
cut line). `document.info` now adds up the length of every path it counts,
the pieces of compound paths included and guides left out, and returns it
as `pathLength` (pt); the Objects section shows it as Path Length in the
document's units, right after the Paths count, so Selection Only gives the
selected paths' length. The label is translated in the catalogs that
cover the menus; pt-br only takes strings the UI itself shows.
The 'Last updated · Change' status lines in docs/roadmap.md, docs/target-app-parity.md and docs/image-trace-quality.md change with almost every merge to main, so this PR's edits to them conflicted each time main moved. They are reverted to main's text; this PR's evidence bullets, history rows and table entries stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
The 'Last updated · Change' status lines in docs/roadmap.md, docs/target-app-parity.md and docs/image-trace-quality.md change with almost every merge to main, so this PR's edits to them conflicted each time main moved. They are reverted to main's text; this PR's evidence bullets, history rows and table entries stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB