Files
Aislo/A00_Common/spreadsheet/spreadsheet_figures.ts
T
eomsangdonandClaude Opus 5.5 fbd9721931 feat(534-1): 내장 엑셀 그림 겹판 · 쪽 나눔 점선 — 늦게 불러옴 · 행열 넣기 · 스크롤 따라감
- spreadsheet_figures: 시트 그림을 닻 범위에 얹음(figureSrc 없으면 이름 상자) · 쪽 나눔 행 아래 점선 · overlay 맨 아래 층
- spreadsheet: figureSrc 선택 · 그림 · 쪽 나눔 있는 문서일 때만 import()

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:40:31 +09:00

106 lines
4.1 KiB
TypeScript

/* =============================================================================
* spreadsheet_figures.ts (주인 P · 534 sub6)
* 그림 겹판 · 쪽 나눔 점선(요구 347-5) — 시트 `그림`(범위 닻)을 칸 위에 얹고 `인쇄.쪽나눔` 행 아래에 점선.
* 격자 overlay 맨 아래 층(고름 테두리가 위) · 누름은 칸으로 통과 · 스크롤 · 확대 · 다시 그림마다 자리를 다시 셈.
* 그림 주소 = 부른 쪽 `figureSrc(열쇠)` · 없으면 이름 적힌 빈 상자. 그림 옮기기 · 나눔 고치기 화면은 없음(1차).
* `createSpreadsheet` 가 그림 · 쪽 나눔이 있는 문서일 때만 불러옴(크기 예산).
* ========================================================================== */
import { parseRange } from "./spreadsheet_address";
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
export type FigureSrc = (열쇠: string) => string | undefined;
const css = (node: HTMLElement, style: Partial<CSSStyleDeclaration>) =>
Object.assign(node.style, style);
export function attachFigures(ctx: SpreadsheetContext, figureSrc?: FigureSrc): PartHandle {
const layer = document.createElement("div");
layer.className = "ss-figures";
css(layer, { position: "absolute", inset: "0", overflow: "hidden", pointerEvents: "none" });
const imgs = new Map<string, HTMLImageElement>(); // 열쇠 → 그림(다시 그려도 다시 안 받음)
function draw(): void {
const overlay = ctx.grid.root.querySelector<HTMLElement>(".aislo-grid__overlay");
if (!overlay) return;
if (overlay.firstChild !== layer) overlay.prepend(layer);
// editorSlot 상자 = 격자 뿌리 기준 → overlay 기준(머리 칸만큼 밀림)
const box = (r: number, c: number) => {
const b = ctx.grid.editorSlot(r, c).box;
return { ...b, x: b.x - overlay.offsetLeft, y: b.y - overlay.offsetTop };
};
const sheet = ctx.sheet();
const parts: HTMLElement[] = [];
for (const f of sheet.그림 ?? []) {
const rg = parseRange(f.닻);
if (!rg) continue;
const a = box(rg.r0, rg.c0);
const b = box(rg.r1, rg.c1);
const frame = document.createElement("div");
frame.className = "ss-figure";
frame.dataset.key = f.열쇠;
css(frame, {
position: "absolute",
left: `${a.x}px`,
top: `${a.y}px`,
width: `${Math.max(0, b.x + b.w - a.x)}px`,
height: `${Math.max(0, b.y + b.h - a.y)}px`,
boxSizing: "border-box",
background: "#fff",
});
const src = figureSrc?.(f.열쇠);
if (src) {
let img = imgs.get(f.열쇠);
if (!img || img.getAttribute("src") !== src) {
img = document.createElement("img");
img.src = src;
img.alt = f.이름 ?? f.열쇠;
css(img, { width: "100%", height: "100%", objectFit: "contain", display: "block" });
imgs.set(f.열쇠, img);
}
frame.append(img);
} else {
css(frame, {
border: "1px dashed #888",
color: "#666",
font: "12px sans-serif",
padding: "4px",
});
frame.textContent = f.이름 ?? f.열쇠;
}
parts.push(frame);
}
const view = ctx.grid.visibleRange();
for (const k of sheet.인쇄?.쪽나눔 ?? []) {
const at = box(k - 1, view.c0);
const line = document.createElement("div");
line.className = "ss-pagebreak";
line.dataset.row = String(k);
css(line, {
position: "absolute",
left: "0",
right: "0",
top: `${at.y + at.h - 1}px`,
borderTop: "2px dashed #3b6fd8",
});
parts.push(line);
}
layer.replaceChildren(...parts);
}
const onScroll = () => draw();
ctx.grid.root.addEventListener("scroll", onScroll, true);
ctx.grid.root.addEventListener("aislo-grid-zoom", onScroll);
draw();
return {
root: null,
refresh: draw,
destroy() {
ctx.grid.root.removeEventListener("scroll", onScroll, true);
ctx.grid.root.removeEventListener("aislo-grid-zoom", onScroll);
layer.remove();
},
};
}