// Justified 레이아웃 — 그리디 행 채우기. 메인 스레드 동기 계산. // (프로덕션 Tauri 빌드에서 module worker가 첫 메시지 후 응답하지 않는 이슈를 피하기 위해 // 워커 대신 동기 계산을 쓴다. 재계산은 리사이즈/줌/데이터 변경 시에만 발생한다.) // // 워커로 옮길 이유가 없다 — V8 실측: 1만 0.8ms / 10만 3.1ms / **100만 18.4ms** // (aspectRatios 언팩 포함 ~20ms). 스크롤 중에는 아예 돌지 않고, 워커로 보내면 // boxes 15MB를 매번 되돌려 받아야 해서 오히려 느려진다. export interface LayoutResult { /** n*4: [x, y, w, h] */ boxes: Float32Array; /** 행별 상단 y (마지막 요소 = 전체 높이) */ rowTops: Float32Array; /** 행별 첫 항목 인덱스 */ rowFirst: Uint32Array; totalHeight: number; } export function computeLayout( ratios: Float32Array, containerWidth: number, targetRowHeight: number, gap: number, ): LayoutResult { const n = ratios.length; const boxes = new Float32Array(n * 4); const rowTopsArr: number[] = []; const rowFirstArr: number[] = []; const clamp = (r: number) => Math.min(4, Math.max(0.25, r || 1)); let y = 0; let rowStart = 0; let ratioSum = 0; const flushRow = (end: number, justify: boolean) => { const count = end - rowStart; if (count <= 0) return; const gaps = gap * (count - 1); let h = targetRowHeight; if (justify) { h = (containerWidth - gaps) / ratioSum; } else { const natural = ratioSum * targetRowHeight + gaps; if (natural > containerWidth) h = (containerWidth - gaps) / ratioSum; } let x = 0; for (let i = rowStart; i < end; i++) { const w = clamp(ratios[i]) * h; boxes[i * 4] = x; boxes[i * 4 + 1] = y; boxes[i * 4 + 2] = w; boxes[i * 4 + 3] = h; x += w + gap; } rowTopsArr.push(y); rowFirstArr.push(rowStart); y += h + gap; rowStart = end; ratioSum = 0; }; for (let i = 0; i < n; i++) { ratioSum += clamp(ratios[i]); const width = ratioSum * targetRowHeight + gap * (i - rowStart); if (width >= containerWidth) { flushRow(i + 1, true); } } flushRow(n, false); const totalHeight = y > 0 ? y - gap : 0; rowTopsArr.push(totalHeight); // 마지막 = 전체 높이 (이진탐색 경계) return { boxes, rowTops: new Float32Array(rowTopsArr), rowFirst: new Uint32Array(rowFirstArr), totalHeight, }; }