@@ -42,9 +42,12 @@ import path from 'node:path';
4242
4343// Bump when the bake recipe changes (capture geometry, timing, encoder, waits…)
4444// so every plugin re-bakes even though its page content is byte-identical.
45- // v2: deck mode (16:9 + slide-walk for fixed-viewport PPT/slideshow pages) +
45+ // v2: deck mode (slide-walk for fixed-viewport PPT/slideshow pages) +
4646// force-load webfonts before capture (CJK templates were baking tofu).
47- const BAKE_VERSION = 2 ;
47+ // v3: bound the slide-walk — cap deckSignal's DOM scan + a wall-time cap — so a
48+ // deck that renders every slide in one giant rail (#deck{width:10000vw})
49+ // no longer drags the walk, and the clip, out to 20s+ in CI.
50+ const BAKE_VERSION = 3 ;
4851
4952// ---- config ---------------------------------------------------------------
5053const BASE_URL = process . env . BASE_URL || 'http://127.0.0.1:17579' ;
@@ -59,6 +62,13 @@ const DECK_W = 1760;
5962const DECK_H = 1344 ; // 1760/1344 = 1.31, the tile aspect
6063const SLIDE_MS = 1150 ; // deck per-slide dwell: ~.9s CSS transition + settle
6164const MAX_SLIDES = 6 ; // advance budget; HOLD + slides stays ~<=9s
65+ const MAX_WALK_MS = 8000 ; // hard wall-time cap on the walk: even when signal
66+ // reads are slow (huge-DOM decks), the clip stays
67+ // bounded (HOLD + walk ~<=10.5s)
68+ const DECK_SCAN_CAP = 600 ; // deckSignal scans only the first N elements — the
69+ // slide rail/track is a structural element near the
70+ // DOM top, so this stays cheap on decks that render
71+ // every slide in one giant rail
6272const OUT_W = Number ( process . env . PREVIEW_W || 640 ) ; // small — tile renders ~393px
6373const FPS = Number ( process . env . PREVIEW_FPS || 30 ) ; // 30 is smooth for a gentle pan, half the bytes of 60
6474const VELOCITY = 0.30 ; // px/ms — base pan pace (snappy but readable)
@@ -116,9 +126,16 @@ async function discoverIds() {
116126// offset of a horizontal track, and any active-slide marker. Deliberately does
117127// NOT read canvas pixels, so a continuously-animating WebGL background doesn't
118128// look like a slide change. Runs in the page; must stay self-contained.
119- function deckSignal ( ) {
129+ function deckSignal ( cap ) {
120130 const parts = [ location . hash ] ;
121- for ( const el of document . querySelectorAll ( '*' ) ) {
131+ // getBoundingClientRect + getComputedStyle force layout/style per element, so
132+ // scanning ALL of them is pathological on a deck that renders every slide in
133+ // one rail (thousands of nodes). The rail/track is structural and near the top
134+ // of the DOM, so the first `cap` elements in document order suffice.
135+ const els = document . querySelectorAll ( '*' ) ;
136+ const n = Math . min ( els . length , cap || 600 ) ;
137+ for ( let i = 0 ; i < n ; i += 1 ) {
138+ const el = els [ i ] ;
122139 const r = el . getBoundingClientRect ( ) ;
123140 if ( r . width > window . innerWidth * 1.5 || r . height > window . innerHeight * 1.5 ) {
124141 parts . push ( getComputedStyle ( el ) . transform ) ;
@@ -155,10 +172,11 @@ async function walkSlides(page, driver) {
155172 let moved = 0 ;
156173 const t0 = Date . now ( ) ;
157174 for ( let s = 0 ; s < MAX_SLIDES ; s += 1 ) {
158- const before = await page . evaluate ( deckSignal ) ;
175+ if ( Date . now ( ) - t0 > MAX_WALK_MS ) break ; // backstop so the clip never runs long
176+ const before = await page . evaluate ( deckSignal , DECK_SCAN_CAP ) ;
159177 await driveDeck ( page , driver ) ;
160178 await sleep ( SLIDE_MS ) ;
161- if ( ( await page . evaluate ( deckSignal ) ) === before ) break ; // reached the last slide
179+ if ( ( await page . evaluate ( deckSignal , DECK_SCAN_CAP ) ) === before ) break ; // last slide
162180 moved += 1 ;
163181 }
164182 return Math . max ( SLIDE_MS , Date . now ( ) - t0 ) ;
@@ -183,14 +201,14 @@ async function bakeOne(browser, id, hash) {
183201 // advances the deck, so decks reload to reset to slide 1 before capturing.
184202 let deckDriver = null ;
185203 {
186- const sig0 = await page . evaluate ( deckSignal ) ;
204+ const sig0 = await page . evaluate ( deckSignal , DECK_SCAN_CAP ) ;
187205 await driveDeck ( page , 'arrow' ) ;
188206 await sleep ( 900 ) ;
189- if ( ( await page . evaluate ( deckSignal ) ) !== sig0 ) deckDriver = 'arrow' ;
207+ if ( ( await page . evaluate ( deckSignal , DECK_SCAN_CAP ) ) !== sig0 ) deckDriver = 'arrow' ;
190208 else {
191209 await driveDeck ( page , 'wheel' ) ;
192210 await sleep ( 900 ) ;
193- if ( ( await page . evaluate ( deckSignal ) ) !== sig0 ) deckDriver = 'wheel' ;
211+ if ( ( await page . evaluate ( deckSignal , DECK_SCAN_CAP ) ) !== sig0 ) deckDriver = 'wheel' ;
194212 }
195213 }
196214 const vScrollable = await page . evaluate (
0 commit comments