Commit Graph
12393 Commits
Author SHA1 Message Date
dotCooCoo 1c8cec3e7b Merge main into fix/document-info-labels-translated 2026-11-13 07:36:52 -08:00
dotCooCoo fdbe4ea32d Merge main into fix/command-batch-artboards 2026-11-13 07:36:48 -08:00
dotCooCoo dcb69871fc Merge main into feat/custom-toolbar-dock 2026-11-13 07:29:45 -08:00
BFlat 77970c6924 Merge pull request #7082 from dotCooCoo/fix/smooth-resets-anchor-selection
Smooth tool: smoothing a path deselects its direct-selected anchors
2026-11-13 22:26:18 +07:00
BFlat 45acda45fe Merge pull request #7084 from dotCooCoo/feat/message-rows-id-nl-da-bg
i18n: Dutch translates status and error messages
2026-11-13 22:26:08 +07:00
dotCooCoo 8b990de004 Join: merge a run of coincident anchors in one pass over the path
Join removed each merged anchor from the path's anchor list as it went,
which moved every anchor after it, so a long selected run in front of
many other anchors took time quadratic in the path's length. It also
looked up each anchor's selection in the selected-anchor set.

Join now marks the merged anchors and removes them in one pass at the
end, reading the selection from one flag per anchor. Each corner or
smooth conversion sees the same neighbors as before, so the resulting
anchors are unchanged. Anchor::smooth_between holds the smoothing that
SubPath::smooth_anchor applies.

Tests compare the new merge bit for bit with the old one on 30,000
generated paths, and count the anchors moved when 300,000 selected
anchors in front of 300,000 others merge: each of the others moves
once.
2026-11-13 07:25:49 -08:00
BFlat 5fcfb0ce6e Merge pull request #7086 from dotCooCoo/feat/library-charstyle-menu
Libraries: a character style's menu offers Apply, Add to Character Styles, Rename and Delete
2026-11-13 22:24:41 +07:00
dotCooCoo 2361d2cb1b 3D: caps stay last when overlapping faces form a depth cycle
- When the faces that overlap form a cycle of depth orders, the order
  broke the cycle only after drawing every face with nothing to wait
  for, so a cap could be drawn before the walls and they painted over
  it. A face drawn last now keeps its place: no face moves across it.
- A face with a corner at or past the eye, which the perspective
  projection holds just in front of the eye, no longer gets a plane,
  so its depth is not measured on a plane its drawing does not match.
- docs/three-d.md states the masked member rule in Groups as the
  Fills and strokes section does, and docs/target-app-parity.md cites
  the Inflate test by its name.
2026-11-13 07:23:56 -08:00
BFlat 7fb0a5b51b Merge pull request #7088 from dotCooCoo/fix/panel-drags-primary-button
Panels: item drags start only with the primary button, as Layers does
2026-11-13 22:23:19 +07:00
dotCooCoo 08e1b7af80 Image Trace: a background trace applies only to the pixels it traced
A trace made away from its command (the background trace, #3098) counted
as current when the object, its image record and the settings were
unchanged, without looking at the image data. When other pixels came to
sit under the same image key (Rasterize after an undo reuses the key and
the id, Update Link, a replaced blob), the finished trace applied its old
paths to the new image.

The trace's inputs now include the encoded bytes it decodes, compared by
allocation first and then by content. TraceRun::current and the hand-off
in imageTrace.make and imageTrace.makeAndExpand both use them, so a stale
trace is dropped and the command traces the pixels there now. Equal bytes
in another allocation still count as the same image.
2026-11-13 07:18:53 -08:00
dotCooCoo 07522fb4e6 Tests: ui-egui tests remove the temp folders they make
Place, taskbar, opening, Save Options, Missing Fonts and one control
test left their folders in the system temp folder after every run.
Other opening and control tests removed theirs only at the end of a
passing test, so a failing one left its folder.

- A test-only Folder guard (crate::tests_folder) removes its folder
  with everything in it when dropped, also when the test panics, as
  the find-files tests' Tree guard does (#6948).
- The opening and control tests that made their own folders use it.
- Each Place test gets its own folder instead of one shared per-process
  folder, so one test's cleanup cannot remove another's files.
- The Missing Fonts helper no longer removes and remakes the folder
  vectorcraft_testkit::temp_dir made, which keeps it owner-only on Unix.
- vectorcraft_testkit::temp_dir is unchanged.
2026-11-13 07:14:08 -08:00
dotCooCoo 7f18643ada Type: measuring a threaded frame copies its own text once
Each measurement of a frame's share started from a copy of the whole frame, its own runs included, and then replaced the runs. The frame is now copied once without its own text, and each measurement copies that. A test counts the frame text copied to measure frames that already hold text.
2026-11-13 07:12:59 -08:00
dotCooCoo fb9160be7a Paths: Delete, Cut and Cut Path read each subpath's own selected anchors
Clear, path.deleteAnchors, Cut, Copy and Cut Path scanned every selected
anchor or segment of a path once for each of its subpaths, so a path with
thousands of subpaths took time that grew with the square of their count.
The selection sets are ordered by subpath, so a new helper, subpath_refs,
reads one subpath's entries with a range lookup instead. The results are
unchanged.

With one selected interior anchor on each of 8,000 open subpaths of one
path, Clear took a median of 74 ms of thread CPU time before and 3 ms
after (release build, five runs each; the timings are noisy because the
CPU was shared and loaded).

Tests in delete_path_pieces.rs check exact surviving subpaths and their
order for anchor and segment selections across open and closed subpaths,
an untouched subpath, references to missing subpaths and indexes, one
Undo/Redo step and a second Clear, and Cut Path on several subpaths.
2026-11-13 06:58:01 -08:00
dotCooCoo ccb14182e9 Plug-ins: plugin.install approves from the grant it checks again, and others wait for its store writes
Without a plug-in store, plugin.install read the process grant once
for its later check and again for its approval. A grant another session
recorded between the two reads approved the module; when that session
revoked it before the install, the check saw the grant it first read
and the install recorded the revoked grant again. The approval now comes
from the first read.

plugin.install published the plug-in before it wrote the module and its
grant to the Plug-ins folder. A plugin.remove in another session in
between removed the plug-in, its file and its grant, and the install then
wrote both back, so the plug-in loaded with its grant at the next launch.
A session attaching the folder in between loaded the old version, which
kept running while the folder held the new one. plugin.remove, loads of
the stored plug-ins and loads of a plug-ins folder now wait until an
install has written the folder, and an install waits for them.

Docs: docs/plugins.md, docs/target-app-parity.md.
2026-11-13 06:56:24 -08:00
dotCooCoo 4fae51023e Paths: Remove Anchor Points removes many anchors of a subpath in one pass
Object > Path > Remove Anchor Points removed each selected anchor with
its own Vec::remove, shifting the rest of the subpath every time, so
removing k anchors from an n-anchor subpath took O(k*n) moves.

- pathops: remove_anchors removes a subpath's anchors the last first,
  reading each anchor's current neighbors, handles, length and closed
  state as removing them one at a time does, and compacts the anchor
  list once. remove_anchor and remove_anchors share one refit, so the
  result is bit for bit the same.
- engine: the command groups the selected anchors by subpath, works out
  the next selected anchor with each subpath reading only its own
  selected anchors, and takes emptied paths out of the selection
  together (Selection::remove_all).
- Tests compare remove_anchors bit for bit with remove_anchor over 600
  generated subpaths and focused cases, count the anchors moved (2001
  anchors, every other one removed: at most one move per anchor left,
  500,500 one at a time), and compare the next-anchor selection and
  the command on many paths and subpaths with removing one at a time.
2026-11-13 06:54:55 -08:00
dotCooCoo b1c684e912 Scripts: E4X operations count their temporary storage against the heap limit
Assigning to a named child counts its list of the matching children, and
assigning through an empty XMLList counts the chain of targets it climbs,
before either changes the tree. copy(), toXMLString(), normalize(), the
`..` selector and the depth check on insertion hold the stack they keep
for each open level of the tree. Inserting an XMLList, and assigning one
to an item of an XMLList, hold their copy of the list's items.

copy() reads attributes in place, and toXMLString() reads attributes and
namespace declarations in place, instead of copying the lists. copy()
charges the declarations it copies before it copies them. Deleting by
name or assigning an attribute compacts the remaining links in place
instead of building a second list.

toXMLString() holds the set of prefixes a namespace lookup passes, and
picks a new nsN prefix in one pass over the declarations in scope
instead of rescanning them for each candidate.

The parser holds its open elements and their names, the text it gathers,
each tag's attribute values, the sets that check declarations and
attribute names, and its one copy of an element's namespaces, and it
charges the strings of the namespace declarations it makes. The
allowance it holds from the source's length covers the copy a value
takes as it becomes a string.

Comparing XML values, converting them to strings, assigning an XMLList
to an attribute, and reading a name, a qualifier, a namespace URI or
embedded text from a string built by joining strings flatten it with
flat_charged. An attribute name read from a string reserves its copy,
and an element name shares the string.

Docs: docs/target-app-parity.md.
2026-11-13 06:54:23 -08:00
dotCooCoo 1ce6431a99 SVG import: raster images follow their EXIF or TIFF orientation
An <image> whose EXIF orientation or TIFF Orientation tag turns or flips
its pixels, linked or as a data URL, is turned before usvg reads it: the
pixels are stored as they are shown, the width and height swap, and the
box fits the turned image under preserveAspectRatio, as CSS
image-orientation: from-image describes. A turned CMYK JPEG or TIFF keeps
its inks as a CMYK TIFF, as does a CMYK TIFF that is not turned.

- Data URLs of type image/tiff, and untyped TIFF bytes, are read.
- TIFFs with extra channels, CCITT Group 3 TIFFs and planar CMYK TIFFs
  are read as Open and Place read them.
- Converted images decode within the decode allowance, and a TIFF that
  claims more strips or tiles than the decoder can count is refused
  before any decoder opens it.
- exif_orientation moves to vectorcraft-doc so Open/Place and SVG import
  share it.

Tests: crates/svg/tests/it/image_orientation.rs.
2026-11-13 06:53:42 -08:00
dotCooCoo 666084fb81 Blend: find the spine segment for an arc length with a binary search
Spine::at_length walked the first subpath's segments from the start
until one ended at or past the arc length, so placing every blend step,
spread key, blend widget and Objects on Path attachment cost one
comparison per earlier segment. It now searches the anchors' arc
lengths, which never decrease, and picks the same segment: the first
whose end is at or past the length, so an exact anchor length and
repeated lengths from zero-length segments keep their segment. The
point and tangent evaluation is unchanged.

Tests compare the search with the old walk on open and closed spines,
one segment, curves, zero-length segments, every anchor, both ends,
out-of-range and non-finite lengths, and check that a 4,096-segment
spine needs at most 13 comparisons.
2026-11-13 06:46:06 -08:00
dotCooCoo 57d37a6dae 3D: fill walls and gapped strokes draw in depth order where they overlap
Extrude & Bevel, Extrude & Bevel (Classic) and Inflate sorted the fill
walls and the faces of a stroke that leaves gaps (dashed, profiled,
brushed) by one depth each, so a far stroke face could draw over a near
fill wall and the reverse. Two such strokes on one shape drew over each
other the same way, and Inflate drew a plain stroke as if it covered
the fill's edge, which an inflated fill can rise in front of.

- Each flat facet now carries its plane, so its depth can be measured
  at any point under it, with orthographic and perspective views.
- Where faces of one shape's fill and strokes overlap in the wrong
  order, the overlapping faces are ordered by their depths where they
  overlap. A fill wall and its covering stroke keep their order.
- A face of a stroke that leaves gaps that passes through a fill wall,
  a covering stroke's face or an earlier such stroke's face is cut
  along the line where their depths meet, keeping its curved outline,
  within the face limit and the paint allowance.
- Under Inflate, no stroke counts as covering the fill's edge.
- The faces go in one order when the fill and strokes are at full
  opacity with Normal blending. A member of a 3D group with an opacity
  mask or a blending mode of its own orders its faces inside it when
  its own paints qualify. A gradient with transparent stops keeps the
  one order with graphics on its cap, and so does Inflate in gradients
  under a colored light. A solid color under a colored light, drawn as
  one composite per surface, still draws each surface whole.
2026-11-13 06:45:13 -08:00
dotCooCoo 5d5b8aa53b i18n: Hindi messages keep their English meaning and the catalog's terms
193 Hindi rows outside the plug-in messages said something other than
their English: "a number of points" read as "many digits", swatches as
clocks, crop as a harvest, resolution as a resolve, and saving used
words the glossary replaces. Others left English prose in place,
dropped a clause ("to keep them all", "what follows it") or ran the
sentence backwards.

- Rows use सेव, स्वॉच, क्रॉप, रिज़ॉल्यूशन, पाथ and टाइप (for a text
  object) as docs/localization-hi.md sets them.
- Values a command reads (enum values, preset names, OpenType tags)
  stay as the English writes them.
- A test checks those terms and the point unit in every Hindi row, and
  another pins rows whose quantity or clause was lost.
- docs/localization-hi.md, docs/localization-parity.md and
  docs/target-app-parity.md carry the new counts and a wording note.
2026-11-13 06:43:32 -08:00
jmvergesandClaude Opus 5.5 e11155c692 Envelopes: a top object's split goes by the longer of handles or bends
The previous commit took the spans from how far each piece bends away
from the split whenever both handles there were within ONE_CURVE of the
anchor. A curve that barely moves at the split but doesn't bend there
(a straight one, say) has short handles and no bend, so that gave 0/0
and the side fell back to the polygon shares. Now the spans come from
whichever pair is the longer: the handles, or the bends (which go with
the squares of the spans). The check of every piece against the
rebuilt curve is unchanged.

Test: a_split_curve_keeps_the_top_object_patch_where_it_barely_moves_and_runs_straight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 14:42:07 +00:00
dotCooCoo df975e6300 Justify: a line with a tab spreads the text after its last tab to the edge
A justified area type line counted a tab as a word space, so it gave
the tab a share of its room that no glyph drew, and the line ended
short of its measure. A word-tab-word line ended short too.

- The text up to a line's last tab keeps its spacing, each tab keeps
  its stop and its leader stays in its gap. The text after the last
  tab takes the line's room: word spacing, letter spacing and glyph
  scaling within their ranges, then its word spaces, or the gaps
  between its letters when it has none. Kashidas go in that text when
  it is one run.
- A tab separates two words, so Single Word Justification does not
  align a word-tab-word line.
- Right-to-left lines take the text drawn after the last tab drawn,
  measured as the line is drawn, and vertical lines work down the
  column. Point type keeps its natural width.

Docs: docs/target-app-parity.md, docs/type-parity.md.
2026-11-13 06:41:22 -08:00
dotCooCoo 4282f4fa99 i18n: Document Info shows every category title and count label in the interface language
Font Details, Linked Images and Embedded Images, and the Objects counts
Compound Paths, Text Objects, Placed Documents, Symbol Instances, Gradient
Meshes, Blends, Envelopes, Intertwine Groups, Opacity Masks and Objects
with Effects had no catalog rows, so the panel and its category menu
showed them in English in every other language. Each complete catalog
now has them, and the Document rows Pattern Swatches (de, it, ja, ko,
pl, uk, vi, zh-hant) and Path Length (bg, el) are added where missing.

The catalog test now checks every category title, every Objects count
label and Document's fixed row labels. document.info and the saved
report keep the English labels.
2026-11-13 06:32:35 -08:00
dotCooCoo 6d0556e60f Type: a threaded story measures each frame with the paragraphs it needs
Distributing a story through threaded frames measured every frame but the
last with the whole rest of the story, and copied that rest several times
per frame, so a long story through many frames cost frames times story
length.

- The rest of the story is a borrowed view of the given runs (run index,
  byte offset, length). Each frame's share is copied once; the slices,
  styles of empty shares and the run left once the story is used up are
  the same as before.
- Area type without Shrink Text to Fit whose lines align to the top is
  first measured with the text through the end of the paragraph that
  reaches about twice the previous frame's share (at least 512 bytes),
  doubling while the story goes on. The share is taken when its text
  stops fitting before the last paragraph break measured; layout places
  lines in order, stops at the first that does not fit and shapes each
  paragraph on its own text. Type on a path, Shrink Text to Fit, other
  vertical alignments and a share ending in the story's last paragraph
  are measured with the whole rest.

Tests compare every slice with the earlier algorithm, kept in the tests,
for many kinds of frames, styles and paragraphs and 12 seeded stories.
A story of 1,500 paragraphs (55,892 bytes) through 40 frames was laid
out over 2,126,762 bytes while measuring and is now laid out over
21,035.
2026-11-13 06:31:37 -08:00
dotCooCoo 5700b0d8c1 Live Paint: the bucket shows a filled bucket over a face and a brush over an edge
The Live Paint Bucket drew the crosshair in place of its own cursor. It
now shows what a click paints: a filled paint bucket over a face it
fills (of a Live Paint group, or a region of the selected paths), a
brush over an edge it paints, and an empty bucket where it paints
nothing. The cursor follows Paint Fills, Paint Strokes and Shift, and
shows with Highlight off too. Alt still shows the Eyedropper, and Use
Precise Cursors shows the crosshair.

With a Live Paint group and ordinary paths selected, a region of the
paths outside the group is now highlighted, as a click there fills it.

The help page: https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html
2026-11-13 06:30:43 -08:00
dotCooCoo 75c74557bb Scripts: $.fileName names the file of the code that reads it
$.fileName gave the file being run, so a function defined in a file
that #include inserted, in a file $.evalFile ran, or in an earlier run
of a named engine reported the calling file instead of its own.

- vectorcraft_js: HostItem::SiteGetter gives a getter the CallSite
  (source tag, line, column) of the code that reads it.
- $.fileName maps that place through the run's source maps, or the
  maps a named engine kept, to the file the code is in. It is empty in
  code from a script given as source and in kept code whose map was
  dropped. Code that eval or Function compiled still gets the file
  being run.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-13 06:19:23 -08:00
dotCooCoo 60daa85f5b Actions: a View menu step in a recorded batch counts in action set files
Save Actions… writes version 6 when a View menu step sits inside a
`command.batch` step, since playback runs that step on its own, and Load
Actions… refuses such a nested step whose params are not an object.
2026-11-13 06:15:31 -08:00
FredApps e8c7421a3d CI: shared gate for the Affinity corpus; drop release's git-sources copy (#6683)
affinity-corpus.yml ran a local job; it now calls craft-common-actions
gate.yml with the same toolchain (1.95), environment and commands, and a
cache of its own. release.yml's git-sources copy is removed: the shared
release.yml runs the same check before any build.
2026-11-13 15:01:22 +01:00
dotCooCoo 4cf010c130 Flatten: grouping sweeps skip boxes that a box already holds or cannot join
The sweep filtered by transparency kept every open box in one tree, so
a box without transparency met every open box without transparency it
overlapped, though such pairs never join. Both sweeps closed an open
box early only when a later box held it, so in a nest of boxes that
share their left and top edges and get smaller to the right and
bottom, each box met every earlier box around it. One see-through
object inside 512 nested opaque objects took 130,817 meets in the
unfiltered sweep and 131,328 in the filtered one.

The filtered sweep now keeps open see-through boxes and open opaque
boxes in separate trees. An opaque box looks only in the see-through
tree; a see-through box looks in both.

In both sweeps, a box inside an open box it met, ending no further
right, is not opened when that box joins everything it overlaps (any
box without the filter, a see-through box with it). Whatever later
overlaps the inner box overlaps the outer one.

The nest above now takes 512 meets in each sweep, and a nest of 512
boxes alone takes 511 in the unfiltered sweep where it took 130,816.
The groups are unchanged.
2026-11-13 05:42:03 -08:00
dotCooCoo eb2f3ba71a Tests: the font menu scroll bar test lists fonts of its own
font_menu::tests::the_scroll_bar_leaves_the_stars_clear starred every
family in the process-wide font database and listed that database, which
tests running alongside add fonts and composite fonts to. In full
parallel runs it sometimes failed with "8 families, no row stars drawn".
With the four bundled families alone it never reached the scroll bar
check, which ran only for seven or more families.

- The menu's header takes the font database it lists and hands it to
  the rows and the name-width measurement. The app passes the
  process-wide database as before.
- Each FontDb has an id of its own (FontDb::id), and the widest family
  name the menu keeps is stored with that id as well as the generation,
  so a width measured for one database is not used for another at the
  same generation.
- The test lists a database of the bundled fonts, stars its four
  families, opens on Inter, and runs the frames the scroll bar takes to
  appear. It checks that the rows overflow the list's scroll area, that
  a row star is drawn inside that area, and that each star stays left
  of the scroll bar's column (#2277).

With the list's scroll bar set to floating, the test fails with a star
under the bar. Without the id in the widest name's key,
each_font_database_has_its_own_widest_name fails.
2026-11-13 05:41:46 -08:00
dotCooCoo e97113c5c9 Docs: ui-parity.md names all nine built-in brush libraries
The Brush libraries row named four libraries. It now names the nine in
BUILTIN order (crates/engine/src/cmd/brushlib.rs), as the Window ›
Brush Libraries menu shows them, and says nine built-in libraries.
2026-11-13 05:40:21 -08:00
dotCooCoo 38bcbb817f Docs: Paragraph Style Options rows name the dialog and its seven sections
A double-click on a paragraph style's row and the panel menu both open
Paragraph Style Options (ui.paraStyleOptions, paraStyle.edit) with its
seven sections. The type-parity and ui-parity rows still called it the
rename dialog and named ui.paramDialog with paraStyle.rename; they now
name the dialog, its sections and its tests.

The target-app-parity entry said each section has a Reset Panel. Reset
Panel is unavailable in General, and the entry now states that.
2026-11-13 05:38:52 -08:00
dotCooCoo 19ab993479 EPS: an image drawn more than once writes its samples once
An image placed several times in an EPS file (the pieces of an envelope
or a Warp effect, copies of one image, a symbol's instances) wrote its
whole ASCII85 data at every placement: 200 copies for one envelope at
the default fidelity, enough for a large photo to pass the 256 MB page
limit and lose pieces.

The samples of such an image are now written once in the setup as
VCim<i>, and each placement reads them from there:
- Level 3: a ReusableStreamDecode stream, rewound with resetfile and
  read through a new FlateDecode filter at each placement.
- Level 2: an array of strings of at most 49,152 compressed bytes, fed
  to RunLengthDecode by a procedure.

An image placed once is written where it is drawn, byte for byte as
before. The page limit counts a repeated image's samples once, and a
colorized image is reused per ink.
2026-11-13 05:38:06 -08:00
dotCooCoo d211e2f613 Artboards: a batch's Fit to Artwork Bounds step fits every selected artboard
A Fit to Artwork Bounds step of `command.batch` that names neither
`index` nor `indices` now fits the artboards the Artboard tool has
selected when the step runs (two or more), as the command does when the
app runs it on its own. Before, the step took the batch's `artboard` as
its `index`, so an action played from the Actions panel fitted only the
active artboard while several were selected.

- A step's own `index` or `indices` is kept, and the batch no longer adds
  an unused `index` to a step that names `indices`.
- The batch's journal entry records the `indices` the step took, so a
  replay fits the same artboards. An action leaves them out and fits the
  artboards selected when it plays.

Docs: command.batch params text, docs/mcp.md, docs/target-app-parity.md.
2026-11-13 05:35:28 -08:00
dotCooCoo 53e8391e41 Recolor Artwork: the Prominent Colors bar keeps the Edit tab's height
- With art, the bar is 10 pt tall and the Edit tab leaves out the gaps
  above the wheel and above the sliders. The dialog is as tall on the
  Edit tab as on the Assign tab, and all of it but the color chips fits
  a 1024 by 576 screen.
- recolor.weigh counts a use for the first row that is not excluded
  when an excluded row lists the same color first.
2026-11-13 05:34:30 -08:00
dotCooCoo 6b79945784 Toolbars: deleting or renaming a toolbar ends the drag of its title bar
Escape during a drag puts the toolbar back by its name. A toolbar deleted
while its title bar was dragged left the drag in place, so Escape docked
and moved another toolbar that had taken the name since. Deleting the
toolbar, or renaming it to a name that differs by more than case, now
ends the drag. The drag of another toolbar goes on.

The drag test also presses Escape in the frame of the first move after
the press, where no drag starts.
2026-11-13 05:33:55 -08:00
dotCooCoo 36c652d942 Control bar: no separator is left at the end of the bar when the controls after it are hidden
A separator that ends a group of the Control bar's controls is now painted
only when a control follows it in the bar. When the groups after it are off
in the panel menu, do not apply to the selection or the tool, or have no
room in the window, the bar ends with its last control. Two separators with
nothing between them (the artboard Transform link and the artboard Align
group, or tool options and the envelope controls) show as one. Each
separator keeps its room, so the groups keep their places and widths.

- Fill and Stroke, Opacity and Graphic Style, Character, image and Image
  Trace options, the Mirror & Cut, Crop Image, Puppet Warp and shape tool
  options, the artboard Transform link and the envelope controls draw
  their outer separator through group_separator. Paragraph and area type
  vertical alignment end through group_end, which draws its separator
  through boundary.
- Regression test: no_separator_is_left_at_the_end_of_the_control_bar
  sweeps window widths from 300 to 1500 points for each case.
2026-11-13 05:28:38 -08:00
dotCooCoo 130f914868 Type: a threaded frame's empty last paragraph keeps a format given to it
A paragraph format given to the empty paragraph after a frame's last
paragraph break was lost when the story flowed again: the paragraph is
shared with the next frame's first paragraph, and the next frame's old
style replaced it, in that frame and in the frames after it.

- The thread reflow also notes which frames' last paragraph changed
  style, and story_paras_restyled keeps that style against the next
  frame's unedited first paragraph, across unedited empty frames too.
  When the same edit formats the next frame's first paragraph, that
  format applies, as before.
- story_paras keeps its behavior for its other callers.
- Tests cover area type and type on a path on either side, a next frame
  holding text, three frames, undo and redo, a format that changes
  nothing, and numbered list counts.
2026-11-13 05:27:02 -08:00
dotCooCoo 6f3ffeb393 Plug-ins: plugin.install writes nothing to the store until the module is checked
plugin.install wrote the module and its grant to the Plug-ins folder,
attached the plug-in's settings and cache and turned a disabled plug-in
on before the registry's last approval check. When another session
revoked the grant, or recorded one for other bytes, after the command
recorded its own grant and before that check, the install was refused
but left the new module and grant in the folder for the next launch.
A plug-in another session installed under the same id between the
command's checks and the install took no part in the decision, so the
install replaced it without `replace: true`.

- registry::install_checked records the grant (or forgets it) and
  installs the plug-in as one step, after a caller check that sees the
  plug-in installed under the id and its grant; a refusal changes
  nothing.
- plugin.install commits through it before any store write, refusing
  with the existing messages when another plug-in now needs `replace`
  or the grant its approval came from changed. The store writes, the
  put-back on a failed write and the disabled-list update now run after
  the commit; their logic is unchanged.
- The grant call-site audit also counts registry::install_checked.

Docs: docs/plugins.md, docs/target-app-parity.md.
2026-11-13 05:25:16 -08:00
dotCooCoo 73a9c22361 Actions: Insert Menu Item adds Object › Align and View menu commands
Insert Menu Item… now lists the Object › Align items and the View menu
commands the app runs itself (Outline, Pixel Preview, Trim View,
Presentation Mode, Screen Mode, zoom, fit, Rotate View, and the show,
hide and snap items).

- An Align step holds the params its click sends at insertion, so a
  later Align To choice does not change it. A step that aligns to the
  key object stops the playback when the selection has no key.
- A View menu step plays in its place at every speed and adds no undo
  step. At Accelerated the steps on each side of it are one undo step
  each. Playing it while recording records it.
- Batch… refuses an action with an included View menu step.
- An action set file with a View menu step is version 6.
- The steps before a stop now play as saved steps, as other playback
  does, so a key that an older version recorded no longer fails there.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-13 05:17:40 -08:00
dotCooCoo 3aa7ea2f78 Recolor Artwork: a weighed color recolors with the row it moved to
- recolor.weigh drops another row's entry for a color that moved, so
  recolor.apply recolors it with the row it moved to and the shares
  describe the recolor. A color that the first color of another row
  with a new color also lists stays where it is.
- Linked swatch colors are grouped through a map, so a row with many
  unlinked colors no longer compares each one with every group.
- The Prominent Colors bar reads the shares again when a row gains or
  loses a new color, since that changes which row takes a shared color.
2026-11-13 04:58:39 -08:00
dotCooCoo 6b55d7ca3e Eyedropper: a whole pick that removes a series mark's painted row replaces the stored transparency
A whole pick from a source with no fill row, or no stroke row, removes
that row from the target. When the target is a graph series mark whose
row had paint, the pick now replaces the row opacity and blend mode the
graph keeps for that kind of row in the series. A row that had no paint
before the pick still leaves the kept values as they are, and a pick of
some attributes from such a source is unchanged.
2026-11-13 04:50:59 -08:00
dotCooCoo ba2a6fc1f3 Type: a further span beside a wrap object is measured across its line's height
Text wrap can split a line into spans that share a baseline. The spans
after the first were measured across the height of the line set in the
first one, so a taller line set in a later span could run into a wrap
object the shorter line stayed clear of. Each further span is now
measured again across the height of the line that goes in it. The line
gets the part that is free, and a span with no free part, or with a part
narrowed to less than the line's first word, is left empty. The
Every-line Composer and the line loop both place lines through
`Pen::place`, so they agree.

The Every-line Composer also keeps a line start whose line found no room
while a longer line from it would be taller. A taller line steps down
the frame by its own leading and can find room the shorter line's steps
missed.

Tests: a_further_span_is_measured_across_the_height_of_its_line,
composition_tries_a_longer_line_after_one_that_cannot_be_set.
2026-11-13 04:48:43 -08:00
dotCooCoo 349034dda2 Smooth tool: each drag smooths the path under it, and repeated drags keep smoothing
Part of #2224.

A Smooth tool drag only refitted the anchors in its reach within
Fidelity, so a wobble wider than Fidelity stayed, and a second drag over
the same place changed nothing. Repeated refits also drifted away from
the path, and a drag over a whole closed path left a corner at its first
anchor.

- path.smoothRegion now smooths the stretch in reach along its length
  with a Gaussian whose spread is a third of the reach, then refits it
  within Fidelity. The run's outer end points and handles stay fixed.
- Each refit stays within twice the distance the smoothing moved the
  path (at least a tenth of Fidelity), so repeated drags do not drift
  (SimplifyOptions::smooth_moved_only).
- A whole closed path is smoothed across its start as a loop, so a
  bump at the first anchor is smoothed like any other and no corner is
  left. Object > Path > Smooth smooths a closed path across its start
  the same way.
2026-11-13 04:44:12 -08:00
jmvergesandClaude Opus 5.5 6ed3935e59 Envelopes: a top object's side split at a cusp still follows its curve
Split where the curve stops (a cusp), both handles at the split lie on
the anchor, so their lengths say nothing of how the curve's parameter
divides between the two pieces, and the side fell back to the polygon
shares. When both handles lie within ONE_CURVE of the anchor, the
spans now come from how far each piece bends away from it (the second
derivatives, in proportion to the squares of the spans). The existing
check of every piece against the rebuilt curve is unchanged.

Test: a_split_curve_keeps_the_top_object_patch_when_split_at_a_cusp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:43:47 +00:00
dotCooCoo e8c3dace32 Effects: a group's geometry effects reshape what its members draw
A group's or layer's geometry effects now run after its members' own:
the geometry effects of each visible path and compound path in it and
of their fills and strokes, and a nested group's geometry and
Pathfinder effects, are applied first, at any depth, and the group's
effects reshape the result. A symbol instance's geometry effects run
after those of its symbol's members in the same way. Before, the group
reshaped the members' outlines and their own effects ran afterwards:
a 100 pt square with its own 10 pt Offset Path in a group with a 50%
Transform drew 70 pt wide, and it now draws 60 pt wide. Existing
documents with such groups draw differently.

The canvas, export (including groups that keep their type), Expand
Appearance and the art that Pathfinder operations and compound shapes
take from such a group use the same order. The reply of
effect.expandAppearance names such a group and no longer names the
members whose effects the group's art already holds.

The renderer now drops the art it keeps for a group when a plug-in is
installed or removed and a member of the group has a plug-in effect.

Docs: docs/target-app-parity.md. Not yet: the own geometry effects of
images, symbol instances, paths with a linked opacity mask or a 3D
effect, and nested groups with a 3D effect in a group still run after
the group's.
2026-11-13 04:41:58 -08:00
dotCooCoo f918c91c13 Edit: settling an interaction keeps the warning of the preview it applies
With Real-time Drawing off, a command that ends a drag first (Undo, Redo,
Clear History, a plug-in command and the others) applies the drag's
deferred preview, but dropped the warning that preview gave, which
releasing the drag shows in the status bar.

- The applied preview's warning is now kept while a top-level command or
  the tool's actions run. A command's result carries it as
  `previewWarning`, and as `warning` when the command has no warning of
  its own, so the status bar shows it.
- Typing kept as its own step when a tool's drag begins, or when Cmd
  lends the Selection tool, shows the warning in the status bar.
- The Cmd-lend path no longer ignores an error from keeping the typing.

Docs: docs/mcp.md (undo/redo row), docs/target-app-parity.md.
2026-11-13 04:36:38 -08:00
dotCooCoo d747f5b365 Learn: Back to skills and the Quick skill guide card run learn.skills
learn.skills {} shows the Learn panel on its Skills tab with the list of
skills, with no skill and no tutorial open, and returns what learn.list
returns. Back to skills after a skill's last tutorial and the Quick skill
guide card on the For you tab now run it, so the control channel and MCP
can reach the same view.

- The command is listed in both UI command catalogs, and its label has a
  row in every catalog.
- Docs: docs/mcp.md, docs/target-app-parity.md, docs/ui-parity.md.
2026-11-13 04:34:35 -08:00
dotCooCoo ab75b67f85 Simplify: the join at the start of a smoothed loop turns by at most 0.5°
A smoothed loop with no corner is refitted along one direction across
its start. The first and the last fitted curve were each compared with
that direction, so when both became lines, the two lines could turn by
up to 1° where they meet at the loop's start.

`refit_part` now compares the last curve of such a loop with the
direction the loop's first segment leaves along. When the line would
turn by more than 0.5° from it, the last piece stays a flat curve.

Test: `simplify_keeps_the_join_at_the_start_of_a_smoothed_loop_smooth`
(`crates/pathops/tests/it/ops.rs`).
2026-11-13 04:33:23 -08:00
dotCooCoo d956d2165f SVG: an absolutely placed right-to-left chunk is anchored by the edges of its glyphs
A text chunk placed by an absolute x was measured from the pen of its
first character to the farthest glyph end. The first character of a
right-to-left chunk is drawn at its right, so a chunk of several such
characters was measured as that one character: anchored by middle or
end, or starting a line beside absolutely placed characters, it opened
off its position by the width of its other characters.

- A chunk's left and right edges are the smallest glyph start and the
  largest glyph end over all its characters, as SVG 2 anchors a chunk
  (https://www.w3.org/TR/SVG2/text.html#TextLayoutAlgorithm). The first
  character's pen is placed at its own distance from the left edge.
- The layout that measures the chunks has the direction the imported
  text gets: left to right unless the text's direction is rtl. Text
  starting with a digit and holding right-to-left letters was measured
  right to left.
- The chunks are measured in the first of the layouts that kern the
  characters onto their x, so anchored text is laid out once less.
2026-11-13 04:32:32 -08:00