/** * 교대 벽체·흉벽 삽도 (2026-11-03, 시안 ② 벽체·흉벽 카드): 교대축 직각 단면(① 구간)과 정면·배면도. 순수 문자열 함수. * 생성본(교대 모델·부재)의 값으로 그린다. 두 그림 모두 가로·세로 축척이 같다(시안 "축척 같음"). */ export interface AbutWallDrawing { /** 교대축 길이 L (m), IP1 → IP3. */ lengthM: number; /** 교좌면 계단 구간 (s, m는 IP1에서), 구간마다 교좌면 EL. */ segments: { sFrom: number; sTo: number; seatEl: number }[]; /** 받침 (s, m), 받침 번호, 패드 상면 z (m), 교좌면 EL (m). */ bearings: { no: number; s: number; z: number; seatEl: number }[]; footingTopEl: number; footingBottomEl: number; /** 흉벽 상단 EL at s = 0 (IP1 끝) and s = L (IP3 끝). */ parapetTopStartEl: number; parapetTopEndEl: number; /** t (m, 전면 +): 벽체 전면, 흉벽 배면, 벽체 배면, 저판 앞굽 끝·뒷굽 끝. */ wallFrontM: number; parapetBackM: number; wallBackM: number; footingFrontM: number; footingBackM: number; bracketHeightM: number; shrinkJointCount: number; /** 시안 치수 줄 "브라켓 폭 + 흉벽 두께 + 교좌면 폭 = 벽체 두께" (mm). */ bracketWidthMm: number; parapetThicknessMm: number; seatWidthMm: number; } const INK = '#18212b'; const MUTED = '#55606e'; const CONC = '#d9c9a8'; const CONC_DARK = '#c7b48d'; const FOOT = '#b9c3ce'; const t3 = (v: number) => v.toFixed(3); /** EL 표기 (2026-11-04): "EL.41.995". */ const el = (v: number) => `EL.${v.toFixed(3)}`; const label = (x: number, y: number, s: string, anchor = 'middle', fill = INK) => `${s}`; const poly = (pts: [number, number][], style: string) => ``; /** 흉벽 상단 EL at s (linear between the two ends). */ function parapetTopAt(d: AbutWallDrawing, s: number): number { return d.parapetTopStartEl + ((d.parapetTopEndEl - d.parapetTopStartEl) * s) / d.lengthM; } /** * 교대축 직각 단면 (① 구간 = 받침 1번이 놓인 구간): 저판, 벽체(기초 상면 ~ 교좌면), 흉벽 배면 단차부(브라켓 상면까지), * 흉벽(흉벽 상단까지), 받침 블럭. 왼쪽이 배면, 오른쪽이 전면. */ export function abutWallSectionSvg(d: AbutWallDrawing): string { const b1 = d.bearings.find((b) => b.no === 1) ?? d.bearings[0]; const seg = d.segments.find((g) => b1.s >= g.sFrom && b1.s <= g.sTo) ?? d.segments[0]; const sAt = b1.s; const top = parapetTopAt(d, sAt); const bracket = top - d.bracketHeightM; const k = 46; // px per m, both directions const tMin = d.footingBackM - 0.6; const elMax = top + 0.7; const X = (t: number) => (t - tMin) * k; const Y = (el: number) => (elMax - el) * k + 10; const W = (d.footingFrontM + 1.4 - tMin) * k; const H = Y(d.footingBottomEl) + 40; const rect = (t0: number, t1: number, e0: number, e1: number, style: string) => poly([[X(t0), Y(e0)], [X(t1), Y(e0)], [X(t1), Y(e1)], [X(t0), Y(e1)]], style); const halfBlock = 0.6; const blockT = (d.wallFrontM + 0) / 2; // drawn at the middle of the 교좌면 width return [ ``, rect(d.footingBackM, d.footingFrontM, d.footingBottomEl, d.footingTopEl, `fill="${FOOT}" stroke="${MUTED}"`), rect(d.wallBackM, d.wallFrontM, d.footingTopEl, seg.seatEl, `fill="${CONC}" stroke="${MUTED}"`), rect(d.wallBackM, d.parapetBackM, seg.seatEl, bracket, `fill="${CONC_DARK}" stroke="${MUTED}"`), rect(d.parapetBackM, 0, seg.seatEl, top, `fill="${CONC_DARK}" stroke="${MUTED}"`), rect(blockT - halfBlock, blockT + halfBlock, seg.seatEl, b1.z, `fill="#9aa7b5" stroke="${INK}"`), ``, label(X(0), 14, '교대축', 'middle', '#c0392b'), label((X(d.wallBackM) + X(d.wallFrontM)) / 2, Y(top) - 18, `${d.bracketWidthMm} + ${d.parapetThicknessMm} + ${d.seatWidthMm} = ${d.bracketWidthMm + d.parapetThicknessMm + d.seatWidthMm}`), label(X(0) + 6, Y(top) - 4, `흉벽 상단 ${el(top)}`, 'start'), label(X(d.wallBackM) - 6, Y(bracket) + 4, `브라켓 상면 ${el(bracket)}`, 'end'), label(X(d.wallFrontM) + 6, Y(seg.seatEl) + 4, `교좌면 ${el(seg.seatEl)}`, 'start'), label(X(d.wallFrontM) + 6, Y((seg.seatEl + d.footingTopEl) / 2) + 4, `벽체 ${(seg.seatEl - d.footingTopEl).toFixed(3)}`, 'start'), label(X(d.footingFrontM) + 6, Y(d.footingTopEl) + 4, `상면 ${el(d.footingTopEl)}`, 'start'), label(X(d.footingFrontM) + 6, Y(d.footingBottomEl) + 4, `기초 하단 ${el(d.footingBottomEl)}`, 'start'), label(X(d.footingBackM), H - 8, '← 배면', 'start', MUTED), label(X(d.footingFrontM), H - 8, '전면 →', 'end', MUTED), '', ].join(''); } /** * 정면·배면도: 교좌면 계단(구간별 교좌면 EL), 흉벽 상단 경사, 받침 블럭, 수축줄눈, 저판. 왼쪽 IP3(① 쪽), 오른쪽 IP1. */ export function abutWallElevationSvg(d: AbutWallDrawing): string { const k = 46; const top = Math.max(d.parapetTopStartEl, d.parapetTopEndEl); const elMax = top + 0.6; const pad = 40; const X = (s: number) => pad + (d.lengthM - s) * k; // IP3 (s = L) on the left const Y = (el: number) => (elMax - el) * k + 10; const W = d.lengthM * k + 2 * pad; const H = Y(d.footingBottomEl) + 40; const parts: string[] = []; parts.push(poly([[X(d.lengthM), Y(d.footingTopEl)], [X(0), Y(d.footingTopEl)], [X(0), Y(d.footingBottomEl)], [X(d.lengthM), Y(d.footingBottomEl)]], `fill="${FOOT}" stroke="${MUTED}"`)); // 흉벽 (behind) from the 교좌면 steps up to the sloped top line. parts.push(poly([[X(d.lengthM), Y(d.footingTopEl)], [X(0), Y(d.footingTopEl)], [X(0), Y(d.parapetTopStartEl)], [X(d.lengthM), Y(d.parapetTopEndEl)]], `fill="${CONC_DARK}" stroke="${MUTED}"`)); // 벽체 front: one block per 교좌면 step. for (const g of d.segments) { parts.push(poly([[X(g.sTo), Y(d.footingTopEl)], [X(g.sFrom), Y(d.footingTopEl)], [X(g.sFrom), Y(g.seatEl)], [X(g.sTo), Y(g.seatEl)]], `fill="${CONC}" stroke="${MUTED}"`)); } for (const b of d.bearings) { parts.push(poly([[X(b.s + 0.6), Y(b.seatEl)], [X(b.s - 0.6), Y(b.seatEl)], [X(b.s - 0.6), Y(b.z)], [X(b.s + 0.6), Y(b.z)]], `fill="#9aa7b5" stroke="${INK}"`)); parts.push(label(X(b.s), Y(b.z) - 6, ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧'][b.no - 1] ?? String(b.no))); } for (let j = 1; j <= d.shrinkJointCount; j++) { const s = (d.lengthM * j) / (d.shrinkJointCount + 1); parts.push(``); parts.push(label(X(s) + 4, Y(d.footingTopEl) - 8, `수축줄눈 ${j}`, 'start')); } const first = d.segments.find((g) => d.lengthM >= g.sFrom && d.lengthM <= g.sTo) ?? d.segments[d.segments.length - 1]; const last = d.segments.find((g) => g.sFrom <= 0 && g.sTo >= 0) ?? d.segments[0]; parts.push(label(X(d.lengthM), Y(d.parapetTopEndEl) - 6, el(d.parapetTopEndEl), 'start')); parts.push(label(X(0), Y(d.parapetTopStartEl) - 6, el(d.parapetTopStartEl), 'end')); parts.push(label(X(d.lengthM) - 4, Y(first.seatEl) + 4, el(first.seatEl), 'end')); parts.push(label(X(0) + 4, Y(last.seatEl) + 4, el(last.seatEl), 'start')); parts.push(label(X(d.lengthM) - 4, Y(d.footingBottomEl) + 4, el(d.footingBottomEl), 'end')); parts.push(label(X(d.lengthM), H - 8, 'IP3', 'middle', MUTED)); parts.push(label(X(0), H - 8, 'IP1', 'middle', MUTED)); parts.push(label((X(0) + X(d.lengthM)) / 2, H - 8, `${d.lengthM.toFixed(3)} m`)); return [ ``, ...parts, '', ].join(''); }