fix(book): chain walk dropped frames past 2 — separate seen sets per direction

_bookCollectThreadChain shared a single 'seen' set between the
backward and forward walks. For a chain A → B → C started from C,
the backward walk visited B then A, leaving seen = {B.id, A.id}.
The forward walk from head A then tried to push B, but found B in
seen and broke immediately — returning chain [A] instead of
[A, B, C].

Symptom: cross-page flow worked for any 2-frame chain (forward
walk only had one node to visit), but broke as soon as the chain
hit 3 frames. The user repro 'A on page 1 → B on page 1 → C on
page 2 doesn't flow' matched exactly.

Fix: use separate seen sets per walk direction. Each starts with
the relevant endpoint (layer for backward, head for forward) and
detects same-direction cycles without bleeding into the other.

Affects every consumer of _bookCollectThreadChain: editor canvas
text rendering (_attachBookFramePorts), tile thumbnails
(renderBookDesignedPageToCanvas via the J4.c.2 hook), PDF export
(same path), and the text-frame inspector's chain / line-count
diagnostic.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-06-04 22:29:18 -04:00
1 parent cbfed8702b
commit ce1e1a783b
1 file changed
+14 -8
+14 -8
View File
@@ -23932,25 +23932,31 @@
function _bookCollectThreadChain(layer) {
// Walk backwards to the head, then forward to the tail.
// Returns layers in flow order (head → ... → tail).
const seen = new Set();
// Backwards: find the layer whose nextFrame.frameId equals
// current.frameId; loop until none exists.
//
// Bug fix: each walk needs its OWN seen-set. Sharing seen
// between backward and forward walks meant the forward walk
// tripped its cycle guard on the middle frames the backward
// walk had already visited. A → B → C starting from C would
// walk back through B + A (seen={B,A}), then the forward walk
// from A would find B in `seen` and break — returning chain
// [A] instead of [A, B, C].
let head = layer;
const seenBack = new Set([layer.id]);
let guard = 0;
while (guard++ < 1000) {
const prev = _findBookFrameLayer(o => o !== head && o.nextFrame?.frameId === head.frameId);
if (!prev || seen.has(prev.id)) break;
seen.add(prev.id);
if (!prev || seenBack.has(prev.id)) break;
seenBack.add(prev.id);
head = prev;
}
const chain = [head];
seen.add(head.id);
const seenFwd = new Set([head.id]);
let cursor = head;
guard = 0;
while (cursor.nextFrame?.frameId && guard++ < 1000) {
const nxt = _findBookFrameLayer(o => o.frameId === cursor.nextFrame.frameId);
if (!nxt || seen.has(nxt.id)) break;
seen.add(nxt.id);
if (!nxt || seenFwd.has(nxt.id)) break;
seenFwd.add(nxt.id);
chain.push(nxt);
cursor = nxt;
}