/** * 확대기초·말뚝 그림 (2단계, 승인 2026-10-23): plan and side views as SVG strings, drawn from the model (footings, piles, * columns) and the input values. Pure: no DOM, so that it can be tested. Units: m in the model; the labels show mm. * Both views use one scale (FOOTING_PX_PER_M), so they can sit side by side. The side view breaks the column and the piles * (끊음 표시) so that its height stays near the plan's. */ import type { PierModel } from '../core/geometry'; export const FOOTING_PX_PER_M = 22; /** Side view: the column is shown this far above the footing top, the piles this far below the footing bottom (m). */ const COLUMN_STUB = 3; const PILE_STUB = 4; const mm = (v: number) => Math.round(v * 1000); /** Horizontal dimension line from x1 to x2 at height y, with 45° ticks and a centred label above it (승인 2026-10-23). */ function dimH(x1: number, x2: number, y: number, label: string): string { const t = 0.12; return `${label}`; } /** Vertical dimension line from y1 to y2 at x, with 45° ticks and the label to its right. */ function dimV(x: number, y1: number, y2: number, label: string): string { const t = 0.12; return `${label}`; } /** A break mark (끊음): two short slanted strokes "//" centred on (x, y), so the member is shown as cut. */ function breakMark(x: number, y: number, halfWidth: number): string { const d = 0.12; return ``; } function svgOf(x0: number, y0: number, w: number, h: number, label: string, body: string): string { const pw = Math.round(w * FOOTING_PX_PER_M); const ph = Math.round(h * FOOTING_PX_PER_M); return `${body}`; } /** Distinct values (to 1 mm), sorted ascending. */ function uniq(values: number[]): number[] { return [...new Set(values.map((v) => Math.round(v * 1000) / 1000))].sort((a, b) => a - b); } /** * A chain of dimensions between the footing edges and the pile lines: from the edge to the first pile, between piles, and to the * far edge. Horizontal when `horizontal` (at height y), otherwise vertical (at x = y argument). */ function chain(lines: number[], edgeA: number, edgeB: number, at: number, horizontal: boolean): string { const pts = [edgeA, ...lines, edgeB]; return pts .slice(0, -1) .map((a, i) => { const b = pts[i + 1]; const label = mm(Math.abs(b - a)).toString(); return horizontal ? dimH(a, b, at, label) : dimV(at, a, b, label); }) .join(''); } /** Plan view: the footing rectangle (length along the 교축, width across), the pile circles, the column circle, the axis. */ export function footingPlanSvg(model: PierModel): string { const f = model.footings[0]; if (!f) return ''; const pad = 1.5; const x0 = f.s - f.length / 2 - pad; const y0 = -f.width / 2 - pad; const w = f.length + 2 * pad; const h = f.width + 2 * pad; const piles = model.piles.filter((p) => Math.abs(p.s - f.s) <= f.length / 2 + 1e-6); const parts: string[] = []; parts.push(``); for (const p of piles) { parts.push(``); } for (const c of model.columns) { parts.push(``); } parts.push(``); // Overall dimensions (전체 길이, 폭) and the pile chains (끝단 거리와 말뚝 간격), as in the 삽도 (승인 2026-10-23). const left = f.s - f.length / 2; const right = f.s + f.length / 2; const top = -f.width / 2; const bottom = f.width / 2; parts.push(dimH(left, right, top - 0.9, String(mm(f.length)))); parts.push(dimV(right + 0.9, top, bottom, String(mm(f.width)))); const xs = uniq(piles.map((p) => p.s)); const ys = uniq(piles.map((p) => p.n)); parts.push(chain(xs, left, right, bottom + 0.8, true)); parts.push(chain(ys, top, bottom, left - 0.8, false)); return svgOf(x0, y0, w, h, '확대기초·말뚝 평면', parts.join('')); } /** Side view (elevation, EL up): the footing, the piles, and the column stub above the footing, both broken (끊음). */ export function footingSideSvg(model: PierModel): string { const f = model.footings[0]; if (!f) return ''; const piles = model.piles.filter((p) => Math.abs(p.s - f.s) <= f.length / 2 + 1e-6); const pileTop = piles.length ? Math.max(...piles.map((p) => p.top)) : f.bottom; const pileLen = piles.length ? piles[0].top - piles[0].bottom : 0; const pad = 1.5; const x0 = f.s - f.length / 2 - pad; const w = f.length + 2 * pad; // Visible band in EL: from the column stub above the footing top, down to the pile stub below the footing bottom. const topEl = f.top + COLUMN_STUB; const botEl = f.bottom - PILE_STUB; const y0 = -topEl; const h = topEl - botEl; const parts: string[] = []; parts.push(``); for (const p of piles) { const from = Math.max(p.bottom, botEl); const to = Math.min(p.top, f.bottom); if (to > from) parts.push(``); } const col = model.columns[0]; if (col) { parts.push(``); // Break mark on the column stub, at its middle. parts.push(breakMark(col.s, -f.top - COLUMN_STUB / 2, col.diameter / 2 + 0.15)); } if (piles.length) { // Break mark across the pile group, in the lower stub (the pile label sits below it). parts.push(breakMark(f.s, -(botEl + 0.6), f.length / 2)); } if (piles.length) { parts.push(`말뚝 ${mm(pileLen)} (끊음)`); } // Dimension lines (치수선): footing length above, footing height at the right, the visible pile length at the left. parts.push(dimH(f.s - f.length / 2, f.s + f.length / 2, -(f.top + 0.8), `${mm(f.length)}`)); parts.push(dimV(f.s + f.length / 2 + 0.5, -f.top, -f.bottom, `h1 ${mm(f.top - f.bottom)}`)); if (piles.length) { parts.push(dimV(f.s - f.length / 2 - 0.5, -f.bottom, -(botEl + 0.6), `${mm(pileLen)} (끊음)`)); } void pileTop; return svgOf(x0, y0, w, h, '확대기초·말뚝 측면', parts.join('')); }