Files
Aislo/B08_Quantity/B08_Quantity_UI_StructureFigure.ts

81 lines
3.4 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_StructureFigure.ts
* 구조물도 한 장의 **상단 그림** — 서버가 낸 모양 목록(m, 위가 +y)을 SVG 로 그림 (PLAN 12장).
*
* ⚠ 읽기 전용 — 치수를 셈하지 않음(서버 `B08_Quantity_Engine_StructureFigure` 한 벌). CAD 편집은 뒤.
* ⚠ 못 그리는 장은 빈칸 대신 **까닭**을 같은 자리에 띄움.
* ⚠ 한 장 = 그림 6 : 표 4(실무 비율) — 그림 칸 높이를 화면 높이의 60% 로 둠.
* ========================================================================== */
import { el } from "./B08_Quantity_UI_StructureSheet_Formula";
export type FigureShape =
| { kind: "path"; points: [number, number][]; role: "wall" | "guide"; dash: boolean }
| {
kind: "text";
text: string;
at: [number, number];
align: "left" | "center" | "right";
size: number;
};
const SVG_NS = "http://www.w3.org/2000/svg";
const ANCHOR = { left: "start", center: "middle", right: "end" } as const;
export const FIGURE_CSS = `
.b08-sheet__figure { height: 60vh; min-height: 16rem; border: 1px solid var(--color-border);
background: var(--color-surface); }
.b08-sheet__figure svg { display: block; width: 100%; height: 100%; }
.b08-sheet__figure--none { height: auto; min-height: 0; padding: 6px 10px; }
.b08-fig__wall, .b08-fig__guide { fill: none; vector-effect: non-scaling-stroke; }
.b08-fig__wall { stroke: var(--color-text); stroke-width: 2; }
.b08-fig__guide { stroke: var(--color-text-secondary, #888); stroke-width: 1; }
.b08-fig__guide.is-dash { stroke-dasharray: 6 4; }
.b08-sheet__figure text { fill: var(--color-text); }
`;
/** 그림 칸 — 모양이 없으면 까닭 한 줄. */
export function figureSection(
figure: FigureShape[] | null | undefined,
reason: string | null | undefined,
): HTMLElement {
const box = el("div", "b08-sheet__figure");
if (!figure?.length) {
box.classList.add("b08-sheet__figure--none");
box.append(el("p", "b08-quantity__message", (reason ?? "그림 없음").replace(/\*\*/g, "")));
return box;
}
const svg = document.createElementNS(SVG_NS, "svg");
const group = document.createElementNS(SVG_NS, "g");
for (const shape of figure) {
if (shape.kind === "text") {
const text = document.createElementNS(SVG_NS, "text");
text.setAttribute("x", String(shape.at[0]));
text.setAttribute("y", String(-shape.at[1]));
text.setAttribute("font-size", String(shape.size));
text.setAttribute("text-anchor", ANCHOR[shape.align]);
text.setAttribute("dominant-baseline", "middle");
text.textContent = shape.text;
group.append(text);
continue;
}
const line = document.createElementNS(SVG_NS, "polyline");
line.setAttribute("points", shape.points.map(([x, y]) => `${x},${-y}`).join(" "));
line.setAttribute("class", `b08-fig__${shape.role}${shape.dash ? " is-dash" : ""}`);
group.append(line);
}
svg.append(group);
box.append(svg);
// 보이는 틀은 **그려진 뒤 실제 글자 폭**으로 — 글자 폭을 서버가 짐작하지 않게.
requestAnimationFrame(() => {
if (!svg.isConnected) return;
const b = group.getBBox();
const pad = Math.max(b.width, b.height) * 0.04;
svg.setAttribute(
"viewBox",
`${b.x - pad} ${b.y - pad} ${b.width + pad * 2} ${b.height + pad * 2}`,
);
});
return box;
}