Commit Graph
5271 Commits
Author SHA1 Message Date
dotCooCoo 2faec2fa11 .ai import: Skew, Stair Step and Gravity measure the path's turn square to it
The spacing fitted from a type-on-a-path layout measured each turn against
the glyph's own foot. Skew's feet stay upright, so on a tilted or leftward
path the turn picked up a false angle and the fitted spacing was wrong (a
straight path at 30 degrees gave -9.7 instead of 0). Effects other than
Rainbow and 3D Ribbon now measure the turn against the baseline turned a
right angle.

The file-format doc states that a frame whose options set key 6 to true
keeps the default effect, alignment and Flip.
2026-11-08 11:22:31 +07:00
dotCooCoo 53f6aafe87 .ai import: type on a path keeps its effect, alignment and flip
Type on a path from a file's text document opened as Rainbow on its
baseline, not flipped, whatever Type on a Path Options the frame kept.
Each frame now keeps its effect (Rainbow, Skew, 3D Ribbon, Stair Step,
Gravity), Align to Path (Ascender, Descender, Center, Baseline) and Flip.
A code or flag that can't be read leaves that option at its default.

Flipped type opens with the frame's path reversed once, after a copied
object takes its own art path, and an open path's end bracket stays so
the type starts where the file's range ends.

Spacing is still fitted from the file's layout. The fit now measures along
the path when Align to Path moves the baselines off it, and it follows the
glyphs' feet under 3D Ribbon, whose baselines stay level.
2026-11-08 11:22:30 +07:00
dotCooCoo bc19dd64db Simplify: Convert to Straight Lines keeps the tip of a path that turns back past its end
`rdp` measured each sample to the infinite line through the ends of the
line that would replace it. A sample beyond either end could be far from
that line segment and still be dropped: a line to (100, 0), a 0.2 pt
U-turn and a line back to (50, 0.2) became one line at a tolerance of
1 pt, leaving the tip 50 pt away. `rdp` now measures to the segment, so a
sample beyond an end is measured to that end, and a zero-length chord
measures to its point as before.

The test `a_path_that_turns_back_past_its_end_converted_to_straight_lines_keeps_its_tip`
covers that path.
2026-11-08 11:22:30 +07:00
dotCooCoo 489552c14b Simplify: the slider's Escape test previews level 0, which changes the path
The test's path zigzags by 0.1 pt. At level 100 the tolerance is 1/32 pt,
and a fit now stays within it, so the preview at that level leaves the
path as it was. Level 0's 32 pt tolerance takes the zigzag out.
2026-11-08 11:22:30 +07:00
dotCooCoo 4a7d732197 Simplify: smoothing a densely sampled path takes time in proportion to its samples
Object > Path > Smooth weighed every sample within three spreads of each
point, so the work grew with the square of the anchor count on traced
paths: 360 steps per sample for 1,000 anchors and 2,949 for 16,000.
Consecutive samples within 1/16 of a spread of each other are now
weighed together as their count at their mean place, found through a
tree of sums over the run's samples. A traced outline takes 203 to 286
steps per sample from 1,000 to 64,000 anchors, and every smoothed point
stays within a thousandth of a spread of weighing the samples one by
one (300 random runs).
2026-11-08 11:22:29 +07:00
dotCooCoo 199c4c3467 Simplify: fitted curves stay within the tolerance of the path, and serpentines simplify faster
The curve fitter split a piece over the tolerance at its worst point and
accepted any piece at depth 41 whatever its error. A worst point near the
end of a piece peels off a few points, so a long path refitted nearly all
of its points at every level. A serpentine of 200,000 anchors kept pieces
with a fitted error of up to about 1,598 pt at a tolerance of 1 pt.

Simplify also took at most 256 samples per segment, so the lines between
the samples on a long arc could be farther from it than the tolerance.

- `fit_rec` moves a split that is near an end of its piece to two fifths
  of the piece from that end, so the depth is logarithmic, and it keeps no
  piece over the tolerance. A piece of 32 points or more is first fitted
  to about 16 of them, and it is split when that fit is over the tolerance.
- A curve is also rejected when it is over the tolerance from the lines
  between the points it fits, with samples close enough that it stays
  within 1% of the tolerance of the chord between them. A pair of points
  that would need more than 1,024 samples counts as too far.
- `join_pieces` joins neighboring pieces when one curve fits them, up to
  33 pieces per curve.
- `sample_steps` takes enough samples of a curved segment to keep it within
  0.5% of the tolerance of the lines between them. A segment that would
  need more than 65,536 samples stays as it is, and so does a segment
  after a run's first 16,777,216 samples or 2,097,152 result segments. With
  Convert to Straight Lines such a segment becomes evenly spaced lines, up
  to 65,536. A fit holds at most 1,048,576 samples, so a longer run is
  fitted in parts.
- Simplify fits at 98% of the tolerance, since turning a nearly straight
  curve into a line moves it by up to 1.5%.
- Above slider level 0, Object > Path > Smooth fits a whole closed path
  with no corner along one direction across its start, when no segment
  stays as it is and the path is fitted as one part. A direct-selected
  section does not get this. The start can still have a kink: a first or
  last fitted curve that is nearly straight becomes a line, which leaves
  along its chord, and a refit with more anchors than the path is
  discarded, so the path stays as it is.

A closed subpath that refits to one curve is still dropped, as on main, so
it can end up farther than the tolerance from the result. docs/mcp.md
states this.

The anchors of every simplified path can differ from before, and so can
the results of the Pencil, Blob Brush and Smooth, which use the same fit.
2026-11-08 11:22:29 +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 fe9d62e05e .ai import: legacy type keeps its writing direction, Tate-chu-yoko runs and upright Roman characters
The legacy reader skipped `TV`, `Tv`, `Ty` and `TY` of section 15.3.4 of
the version 7 file format specification, so type set with `1 TV` opened
horizontal.

- `TV` sets a text object's writing direction. VectorCraft keeps one
  direction for each text object, the one in force at its first
  character, with a warning when a later character has another.
- `Ty` marks a Kumi block as a Tate-chu-yoko run, which is VectorCraft's
  own mapping, and `TY` does the same in horizontal type. A `TY` block in
  vertical type opens unmarked. Two blocks with nothing between them open
  as one block.
- `Tv` sets Standard Vertical Roman Alignment.

All four are modal, and an operand other than 0 or 1 leaves the value as
it was.

Docs: docs/file-format-parity.md.
2026-11-07 09:17:53 +07:00
dotCooCoo 1c2a8c7f7a Type: Justification after Select All covers a frame whose thread broke
After Select All in threaded text, the Justification dialog kept the edited
frame's selected range when that frame was no longer threaded at OK. A thread
broken after the frame moves the rest of the story into it, so the paragraphs
past the old range kept their values. The dialog now drops the range and
changes every paragraph in that frame.
2026-11-07 09:17:48 +07:00
dotCooCoo 24a42c5bb0 Type: Tabs indent markers apply to every paragraph when threaded text reflows
With mixed indents, the Tabs panel sends one text.setFormat per paragraph,
addressed by frame and byte offset. Threaded text reflows after each step,
so an indent that made earlier paragraphs wrap onto more lines moved text
into the next frame, and the later steps landed on the wrong paragraphs or
on none. The steps now run from the story's last paragraph to its first, so
each step's offset comes before every change already made.
2026-11-07 09:17:48 +07:00
dotCooCoo c592b31a1e Type: Tabs indent markers and Justification follow the whole story
After Select All in threaded text, the Tabs panel's indent markers read
the indents of the edited frame alone. When another frame's paragraphs
had a different left indent, a marker drag changed them by the edited
frame's values, or left them alone. The markers now take every
paragraph of every frame in the story, so each paragraph keeps its own
left indent or first-line position.

The Justification dialog kept the frames of the story from the moment
it opened. OK now acts on the frames in the edited frame's thread when
it is pressed, so a frame that left the thread while the dialog was
open is not changed, and a frame that joined it is.
2026-11-07 09:17:48 +07:00
dotCooCoo 40a46e539d Type: story-wide panel changes follow the story's current thread
After Select All in threaded text, the panels read the frames from the
list saved at Select All. A frame that had left the thread still took
Paragraph, Tabs, Justification and Text Styles changes, a frame added
later did not, and when the first saved frame had left, Character,
OpenType and font changes went to that frame alone.

text_story now reads the frames of the edited frame's thread from the
document. Locked and hidden frames stay in the story.
2026-11-07 09:17:46 +07:00
dotCooCoo 0d91749c74 .ai import: drawn looks in pattern and brush art count in the note
An object read as its drawn look counted in the import's "came in as
their drawn look" note when it was on the page or in a kept symbol, but
not when it was in a pattern's art or a brush's art. A file whose only
such objects were in a pattern or a brush opened without the note.

- A kept pattern's drawn looks count, once for each pattern name: a
  definition that a later one of the same name replaces no longer counts.
- A brush's art counts once a brush that is kept uses it; art no kept
  brush uses adds nothing.
2026-11-07 09:00:23 +07:00
Don Omondi ed00a8870e EPS/.ai editing data: a transparency set inside a clipping group stays after it
Q restores the painting state but not the transparency (Xy): the writer
sets Xy only when the next object's state differs from the last one it
wrote. Restored at Q, type after a clipping group following a Screen glow
drew in Screen and nearly vanished on a light background.
2026-11-07 09:00:20 +07:00
Don Omondi 95ac8d7cc7 EPS/.ai editing data: blends with no spine written come in live
A blend whose spine was never edited has only its keys in the editing
data, no spine part. Its spine is the straight lines between the keys'
centres in the record's order; read as such, the blend comes in live
instead of as its drawn steps.
2026-11-07 09:00:15 +07:00
Don Omondi 2626791d7d EPS/.ai editing data: symbols defined before the art styles take them
Illustrator 24 writes its symbol definitions ahead of %AI9_BeginArtStyles,
so a styled object inside a symbol found no style and came in as its drawn
look. The pre-pass that finds swatch names now also keeps the first
top-level art styles section, which the reader parses before the art.
2026-11-07 08:59:55 +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 20f35e4655 Dialogs: a held Enter or Escape stops at the dialog it opened or closed
Enter and Escape counted their key repeats as presses. Holding Enter with
a selection and a selection tool opened the Move dialog, confirmed it,
and opened it again at the key's repeat rate. After a drag, which gives
Move an offset, each round moved the object again.

The app now records what Enter or Escape went down on: the open dialog,
the command search, Edit Toolbar's drawer or Presentation Mode. When that
changes, the key's presses are removed from the input until it is let go.
The tool keys, the dialog's OK and the dialog's Escape also ignore
repeats, so holding Escape with the place cursor loaded discards one file.
A new press still acts, and Enter held in the Type tool still repeats line
breaks.

This also fixes these related faults:
- Ctrl+Enter or Cmd+Enter opened Move and confirmed it in the same frame.
- Holding the Enter that confirmed a dialog typed line breaks into the
  text being edited.
- A command bound to Enter reopened its dialog after the dialog's OK.
- Holding Enter on a value a field rejected confirmed the dialog.
- A command search that opened while Enter was down ran its first result.
- Holding Escape over a dialog's Tips closed the dialog as well.
- Holding Escape over a dialog went on to close Edit Toolbar's drawer,
  leave isolation mode or run a command bound to Escape.
2026-11-07 08:38:15 +07:00
dotCooCoo 2e8e8a1909 Offset Path: the spurious-contour check tests only the source segments near each contour
The check tested one box per source segment for every candidate hole or
island. It now searches a tree of the segments' bounding boxes, built once
when the first contour is checked. The answer for each contour is the same,
so the output paths do not change. MAX_OFFSET_SEGMENT_PAIRS does not change.
2026-11-07 08:38:12 +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 2facf55c29 Image Trace: the logo fitter keeps no piece over its tolerance
The fitter accepted a piece at depth 14, or with fewer than six points,
whatever its error, so a traced outline could stay farther than the
tolerance from its points. Synthetic 2 px block noise at Paths 100 and
Corners 0 kept a piece 2.7 px off at a tolerance of 0.1.

Such a piece is now accepted only when each point is within the tolerance
of the curve. Otherwise a short run is cut at its worst point, down to
two points, and a run at the depth limit is cut in the middle, which keeps
the recursion shallow. Every other piece is unchanged. Noisy art can get
more anchors.

a_traced_pixel_that_straightens_to_no_area_... now traces at Paths 80. At
Paths 90 that pixel's no-area result came from pieces over the tolerance.
2026-11-07 08:19:44 +07:00
Don Omondi dfa9548938 EPS import: an image too large to read skips its data
An image over the pixel cap was left out without reading its data, so
the program went on to read that data as PostScript and the file failed
to open (Ghostscript's eps2write writes a page with a large inline image
this way). The data is now skipped: the filters that find their own end
(ASCIIHexDecode, ASCII85Decode, SubFileDecode) read the file up to it,
without decoding what they pass.
2026-11-07 08:19:40 +07:00
Don Omondi fdbd116577 EPS import: cvi and cvr read radix numbers
cvi and cvr read a string as the PostScript syntax reads a number
(PLRM 3rd ed., cvi, cvr), so (16#20) cvi is 32. The reader parsed strings
as decimal only. Ghostscript's eps2write prolog undoes a name's #20 escape
that way, so its files with a spot colour named with spaces stopped
reading at that colour.
2026-11-07 08:19:36 +07:00
Don Omondi 8c57a65ee0 EPS import: SubFileDecode reads counts past the allocation cap
The count of a SubFileDecode filter bounds what it passes through, not
what it allocates, but the reader refused counts over 4 MiB as a
rangecheck. Ghostscript's eps2write files read each content stream with
currentfile Length () /SubFileDecode filter, so a page whose stream is
longer than 4 MiB failed to open.
2026-11-07 08:19:33 +07:00
Don Omondi 06a035e86b EPS import: bind leaves the procedures it already bound alone
bind makes each procedure it goes into read-only and ignores read-only
procedures (PLRM 3rd ed., bind). The reader bound them again, so a prolog
that binds a procedure holding an earlier-bound one, after defining a name
such as M as an operator, turned that procedure's own variable M into
setmiterlimit. Ghostscript's eps2write prolog does this, and its files with
sampled functions (transfer, black generation) failed to open.
2026-11-07 08:19:29 +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 f4576de615 Artboards: the panel menu's New Artboard makes the new artboard active
The panel menu's item added the artboard and left the active artboard
where it was, while the panel's button, the Control bar and Properties
made the new one active. All four now call one helper. With the Artboard
tool, the new artboard is then the only one selected.
2026-11-07 08:19:12 +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
bflatastic ccdccb1299 Tests: an image next to type keeps its knockout shape flag 2026-11-07 07:36:26 +07:00
dotCooCoo 0be78766f4 .ai import: a compound of clipping paths keeps their knockout shape flag
Clipping paths in a compound path that holds a drawn look become one
compound path. It took the paths' shared stroke, opacity and blend mode
but dropped the knockout shape flag. It now has the flag when every
clipping path has it.
2026-11-07 05:15:19 +07:00
dotCooCoo a9b49f716b .ai import: images, meshes and blends keep the knockout shape flag
An `Xy` whose shape operand is set now reaches an image (embedded or
linked) and a gradient mesh when it is written before the object, and a
mesh or blend when it is written after the object's end. A live blend
takes the flag from its drawn steps. Before, the reader dropped the flag
on these objects, so an image saved with Opacity & Mask Define Knockout
Shape lost it on reopening, with no warning.

A styled object takes the flag written on its drawn look. For a text
object, the flag goes to the object when the look draws only its type,
and otherwise to the group that holds the look and the type. An opaque
look that draws more than the type and sets the flag now goes into that
group as well.
2026-11-07 05:15:19 +07:00
Kyle Kanouse 86564528d3 SVG: centred and right-aligned type on a path keeps its place
Squashed from #5977 onto main.
2026-11-07 05:15:17 +07:00
ruchirguitar 5bb21cdddf Add hyphenation for eleven Indian languages
Squashed from #5946 onto main.
2026-11-07 05:15:17 +07:00
dotCooCoo 7be6f8731c .ai save: check a frame's segment count before building its path
A frame or path past the segments the reader reads is rejected from its count, before it is copied and converted.
2026-11-07 05:15:17 +07:00
dotCooCoo 9006177033 .ai save: test a thread with a frame in a clipping group
A thread with a frame in a clipping group whose clipping object is a path stays type and comes back as one thread.
2026-11-07 05:15:16 +07:00
dotCooCoo 63dbb0f0dd .ai save: an open frame whose ends meet is written as outlines
Area type whose open frame ends where it starts was written as text and
read back in a closed frame. It is now written as its outlines, alone or
as a frame of a thread.
2026-11-07 05:15:16 +07:00
dotCooCoo 0c5ad8e242 .ai save: objects next to type keep their own transparency
The reader gives an Xy right after a compound path or a text object to
that object. The writer now writes XR before the Xy of type, images and
guides, so type next to type, a compound path before type, image or
guide, and type before an image or guide keep their own opacity, blend
mode and knockout shape flag.

- Type keeps its Isolate Blending and Knockout Group state in a state
  block after its text object, as its outlines did on their group.
- The reader no longer gives a layer the Xy before its last text object.
- The ASCII85 end marker of the text document stays on one line.
- The warning and docs/file-format-parity.md also list area type in an
  open frame, type on an open path whose ends meet, type of only white
  space and threads of type on a path as written as outlines.
- README.md, ROADMAP.md, docs/gaps.md, docs/roadmap.md and
  docs/target-app-parity.md describe the editing copy a saved .ai holds.
- clip_path's doc comment is back on clip_path.
- import_fuzz reaches threaded area type and vertical type.
2026-11-07 05:15:16 +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 7e22d3816a Tests: the pan test also pans with the Selection tool
a_pan_moves_the_marks_without_building_them covered Direct Selection
only. It now runs with both tools.

The module comment and MAX_PAN's comment now say what is kept as
triangles and as document points, and when a pan builds the marks again.
A test label now says points rather than px.

The timings in docs/target-app-parity.md are measured again with 10 fill
colors on the 2,000 circles. The pixel sentence there also names the
rounding errors of kept vertices.
2026-11-07 05:15:15 +07:00
dotCooCoo 6c1f426dd0 Canvas: selection marks are placed by the same sum as the rest of the canvas
Kept squares and handles hold document points, and each frame maps them from the view's center as Xf::to_screen does. A pan's distance comes from the difference of the two view centers.
2026-11-07 05:15:14 +07:00
dotCooCoo dcc1b2158c Tests: canvas frame helper reads the selection's marks as shapes
A frame that repeats a view now draws the marks as one mesh. frame_output drops the kept marks first, so the tests that read outlines and anchors from the frame's shapes keep working.
2026-11-07 05:15:14 +07:00
dotCooCoo 94741cdae1 Canvas: selection subpaths and handles out of view add no triangles to a frame
Each subpath and each direction handle is now tested against the view on its own, and gets its triangles the first time it is in view. A group or compound path with one subpath in view, or a long path zoomed in with a few of its handles in view, draws only those.
2026-11-07 05:15:13 +07:00
dotCooCoo 4a5ff880ff Canvas: selection outlines and anchors are reused between frames
The canvas flattened every selected object's outline and tessellated its
anchors and handles again in each frame. It now keeps them per object as
one mesh, valid for the object's node, layer color and selected anchors.
An idle frame draws the mesh again. A pan moves kept outlines and handles
and places the anchor squares again.

A frame with a new zoom, rotation or display scale draws plain shapes and
keeps nothing, and the next frame at that view builds the marks. Entries
of objects that leave the selection or the document are dropped.
2026-11-07 05:15:12 +07:00
Sven Lafebre a83a0054ea Part of #5964: stroke gradients cover an arrowhead to the end of its reach
With ribs only at the path's corners (an Along gradient), the ribs that reach
across an arrowhead stopped at the last corner inside its reach, so the slice
narrowed from the head's tip towards the next corner and left the corners of
a Triangle head's base outside every slice. Where each arrowhead's reach ends
now gets a rib of its own.
2026-11-07 05:15:12 +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