Commit Graph
8523 Commits
Author SHA1 Message Date
BFlat 88496dcefd Merge pull request #5783 from dotCooCoo/fix/ai-os22-g8
.ai import: area type with linked areas opens as threaded frames
2026-11-05 17:31:42 +07:00
BFlat 250098e19f Merge pull request #5782 from dotCooCoo/fix/stale-pointer-requests-frame-limit
MCP: given-up stale pointer requests do not use up a frame's request limit
2026-11-05 17:31:27 +07:00
BFlat 6f0e15452c Merge pull request #5781 from storytold/perf/1280-bounds-nan-close
#1280: PathData::bounds closes a NaN anchor as kurbo does; the perf row names what it times
2026-11-05 17:31:11 +07:00
dotCooCoo 48f0347351 .ai import: area type with linked areas opens as threaded frames
In the legacy format, area type whose story flows through several linked
areas has one text path (`Tp` ... `TP`) for each area, each with its own
matrix and frame (version 7 specification, sections 15 and 15.5.3). The
reader kept only the first frame, placed it with the text object's last
valid matrix, and put the whole story in that one frame.

Each linked area with a frame now opens as its own area type object,
placed with its own matrix, and the frames are threaded in file order.
The first frame holds the story: the characters after every `TP`, and
the overflow text. When the file opens, the story is laid out again
through the thread (section 15.1 lists the flow from area to area as
final-form information). A linked area without a frame is left out.
Area type with one area opens as before.

The dictionary that follows a text object (its opacity mask, text wrap,
name and other keys) went to its one object. Its opacity mask and text
wrap now go on every frame, and its other keys on the first frame.

Point type is read with its first text path's matrix, and type on a
path with the matrix and start of the first text path that has a path.
Area type with no frame at all opens as point type with its first text
path's matrix. Before, these used the last valid matrix and, for type
on a path, the last start.

One text object reads at most 4,096 text paths. The number of frames
times the story's character count is kept to 4,194,304, so a longer
story keeps fewer frames. Frames past either limit are left out, with a
warning.
2026-11-05 17:17:36 +07:00
dotCooCoo 7e97255334 MCP: given-up stale pointer requests do not use up a frame's request limit
A ui.pointer request made before synthetic input was dropped counted
toward the per-frame request limit even when its caller had already
given it up and no reply was sent. A backlog of such requests could hold
live requests back for a frame. It now counts only when the app claims
it and replies with the error, as the other given-up requests are
skipped.
2026-11-05 17:17:36 +07:00
FredAppsandClaude Opus 5.5 c1f7e4ed6d #1280: PathData::bounds closes a NaN anchor as kurbo does; the perf row names what it times
Follow-up to CodeRabbit's review of #5778.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:17:36 +07:00
BFlat b13335d188 Merge pull request #5780 from dotCooCoo/fix/font-wheel-raised-layer
Keep font wheel input with the top native panel layer
2026-11-05 17:17:22 +07:00
BFlat 8bd84206fa Merge pull request #5779 from dotCooCoo/fix/ai-os22-g7
.ai import: version 7 linked images open as linked images with a placeholder
2026-11-05 17:17:07 +07:00
dotCooCoo 89875f1a6b Keep delayed font wheel turns with the current field 2026-11-05 17:02:57 +07:00
dotCooCoo c5269bd096 .ai import: version 7 linked images open as linked images with a placeholder
A legacy .ai in the version 7 form names a linked image's file with
`(path) modified XG` and draws the image with `XF`, which takes the
operands `XI` takes and has no samples after it (Adobe Illustrator File
Format Specification 7.0, sections 8.2 and 8.3). The reader ignored both
operators, so the image was dropped without a warning.

- `XF` after `XG` becomes a linked image of that file, its pixels placed
  as an `XI` image of the same operands is placed. The file keeps no
  pixels of it, so a placeholder stands in until the file is relinked in
  the Links panel, with a note in the open result. No pixels are read,
  so the image is not held to the pixel limit of `XI`'s samples.
- An `XF` without a file name before it, or with operands `XI` couldn't
  read (no matrix, too few numbers, no pixels, or samples of other than
  1 or 8 bits), is left out with the existing "an image VectorCraft
  can't read was left out" warning. An `XG` applies only to the image
  after it in the same raster object; `() 1 XG` before an embedded `XI`
  keeps that image embedded.
- A PostScript page draws nothing for `XF`, so there the placeholder's
  area is left out of the comparison of the layers with the page, and
  the file keeps its layers. A PDF part draws the image's pixels, so a
  file with one is compared with it there as before, and its layers are
  not used without the PDF part.
- Linked images of different files get image keys of their own even
  when they show the same pixels (the placeholder, or two identical
  placed-file previews), so updating or exporting one file's image
  reads that file only.
- `vectorcraft_doc::links::missing_placeholder` draws the placeholder: a
  light tile, crossed out, as SVG import shows for a missing file.

Tests: reader tests for the placement, the image keys and the cases left
out, an end-to-end test of a legacy file with a linked image, a test of
a file with a PDF part, and a fuzz case for damaged files with `XG` and
`XF`. The new note has a row in each catalog that translates the
placed-file note.
2026-11-05 17:02:57 +07:00
dotCooCoo d0069d7134 Keep font wheel events with their overlay target 2026-11-05 17:02:57 +07:00
dotCooCoo 1d00259168 Preserve font wheel turn order across pointer leaves 2026-11-05 17:02:57 +07:00
dotCooCoo 98f0c69a1c Restore floating viewport pointer after wheel replay 2026-11-05 17:02:57 +07:00
dotCooCoo 38354d7864 Keep floating font wheel input with the visible layer 2026-11-05 17:02:57 +07:00
dotCooCoo a5ac92d056 Defer floating font wheel input until layer order settles 2026-11-05 17:02:57 +07:00
BFlat 5ca8ef49e2 Merge pull request #5778 from storytold/perf/1280-bounds-without-bezpath
#1280: perf --deep measures pointing; PathData::bounds builds no path
2026-11-05 17:02:47 +07:00
FredAppsandClaude Opus 5.5 0a8566e1b0 perf: the import comes first
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:49:06 +07:00
a33d83d84f #1280: perf --deep measures pointing; PathData::bounds builds no path
First slice of https://github.com/storytold/vectorcraft/pull/1281, split
as the review asked. `--deep` gains rows for hovering, a hit test that
misses, marquee, smart guide targets and shift-click on a large document,
so the later slices have a baseline.

PathData::bounds built a kurbo path on every call. It now measures the
segments in place, with the same guards, and a proptest compares the
result bit for bit with what the path gave.

Co-Authored-By: arhammusheer <arhammusheer@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:49:06 +07:00
BFlat be2228c277 Merge pull request #5777 from dotCooCoo/fix/large-canvas-eps-type
.ai/EPS import: type that names no style takes the normal styles
2026-11-05 16:48:56 +07:00
dotCooCoo 6dd86b337a .ai/EPS import: type that names no style takes the normal styles
Type in the text document of a .ai or EPS file's editing data that names
no character or paragraph style, a story without style runs included, is
set in [Normal Character Style] and [Normal Paragraph Style]. The reader
took what such type leaves out from the text document's base attributes
instead, or built-in defaults for a story without paragraph runs, and it
did not read paragraph styles.

A Large Canvas file (1:10) holds the normal styles' lengths at the art's
scale (1.2 for 12 pt) and the base attributes' at the page's (12). The
reader scaled the base attributes' 12 by 10, so the type came in at
120 pt. In an EPS the page comparison then rejected the layers, and the
file opened as its printed page without its linked images.

In any file whose normal styles differ from the base attributes, such
type now takes what the normal styles set, such as font, size, colors,
stroke, alignment, indents, space before and after, and hyphenation.
2026-11-05 16:37:13 +07:00
BFlat 413446fdc1 Merge pull request #5775 from dotCooCoo/fix/ai-object-tags
.ai import: object tags open as each object's URL and data
2026-11-05 16:17:01 +07:00
BFlat 3ac2bd6afc Merge pull request #5774 from ruchirguitar/fix/5663-batch-views
Document tabs: views stay with their documents when one batch closes and moves documents (#5663)
2026-11-05 16:16:41 +07:00
dotCooCoo a90ffe2a5f .ai import: object tags open as each object's URL and data
`.ai` and EPS editing-data import now reads object tags
(`/identifier (string) XT`) and applies each one to the object before it,
whatever kind of object it is. The `AdobeURL` tag sets the object's URL.
Every other tag is added to the object's data in order, up to 64 per
object. A tag with no object right before it, with an empty identifier,
or with an identifier or string longer than 64 KiB is ignored, and so
are tags in a pattern's or brush's art. A clipping path keeps its own
tags; the copy that paints its fill gets none.

Before, a tag on art that shows was an operator the reader did not know,
so it stopped the editing data from being read, and brush art holding a
tag was not read, so the brushes using it were left out.

An image's tags follow the `XH` that ends its data, and the state written
after a group or a gradient mesh is still theirs when their tags come
first. When a drawn look stands for an object whose style is not found,
the look keeps the object's note, URL and data, and a live blend keeps
those of its drawn steps.
2026-11-05 16:04:32 +07:00
ruchirguitarandClaude Opus 5.5 a89b44ee2e Document tabs: views stay with their documents when one batch closes and moves documents (#5663)
`sync_views` matched views to documents by uid only while the number of
documents was unchanged. A `command.batch` (or a recording) that closed a
document fell back to matching by position, so the documents after the closed
one took their neighbours' views, and a move later in the same batch was not
applied to the views. The views are now matched by uid whenever the documents
change: a closed document's view is dropped and a new document gets its saved
view. Closing from the UI no longer removes the view by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:04:29 +07:00
BFlat 7bfcc005f4 Merge pull request #5772 from dotCooCoo/fix/discarded-batch-cancels-pointer
MCP: pointer_gesture fails when the input sent before it is dropped
2026-11-05 15:50:40 +07:00
BFlat 6ed686bae2 Merge pull request #5771 from dotCooCoo/feat/ai-editing-data-s3
.ai save: the editing copy keeps gradients and patterns
2026-11-05 15:50:24 +07:00
dotCooCoo cefb44c033 MCP: pointer_gesture fails when the input sent before it is dropped
Squashed from #5772 onto main.
2026-11-05 15:38:38 +07:00
dotCooCoo 1f68248bc5 .ai save: the editing copy keeps gradients and patterns
The editing copy for other apps now writes linear and radial gradients and
grid patterns as themselves instead of as average colors and cut tiles.

- Gradients the art uses are defined in the setup (`n Bn`, then
  `%AI5_BeginGradient` … `Bd`, the stops as `Bs` with their opacity and
  midpoints, `BD`), and gradient swatches nothing uses in a non-printing
  block. Stops keep gray, CMYK, RGB and spot colors at a tint. A
  gradient that isn't a swatch gets a name that no swatch or pattern of
  the document has.
- Each fill or stroke with a gradient is an instance around its painting
  operator (`Bb`, `Bh`, `Bg`, `Xm`, `Bm`, `Bc`, `BB`), on a compound
  path's last subpath and on clipping paths too. `Bg` places the gradient
  relative to the path: a linear one by its angle and its start and length
  as fractions of the path's extent along it, a radial one by the matrix
  that takes the unit circle onto its extent ellipse, its center as
  fractions of the path's bounds in that circle's space and its radius as
  a multiple of the one the center's distances to the right side and to
  the bottom give. In a pattern's tile the placement is mirrored top to
  bottom.
- Patterns whose tiles make a grid and hold their art are defined with
  `%AI3_BeginPattern` … `E`, the tile's box first, and fills and strokes
  paint with `p` and `P`; gradient and pattern swatches are in the palette.
- Freeform gradients, gradients along or across a stroke and gradients
  without stops, with more than 256 stops or without extent are written
  as their average color, and other patterns as their tiles cut to the
  shape, each with a warning. Gradients that dither or mix perceptually
  are written as classic ones, stops linked to global process colors as
  their colors, and pattern swatches without the placement that moves,
  turns or scales their pattern, with warnings.
- Reading: an unpainted box that a pattern's definition marks as its
  tile's box (`AIPattern_Editor_Backing_Tile_Rect`) is no longer read as
  art, and a linear gradient whose stops all lie at one place keeps each
  stop on its side.

The definitions, instances and patterns follow sections 3.4, 3.5 and 14
of the version 7 specification
(http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf). The stop
opacity, `Bg`'s placement and the marked tile box follow the newer form
that current .ai files carry, which that specification predates.

Each of these is written in the form VectorCraft's .ai reader reads, and
the tests read the written data back through that reader.

Docs: docs/file-format-parity.md.
2026-11-05 15:38:38 +07:00
BFlat e5f8aff49e Merge pull request #5770 from dotCooCoo/fix/ai-editing-guides
.ai save: guides in the editing copy end with the guide operator only
2026-11-05 15:38:20 +07:00
dotCooCoo 31250ad315 .ai save: guides in the editing copy end with the guide operator only
The editing copy of the art now writes a guide as its path followed by
`(N) *`, or `(n) *` when the path is closed, without the `(N) *` before
the path. The legacy version 7 .ai keeps both, as section 6 of the
version 7 specification shows them
(http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf).
2026-11-05 15:26:36 +07:00
BFlat 8af25c206b Merge pull request #5768 from dotCooCoo/fix/withdrawn-pointer-skips-wait
MCP: a given-up pointer request no longer holds back later requests
2026-11-05 15:25:48 +07:00
dotCooCoo 3a997ec516 MCP: a given-up pointer request no longer holds back later requests
A ui.pointer request waits, ahead of the requests after it, until the
synthetic input sent before it has been applied. When its caller gave
it up during that wait, the request still waited there, and the
requests behind it waited for up to 1024 queued input events and could
time out.

A given-up pointer request is now dropped at once instead of waiting.
2026-11-05 15:16:46 +07:00
BFlat 04919cae3c Merge pull request #5766 from dotCooCoo/fix/story-selection-cleanup-commands
Type: text cleanup and Convert to Text act on every frame of a selected story
2026-11-05 14:46:01 +07:00
dotCooCoo bf7ca194bb Type: Convert to Text leaves a paragraph that starts in a locked frame of the selected story
While Select All holds a threaded story, a paragraph whose text starts in one of the
story's locked or hidden frames is no longer converted, and the empty parts of a converted
paragraph in such a frame are no longer restyled. Before, a locked frame whose first
paragraph took in the previous frame's empty last part was converted, and so was a
paragraph that ran on from a locked first frame into an editable one.
2026-11-05 14:34:27 +07:00
dotCooCoo 2c1f3d7605 Type: text cleanup and Convert to Text act on every frame of a selected story
After Select All in a frame of threaded text, Remove Line Breaks and
Remove Paragraph Breaks changed only the edited frame, and Convert to
Text over MCP converted only the selected frame. Both now act on the
text of every frame of the story as one undo step.

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

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

Fixes #5750
2026-11-05 14:26:32 +07:00
BFlat 357d48de6d Merge pull request #5763 from dotCooCoo/fix/svg-export-long-path
SVG export: numbers are written from integers, not through the exact float formatter
2026-11-05 14:25:59 +07:00
BFlat 8e25b828c2 Merge pull request #5762 from dotCooCoo/fix/pointer-backquote-boolean
MCP: pointer_gesture rejects a backquote that is not a boolean
2026-11-05 14:25:47 +07:00
BFlat 85aba7ac5b Merge pull request #5761 from dotCooCoo/fix/coincident-lines-under-outline
Shape Builder: a merge or erase takes the stretch of every coincident line
2026-11-05 14:25:31 +07:00
BFlat 39defe8b7e Merge pull request #5744 from dotCooCoo/fix/render-path-splice-on-edit
Render: adding or deleting an anchor of a long path splices the kept path
2026-11-05 14:25:15 +07:00
BFlat 531c6c36c3 Merge pull request #5759 from ruchirguitar/feat/5526-select-by-link-status
Fix #5526: select images by link status
2026-11-05 14:24:57 +07:00
dotCooCoo 2038dc7849 SVG export: numbers are written from integers, not through the exact float formatter
push_num formatted every number with write!("{:.*}"), which runs the
exact decimal float formatter, then scanned and trimmed the text. That
formatter was about 95% of the time to export a path of 200,000
anchors.

At 9 decimals or fewer, push_num now scales the value by 10^decimals
and writes the digits of the rounded integer when that rounding is
certain: the scaled value is below 2^53 and further from a half than
the scaling's rounding error. Ties and larger values still go through
the formatter, so the output is the same byte for byte.

Exporting one path of 200,000 anchors takes a median of 43.4 ms
instead of 167.0 ms, and 20,000 rectangles 81.4 ms instead of 141.4 ms
(Windows 11, release build).
2026-11-05 14:12:19 +07:00
dotCooCoo e9be911c9b MCP: pointer_gesture rejects a backquote that is not a boolean
A `backquote` given as a string or a number, on the request or on an
event, was read as absent, so `"true"` moved the object's geometry
instead of its pattern. `pointer_gesture` and `ui.pointer`, headless
and desktop, now answer with an error before any event runs.
2026-11-05 14:12:18 +07:00
dotCooCoo 19d9843971 Shape Builder: a merge or erase takes the stretch of every coincident line
Where two open paths lie along the same stretch, the planar map gave
that stretch to one of them only, so a merge or an erase across it left
the other path in place there. Every other open path along an edge now
has its own piece in `lines_under`, under a closed path's outline or
along a line in front. `lines`, `edges` and Live Paint's edges stay one
per stretch, and an Alt-click there still takes the piece in front.
2026-11-05 14:12:18 +07:00
dotCooCoo 924ec6f0d6 Tests: random edits of long paths keep what making the path again makes
Two seeded tests run 300 random edits each on a long path and a long
compound path: anchors added and deleted at the start, middle and end of a
subpath and where a run of segments ends, subpaths closed and opened, anchors
and handles moved, and in the compound path members added and removed and
subpaths added to a member. The subpaths range from 2 anchors to 3,000, open
and closed. After each edit the kept path's elements, subpaths, bounds and
miter tips equal those of a new `KeptPath`, its runs hold each subpath's
segments, and each run's box and tips equal those measured again.
2026-11-05 14:12:17 +07:00
dotCooCoo e3d30a7047 Render: kept-path splices check their arithmetic and make the subpath again on a mismatch
The index arithmetic in `Sub::spliced`, `Sub::resize` and `KeptPath::update`
uses `checked_sub` and `checked_add` instead of plain subtraction. When the
segments or runs kept for a subpath do not fit its anchors before the edit,
`resize` returns `None` without changing anything, and `update` writes the
subpath's elements again from the new anchors and measures it whole, as it
does for a subpath of fewer than two anchors. A compound path whose kept
members do not pair with the new ones returns false, and the renderer makes
the path again whole.
2026-11-05 14:12:17 +07:00
dotCooCoo 50128e6025 Render: adding or deleting an anchor of a long path splices the kept path
When anchors of a kept path (a path or compound path of 1,024 anchors or
more) are added or deleted, or a subpath is closed or opened, the next
frame writes again only the path elements from the first anchor that
changed to the last, and the closing segment and ClosePath when an end
anchor changed or the subpath was closed or opened. The elements after
the change move with it. Before, such an edit made the whole path again
and measured it whole.

- The runs of segments now carry their own ends. The runs that held the
  segments written become one run, split into runs of 128 when it grows
  past 256, and only those runs are measured again, with the miter tips
  of the run of the segment with length before each span. When a
  subpath is closed or opened and the stroke reaches farther from one
  than from the other, all of that subpath's tips are measured again.
- On a subpath that is open before and after the edit, the handle before
  the first anchor and the one after the last are not compared, so
  Delete Anchor Point, which clears them, writes again only the segments
  around the anchor deleted.
- A subpath that has fewer than two anchors before or after the edit is
  written again whole. A compound path member that gains or loses
  subpaths is made again. When members are added or removed, those
  members and every member between the first and the last of them are
  made again, and the members before and after them stay.
- An edit that adds or deletes anchors in, closes or opens more than 16
  subpaths makes the whole path again, as before.
- An edit that leaves every anchor as it was, such as a new stroke,
  leaves the kept path as it is.

The pixels and the cull bounds are the same as with a path made again.

Docs: docs/target-app-parity.md.
2026-11-05 14:12:17 +07:00
ruchirguitarandClaude Opus 5.5 96505d3a77 Fix #5526: document the functions the link-status selection touches
Doc comments for the Select add-on commands, the Select by Link Status
dialog body, the Links panel menu and the test helpers (review: docstring
coverage).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:12:11 +07:00
ruchirguitarandClaude Opus 5.5 7dc66d660a Fix #5526: select images by link status
Selection Tools add-on: addon.select.byLinkStatus selects the image
objects and placed documents whose link is linked, embedded, missing or
modified (one status or several), as links.list reports it. Optional ids
limit the search, add adds to the selection, and zoom runs the existing
Zoom to Selection in the desktop app. Objects that are hidden, locked, on
a template layer or outside the isolated group are left out and returned
with the reason. Only the selection changes.

Select Linked / Embedded / Missing / Modified are in Object > Plug-ins >
Selection Tools and the Links panel menu. Each is off when no image has
that status, and Shift adds to the selection. Select by Link Status...
lists the images with name, status and format, with a name search, a
status filter, checkboxes with All / None and Zoom to Selection. The
status bar says what was selected and how many were left out, with
plural rows in every catalog.

Tests: engine (statuses, ids, add, zoom, skip reasons, bad params),
command sweep, and UI (message, list filtering, menu items, Links panel
menu, headless dialog frame). Docs: mcp, target-app-parity, ui-parity,
roadmap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:12:10 +07:00