- spreadsheet_figures: 시트 그림을 닻 범위에 얹음(figureSrc 없으면 이름 상자) · 쪽 나눔 행 아래 점선 · overlay 맨 아래 층 - spreadsheet: figureSrc 선택 · 그림 · 쪽 나눔 있는 문서일 때만 import() Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
4.1 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|