Commit Graph
9906 Commits
Author SHA1 Message Date
BFlat e8aa3ae34c Merge pull request #7249 from dotCooCoo/fix/cleanup-selection-one-pass
Artwork Cleanup: update the selected objects in one pass after each fix
2026-11-14 03:22:08 +07:00
BFlat 00fba7ae24 Merge pull request #7121 from dotCooCoo/fix/graph-series-paint-other-commands
Graphs: stroke, gradient and recolor commands update a series' stored paint
2026-11-14 03:21:58 +07:00
BFlat cf6c666d25 Merge pull request #6980 from addibble/seam-round-joins
3D effects: every facet's seam has round joins
2026-11-14 03:21:49 +07:00
dotCooCoo 88b587baa6 PDF/SVG export: knockout groups nest no deeper with more elements
A knockout group was written as a chain of soft masks, one inside the
next for each element, so a group of about 200 elements made a PDF that
VectorCraft's own PDF import refused (form nesting past 50) and an SVG
that the web build refused as too deep.

- PDF writes a knockout group as one transparency group with /K true,
  with /I true when the group isolates, and PDF import reads it back as
  a knockout group. An element with several paints is drawn as one
  group inside it, so its own fills and strokes do not knock each other
  out.
- Where an element knocks out less than the area it paints (its opacity
  or mask defines its knockout shape, or it paints translucently), each
  element is drawn through one soft mask of the overlapping elements
  above it. Past 65,536 objects in a group's masks, or 2,048 elements,
  the group is a PDF knockout group, with a warning.
- SVG draws each element through one mask of the overlapping elements
  above it, built from one shared copy of each element's shape, so the
  elements stay siblings. Past 65,536 shapes in a group's masks, or
  2,048 elements, the group is written as a plain group, with a warning.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-13 12:21:39 -08:00
dotCooCoo add71548db Merge main into fix/blob-brush-preview-bounded 2026-11-13 12:15:59 -08:00
dotCooCoo 8d0e45ef52 Actions: keep empty path values in ui.inspect's copy of recorded steps
Under a read root, an empty or blank value under a path param, or in a
path param's list, names no path. ui.inspect now keeps it, so a recorded
prefs.set step that clears a folder preference shows the reset.
2026-11-13 12:15:49 -08:00
dotCooCoo 4d29ea10ba Fix #7093: the font menu tests draw until every shown sample is rendered
open_menu_with waited once for the samples rendering and then drew its
last frame. A frame starts at most four samples, and none while eight
are rendering, so that last frame could start the samples of the rows
it showed instead of drawing them, and
show_samples_on_white_draws_dark_type_on_white_in_every_brightness
failed with "dark: samples are drawn" when the workers were slow.

wait_for_samples now returns whether any sample was rendering, and the
helper draws and waits until a frame leaves none rendering. Only test
code changes: wait_for_samples is cfg(test) and open_menu_with is a test
helper.

Diagnosis and approach by Taha-Mahmoodi in #7093.
2026-11-13 12:15:29 -08:00
dotCooCoo dd5a9e7cbc Text Wrap: Invert Wrap intersects a line's spans in one pass
With Invert Wrap, each piece of a line's band intersected the line's
spans so far with the spans the wrap shape holds over that piece by
comparing every pair of them. A wrap shape of many parts (a comb or a
zigzag) gives a line many spans, and with an offset the band is cut
into up to 128 pieces, so a dense shape made layout quadratic in the
number of parts for every line.

intersect_intervals now walks the two sorted lists once, stepping past
whichever interval ends first, and gives the same spans in the same
order. Tests compare it with the pairwise intersection on every list of
non-overlapping intervals with ends among seven points, check that two
interleaved lists of 3,000 and 2,000 intervals take at most 5,000
steps, and lay out a line inside a 500-tooth Invert Wrap comb with an
offset.
2026-11-13 12:12:22 -08:00
dotCooCoo 89ccf89461 Document Info: spot colors from gradient stops that color nothing are not listed
Spot Color Objects listed every spot color linked from a gradient's stops
once any stop was above opacity 0. A spot stop at opacity 0 between other
clear stops, at the end of the ramp after a clear stop, or meeting an
opaque stop only at one offset was listed though it prints no ink, and so
was an opaque stop out of order whose pairs have no width.

A stop now counts when it bounds a stretch of some width that the
gradient sampler mixes and either stop of that stretch is above opacity
0, or when it is the first or last stop and above opacity 0, since those
color the offsets before and after the others. Stops in any order are
judged by the stretches the sampler uses. A freeform gradient keeps the
old rule, since it draws from its points. Type runs follow the same rule.
The document.info params text, docs/mcp.md and the progress line
describe it.
2026-11-13 12:11:20 -08:00
dotCooCoo 1a77932167 .ai export: the pieces of a warped image write only the pixels they show
The pieces of a Warp effect, an envelope or their expanded art each
held a copy of the whole image in the .ai editing data and in legacy
.ai files: 200 copies for a Warp's 10 x 10 mesh.

- An embedded image cut to a clipping path (in the editing data, also
  an image under an opacity mask that clips) is written whole the
  first time; later copies of the same pixels are written as the
  rectangle the path or mask shows, 2 pixels more on each side, with
  the matrix moved to keep their place. A copy that the path shows
  none of is written as one pixel.
- An opacity mask's window covers everything the mask's art paints,
  its effects and symbol art included.
- An image's editable original and an image in an opacity mask's art
  are written whole. A live Warp's drawn look is not cut to the
  clipping groups around the object, so a clipped image with a live
  Warp reads back with its Warp live.
- When art that held an image's whole copy is left out (an opacity
  mask's art, a live envelope or a definition past the object limit),
  the next copy is written whole again. A whole copy written on hidden
  lines after an earlier one leaves the earlier one as the copy that
  counts.
- The writers read an image's pixels once for the pieces that follow
  one another.

Docs: docs/target-app-parity.md.
2026-11-13 12:11:12 -08:00
dotCooCoo fd31331079 Merge main into fix/space-in-text-edit 2026-11-13 12:09:58 -08:00
ncic ee52e6b167 Merge remote-tracking branch 'upstream/main' into pencil-no-guides 2026-11-13 21:09:31 +01:00
ncic 09d3d2a76a Merge remote-tracking branch 'upstream/main' into snap-labels 2026-11-13 21:09:17 +01:00
dotCooCoo dd3438cb7f Live Paint Bucket: Cursor Swatch Preview and arrow keys pick swatches
Live Paint Bucket Options gets a Cursor Swatch Preview checkbox (on at
first, saved with the other options). With the Bucket selected, Left and
Right set the active fill, or the stroke when the Stroke proxy is in
front, to the color swatch before or after it in the Swatches panel,
wrapping around at the ends, with the preview on or off. The selected art
keeps its paint. The preview shows the active paint above the pointer
between the swatches before and after it; a color that is no swatch shows
alone until Right picks the first swatch or Left the last, and a paint
that is not a solid color shows no chip. The chips hide during a fill
drag and while Alt samples with the eyedropper. The fill and the stroke
each remember the swatch picked for them, so swatches of the same color
are each reached in turn.

The Bucket and the Shape Builder now take the arrows for swatches only
without Shift, Alt or Cmd/Ctrl, judged by the modifiers held when the key
went down, so modified arrows keep their shortcuts
(Tool::claims_key_with, Session::tool_claims_key_with).

Docs: docs/mcp.md, docs/target-app-parity.md, tool.options params text.
2026-11-13 12:09:16 -08:00
dotCooCoo f42ec13bd2 PDF: the long-page warning names the in-points option
In a PDF without a standard, the warning for a page longer than 14,400 pt
written with a UserUnit above 1 now ends with "turn on Write pages longer
than 200 in at full size in points". The warning already said that apps
ignoring UserUnit show such a page smaller; it now names the Save PDF
option that writes the page at its size in points instead. PDF/A files,
which keep the UserUnit with the option on, and .ai saves, whose options
do not have it, keep the warning as it was.

The longer message has a row in every complete-message catalog. The
params text of document.exportPdf, docs/mcp.md and the evidence line in
docs/target-app-parity.md describe it.
2026-11-13 12:08:49 -08:00
dotCooCoo 291afd8aa6 Free Distort: the bounding box turns with the warped art
object.distort warped the anchors of paths, groups, compound paths and
clip groups but left their bounding-box angle as it was, so a rotated
path kept its old box after a warp and an upright one stayed square to
the page however far the warp tilted it. Text, images and symbol
instances already took the angle of the warp's affine approximation.

- Each path, group, compound path and clip group now takes the angle
  the warp turns its x axis to at its own center before the warp, the
  members of a warped group and the art of a linked opacity mask each
  at their own. An unlinked mask's art keeps its angle. Mirroring keeps
  the nearer angle and a collapsing warp keeps the old one. Reset
  Bounding Box squares the boxes again.
- Each subtree's bounds are measured once while it is warped.
- Corners that equal the corners of the box being warped change
  nothing and add no undo step.

Docs: object.distort params text, docs/mcp.md, docs/target-app-parity.md.
2026-11-13 12:07:21 -08:00
dotCooCoo fdedfb0e59 Merge main into feat/warichu 2026-11-13 11:59:22 -08:00
dotCooCoo 1567957482 Smooth tool: an open path's end turns with the smoothing when a drag reaches it
When a Smooth tool drag refits the stretch at the first or last anchor
of an open path, the curve there now leaves or arrives along the
smoothed path within the tool's reach instead of along the first or
last segment, so a jagged end no longer keeps its jag. The end point
stays where it is. An end where the smoothed path turns back by 90
degrees or more keeps its segment's direction, and a stretch that ends
inside the path keeps the direction of the part outside the drag.

- pathops: SimplifyOptions gains free_start and free_end, off by
  default; closed subpaths and simplify_part ignore them, so Simplify
  and Object > Path > Smooth give the same output as before.
- engine: path.smoothRegion sets them for the ends of an open path
  that a run's refit reaches.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-13 11:51:02 -08:00
ncic 2a9c32bc39 Merge remote-tracking branch 'upstream/main' into pencil-no-guides
# Conflicts:
#	crates/tools/src/draw2/gesture.rs
#	docs/target-app-parity.md
2026-11-13 20:50:22 +01:00
dotCooCoo fb476ca64d Smooth tool: smoothing over a closed path's first anchor keeps its start
When a drag reached the first anchor of a closed path, the smoothed path
started at the first anchor past the drag, and a drag over the whole
path could start it at a corner the refit kept. Dashes and type on the
path then started somewhere else.

The smoothed path now starts at its point nearest where the first anchor
was: at an anchor there, or at a new anchor split into the segment
there, which leaves the outline as it is. Of points equally near, the
one earliest along the path before smoothing, from its first anchor,
is used. A drag that does not reach the first anchor leaves it first,
as before.

Part of #2224.
2026-11-13 11:49:35 -08:00
FredApps 4db24740e4 Merge branch 'main' into fix/spiral-decay-setting-20261011
# Conflicts:
#	crates/engine/src/tests/toolsettings.rs
2026-11-13 20:48:28 +01:00
dotCooCoo 8091e4861e Blend: a spine whose length overflows gives two keys no anchors
Spreading two keys along a spine whose length overflows splits nothing, so
it returned the end anchors, and a closed spine was then opened at its start.
spread_anchors now rejects a length that is not finite before it makes any
target, so two keys get no anchors and the path stays as it was, as for
three or more keys.
2026-11-13 11:47:15 -08:00
dotCooCoo b7709a3c1f MCP: SVG export by a confined caller refuses script links outside its read roots
An SVG or SVGZ writes each linked JavaScript file of SVG Interactivity
into a <script xlink:href> element as stored, so a caller with
automation read roots could read a path outside them from the exported
text. With read roots in force, an SVG or SVGZ of a document that links
such a file is now refused with "a linked JavaScript file is outside the
read roots", an error that names no file or folder. No file or folder is
written, and the document keeps the link.

- The check uses the rule document.json already applies to
  svg_scripts: a relative path is taken from the document's folder, a
  file: URL is a file, and another URL is not checked.
- It covers document.serialize, document.export, document.exportSelection,
  document.exportForScreens and assets.export (before the folder is made
  or any row is written), the SVG and SVGZ saves (also a save without a
  path of a document saved as SVG), file.exportAs and the SVG Options
  dialog's Show Code.
- Callers without roots, and the person at the keyboard, export the
  links as stored.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-13 11:46:18 -08:00
dotCooCoo 82bf9624bf Smooth: the cursor shows Not Allowed when no path is selected to smooth
The Smooth tool showed its own cursor with nothing selected, though a
drag then changes nothing. It now shows Not Allowed until a path it can
act on is selected (an editable path, not a guide, with three or more
anchors on one of its subpaths); a selected path anywhere on the canvas
gives the Smooth cursor wherever the pointer is.

- The Pencil with Alt key toggles to Smooth Tool on and the Blob Brush
  show Not Allowed the same way when Alt gives them the Smooth tool. The
  Paintbrush keeps its closing cursor, since its release with Alt closes
  the stroke.
- A Smooth release, from the Smooth tool or from the Pencil, Paintbrush
  or Blob Brush with Alt, sends path.smoothRegion only while such a path
  is selected. With only type or other objects selected it sends no
  command.
- Use Precise Cursors leaves Not Allowed as it is.
- The cursor catalog and docs/target-app-parity.md describe the rule.
2026-11-13 11:44:24 -08:00
dotCooCoo d7c7b118ec Merge main into feat/rotated-box-scale-reflect 2026-11-13 11:43:40 -08:00
dotCooCoo e1a918636e Document Info: values show in the interface language
The panel showed the values Document Info makes in English in every
language. It now shows the Units, the Raster Effects resolution, each
font's file, substitution and embedding permission, each image's size
and linked file, each Repeat object's kind and counts (in the
language's plural forms) and gradients named by their kind in the
interface language. Font, face, file and swatch names stay as they
are, a gradient swatch named like a kind included.

document.info keeps its English rows and adds the values the panel
builds them from: document.unitId, fontDetails, imageObjects and a
kinds list on each gradientObjects entry. Save… and Package's report
stay in English.
2026-11-13 11:42:24 -08:00
dotCooCoo 51e8938374 Blob Brush and Eraser: the drag outline does a bounded amount of work per frame
The outline of the area a Blob Brush or Eraser drag covers was outlined
again for the whole stroke on every frame, so a long stroke slowed the
canvas as it grew.

- The drag is outlined in pieces of up to 32 segments (one segment each
  with Size set to Pressure), and a frame outlines again only the pieces
  it added segments to. A grid of squares lists the segments near each
  square, and each piece's outline drops the parts inside the rest of
  the area, so the pieces show as one edge.
- One frame takes at most 256 new points and a fixed amount of work;
  Tool::overlays_pending makes the canvas draw again until the rest is
  done with the pointer still. Points not outlined yet show as a thin
  line, and the tip shows at the pointer.
- The settled outline is kept in joined paths of 64 pieces, and a frame
  joins again only the paths that changed. The grid's size is capped.
- The Blob Brush's length label and the Size-by-pressure check keep
  running values instead of rescanning the stroke each frame. With Size
  set to Pressure the outline shows only the edge of the whole area.

The release sends the same path.blob and path.eraseRegion parameters.
2026-11-13 11:40:53 -08:00
BFlat 08b6cfd3e8 Merge pull request #7227 from dotCooCoo/fix/hindi-variable-and-join-terms
i18n: Hindi uses one word for a variable and जोड़ें for joining paths
2026-11-14 02:40:46 +07:00
dotCooCoo d9c1f4335e Threaded Text: thread edits find frames and threads in one pass
Several threaded-text paths in threads.rs searched a list once for each
frame, so their work grew with the product of the frame count and the
list length:

- leave_threads (Create Outlines, Expand Appearance, Rasterize, Flatten
  Transparency, symbol replacement, opening with text as outlines)
  searched the threads for each frame that stayed and ran continued over
  that frame's whole thread again, about n^2 frame visits for a thread of
  n frames. It now marks each changed thread with one continued call,
  and keeps the frames that left in a map instead of a list.
- has_thread_selection (the availability of Release Selection and Remove
  Threading), with_threads, keep_shown_text and cut_to_shown, release and
  remove test ids against a hash set instead of a list.
- create finds the thread of each selected frame from a table built
  once, and each frame still takes in the first thread not yet merged
  that lists it.
- release writes each thread back at its own index instead of searching
  the thread list for an equal thread.

Marks, thread membership, frame order, text and overflow are unchanged.

A 256-frame thread that loses one frame visited 65,025 frames before
this change and at most 256 now. Six tests on many threads count the set
lookups: with a list in place of the set they make 67,650 to 258,226.
Another test compares the marks with a scan of the frames before each
one.
2026-11-13 11:40:39 -08:00
BFlat a105e14aef Merge pull request #7220 from dotCooCoo/feat/image-indexed-color
Edit Colors: convert embedded images to indexed color
2026-11-14 02:40:36 +07:00
BFlat 5f8e4c1913 Merge pull request #7240 from dotCooCoo/feat/smooth-reach-overlay
Smooth tool: the drag shows the band the tool smooths
2026-11-14 02:40:24 +07:00
BFlat 6e54257294 Merge pull request #7225 from dotCooCoo/fix/modal-blocks-canvas-touch
Touch: canvas taps, presses and view navigation are ignored while a modal dialog is open
2026-11-14 02:40:15 +07:00
BFlat 90a9e00fdd Merge pull request #7205 from dotCooCoo/fix/svg-interactivity-hide-script-paths
SVG Interactivity: replies name JavaScript files outside the read roots by file name
2026-11-14 02:40:06 +07:00
BFlat 3a7295e588 Merge pull request #7201 from dotCooCoo/fix/revert-drops-pending-place-reads
Place: File › Revert drops reads started for the content it discards
2026-11-14 02:39:57 +07:00
BFlat 95b328dcca Merge pull request #7193 from dotCooCoo/fix/trace-blur-rings
Image Trace: blurred edges no longer trace as rings of in-between colors
2026-11-14 02:39:47 +07:00
BFlat 58dd9e8908 Merge pull request #7230 from dotCooCoo/fix/docinfo-hidden-object-paints
Document Info: hidden objects and objects at an opacity of 0 list no brushes or paints
2026-11-14 02:39:39 +07:00
BFlat 34457735e5 Merge pull request #7231 from dotCooCoo/fix/threaded-type-measures-paths-and-aligned-frames
Type: threaded paths and frames aligned off the top measure the paragraphs they need
2026-11-14 02:39:29 +07:00
BFlat 821e2d0bb5 Merge pull request #7224 from dotCooCoo/fix/close-modified-background-tab-keeps-front
Documents: closing a modified background tab keeps the document in front
2026-11-14 02:39:19 +07:00
BFlat 4dc9c5c5d3 Merge pull request #7218 from dotCooCoo/fix/batch-fit-journal-index
Artboards: a batch's Fit to Artwork Bounds step journals the artboard it fits
2026-11-14 02:39:07 +07:00
BFlat f0c3646e90 Merge pull request #7198 from dotCooCoo/fix/area-type-band-coverage
Type: area type lines stay inside a frame that pinches in within their band
2026-11-14 02:38:38 +07:00
BFlat 1e385ba06e Merge pull request #7210 from dotCooCoo/fix/envelope-empty-type-box
Envelope: type that draws nothing adds no box next to other content
2026-11-14 02:38:27 +07:00
BFlat b004977dba Merge pull request #7202 from dotCooCoo/fix/raster-effects-skipped-transform-budget
Effects: raster effects before a Transform effect past the budget stay with the art
2026-11-14 02:38:10 +07:00
dotCooCoo 1ed7785bc5 Actions: keep folder preferences out of saved sets and confined replies
A prefs.set step, recorded inside a command.batch or loaded from a
.vcactions file, can hold a folder preference such as scratchPrimary.

- ui.inspect under automation read roots now checks such a step's
  value for {key, value} and each entry of values, also in a batch
  step's commands at any depth, as written and trimmed, including bare
  names, subfolders and file: URLs. An empty value and the scratch disk
  choices Startup and None stay.
- Any other step text written as an absolute path or a file: URL
  outside the read root is left out also when whitespace surrounds it.
- Save Actions refuses a set with a prefs.set step that holds a folder
  value and names the action and the preference instead of writing the
  file. An empty folder and the scratch disk choices save.
- prefs.export leaves out scratchPrimary and scratchSecondary, like the
  other folder preferences. prefs.import refuses a scratch disk set to a
  folder and still reads Startup, None and an empty value.

The folder list, the scratch disk choices and the folder check now live
in one place in the engine. Docs: docs/mcp.md, params text and a
progress line in docs/target-app-parity.md.
2026-11-13 11:37:49 -08:00
dotCooCoo 5c0f22b345 Graphs: Appearance edits on a label are kept for every label of its kind
A fill or stroke row that the Appearance commands gave a graph label, its
object opacity, and the characters the Eyedropper gave it lasted only
until the graph was drawn again: the graph stored nothing for a label's
rows, and the Eyedropper's character attributes were not stored.

- The graph keeps, for value axis numbers, category labels and legend
  labels, the paint, weight, opacity and blend mode of a label's topmost
  visible fill and stroke row, their order and place relative to the
  Characters row, and the label's object opacity and blend mode
  (labelStyles valuesRows, categoriesRows, legendRows). Every label of
  the kind gets those rows when the graph is drawn again, with a placed
  gradient fitted to each label.
- Every appearance.* command stores them, and so do graphicStyle.apply
  and redefine and the paint, stroke, gradient and transparency edits
  aimed at a row, even when the label already showed them.
  transparency.set without a row and object.setProps store only the
  object opacity and blend mode. With several labels of a kind, the last
  one edited wins.
- appearance.copyFrom onto all of a label's text stores the character
  attributes it applied and, from type, the paragraph attributes; a
  transparency pick stores the object opacity and blend mode, and
  append stores the rows.
- paint.setFill and paint.setStroke store a character fill or stroke
  only when the characters take the paint (the whole label, or all of
  its text highlighted with the Type tool); aimed at a row, they store
  the row.
- Document Color Mode and swatch edits reach the kept rows.

Docs: docs/mcp.md, the commands' params text, docs/target-app-parity.md.
2026-11-13 11:35:27 -08:00
dotCooCoo 1d35e00981 Live Paint and Shape Builder: tool.setOption refuses invalid options
tool.setOption for the Live Paint Bucket, the Live Paint Selection tool
and the Shape Builder used to keep the old value when a switch was not
true or false, a color could not be read or a width or gap length was
not a number, and it set the other values sent with it. It also took
the RGB of a color with an alpha part, and the Shape Builder took any
`pickColorFrom` other than `swatches` as `artwork` and any `selection`
other than `straightLine` as `freeform`.

The command now checks every value before it sets any, whether the tool
is chosen or not: a switch must be true or false, a highlight color
must be an RGB color or a layer color's name without an alpha part, a
width or gap length must be a number, `pickColorFrom` and `selection`
must be one of their names, and an option the tool does not have is an
error. The error names the option, and nothing is set or saved. A width
outside 1 to 20 and a gap length outside 0 to 72 are still clamped, and
saved settings with an invalid value still load.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-13 11:34:44 -08:00
dotCooCoo b00e50d4c5 Transform: pattern-only Scale and Reflect of selected anchors follow the box
With anchors selected and Transform Objects off (objects: false, or the
backquote key during a Scale or Reflect drag), the transform moves the
whole objects' pattern tiles, so it now acts along the turned bounding
box instead of the page axes that apply to the anchors alone.
2026-11-13 11:32:51 -08:00
dotCooCoo 18b879a5bc Merge main into fix/graph-series-paint-visible-rows 2026-11-13 11:30:16 -08:00
Drew Dibble 09d5522785 Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/effects-parity.md
#	docs/gaps.md
#	docs/target-app-parity.md
2026-11-13 12:29:58 -07:00
dotCooCoo b6e3e96db3 Brushes: tests expect the first brush of a repeated name
With repeated names numbered when a brush library is read, a stroke that
names a repeated brush draws the first brush of that name, and a later
brush of the name is read as "Name 2". Three tests built a library with a
repeated name and expected the later brush:

- Document Info lists a name two stored brushes share with the first
  brush's type (art), not the last one's.
- The two render memory tests add their own `Dots` scatter brush beside
  the default library's `Dots`. They now remove the default `Dots` first,
  so the stroke draws their brush.
- The .ai writer test that leaves out a Pattern brush "X" gets the
  Calligraphic brush after it back as "X 2", the name it had in the
  document.
2026-11-13 11:23:40 -08:00
dotCooCoo 6326b17b0e Blend: spreading keys along a replaced spine measures it once
Giving the keys of a blend whose spine came from Replace Spine anchors of
their own flattened and measured the whole spine again for every key,
then walked its segments one by one. 1,000 keys on a spine of 4,096
segments flattened it 999 times.

The spine is now flattened once into one buffer. A split flattens only
its two halves and measures the segments after it again, in order, only
as far as the next key, so the arc lengths are summed as before and the
keys get the same anchors. A binary search finds each key's segment.

A spine so large that its length overflows no longer gets an anchor
split at a parameter that is not a number: spreading keys along it
returns nothing and leaves it as it was.
2026-11-13 11:21:31 -08:00