From fc3f46c302d3fd5875c4d91343c6b69852ded2ba Mon Sep 17 00:00:00 2001 From: 3dtours Date: Fri, 9 Oct 2026 22:17:21 +0700 Subject: [PATCH] fix(library): keep the wall row step, so its window cannot chase its own tail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting thumbs or flipping SINGLE <-> ALL THUMBS on a roll whose captions wrap to different heights crashed the shelf with React #185 ("Maximum update depth exceeded") and left the screen blank. useWallWindow read the step from one row to the next off the rows the window itself had drawn, and the average of a run of rows moves as the run grows: `to` traded places with a sum a row away — 35, 40, 35, 40, three milliseconds apart — every pass a render, until React gave up. The step is taken once per column count and kept, re-read when the wall re-columns, which is the only thing that changes a card own height (a caption wraps at the width it is given). The window is a function of the scroll and nothing else, so it settles. Reproduced with the storm probe against a 3000-frame roll of unevenly named frames: before, blank at round 1 with #185; after, 12 rounds of hard SINGLE/ALL THUMBS toggling plus a round trip through the studio — no error, no blank screen, and D18 unchanged (tile CHANGED, the shelf raising the studio frame 4px off centre, the studio link opening the same frame). --- docker/frontend/src/Library.tsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index d88d0cd..fff2e1a 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -3070,6 +3070,12 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject void>(() => {}); + // The step from one row to the next, taken once per column count and kept. It + // is a ref rather than a value read on every pass because the pass reads it off + // the rows the window drew — and a step that moves with the window is a window + // that moves with the step: the two trade places forever (see `measure`). + const pitchRef = useRef(0); + const colsRef = useRef(0); // The window over a scroll container is taken rather than kept, so it is taken // after every render: the first pass draws a screenful, the next reads the row's // step off it and lays out the spacers, and the pass after that leaves both @@ -3108,7 +3114,23 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject 0) { + colsRef.current = cols; + pitchRef.current = drawn; + } + } + const pitch = pitchRef.current; if (!(pitch > 0)) return; const rows = Math.ceil(count / cols); const row0 = Math.max(0, Math.floor(el.scrollTop / pitch) - WALL_ROWS);