Commit Graph
383 Commits
Author SHA1 Message Date
dotCooCoo 8a730bab6b Reject invalid New Art Basic parameters 2026-11-06 11:15:35 +07:00
dotCooCoo b26bd3d472 Reject invalid Guide Lock parameters 2026-11-06 10:41:08 +07:00
dotCooCoo 398131b9cc Reject invalid Transparency Grid parameters 2026-11-06 10:12:42 +07:00
dotCooCoo b4a4f2aa4f Reject invalid Slice toggle parameters 2026-11-06 09:42:35 +07:00
Don Omondi 97697420f9 Variables: Load Variable Library keeps a bound text variable the library declares (#4136)
Loading a variable library deleted every variable of the document with its
bindings, including a text variable of the same name that the library
declares. Text made dynamic and then given a library's variable name lost its
binding, so selecting the library's data sets changed nothing. Such a
variable now stays bound, and the library's data sets change its text; other
variables are deleted as before.
2026-11-06 07:45:21 +07:00
Don Omondi f987c2bbb0 Intertwine: the object brought over leaves no hairline of the one below (#5337)
The area an Intertwine member is drawn over another in was exactly the
overlap face, so its edge lay on the edge of the member below. Both edges
were anti-aliased, and the member below showed through as a thin line along
them. The area now reaches 2 pt past the face into the parts where only the
raised member paints (where drawing it again changes nothing), so its edge
lies inside that member.
2026-11-06 07:23:35 +07:00
Don Omondi dfe628e906 Blend spine: Shift-click picks several spine points, which drag together (#5285)
Direct Selection kept only one picked point of a blend's spine. Shift-click
now adds a point of the same spine or takes it away, and dragging any picked
point moves them all. object.blend.spine.moveAnchor takes `with`, the other
points to move by as much, with the key objects on them, in one undo step.
2026-11-06 07:23:19 +07:00
Don Omondi 483de72dbb Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box (#3901)
Squashed from #5851 onto main.
2026-11-06 05:33:00 +07:00
Don Omondi b976de560d Join tool: scrubbing the ends of two crossing paths trims them and joins the paths at the crossing (#5844)
The Join tool drew a segment between the open ends of two crossing paths
instead of trimming what overhangs the crossing. path.joinScrub now looks for
two crossing open subpaths whose overhanging ends the scrub runs over, cuts
both at the crossing and joins the kept parts there with a corner, in one undo
step. Without a crossing the endpoint join is unchanged.
2026-11-06 05:32:24 +07:00
FredAppsandClaude Opus 5.5 64f7b49dea Selection: a hover checks the bounds memo once, not once per object
Hovering looks up the bounds of every selected object, and each lookup
went through the memo key check, which compares the whole selection. That
made a hover quadratic in the selection: 1.5 s at 50,000 selected paths.

The walk now checks the key once and shares it across its lookups.

Part of #1280.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 01:07:34 +07:00
Don Omondi 22a4dd6285 Libraries: spot colours stay spot colours in every document (#5812)
A library colour kept only its values, so a spot colour came back as a
process colour once its document was closed or in a new one. The library
now keeps a spot colour's ink name and tint (in the .vclibrary file too);
library.use links the paint to the document's spot swatch of that name,
adding it when the document has none of that colour, in one undo step.
A spot swatch dropped on the Libraries panel brings its name and tint.
2026-11-06 00:42:10 +07:00
dotCooCoo bed4154597 Links: Find Linked Files resolves parent folders in a saved path
A saved link path such as `Disk:Projects:Art::photo.png` (classic Mac OS)
or `/old/Projects/Art/../photo.png` names a file in `Projects`. Ranking
the files found compared `Art` first, so a file under a folder named
`Art` came before one under `Projects`. The `::` runs and `..` components
are now resolved before the folder names are compared.
2026-11-06 00:24:44 +07:00
Alex 9e2e0e0f69 Pencil: a new stroke with Keep selected off leaves nothing selected (#5752)
Drawing a new Pencil or Paintbrush stroke kept the earlier selection: the engine's
`insert_look` selects the new path only when `keepSelected` is on, and with it off left the
selection as it was. Illustrator starts such a stroke by dropping the selection, and the new
stroke is not selected with Keep selected off, so the command now clears the selection after
inserting a new path.

What stays as before: with Keep selected on a stroke selects only the path it draws (the
earlier selection is replaced); a stroke that continues or redraws a selected path with Keep
selected off deselects that path and keeps the rest of the selection. The Blob Brush keeps its
own Keep selected and Merge only with selection options.

`path.freehand`'s documentation, the engine test
`pencil_with_keep_selected_off_leaves_nothing_selected` and the Tool Options test
`the_new_path_is_selected_only_with_keep_selected_on` cover the three cases. Docs: roadmap,
target-app-parity and ui-parity carry the change.
2026-11-05 23:42:01 +07:00
dotCooCoo 84904020e8 Images: a colorized image's missing-link placeholder shows in its own colors
A linked 1-bit image mask from a .ai file keeps its fill color as a
colorize paint, so the image takes the color once its file is relinked.
Until then the image shows the missing-link placeholder, and the canvas
and PDF export colorized that placeholder too: its light tile turned
nearly transparent and its red cross took the fill color.

ImageBlob::takes_colorize is false for a preview whose pixels are not
grayscale. While it is false, the canvas draws the image and PDF export
writes it as it is, printed separations separate it as an uncolorized
image, and transparency flattening (EPS, PDF 1.3 and PDF/X-1a export,
Flatten Transparency) does not count its paint as transparency. The
paint stays on the image, and a grayscale preview is still colorized.
SVG and EPS export already write only grayscale images colorized.
2026-11-05 23:25:05 +07:00
dotCooCoo 48cf28fe48 Links: a classic Mac OS path is named by its file name
A linked image read from a version 7 `.ai` file can keep its path in
the classic Mac OS form, with colons between folders
(`Macintosh HD:Images:photo.tif`). The link's name was the whole path,
so the Links panel showed it as one long name, and Relink to Folder,
the search of the document's folder and Find Linked Files looked for a
file with that whole string as its name.

A path with a colon and no `/` or `\` is now read as a classic Mac OS
path. Its name is the part after the last colon, and `links.info` gives
the rest as `location`. Relink to Folder, the document-folder search and
`links.findFiles` look for that name, and Find Linked Files compares the
path's folders with the folders of the files it finds. The link keeps
the path as stored. Paths with a `/` or `\` keep those as separators,
so a file name may still hold a colon (`/Users/a/x:y.png`).

Docs: docs/mcp.md (Links), docs/file-format-parity.md (version 7
linked images).
2026-11-05 21:46:43 +07:00
Hamza 728352269d Color Effects: Apply Fill to Stroke and Apply Stroke to Fill become add-on commands
As asked in review: the reference app has no such commands, so they ship
in the Color Effects add-on. paint.fillToStroke / paint.strokeToFill are
now addon.color.fillToStroke / addon.color.strokeToFill in
cmd/addons/color.rs, listed in Object > Plug-ins > Color Effects instead
of the Color panel menu, with catalog rows in all 26 catalogs.

Two fixes from CodeRabbit's review:
- A graph's series marks and labels give the graph the copied paint in
  the same undo step (capture_series_paints / capture_label_styles, as
  paint.setFill does), so drawing the graph again keeps it. Targets go
  through graph::paint_targets, so a series group stands for its marks.
- A copy that would leave the effective paint as it is skips the object,
  so None copied onto a missing fill or stroke adds no item and records
  no undo step.
2026-11-05 21:46:43 +07:00
Hamza 0db0c7ddae Paint: Apply Fill to Stroke and Apply Stroke to Fill (#5530)
paint.fillToStroke and paint.strokeToFill copy each target's own top fill onto its top stroke, or the other way, so a mixed selection keeps each object's colour. They aim and undo like Swap Fill and Stroke: groups and compound paths by their members, each text run with its own paint, and only the characters the Type tool has selected. A stroke given paint at width 0 gets 1 pt, skipNone leaves objects whose source paint is None alone, and the defaults for new art change the same way. Both are in the Color panel menu after Invert and Complement.
2026-11-05 21:46:43 +07:00
NetterSilverandClaude Haiku 5.5 0001a1c702 Disable Object > Path items when no path is selected
Fixes #5796

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 21:34:32 +07:00
FractalEncrypt 884a330fae Keep Group Selection drags on the selected outlined component
Squashed from #3272 onto main.
2026-11-05 20:00:12 +07:00
dotCooCoo bf7ca194bb Type: Convert to Text leaves a paragraph that starts in a locked frame of the selected story
While Select All holds a threaded story, a paragraph whose text starts in one of the
story's locked or hidden frames is no longer converted, and the empty parts of a converted
paragraph in such a frame are no longer restyled. Before, a locked frame whose first
paragraph took in the previous frame's empty last part was converted, and so was a
paragraph that ran on from a locked first frame into an editable one.
2026-11-05 14:34:27 +07:00
dotCooCoo 2c1f3d7605 Type: text cleanup and Convert to Text act on every frame of a selected story
After Select All in a frame of threaded text, Remove Line Breaks and
Remove Paragraph Breaks changed only the edited frame, and Convert to
Text over MCP converted only the selected frame. Both now act on the
text of every frame of the story as one undo step.

- addon.cleanup.removeBreaks reads the frames of the story as one text,
  so a break at the end of a frame before a word in the next one becomes
  a space. The same applies when every frame of a story is selected as
  objects.
- text.convertListToText without ids, id or a range converts every
  paragraph of the story. Locked and hidden frames of the selected story
  are not converted, even when ids names them. When the call converts
  the whole story, the story stays selected; before, the call reset the
  Type tool's selection to the edited frame.
- A selection inside one frame, and unthreaded type, behave as before.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-05 14:34:27 +07:00
dotCooCoo 96ca33b0ef Tests: the Devanagari fallback-glyph test passes when the fallback face substitutes the vowel sign
`the_fallback_glyph_of_a_consonant_after_its_vowel_sign_is_reported` expected
the fallback face's cmap glyph for the vowel sign I. On the Ubuntu runner the
fallback face is Noto Sans Devanagari, whose shaping draws the sign with
another glyph (545 instead of 32), so the test failed on `main`. It now checks
that two glyphs are drawn in Ka's cluster, the first is not Ka's and the
second is, and keeps the `text.glyphAlternates` checks.

Fixes #5750
2026-11-05 14:26:32 +07:00
dotCooCoo 19d9843971 Shape Builder: a merge or erase takes the stretch of every coincident line
Where two open paths lie along the same stretch, the planar map gave
that stretch to one of them only, so a merge or an erase across it left
the other path in place there. Every other open path along an edge now
has its own piece in `lines_under`, under a closed path's outline or
along a line in front. `lines`, `edges` and Live Paint's edges stay one
per stretch, and an Alt-click there still takes the piece in front.
2026-11-05 14:12:18 +07:00
NetterSilverandClaude Opus 5.5 568bb9315d Line a moved selection's edges up with ruler guides
Fixes #5746

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:45:49 +07:00
jo-quast bddb834231 Part of #394: each run hyphenates in its own language; Default Language sets new type's
Squashed from #2818 onto main.
2026-11-05 12:17:33 +07:00
ruchirguitarandClaude Opus 5.5 e0645341a4 Document tabs: drag a tab along the strip to reorder the open documents (#5663)
A document tab dragged sideways along the tab strip moves its document to the
place an insertion line marks; the current document stays current. Dragged
above or below the strip, the tab still tears off into a floating window. The
views Window > New Window opened move with their document, and each document
keeps its own zoom, centre and artboard.

The move is the engine command `document.move {from?, to}`, so the control
channel, MCP and recordings reach it, and a replay moves the document before
the steps recorded after it. `sync_views` keeps the views with their documents
by uid when the documents change order.

Behaviour: https://helpx.adobe.com/illustrator/using/customizing-workspace.html

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:02:46 +07:00
NetterSilverandClaude Opus 5.5 1db874610f Keep Pencil and Paintbrush Fidelity in screen pixels at any zoom
Fixes #5729

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:52:34 +07:00
FredApps ed78763866 Merge pull request #4784 from datatimp/fix/3260-halftone-radius-pixels
Color Halftone: Max Radius in pixels of the raster effects resolution
2026-11-05 04:09:48 +01:00
Don Omondi 23bcef97c7 Isolation mode: a locked or hidden layer keeps the isolated art from clicks and marquees (#5309)
Locking or hiding the layer (or a group) around the isolated object left
its art selectable with the Selection tool: the isolation branch of the
hit test only checked for a locked artboard, and the marquee's checked
nothing. Both now pass over isolated art that isn't editable.
2026-11-05 09:32:28 +07:00
Don Omondi ce4f16369e PDF import: a DeviceN gradient of a spot ink over process plates keeps the ink and the plates, and PDF export writes it as DeviceN again (#3235) 2026-11-05 09:09:53 +07:00
dotCooCoo f0335d2f17 Glyphs: a fallback face's consonant glyph is reported, not its vowel sign's
When a fallback face draws Ka with the vowel sign I, the vowel sign's
glyph comes first in Ka's cluster, and text.glyphAlternates reported it
for Ka, laid out or overflowing. The query now finds the face that draws
the character and takes that face's glyph for it by the same rule as
for the requested face.
2026-11-05 08:37:18 +07:00
dotCooCoo 8118b59d19 Glyphs: the Glyphs query reports the fallback glyph of an overflowing character
When the requested font has no glyph for a character, the text draws it
from a fallback face, and text.glyphAlternates reports that glyph. For
a character that overflows its area, the query shaped the character
alone but kept only a glyph of the requested face, so it reported the
requested face's default glyph (0). It now takes the first glyph drawn
for the character when the requested face draws none, as it does for
laid-out text.
2026-11-05 07:56:26 +07:00
dotCooCoo ee9cce43dc Symbols: an instance fill with no opacity keeps the art's swatch links
A symbol instance whose fill has an opacity of 0 no longer counts as
stained. Expand Appearance and the other places that build an
instance's art used to rebuild every solid paint as an unlinked color
even though the tint changed nothing, so the art lost its global and
spot swatch links.
2026-11-05 07:44:08 +07:00
dotCooCoo e33be0eea9 Glyphs: a character's glyph is the first of its glyphs in its cluster
Ka, the halant, Ka again and the vowel sign I are shaped as one cluster:
the vowel sign, the first Ka's half form, then the second Ka's default
glyph. The glyph lookup looked for the character's default glyph across
the whole cluster before its alternates, so for the first Ka it found
the second Ka's default glyph instead of the half form. It again goes
through the cluster in order, and takes the first glyph that is the
character's default glyph or one of its alternates and is not the
default glyph of another character of the grapheme, so a vowel sign's
glyph that the font also lists as an alternate of the consonant is not
reported for the consonant.
2026-11-05 07:30:43 +07:00
dotCooCoo ea75150131 Menus: Delete Empty Artboards and Save Gradient Preset decide their enabled state with less measuring
The enabled check of artboard.deleteEmpty built the full list of empty
artboards, measuring the visual bounds of the art against every
artboard, each time the Artboards panel menu was drawn. It now returns
false at once in a document with one artboard (that artboard is never
deleted). With two or more, it asks the new
Document::any_artboard_without_art whether any unlocked artboard has no
art, which is the same answer as a non-empty list.

Document::any_artboard_without_art builds the symbol boxes once for all
the artboards and first tests each path and compound path by a quick
box: its control points' box grown by the farthest its strokes reach
(Appearance::outset), with a margin for rounding. An artboard that no
quick box meets is reported empty without measuring along any path.
Only when every artboard may hold art is each one measured.
Document::artboard_has_art also skips measuring an object whose quick
box misses the artboard; its answers are unchanged.

The check of gradient.presets.save found the proxy paints through
Node::proxy_paint, which measures the object's geometric bounds for a
gradient box the check does not use. Node::proxy_paint_only picks the
same paint without measuring, and the engine's proxy_paint uses it, so
the Fill and Stroke proxies and the mixed-paint test no longer measure
each object either.

With an open stroked path of 200,000 anchors selected, the two checks
take under 0.01 ms instead of 21.9 ms and 17.2 ms in a release build, and
Delete Empty Artboards' check takes 0.85 ms instead of 42.1 ms with a
second, empty artboard.

Tests compare each check with what its command does (one artboard, all
empty, one empty, locked empty, nested art; with and without a
gradient), compare any_empty_artboards with the previous check and with
measuring every object on 120 seeded documents, compare the quick boxes
with the visual bounds on seeded strokes, and compare proxy_paint_only
with proxy_paint on seeded objects.

Docs: docs/target-app-parity.md, one line under Delete Empty Artboards.
2026-11-05 06:55:59 +07:00
dotCooCoo 8f8f71f803 Glyphs: the Glyphs query reports the glyph of an overflowing character
text.glyphAlternates reported the default glyph as `chosen` for any
character that overflows its area. A character whose style substitutes
another glyph (`salt`, or `vert` in vertical type) was reported with a
glyph it is not drawn with. The query now shapes such a character alone,
with the rest of its grapheme in their own styles and in the text's
writing direction, by the same helper text.setGlyphAlternate uses.

The helper also shapes the character in the direction its paragraph
sets it in, from its bidi level, so a parenthesis set right to left is
reported with its mirrored glyph in both commands.
2026-11-05 06:55:55 +07:00
dotCooCoo 1968077984 UI-1676: Ignore hidden symbol tint when expanding 2026-11-05 05:54:43 +07:00
dotCooCoo dcefceddf6 UI-1675: Preserve symbol tint when expanding effects 2026-11-05 05:52:34 +07:00
dotCooCoo 92ecb4a6b0 Glyphs: a consonant's ligature in a cluster with its vowel sign is its glyph
Ka with a nukta and the vowel sign I is shaped as one cluster: the vowel
sign's glyph, then the ligature of Ka and the nukta. The ligature is
neither Ka's default glyph nor an alternate, so the lookup found no
glyph for Ka: text.glyphAlternates reported the vowel sign's glyph and
text.setGlyphAlternate reported Ka's default glyph. When the cluster
holds neither, the lookup now takes the first glyph that is not drawn
for another character of the grapheme.
2026-11-05 05:20:03 +07:00
FractalEncrypt 77a86288e7 Transform Each: Relative and Absolute scaling, the scale link, and the documented panel order
Squashed from #5695 onto main.
2026-11-05 04:37:46 +07:00
dotCooCoo 67140d5e54 Glyphs: the Glyphs query reports a consonant's glyph, not its vowel sign's
text.glyphAlternates reported the first glyph of the character's
cluster as the chosen glyph. A vowel sign drawn before its consonant
(Devanagari Ka with the vowel sign I) comes first in that cluster, so
the query reported the vowel sign's glyph for Ka, while
text.setGlyphAlternate reported Ka's own glyph. Both commands now take
the character's glyph by the same rule, and so does the check in
text.setGlyphAlternate that the chosen glyph is drawn.

A character that overflows its area is shaped with its grapheme, and
each character of the grapheme now keeps its own style there, so a
nukta of another size no longer makes a ligature with its consonant
that the laid-out text does not draw. A test font with a Devanagari
nukta ligature checks both.
2026-11-05 04:11:37 +07:00
dotCooCoo af74427ef4 Appearance: edits to the active row reach the matching row in each selected object
Paint and effect edits matched the active row across all selected
objects at once, so one object without an equal row made every object
edit the row at the raw index. ItemTarget::per_target and
effect_targets now match each object against the first one on its own,
as items_per_target does. effect_targets also looks up the row in the
stack it belongs to when deciding whether an effect edit is
object-level, so an effect on a row that sits lower in another object's
stack no longer fails with "no appearance item".
2026-11-05 03:55:55 +07:00
dotCooCoo 0f99ca1895 Gradient: Perceptual PDF help says an RGB destination keeps RGB stops in RGB
The `method` text of `paint.editGradient` and two passages of
docs/mcp.md said a CMYK document writes RGB and Lab gradient stops in
CMYK. An export that converts colors to an RGB destination profile
converts them to that profile first, so in a CMYK document they stay
RGB and a Perceptual spot gradient with such stops is written as an
image with the spot warning. The texts now say so, and the test of RGB
stops in a CMYK document also checks the RGB destination case.
2026-11-05 03:55:52 +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
datatimp 1a1285fecd Merge remote-tracking branch 'upstream/main' into fix/3260-halftone-radius-pixels
# Conflicts:
#	crates/doc/src/limits.rs
#	crates/engine/src/cmd/fileio/tests_pdfedit.rs
#	crates/format/src/lib.rs
#	docs/target-app-parity.md
2026-11-04 12:30:23 -05: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