Commit Graph
2193 Commits
Author SHA1 Message Date
dotCooCoo acb6b880d5 Links: links inside symbols, patterns and masks are saved with relative paths
A save wrote the path relative to the document only for linked images and
placed documents in the layers. Links inside symbols, graph designs,
patterns and opacity masks kept only their absolute path, so after the
document and its files moved to another folder together, those files were
not found.

- The save sets the relative path of every link the document holds.
- On open, a link inside that art whose file was found away from its path
  takes the new path, as a link in the layers does, so the next save
  writes the right relative path.
- Document::update_links_where changes only the links it picks and copies
  only the nodes on the way to them.

Docs: docs/mcp.md (Linked images), docs/target-app-parity.md.
2026-11-08 12:47:57 +07:00
dotCooCoo e620da7ed6 Artboard Options: the fade covers art outside the artboards and OK sets the Global checkboxes after the artboard
With Fade Region Outside Artboard on, the canvas now draws the fade over the
art as well as the pasteboard: one mesh of black at 40% opacity covers the
canvas outside the artboards, so the pasteboard and any art on it show each
channel at 60% of its value, and art inside an artboard is unchanged. It
replaces the darker pasteboard fill.

Artboard Options sets the Global checkboxes' preferences only after
artboard.setProps or artboard.new succeeds. When the artboard command
refuses a value, the preferences stay as they were and the dialog stays
open. A Global value that is not true or false is refused before anything
changes.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:47:54 +07:00
dotCooCoo 1063f5090a Artboard Options: Fade Region Outside Artboard darkens the pasteboard while the Artboard tool is active
Artboard Options gets a Global group with Fade Region Outside Artboard and
Update While Dragging. Both are application preferences
(fadeRegionOutsideArtboard, artboardUpdateWhileDragging), on by default and
also listed under Preferences > General > Artboard Options.

While the Artboard tool is active, the canvas draws the pasteboard with each
color channel at 60% of its value. With Update While Dragging off, the
pasteboard draws as usual while the tool moves or resizes an artboard and
darker again when the drag ends. Exports and print do not change.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:47:53 +07:00
dotCooCoo e5ddabf51f Type: find and replace and Type menu commands reject a param of the wrong type
Find and Replace, Find Next, Change Case, Smart Punctuation, Insert
Character and Type on a Path Options read their params with helpers that
fell back to the default for a value of the wrong type: Find and Replace
with `replace: 5` replaced every match with nothing, and `flip: "true"`
did not flip. They now return an error and leave the document, the
selection and the undo history unchanged:

- `find`, `replace`, `case`, `char`, `text`, `unicode`, `effect` and
  `alignToPath` must be a string or null.
- `matchCase`, `wholeWord`, `dashes`, `ellipsis`, `quotes` and `flip`
  must be true, false or null.
- Smart Punctuation's `scope` must be `selection` or `document`.
- Type on a Path Options' `spacing` must be a number or null.
- Insert Character's `code` must be a whole number or null.
- Find and Replace's `ids` must be a list of existing object ids.

Insert Character checks `char`, `text`, `code` and `unicode` before it
picks one, so `{"char":"bullet","unicode":176}` is refused too. The
order the sources are used in is unchanged. Null counts as left out, so
`type.insert {code: null, unicode}` inserts the Unicode character.
Omitted params keep their defaults.
2026-11-08 12:47:53 +07:00
dotCooCoo c32f44ba3a Color: Recent Colors keeps the last 30 colors used, across documents
Recent Colors now keeps up to 30 solid colors instead of 10, newest
first and each once; applying a 31st drops the oldest. The Session
holds the list, so it stays the same when another document is opened
or made active, and it lasts for the session.

The Color and Swatches panels wrap the chips onto further rows when
the panel is too narrow for one row, so every color can be clicked.
A list that fits one row draws as before.

Docs: paint.recent params text, docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:47:51 +07:00
dotCooCoo e29e01b650 SVG import: each linked file keeps image keys of its own
Images were keyed by their bytes alone, so two <image> elements that
link different files with the same bytes shared one image key. Every
missing file shows the same placeholder, and files can also be copies
of each other. Export reads a linked file's pixels into the image key
when the document holds only a preview, so once one of the files was
found or changed, the images of the other file exported and printed
that file's pixels.

A linked image's key now also holds a hash of its link's path, as the
.ai reader's linked images do. Images of one file still share a key.
The missing-file placeholder comes from
vectorcraft_doc::links::missing_placeholder instead of a copy in the
SVG importer.
2026-11-08 12:23:53 +07:00
dotCooCoo d495b4a378 Object: Reset Bounding Box also squares the objects inside the selection
Reset Bounding Box set the angle to 0 on the selected objects only. A
rotation changes the box angle of the selected objects and of every
object inside them, so the members of a rotated group kept their angle:
after rotating two rectangles 20 degrees, grouping them, rotating the
group 10 degrees and resetting, Ungroup brought back a box at 30
degrees, and a second reset reported 2 objects to change.

The command now also squares every object inside the selected ones: the
children of groups, compound paths, clip groups, blends, envelopes,
repeats and intertwines, and the art of linked opacity masks. The art of
an unlinked mask does not turn with the object, so it keeps its angle.
Ungroup, Release Clipping Mask, Release Compound Path, Blend Expand,
Release Envelope, Release Repeat, Release Intertwine and Release Opacity
Mask then bring back boxes square to the page. The whole reset stays one
undo step.

Two results change. `changed` counts every object reset, the objects
inside included, so the steps above return 3 where they returned 1. A
group whose own box is square but whose members are turned used to
return 0 with no undo step; it now squares the members in one step.

Docs: params text, docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:22:01 +07:00
dotCooCoo b093e53e77 Smart Guides: Snap to Last Location puts a moved selection back where it began
Preferences > Smart Guides gets Snap to Last Location (`snapToLastLocation`,
off by default). With it and Smart Guides on, a selection moved by dragging
with the Selection, Group Selection or Direct Selection tool, Shift too, goes
back to where the move began while the move is within Snapping Tolerance,
labeled "last location". An Alt copy is not pulled back onto its original.

Source: https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/smart-guides-options.html
The distance, the label and the copy rule are VectorCraft's own.
2026-11-08 12:21:36 +07:00
dotCooCoo fff2ecd76b Symbols: Reset Transformation tests cover a mixed selection and negative stored sizes
An instance in perspective selected with a turned instance stays as it
was while the other is reset and returned in one undo step. Stored
natural sizes that are negative on either side give the art its own
size, unmirrored, as a zero size does.
2026-11-08 12:21:14 +07:00
dotCooCoo 283d46c1cc Symbols: Reset Transformation's enabled check does not measure the symbol's art 2026-11-08 12:21:14 +07:00
dotCooCoo 1cd607f596 Symbols: Reset Transformation keeps the art's size when the stored size is not positive 2026-11-08 12:21:14 +07:00
dotCooCoo d687456978 Symbols: Reset Transformation returns an instance to its symbol's scale, rotation and shear
The Symbols panel menu's Reset Transformation and the Control bar's Reset
(shown while symbol instances are selected) give the selected instances
their symbol's own scale again, without rotation, shear or reflection, as
one undo step (symbol.instance.resetTransform). Each instance keeps the
center of its art's bounding box in place. Instances in perspective and
instances already at their symbol's scale are left as they are, no undo
step is added when nothing changes, and both controls are enabled only
while a selected instance would change.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:21:12 +07:00
dotCooCoo 1d32aff63f Tests: the CSS, glow and .ai Zstandard guards count work instead of timing it
Three tests failed on a loaded machine because they asserted elapsed
time. Each now counts the work that the slow behavior it guards
against would repeat, with a counter compiled only into tests:

- The CSS cascade test counts selector tests, at most two per element
  (testing every rule against every element tests N x N). It styles
  5,000 elements, so the slow behavior fails on the count rather than
  on the hang guard, which now waits 300 s.
- The stacked-glow test counts the silhouettes drawn, 30 for 30 glows
  with and without the shadow cache.
- The Zstandard frames test counts the bytes looked at to find the
  zero padding.

The hang guards for invalid dash patterns, text layout and script
checks of a pipe wait 300 s instead of 20 s.
2026-11-08 12:21:06 +07:00
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 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 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
dotCooCoo aed9898b8d .ai save: the editing copy keeps type as type
The editing copy of a PDF-compatible .ai wrote every text object as its
outlines. Point type, area type and type on a path are now written as
text: a text object in the layers that names its story, and the text
document after the document data (%AI11_BeginTextDocument) holding the
stories.

- Each story holds its characters (a paragraph ends in \r, a forced line
  break is \x03), its character runs (font, size, leading, tracking,
  baseline shift, horizontal and vertical scale, solid fill and stroke
  with its weight, capitals, Tate-chu-yoko) and its paragraph runs
  (alignment, indents, space before and after, hyphenation, right-to-left
  direction).
- Point type keeps its point and, when it is turned, its matrix. Type
  scaled by one factor, such as all type on a large canvas, has its scale
  in its sizes, frame and path. Area type keeps its frame, rows,
  columns, gutters and first baseline, type on a path its path and where
  it starts and ends, and vertical type its orientation. A threaded story
  is one story through its frames in order. Area type and type on a path
  carry their frame's path in the layers as well.
- Type the text document can't hold is written as its outlines, with a
  warning: type in opacity masks, pattern tiles and art cut to a clipping
  object, mirrored type, turned or unevenly scaled area type and type on
  a path, frames of several parts, area type that flows around a text wrap,
  threads whose frames differ in orientation or include a frame written
  as outlines, type and stories past the sizes the reader reads, and type
  with inline graphics, ruby, gradient or pattern paint or an appearance
  or opacity mask of its own. Other character and paragraph attributes are left out, with
  a warning.

Tracking is written as a whole number of thousandths of an em, and
solid paints as simple paints; tracking that isn't a whole number is
rounded, with the warning.

Each construct is written in the form VectorCraft's .ai reader reads
(crates/eps/src/import/ate.rs at 36ef34dfdd: Texts::read :407, story
:501, frame :456, para :562, style :588, paint :622, AreaFrame::read
:284; the text object in import/ai/read/art.rs:15 and its path in
import/ai/frames.rs:26), and the tests read the written data back
through that reader. The scripting
reference documents the same attributes on the reading side (text frame
kinds, columnCount, columnGutter, rowCount, startTValue, endTValue,
orientation, and the character attributes):
https://web.archive.org/web/20160410101607/https://wwwimages.adobe.com/content/dam/Adobe/en/devnet/illustrator/sdk/CC2014/Illustrator%20Scripting%20Reference%20-%20JavaScript.pdf

The reader now closes a text object's frame path painted with a closing
operator (n, f, s, b, h), so area type's frame comes back closed; places
point type whose story keeps no composed lines at its frame's point;
keeps a story's last empty paragraph (only the \r that ends the story is
dropped); gives type made from the text document its text object's lock,
as it gives it its visibility; and gives type its text object's knockout
shape flag, as it gives it its opacity and blend mode.

Tests: the round trip of every kind of type and its state, a second
write of what is read back giving the same bytes, the text document and
stubs, scaled point type, a story split between threaded frames, the
fallbacks and the warnings, and the engine's editing copy with every save
option; a fuzz case for damaged text documents and stubs.

Docs: docs/file-format-parity.md.
2026-11-07 05:15:15 +07:00
dotCooCoo 18015f3631 Type: a selected story stays selected when its text leaves the edited frame
When a frame threaded in before the edited frame takes all of the story's
text, the edited frame is left with no text while the story stays
selected. The other frames' text is now highlighted, Delete and Backspace
replace the story, an Alt+arrow step changes the tracking of every frame,
and Copy and Cut take the story. A story whose frames hold no text has
nothing to copy, and Delete in it records no undo step.
2026-11-07 05:15:12 +07:00
dotCooCoo 22bb2ac8a6 Type: a selected story's highlight and typing follow its current thread
With a whole story selected, the highlight and typing read the frames saved
at Select All, while a pick, a fill and the other story-wide edits read the
document's thread. A frame added by text.thread.add took a pick without being
highlighted, and typing left its text in place.

Both now read the thread through Document::story_of. A frame threaded in
during the selection is highlighted and takes the edit, and a frame that
leaves the thread is not highlighted and keeps its text.
2026-11-07 05:15:11 +07:00
dotCooCoo c969b7857d Type: an Eyedropper pick on a selected story styles every frame
appearance.copyFrom and a fill or stroke change on a story selected by
Select All no longer skip locked frames, hidden frames and frames on a
locked artboard. They apply to every frame of the story, as the Character
panel's story edits do. appearance.copyFrom still leaves out a frame that
is its source.
2026-11-07 05:15:11 +07:00
dotCooCoo f534b0061a Type: switching to the Eyedropper keeps a whole-story selection
With a threaded story selected by Select All, switching to the Eyedropper kept
only the edited frame's selection, so a pick styled one frame. The selection now
carries the story's frames, a pick or Shift-click applies to the text of every
frame as one undo step, and switching back to a Type tool restores the story
selection. A selection inside one frame and unthreaded type behave as before.

A pick and a fill or stroke change on a selected story leave locked and hidden
frames and frames on a locked artboard as they are. appearance.copyFrom leaves
out a frame that is its source.

Typing over a selected story no longer removes the text of a frame whose thread
was removed after Select All.
2026-11-07 05:15:10 +07:00
Sven Lafebre 1b62f86b8c Fix #5714: doc comments on the width profile tests 2026-11-07 05:15:10 +07:00
Sven Lafebre 9da56717a0 Fix #5714: widths run smoothly between width points
The width between two width points was a straight line, so a variable-width
stroke turned a corner at every width point.

- Each side of a width profile now follows a monotone cubic (Fritsch-Carlson)
  through its points: no kink at a point, flat at a point wider or narrower
  than both neighbours, never past either point of a span. Points on one line
  stay on it and two-point tapers are unchanged; stacked points still step.
- widest() counts each span's wider left and wider right, since the two sides
  needn't peak together.
- The outliner samples straight runs between width points closely enough to
  follow the curve to the drawing tolerance (at most 64 samples per span and
  4,096 per piece); straight spans get none.
- Tests that expected straight-line widths now expect the curve. The stroke
  gradient coverage test uses Teardrop instead of Wave until #5964 is fixed.
2026-11-07 05:15:09 +07:00
dotCooCoo 0c570dee87 .ai import: paths with an Art brush open with the brush on their stroke
A path with an Art brush comes in as the path, with the brush on its
stroke at its weight, instead of as its drawn look, when the brush
stretches art of solid-filled paths along the path, unflipped, at 100% in
Tints. The art runs on the side of the path the file draws it.

The drawn look stays for other colorizations and widths; Proportional,
between-guides and flipped brushes; type; a path with settings of its own,
a variable width, effects, a swatch stroke or an entry that isn't read;
and art with strokes, gradients, patterns, masks or effects, or no length.

A brushed stroke, Calligraphic ones too, takes the brush the Brushes panel
keeps under its name, and an image with a brush keeps its drawn look.

In every document, Tints and Tints and Shades take art in black ink alone
by its ink amount, and a process or gray stroke color's tints and shades
stay in that color's model.

Place and Expand bring the brushes the art uses, renaming on conflict.

Part of #637.
2026-11-07 05:15:09 +07:00
dotCooCoo ab3ac3d429 .ai import: envelopes map from their stroked box when the file saves its size
When Distort Appearance is on and the size a `.ai` saves for an
envelope's contents (`AIDeformArtWidthKey`, `AIDeformArtHeightKey`)
matches the contents' bounds with strokes, the reader now maps the
contents from that stroked box. It used to map them from the geometric
bounds, half a stroke inside that box, so an envelope of stroked art
could differ from its drawn art and keep the drawn art with a note.

Envelope Options hold the box's margins as `boundsMargin`, shares of the
geometric bounds at the top, bottom, left and right. Native files write
them only when they are not zero, and `object.envelope.info` reports
them then. Leaving Edit Contents measures them again when the strokes
reach differently than when Edit Contents began, and the node holds
what they measured then until it leaves. New envelopes have no
margins, so their results do not change. Contents with no width or no
height, such as one straight line, get no margins.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-07 05:15:08 +07:00
FractalEncrypt 1c271c776c Shape tools: review follow-ups from CodeRabbit
- The 45° move assertion compares dx and dy with a small tolerance instead of
  exact equality (the matrix's floats are rounded), keeping the non-zero check
  and the message.
- The engine test helpers say what they do, so the docstring coverage of the
  functions this pull request touches is complete.
2026-11-07 05:15:05 +07:00
FractalEncrypt 6d3c0fef99 Shape tools: guides take the centre point's pointer too, and more regressions
The Selection tool's cursor now checks the ruler guides before a selected
object's centre point, as a press does. Tests: a ruler guide under the centre
point keeps the pointer and drags; on a box too thin for its centre to clear the
handles the handles take both; a shape-tool move from the centre snaps and shows
the hollow arrow; and Alt pressed mid-drag copies the shape through the engine.
2026-11-07 05:15:04 +07:00
FractalEncrypt c36971afe9 Shape tools: the centre point's cursor comes after the box's handles
The Selection tool's cursor now checks the bounding-box handles before a
selected object's centre point, as a press does (on a thin box the centre lies
within a handle's reach). The shape tools' tests cover a Live Corners widget
that lands on another selected shape's centre (the widget rounds), Alt pressed
mid-drag (the preview copies and the copy cursor shows), and the engine tests
check the copy's place and both gestures' Undo and Redo. The cursor catalog says
which arrow a drag of the selection shows.
2026-11-07 05:15:03 +07:00