Commit Graph
3013 Commits
Author SHA1 Message Date
dotCooCoo 0ce3147aab History: the New Document button makes a document from the selected state
The History panel gets a New Document from State button at its bottom,
which runs the new edit.newDocumentFromState command. The command makes
a new document holding the art of a History panel row ({index?}, the
current state when omitted) and makes it the active document. An index
that names no state is an error and changes nothing.

The new document takes the next Untitled-N name, is marked unsaved, has
nothing selected, no steps to undo or redo and no modified date, and
leaves symbol, opacity-mask and pattern editing; the art drawn in a
pattern's tile is stored in the pattern, as Done stores it. Its links point at the files the
source finds, including files found in the source's folder away from the
links' paths. Those files are looked for only within the folders the
asking caller may read; for a caller without limits, within the folders
the caller that opened the source may read. The source
document, its steps and its saved state stay as they are; typing or a
drag in progress is first kept as a step, as Undo keeps it. While an IME
composes, the command waits, as Undo does.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:20:59 +07:00
dotCooCoo 17dc4ed46d Selection: tests check mesh lines in drag outlines; docs list the changed cases
The mesh tests now count each feedback path's subpaths, so a mesh that
drew its boundary instead of its mesh lines fails them. The progress-doc
lines name the Pathfinder, mesh and symbol instance outlines and the
limit's count of evaluated objects.
2026-11-08 12:20:55 +07:00
dotCooCoo 63de44dcca Selection: a group dragged with Real-time Drawing off shows each member's outline
With Real-time Drawing and Editing off, each pointer move of a drag ran one
boolean union of a group's members to draw its outline. A group of 256
overlapping ellipses took seconds per move, and at 1,024 overlapping ellipses
the union went over its budget and no outline showed.

A move or handle drag now draws the outline of each visible member of a group
or intertwine, and of each object a blend, envelope, repeat, Pathfinder effect
or object effect evaluates to, without uniting them. A gradient mesh draws its
mesh lines and a symbol instance its bounds, as their selection highlights do.
The limit of 100,000 counts outline points and evaluated objects, so a
selection past it shows its moved bounds.

A compound shape or Pathfinder effect still evaluates its boolean on each move,
and so does a clip group whose clipping path is a group.
2026-11-08 12:20:55 +07:00
dotCooCoo ad249f1375 Merge main into fix/edit-copy-artboard-route 2026-11-07 21:17:29 -08:00
dotCooCoo f00308fa4a Merge main into feat/plugin-effect-paint 2026-11-07 21:15:56 -08:00
dotCooCoo 142366d9ee Effects: placed gradients move with Transform and Free Distort effects
A gradient placed with the Gradient tool stayed where it was when the
object's Transform or Free Distort effect moved, turned or scaled the
outline, so the shape slid across its own gradient.

The Transform effect now maps placed gradients by its own transform,
and each of its copies takes them as many times as it takes the
transform. Under Free Distort, each path's placed gradients move and
scale from the path's bounds before the effect to its bounds after it.
This change does not alter gradients fitted to the object.

The canvas, Expand Appearance and export, Pathfinder and compound
shapes, groups, a fill's or stroke's own effects, and Object › Expand
of a gradient fill apply the same rule. In a group, the own fills and
strokes of the group, of the groups inside it and of a symbol's art
follow it too, and images and symbol instances that a group's
Transform effect moves take the placed gradients of their own fills
and strokes along. A group whose path keeps its shape because the
effect would pass the geometry budget keeps its own gradients in
place.

This changes how existing documents draw: a gradient placed on a path,
or on an image's or symbol instance's own fill, inside a group with a
Transform effect now moves with it. Before, only the gradients of type
moved (#2214).

How gradients follow each effect is VectorCraft's own rule. The
Distort & Transform help page describes what the effects do to the
object:
https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html
2026-11-07 21:11:07 -08:00
dotCooCoo 6a0400bff7 Merge main into feat/plugin-importers 2026-11-07 21:03:05 -08:00
dotCooCoo 90cd4797ea Graphs: an added or duplicated top row sets the series' stored paint
appearance.addFill, appearance.addStroke and appearance.duplicateItem can
put a new topmost row on a series mark with the paint the mark already
shows. When another mark of the series had set a different stored paint,
the mark's top paint did not change, so the graph kept the other mark's
paint and drew it again. A new topmost visible row now stores its paint,
as an Eyedropper append already does.
2026-11-08 12:01:39 +07:00
dotCooCoo 0ce16936b7 Graphs: docs say a band's stroke opacity is still kept
The Appearance commands do not store a stroke paint or weight for a series
that draws a Filled Lines band, but they do store a stroke opacity set on the
band, and the band's 0.25 pt black outline takes it when the graph is drawn
again. docs/mcp.md now says so, and a test covers it.
2026-11-08 12:01:39 +07:00
dotCooCoo d136ddb0cc Graphs: Appearance commands' params text names the stored opacity
The params text of the Appearance commands that update a series' stored
paint said the graph stores the paint and weight of the topmost visible
fill and stroke. Each of them also stores that row's opacity, as
docs/mcp.md says. The text now names the opacity too.
2026-11-08 12:01:39 +07:00
dotCooCoo 9a08f64a04 Graphs: Appearance commands and the Eyedropper update a series' stored paint
paint.setFill, paint.setStroke, paint.none, Apply Fill to Stroke and Apply
Stroke to Fill stored a series' paint on the graph. appearance.setItem, the
Eyedropper, appearance.transfer, Clear Appearance, Reduce to Basic Appearance
and the add, remove, move and duplicate row commands stored no paint or weight.
An Eyedropper pick of a blue fill at 40% onto a red column drew red at 40%
after the next redraw.

These commands and Show All Hidden Attributes now store the paint and weight
of the topmost visible fill and stroke of a path directly in a series group,
in the same undo step. A color, weight or opacity that a command writes to that
row replaces the stored value even when the mark's own value does not change. A
row that has no paint before and after the command does not change the stored
paint. These commands store no stroke for a series that draws Filled Lines
bands, and a band no longer takes its color from an empty stored stroke.

The stored opacity also comes from the topmost visible row: hiding a top row
stores the row under it, and an opacity set on a hidden top row is not stored.

Follows #5501.
2026-11-08 12:01:39 +07:00
dotCooCoo a8a1c82a43 PDF export: warn when a PDF/X file leaves out the document's bleed
Choosing a PDF/X standard without a PDF/X preset writes a file with no bleed
(BleedBox = TrimBox) even when Document Setup sets one, and the export gave no
warning about it. The export now warns when a PDF/X file has no bleed on a side
where the document's bleed has one. The warning comes ahead of the flattening
note, because the status bar shows only the first warning. The code adds a
warning and does not change what is written.

These cases do not warn: the PDF/X presets, Use document bleed settings, a
nonzero bleed on every side the document sets, a document with no bleed, files
that aren't PDF/X files, and Export for Screens rows whose artboards Include
Bleed grows.

The warning has a row in every catalog. The Summary's warning and error rows in
the Save PDF and Print dialogs now go through the catalog, so they show its
translations.
2026-11-08 11:42:47 +07:00
dotCooCoo 4cf4ea3a39 Tests: the shape builder and large edit guards count work instead of timing it
The Shape Builder, Live Paint and run_around tests in planar/tests.rs
measured elapsed time against a 5 s limit, and the large edit tests
(large_edits.rs, with the Lock and Hide All Artwork Above and perspective
Alt-drag tests that share `soon`) against 20 s. On a loaded machine they
failed without any change in the code.

- pathops: a test-only counter adds up the items `RectIndex::at` reads
  and the segments `nearest_u` searches. The four planar tests assert a
  bound on that count.
- doc: `tree_walks` counts, per thread, the walks of a document's tree
  that lookups by id make. `soon` fails when a command walks the tree
  more than 64 times, and keeps a 300 s wait for a hang. The tests that
  named a 20 s limit are renamed.

Looking objects up one at a time in Delete or Offset Path, or scanning
the whole index or ring in the planar code, makes these tests fail.
2026-11-08 11:42:46 +07:00
dotCooCoo dbcdbf2466 Open: the first frame uses the layouts the open made for type objects
Opening a document laid out every type object for its bounds and
baselines and threw the layouts away, and the first frame laid the same
objects out again. The open now keeps them, up to 128 MiB, and a frame
takes each one once.

A frame takes a layout only for a text object equal to the one laid out,
and only while the fonts, the system fallback setting and the hyphenation
exceptions are unchanged. When the last frame being drawn ends, the rest
are dropped if a frame took one, and all of them are dropped after eight
frames that take none. A layout with bitmap or color glyphs and a text
object with a gradient are not kept.
2026-11-08 11:42:45 +07:00
dotCooCoo 7d8f2a315a Paste: skips relinking when nothing changes and finds pasted type in one walk
A paste relinked every copy to the document's resources even when no
resource had another name or color and the objects linked no graphic
style. It now leaves those copies as they are.

Finding the pasted type looked each id up and compared every text object
with the ones already found. Text commands and the bounds refresh after
a paste now look all ids up in one walk and keep the found ids in a set.
Giving a pasted group new ids builds its map of old to new ids only for
an Objects on Path group, which is the only kind that reads it.

A pasted solid paint with no swatch link and a tint other than 1 now
keeps that tint when no relinking runs. Saved art carries a tint of 1 on
such a paint.
2026-11-08 11:22:40 +07:00
dotCooCoo 2f09d9998e Menus: Delete Empty Artboards' enabled check does not walk the document every frame
The Artboards panel menu asks whether Delete Empty Artboards can run on
every frame while it is open, and each ask measured the document. With
an open path of 200,000 anchors across two artboards, that took 70.4 ms
a frame in a release build.

The answer is now kept per open document with the version of the
document it was measured on, and is measured again when the document
changes. A later frame takes under 0.001 ms. The first frame after an
edit measures once. A change of the selection or the view keeps the
answer.
2026-11-08 11:22:40 +07:00
dotCooCoo 9e0e8993b9 Dimension tool: state the Expand rule after object.expand's parameter list
The sentence inside the braces made every_description_parses fail. The object.expand and effect.expandAppearance texts now also limit the replaced-art case to a dimension selected with that art.
2026-11-08 11:22:38 +07:00
dotCooCoo bc9ea516f5 Dimension tool: Expand keeps a dimension as plain art after it moves
Object > Expand left a dimension's recipe on its group, so the next move
redrew the live dimension and discarded the expanded art. Expand now ends
the live link of a dimension whose art it changes, in the same undo step.
Moving or scaling the group leaves its art as it is, the art no longer
follows what it measured, and undo gives the live dimension back.

Expand Appearance, Create Outlines and Outline Stroke had the same fault
and now do the same, also when they reach only a part of a dimension. A
dimension whose art a command leaves as it was stays live. A dimension
selected with the art it measures becomes a plain group when Expand,
Expand Appearance or Outline Stroke replaces only that art.
2026-11-08 11:22:38 +07:00
dotCooCoo a0bd9bbbad Artboards: duplicate, copy, cut, move and rearrange journal whether locked and hidden art goes
Squashed from #5994 onto main.
2026-11-08 11:22:29 +07:00
dotCooCoo b8233f7c19 .ai save: a reopened file opens on the artboard active when it was saved
Squashed from #5975 onto main.
2026-11-08 11:22:28 +07:00
dotCooCoo 74863fa778 Engine: execute refuses keys a command doesn't name, in every build
Session::execute ran check_params only in the engine's own tests, so a
direct execute in a release build ran a misspelled key with the
command's default. It now checks every call, the commands a command runs
included, as MCP and the control port already did. A command.batch or
an action.batch is checked before its first step runs, and each step is
checked again as it runs, so an effect.setParams step is checked
against the effects the steps before it added.

Saved steps play through Session::replay, which takes keys a command no
longer reads, so an action recorded by an older version still plays and
Session::replay still plays an older journal. The Actions panel's Play
(Accelerated, Step by Step and Pause For, its function keys and
ui.action.play) and the Batch dialog use it.

Docs: the cmd/params.rs module doc, the Parameter keys section of
docs/mcp.md and a line in docs/target-app-parity.md describe both
paths.
2026-11-08 11:22:28 +07:00
dotCooCoo d503688e16 3D (Classic): effect.lightingPreset refuses Extrude & Bevel (Classic)
Extrude & Bevel (Classic) has no lighting presets; its lights are set in
its own options. effect.lightingPreset now refuses a target whose effect
at the index is Extrude & Bevel (Classic) with an error that names it,
before any object changes. Before, every preset failed on it with an
unknown-parameter error for lightColor. The params text and docs/mcp.md
state the rule.
2026-11-07 20:10:52 -08:00
dotCooCoo f916d981d3 Plug-ins: a live effect's commands may change the document
A live effect that declares commands may list `doc.write` and `state`, and
gets them when its manifest leaves `permissions` out, as a filter does. Only
its commands use them: drawing the effect changes no object and keeps no
data. Before, every output of such a command that changed an object was
refused, since a live effect could never hold `doc.write`.
2026-11-07 19:42:38 -08:00
dotCooCoo b6fdecc2e0 Graphs: Appearance, Default Fill and Stroke and graphic style help names the stored opacity and blend mode
The help text of appearance.addFill, addStroke, clear, reduceToBasic,
removeItem, duplicateItem, moveItem, copyFrom, showAllHidden and transfer,
paint.default, and graphicStyle.apply and redefine said the graph stores
only the paint and weight of the topmost visible fill and stroke. These
commands also store that row's opacity and blend mode, as
appearance.setItem's help and docs/mcp.md already say.
2026-11-07 19:42:24 -08:00
dotCooCoo 14ea556e92 Plug-ins: a malformed ids is refused, and a command out of budget changes nothing
plugin.run and plug-in commands read `ids` strictly: a value that is not
a list, or an entry that is not a non-negative integer, is an error
naming `ids` ("ids must be an array of non-negative integer object
ids"). Left out or null, `ids` is still the selection.

A new test shows that a plug-in command that runs out of its
instruction budget leaves the document and the undo history unchanged,
and that the error names the plug-in and the command.

docs/plugins.md and docs/mcp.md name MCP's `command_list` and
`list_commands` as the listings that show plug-in commands, and the
params text, docs and progress line describe the `ids` rule.
2026-11-07 18:47:22 -08:00
dotCooCoo ea27cc0a2e Plug-ins: a command list read during an install is not cached under the new revision
registry::install now changes the revision while it holds the plug-in set's write lock, so a reader that sees the new revision and lists the plug-ins always finds the new one. command_specs also skips caching a list when the revision changed while it was listing. Before, a menu or shortcut lookup that ran during an install could keep the old command list until the next install or removal.
2026-11-07 17:43:02 -08:00
dotCooCoo ed5de5edcb Plug-ins: a plug-in adds menu commands of its own under Object › Plug-ins
A filter or live effect may declare up to 32 `commands` in its manifest,
each `{id, label, params?}`. While the plug-in is installed each one is the
command `plugin.<id>.<command>`: listed by `command.list` and in the
plug-in's `plugin.list` entry, run through `run`, MCP and `command.batch`
with its parameters checked before the plug-in runs, and recorded in
actions. A run goes through `vc_run` on the selection with
`_context.mode` "command" and `_context.command`, under the plug-in's
permissions, as one undo step named after the command. A failing run
leaves the document as it was, and its error starts with the plug-in's
name and the command's label. Removing or replacing the plug-in removes
its commands.

In the app, a plug-in with commands is a submenu named after it under
Object › Plug-ins (Effect › Plug-ins for a live effect), holding its own
item and then its commands; a `menu` key in a manifest is ignored. A
command with parameters opens a dialog for them from its menu item or a
shortcut. Edit › Keyboard Shortcuts lists the commands for the user to
give a shortcut; a manifest cannot set one. Labels and names are shown
with control and bidirectional characters escaped, and a malformed entry
is refused by name when the module is installed.

Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-07 17:32:54 -08:00
dotCooCoo 1f352bc7c1 Plug-ins: manifests list permissions, the user grants them at install, and the host enforces the grant
A manifest may list `permissions` (doc.read, doc.write, state,
effect.render, export.write) and an `api` level. doc.read, doc.write
and state follow the capability model proposed in
storytold/photocraft#662; the other names are VectorCraft's own. An
unknown, reserved, duplicate or wrong-kind name is refused by name. A
manifest without the key gets the permissions its kind implies and
installs as before.

- Install Plug-in... and File > Open show the name, id, version, file
  SHA-256 and each permission in plain words, with control and
  bidirectional characters escaped and each string cut to 64
  characters. OK installs the module only if it still has the SHA-256
  shown; Cancel installs nothing. The heavy-budget prompt and the
  status line show the escaped name too, and OK keeps the `replace` it
  was given.
- `plugin.install` needs `grant` listing every permission, and
  `plugin.inspect` reports `needsApproval`. `hash` pins the module:
  one with another SHA-256 is refused. The grant is kept with the
  module's hash in the store's `grants.json`, and only that store's
  grants count while it is attached; the same set does not ask again,
  more permissions do, and `plugin.remove` forgets it.
- The Additional Plug-ins Folder and `plugin.reload` install only
  plug-ins the attached store has granted and report the others as
  needing approval. At startup the store skips an ungranted plug-in.
- A filter run without doc.read is given no objects; without state it
  neither reads nor writes `documentData`; without doc.write an output
  that changes the document is refused and the document is unchanged.

Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-07 15:20:07 -08:00
ruchirguitar 2dc6991cf3 M2.1: retain gradient placement when deletion splits paths (#3380) 2026-11-08 03:51:00 +05:30
ruchirguitar c232919dd3 M2.1: clarify deletion evidence and function documentation (#3380) 2026-11-08 02:02:31 +05:30
dotCooCoo 74caec60d4 Commands: the View commands check their params against machine-readable schemas
A new standalone crate, vectorcraft-apischema, describes a command's
parameters: type (number with range and unit, integer, boolean, string,
enum, id, id list, color, point, object, array), whether each is
required, the default of an omitted value, whether null counts as
omitted, the API levels it was added and deprecated at, and the lenient,
cost and writes fields. It validates a params object and names the
parameter of the first problem, writes the params text, writes the
schema in the shape of a JSON Schema 2020-12 object schema, and hashes a
set of schemas.

CommandSpec gains an optional schema (both builders: the cmd! macro and
the text style specs). Session::execute checks the params of a command
that has one before anything else, refusing a value of the wrong type,
out of range or not in its list, a missing parameter or an unknown key
with an error naming the parameter, and fills in defaults; the journal
keeps the params as given. engine.commands lists each schema.

The six commands in cmd/views.rs have schemas, and their params text is
the text the schema writes. schemas::API_VERSION is 1, and a test fails
when a schema changes without a new row in SCHEMA_HASHES.

Docs: docs/mcp.md (Parameter schemas), docs/target-app-parity.md.
2026-11-07 10:37:02 -08:00
dotCooCoo 72c46b8aa2 Path ops: boolean contour passes test the shapes covering each face instead of a flag per shape
The contour passes of the two-path boolean operations, Offset Path and
the many-shape boolean (`boolean_n`, used by compound shapes and
Intertwine) built a flag for every input shape for each winding they
tested. They now test the list of inputs that cover the face, ascending,
refilled in one buffer per pass, so a test costs the size of the cover
rather than the number of shapes. `boolean_n` and `try_boolean_n` take
that list; the compound shape and Intertwine tests read it directly.

The contours, their order and the output are the same: a new test
compares every pass's test with the flag form on overlapping, nested,
touching and apart squares and on 72 overlapping squares, and another
checks that a pass over 100 squares apart hands its test one input at
most.
2026-11-07 10:28:25 -08:00
dotCooCoo 2a32d1bc65 .ai import: symbol libraries open with their symbols
A .ai or EPS whose editing data defines symbols but has no layers came
in as its PDF part or page without symbols, and Other Library in the
Symbols panel listed none. Such a file, and a file read as its PDF part
or page because its layers can't be used, now gets the symbols its
editing data defines before its first layer, as a file read from its
layers does: File > Open shows them in the Symbols panel in the order
of the symbol list, and Other Library lists them in a library panel.

- Each symbol keeps its registration point. A symbol that holds an
  instance of a symbol the file doesn't define is left out, and an
  object linked to a listed graphic style keeps the link.
- The symbols' art takes ids of the document, the art of opacity masks
  in it included, and the images it shows come with it. Instances
  drawn too deep are removed with the usual note.
- The global and spot colors the art is linked to come in as swatches.
  A color whose name another swatch has takes that swatch's place when
  it is a default swatch of a file without a palette; otherwise it
  comes in under a free variant of the name ("Ink 2"), which the art
  then names.
- Symbol art keeps the editing data's units, as in a file read from its
  layers, so in a file in larger units its linked graphic styles are
  drawn at the scaled size and the symbol art is not.
- The art of the patterns a palette or brush brings into a file read as
  its PDF part or page now also takes new ids for its opacity masks.

Part of #3349
2026-11-07 05:29:38 -08:00
MarekSpodBiedry 6162c853b9 Crop Image: cut from the pixels only when the rect is on the pixel grid, and format put_pixels (#5534) 2026-11-07 13:42:44 +01:00
MarekSpodBiedry cc75aa4916 Merge remote-tracking branch 'origin/main' into review-5560 2026-11-07 13:41:41 +01:00
dotCooCoo 5a9a08fcb1 Graphs: a series keeps the object opacity and blend modes set on one of its marks
An object opacity or blend mode set on a path directly in a graph series, and
the blend mode of its topmost visible fill or stroke, were lost when the graph
was drawn again. The graph now stores them per series next to the row opacity
it already stores, and puts them on every path directly in the series when it
draws the series again.

- transparency.set and object.setProps store the object's opacity and blend
  mode. A value the mark already has still replaces the value another mark of
  the series stored.
- appearance.clear, appearance.transfer, appearance.copyFrom with Transparency
  picked up, graphicStyle.apply without add and graphicStyle.redefine store
  the object's opacity and blend mode they leave on the mark.
- The other Appearance and style commands store them when they change them
  on a mark.
- Every command that stores the opacity of the topmost visible fill or stroke
  also stores its blend mode. A Filled Lines band's outline takes the stored
  stroke opacity and blend mode and keeps its black 0.25 pt stroke.
- An opacity and blend mode are stored as a pair from the edited mark:
  setting only the opacity of one mark also stores that mark's blend mode.
- A graph design's copy in a series keeps its own values.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-07 04:28:27 -08:00
dotCooCoo 434cf3e692 Graphs: graphic styles and Default and Swap Fill and Stroke update a series' stored paint
Applying a graphic style, Redefine Graphic Style, Default Fill and Stroke
and Swap Fill and Stroke on a path in a graph series changed the path
but not the paint the graph stores for the series. When the graph was
drawn again, the series went back to its earlier paint.

These commands now store the mark's topmost visible fill and stroke on
the graph, in the same undo step as the command:

- graphicStyle.apply stores the paint, weight and opacity of both kinds
  of row. With `add` it stores only the kinds of row that are visible
  in the style.
- graphicStyle.redefine stores the paint, weight and opacity of the
  marks still linked to the style.
- paint.default stores the white fill and the 1 pt black stroke, both
  opaque.
- paint.swap stores the swapped paints and the stroke weight and keeps
  the stored opacity. It swaps the topmost rows even when they are
  hidden, and a hidden row it swaps is not stored.

The rule for each kind of mark is the one docs/mcp.md gives for the
Appearance commands. docs/mcp.md also says what the graph does not
store from a graphic style and that a series group given to a graphic
style command is not stored. The params text, docs/mcp.md and the
Graphs line in docs/target-app-parity.md list the commands.
2026-11-07 02:54:42 -08:00
ruchirguitar e6a899857f M2.1: split disconnected path pieces after deletion (#3380) 2026-11-07 15:20:14 +05:30
Taha-Mahmoodi 727bc8790c Merge remote-tracking branch 'origin/main' into fix/ruler-double-click-guide
# Conflicts:
#	crates/ui-egui/src/canvas/tests.rs
#	docs/gaps.md
#	docs/target-app-parity.md
#	docs/ui-parity.md
2026-11-07 12:40:04 +04:30
dotCooCoo 9fe7aec49c Graphs: Appearance commands and the Eyedropper update a series' stored paint
paint.setFill, paint.setStroke, paint.none, Apply Fill to Stroke and Apply
Stroke to Fill stored a series' paint on the graph. appearance.setItem, the
Eyedropper, appearance.transfer, Clear Appearance, Reduce to Basic Appearance
and the add, remove, move and duplicate row commands stored no paint or weight.
An Eyedropper pick of a blue fill at 40% onto a red column drew red at 40%
after the next redraw.

These commands and Show All Hidden Attributes now store the paint and weight
of the topmost visible fill and stroke of a path directly in a series group,
in the same undo step. A color, weight or opacity that a command writes to that
row replaces the stored value even when the mark's own value does not change. A
row that has no paint before and after the command does not change the stored
paint. These commands store no stroke for a series that draws Filled Lines
bands, and a band no longer takes its color from an empty stored stroke.

The stored opacity also comes from the topmost visible row: hiding a top row
stores the row under it, and an opacity set on a hidden top row is not stored.

Follows #5501.
2026-11-07 00:07:40 -08:00
dotCooCoo 6ffcf800c8 Export: the export commands refuse an unknown option key
document.export, document.exportSelection, document.serialize, app.export
and file.export took {noSuchOption: true} and wrote the file with no
warning. They now refuse a key that no format lists in document.formats,
and name the nearest key when one is close. document.exportPdf,
document.pdfSettings and pdf.preset.save refuse a key PDF doesn't take,
document.exportEps one EPS doesn't take and document.exportDxf one DXF
doesn't take. Each refuses an unknown key inside an option object it takes,
such as marks.useDocumentBleed, and names its path.

A key that only another format reads, such as {format: pdf, quality: 80},
is still ignored: docs/mcp.md says one object can carry several formats'
options. The check runs for callers (the control channel, MCP, the CLI).
A journal or a recorded action isn't checked, and a saved PDF preset with
a key that is no longer read still loads.

document.formats now also lists PDF's thumbnails, fastWebView and
viewAfterSaving.
2026-11-06 23:09:15 -08:00
dotCooCoo 72af76d418 Artboards: a Cut or Copy with the Artboard tool replays on its artboards
edit.copy and edit.cut chose the artboard route by the tool chosen, which
the journal does not record. A replay in a new session acted on the objects
selected there, or stopped with "nothing selected" when there were none.

With the Artboard tool chosen, both now record selectedArtboards and art in
the journal entry. A given selectedArtboards runs the artboard route with
any tool and needs only a document. A recorded action leaves both values
out, so it follows the tool chosen when it plays. Cut and Copy of objects
record nothing new.
2026-11-06 21:35:41 -08:00
dotCooCoo 9b9803aa0f Tests: the unusable-folder prefs test restores folder modes when it fails
set_refuses_a_recovery_or_plugins_folder_it_cannot_use takes write or
list permission away from two folders. A guard now gives each folder mode
0o755 when it is dropped, also when an assertion panics, so the test
folder is removed afterwards.
2026-11-07 08:38:20 +07:00
dotCooCoo 104d9562d4 Preferences: prefs.set refuses a missing or unusable recovery or plug-ins folder
prefs.set did not check the folder given as recoveryFolder or pluginsFolder,
beyond the automation roots check, so a control or MCP caller could save a
folder that does not exist, a file, or a folder the app cannot use. Only the
Preferences dialog checked the folder itself.

The dialog's folder checks move to the engine, and prefs.set runs them after the
automation roots check. A folder that fails returns the same error the dialog
shows, and no preference changes. An empty value, an omitted value and a folder
the preference already holds are not checked. The Wasm build skips the check, as
the dialog did. The dialog's OK now gets the check from prefs.set, and Choose…
calls the moved check directly.
2026-11-07 08:38:20 +07:00
dotCooCoo c7c8e5428d Artboards: the Artboard tool's canvas menu cuts, copies, pastes, renames, rearranges and exports artboards
With the Artboard tool, the canvas context menu lists the entries of the
public Use the artboard context menu page in its order, in place of the
entries for objects and the view. Lock Artboards and Unlock Artboards are
two submenus. An entry that cannot run is left out, and Undo and Redo stay
dimmed. A right-click on an artboard that is not selected selects it alone
first, and the menu measures the art for Delete Empty Artboards and Fit to
Artwork Bounds once, at the right-click.

Cut, Copy, Duplicate Artboards, Rename, Delete Artboards, Lock Selected,
Unlock Selected, Fit to Artwork Bounds, Switch Orientation and Export act on
the artboards the tool has selected, else the active one. docs/mcp.md lists
what the other entries act on.

- artboard.rename renames in one undo step and numbers several names. The
  menu's Rename opens a dialog that runs it, and the Artboards panel's name
  field runs it too.
- artboard.switchOrientation swaps width and height and keeps the top-left
  corner, skipping square and locked artboards.
- artboard.selectAll chooses the tool and selects every artboard.
- edit.paste takes artboardsAt, and the menu's Paste centers copied
  artboards on the right-click's point.
- The menu's Cut and Copy run artboard.cut and artboard.copy. In the desktop
  app those commands publish to the system clipboard, and the menu shows the
  Cut and Copy shortcuts for them.
- artboard.fitToArt and the panel's name field record no undo step when
  nothing changes.

artboard.duplicate, copy, cut, rename and switchOrientation take their
artboards from the tool's selection when a call names none. Each now records
them in the journal as selectedArtboards, which a replay reads and a recorded
action leaves out. The copy commands also record art. switchOrientation
records scaleArt, and it takes patterns and lockedAndHidden and records what
its artboard.setProps step resolved for them.

While the Artboard tool is in use, a command chosen from any menu or the
command palette moves the tool's selection as the Artboards panel does:
artboard.new makes the new artboard active, artboard.duplicate selects the
copies, and artboard.delete and artboard.deleteEmpty make a remaining
artboard active.
2026-11-07 08:37:44 +07:00
dotCooCoo 1e2ff653b5 Save: the save commands' params text names the notes on options
document.save's params text, which file.saveAs, file.saveCopy and
file.saveAsTemplate share, now says that the warnings also name each key
given in `options` that the format doesn't read.
2026-11-07 08:19:25 +07:00
dotCooCoo 7ee6b429ba MCP: save_file notes name the legacy or non-PDF-compatible .ai
save_file's note for a key in `options` that the written format doesn't
read named every .ai file a PDF-compatible .ai, also when the save wrote
a version 7 file (`aiVersion: "7"`) or one without PDF content
(`pdfCompatible: false`).

The save itself now writes that note, beside its note on a top-level
`compress`, `version`, `preview` or `precision`, and names the file it
writes, including when it reuses the options the document was last saved
with. save_file returns the save's reply unchanged, and
`run_command document.save` and the other save commands give the same
notes.
2026-11-07 08:19:24 +07:00
dotCooCoo 3d7dda200e .ai import: opacity mask art of one object opens as that object
The reader put an opacity mask's art in a group every time, so a mask
made from one path came back from a saved .ai as a group holding the
path, while Make Opacity Mask keeps one object as the art. This also
changes how existing files with single-object masks open, because
the editing data of a .ai, an EPS and a PDF goes through this reader:
the art is the object.

A hidden object or a guide stays in a group, because the canvas draws
the art's root whether it is shown or not and whether guides show. A
clipping path alone stays a clipping group.

The editing copy writes a plain group of one object as a group (u, the
object, U), so it reads back as a group. Its other plain groups are
still written as their members. Hidden mask art is written shown,
because the canvas draws it and a group read back would not.
2026-11-07 08:19:20 +07:00
dotCooCoo d323716068 Links: a link check whose worker ends without a result no longer stops later checks
When the link-watch worker unwound before storing its stamps,
poll_link_scan returned None on every frame and left link_scan set. The
app then asked for a repaint every 100 ms and start_link_scan returned
early, so the app did not look for changed linked files in the
background again for the rest of the session.

poll_link_scan now clears a look that has ended without stamps. The new
ended_without_stamps helper holds the slot's lock across its read of the
stamps and its read of the ended flag, so a look whose worker stores its
stamps during the check is applied. The app goes back to its two-second
wake, and the next look starts on schedule. A look still running and a
look that stored its stamps behave as before.
2026-11-07 07:36:26 +07:00
ruchirguitar 5bb21cdddf Add hyphenation for eleven Indian languages
Squashed from #5946 onto main.
2026-11-07 05:15:17 +07:00