Commit Graph
4952 Commits
Author SHA1 Message Date
dotCooCoo ed3efcbd7d Render: a long dashed stroke is outlined only near the view after an edit
A dashed stroke of a path with more than 1,024 path elements was outlined
whole in every frame after an edit. It is now outlined in and around the
view like a solid stroke. dash_kept runs the pattern along every segment
and keeps only the dashes and dots on the segments near the view, so they
sit where dashing the whole path puts them and the pixels are the same.
A pattern too fine to draw is still drawn solid, and that solid stroke is
cropped too.

At 1600 x 1200 the frame after one anchor moves takes 14 ms instead of
102 ms on a filled closed path of 50,000 anchors with 6 pt dashes and
4 pt gaps. anchor_bench now includes that document.
2026-11-05 03:55:48 +07:00
ruchirguitarandClaude Opus 5.5 b87fddba11 Type: Optical kerning spaces pairs by the font's own stem spacing (#5336)
Optical moved every pair all the way to a fixed 0.08 em ink gap, tighter
than most text faces' own stem spacing, so ordinary text was squeezed
(at 100 pt, Source Sans 3 `nn` closed from 54.7 to 46.9 pt).

Each pair now moves 0.6 of the way toward the face's reference gap (the
median ink gap of `nn`, else `HH` or `oo`; 0.08 em when it has none),
keeps at least 0.3 of it at its narrowest, and changes by at most
0.12 em tighter or 0.06 em looser. Ideographs, kana, hangul, fullwidth
and CJK punctuation forms and dashes keep the font's spacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:04:51 +07:00
dotCooCoo 84c197cfbe Symbols: quick boxes hold art that nested 9-slice guides move
An instance of a symbol whose art holds a 9-slice instance draws that
instance through its guides with the transform of every instance above
it. When the outer symbol has no guides, the quick box of an outer
instance that scales or turns its art was its art box placed by its
transform, and the renderer could cull art the nested guides move past
that box.

Symbol art boxes now record whether the art holds an instance of a
symbol with 9-slice guides at any depth. For such art, quick boxes
place the art by the instance's transform and measure it, once for each
symbol and linear part. One set of boxes places at most
MAX_SYMBOL_NODES objects this way, the art of opacity masks included.
2026-11-05 03:04:48 +07:00
dotCooCoo 08aa511252 Preferences: a recovery folder that can't be listed is refused
Data Recovery lists the folders in its recovery folder before it starts, so a
folder that can be written but not listed (a Unix folder with mode 0333) left
it unable to start. Choose... now lists the picked folder as well as making a
folder in it, and refuses it with a status message when either fails. The
progress line says what is checked.
2026-11-05 03:04:39 +07:00
dotCooCoo 13d0cbc51f Preferences: the recovery folder check makes a folder, and a pick survives a closed document
The Choose... check now creates and removes an empty folder in the picked
folder, as the recovery store makes its own folder there, and reports a
folder it cannot remove again. The folder picker's answer fills the open
Preferences dialog even when the active document closed while the picker
was open, since the recovery folder is an app preference.
2026-11-05 03:04:39 +07:00
dotCooCoo 9613bfb31d Preferences: Data Recovery's Folder gets a Choose button
File Handling > Data Recovery > Folder has a Choose... button on the
desktop that opens the folder picker and puts the chosen folder in the
field. Cancel leaves the field as it was, and the field stays editable.
A chosen folder that no file can be created in is refused with a status
message. The web build has no folder picker, so the button is not shown
there.
2026-11-05 03:04:39 +07:00
dotCooCoo 3e96d59fd8 Numeric fields: 1,000% in a size field is refused
A percentage typed alone in a size field (W, H and the other fields
that take a share of their value) read its decimal comma before the
`%` was dropped. The thousands check then saw `1,000%`, and the comma
was read as a decimal point, so `1,000%` set the size to 1% of its
value. `share_of` now reads the decimal comma after dropping the `%`:
`1,000%` is refused, and `12,5%` still reads as 12.5%.
2026-11-05 03:04:35 +07:00
bflatastic b814fcf689 Pathfinder: holes that touch at a point stay holes of their region
The Live Paint faces were right; the Pathfinder regions over-counted.
Two small circles inside a big one, touching each other at one point,
gave the big circle's region only one of its two holes, and the other
hole came out as a region of its own.

linesweeper walks the boundary of the two touching holes as one loop
and, when the walk comes back to the shared point, cuts the second
hole off as a child of the first and flags it outer, as if it were an
island inside it. face_groups trusted that flag, so the hole started a
face. The same cut goes wrong the other way when an outer contour
touches its own hole: the outer contour comes out as an island inside
the hole.

Such a loop winds the same way as the contour it was cut from, against
its flag. face_groups now checks every contour's flag against its
winding; a pass where any disagrees is grouped by geometry instead
(each hole goes with the smallest outer contour of its mask around it).
Passes where they all agree keep the sweep's tree as before, at the
cost of one area per contour.

Adds deterministic tests for both shapes of the bug and checks in the
proptest regression seed.
2026-11-05 03:02:03 +07:00
dotCooCoo cd71257ac4 UI-5636: Preserve styled Type-range copies on canvas paste 2026-11-05 02:23:51 +07:00
dotCooCoo a5157654f4 Tests: the link-watch and place tests wait for the link scan instead of sleeping
The link-watch worker now signals a condition variable when it ends,
whether it stored its stamps or unwound, and Session::wait_link_scan
blocks until then. The place tests' idle helper waits on it once per look
in place of a 5 ms sleep under a 20 s wall-clock bound, and the engine's
link-watch and link-path tests wait on it in place of spinning under a
10 s bound. The link-watch tests date a rewritten file 2 s after the one
it replaces instead of sleeping 30 ms before writing it.
2026-11-05 02:23:48 +07:00
dotCooCoo 440b3b7444 Glyphs: overflowing vertical type keeps a chosen default glyph
Squashed from #5676 onto main.
2026-11-05 02:23:42 +07:00
dotCooCoo b0d1ca4419 Glyphs: a character with a combining mark reports the glyph it is drawn with
text.setGlyphAlternate looked for the character's glyph only in a
cluster as long as the character. A character followed by a combining
mark is shaped in one cluster with the mark, so the lookup found
nothing: under a substituting feature such as salt, `gid: null`
reported the default glyph instead of the one drawn, and a request for
the default glyph changed nothing.

The lookup now also takes a cluster that holds the character's
grapheme, and in a cluster of several glyphs takes the character's
default glyph or one of its alternates. Text that overflows its area
is shaped with the whole grapheme.
2026-11-05 01:56:16 +07:00
dotCooCoo 584b9706cc Gradient: Perceptual PDF help counts stops as written, CMYK documents included
The method help of paint.editGradient and docs/mcp.md said a Perceptual
gradient with spot stops becomes an image in PDF unless every other stop
is paper white, CMYK or gray. PDF export checks each stop's color as it is
written: a CMYK document, or an export that converts colors to a CMYK
destination profile, writes RGB and Lab stops in CMYK, so the gradient
becomes a DeviceN shading. Both texts now say so, and a test covers the
CMYK document and CMYK destination cases.
2026-11-05 01:31:24 +07:00
dotCooCoo bfe81407bc Package: name every font left out when no dialog is shown
A package written in the background that finished while another dialog
was open showed no Package created dialog, but its status line still cut
the list of fonts left out at 500 bytes, so the fonts past the cut were
named nowhere. The status line now cuts the list only when the dialog is
shown.
2026-11-05 01:22:06 +07:00
dotCooCoo d4f3ec6c96 Character: Kerning refuses (1,000) instead of reading it as 1
field_number read a decimal comma before it dropped Kerning's pair of
parentheses, the suffix, % and degrees, so the thousands check saw
(1,000) and 1,000% and read the comma as a decimal point: (1,000)
committed 1. The comma is now read after those are dropped, so
(1,000), 1,000% and 1,000 degrees are refused as 1,000 already is,
while (1,5) and 12,5% still read as 1.5 and 12.5.
2026-11-05 01:08:30 +07:00
dotCooCoo 7f449cc3c8 UI-1628: Keep drawing cursor during mid-drag Cmd+Space 2026-11-05 01:08:26 +07:00
dotCooCoo be8438c13d Tabs: setting the stops a paragraph already has ends the typing session
text.tabs.set and text.tabs.clear return without an edit when every
targeted paragraph already holds the stops. That path skipped the
settlement Session::edit does first, so a typing session in progress
stayed open. The command now ends it first, as the Variables commands
do: the typing is its own undo step whether or not the stops change.
2026-11-05 00:55:35 +07:00
FredAppsandClaude Opus 5.5 178e4a00df Refit credits tables on what they measure, not the fitted widths
Comparing the fitted widths missed the case it was meant for. Name is the
only growing column, so a longer name takes its extra width from the
spare room and the fit comes out the same: a dragged Name width stayed,
clipping the longer name.

It could also drop dragged widths by accident. The fit comes from the
available width inside the scroll area, which scrolling can nudge, so
scrolling a widened table could reset it.

The table now compares the natural widths and the clip rect's width,
which scrolling does not move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:55:32 +07:00
d8abyte-dkandClaude Opus 5.5 ad93abd484 Plug-ins: compound guides aren't filter inputs; colors follow the working RGB space
A compound path a filter made a guide was still given to later filters, which
could change or delete it; compound paths of guide paths are now left out as
guide paths are. Hex colors and the color button's choice are display sRGB and
are now converted into the working RGB space, so they show as given whatever
the working profile; a color default is kept as written and read when used.

Follow-up to #5630.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:42:23 +07:00
Janiz-zzzandClaude Opus 5.5 28a46e96c7 i18n(fi): translate the 5 labels Finnish lacks
Check for Updates and its window, the panel-layout reset notice and the
Image Trace progress lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:32:31 +07:00
ruchirguitarandClaude Opus 5.5 7f3d2b6406 Select by Artboards dialog: a plain click keeps artboards the search hides (#5548)
A plain click chose only the clicked row and dropped artboards chosen
but hidden by the search. It now replaces only the rows shown, as None
and Select All already do, so hidden rows keep their state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:16:17 +07:00
dotCooCoo f026ebe686 Tests: an RGB spot color converts with Document Color Mode and stays spot
A spot swatch defined in RGB keeps its spot flag and its own plate when
the document changes to CMYK, and its definition converts like any other
swatch: a fill that uses it and a 40% tint stroke take the converted
color. The Lab spot test's comment now says it covers a Lab definition.
2026-11-04 23:58:58 +07:00
dotCooCoo fae49d31fd Glyphs: the default glyph can be chosen back under a substituting feature
Follows #5442.

- text.setGlyphAlternate keeps the default glyph as a choice when a
  feature of the character's style (salt, a stylistic set) draws another
  glyph for it, so the default glyph is drawn. A character that is not
  laid out (overflowing text) is shaped alone in its style to decide.
  gid: null reports the glyph the style draws.
- The Glyphs panel's alternates grid shows the default glyph first when
  the font does not list it, so a chosen alternate can be put back.
- A test for a chosen character that starts a longer grapheme (f with a
  combining acute), which is not split from its mark.
- The alternates tests in the engine hold the shared font lock while
  they add fonts to the global font database.
2026-11-04 23:58:54 +07:00
dotCooCoo 0a65178696 Images: a PNG read a row at a time counts the reader's buffer while it grows
The png reader's stream buffer grows by doubling its reservation, and
while it grows the old reservation and the new one are both allocated.
The room estimate counted the buffer twice, for the doubled
reservation, but not the moment both are held, so a wide PNG could be
approved for a row-at-a-time decode with less room than reading it
takes. The estimate now counts the stream buffer three times.
2026-11-04 23:45:05 +07:00
dotCooCoo 2395a390b5 Control bar: the inline X, Y, W and H fields show only when they fit
The bar decided to show the inline fields against a fixed 440-point
width, smaller than the room the fields take, so just above that width
the H field ran past the bar's right margin and was clipped. The bar
now adds up the room from the values that draw the fields: the label
widths in the current language, the four 80-point fields, the W/H link
and the item spacing ahead of each. With less room it shows the
Transform link alone, as before.
2026-11-04 23:45:02 +07:00
dotCooCoo a9eae3a333 Appearance: Add New Fill and Add New Stroke comment names the paint they take
The comment in add_item said a new fill or stroke is a copy of the top
one. The code takes only the top item's paint, and for a stroke its
weight. The comment now says so, as the params text does.
2026-11-04 23:44:58 +07:00
dotCooCoo 539195d68b Tests: native menu tests wait for the installed fonts' scan
VectorcraftApp::new starts the installed fonts' scan in the background,
and Type > Font lists only the loaded fonts until it ends (#5483). When
the scan ended between two reads of the menus, the second read had the
installed families too, and every_in_window_item_is_in_the_mac_menu_bar_once
and documents_and_selections_change_state_not_structure failed. The
tests' app() helper now waits for the scan after making the app.
2026-11-04 23:44:55 +07:00
Alex cd50091212 Canvas: Space during a tool's drag keeps the tool's cursor
Space pressed after a tool's press has begun (Pencil, a brush, a shape, a
selection drag) switched the cursor to the hand but panned nothing: the pan
starts only with Space down at the press. It is ignored now, so the cursor keeps
the tool's own.

Test: space_during_a_pencil_drag_keeps_the_pencil_cursor (fails without the fix:
Grab instead of the pencil's crosshair).
2026-11-04 23:24:59 +07:00
FredAppsandClaude Opus 5.5 380366d3d2 Let every credits column be dragged, and refit changed tables
The last column was egui's remainder column, which has no drag handle, so
it could only be resized from the edge before it. It is now an ordinary
column with its own handle.

A table keeps its column widths, so switching to a longer name left the
Name column at its old width and clipped the text. The table now goes
back to the opening fit whenever that fit changes: a longer widest cell
or a resized window. Dragged widths are dropped at that point.

fit_widths could overflow the room when one column hit the minimum,
because the other columns did not take up its share. Columns now give
way in proportion to their width above the minimum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:24:56 +07:00
ruchirguitarandClaude Opus 5.5 f140301aaa Select by Artboards dialog: None keeps hidden rows; translate its strings (#5548)
None now clears only the rows the search shows, as Select All adds only
those, so artboards hidden by the search stay chosen. The count reads
"Chosen: {n} of {total}". The dialog's new strings have rows in all 26
catalogs, written from their meaning with each catalog's own terms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:57:59 +07:00
ruchirguitarandClaude Opus 5.5 6e1c1c9377 Select by Artboards: test that a symbol instance is measured by its art (#5525)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:57:59 +07:00
ruchirguitarandClaude Opus 5.5 471cf3b81b Select by Artboards dialog: double-click a row to fit its artboard (#5549)
A double-click makes the row's artboard active and fits it in the
window (view.goToArtboard, as the Artboards panel does), keeping the
dialog open and the artboards chosen before it. Parity docs record the
Selection Tools add-on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:57:59 +07:00
ruchirguitarandClaude Opus 5.5 3a50b20b95 Select by Artboards dialog: artboard list, search and multi-select (#5548)
Object › Plug-ins › Selection Tools › Select by Artboards… lists each
artboard's number, name and size, filters by name, chooses rows with
click, Shift-click and Cmd/Ctrl-click, and runs addon.select.byArtboards
on OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:57:57 +07:00
ruchirguitarandClaude Opus 5.5 7e016a0bcc Select by Artboards: select objects overlapping, inside or outside several artboards (#5525)
addon.select.byArtboards {artboards, relation} in a new Selection Tools
add-on. Overlapping is All on Active Artboard's test, now one shared
helper (select::on_artboard); Fully inside is within one artboard, edges
included; Fully outside is on none. Objects are those Select All takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:57:57 +07:00
dotCooCoo aee5671734 File formats: Pixar tile pointers into the header or tile table are refused
A nonzero tile pointer below the end of the tile pointer table made the reader decode header or table bytes as pixels. The table end is now computed with checked arithmetic, and such a pointer is refused as damaged image data.
2026-11-04 22:57:53 +07:00
dotCooCoo a07ca35c47 Geometry: the nearest point on a long path skips segments whose box is farther than the best found
PathData::nearest, which the Direct Selection cursor runs to find the
segment under the pointer, projected the pointer onto every segment of
the path.

It now projects the pointer first onto a segment at the anchor nearest
to it, then skips each segment whose control-point box lies farther
away than that distance or the nearest point found so far. Each box is
measured once and the search allocates no memory. The result is the
same, including which of equal distances wins. A segment is never
skipped when the pointer or one of its control points is past 1e100 or
not finite.

Over a segment of an open path of 200,000 anchors, the cursor takes
51 ms instead of 59 ms in a release build (anchor_bench). The progress
line in docs/target-app-parity.md gives the timing.

Tests compare nearest with every segment projected over 400 random
paths, each probed at 30 points on a coarse grid (ties included), and
on open, closed, compound, one-anchor, empty, huge and non-finite
paths.
2026-11-04 22:57:47 +07:00
dotCooCoo 0c3bf975d2 Type: Tate-chu-yoko Settings refuses a value that is not a length
OK in Tate-chu-yoko Settings read an Up/Down or Left/Right that is not a
length as 0 and applied it. It now returns "Up/Down must be a number" or
"Left/Right must be a number" and keeps the dialog open, and a blank
field leaves its value alone.
2026-11-04 22:57:43 +07:00
dotCooCoo 88b5170622 Glyphs panel: alternates for the current text selection
Squashed from #5442 onto main.
2026-11-04 22:29:53 +07:00
dotCooCoo 0990bfb244 .ai save: the editing copy keeps transparency, clipping and opacity masks
The editing copy of the art that a PDF-compatible .ai carries now holds:

- Transparency: `mode opacity isolate knockout shape Xy` (the blending
  mode's number, Knockout Group 0 off, 1 on, 2 neutral), written when it
  changes: in a path's paint after its colors, before a guide, and after
  a group's end followed by `0 0 Xd` and `6 () XW`. A group without
  transparency of its own ends `9 () XW`, and a group's name comes
  before its state block. A layer's transparency follows its art: `Xy`,
  `0 0 Xd`, `7 () XW`, `LB`.
- Clipping groups as `q`, the art, the clipping path or compound path last
  with `h` (or `H`) and `W` before its painting operator, `Q`, as section
  5.6.2 of the version 7 specification shows them
  (http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf). A clipping
  layer whose clipping object is a path or compound path sets the last
  `Lb` flag and writes its clipping path last.
- Opacity masks as an `AI9_Mask` entry in the object's dictionary: the
  mask's art between `X=` and `X+`, and Clipping, Inverted, Disabled and
  Linked.

Before, clipping groups were written as their art cut to the clipping
path, and transparency and opacity masks were left out. Still left out,
with a warning: the opacity and blending mode of single fills and
strokes, opacity masks on layers (disabled ones too), and opacity masks
nested more than 8 deep. A clipping group or layer whose clipping object
isn't a path or compound path is written as a group of the art cut to
that object, in a layer that doesn't clip, and so are the clipping
groups inside that art. A clipping path's paint that a plain fill and
line can't draw is written as paths inside the clipping, fills under the
art and strokes over it, each side in a group of its own when it has
several, with the clipping path's transparency, lock and hidden state;
the clipping path's opacity mask stays on the clipping path, with a
warning. A clipping group without members is an empty group, and an
empty clipping layer is an empty layer.

Each of these is written in the form VectorCraft's .ai reader reads, and
the tests read the written data back through that reader.

Reading .ai editing data: an `Xy` after a layer's last group or compound
path, followed by the layer's `7 () XW`, is the layer's (a following
`6 () XW` keeps it the group's); groups, compound paths and layers keep
Opacity & Mask Define Knockout Shape.

The `.ai` option `editingData` and docs/file-format-parity.md describe
what the copy now holds.
2026-11-04 22:29:43 +07:00
d8abyte-dkandClaude Opus 5.5 b9594b233c Plug-ins: color parameters, and filters that make guides and lock
A `color` parameter shows a color button that opens the color picker in plug-in
dialogs and the Appearance panel's effect editor; its default and values may be
a color object or a hex string, and the plug-in gets a color object. A filter's
returned objects may set `guide` and `locked`; objects it locked or made guides
are left out of the selection after the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:29:38 +07:00
dotCooCoo 9b8561b225 Tests: a file placed while idle renders on the test thread
a_file_placed_while_idle_follows_its_changes asserts that an idle
document without links asks for no frame. Its app started the real render
worker, and once a frame took over 8 ms the canvas sent the next render to
it. The worker asks for a repaint when it finishes, at a time the test
does not control, so under heavy CPU load the asserted frame could report
a repaint delay of 0 instead of none.

The test now sets canvas.worker_started, as the cursor idle test does, so
every frame renders on the test thread. It also dates the rewritten file
2 s after the original with File::set_times, as the opening tests do,
instead of sleeping 30 ms before the write. Shipped code is unchanged.
2026-11-04 22:29:35 +07:00
dotCooCoo 3c80b314e1 Select: All on Same Layers takes the layers of objects that can be selected
Select > Object > All on Same Layers took the layer of every selected
object, including one that select.set had kept while it is hidden or
locked, on a template layer, on a locked artboard, or outside the
isolated group. That object's layer then decided which art was
selected. The command now takes the layers only of the selected
objects that the Select commands could select. When the selection
holds no such object, it clears the selection.
2026-11-04 22:29:22 +07:00
dotCooCoo 83f5b7eead OpenType: a menu choice at a Type tool caret reaches every character
At a caret the panel sends text.setRangeStyle with no range, which
styles all of the text, but the check that skips a choice that changes
nothing read only the insertion style. When the characters at the caret
already had the chosen feature and others did not, the choice did
nothing. The check now covers every character of the text at a caret,
so the choice applies, and a choice that changes no character still
adds no undo step.
2026-11-04 22:29:18 +07:00
dotCooCoo e0ebc13a2e Type: right-to-left manual kerning keeps whole-point spacing on both sides
With System Layout on or Fractional Widths off, each advance is rounded
to whole points, but a right-to-left character with manual kerning
still moved by the unrounded kerning value. The difference between the
two changed the space after the character before it in reading order.
The character now moves by the change the kerning makes to its rounded
advance, so only the space before the next character changes.
2026-11-04 22:29:14 +07:00
dotCooCoo a574139814 i18n: a count in a translated message matches only a number
A plural status message such as "Saved {path} with {n} note: {first}"
matched a path that holds " with " by taking the first split, so for
"/out/art with notes.pdf" the count became "notes.pdf with 1". The
count did not parse, the translation took the plural form and split
the path.

The {n} of a @msgplural template now matches only a number, and the
matcher tries the next split when it does not. {n} in a @msg template
still matches any text, since those name brushes, symbols and presets.
2026-11-04 21:59:15 +07:00
dotCooCoo 32456839eb Native format: a saved mask-editing id drops only a top-level layer
Files saved mid-edit by older versions carry the opacity-mask editing
layer at the top level. An id there that names a sublayer is ignored on
load, so that sublayer and its art are kept.
2026-11-04 21:59:11 +07:00
dotCooCoo 9992fa0bf6 Symbols: saving leaves out a nested editing layer and keeps layers named in file data
Leaving opacity-mask or symbol editing now removes the editing layer
wherever it is, so a save or export after Layers moved it into another
layer no longer writes it. An editing id that names anything other than
a layer is left alone.

symbol_edit is no longer read from files: a value in file data is
dropped on load, so the layer it names is kept and saving does not
write the value again.
2026-11-04 21:59:11 +07:00
dotCooCoo 9dc172a26d Symbols: the quick box of a 9-slice instance holds art its guides move
An object in 9-slice art that crosses a guide and draws symbol art is
placed by the map of the cell its center is in, so it can draw outside
the instance's transformed art box. Bounds measured without placing the
art (the renderer's culling, and placements past the object limit) used
that box alone, so the displaced art could be culled and not drawn.

The fallback now takes in the art box placed by each of the nine cells'
maps (NineSliceMap::placement_bounds), grown by the art's reach as
before.
2026-11-04 21:59:07 +07:00
Don Omondi ef5a4d967a Fix #5502: Export As PNG exports the active artboard, not always the first
File > Export > Export As PNG... opened PNG Options with no artboard
chosen, and a single-image export without one writes the first
artboard, whichever was in use. With no artboard choice given, it now
takes the active artboard, or each artboard the Artboard tool has
selected (one file each). A choice the caller gives is kept.
2026-11-04 21:59:04 +07:00
dotCooCoo eb03e8943f Gradient: PDF shading notes name linear and radial gradients and DeviceN
The PDF writer writes a spot gradient as a Separation or DeviceN shading
only when it is linear or radial; freeform gradients and dithered fills
are written as images in process colors.

- `paint.editGradient`'s method help and the Method paragraph in
  docs/mcp.md limit the Perceptual exception to linear and radial
  gradients.
- The Linked gradient stops paragraph in docs/mcp.md no longer says the
  writer has no DeviceN: a gradient mixing spot inks with other spot,
  CMYK or gray inks exports as a DeviceN shading, and only a mix with RGB
  or Lab colors is written in process colors, with a warning.
2026-11-04 21:58:59 +07:00