Commit Graph
3435 Commits
Author SHA1 Message Date
aiongg 42e11117e2 refactor(ui-egui): move panels/properties.rs tests to panels/properties/tests.rs 2026-10-26 18:18:16 +00:00
aiongg 2377df07ee refactor(ui-egui): move panels/layers.rs tests to panels/layers/tests.rs 2026-10-26 18:18:16 +00:00
aiongg 732d4ded79 refactor(tools): move select.rs tests to select/tests.rs 2026-10-26 18:18:16 +00:00
aiongg 6aa67a5736 refactor(ui-egui): move canvas.rs tests to canvas/tests.rs 2026-10-26 18:18:16 +00:00
aiongg 2cd1a3a3f1 refactor(engine): move cmd/graph.rs tests to cmd/graph/tests.rs 2026-10-26 18:18:16 +00:00
aiongg a042e3ff25 refactor(ui-egui): move dialogs/mod.rs i18n/mod.rs panels/mod.rs to foo.rs files 2026-10-26 18:18:16 +00:00
aiongg 7695057780 refactor(trace): move centerline/mod.rs logo/mod.rs to foo.rs files 2026-10-26 18:15:22 +00:00
aiongg f1ab4965c4 refactor(render): move encode/mod.rs to foo.rs files 2026-10-26 18:15:22 +00:00
aiongg 0b04172c45 refactor(pdf): move print/mod.rs to foo.rs files 2026-10-26 18:15:21 +00:00
aiongg b9a73ba1af refactor(eps): move import/mod.rs import/ai/mod.rs print/mod.rs to foo.rs files 2026-10-26 18:15:21 +00:00
aiongg 62261b17c9 refactor(engine): move cmd/mod.rs cmd/fileio/mod.rs cmd/findfiles/mod.rs cmd/place/mod.rs to foo.rs files 2026-10-26 18:15:21 +00:00
aiongg 6bbfb91d16 refactor(effects): move pixel/mod.rs to foo.rs files 2026-10-26 18:15:18 +00:00
aiongg 6c57ddb063 refactor(color): move cms/mod.rs to foo.rs files 2026-10-26 18:15:18 +00:00
aiongg 777a764a0e test: scan source directories recursively
The EPS error-meaning test walks src/import and its subdirectories. The
picks and tl! literal scans skip directories named tests, as the undo and
message scans already do, so test modules moved to foo/tests.rs or
tests/<topic>.rs are not read as interface code.
2026-10-26 18:05:40 +00:00
nrq6993-a11y b7b107ce24 G11: Join tool connects endpoints near scrub line ends
Squashed from #4272 onto main.
2026-10-26 22:07:58 +07:00
dotCooCoo 1c4ec81ec2 Placed documents: a ready bitmap redraws only its objects, and bitmaps on screen are not made again
On screen, placed documents (PDF, .ai and .vectorcraft) draw from
bitmaps made in the background. Each bitmap that became ready changed
a generation in the canvas key, so the canvas rendered the whole frame
again. The bitmaps share a 512 MiB budget, and making one dropped the
least recently drawn others, even those on screen: with more than that
on screen, every frame dropped a bitmap it drew and asked for it again,
without end. A bitmap that could not be made (the file can't be read
and there is no preview) also changed the generation and was asked for
again in every frame.

When a bitmap is ready, placed_document::made_between names its file,
and the canvas renders again only where the objects showing it are
(placed_area), as it does after an edit, when the frame shown was
rendered on the UI thread and that part is at most half the frame.
Art of a symbol, a graph design, a pattern or a brush that shows the
file still renders the whole frame.

Bitmaps drawn or made since a frame began are not dropped to make room
for the ones it asks for. They stay past the budget up to 2 GiB. A
bitmap that would take them past 2 GiB is not kept, and its file draws
exactly at that zoom. A bitmap that can't be made shows the
placeholder, does not change the generation, and is not asked for
again.

Docs: docs/target-app-parity.md.
2026-10-26 21:41:44 +07:00
dotCooCoo 6ed4608d82 Images: an image past the 2 GiB decode allowance draws at reduced size instead of a crossed box
A PNG, JPEG, GIF or WebP image that takes more than the decode
allowance as RGBA (2 GiB, 512 MiB in the web app), such as a 24000 ×
24000 JPEG, was placed with the warning "too large to draw" and then
drew as a crossed gray box on the canvas and was left out of raster
exports. A 16-bit PNG whose decoded samples were past the allowance
was shown the same way, with the same warning, although its RGBA fits.

The renderer now decodes such an image at the largest size, keeping its
proportions, that fits the allowance as RGBA. Each pixel of the result
is the mean of the image's pixels it covers. A PNG that isn't
interlaced is read a row at a time; other images are decoded whole when
their decoded samples fit the allowance. A 16-bit PNG that isn't
interlaced and whose RGBA fits is read a row at a time at its own size.
An image of more pixels than the allowance has bytes still draws as the
crossed box, and so does one whose decoded samples are past the
allowance, other than a PNG that isn't interlaced. The renderer
decodes an image at reduced size only while the result and the memory
reading it takes (the image decoded whole, or the PNG reader's row
buffers) fit beside the images being painted; otherwise the image is
not decoded on that render (on the canvas it draws as the crossed box)
and is tried again on the next render. The document keeps the whole
file for saving and export. A placed document's saved preview past the
allowance is decoded the same way, at the size it is drawn at or at the
reduced size when that is smaller.

Open, Place and the Links panel say "an image of W × H pixels is shown
at reduced size" for an image drawn smaller. They say nothing for an
image drawn at its own size, and they say "too large to draw" for one
that is not drawn at reduced size.

Docs: docs/mcp.md, docs/target-app-parity.md (the crossed box's line
now names an image over the allowance that can't be drawn at reduced
size).
2026-10-26 21:41:42 +07:00
dotCooCoo 7705983a72 PDF: list markers export as selectable text
Squashed from #4098 onto main.
2026-10-26 21:00:30 +07:00
dotCooCoo 7470505113 Render: cropped shadow and blur rasters are kept across pans within a margin
A Drop Shadow, Gaussian Blur, Feather or Photoshop-style filter raster
too large to keep whole is cropped to the view. It was drawn again
only while a later frame's rect, panned back, lay inside the frame it
was made in, so every pan that showed new pixels made it again. For a
Drop Shadow on a 6000 x 4000 pt image in a 2560 x 1440 view at 100% to
400%, a pan took 60 to 84 ms; with Gaussian Blur or Feather, a pan took
158 to 217 ms.

When such a raster is made again at the zoom and rotation of the one
kept for the object, it now also covers a margin around the view: up
to half the view's longer side, while the raster stays within the
8.4 million pixel budget at full size (441 pixels on each side of a
2560 x 1440 view for a 20 pt blur at 100%). Later pans that keep the
view inside the margin draw that raster. The first frame at a zoom, a
zoom step and the frames of a drag make the raster without the margin,
as before. A shadow made over an earlier cropped shadow covers only
the rect the earlier one was made for.

A kept raster is drawn again only after a pan by whole device pixels,
but the wheel, a trackpad and the scroll bars scrolled the view by
amounts that are not whole pixels. The canvas now moves the view by
whole device pixels when it scrolls or is dragged. A scroll by the wheel, a trackpad, a touch screen or a scroll
bar is rounded, and the part of a pixel left over is added to the next
scroll of the same view; it is dropped after a zoom, a turn, Fit or a
document switch. A Hand tool, Space or middle-button drag and a scroll
bar thumb drag round the distance from where the drag began.

The silhouette of a shadow or glow raster is now drawn with art culled
against the raster's own area instead of the view, as filtered content
already is. Before, a group member outside the view cast no shadow
into a raster kept whole, so a pan that showed the member drew it
without its shadow. A cropped raster kept across the margin would have
done the same. The moved silhouette that a drag compares is drawn the
same way.

Docs: docs/target-app-parity.md.
2026-10-26 20:43:54 +07:00
bflatastic 749523d99c Effects: keep the isolate_lowest_one allow #4263 dropped (clippy 1.99, the pinned toolchain, still flags the manual form) 2026-10-26 20:35:04 +07:00
nrq6993-a11y fff6227351 #4122: File → Open starts in the last opened file's folder
Squashed from #4263 onto main.
2026-10-26 20:34:53 +07:00
nrq6993-a11y 628ecf68d5 G10: skip kashida test when PDF text import is empty
Squashed from #4264 onto main.
2026-10-26 20:34:48 +07:00
dotCooCoo 16205440e2 Render: the decoded-image cache sizes from installed memory on Windows, macOS and FreeBSD
The renderers share decoded images within a budget of an eighth of the
installed memory, at most 1 GiB. The installed memory was read only on
Linux, so on Windows, macOS and FreeBSD the budget was always 1 GiB,
also on machines with less than 8 GiB.

The installed memory is now read with GlobalMemoryStatusEx on Windows
(through winsafe, which vectorcraft-cli already uses), sysctl
hw.memsize on macOS and sysctl hw.physmem on FreeBSD. The budget rule
is unchanged: an eighth of the installed memory, at most 1 GiB, 1 GiB
where the memory can't be read, and 256 MiB in the web build.
2026-10-26 20:18:33 +07:00
nrq6993-a11y b467f37d1d G4: mixed Small Caps and super/subscript in Character panel
Squashed from #4260 onto main.
2026-10-26 20:12:52 +07:00
nrq6993-a11y fc9ced75d9 Clippy fixes after upstream merge
Squashed from #4258 onto main.
2026-10-26 19:51:06 +07:00
dotCooCoo 7a815b0acc Type: text.convertListToText moves the Type tool's caret past the markers
The Type tool's anchor and caret were moved past the inserted markers
only by the menu item in the desktop app. When text.convertListToText
came over MCP or the control channel while the Type tool was editing,
they kept their old byte offsets, and the inserted characters (a bullet
is 3 bytes) could leave an offset inside a character.

The command now ends the Type tool's typing, then moves its anchor and
caret past the characters inserted at or before them. In threaded type,
when the caret's character moved to another frame, the tool edits that
frame, which becomes the selection when it was not selected. The menu
item runs the command without adjusting the selection itself.

Docs: docs/mcp.md.
2026-10-26 19:38:12 +07:00
dotCooCoo 2f445e117f Type: Convert to Text makes a list's bullets and numbers characters of its text
The reference app's Type › Bullets and Numbering › Convert to Text turns
the bullets and numbers of the selected list paragraphs into editable
text. VectorCraft drew list markers but could not convert them.

Type › Bullets and Numbering › Convert to Text (type.convertListToText,
text.convertListToText) puts each list paragraph's marker at the start
of the paragraph as characters: the bullet, or the number and its
period as the paragraph showed it, then a tab, in the marker's font,
size and color. The paragraph leaves its list. Where the marker's tab
went to the left indent, the paragraph gets a plain left tab stop
there, so the text after the tab starts where it did. Where the
marker's tab went to a stop with a leader, or to a right, center or
decimal stop, a plain stop where the tab ended keeps the converted tab
there without the leader; right, center and decimal stops between the
marker and that place are removed. Past the left indent this applies
only to a paragraph with no tab of its own. In threaded type the
numbers are those the story counted, and a paragraph that goes on into
the next frame is converted once, where it starts. A right-to-left
paragraph with no direction set gets the Right to Left direction when a
letter or roman numeral in front of it would make it run left to right.
The change is one undo step. The Type tool's caret stays next to the
character it was next to, and in threaded type it goes with that
character to the frame it moved to, which becomes the selection when it
was not selected.

With the Every-line Composer a paragraph that holds a tab is broken
line by line, so a converted paragraph can break its lines at other
places than the list paragraph did.

Docs: docs/mcp.md, docs/target-app-parity.md.
i18n: Convert to Text in the 24 catalogs.
2026-10-26 19:38:12 +07:00
dotCooCoo bdba8c1cea Render: an image that can't be decoded shows as a crossed box and isn't decoded again
An image whose pixels can't be decoded, such as a PNG cut short by an
interrupted download, drew nothing on the canvas and the status bar
said nothing. Every render decoded it again, so each frame that drew it
paid for a decode that fails.

The renderer now notes such an image by its content and does not decode
it again. Each render still counts it in decode_failures, so an export
reports it as before. An image refused only because the images being
drawn leave it no room within twice the decoding allowance is not
noted, and the next render tries it again.

On the canvas the image draws as a light gray box crossed from corner
to corner, and the status bar says once for each image that it couldn't
be decoded. Exports still draw nothing there.

Docs: docs/target-app-parity.md.
2026-10-26 19:00:28 +07:00
nrq6993-a11y 9f74540d58 G11: Anchor Point tool Space moves anchor mid-drag
Squashed from #4254 onto main.
2026-10-26 18:49:33 +07:00
nrq6993-a11y a0b038e9e2 Fix clippy after upstream merge (art style test, Fenwick step, pencil bool)
Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-26 18:46:53 +07:00
nrq6993-a11y 37995765a1 G8: AI art style and gradient import regression tests for hostile values
## What and why

Hostile `.ai` art styles and gradients (out-of-range caps, joins, miter limits, dash arrays, stroke widths, width profiles, blend opacity, paint flags, operand counts, too many gradient stops) must be clamped or ignored on import. The tests only: the limits and checks they cover are already on main, so the tests pin that behaviour so a later change can't silently drop it.

## How it was tested

The new tests pass, along with the full `cargo xtask ci` on the branch.

Combines the tests from: stream/vc-featD-g8-art-style-blend-opacity, stream/vc-featD-g8-art-style-cap-join, stream/vc-featD-g8-art-style-miter-cap, stream/vc-featD-g8-art-style-operands-cap, stream/vc-featD-g8-art-style-paint-flags, stream/vc-featD-g8-art-style-stroke-width, stream/vc-featD-g8-gradient-stops-cap, stream/vc-featD-g8-art-style-dash-cap, stream/vc-featD-g8-art-style-width-profile-cap
2026-10-26 18:46:49 +07:00
bflatastic f024069002 Effects and pathops: keep the isolate_lowest_one allows #4252 dropped (clippy 1.99 still flags the manual form) 2026-10-26 18:46:46 +07:00
nrq6993-a11y c38185f79c ci: Rust 1.95 clippy and flaky Arabic PDF test after upstream merge
Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-26 18:40:47 +07:00
nrq6993-a11y 789b69ce8e G1: Blob Brush drag shows scrub path length when Measurement Labels on
Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-26 18:40:46 +07:00
dotCooCoo bba3537ea3 Part of #2384: imported PDF text keeps its place beside a drawn no-break or thin space
A space the file draws between two words gets tracking of its own so the
word after it stays where the file put it, but only U+0020 counted as a
space. A drawn no-break space (U+00A0, U+202F) or fixed-width space
(U+2000 to U+200A, thin and hair spaces included) took the old path: the
gaps on both sides of it also read as spaces, so the line came in with
an extra space before and after it and the words after it out of place.
These characters now count as spaces in the line's gap and drawn-space
rules, and a space of more than one byte is split into its own run whole.

The wrapped-line space tolerance in area type, a fiftieth of an em, is
named SPACE_TRACKING_TOLERANCE.
2026-10-26 18:39:20 +07:00
Caio Pontes 311b343219 Fix #4128: text.create and text.setStyle store a font style the family has
Given a family and no font style, text.create and text.createInPath
kept the default Regular, and text.setStyle kept the text's previous
style, even when the family has no such style. A family whose styles
are numbered (Hiragino W3, W6) has no Regular, so the file named a
font that isn't installed and opened with Missing Fonts.

They now keep the style when the family has it, else take the
family's closest one, as text.setRangeStyle already did. Its rule
moves to `textedit::fit_font_style`, which the three commands share;
Graph labels styled through text.setStyle follow it too. A style the
caller names is kept as given.

The test builds a family with W3 and W6 and no Regular in code
(`test_fonts::numbered_font`), so no font file is added.
2026-10-26 18:12:51 +07:00
ashokram-si 87470ef0b7 View: all 25 saved views in the View menu; Edit Views… lists them
A document keeps up to 25 saved views, but the View menu listed only the
first 10, so views 11 to 25 could be saved and never chosen. All 25 are
listed now, by name, and choosing one changes the current window to its
zoom, centre and rotation (within the canvas's zoom range).

Edit Views… used to open the generic parameter dialog on the first view,
where the name of the view to change had to be typed. It now opens a list
of the document's views, like Edit Selection…: pick one to rename it or
delete it; OK applies every change as one undo step and Cancel leaves the
views as they were. view.saved.edit takes {edits: [{name, newName?,
delete?}…]} for that, the one-view form still works, and the command is
off while the document has no saved view.

A file's views are untrusted: on open at most 25 are kept, names are
trimmed, capped at 255 characters and unique, and views without a finite
centre and a positive zoom are dropped. New View… starts from the first
free "View N" and caps names the same way.

Behaviour from the public help page "Use multiple windows and views":
https://helpx.adobe.com/illustrator/using/viewing-artwork.html

docs/gaps.md (G7) and docs/target-app-parity.md take New View/Edit Views
off the missing list; docs/mcp.md documents the view.saved commands; the
catalogs get the Saved View 11-25 rows in the pattern of 1-10.
2026-10-26 17:40:42 +07:00
dotCooCoo 389dce727d Undo: a canceled undo scope drops the journal entries it cut from open undo groups
When an undo scope that joined an open undo group was canceled, the
journal was cut back to where the scope began, but the group still
listed the cut entries' numbers. The next command took one of those
numbers, so a later cancel of the group also removed that command's
entry, even when it ran in another document, and a replay of the
journal no longer matched the documents. end_undo_scope now removes
the cut numbers from every open undo group's journal entries and batch
steps.
2026-10-26 16:58:49 +07:00
dotCooCoo 1edfb8f450 Render: brush art is made again when a symbol or pattern it shows is renamed
The brush cache dropped brush art that can show a symbol or a pattern
only when the art of the document's symbols or patterns changed. Renaming
one, for example swapping the names of two symbols, left their art as it
was, so a brush whose art holds an instance of one of those names kept
drawing the old symbol. The cache now compares the names along with the
art.
2026-10-26 16:38:04 +07:00
nrq6993-a11y 97652362fa G8: EPS import regression tests for operator type checks
## What and why

Hostile EPS programs that give an operator the wrong operand type (a non-integer count, a non-procedure body, a non-boolean condition) must stop the import with a `typecheck` error instead of misbehaving. One test per operator: copy, for, idiv, if, ifelse, index, loop, mod, repeat, roll and srand. The tests only: the limits and checks they cover are already on main, so the tests pin that behaviour so a later change can't silently drop it.

## How it was tested

The new tests pass, along with the full `cargo xtask ci` on the branch.

Combines the tests from: stream/vc-featD-g8-eps-copy-typecheck-count, stream/vc-featD-g8-eps-for-typecheck-procedure, stream/vc-featD-g8-eps-idiv-typecheck-operand, stream/vc-featD-g8-eps-if-typecheck-condition, stream/vc-featD-g8-eps-if-typecheck-procedure, stream/vc-featD-g8-eps-ifelse-typecheck-condition, stream/vc-featD-g8-eps-ifelse-typecheck-procedure, stream/vc-featD-g8-eps-index-typecheck-count, stream/vc-featD-g8-eps-loop-typecheck-procedure, stream/vc-featD-g8-eps-mod-typecheck-operand, stream/vc-featD-g8-eps-repeat-typecheck-count, stream/vc-featD-g8-eps-repeat-typecheck-procedure, stream/vc-featD-g8-eps-roll-typecheck-j, stream/vc-featD-g8-eps-roll-typecheck-n, stream/vc-featD-g8-eps-srand-typecheck-operand
2026-10-26 16:06:20 +07:00
Taha-Mahmoodi de151f849a Type: the thread-break comments describe the behaviour in our own words
Review follow-up (CodeRabbit, clean-room): two doc comments quoted the
cited help page. Comments only.
2026-10-26 16:00:39 +07:00
Taha-Mahmoodi 54d9fbc162 Type: a double-click on a threaded port breaks the thread there 2026-10-26 16:00:39 +07:00
dotCooCoo 45440b5444 Toolbar: double-clicking the Type on a Path tool opens Type on a Path Options
A double-click on the Type on a Path or Vertical Type on a Path tool
reported that the tool has no options. It now opens Type on a Path
Options for the selected type on a path, the same dialog as Type ›
Type on a Path › Type on a Path Options…. With nothing selected, or
with no type on a path selected, nothing opens and `tool.options`
returns the menu command's error, as the Rotate tool's double-click
does without a selection.

Docs: docs/control-protocol.md, the `tool.options` params doc.
2026-10-26 15:44:50 +07:00
Taha-Mahmoodi cfd085f34a Fix #3426: the Pathfinder panel's Pathfinder Options… sets Precision, Remove Redundant Points and Divide and Outline Will Remove Unpainted Artwork
Squashed from #4141 onto main.
2026-10-26 15:28:51 +07:00
Taha-Mahmoodi 0a241a7fdd Brushes: Window › Brush Libraries opens brush libraries whose brushes click or drag into the document's Brushes
Squashed from #4020 onto main.
2026-10-26 15:17:30 +07:00
ham9zah 9a05383f34 Import: live blend check follow-ups
Review follow-ups to the live blend import (#4127):

- The check's grid saturates its neighbours' keys: a step far out (a
  coordinate like 1e300) saturates its cell, and the cells round it
  overflowed (a panic in debug builds, a wrong cell in release).
- Each laid object matches one drawn step: two drawn steps near one laid
  object no longer both take it, so a drawing the blend doesn't lay out
  isn't taken for it (the drawn steps stay).
- A blend's record is read to the length its XP gives: hex digits after
  it on the last line no longer spoil it.

Tests: a laid object matches one drawn step, 1e300 coordinates in the
check and in the import fuzz property, and a record line longer than
the record.
2026-10-26 15:11:45 +07:00
ham9zah 4214d4c9d3 Import: live blends are checked in bounded time and keep no stray art
Review follow-ups for the live blend import:

- The check of a blend against its drawn steps matched every drawn step
  against every laid one and measured centres inside the loop, so a
  crafted blend (64 keys, 1,000 steps between each) could stall the
  import. Centres and fills are now measured once, and each drawn step
  finds its neighbours through a grid of cells the tolerance wide.
- A blend lays out at most 5,000 objects to be made live, and all the
  blends of one import at most 50,000 (each laid out twice, once per
  orientation); past either, the drawn steps stay, with the note. The
  live steps take the place of drawn steps already counted against the
  reader's object limit.
- A fill the check can't compare (a gradient, a pattern) or a missing
  one no longer passes: the fills must both be none or solid colours
  within the tolerance.
- When what follows the blend's own art isn't a group of drawn steps,
  that art (its keys and spine) goes too, with the note, instead of
  staying drawn.
- The spine test can no longer pass without asserting: a spine written
  backwards keeps the keys' order (live), and a curved spine is read
  exactly while its straight-drawn steps stay drawn. New tests for steps
  with a gradient, for the blend's art when its steps aren't a group,
  and for the budget; import_fuzz gains a mutated-blend property and a
  large-blend test (998 steps live, 1,500 kept drawn, both quickly).
2026-10-26 14:10:37 +07:00
ham9zah f8bcf69b23 Import: Illustrator blends come in as live blends
A blend in a .ai file's editing data came in as plain art twice over:
the blend's own art (its key objects and spine, on `%_` lines) and its
drawn steps were both kept visible, so the keys lay doubled under the
drawn ones and the spine showed as a stray path, and nothing was live.
The spine itself was read exactly (its line and curve segments match).

The `(Adobe Path Blends) 1 0 n XP` record is now read (n bytes of hex on
the `%` lines after it, capped, checked: `DCBA`, version 1, the key
count and a record per key starting with its id). Each object's
dictionary names its part (`ShapeBlendPathType` K or S) and a key's id
(`ShapeBlendPathID`); the keys the record lists and the spine become one
live blend, the keys sitting on the spine's anchors. The record's other
fields aren't known, so the spacing is the drawn steps between two keys
(Specified Steps) and the orientation whichever lays the blend out as
drawn; the blend replaces the drawn steps only when it comes out as
drawn (each step within 0.5 pt and 0.02 of its colour). Otherwise the
drawn steps stay, without the blend's own art over them, with a note.
Behaviour as documented: https://helpx.adobe.com/illustrator/using/blending-objects.html

Save As .ai writes the PDF-compatible part only (no editing data), so a
blend is written back as its steps drawn, not live.

Hand-written tests: two keys and a straight spine as one live blend with
three steps and interpolated colours; a curved spine (l, c, L) in both
directions keeping its points and the keys' order; a garbled or short
record, a key without an id and ids the record doesn't list keeping the
drawn steps with a note; moving a key moving the steps; record parsing
on hostile input. The import fuzz target gains the record and the new
mesh keys.
2026-10-26 14:10:37 +07:00
Taha-Mahmoodi a34055beec Artboards: the Artboards panel's Delete Empty Artboards deletes every artboard with no artwork in one undo step
Squashed from #4142 onto main.
2026-10-26 14:10:36 +07:00
Taha-Mahmoodi 5b91057dca Repeat: a selected Grid Repeat has on-canvas handles for its rows, columns and spacing
With the Selection tool, a single selected grid repeat shows a handle below the grid (rows), one right of it (columns), one past its bottom-right corner (both) and sliders between its first two columns and rows (horizontal and vertical spacing), as the public Set grid repeat options page describes. Each drag previews object.repeat.options and commits as one Repeat Options undo step; Escape cancels, and a drag that ends where it began leaves no step. Bounding-box handles keep priority where they overlap, and the controls hide with the edges or the bounding box. Before, a drag there scaled the source or started a marquee.

Part of #2691.
2026-10-26 13:44:06 +07:00