874 Commits
Author SHA1 Message Date
bflatastic d9c9526898 Keyboard Shortcuts: shortcut set reading and matching stay bounded on hostile files
A set whose entries sit in long, deeply nested sections kept every section's names again for each entry (up to hundreds of megabytes from a 4 MiB file); the names kept over all entries now stop at 8 MiB. Name matching compares character pairs in linear time, so a 20,000-entry set of long names is matched in about 2 s instead of 17 s.
2026-10-14 13:49:59 +07:00
dotCooCoo 8c889279f8 Keyboard Shortcuts: importMatches.list answers an unknown parameter key with an error
Since #1442 (3acc29f8), a caller outside the app gets an error for a
parameter key that the command's params description doesn't name, and
every_description_parses requires each top-level entry of the params
object to start with a key. The note on query in importMatches.list had
a comma at the top level ("text in the name, target or label"), so the
description didn't parse: the test failed, and the command took any key.
The note is now in parentheses, and the command takes kind and query.
A test checks that shortcutSet.read and the three importMatches
commands take every key they read and refuse a misspelled one.
2026-10-14 13:22:08 +07:00
dotCooCoo 8c14609370 Keyboard Shortcuts: Import… and File › Open read text shortcut sets (.kys)
A shortcut set another app saved as slash-name text (a .kys file whose
text starts with a slash name and "{", such as "/Menus {") opens in
Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts ›
Import… in the desktop app, or from a drop on the window, whether or
not a document is open. The dialog proposes a target for an entry when
an earlier import kept one for that name, or else when one tool,
command or panel matches the name clearly by its id or by one of its
labels. It decodes the file's key and modifier codes from the codes
earlier imports kept, from the example in the AiCommandPalette README
(/Key 48 for the 0 key, with /Modifiers 0 taken as no modifiers), and
from VectorCraft's default shortcuts where at least two entries vote and
four votes in five agree. The user picks the remaining targets, presses
the key for each unknown key code or marks it No equivalent, and chooses
the modifiers for each unknown modifier code. Apply assigns the ready
rows with the Keyboard Shortcuts rules: a tool takes a single key,
optionally with Shift. It goes through the rows from the last one back
and leaves a row out when a later row it applies has the same shortcut
or the same tool, command or panel. Keyboard Shortcuts then shows the
result as unsaved changes until the user clicks OK. Copy Report copies
the set's name, the rows Apply leaves out and the parts of the file that
weren't read.

- shortcutSet.read is a query command: it reads a set and does not
  modify the document or the preferences. It returns each entry with
  its section, codes and line, and up to 1,000 skipped tokens, each cut
  to 80 characters. It reads at most 4 MiB of text and 20,000 entries,
  skips blocks nested more than 16 deep and cuts names at 1,024 bytes.
  With a path it reads at most 4 MiB and 4 bytes of the file, and with
  --automation-read-root (#832) the path must lie inside the read root.
  A file whose text starts with "<", such as an XML shortcut file saved
  as UTF-8, fails with "This shortcut file is in a form VectorCraft
  can't read."
- importMatches.list, importMatches.set and importMatches.reset read
  and change the matches kept with the preferences; resetting the
  preferences keeps them. With Remember My Choices on (the default),
  Apply keeps the targets and codes the user chose, and for the rows it
  assigns, it also keeps the matched targets and the codes decoded by
  agreement. A later import proposes them.
- The name matcher compares a name with ids by exact match and shared
  words. It compares the name with labels, in English and in the
  interface language, by exact match, shared words and character
  pairs. It proposes a match only when one tool, command or panel
  scores at least 0.6 and at least 0.1 more than the next one.
- When Import Shortcut Set is opened from Keyboard Shortcuts, Cancel,
  Escape and ui.dialog.cancel close it and return to Keyboard Shortcuts
  as it was (dialogs::PARENT). A document's queued Missing Fonts dialog
  (#792) opens after Keyboard Shortcuts closes.
- File › Open has a "Shortcut sets" filter, and in the desktop app
  Keyboard Shortcuts' Import… shows .json and .kys files.

Docs: docs/mcp.md, docs/control-protocol.md, docs/development.md,
docs/target-app-parity.md, docs/roadmap.md.
2026-10-14 13:22:07 +07:00
bflatastic 7f1ad31a7b Fix #1881: a Ctrl/Cmd drag while typing keeps what was typed
With the Type tool editing, a press with Ctrl (Cmd on the Mac) lends the
Selection tool for the drag. Its drag joined the open typing session, so its
preview started from the document before the typing: the type moved or
scaled and lost what had just been typed. The typing is now kept as its own
undo step before the lent tool takes the press, as Undo already does, and
typing goes on after the drag.
2026-10-14 12:27:04 +07:00
bflatastic 6bc0222074 #1723 follow-up: pattern tiles are drawn again after a brush edit or new Color Settings
A tile was kept while the pattern's art and what it shows were unchanged,
but a brush the art is stroked with lives in the document's other data, and
every tile is drawn through the color settings. A tile whose art reads more
than itself now also remembers the document's other data, and every tile
remembers the color settings it was drawn through (both weakly).
2026-10-14 12:27:04 +07:00
Marco Arnulfo eff2ccdc34 Text: draw tab leaders in the tab's gap (fixes #1877) 2026-10-14 12:27:03 +07:00
bflatastic 65e2168791 Shape Builder: the gaps closed paths enclose are regions (#1691) 2026-10-14 11:28:21 +07:00
dotCooCoo c44ece3113 Render: the art prepared for Separations Preview and Overprint Preview is kept between frames
With Separations Preview on, the renderer copied every node of the
document in every frame and separated each of its colors again. With
Overprint Preview on, it copied each object with an overprinting fill
or stroke in every frame, with the groups and layers that hold it. The
copies were new Arcs, so the geometry, stroke outline and raster
caches missed for each of them in every frame. In a release build,
drawing a 2880x1800 frame of a 50,000-path document again with the
same view and Separations Preview took 795 to 908 ms.

The renderer now keeps the art it prepared and what it was made from,
and in the next frame it uses that art again for each layer, symbol
art and member that has not changed. Only the objects that changed are
prepared again, with the layers and groups that hold them. All of the
art is prepared again when Discard White Overprint changes and, with
Separations Preview, when the plates shown, the proof setup, the
swatches, the Spot Colors option or the color settings change or a
profile is registered (cms::profiles_generation). The renderer now
draws that frame again in 98 to 107 ms, with the same pixels.

Docs: docs/target-app-parity.md.
2026-10-14 11:22:41 +07:00
bflatastic 991ce2c635 Importers: PDF streams the reader would fail on are refused (#1700, #1701)
hayro (0.7.2 and 0.8.0 alike) inflates Flate and LZW data with no ceiling, so a few megabytes
of zeros ask for gigabytes, and applies a predictor before checking its parameters: Columns 0
divides by zero (a panic, an abort on the web), a huge Columns allocates a row that large (an
abort everywhere). Both happen inside the reader, before any of our limits.

A file is refused ("a stream in it would decode to more than 256 MB, or its decode parameters
can't be applied") when a stream's decode parameters ask for a predictor the reader can't apply,
or when it decodes past 256 MB through Flate, LZW, run-length and the ASCII encodings before
them, decoded here with a ceiling (only streams large enough to get there are decoded). Every
object's stream is looked at once the file is open (decrypted, references followed), with the
inline images of anything that can be content; object and cross-reference streams, which the
reader decodes while it opens the file, are looked at first in the file's text.
2026-10-14 10:13:45 +07:00
bflatastic 0efcaeb558 Importers: a brush that draws itself by name no longer crashes
A scatter brush whose art is an instance of a symbol stroked with that brush, or any brush whose
art is painted with a pattern whose tile is stroked with it, drew itself without end: a stack
overflow in a hand-written native file. Brush pieces already strip the brushes of their own
strokes; now what they draw by name that would draw a brushed stroke (a symbol instance, or a
pattern paint, also through the symbols and patterns those use) is left out too, in one place
(`stroke_pieces`, which every renderer and exporter calls with the document).

Probed the rest of item 2 of the depth follow-up with hand-written hostile inputs, kept as tests:
graph designs of deep art and of a symbol holding itself, Affinity groups nested to and past
both of its reader's caps, DXF blocks inserting themselves, EMF/WMF saving states without end,
and plug-in manifests and output nested 30,000 to 100,000 deep. None of them overflowed.
2026-10-14 10:13:45 +07:00
bflatastic 17d2493616 Importers: PDF nesting inside compressed streams no longer crashes (hayro 0.8)
hayro-syntax 0.8.0 reads arrays and dictionaries 64 deep and no deeper wherever they are (the
file's text, object streams, the content of pages, forms, patterns, glyphs and masks), walks the
page tree without recursion and follows a chain of object streams once: what 0.7.2 overflowed
the stack on. hayro-interpret 0.8.0 comes with it (a new device interface: the importer's device
takes it as before). Our raw pre-scan of the file's text and page content goes: the reader's own
cap covers more, and the scan miscounted the bytes of unfiltered binary streams.

What 0.8.0 still builds by recursion with no cap is refused when the file opens: functions and
colour spaces built on themselves or on one another more than 16 deep, and calculator functions
whose procedures nest more than 64 deep. The import runs on a 16 MB thread, as the SVG importer
does.

hayro-syntax 0.8 turns on flate2's zlib-rs backend, which the whole app then deflates with: a
compressed native file is the same gzip stream as before (header and data), but its deflate
blocks now depend on how the data is fed, so the streaming test checks the header and the
unpacked data instead of the bytes.
2026-10-14 10:13:45 +07:00
bflatastic 3ae71c16ae Importers: an SVG's linked files are read within the automation roots again
The SVG importer runs on a thread of its own since #1852 (for its stack), and the automation
roots in force on the thread that opened the file didn't hold there: an agent confined to a
read root could have the app read any file an SVG links to. The reads now run in the scope of
the thread that asked.
2026-10-14 10:13:45 +07:00
bflatastic 0cb55bc0aa Fix #1717: sampling and recolouring check an image's RGBA size before decoding
The decoder's 512 MB limit is on the file's samples; a grey image within it
is four times that once converted to RGBA for the Eyedropper or a recolour,
which the web build can't allocate. The size is now read from the header and
held to the allowance the importers and the renderer use.
2026-10-14 08:47:10 +07:00
bflatastic 355bb5c4f6 Fix #1697: a failed command.batch restores each document whole
The rollback undid the batch's interaction, which holds the art, the selection
and panel state only: a Save As step's new file, format and saved state stayed,
and an Undo step (which ends the interaction) left the art and the history
undone. The batch now keeps the documents as they were and puts them back,
and settles a drag or typing in progress first, as Undo does, instead of
running inside it and keeping its baseline as the batch's step. Files the
steps wrote stay written.
2026-10-14 08:47:10 +07:00
bflatastic 61ef472e71 object.setBounds: one helper for the asked width and height 2026-10-14 08:17:14 +07:00
Marco Arnulfo 4ef0cb76e6 Transform panel: X/Y/W/H edit the anchors selected with Direct Selection (fixes #1824) 2026-10-14 08:16:26 +07:00
Mara Johnson 891a58eff8 fix(snap): snap Direct Selection anchor drags to grid intersections (#1816) 2026-10-14 08:16:24 +07:00
bflatastic f97d57f859 Fix #1719: Redo keeps a drag in progress as a step, as Undo does
Redo swapped the document and the history under a live interaction, whose
snapshot was still the document from before: the next preview or a cancel
brought that one back while the history recorded the redo, so the redone edit
was lost. Redo now settles the interaction first, the way Undo already did: a
drag that changed something is kept as the newest step (which ends the steps
to redo), one that changed nothing is dropped.
2026-10-14 08:11:02 +07:00
Gert Pellin e54fe9c31e Native reopen: type in symbols gets its layout bounds back (#1738)
Type's layout bounds are a cache that isn't saved. On open they were
computed again only for type in the layers, so type in a symbol definition
(or a pattern tile, graph design or opacity mask) kept the rough estimate,
and an envelope warping it was fitted to a different box: warped type in a
symbol changed shape after a save and reopen. The refresh now goes through
every art tree, masks included.
2026-10-14 08:01:50 +07:00
Gert Pellin 904414aa7d Global swatch edits reach opacity-mask art (#1735)
A mask's art lives outside the layer tree (in the masked object's extras),
and the walks over linked colours skipped it: editing a global swatch
recoloured everything but the mask, which kept showing (or hiding) as
before, and a swatch used only in a mask counted as unused. The colour
walks now go into opacity masks: map_solid_paints (swatch edits, renames,
deletes, Merge and the like) and visit_paints (unused swatches).
2026-10-14 08:01:50 +07:00
Ayberk A. 8894da68d7 Part of #1736: one toolbar column runs on without group labels too
Without group labels, the single column no longer draws a dash between
the groups: its tools are evenly spaced from top to bottom, as two
columns already run on from group to group. With the labels, each group
still starts under its name.
2026-10-14 07:37:54 +07:00
Ayberk A. 03c0b8f355 Part of #1736: the toolbar without group labels, and the All Tools drawer's groups, flyout menu and Show section
- Preferences › User Interface › Show Tool Group Labels is off as it
  ships: the toolbar sets its groups apart with a faint dash, as the
  Basic and Advanced toolbars of the public toolbar reference do (the
  categories there are the Getting Started toolbar's). It no longer cuts
  "Navigate" to "Navi...". The labels are a preference away.
- The All Tools drawer lists the tools one per row (icon, full name,
  shortcut on the right), each flyout group framed together under the
  heading most of its tools have (the Pencil's group, led by the Shaper,
  under Draw). Tools already in the toolbar stay dimmed; dragging and
  picking are as before.
- Its ☰ menu has Reset and the Basic and Advanced toolbars, the drawer's
  flyout menu of the reference.
- Its Show section shows or hides the controls under the toolbar's tools
  (Fill and Stroke, Coloring, Draw Mode, Screen Mode), as the reference
  describes; `toolbar.show {control?, on?}`, saved with the preferences
  (`ui.toolbar_show`, all on for older ones). Edit Toolbar's ••• stays.
- New strings in every catalog.

Reference: https://helpx.adobe.com/illustrator/using/tools.html (the look
is VectorCraft's own, with its own icons).
2026-10-14 07:37:54 +07:00
bflatastic c10da25ce4 Importers: nesting is capped, so a deeply nested file can't overflow the stack 2026-10-14 07:37:52 +07:00
bflatastic 1f4daf519d Image Trace: enhanced versions of the presets
Each documented preset has an enhanced version (`enhanced: true` in
`imageTrace.make`, an Enhanced checkbox under the panel's preset): the preset
with Auto Grouping, and with Shapes in all but High Fidelity Photo. Gradients
stay an option. Flat Logo, a preset of our own, has none. The legacy presets,
and documents traced with them, are as they were.

So that an enhanced preset is no worse than its preset on that preset's inputs:
- A hole that one colour fills is the same shape as the path that fills it, so
  the two still share one outline (a shape beside a fitted hole left slivers).
- A rectangle is as large as where its sides lie on average, not their farthest
  points.
- Auto Grouping keeps the paths of a group in the order they were traced in. It
  put a path first and what is inside it on top: a white counter traced under
  its black letter was then painted over the letter's edge, a light seam
  (Default with Auto Grouping, edge dE 10.24 -> 9.88; legacy 10.05).

The harness measures every enhanced preset beside its preset.
2026-10-14 06:43:57 +07:00
bflatastic 638d55983f Image Trace: stroked lines get square caps where their ink is cut square
Create: Strokes drew every line with round caps: a square-ended line 10 px wide
had its corners 2.94 px outside the stroke.

- A line's ends are square when its ink reaches as far a third of the width to
  either side of its middle as along it (a round end stops short there by a
  seventh of the width); told from lines 3 px wide and more, all the ends of a
  line together. The stroke then has square caps (`TracedPath::square_caps`).
- The last points of a wide line's skeleton can turn towards a corner of its
  end. An end that has left the line the skeleton followed a width back (by a
  quarter of the width, 1 px at least) is put back on it; and an end may be
  pulled back as well as pushed out to where the ink ends.

Outline on the thin-line inputs: largest distance from the art 2.94 -> 0.88 px
(clean 2.94 -> 0.41; aliased 0.88 -> 0.81), mean 0.055 -> 0.050 px, anchors
14 -> 10.
2026-10-14 06:43:57 +07:00
sanyamsingh001andCodex bb07d267aa G1: keep Artboard deletion consistent across controls
Co-authored-by: Codex <[email protected]>
2026-10-14 05:15:42 +07:00
sanyamsingh001andCodex 5f4da3beb2 G4: capture character styles from selected text
Co-authored-by: Codex <[email protected]>
2026-10-14 05:15:34 +07:00
sanyamsingh001andCodex 123b8ad8ea G4: propagate character-style Auto-leading resets
Co-authored-by: Codex <[email protected]>
2026-10-14 05:15:30 +07:00
sanyamsingh001andCodex 0e6b2e331d G1: create width points through Width Point Edit
Co-authored-by: Codex <[email protected]>
2026-10-14 05:14:20 +07:00
bflatastic bc50c7fa56 Identify Objects By: rustfmt 2026-10-14 05:13:36 +07:00
Fabio William Conceição 958c1dffe8 Part of #394: Identify Objects By shows and edits Layers names as XML IDs 2026-10-14 05:13:15 +07:00
bflatastic cfa65a2148 Appearance of Black › Printing / Exporting: cover ink-plane documents and CMYK exports
Drops the documented richBlack parameter: commands don't list it.
2026-10-14 05:12:59 +07:00
Fabio William ConceiçãoandCursor b6d14b1fd0 Part of #394: Appearance of Black › Printing / Exporting draws pure CMYK black as RGB black
Co-authored-by: Cursor <[email protected]>
2026-10-14 05:11:55 +07:00
GameDirection b5bc8f0d19 Corner Radius: a field per corner of a live rectangle, with a link toggle
The Corner Radius field in the Properties and Transform panels has a
link toggle behind it (the "singleCornerRadius" preference, on by
default). When the link is on, one field sets every corner, as before.
When it is off, the panels show one field for each corner of a live
rectangle: Top Left, Top Right, Bottom Right and Bottom Left. Each field
sets its own corner through object.setLiveShape {corners, radius}.

This is the part of #1594 that does not change the corner widget drag
or its modifier keys, split out for review.

Tests: a Properties panel test (the link on shows one field; off shows
the four labelled fields with each corner's radius, in line and in
order). The new labels are translated in all 15 catalogs.
2026-10-14 05:11:22 +07:00
Ayberk A. 043b446b94 3D and Materials: the light takes the documented settings
The public 3D and Materials panel options give the Light Settings as
Intensity (0–100 %), Rotation (-180° to 180°), Height (0° to 90°),
Softness (0–100 %) and Ambient Light (0–200 %), with the light moved to
the back or the front of the object. The 3D effects (Revolve included)
had Direction and Elevation (an azimuth and elevation), Intensity and
Ambient (0–100 %).

- Rotation turns the light round the object on the page (our reading:
  0° on its right, counterclockwise); Height raises it from beside the
  object (0°) to straight in front (90°); Light behind the object puts
  it at the back. The light gizmo's ring and line are Rotation and
  Height.
- Softness wraps the light round the object: at 0 % it falls off with
  the angle as before, at 100 % it reaches nothing only straight away.
- Ambient Light runs to 200 %; faces stay no brighter than the art.
- lightRotation, lightHeight, lightBehind and lightSoftness replace
  lightAzimuth and lightElevation. An earlier light is read as the same
  direction, so it shades exactly as before (old and new code give the
  same colours for the four effects and five lights); effect.setParams
  turns a stored one into the new form before applying new values, and
  the parameter description names lightAzimuth and lightElevation as the
  earlier form, so effect.apply and effect.setParams still take them. The
  default light is the earlier default's: Rotation 125.26°, Height 30°.
- The dialog's Lighting tab shows the settings in the documented order;
  Softness, Ambient Light and Light behind the object are in every
  catalog, and the unused Direction, Elevation and Ambient are gone.

Light colour, presets, several lights and shadows are not built.
2026-10-14 05:10:48 +07:00
Ayberk A. 39db6abfce 3D and Materials: Perspective runs 0° to 160°, as documented
The public 3D and Materials panel options give Perspective as 0° to 160°
("how close or far the object appears"); it was a 0–100 % strength, for
Revolve, Extrude & Bevel, Inflate and Rotate alike.

- The eye stands where a sphere about the object's centre, √3 times its
  half size (the box around it fits inside), fills that angle of the
  view: 0° is none, wider brings the eye nearer, and at 160° it is just
  outside the sphere, never inside the object. The documentation doesn't
  say how the angle places the eye; docs/three-d.md says this is our
  reading.
- `perspectiveAngle` (degrees) replaces `perspective` (%). A strength from
  an earlier file or script is read as the angle that shows exactly the
  same view: sin(angle / 2) = √3 × strength / 4, 100 % about 51.3°. An
  angle given too wins. effect.setParams turns a stored strength into the
  angle before applying new values. The parameter description names
  `perspective` as the earlier form, so effect.apply and effect.setParams
  still take it under the key check of #1592. Old and new code draw the four effects identically at 0, 25,
  60 and 100 % (bounds equal to 1e-9).
- The dialogs' Perspective fields run 0–160°.

# Conflicts:
#	docs/target-app-parity.md
2026-10-14 05:10:09 +07:00
dotCooCoo 4844d54fda Type: an edit copies a threaded story only when it changes one of its frames
After every edit of a document with threaded text, each frame of a
drag included, threads::reflow copied the runs and the text object of
every frame of every thread, and then dropped the copies for each
thread whose frames the edit had not changed. With a story of 50,000
characters threaded through 8 frames with a style change at every word
(9,618 runs), moving another object took 7.8 ms in a release build,
and a frame of a drag took 7.2 ms.

reflow now copies a thread's story only when one of its frames
changed. The same move takes 0.013 ms, and the drag frame takes
0.012 ms. Threads with a changed frame re-flow as before, and the
documents and undo steps after edits are the same as before.

Docs: docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
dotCooCoo 85cea7efea Open: the opening thread also reads linked files and lays out type
When the desktop app opened a document on a thread of its own, that
thread parsed it, but open_read, which the app ran on the UI thread,
read the document's linked files, decoding BMP, PSD and RGB TIFF files
and storing them again as PNG, and laid out the type. In release builds,
a document with 12 linked 3000 x 2000 TIFFs kept the UI thread busy for
1.04 s, and one with 300 Auto Size area type objects, every second one
in three columns, kept it busy for 0.85 s.

read_for_open now reads the linked files and lays out the type on the
opening thread, with the path and the Update Links preference that
opening::start passes it, and open_read only adds the finished document.
open_read now takes 0.02 ms and 0.1 ms on those documents. For the
measured documents, the opened documents, results and undo states are
the same as on main.

Docs: docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
dotCooCoo d9bd9b134d Symbols: instances of imported symbols are measured by their art
Every symbol instance was measured as its transform applied to the
20 × 20 square around the origin, which symbols made in the app keep
their art in. The SVG and DXF importers keep symbol art as drawn, so
the box of an instance of an imported symbol did not match its art.
The SVG Star instance at x 20, y 30 draws from (20, 30) to (40, 50)
and was measured from (10, 20) to (30, 40). Align moved such an
instance by the wrong amount, clicks and marquees beside its art
selected it, and the canvas, exports and prints left it out when its
art reached the view or the page and the square did not.

An instance is now measured by its symbol's art, which
Document::symbol_boxes looks up in the document each time
(Node::geometric_bounds_with, visual_bounds_with and
reach_bounds_with). With Use Preview Bounds on, the strokes in the art
count at their own weight, as the canvas draws them, and
object.setBounds counts them at that weight when it sizes an instance
or an object that holds one. When strokes do not scale and a width or
height of 0 or more is smaller than what a path's stroke adds,
object.setBounds now gives the path no width or height, as it does
for an instance whose art has strokes; before, it mirrored the path.
With Use Preview Bounds on, object.setBounds now measures the margins
that strokes add object by object, the members of groups and the
objects in symbol instances' art included, and solves for the width,
height and reference point asked for when the width and height are 0
or more. Before, it measured the margins of the whole selection and
could miss the size or the reference point asked for when strokes did
not scale and the objects' strokes added different margins, and when
strokes scaled and the width and height were out of proportion, even
for one path. A clip group, a Shaper Group, a compound path, type, and
a group or instance with effects or an opacity mask of its own are each
measured as one object, and so is an instance in perspective or one
turned or slanted relative to the bounding box, other than by quarter
turns. When the selection holds more than 4,096 objects to measure this
way, each selected object is measured as one. Instances inside symbol
art are measured by their own symbol's art. An instance is measured as
the square when its symbol is missing, when its symbol or a symbol
nested in its art holds an instance of itself, and when symbols nest 32
deep in its art.

Align and Distribute, the Transform panel, the bounding box and the
selection outline, clicks, the marquee, document.inspect and
document.node, Fit to Artwork Bounds and Fit to Selected Art, canvas
culling, thumbnails, the SVG filter region of an instance with a
raster effect, raster effect images, Export Selection, the SVG of an
artboard, printing, and the PDF, EPS, EMF/WMF and DXF writers use the
new box. The renderer also culls an instance by the effects and
brushed strokes in its art, and it culls a blend, envelope or repeat
by the brush art of what it holds and the effects in the art of the
instances in it. It culls an instance with geometry effects of its own
by the effects in its art after those geometry effects reshape the art.
It measures the effects in a symbol's art once for each symbol and
linear part of the transform that places it. For symbols whose art
holds instances, it measures at most 4,096 placements a frame, and at
most 4,096 more for each pattern tile it rasterizes and each placed
document it draws. Past that, it estimates the reach from the symbol's
reach with no transform, which it measures once. When the drawn
document's symbols change, the renderer drops the cached bounds of the
instances and of the objects that hold them.

The PDF and EPS writers, the SVG of an artboard and Skip Blank
Artboards grow the box of an instance whose art holds brushed strokes
by 16 times the widest stroke (at least 1 pt) of the objects in that
art that have a brushed stroke, plus 8 pt, as the renderer grows the
cull bounds of a brushed path. They keep an instance whose brush art
reaches the page. The SVG of an artboard also keeps an object that
holds an instance when what the canvas paints for it, the effects in
the symbol's art and the instance's own included, reaches the artboard.

Not yet: other commands that measure objects, such as Select › All on
Active Artboard, Transform Each and Convert to Artboards, still measure
an instance by the square.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
dotCooCoo 6504dc7da2 Render: live objects are evaluated again when patterns, symbols, fonts or color settings change
The renderer kept the evaluation of each blend, envelope and repeat
while the object and a hash stayed the same: the addresses of the art of
the symbols the object showed and of the patterns it painted with, and
the patterns' tile bounds. When anything else the evaluation reads
changed, such as a pattern's tile type, symbols in symbol art, the
fonts or the color settings, a renderer that had drawn the object went
on drawing the old evaluation. The quads of a gradient mesh were kept
the same way after a change to the color settings, and the art drawn
for a live object with an effect of its own was kept by the object's
address alone.

resources_key now also walks the opacity-mask, symbol and pattern art
it finds. It hashes what it finds, the font generation when it finds
type or a placed document, and the color settings. Mesh quads are made
again when the color settings change. The art drawn for an object's
effects is made again when the hash changes, if it was made from
evaluated live objects.

With other builds running, a temporary release test timed the hash for
400 live objects at 17 to 506 microseconds a frame, against 10 to 16
before.

Docs: docs/target-app-parity.md, docs/gaps.md.
2026-10-14 04:56:29 +07:00
ham9zahandClaude Sonnet 5.5 66a0f457e9 Add a Precise Cursors command and fix the misleading Caps Lock comment
General > Use Precise Cursors already worked and is saved with the other
preferences. This adds a `view.preciseCursors` command that toggles that same
preference, so it can be given a shortcut in Edit > Keyboard Shortcuts. It has
no default shortcut and no View menu item.

The comment in canvas.rs claimed Caps Lock gave precise cursors; nothing reads
Caps Lock (egui has no state for it), so it now describes what the code does.

Tests: for the tools whose own pointer the preference replaces (Pen, the anchor
tools, Curvature, Mesh, Eyedropper, Slice, Blend, Shape Builder) the pointer is
their own when off and a crosshair when on; every state of the Pen's pointer
turns into a crosshair; the preference round-trips through
to_json/apply_prefs; the command toggles it, updates the saved UI state, is
listed in the shortcut editor without a key and can be assigned one; switching
it creates no undo step and leaves the document saved; with system cursors the
glyph fallback is the system crosshair either way. The new label is translated
in the languages that claim complete menus.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-14 03:55:41 +07:00
ham9zahandClaude Sonnet 5.5 3ea4802a53 Pen: test that a click after a smooth anchor curves from its side only
Covers the Illustrator behaviour that a corner placed after a smooth anchor
has no handles, so the segment is shaped by the previous outgoing handle alone.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-14 03:55:41 +07:00
sanyamsingh001andCodex 4944facd0a CI: isolate text import fuzzing from installed fonts
Co-authored-by: Codex <[email protected]>
2026-10-14 03:55:41 +07:00
Gert Pellin ed1e976264 Fix #1748: text.create takes justify, so point type can be centred on its point
text.create, text.createInPath and the MCP add_text tool now take a
`justify` (auto|left|center|right|justifyAll, as text.setStyle). Point
type already laid out centred or right-aligned text around its origin;
it just could not be made that way, so scripts measured the bounds and
moved the text. Changing it afterwards stays text.setStyle {justify}.
2026-10-14 03:55:41 +07:00
Marco Arnulfo d9328c7668 Select > Same: type matches on its characters' fill and stroke (fixes #1737) 2026-10-14 03:55:41 +07:00
Ayberk A. 2f4cc8a1cc Fix #1756: Object → Expand expands repeats
Expand had nothing for live repeats, so a radial, grid or mirror repeat stayed
live. Its Object option now turns every repeat in the selection (inside groups
too, inner ones first) into a group of its instances, keeping the repeat's id
and place, in the same undo step; the dialog enables Object for a repeat.
Expand Repeat shares the code.
2026-10-14 03:55:41 +07:00
bflatastic 3152436020 Brackets: keep to the tools whose size the keys are documented for
The typed [ and ] now reach the Blob Brush, the Eraser and the Symbolism
tools, whose bracket keys are in the public shortcut lists. A size of the
Paintbrush's own (and path.freehand's scale) is left out until its
behaviour is decided: #1661 stays open for that.
2026-10-14 03:55:41 +07:00
Ayberk A. 3f1e72d983 Fix #1661: [ and ] size the Paintbrush, and reach the tools that take them
The tools took `[` and `]` (the Blob Brush's and the Eraser's size, the
Symbolism tools' diameter and, with Shift, intensity), but the app never
passed the keys on: only `ui.key` over MCP reached them.

- The keyboard handler gives a typed `[`, `]`, `{` or `}` (Shift) to the
  active tool when it claims the key. It reads the typed character, so
  layouts where a bracket takes Option or AltGr (Option-8 on a Turkish
  Mac) work too. A field with the keyboard, the Type tool and Cmd/Ctrl
  shortcuts are left as they were.
- The Paintbrush has a size of its own, 10 to start as the Blob Brush's:
  `[` and `]` take a point off or add one, and the next stroke is drawn
  at size / 10 of the stroke weight (path.freehand `scale`). Public
  references describe the keys as changing the size the Paintbrush draws
  with, before drawing, the brush definition left as it is; so the
  stroke weight new art takes stays, and the selected path keeps its
  stroke. Every brush type, as the report and the Tuts+ guide describe;
  the step is our own.
2026-10-14 03:55:41 +07:00
Ayberk A. ef2d678cbf Fix #1679: a filled pencil path keeps a stroke of None
The Pencil gave every path a black stroke when the stroke was None. With Fill
new pencil strokes on and a fill set, the path now keeps no stroke; only a path
that would show nothing (no fill, no stroke) still gets black.
2026-10-14 03:55:40 +07:00
Ayberk A. 1e60a572b1 Fix #1686: Escape in type editing goes to the Selection tool
Escape still ends editing (an empty new point type is discarded) and the type
stays selected; the Type tool now claims Escape while editing and asks for the
Selection tool, so tool shortcuts work again without the mouse.
2026-10-14 03:55:40 +07:00