Commit Graph
8368 Commits
Author SHA1 Message Date
dotCooCoo d9d33c9e4b Render: the frame after anchors of a long path move updates only the segments that touch them
The renderer keeps the path of a path or compound path of 1,024 anchors
or more by object id, with the bounds of its segments and of its miter
spikes in runs of 128 segments. When anchors move and the subpaths and
anchor counts stay the same, the next frame rewrites only the path
elements of the segments that start or end at a moved anchor, and
measures again only the runs that hold them and the runs of the
segments with length before them. Other edits make the path again as
before. The pixels and the cull bounds are the same.

- StrokeLayer::bounds_on is unchanged. The miter tip of one joint
  (miter_tip) and the stroke box with given tips (bounds_with_tips) are
  new functions beside it, and Appearance::stroked_bounds_by takes the
  box of each shaped stroke, so the cull bounds of a kept path use the
  kept tips.
- geom::bez::has_length is the predicate reach.rs used for segments
  that draw, made public.

Docs: docs/target-app-parity.md.
2026-11-05 06:28:22 +07:00
BFlat a9c1fe085b Merge pull request #5705 from dotCooCoo/fix/layers-thumbs-mask-anchors
Layers: opacity masks and edited objects count toward the thumbnail anchor limit
2026-11-05 06:17:04 +07:00
dotCooCoo f31869cf27 Layers: opacity masks and edited objects count toward the thumbnail anchor limit
The anchor count that sends a row's thumbnail to the thumbnail thread now
includes the art of opacity masks that are not disabled, since the
thumbnail draws that art. A row drawn before was rendered on the UI thread
whenever its last thumbnail had been quick, even when its object had since
become a long path; it now goes to the thumbnail thread when the object
has more than 2,000 anchors.
2026-11-05 06:07:55 +07:00
BFlat 5f3550735a Merge pull request #5704 from dotCooCoo/fix/symbol-break-hidden-tint
Keep symbol colors when expanding a hidden tint
2026-11-05 06:07:14 +07:00
dotCooCoo 1968077984 UI-1676: Ignore hidden symbol tint when expanding 2026-11-05 05:54:43 +07:00
BFlat 1fb0b4150d Merge pull request #5703 from dotCooCoo/fix/symbol-effect-expand-tint
Fix tinted symbol colors in Expand Appearance
2026-11-05 05:52:50 +07:00
dotCooCoo dcefceddf6 UI-1675: Preserve symbol tint when expanding effects 2026-11-05 05:52:34 +07:00
Dylan M. Taylor 632cb9d649 Pass the Linux signing key to the release workflow (#5702) 2026-11-04 17:45:03 -05:00
BFlat cd5e3b7a35 Merge pull request #5701 from dotCooCoo/fix/glyph-cluster-ligature
Glyphs: a consonant's ligature in a cluster with its vowel sign is its glyph
2026-11-05 05:32:35 +07:00
dotCooCoo 92ecb4a6b0 Glyphs: a consonant's ligature in a cluster with its vowel sign is its glyph
Ka with a nukta and the vowel sign I is shaped as one cluster: the vowel
sign's glyph, then the ligature of Ka and the nukta. The ligature is
neither Ka's default glyph nor an alternate, so the lookup found no
glyph for Ka: text.glyphAlternates reported the vowel sign's glyph and
text.setGlyphAlternate reported Ka's default glyph. When the cluster
holds neither, the lookup now takes the first glyph that is not drawn
for another character of the grapheme.
2026-11-05 05:20:03 +07:00
BFlat 961b28d420 Merge pull request #5700 from dotCooCoo/fix/layers-thumbs-long-paths-off-thread
Layers: thumbnails of long paths are drawn off the UI thread
2026-11-05 05:19:48 +07:00
dotCooCoo 570296b97a Layers: the painted thumbnail's comment states the per-frame cost of a long path
Measuring a long path's bounds takes time in proportion to its anchor count on every frame; it does not cost as much as rendering the path.
2026-11-05 05:10:41 +07:00
dotCooCoo b48c62f0cf Layers: area type and type on a path count their anchors toward the thumbnail limit
The first thumbnail of a row goes to the thumbnail thread when its paths hold
more than 2,000 anchors in all. The count now also takes in the frame of area
type and the path of type on a path, so a long frame or path no longer renders
on the UI thread, and the painted fallback thumbnail leaves such a row out.
2026-11-05 05:10:41 +07:00
dotCooCoo e73f51debc Layers: thumbnails of long paths are drawn off the UI thread
The Layers panel chose between the UI thread and the thumbnail thread
for a row's first thumbnail by counting objects only, so one path of
200,000 anchors was rendered on the UI thread. A row whose paths hold
more than 2,000 anchors in all now goes to the thumbnail thread, as a
row of more than 200 objects already did.

While such a row waits, the painted fallback thumbnail draws nothing
for it instead of measuring the path's bounds on every frame.

Opening the panel on one path of 200,000 anchors: the first frame takes
2.4 ms instead of 393 ms (release build, median of three runs).
2026-11-05 05:10:41 +07:00
BFlat efbdd6f40a Merge pull request #5677 from sadkotheguest/feat/find-font-replacement-rows
Find Font: Replace With Font lists family and style rows
2026-11-05 05:10:07 +07:00
Alex 31f05d3545 Find Font: a stand-in style is never the target, and the closest style comes from the catalog
A document font drawn in a substitute style stayed listed and could be chosen as the
replacement: the family is installed, so the old check passed, and Change sent the
unavailable style to text.replaceFont, which stored it and warned that the text is drawn
in a stand-in. The exact face now decides both the automatic choice and whether Change
is available; the row stays listed.

Listing System asked the font database for each family's style-less face, and for a
family not loaded yet that reads all its cataloged files, so opening the dialog could
load a whole font library. FontDb::closest_style answers from the catalog's weight and
slant instead, and the rows put that style first as before.

Tests: the dialog's 26 (the closest style is the face a style-less request resolves to,
a stand-in style is listed but not chosen) and the text crate's 474.
2026-11-05 04:47:34 +07:00
Alex 33cd7ef21a Find Font: every row names its style, and the search uses the catalog's label
The rows repeated each family: a nameless "closest style" row came first and then
the same style again with its name. A family now lists each of its styles once,
the closest style first under its own name, so every row reads "Family Style" as
the reference app's list does.

The search field used a label no translation catalog has; it now uses the font
menu's "Search", so the two fields read the same in every language.
2026-11-05 04:47:34 +07:00
Alex a53b58d7b7 Find Font: a new source picks a listed row, and confirming forgets the search
Switching Replace With Font From kept a family and style the new list might not
show, so Change could replace with a closest style of its own choosing instead of
a row on screen; the family and the style now both come from a row the list
shows, and the chosen font stays only when that exact pair is one of them.

Confirming the dialog with Enter closed it without clearing the search the Done
and Escape paths clear; all three now share the same cleanup.

Tests: a new source picks a row its list shows (family and style), and confirming
the dialog clears the replacement search.
2026-11-05 04:47:34 +07:00
Alex ad7cd71c39 Find Font: Replace With Font lists family and style rows
Choosing a replacement meant picking the family from one drop-down and its style
from another, and only then seeing what the style would be. Replace With Font now
lists rows of a family and one of its styles - the family's closest style first,
then each style it has - and one click sets both. A search above the list filters
the rows by family and style, as the Character panel's font menu searches.

Each list scrolls on its own: the document's fonts and the replacement rows keep
separate scroll states and scrollbars, and the replacement list is ten rows tall
at most, no taller than the rows it lists.

Review: the rows are rebuilt after a replacement, the search clears when the
dialog closes, and a double-click respects the same guard as the Change buttons;
the type-parity checklist has its status line and revision row.

Closes #4972.
2026-11-05 04:47:33 +07:00
BFlat c24c03c414 Merge pull request #5695 from FractalEncrypt/fix/transform-each-relative-absolute
Transform Each: Relative and Absolute scaling, the scale link, and the documented panel order
2026-11-05 04:38:19 +07:00
FractalEncrypt 77a86288e7 Transform Each: Relative and Absolute scaling, the scale link, and the documented panel order
Squashed from #5695 onto main.
2026-11-05 04:37:46 +07:00
BFlat b33d3e72f2 Merge pull request #5696 from dotCooCoo/fix/external-plain-text-paste-style
Use destination style for external same-text paste on Windows
2026-11-05 04:36:56 +07:00
BFlat 4ea3fc91b8 Merge pull request #5697 from dotCooCoo/fix/glyph-alternates-own-glyph
Glyphs: the Glyphs query reports a consonant's glyph, not its vowel sign's
2026-11-05 04:36:39 +07:00
dotCooCoo 8555ac42c4 UI-1661: Use Windows clipboard ownership for Type paste 2026-11-05 04:11:40 +07:00
dotCooCoo 67140d5e54 Glyphs: the Glyphs query reports a consonant's glyph, not its vowel sign's
text.glyphAlternates reported the first glyph of the character's
cluster as the chosen glyph. A vowel sign drawn before its consonant
(Devanagari Ka with the vowel sign I) comes first in that cluster, so
the query reported the vowel sign's glyph for Ka, while
text.setGlyphAlternate reported Ka's own glyph. Both commands now take
the character's glyph by the same rule, and so does the check in
text.setGlyphAlternate that the chosen glyph is drawn.

A character that overflows its area is shaped with its grapheme, and
each character of the grapheme now keeps its own style there, so a
nukta of another size no longer makes a ligature with its consonant
that the laid-out text does not draw. A test font with a Devanagari
nukta ligature checks both.
2026-11-05 04:11:37 +07:00
BFlat d8d86173e4 Merge pull request #5692 from dotCooCoo/fix/appearance-active-edits-each-target
Appearance: edits to the active row reach the matching row in each selected object
2026-11-05 04:09:08 +07:00
BFlat 4fd9df5898 Merge pull request #5691 from dotCooCoo/fix/perceptual-pdf-help-rgb-destination
Gradient: Perceptual PDF help says an RGB destination keeps RGB stops in RGB
2026-11-05 04:08:50 +07:00
BFlat 6a8c57ac0f Merge pull request #5690 from dotCooCoo/fix/render-long-path-frame
Render: a long dashed stroke is outlined only near the view after an edit
2026-11-05 04:08:33 +07:00
dotCooCoo af74427ef4 Appearance: edits to the active row reach the matching row in each selected object
Paint and effect edits matched the active row across all selected
objects at once, so one object without an equal row made every object
edit the row at the raw index. ItemTarget::per_target and
effect_targets now match each object against the first one on its own,
as items_per_target does. effect_targets also looks up the row in the
stack it belongs to when deciding whether an effect edit is
object-level, so an effect on a row that sits lower in another object's
stack no longer fails with "no appearance item".
2026-11-05 03:55:55 +07:00
dotCooCoo 0f99ca1895 Gradient: Perceptual PDF help says an RGB destination keeps RGB stops in RGB
The `method` text of `paint.editGradient` and two passages of
docs/mcp.md said a CMYK document writes RGB and Lab gradient stops in
CMYK. An export that converts colors to an RGB destination profile
converts them to that profile first, so in a CMYK document they stay
RGB and a Perceptual spot gradient with such stops is written as an
image with the spot warning. The texts now say so, and the test of RGB
stops in a CMYK document also checks the RGB destination case.
2026-11-05 03:55:52 +07:00
dotCooCoo b742c77596 Render: the dashed-stroke edit test moves a line anchor in the view
Anchor 1500 of the test zigzag is a curve, so the first edit left the path
unchanged. The test now moves anchor 1501, a line in the view, and fails
when either anchor it moves is not a line.
2026-11-05 03:55:48 +07:00
dotCooCoo ed3efcbd7d Render: a long dashed stroke is outlined only near the view after an edit
A dashed stroke of a path with more than 1,024 path elements was outlined
whole in every frame after an edit. It is now outlined in and around the
view like a solid stroke. dash_kept runs the pattern along every segment
and keeps only the dashes and dots on the segments near the view, so they
sit where dashing the whole path puts them and the pixels are the same.
A pattern too fine to draw is still drawn solid, and that solid stroke is
cropped too.

At 1600 x 1200 the frame after one anchor moves takes 14 ms instead of
102 ms on a filled closed path of 50,000 anchors with 6 pt dashes and
4 pt gaps. anchor_bench now includes that document.
2026-11-05 03:55:48 +07:00
BFlat a2e98ac57c Merge pull request #5686 from ruchirguitar/fix/5336-optical-kerning-reference-gap
Fix #5336: Optical kerning spaces pairs by the font's own stem spacing
2026-11-05 03:30:17 +07:00
BFlat b402013f06 Merge pull request #5685 from dotCooCoo/fix/quick-bounds-nested-nine-slice
Symbols: quick boxes hold art that nested 9-slice guides move
2026-11-05 03:30:00 +07:00
BFlat 34b1fc9f42 Merge pull request #5681 from dotCooCoo/feat/recovery-folder-choose
Preferences: Data Recovery's Folder gets a Choose button
2026-11-05 03:29:45 +07:00
BFlat 23f56b6cfa Merge pull request #5682 from dotCooCoo/fix/share-of-thousands-comma
Numeric fields: 1,000% in a size field is refused
2026-11-05 03:29:20 +07:00
ruchirguitarandClaude Opus 5.5 b87fddba11 Type: Optical kerning spaces pairs by the font's own stem spacing (#5336)
Optical moved every pair all the way to a fixed 0.08 em ink gap, tighter
than most text faces' own stem spacing, so ordinary text was squeezed
(at 100 pt, Source Sans 3 `nn` closed from 54.7 to 46.9 pt).

Each pair now moves 0.6 of the way toward the face's reference gap (the
median ink gap of `nn`, else `HH` or `oo`; 0.08 em when it has none),
keeps at least 0.3 of it at its narrowest, and changes by at most
0.12 em tighter or 0.06 em looser. Ideographs, kana, hangul, fullwidth
and CJK punctuation forms and dashes keep the font's spacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:04:51 +07:00
dotCooCoo 84c197cfbe Symbols: quick boxes hold art that nested 9-slice guides move
An instance of a symbol whose art holds a 9-slice instance draws that
instance through its guides with the transform of every instance above
it. When the outer symbol has no guides, the quick box of an outer
instance that scales or turns its art was its art box placed by its
transform, and the renderer could cull art the nested guides move past
that box.

Symbol art boxes now record whether the art holds an instance of a
symbol with 9-slice guides at any depth. For such art, quick boxes
place the art by the instance's transform and measure it, once for each
symbol and linear part. One set of boxes places at most
MAX_SYMBOL_NODES objects this way, the art of opacity masks included.
2026-11-05 03:04:48 +07:00
dotCooCoo 08aa511252 Preferences: a recovery folder that can't be listed is refused
Data Recovery lists the folders in its recovery folder before it starts, so a
folder that can be written but not listed (a Unix folder with mode 0333) left
it unable to start. Choose... now lists the picked folder as well as making a
folder in it, and refuses it with a status message when either fails. The
progress line says what is checked.
2026-11-05 03:04:39 +07:00
dotCooCoo 13d0cbc51f Preferences: the recovery folder check makes a folder, and a pick survives a closed document
The Choose... check now creates and removes an empty folder in the picked
folder, as the recovery store makes its own folder there, and reports a
folder it cannot remove again. The folder picker's answer fills the open
Preferences dialog even when the active document closed while the picker
was open, since the recovery folder is an app preference.
2026-11-05 03:04:39 +07:00
dotCooCoo 9613bfb31d Preferences: Data Recovery's Folder gets a Choose button
File Handling > Data Recovery > Folder has a Choose... button on the
desktop that opens the folder picker and puts the chosen folder in the
field. Cancel leaves the field as it was, and the field stays editable.
A chosen folder that no file can be created in is refused with a status
message. The web build has no folder picker, so the button is not shown
there.
2026-11-05 03:04:39 +07:00
dotCooCoo 3e96d59fd8 Numeric fields: 1,000% in a size field is refused
A percentage typed alone in a size field (W, H and the other fields
that take a share of their value) read its decimal comma before the
`%` was dropped. The thousands check then saw `1,000%`, and the comma
was read as a decimal point, so `1,000%` set the size to 1% of its
value. `share_of` now reads the decimal comma after dropping the `%`:
`1,000%` is refused, and `12,5%` still reads as 12.5%.
2026-11-05 03:04:35 +07:00
BFlat 58b445f27a Merge pull request #5683 from storytold/fix/planar-faces-cover-regions
Pathfinder: holes that touch at a point stay holes of their region
2026-11-05 03:02:21 +07:00
bflatastic b814fcf689 Pathfinder: holes that touch at a point stay holes of their region
The Live Paint faces were right; the Pathfinder regions over-counted.
Two small circles inside a big one, touching each other at one point,
gave the big circle's region only one of its two holes, and the other
hole came out as a region of its own.

linesweeper walks the boundary of the two touching holes as one loop
and, when the walk comes back to the shared point, cuts the second
hole off as a child of the first and flags it outer, as if it were an
island inside it. face_groups trusted that flag, so the hole started a
face. The same cut goes wrong the other way when an outer contour
touches its own hole: the outer contour comes out as an island inside
the hole.

Such a loop winds the same way as the contour it was cut from, against
its flag. face_groups now checks every contour's flag against its
winding; a pass where any disagrees is grouped by geometry instead
(each hole goes with the smallest outer contour of its mask around it).
Passes where they all agree keep the sweep's tree as before, at the
cost of one area per contour.

Adds deterministic tests for both shapes of the bug and checks in the
proptest regression seed.
2026-11-05 03:02:03 +07:00
BFlat 959662fde6 Merge pull request #5680 from dotCooCoo/fix/styled-text-paste-after-edit
Preserve styled text after copying a Type range
2026-11-05 02:38:33 +07:00
BFlat b145264150 Merge pull request #5679 from dotCooCoo/fix/place-tests-scan-handshake
Tests: the link-watch and place tests wait for the link scan instead of sleeping
2026-11-05 02:38:18 +07:00
BFlat bc54bbd594 Merge pull request #5678 from MMexicann/fix/xtask-prerelease-leading-zeros
Tooling: reject numeric prerelease leading zeros
2026-11-05 02:38:02 +07:00
BFlat df1cab1403 Merge pull request #5676 from dotCooCoo/fix/vertical-overflow-glyph-probe
Glyphs: overflowing vertical type keeps a chosen default glyph
2026-11-05 02:37:47 +07:00
dotCooCoo cd71257ac4 UI-5636: Preserve styled Type-range copies on canvas paste 2026-11-05 02:23:51 +07:00
dotCooCoo a5157654f4 Tests: the link-watch and place tests wait for the link scan instead of sleeping
The link-watch worker now signals a condition variable when it ends,
whether it stored its stamps or unwound, and Session::wait_link_scan
blocks until then. The place tests' idle helper waits on it once per look
in place of a 5 ms sleep under a 20 s wall-clock bound, and the engine's
link-watch and link-path tests wait on it in place of spinning under a
10 s bound. The link-watch tests date a rewritten file 2 s after the one
it replaces instead of sleeping 30 ms before writing it.
2026-11-05 02:23:48 +07:00