From ce1e1a783b2e632deaf74bb431dfdc975597fed0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:29:18 -0400 Subject: [PATCH] =?UTF-8?q?fix(book):=20chain=20walk=20dropped=20frames=20?= =?UTF-8?q?past=202=20=E2=80=94=20separate=20seen=20sets=20per=20direction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _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 --- src/index.html | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/index.html b/src/index.html index 0631323..3bd6937 100644 --- a/src/index.html +++ b/src/index.html @@ -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; }