Commit Graph
6133 Commits
Author SHA1 Message Date
dotCooCoo 71a67601a5 Layers: Alt-click selects only on the drawn name, and in an isolated layer
An Alt-click right of a short layer name, inside the wider rename
target, no longer replaces the selection. The isolated layer's own row
now selects the art inside it (layer.selectAll), where it cleared the
selection before. The doc line says the click selects visible, unlocked
art and clears the selection on a hidden or locked layer.
2026-10-29 10:03:20 +07:00
dotCooCoo ec988442b5 Layers: Alt-click on a layer name selects the layer's art
The reference app selects all objects on a layer with an Alt-click
(Option-click) on the layer's name in the Layers panel. In VectorCraft
an Alt-click on a name did what a plain click does: it highlighted the
row and made the layer current, and the art stayed unselected.

An Alt-click on the name of a layer or sublayer now also selects its
visible, unlocked art, as the row's selection square does
(layer.selectAll), and the layer still becomes current
(layer.setCurrent). On a locked or hidden layer the click clears the
selection, as that row's selection square does. On a group or object
row, beside the name, and with Shift or Ctrl/Cmd held as well, the
click does what it did before.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-29 10:03:20 +07:00
dotCooCoo 5c1ab5042f Gradient: the Cmd/Ctrl-click reset paints only the selected characters and gives an unpainted stroke its weight
The reset ran paint.editGradient, which changes every run of a type
object and leaves a stroke without paint at weight 0. It now runs
paint.setFill or paint.setStroke with the linear White, Black gradient:
characters selected with the Type tool take it and the rest keep their
paint, and a stroke without paint gets the 1 pt weight, as a plain click
on the Gradient button already does.
2026-10-29 10:03:12 +07:00
dotCooCoo 85d7913f10 Gradient: Cmd/Ctrl-click on the Gradient Fill box or the Gradient button resets to White, Black
The reference app resets a gradient to black and white when the
gradient button in the Tools panel is Cmd/Ctrl-clicked, and to the
default black and white linear gradient when the Gradient Fill box in
the Gradient panel is Cmd/Ctrl-clicked. In VectorCraft a Cmd/Ctrl-click
on either did what a plain click does, and the reset was only in the
Gradient panel menu.

A Cmd/Ctrl-click on the Gradient panel's thumbnail or on the Tools
panel's Gradient button now makes the gradient of the active Fill or
Stroke a linear gradient with the White, Black stops, through
paint.editGradient, as one undo step. A solid or no paint becomes that
gradient, and with nothing selected the default paint changes. A plain
click keeps its behavior, and the panel menu's Reset to White, Black
is unchanged.

Docs: docs/target-app-parity.md.
2026-10-29 10:03:12 +07:00
dotCooCoo 62b231aaa5 Color panel: Cmd/Ctrl-click and Cmd/Ctrl+Shift-click on the spectrum apply Complement and Invert
The reference app selects the complement of the current fill or stroke
on a Cmd/Ctrl-click of the Color panel's color bar and the inverse on a
Cmd/Ctrl+Shift-click, and with Alt (Option) also held it changes the
nonactive one. In VectorCraft these clicks picked the spectrum's color,
and a Cmd/Ctrl+Shift-click cycled the color modes.

A Cmd/Ctrl-click on the spectrum now applies Complement and a
Cmd/Ctrl+Shift-click applies Invert to the active Fill or Stroke, as
the panel menu's items do: a selected gradient stop or freeform point
is recolored in place, and a solid color goes through paint.complement
or paint.invert. With Alt also held, the click runs the command on the
inactive proxy, which stays behind. A Cmd/Ctrl drag still picks colors,
and plain, Alt and Shift clicks keep their behavior. The panel menu's
Invert and Complement use the same code.

Docs: docs/target-app-parity.md.
2026-10-29 10:03:02 +07:00
dotCooCoo fa75b6f53d Part of #4326: a lost mouse-up after the pointer moved does not click at the press
The released buttons go after the frame's last pointer movement, at its
position. Before, they went first, at the press position, so a press that
moved past the click distance in the same frame ended as a click on the
widget under the press.
2026-10-29 10:02:50 +07:00
dotCooCoo f9fc20b3b9 Part of #4326: a drag whose mouse-up was lost ends once macOS reports no button down
egui ends a drag only on a button release or Escape. When the window
never receives the mouse-up, a drag such as a dock column's resize keeps
following the pointer with no button held.

On macOS, each frame's input now gets a release for every button egui
still holds when NSEvent.pressedMouseButtons reports no button down, at
the pointer's last position. Frames that carry a button event, and
control-channel input that holds a button between steps, are left as
they are.
2026-10-29 10:02:50 +07:00
dotCooCoo 914a109dc9 Scissors: Alt held switches to the Add Anchor Point tool
A click with the Scissors tool cut the path whether or not Alt was
held, and the cursor stayed the Scissors cursor. The reference app's
shortcut list has "Switch Scissors tool to Add Anchor Point tool" on
Alt (Option).

While Alt is held, a click with the Scissors tool now does what a click
with the Add Anchor Point tool does: on a segment it adds an anchor
point with path.insertAnchor in one undo step, and the path is not cut.
The cursor is the Add Anchor Point tool's while Alt is held. When Alt
is let go, the cursor is the Scissors again and a click cuts.

Docs: docs/target-app-parity.md, crates/tools/src/cursors.toml.
2026-10-29 10:02:43 +07:00
dotCooCoo f0c5de420b Type tools: Shift switches between horizontal and vertical, and Alt switches between area and path type
The reference app's shortcut list switches between its Type tools with
Shift and with Alt (Option). VectorCraft's Type tools had no such switch.

While no type is being edited, a press with Shift held makes new type
with the chosen tool's horizontal or vertical counterpart: Type and
Vertical Type, Area Type and Vertical Area Type, Type on a Path and
Vertical Type on a Path. A press with Alt held makes it with Type on a
Path in place of Area Type, or the reverse, in the same writing
direction. With both keys held, both switch. The keys held when the
press starts are the ones that count. The pointer shows the tool the
press acts as, and the chosen tool stays selected. While type is being
edited, Shift and Alt work as they did.

Docs: docs/target-app-parity.md.
2026-10-29 10:02:34 +07:00
dotCooCoo 7bc7ea58b5 Document: Cmd+Alt+Shift+U switches to the next unit of measurement
The reference app switches the units of measurement with
Ctrl+Alt+Shift+U on Windows and Option+Shift+Command+U on macOS.
VectorCraft could set the document's units with document.setUnits or
a right-click on a ruler, but no command stepped through them and no
key did it.

document.switchUnits (Switch Units, default Cmd+Alt+Shift+U) sets the
document's units to the next one in the order a right-click on a
ruler lists them: Points, Picas, Inches, Millimeters, Centimeters,
Pixels, Feet & Inches, Meters, Yards, Feet, and from Feet back to
Points. Each press is one undo step, and the reply names the new
units. Keyboard Shortcuts lists it under Document.

The precise cursors shortcut-editor test used Cmd+Alt+Shift+U as a
free key; it now uses Cmd+Alt+Shift+Q.

Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
2026-10-29 10:02:27 +07:00
dotCooCoo a4ba07dd1b Width tool: Escape deselects the selected width points
With the Width tool, Escape did not deselect the selected width points:
they stayed selected, Delete still removed them, and in isolation mode
the same Escape left isolation mode. The reference app's shortcut list
gives Esc for deselecting a width point.

Escape now deselects the Width tool's selected width points. The path,
its widths and the undo history do not change, and no command is sent.
While width points are selected the tool takes Escape, so that Escape
does not also leave isolation mode. With no width point selected, and
during a drag, Escape does what it did before.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-29 10:02:18 +07:00
dotCooCoo aa503c2648 Arc tool: C, F and Up/Down change the arc while it is dragged out
The reference app switches an arc between open and closed with C,
flips it with F and changes its angle with Up/Down while it is dragged
out. VectorCraft's Arc tool ignored these keys during a drag.

While an arc is dragged out, C now switches it between an open and a
closed path, F switches its base between the X and Y axes in the same
box, and Up/Down raise and lower its slope by 10 within -100 to 100.
Each press switches once. The arc drawn on release uses the new values,
and they stay as the Arc tool's options. With nothing dragged, C and F
are shortcuts as before.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-29 10:02:03 +07:00
dotCooCoo 46721fe093 Blob Brush: a release away from the press point paints with Alt down
A release that arrives at a new position with no drag before it now counts
as movement, so Alt pressed at that moment no longer switches the stroke
to the Smooth tool.
2026-10-29 10:01:53 +07:00
dotCooCoo 7e25203660 Blob Brush: Alt switches to the Smooth tool only before the pointer moves
Alt switched a Blob Brush stroke to the Smooth tool while the stroke held fewer than two
points, so a drag that had moved less than the sampling tolerance still switched when Alt
went down. The tool now records whether the pointer has moved from the press point at all.
A release with Alt held, after a press without Alt and no movement, also smooths instead of
painting a dot, matching the Smooth cursor shown before it.
2026-10-29 10:01:53 +07:00
dotCooCoo 80bf11dd8b Blob Brush: Alt switches to the Smooth tool while it is held
The reference app switches the Blob Brush to the Smooth tool while Alt
(Option) is held. In VectorCraft an Alt drag with the Blob Brush painted
a blob like any other drag.

With Alt held at the press, or pressed before the drag has moved, the
Blob Brush shows the Smooth tool's cursor and a thin line without the
length label, and the release smooths the selected paths under the
drag with path.smoothRegion, as the Pencil does when Alt key toggles to
Smooth Tool is on. Shift does not constrain a smoothing drag, a stroke
that has moved keeps painting when Alt goes down, and with nothing
selected the release changes no path.

Choosing another tool during a Pencil smoothing drag no longer leaves
the Pencil showing the Smooth tool's cursor when it is chosen again.
deactivate now clears the smoothing state, for the Blob Brush too.

Docs: docs/target-app-parity.md.
2026-10-29 10:01:52 +07:00
dotCooCoo 6bb5398119 Pencil: Alt smooths as far from the drag as the Smooth tool does
With "Alt key toggles to Smooth Tool" on, an Alt drag with the Pencil
smoothed within the Pencil's "Edit selected paths" Within distance. At
0, which turns editing off, it smoothed nothing. It now smooths within
the Smooth tool's 12 px radius at every Within value.
2026-10-29 10:01:33 +07:00
dotCooCoo 4116fdbe47 Shape Builder: a merge looks up the picked points of a coverage once
The search for the faces a merge unites compared each reported face's
coverage with every picked point's coverage. The picked points are now
grouped by coverage once, before the walk, and each face is tested only
against the points of its own coverage.
2026-10-29 10:01:22 +07:00
dotCooCoo 32601a6bc3 Shape Builder: a merge walks the arrangement once per batch of faces
A merge found the contours of the faces it unites with one contour pass
over the whole arrangement for each coverage among them, and the search
for slivers next to them did the same for each neighboring coverage.
Each in-place remnant of a merge is such a merge too, so a click on
many overlapping shapes made one pass over the whole arrangement for
each coverage, for the merged faces and again for each remnant. Both
now use the batching that Divide's regions use: one pass for each batch
of coverages whose faces share no vertex. The batching looks a face's
coverage up by a hash of the inputs that cover it, computed as it walks
them. The search for slivers looks up the merged faces' coverages the
same way.

Merging a frame no longer takes in an island of the same coverage in
its hole: each face is its outer contour and the holes directly in it,
as the regions are.
2026-10-29 10:01:22 +07:00
NetterSilverandClaude Opus 5.5 8b4ebf50be Fix Shift-scaling a corner handle along a single axis
Fixes #4262

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:00:59 +07:00
dotCooCoo 7a2683d6d0 Stroke Weight: a spinner click from a weight shown as a preset goes past it
A weight of 2.004 pt shows as 0.167 p in picas, the same text as the
2 pt preset. One click down on the spinner went to 2 pt and the field
did not change. The spinner now treats a preset shown with the same text
as the weight as reached, as the field's arrow keys do, so the click
goes on to 1 pt. A plain step also passes a whole-point step shown with
the same text as the weight: 2.0004 pt goes down to 1 pt.
2026-10-29 10:00:43 +07:00
dotCooCoo c252d22465 Stroke Weight: a step from a weight between the presets goes to the next preset
A weight between the presets stepped to the next whole point, so 0.52 pt
went down to 0 pt. In the Stroke panel, the
Control bar and the Properties panel, a plain step of the spinner, the
arrow keys or the wheel now goes to the nearer of the next whole point
and the next preset in its direction: 0.52 pt goes down to 0.5 pt and
up to 0.75 pt, 0.4 pt goes down to 0.25 pt, and 1 pt goes down to
0.75 pt. From 1 pt up a step is still a whole point (14 pt to 15 pt),
and Shift and Ctrl/Cmd steps are unchanged.

In the field, a preset keeps its decimal places (0.0156 in), and a preset
the field shows with the same text as the value (2 pt as 0.167 p) counts
as reached, so the next press goes on to the next preset.
2026-10-29 10:00:43 +07:00
dotCooCoo 686cd1f2f5 Curvature: deleting a point re-curves the path through the remaining points
Deleting a point of a finished path with the Curvature tool sent
path.removeAnchor, which refits the two segments that met at the point so
one curve follows them. The path kept the bend of the deleted point
instead of curving through the points that remain.

The tool now sends path.removeAnchor with curvature: true, which uses the
new curvature_remove. Each smooth neighbor takes new handles from its new
neighbors, and an end beside one of them takes its end handle again. A
corner neighbor keeps its handle directions, and its handle on the new
segment scales with the segment's chord. With two points left, the points
that curve lose their handles. Object > Path > Remove Anchor Points and
the Delete Anchor Point tool still refit the curve.
2026-10-29 10:00:30 +07:00
ham9zah 4661615af4 Properties: test that typed transforms turn about the reference point (#4285) 2026-10-29 10:00:18 +07:00
NetterSilverandClaude Opus 5.5 ec98f498fd Apply Transparency opacity to the clicked gradient mesh point
Fixes #4206

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:00:05 +07:00
el-j c0d7d22d42 Fix #4151: verify continuous angular direction for cubic arc conversion (#4151) 2026-10-29 09:57:34 +07:00
el-j a24326e046 fix(cad): verify circular subpaths complete a full turn without reversing (#4151) 2026-10-29 09:57:34 +07:00
el-j 7f48664fed test(cad): add regression test for closed sliver along a circle in DXF (#4151) 2026-10-29 09:57:34 +07:00
dotCooCoo 8e93155ed9 Tests: the idle cursor test renders every frame on the test thread
the_canvas_hands_the_tool_cursor_to_the_os_and_idles_while_the_pointer_rests
asserts that the window asks for no repaint once the pointer rests. On a
loaded machine a canvas frame takes longer than 8 ms, so later frames go
to the render worker, and a worker frame that finishes during the idle
frames asks for a repaint (repaint delay 0, cause render_worker.rs:63).
The test now sets canvas.worker_started, as the canvas tests do, so every
frame renders on the test thread.
2026-10-29 09:57:34 +07:00
dotCooCoo c3a6fbf250 Appearance: Add New Fill and Add New Stroke with an empty ids list run nothing without a selection
Called with `"ids": []` and nothing selected or targeted, the two commands
ran and put "select objects or give ids" in the status bar. An empty ids
list now counts as no ids, so the commands run nothing, as they do when
ids is left out. A non-empty ids list still adds to those objects.
2026-10-29 09:57:34 +07:00
ham9zah 411f96b7c8 Appearance: the object chip of a selection whose fills differ shows "?" (#4174)
The object row drew the None swatch when the selected objects' fills
differ, which reads as "no fill". It now draws the "?" chip the Control
bar and Properties panel use for differing paints.
2026-10-29 09:57:34 +07:00
HassanKhanandClaude Opus 5.5 a5961f264b Fix #4237: Add Anchor Point selects the path it adds to
An anchor added to a path that wasn't selected now selects that path,
the new anchor direct-selected, so the point shows and can be edited at
once instead of only after a Direct Selection click. Undo restores the
old selection; a path already selected (the Pen's Auto Add) keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:57:34 +07:00
NetterSilverandClaude Opus 5.5 bcbc8a1713 Expand the stroke of stroked type into filled outlines
Fixes #4306

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:57:34 +07:00
dotCooCoo 6026772765 Fix #3773: Offset Path offsets a path whose ends meet as a closed shape
Offset Path outlined an open path on both sides, so a shape whose last
anchor sits on its first came out as a ring with a hole. Such a path is
now offset as a closed shape: its last anchor merges into its first,
which takes the last anchor's in-handle, with the same 1e-6 test that
Object › Path › Join uses for one path (crates/engine/src/cmd/path.rs).
The Offset Path effect calls the same function.
2026-10-29 09:57:34 +07:00
NetterSilverandClaude Opus 5.5 b6422e5ba1 Keep compound path holes when using the Eraser and Knife
Fixes #4495

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:57:34 +07:00
el-j c96f0c30f7 perf(ui-egui): eliminate zero-filling allocation on canvas texture upload in color_image (#4504)
- Replace vec![Color32::TRANSPARENT; N] zero-initialization with direct single-copy bytemuck::cast_slice(&img.pixels).to_vec() in color_image.
- Eliminates redundant zero-filling of ~20.7 MB buffer on every canvas frame render on Retina displays, saving memory bandwidth and allocation overhead.
- Fallback preserves transparent image for undersized or mismatched pixel buffers.

Fixes #4504.
2026-10-29 09:57:34 +07:00
bflatastic ace60bd542 Edit Colors: import f64_req and check_numbers in colorcmds.rs (#4299 made the imports explicit) 2026-10-29 09:57:34 +07:00
dotCooCoo 78c51b86e6 Docs: Edit Colors and Color Guide read a null parameter as one left out 2026-10-29 09:57:33 +07:00
dotCooCoo 8c0750e848 Edit Colors: Saturate without an intensity is an error instead of a pass at 0
edit.colors.saturate's params text names intensity as required, but a
call without it, or with a string such as "40", read the intensity as
0 and still ran Saturate. It changed no visible color but recorded a
Saturate undo step.

edit.colors.saturate now reads intensity with f64_req, as shape.ellipse
reads its box. A missing, null or non-number intensity is the error
"missing number `intensity`", which changes nothing and records no undo
step. A number outside -100..100 still counts as its nearest end, and
the params text says so.

Saturate returns a color whose saturation stays the same, as at an
intensity of 0, as it was. The HSB round trip rewrote most such colors
in their last bit, so they counted as changed and lost their swatch
link.

In the same file, edit.colors.adjustBalance read a channel that is not
a number as 0, and color.harmony read steps or amount that is not a
number as its default. Both now refuse it with check_numbers, as the
commands docs/mcp.md lists for #1952 do. A null is still read as a key
left out.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-29 09:57:33 +07:00
bflatastic 49509be9a8 Layers: import switch_or in layer.rs and layerpanel.rs (#4299 made the imports explicit) 2026-10-29 09:57:33 +07:00
dotCooCoo 4519346cfb Layers: the Layers panel's commands refuse a boolean given as text
layer.new, layer.selectAll, layer.target, layer.move, layer.template,
layer.pasteRemembersLayers and layer.releaseToLayers read their on/off
params with bool_or or as_bool, which falls back to a default for a
value that is not true or false. layer.new {"top": "true"} added the
layer above the current one instead of on top, layer.template
{"on": "false"} and layer.pasteRemembersLayers {"on": "false"} turned
the option on when it was off, layer.move {"copy": "true"} moved the
rows instead of copying them, layer.selectAll and layer.target with
"add": "true" replaced the selection, and layer.releaseToLayers
{"build": "true"} released the objects without building up copies.

These commands now fail with "`<key>` must be true or false" for a
value that is not true, false or null, as the artboard and Object menu
commands do, and leave the document, the selection, the highlighted
rows and the undo history as they were. layer.newSublayer, which takes
layer.new's options, refuses such a top too. A null is read as a key
left out.

docs/mcp.md also lists showCenterMark, showCrossHairs and
showVideoSafeAreas among the keys artboard.new and artboard.setProps
refuse in this way, as they already did.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-29 09:57:33 +07:00
dotCooCoo 0622a10415 Docs: the artboard refusal line names null and every other wrong type 2026-10-29 09:57:33 +07:00
dotCooCoo 1deb1ec152 Artboards: artboard.new and artboard.setProps refuse a number or name of the wrong type
artboard.setProps refused an x, y, width or height given as text, but
artboard.new read such a value as the key left out: {"x": "10"} added
an artboard at the default position and recorded an undo step. Both
commands read a name that is not a string, such as 5, as no name, so
artboard.new gave the default name and artboard.setProps kept the old
one.

artboard.new now refuses an x, y, width or height that is not a number
with "`<key>` must be a number, not <value>", the message
artboard.setProps gives, and both commands refuse such a name with
"`name` must be a string", for each entry of artboard.setProps'
artboards too. They refuse it before they change anything. A null is
still read as the key left out.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-29 09:57:33 +07:00
NetterSilverandClaude Haiku 5.5 ad43722a56 Accept whole-number floats as grid divider counts
Found while testing crates/engine/src/cmd/create.rs:584 (dividers)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 09:57:33 +07:00
bflatastic cfe21231c8 SVG import: import NodeId and SvgError in importer.rs (#4299 made its imports explicit) 2026-10-29 09:57:33 +07:00
el-j 39341ef4c8 Fix #3564: report TooManyImages error when nested SVG image cap is reached 2026-10-29 09:57:33 +07:00
el-j b65fb23338 fix(svg): cap converted objects and nested images (#3564) 2026-10-29 09:57:33 +07:00
Don Omondi def9e0f08e Blend: nested blends keep to a step budget (#3579)
A blend whose keys are blends draws each of its steps as a copy of a key,
and that copy makes its own steps when it is drawn, so step counts capped
per pair multiplied with nesting: five levels of 15 steps were 1.3 million
steps from a 23 KB file, 3.4 GB to export.

A blend now makes at most 100,000 steps in all (MAX_BLEND_STEPS), counting
the steps of the blends inside its keys and steps. Past the budget each pair
of keys keeps the same share of its steps; a copy of a blend spaced by
distance counts the most steps its spacing can make, since scaling it up
adds steps. Blends within the budget draw every step as before.
2026-10-29 09:57:33 +07:00
BFlat af95df8eb5 Merge pull request #4514 from TheGalitube/fix/h1576-dxf-integer-fields-truncate-frac
Reject fractional DXF integer values
2026-10-29 09:56:59 +07:00
BFlat f38ae76aeb Merge pull request #4460 from Silvertree2010/fix/h3582-eps-import-postscript-radix-numb
Reject a sign after # in EPS PostScript radix numbers
2026-10-29 09:56:32 +07:00
BFlat a6e4df49b0 Merge pull request #4449 from Silvertree2010/fix/h3554-eps-import-glyph-char-accepts-a
Reject non-hex characters in EPS uni/u glyph names
2026-10-29 09:56:01 +07:00