docs/gaps.md now lists the same cases as docs/architecture.md and
reuse::plain. A doc comment on rejoin says that a run style an edit uses
for the first time stops the reuse until the old composition reaches the
same style at the same index.
Every edit of a story composed it from its first paragraph, so a typed
character in a long threaded story cost as much as laying out the whole
story: 456 ms in a story threaded over 300 pages.
A composition that lays every paragraph once keeps its state before each
paragraph. After an edit, composing starts one paragraph above the first
changed one, in the kept state and with the lines above copied. It stops
at the first unchanged paragraph it reaches in the state kept for that
paragraph, and copies the lines from there on with their story offsets
moved by the length of the edit.
- The cache does this when only the story differs from its entry.
- Composed whole as before: stories with footnotes, endnotes, tables,
anchored objects, named lists, tracked deletions, balanced columns,
vertical justification other than Top, column rules, type on a path,
spanning or split paragraphs, paragraph borders or shading, and a
layout that goes back over a paragraph (keep options).
- An edit that moves lines into other frames is composed to the end of
the story.
In a 240-paragraph story a typed character composes 3 paragraphs
(240 before).
Docs: docs/architecture.md, docs/gaps.md (gap 20), ROADMAP.
Refs #2298
A clipped contour now looks only at the edges of the outline and its clips
that reach a line band, and charges its work budget for those alone. An
outline with thousands of points elsewhere no longer exhausts the budget
and falls back to the projections' common part. The Text wrap row of
docs/typography-parity.md notes that an image over 40 megapixels, or one
that can't be decoded, follows the frame's path.
* fix(engine): index lookups that were quadratic in a document's styles, tags, swatches and runs
* address review: first swatch per name in book sync; bound spelling input before collecting
* address review: keep the last source style per name in book sync; bound GREP replacement bytes while generating them
* address review: write GREP replacements into a byte-bounded buffer; gate keep_last test off wasm
* merge main into fix/sec-engine-linear-lookups
* address review: make the suggestion length-cap test deterministic
Center, Bottom and Justify moved the lines of a shaped frame without
testing them against the outline, so in a frame that narrows towards the
bottom a line set in a wide band ended up crossing the outline. Each moved
line's slot is now tested against the outline over its new band, as when
the line was fitted, and the text goes only as far down as every slot
fits. In a frame that narrows and widens again, the search scans the
rooms from the most down (every point, at most 512 steps) before halving,
so the text can still reach a lower part past a narrow waist.
- The status bar and the Preflight panel show a gray light and "Checking" until the active
document's first check ends, instead of a green "No errors".
- The cross-reference and running-header indexes are keyed on the baseline grid and the font
scope as well, so a change to either builds them again, in a cache and in a cache over it.
- The test helper that waits for the preflight worker fails when the worker ends without a result.
Opaque pixels outside the placed graphic's clipping path (inverted or not) are not drawn, and they no longer push text aside. An outline that only touches a clip's edge blocks nothing.
- each traced Alpha Channel outline has a serial number, and the composition signatures include the outlines in use, so a relink or link update that changes only the image data no longer leaves text laid out around the old image
- a wrap with the default alpha threshold is saved without it, as before thresholds were stored
- docs/typography-parity.md says Alpha Channel contours follow the image's visible pixels
- The composition cache compares each Alpha Channel outline by a serial
number, so a relink or link update lays the text out again around the
new image.
- A contour clipped by its frame blocks only the part of the outline the
frame shows, even where the two share x positions in one line band.
A fragmented frame, or shapes too detailed for a line's budget, keep
the intersection of the two projections.
- An anchored frame whose wrap is set only on its placed graphic wraps
text.
- The default alpha threshold is not saved.
- docs/typography-parity.md describes the Alpha Channel contour.
A contour's zero-width spans (a vertical open path) are kept until the
wrap offset widens them, so text no longer crosses such a path. A record
previewed into an image placeholder gets the image's Alpha Channel
outline, as an edit would.
An outline with more path elements than composition follows is traced on
a coarser grid, so a fragmented image keeps an Alpha Channel contour
instead of falling back to the frame. An image with no pixel more opaque
than the threshold leaves the text alone.
The worker checks the document state it was given, which is often one
edit behind the interface. It composed that older state into the shared
composition cache, replacing the interface thread's composition of the
current state, so the next frame composed the edited story again (about
14 ms per edit for a 10-page story).
Cache::over makes a cache that uses another cache's compositions of the
same state and keeps whatever it composes to itself. The worker checks
with one over the interface thread's cache, keeps it for the next check
of the same document, and drops it after a minute without jobs.
With no active document, the job waiting for a busy worker is dropped,
so it no longer keeps a closed document in memory until another
document opens.
Bottom and Justify now put the last baseline on the bottom inset in a
frame with a corner radius as in a rectangular one, so a radius no longer
moves the text when the bottom inset is above 0. Center puts the last
baseline halfway between where Top sets it and the bottom inset even when
the frame is too short for the descenders under it, so Cap Height text in
a tight frame is centered. The last line's descenders hang below its
baseline, into the bottom inset and past the frame's edge when the inset
is smaller than the descent. An earlier line whose descenders reach below
the last baseline (tight leading) is not moved past the text area. Above
footnotes and in vertical frames the descenders still stay inside the
text area.
Vertical justification measured the free space down to the bottom of the
last line's descenders, so Center set text too high (most visibly with a
Cap Height first baseline) and Bottom and Justify left the descender depth
empty. The last baseline is now the bottom of the text: Bottom and Justify
put it on the bottom inset, and Center leaves as much room below it as
above the first line. The descenders may reach into the bottom inset but
not past the frame's edge; above footnotes, in vertical frames and in
shaped frames they stay inside the text area. Tables keep their bottom.
A shaped frame no longer tests the part of a line above its text area
against the outline, so a small corner radius leaves a Cap Height first
line where a rectangular frame puts it.
Fixes#1999
placed::locate only looked for an anchored frame in host frames that sit
on a spread. When the host frame was itself anchored, the inner frame got
no placement and composed where it is stored, on no page. The host frame
is now placed through its own host first, up to MAX_DEPTH hosts out.
Copying, pasting, deleting or releasing an anchored text frame left a
document that Document::check rejects, so once saved it could not be
opened again.
- Copy and paste: copied text takes the stories of its anchored text
frames along. Pasting text (also into another document) gives the
anchored copies fresh ids and stories of their own
(Document::adopt_anchored), and the text clipboard keeps a copy of the
source document holding only those stories and the graphics the text
and they place, which come along under the destination's asset ids
(Document::adopt_anchored_from). A story shown by frames in several
copied stories is copied once per paste and threads all their copies, as
the original does, so shared stories don't multiply. Copied frames
(clip_doc) keep the stories of their anchored frames too.
- Document::fix_anchored_text_frames, run after edits and for new
documents: copies with taken ids get fresh ones and stories of their own,
deleted anchored frames leave their threads and their stories go with
them, and a frame whose story is missing gets an empty one.
- Release threads the released frame, under its new id, to its story.
- The generated endnote story is set before the repair, and again when the
repair changed anything: text with an anchored frame pasted into it is
replaced, and the repair copies and removes stories with their endnotes.
Duplicating and pasting frames already give anchored items fresh ids and
story copies on main (#1522); this covers text copied with them, deletion
and release.
* warichu: split long runs in one pass instead of trying every length
The exact split tries every line length for every start, so its work grew with
the square of the run's length, and a document can set warichu on a run of any
length. Runs longer than 512 glyphs are now split where the running advance
reaches each line's share, keeping the break minimums. The glyphs drawn with the
run find their anchor by binary search instead of a scan per glyph.
* address review: warichu split picks the break closest to the target
The close-up test checked the half-em line-end set only; the other Japanese
base set closes up the same pairs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* hyphenation: linear heuristic, cache misses outside the lock
The syllable heuristic built each word suffix as a String to test it, which is
quadratic in the word's length; it now uses prefix sums of vowels and UTF-8
lengths. Mapping a word's break points to glyphs scanned the word for each
point; both are indexed once per word. A word not in the hyphenation cache is
worked out without holding the cache's global mutex.
* hyphenation: compare the word ending with ends_with
A cached story copied every wrap neighbour's whole signature and item list into
its entry; stories on one spread share its wrapping items, so entries grew with
all of theirs together. A neighbour's signature now goes in as its hash and its
items are kept once each.
Anchored-frame placement asked for the host's composition once per frame, and
again at every level a host was reached from. Each host is now asked for once
per placement pass, and the cache and the uncached path remember the hosts they
resolved for the whole traversal.
- Footnote references on a line are deduplicated with a set instead of
searching the references collected so far.
- Nested styles stream the paragraph up to their delimiter with one character
of lookahead instead of copying the rest of the paragraph for each style,
and a delimiter's characters are indexed once per style.
- Shaping resolves which nested/GREP style is on top with one sweep over the
overlay boundaries instead of scanning every overlay for each stretch.
- Column rules group crossing lines by paragraph through an index.
- Table fragments index each row's top once instead of searching the rows
for every cell.
The drop cap is set at the indent and the list number beside it, so the
number's character is the later one on the line and sets the indent.
Pin that with a test, and say so where the character is looked up.
The character was looked up among the line's body glyphs, so one inside
the drop cap was missed, and one in a list number's ^i matched the first
body glyph (after the label's tab) instead of itself. Find it among the
placed glyphs, which include the drop cap's and the label's.
The lines after an Indent to Here start at the character, which already sits past the drop cap, but the lines beside the drop cap still added its width on top. They no longer do. Adds tests for the drop cap, for Indent to Here after a tab (the usual hanging number) and for a right-to-left paragraph, and documents how the indent behaves beside a text wrap.