Auto Grouping (params `autoGrouping`, a checkbox in the panel; off unless asked
for): each traced path that has others inside its outer contour comes out in a
group with them, the path first, nested as deep as the picture is. The path that
covers the whole image is nobody's group.
The grouped result paints the same picture: where paths can overlap (stacked
layers, Flat Logo, strokes) a path only joins a path below it, and only if that
moves it under nothing that shares any of its box.
The harness measures three presets with the option on ("grouped %"), painting
them in the grouped order.
Shapes (params `shapes`, a checkbox in the panel; off unless asked for): a filled
outline that is a circle, an ellipse, a rectangle or a regular polygon comes out
as that live shape. A shape is only made when every point of the traced outline
lies within 0.5 px (0.2% of the shape's size, if more) of it; an ellipse whose
radii differ by less than twice that is a circle of its area.
The outline before fitting: a corner's own pixel no longer pulls the two edges
that meet there, and a one-pixel step between two long edges is the corner where
they meet. Sharp corners kept: Default 69% -> 76%, the colour presets 86% -> 91%.
The harness gains plain-shape inputs and "shapes %".
Create: Strokes (the Outline preset, or the option) traced lines along the pixel skeleton: right to a pixel,
with a width counted in pixels, and fitted by a separate, older fitter.
- Each skeleton point is moved to the middle of the line's ink, read from the image across the line: every
pixel counts as much ink as its colour is the line's rather than the paper's, the ink's centroid is the
middle and its sum the width (an anti-aliased or blurred line says both to a fraction of a pixel).
- The stroke width is the median of those widths, no longer a whole number of pixels.
- Each end goes where the ink ends, less the half width a round cap adds.
- Centre lines are fitted as outlines are: a straight line is two anchors, corners stay corners, Snap Curves To
Lines levels them. The old Douglas-Peucker and simplify fitter, and the crate's pathops dependency, go.
Outline on the thin-line inputs, all sizes and variants (docs/image-trace-quality.md), before -> after:
- painted outline 0.310 -> 0.055 px off on average; 81 -> 14 anchors for five lines
- centre line 0.295 -> 0.052 px from the true centre line; stroke width 0.13 -> 0.02 px off; 18.4 -> 2.7
anchors a stroke (blurred, noisy and scanned lines: 2.0 each; aliased: 4.6)
- largest distance 1.84 -> 2.94 px: the square ends of a 10 px line under a round cap
Presets without Create: Strokes are unchanged.
The documented option "replaces slightly curved lines with straight lines and snaps lines near 0 or 90 degrees
to absolute 0 or 90 degrees" (helpx, Image Trace panel options); ours did the first half only.
- With Snap Curves To Lines, a straight piece at least 2 px long and within 2.5 degrees of level or upright is
made exactly so, by moving its ends across it to their mean (the page gives no angle: 2.5 degrees is our
choice). The pieces next to a moved end follow it.
- A junction other regions share stays where it is, so abutting regions still have one curve along their edge;
a line between a junction and a free end is levelled to the junction.
- The harness gains askew inputs (a rectangle, a plus sign and two lines, 1 to 2 degrees off level and upright)
and an "upright %" column: the share of the outline length that is exactly level or upright.
On the clean askew inputs (docs/image-trace-quality.md): Black and White Logo and Technical Drawing, the presets
with the option on, go from 0% to 91% and 95% of their outline exactly level or upright. Their outlines are then
0.49 and 0.59 px from the art as drawn where they were 0.05 (4.2 px at the far end of a 300 px side turned 1.5
degrees): that distance is what the option is for. Presets without the option are unchanged.
Against the documented panel (https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/image-trace-panel-options.html):
- Ignore Color: "Ignores a specific color while tracing. Use the color picker to selectively ignore a color."
The panel's Ignore White is now Ignore Color with a colour button beside it (white until another colour is
picked); `imageTrace.make` takes `ignoreColor: "#rrggbb"` beside `ignoreWhite`, and leaves out the palette
colours within about 30 levels of it. Documents saved before have no such setting and ignore white as they
did. Flat Logo mode leaves out white only.
- Auto Color ("Creates a posterized image from a photo or artwork"): Color mode with the Automatic palette.
- Outline ("Simplifies the image to black outlines"): Black and White with Create: Strokes and no fills.
- The Outline preset's name is translated as a preset's name, not as the Outline view's.
Measured (docs/image-trace-quality.md): Auto Color on flat art is 0.140 px off on average with 4.0 paths (16
Colors: 0.141, 4.2). Outline on the line inputs, the first measure of Create: Strokes: 0.310 px off on average,
1.84 at worst, 81 anchors for 5 lines (Line Art's filled outlines: 0.055, 0.96, 46).
Black and White read every pixel as a blend of "black" and "white", and took for those the mean luminance of
the image's black and white pixels. In blurred or scanned line art most black pixels are part paper, so the
mean black was a gray: the middle of every line counted as more than all ink, and by how much changed as the
line crossed the pixel rows, which put a ripple on its edges that the fit then followed with anchors.
- Black is the image's darkest black (the 2nd percentile of its black pixels' luminance).
- White is the lightest pixel within a few pixels of each crack: the paper as it is there. A scan's paper is
darker in one corner than in another, and read against the image's lightest white it counted as part ink.
On each preset's own inputs, all sizes and variants (docs/image-trace-quality.md), before -> after:
- Line Art: 78 -> 46 anchors, outlines 0.093 -> 0.055 px off on average (blurred lines 168 -> 53 anchors,
scanned 199 -> 70); before sub-pixel edges it was 25 anchors and 0.158 px off
- Technical Drawing: 56 -> 46 anchors, 0.092 -> 0.084 px off
- Sketched Art: 0.197 -> 0.186 px off; Default 0.092 -> 0.090; Black and White Logo 0.063 -> 0.059 with
27 -> 29 anchors
Not kept: reading lines up to six pixels deep by their ink's centroid and width, and a looser tolerance for
straight runs, neither of which measured better.
The font scan read /usr/share/fonts, /usr/local/share/fonts, ~/.fonts and the
XDG data folders. Packages add folders outside those to fontconfig's
configuration (TeX Gyre under /usr/share/texmf/fonts), so fc-list and other
apps showed fonts VectorCraft didn't have. The scan now also reads the <dir>
folders of /etc/fonts/fonts.conf and the files it includes (conf.d, the user's
own), as the fontconfig documentation describes them. The files are read as
they are: no library is linked and no program is run.
A PDF-compatible .ai keeps its art past the artboard edge unclipped (#646),
and its pages are laid out in a row 36 pt apart. Art that a page draws far
to the left of its artboard (notes beside a design, say) then lay over the
page before it. There it was taken for that page's art: the check against
the editing data found art its layers don't have and opened the file as
its PDF part, without the art outside its artboards, and place_pages left
that art behind when it moved the pages.
Such a page now moves right until its art clears the art before it.
Plain PDFs (clipped to the page) keep the 36 pt gap.
Every colour layer took a mask, component labels, a fill stack and an edge map the size of the whole image,
and as many layers were traced at once as the machine has cores.
- A layer is traced within the box its pixels lie in.
- The stack that fills components holds `u32` indices (a mask has at most 64 million pixels).
- Layers are traced as many at a time as fit in half a gigabyte (about ten bytes a pixel of the largest
layer's box each), on however many cores.
Peak memory above the image itself, photo-like image (docs/image-trace-quality.md), before -> after:
- 16 megapixels: High Fidelity Photo 3,856 -> 880 MB (2.85 -> 3.65 s), Low Fidelity Photo 1,972 -> 754 MB
(1.49 -> 1.80 s), 16 Colors 1,625 -> 298 MB, Shades of Gray 852 -> 407 MB, Default 256 -> 199 MB
- 4 megapixels: High Fidelity Photo 981 -> 643 MB, 16 Colors 408 -> 122 MB, times unchanged
The traces are the same, path for path.
An image whose decode needs more than the 2 GiB allowance (512 MB on the web) is not drawn.
A raster export says so since #1517; on screen it was still blank with no word. Open and
Place now carry a warning for it, and the Links panel's row a note (links.list: note).
Paste read PDF only from the Windows clipboard and EPS nowhere, so on macOS a copied
equation or drawing pasted nothing new (the last thing copied in the app came back).
- macOS: the pasteboard's PDF, EPS and SVG are read, ahead of text and bitmaps.
- Windows: EPS is read beside PDF.
- Everywhere: an .eps file copied in a file manager pastes.
- clipboard.importEps loads an EPS into the clipboard as document.open reads one.
Complete Polish catalog (4,492 rows): every menu string, tl! literal,
plural and status/error message, enforced by the existing completeness
tests (complete_menus, KEEPS_MENU_NAMES, COMPLETE_MESSAGES).
- pl.tsv: clean-room translation from the English meaning, following
common Polish desktop-software conventions (obszar kompozycji for
artboard, obszar roboczy for workspace, obrys, wypełnienie, próbki…)
- plural_polish: one / few (2–4, not 12–14) / many (0, 5–21, 25…)
- every pl-* locale (pl-PL, pl_PL.UTF-8) resolves to it
- tests: Polish plural forms, vocabulary, nested message translation;
Polish letters in the bundled-font coverage test
- docs: README, development.md, localization-parity.md
Drafted with AI assistance; native review welcome.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DukHNvUY45rjRnvtz91x33
Each region's outline was fitted on its own, so the edge between two regions was fitted twice, a little
differently, and left hairline gaps and overlaps.
- An outline is cut at its junctions: the pixel corners where three or more regions meet, where two parts of a
region touch by a corner only, and where an edge reaches the image's border (each side of the image is a
neighbour of its own, so the image's corners stay exact). A junction lies where the edges around it say, the
same for every region around it.
- Each piece between two junctions is the whole edge between two regions. Both get exactly the same points for
it (shifts are rounded to 1/1024 px so that the two sides agree to the last bit; whether a piece is hard is
decided by the piece alone) and it is fitted the same way from either side (from its end further left), so it
is one and the same curve in both paths. A loop without junctions is fitted clockwise from its leftmost point:
a shape's outline and the hole it leaves in the shape around it are one curve too.
- Stacked layers (Overlapping) share no edges and are cut at the image's border only.
On each preset's own inputs, all sizes and variants (docs/image-trace-quality.md), before -> after:
- Default: gaps 22.2 -> 0.8 px, overlaps 22.4 -> 0.5 px
- 3 Colors: gaps 48.6 -> 0.0, overlaps 29.4 -> 0.0; outlines 0.087 -> 0.104 px off on average
- 6 Colors: gaps 39.0 -> 0.2, overlaps 35.7 -> 0.3; 0.137 -> 0.140 px off, largest 1.09 -> 1.34
- 16 Colors: gaps 34.5 -> 0.2, overlaps 26.4 -> 0.2; 0.133 -> 0.141 px off, largest 1.09 -> 1.31
The public 3D and Materials panel options list Depth, Twist, Taper and Cap
for every 3D type; Inflate had only Depth.
- Twist (0° to 360°) and Taper (1–100 %) turn and scale the back about the
shape's centre, as Extrude & Bevel's do: a puffed back is turned and
lowered in proportion, and the sides follow in slices.
- Cap: Solid closes the back; Hollow leaves it off, so the inflated front is
a shell open behind. The documentation doesn't say what Cap means for
Inflate; this reading is stated in docs/three-d.md.
- The dialog's Object tab shows them in the documented order (Depth, Twist,
Taper, Cap), then Volume and Both sides.
At 0 % volume a twisted, tapered Inflate has the extrusion's outline from
every side (tested).
A twisted, tapered, hollow Inflate draws on the canvas exactly as it
exports, and expands to faces with their own ids (tested). ROADMAP and the
parity pages list the new options; the 3D score stays at ~25 %.
The public 3D and Materials panel options give Twist as up to 360°; it took
-360° to 360°. Values beyond either end now hold at that end (the effect is
a day old, so only fresh files carry a negative twist).
On macOS, native_menu::sync reads every row of the menus (each
command's enabled and checked state, and each label) whenever
state_hash changes, and the macOS backend updates the AppKit items
whose label, enabled or checked state changed. The hash included the
active document's revision, and Session::preview makes a new revision
at each step of a drag, so the app read every row again at each step
of a move, scale or shape drag. In a release build on an M2 Max, that
took about 1.5 ms a step with one object selected, and 13.8 ms a step
with 5,001 objects selected.
While an interaction is open, state_hash leaves the revision out.
Move, scale and shape drags, dialog previews and the font menu's
preview are interactions. The menus are read when an interaction
begins and again on the frame it ends: when a drag's button is
released, when Escape cancels a drag, when a move is dragged onto a
panel, when a dialog's preview is applied or canceled, when the font
menu's preview ends, or when a control request such as
ui.dialog.cancel ends it. They are still read on a click or a key
press during an interaction.
docs/development.md says when the menus are updated during an
interaction.
Offset Path, Outline Stroke, the Pathfinder panel, Clean Up, Make
Blend, Rasterize and the other commands that #1429's table marks 3,
except Split Into Grid, added, removed or replaced their objects one at
a time, and so did Remove Anchor Points, Redefine Symbol and the
removal of Shaper groups whose source art is gone. Each insertion or
removal moved the later objects of the layer or group. The lookups by
id that followed walked the tree, and every fourth walk rebuilt the
lookup cache. On 25,000 rectangles a release build of vectorcraft-cli
took 29.1 s for Offset Path.
These commands now make their edits with Document::wrap,
Document::insert_beside and Document::remove_many, in one pass over the
children of each layer or group, and Outline Stroke and Expand write
each new object into the place of the object it replaces. A list that
holds an object twice, or an object and another inside it, and that was
edited without an error before, is still edited one object at a time.
The documents, selections and undo steps are the same. On the same
rectangles Offset Path now takes 1.3 s.
Docs: docs/target-app-parity.md.
These commands removed, moved or copied one row or object at a time:
the Layers panel's Delete Selection, Duplicate Selection, Collect in
New Layer, row drags, Merge Selected, Flatten Artwork and Release to
Layers, and Cut Artboard, Duplicate Artboard and the Artboard tool's
Alt-drags that copy art. Each edit moved the later rows of the same
layer or group to other indices. The next lookup by id then walked the
tree, and after every fourth such walk Document::index_path rebuilt
the whole lookup cache. Release to Layers also walked the document
twice for each new layer to name and color it. With 25,000 rectangles
in one layer, a release build of vectorcraft-cli took 11.9 s for
Duplicate Selection and 17.7 s for Release to Layers.
The commands now remove, move and copy the rows in one pass over each
layer or group, with Document::remove_many and Document::wrap, and
with copy_above, the copy pass of duplicate_in, which Duplicate
Selection and the artboard copies now share. Release to Layers names
its new layers from one walk of the document. Merge Selected checks
the target layer's objects against a set of the listed rows, and
Delete Selection checks the top-level layers against such a set. In a
document whose ids are unique, the rows keep their order, new rows and
layers take the same ids, names and colors, and the selection, the
undo step and the journal are the same. A release build with this
change takes 98 ms for Duplicate Selection and 26 ms for Release to
Layers.
Docs: docs/target-app-parity.md.
A photo's noise is as large as the step from one palette colour to the next, so noise decided pixel by pixel
which of two neighbouring colours a pixel got: tens of thousands of specks, or (since the palette change)
neighbouring tones merged into one.
- A continuous-tone image (eight colours leave much of it unsaid; flat art, however noisy or unevenly lit, is
not one) is smoothed before it is quantised: three rounds of a small bilateral filter, which averages noise
away within a tone and keeps the edges between tones where and as sharp as they were.
- With Method: Overlapping, layers are stacked by lightness, from the largest colour's end of the scale,
instead of by area. Each layer reaches under those above it, so where two colours meet every layer between
them in the stack ends on that same edge and shows through its anti-aliasing: by area that was an unrelated
colour's hairline around every region; by lightness it is a neighbouring tone.
- Ignore White with Overlapping: no layer reaches under the white that is left out.
On each preset's own inputs, all sizes and variants (docs/image-trace-quality.md), before -> after:
- High Fidelity Photo: colour error 2.37 -> 1.75 (2.30 at the baseline), edge error 2.59 -> 1.98, 639 -> 121
paths (3,653 at the baseline), 25,604 -> 9,818 anchors, specks 152 -> 10; a 16 megapixel photo-like image is
traced in 3.5 s (31,131 paths) where it was refused
- Low Fidelity Photo: colour error 2.67 -> 2.45, 55 -> 37 paths
- Shades of Gray: 73 -> 17 paths, colour error 9.33 -> 9.26
- The other presets' numbers are unchanged
Color and Grayscale chose their colours from every pixel, so the blends along anti-aliased edges took palette
entries and traced as slivers, and with more colours asked for than the art has, k-means split flat colours
along their noise and the halves traced as specks all over each other.
- The palette is made from the picture's own colours: a pixel that lies between the pixels on opposite sides
of it is a blend and counts for nothing (lines one pixel wide and noise are no blends).
- Colours that noise made of one colour merge: close colours whose pixels are mixed through each other. The
bands of a gradient keep to themselves and stay apart.
- A blend pixel whose nearest palette colour is a third colour (halfway from navy to yellow lies an olive that
is nearer to a green elsewhere) goes to the nearer of the two colours it lies between.
- Boxes are cut where their two sides differ most (Otsu's criterion) instead of at the median, which split the
largest colour of flat art along its noise and left two smaller colours in one box.
- Colours are compared in Oklab when pixels are assigned and in k-means.
On each preset's own inputs, all sizes and variants (docs/image-trace-quality.md), before -> after:
- 16 Colors: 69.9 -> 4.2 paths, 1743 -> 66 anchors, outlines 5.08 -> 0.13 px off on average (largest 85.9 ->
1.09), kinks 1376 -> 4.4, paths in a colour the art doesn't have 5.9 -> 0
- 6 Colors: 21.2 -> 4.0 paths, 4.44 -> 0.14 px off; 3 Colors: colour error 4.86 -> 0.43, 0.33 -> 0.09 px off
- High Fidelity Photo: 3636 -> 639 paths, 81,597 -> 25,604 anchors, colour error 2.50 -> 2.37; a 1 megapixel
photo-like image 1.98 -> 0.42 s (51,389 -> 4,298 paths), a 4 megapixel one traced (27,583 paths) where it was
refused
- Low Fidelity Photo: 128 -> 55 paths, colour error 2.59 -> 2.67; Shades of Gray: 91 -> 73 paths, 9.29 -> 9.33
text.create, text.createInPath, text.setText and text.setStyle answer with
overflow: [id…] for the area type and type on a path whose text doesn't fit (left
out when all fits), and MCP add_text with overflow: true. add_text takes area
type's fit and fitMinPercent.
Extrude & Bevel, Inflate and Rotate sampled every curve at sixteen points and
refused a shape past 1,024 points, about 64 curves: outlined words reached
that quickly, and Rotate refused them although it only turns the outline.
- Each curve takes as many straight pieces as keep it within 1/2000 of the
shape's size (at least 0.05 pt), by Wang's bound: 1 to 32.
- Extrude & Bevel and Inflate take up to 2,048 points, Rotate 65,536.
- Inflate's distance and crossing tests use a bucketed edge index, and
cells no edge crosses skip clipping: about 80 ms at 2,000 points (it was
over 600 ms with the old search; 95 ms at the old 1,024-point limit).
Revolve's own sampling is unchanged.
Object > Path > Split Into Grid took a number of rows and columns and one gutter, and split each
selected path on its own. The dialog now has the documented options, the rows beside the columns:
number, height or width, gutter and total, with Add Guides and Preview. Several selected paths
become one grid over them all, painted as the topmost one, as documented.
object.path.splitIntoGrid takes rowGutter and columnGutter (gutter still sets both), totalHeight
and totalWidth, height and width (the size of each row and column, which sets the total) and
addGuides, which adds ruler guides along the row and column edges on the grid's layer.
Behaviour reference:
https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/divide-or-split-objects.html
On each pointer move of an Alt-drag with the Selection, Direct
Selection, Group Selection, Rotate, Scale, Reflect or Shear tool,
Session::preview put the drag's snapshot back and ran object.transform
with copy again, and duplicate_in copied every selected object again.
The Move, Rotate, Scale, Reflect and Shear dialogs with Copy and Preview
on copied every selected object again on each change of a value.
The interaction now keeps the untransformed copies that its first
preview with copy makes of the snapshot. Each later preview that
copies the same objects of the same snapshot transforms only those
copies. The copies keep their ids, and the document, the selection,
the undo step and the journal are the same as before. With 25,000
rectangles selected, a release build of vectorcraft-cli took a median
of 138.5 ms for each Alt-drag move after the first preview and takes
105.7 ms with this change.
Docs: docs/target-app-parity.md.
Each crack between an inside and an outside pixel is moved to where the pixels across it say the edge is (the
blend of the two tones in the column of pixels across the crack; a thin line by the centroid and sum of its
tone; hard-edged images are read as staircases), and the outline is fitted as Flat Logo mode fits its own:
corners put back, straight runs as lines, a round outline as a circle, the rest as few curves as Paths allows.
Snap Curves To Lines straightens slightly curved runs and makes arcs. A fit that strays from its outline
gives way to a plainer one, and at last to the outline's own points.
On each preset's own inputs, all sizes and variants (docs/image-trace-quality.md):
- Default: outlines 0.313 -> 0.091 px off on average, largest 1.51 -> 1.02; edge error 17.2 -> 10.1;
gaps between abutting paths 63.5 -> 22.2 px, overlaps 67.0 -> 22.4 px
- Black and White Logo: 0.270 -> 0.064 px off, kinks 15.3 -> 0.3, anchors 3.78 -> 2.73 times the art's
- Technical Drawing: 0.201 -> 0.086 px off; Line Art: 0.158 -> 0.093
- Worse: anchors on blurred and scanned thin lines (Line Art 25 -> 77 on average), High Fidelity Photo
(colour error 2.30 -> 2.50, anchors 46,866 -> 81,597, 1 megapixel photo-like image 0.37 -> 1.98 s)
Bevel Shape, Space, Bevel Inside, Bevel both sides and Presets, as the public 3D and Materials panel
options page names them. The 3D effects' error messages get rows in the twelve complete catalogs.
The three 3D types besides Revolve, as live effects in the three-d crate,
drawn as shaded vector faces by the canvas, SVG/PDF export and Expand
Appearance alike, from the public 3D and Materials documentation:
- Extrude & Bevel: depth (0-2000 pt), twist, taper, solid or hollow caps,
and bevels in six original cross-sections (classic, rounded, cove, step,
ogee) with width, height, repeat, spacing, inside and both sides.
- Inflate: the shape as a membrane under pressure (a Poisson solve held at
zero along the outline and holes), so it rises vertically from the edge
and rounds without creases; volume, depth and both sides. Steep cells
near the outline are refined without cracks.
- Rotate: every fill and stroke turned in 3D and shaded as one plane.
All three share Revolve's view and lighting parameters, the rotation and
light gizmos (turning about the art's centre) and a dialog with Object,
Rotation and Lighting tabs, with Position presets (Front ... Isometric
Bottom, our own values). An object takes one 3D effect. Inflate cuts the
grid cells that straddle a hole's edge with pathops, hence the new
pathops -> three-d layer order.
Docs: docs/three-d.md, mcp.md, and the 3D rows of the parity docs.
The renderer kept the rasters of an object's drop shadows, outer glows
and filtered content (Gaussian Blur, Feather and the Photoshop-style
filters), and drew them again while the object, the zoom and rotation
and the ink plane stayed the same. The content drawn into a raster also
reads things outside the object: patterns, symbols, brushes, images,
document, layer and color settings, the knockout a group inherits, the
fonts, and the options the frame is drawn with. When one of them
changed, a renderer that had drawn the object went on drawing the
raster it had kept, made from the old values.
Each kept raster now records what its content was drawn from: which of
those things the object reads, found by walking the object when the
raster is made, and a hash of their values then. The renderer draws
the raster only while that hash is the same in the current frame. Once
a frame, the renderer hashes the document's patterns, symbols and
images. It hashes their art and pixels by the addresses of their
shared values, and it keeps weak references to those values while a
raster made with them is kept, so no other value can take one of those
addresses. A filtered raster also records the color channels its
filters split, and under De-Interlace the raster resolution.
A temporary release test timed the inputs of 400 kept rasters at about
10 microseconds a frame.
crates/trace/tests/quality draws its inputs from known vector art (no image files), traces them with every
built-in preset and measures fidelity, distance from the true outlines, economy, artefacts, time and memory.
docs/image-trace-quality.md records the numbers on main before any change, beside the same measures of an
open tracer run as a black box. No change to the tracer.
Delete, Cut, Group, Make Compound Path, Make Clipping Mask, Send to
Current Layer, Ungroup and Release Compound Path removed, inserted or
moved one object at a time. duplicate_in inserted one copy at a time;
Duplicate, Copy in the Move, Rotate, Scale, Reflect and Shear dialogs,
Transform Again after a copy, Transform Each with Copy, the perspective
copies and each move of an Alt-drag call it. Each insertion or removal
moved the later objects of the same layer or group to other indices.
The next lookup by id then walked the tree, and after every fourth such
walk Document::index_path rebuilt the whole lookup cache. Each command
did work that grew with the number of objects times the size of the
document: with 25,000 rectangles selected, a release build of
vectorcraft-cli took 7.2 s to group them and 11.5 s to copy them with
object.transform.
Document::remove_many, Document::wrap and Document::insert_beside
remove nodes, wrap them in a new node, or put nodes directly above or
in place of their anchors, in one pass over the children of each
parent. Each finds the nodes with one index_paths call, checks every id
before it changes anything, and clears the lookup cache once. The
commands above use them. The objects keep their order, the copies take
their ids in the same order, and the selection, the undo step and the
journal are the same. duplicate_in still copies one root at a time when
a root is listed twice or lies inside another root. A release build
with this change groups the 25,000 rectangles in 27 ms and copies them
in 121 ms.
Docs: docs/target-app-parity.md.