feat(spreadsheet): C 격자 — 행·열 묶어 접기(윤곽 +/- 단추, 계약 더함)

RowInfo·ColumnInfo.묶음{층,접힘} · Command「묶음설정」더함(계약 더함) — 접힘 상태 저장 ·
접으면 숨김처럼 그리되 머리 이중선 표시는 숨김·걸러짐과 갈라 안 보임(따로 표시).
commands.ts 는 기존 setAxis 틀에 「묶음」prop·case 만 최소로 얹음(exhaustive switch 깨짐 막음).
ORCA 시험 틀에서 접기·펴기 dispatch·저장·머리 이동·표시 구분 수치로 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVM3xZ1aUPcUTuW2WZSnwg
This commit is contained in:
2026-09-27 21:31:11 +09:00
co-authored by Claude Sonnet 5
parent 7aa9f2229d
commit ebdee04faa
6 changed files with 195 additions and 10 deletions
+31 -2
View File
@@ -105,7 +105,7 @@ export function buildColGeometry(book: Workbook, sheet: Sheet): LineGeometry {
for (const [key, info] of Object.entries(sheet.열 ?? {})) {
const c = colIndex(key);
if (c < 0) continue;
if (info.숨김) overrides.set(c, 0);
if (info.숨김 || info.묶음?.접힘) overrides.set(c, 0);
else if (info.폭 !== undefined) overrides.set(c, colCharsToPx(info.폭));
}
return new SparseGeometry(base, overrides);
@@ -117,12 +117,41 @@ export function buildRowGeometry(book: Workbook, sheet: Sheet): LineGeometry {
for (const [key, info] of Object.entries(sheet.행 ?? {}) as [string, RowInfo][]) {
const r = Number(key) - 1;
if (!Number.isInteger(r) || r < 0) continue;
if (info.숨김) overrides.set(r, 0);
if (info.숨김 || info.걸러짐 || info.묶음?.접힘) overrides.set(r, 0);
else if (info.높이 !== undefined) overrides.set(r, ptToPx(info.높이));
}
return new SparseGeometry(base, overrides);
}
/** 접힌 윤곽 묶음(연속 자리) — 묶음 있는 자리를 모아 붙은 것끼리 한 덩이로(같은 접힘 값끼리만). */
export interface OutlineRun {
from: number;
to: number;
collapsed: boolean;
}
export function buildOutlineRuns<T extends { 묶음?: { 층: number; 접힘?: boolean } }>(
records: Record<string, T> | undefined,
indexOf: (key: string) => number,
): OutlineRun[] {
const items = Object.entries(records ?? {})
.map(([key, info]) => ({ i: indexOf(key), collapsed: !!info.묶음?.접힘, has: !!info.묶음 }))
.filter((x) => x.has && x.i >= 0)
.sort((a, b) => a.i - b.i);
const runs: OutlineRun[] = [];
for (const it of items) {
const last = runs[runs.length - 1];
if (last && last.to === it.i - 1 && last.collapsed === it.collapsed) last.to = it.i;
else runs.push({ from: it.i, to: it.i, collapsed: it.collapsed });
}
return runs;
}
/** 그 자리가 숨김 · 걸러짐(사용자 · 필터) 인가 — 묶음 접힘은 빼고(화면 표시가 다름). */
export function isExplicitHidden(info: { 숨김?: boolean; 걸러짐?: boolean } | undefined): boolean {
return !!info?.숨김 || !!info?.걸러짐;
}
/** `from..to` 안에서 숨기지 않은 자리만. */
export function visibleIndices(geo: LineGeometry, from: number, to: number): number[] {
const list: number[] = [];