/** * 교대 날개벽 삽도 (2026-11-04, 시안 ② 날개벽 카드): 평면(좌우 날개벽과 접속슬래브), 측면(날개벽 축 방향), 날개벽–흉벽 모서리 평면. * 순수 문자열 함수. 평면·측면은 생성본 값으로 그리고 가로·세로 축척이 같다. 모서리 상세는 치수를 나타내는 모식도다. */ import type { AbutPlanOutline } from './abut-scene'; const INK = '#18212b'; const MUTED = '#55606e'; const WING = '#d6bb8c'; const WALL = '#b7c7d3'; const SLAB = '#f3eee4'; const text = (x: number, y: number, s: string, anchor = 'start', fill = INK) => `${s}`; const poly = (pts: [number, number][], style: string) => ``; export interface AbutWingPlanDrawing { lengthM: number; outline: AbutPlanOutline; /** 좌 = ① 쪽 끝 (sEnd), 우 = 반대 끝. 이름에 형식을 붙여 표시한다. */ leftLabel: string; rightLabel: string; leftSEnd: number; slabWidthM: number; } /** 평면: 벽체(skew), 좌우 날개벽, 그 사이 접속슬래브. 왼쪽이 ① 쪽(좌), 위가 전면. */ export function abutWingPlanSvg(d: AbutWingPlanDrawing): string { const pts = [...d.outline.wall, ...d.outline.slab, ...d.outline.wings.flatMap((w) => w.poly)]; // ① 쪽(좌)을 왼쪽에 둔다: 좌가 s = L이면 s를 뒤집는다. const flip = d.leftSEnd !== 0; const xs = pts.map(([s]) => (flip ? -s : s)); const ts = pts.map(([, t]) => t); const k = 30; const xMin = Math.min(...xs) - 0.6; const tMax = Math.max(...ts) + 0.4; const X = (s: number) => 20 + ((flip ? -s : s) - xMin) * k; const Y = (t: number) => 18 + (tMax - t) * k; const W = 40 + (Math.max(...xs) - xMin + 0.6) * k; const H = Y(Math.min(...ts) - 0.4) + 30; const map = (p: [number, number][]) => p.map(([s, t]) => [X(s), Y(t)] as [number, number]); const parts = [ poly(map(d.outline.slab), `fill="${SLAB}" stroke="#b49a6b" stroke-dasharray="5 3"`), poly(map(d.outline.wall), `fill="${WALL}" stroke="${MUTED}"`), ...d.outline.wings.map((w) => poly(map(w.poly), `fill="${WING}" stroke="#9c8763"`)), ]; const slabC = d.outline.slab.reduce((a, [s, t]) => [a[0] + s / d.outline.slab.length, a[1] + t / d.outline.slab.length], [0, 0]); parts.push(text(X(slabC[0]), Y(slabC[1]), `접속슬래브 폭 ${d.slabWidthM.toFixed(3)} m`, 'middle')); for (const w of d.outline.wings) { const far = w.poly[1]; parts.push(text(X(far[0]), Y(far[1]) + 16, w.side === 'left' ? d.leftLabel : d.rightLabel, 'middle')); } parts.push(text(X(d.lengthM / 2), 12, '흉벽 · 벽체 (위 = 전면)', 'middle', MUTED)); return [ ``, ...parts, '', ].join(''); } export interface AbutWingSideDrawing { name: string; form: 'general' | 'short'; lengthM: number; endHeightM: number; heelDistanceM: number; topLine: { dM: number; el: number }[]; bottomLine: { dM: number; el: number }[]; bands: { name: string; zTopEl: number; zBottomEl: number }[]; seatEl: number; footingTopEl: number; /** 흉벽 상단 EL at this wing's end, and the 브라켓 높이 (m): 브라켓 상면 = 흉벽 상단 − 브라켓 높이. */ parapetTopEl: number; bracketHeightM: number; /** 단면 위치 t (m, 전면 +): 벽체 배면 · 흉벽 배면 · 벽체 전면, 저판 앞굽 끝 · 뒷굽 끝, 저판 두께. */ wallBackM: number; parapetBackM: number; wallFrontM: number; footingFrontM: number; footingBackM: number; footingThicknessM: number; } /** EL 표기 (2026-11-04): "EL.41.995". */ const el = (v: number) => `EL.${v.toFixed(3)}`; /** * 측면 (날개벽 축 방향으로 잘라 본 모습): 벽체 쪽이 오른쪽, 날개벽 끝이 왼쪽. * 벽체 · 흉벽 · 브라켓 단차와 저판을 단면으로 함께 그린다. 가로 위치는 t를 날개벽 거리로 바꾼 값(dM = −t / k, k = 뒷굽 / 뒷굽 거리). */ /** 치수선 (2026-11-04): 점 a, b에서 치수선 위치 at(가로면 y, 세로면 x)까지 보조선, 치수선, 양 끝 사선 틱, 치수. after = 치수를 선의 아래 · 오른쪽에. */ export function dimLine(a: [number, number], b: [number, number], at: number, dir: 'h' | 'v', label: string, after = false): string { const line = (x1: number, y1: number, x2: number, y2: number) => ``; const tick = (x: number, y: number) => line(x - 3, y + 3, x + 3, y - 3); if (dir === 'h') { const ext = (x: number, y: number) => line(x, y + Math.sign(at - y) * 3, x, at + Math.sign(at - y) * 3); const lab = text((a[0] + b[0]) / 2, after ? at + 13 : at - 4, label, 'middle'); return [ext(a[0], a[1]), ext(b[0], b[1]), line(a[0], at, b[0], at), tick(a[0], at), tick(b[0], at), lab].join(''); } const ext = (x: number, y: number) => line(x + Math.sign(at - x) * 3, y, at + Math.sign(at - x) * 3, y); const lab = after ? text(at + 4, (a[1] + b[1]) / 2 + 4, label, 'start') : text(at - 4, (a[1] + b[1]) / 2 + 4, label, 'end'); return [ext(a[0], a[1]), ext(b[0], b[1]), line(at, a[1], at, b[1]), tick(at, a[1]), tick(at, b[1]), lab].join(''); } export function abutWingSideSvg(d: AbutWingSideDrawing): string { const k = 40; const kt = Math.abs(d.footingBackM) / d.heelDistanceM; // |t| per m along the wing const dmOf = (tM: number) => -tM / kt; const bracketTop = d.parapetTopEl - d.bracketHeightM; const footingBottom = d.footingTopEl - d.footingThicknessM; const els = [...d.topLine.map((p) => p.el), ...d.bottomLine.map((p) => p.el), d.parapetTopEl, footingBottom]; const elMax = Math.max(...els) + 0.8; const elMin = Math.min(...els) - 0.5; const dmRight = dmOf(d.footingFrontM); // 저판 앞굽 끝 (rightmost) const X = (dM: number) => 40 + (d.lengthM - dM) * k; // wall (dM 0) toward the right const Y = (y: number) => 14 + (elMax - y) * k; const W = X(dmRight) + 110; const H = Y(elMin) + 34; const rect = (dm0: number, dm1: number, e0: number, e1: number, style: string) => poly([[X(dm0), Y(e0)], [X(dm1), Y(e0)], [X(dm1), Y(e1)], [X(dm0), Y(e1)]], style); const outline: [number, number][] = [ ...d.topLine.map((p) => [X(p.dM), Y(p.el)] as [number, number]), ...[...d.bottomLine].reverse().map((p) => [X(p.dM), Y(p.el)] as [number, number]), ]; const wb = dmOf(d.wallBackM); const pb = dmOf(d.parapetBackM); const wf = dmOf(d.wallFrontM); const parts = [ poly(outline, `fill="${WING}" stroke="#9c8763"`), // 저판 (뒷굽 끝 ~ 앞굽 끝), 벽체 (기초 상면 ~ 교좌면), 브라켓 단차 (벽체 배면 ~ 흉벽 배면, 브라켓 상면까지), 흉벽 (흉벽 상단까지). rect(dmOf(d.footingBackM), dmRight, footingBottom, d.footingTopEl, `fill="#c9d1da" stroke="${MUTED}"`), rect(wb, wf, d.footingTopEl, d.seatEl, `fill="${WALL}" stroke="${MUTED}"`), rect(wb, pb, d.seatEl, bracketTop, `fill="#9fb1c0" stroke="${MUTED}"`), rect(pb, 0, d.seatEl, d.parapetTopEl, `fill="#9fb1c0" stroke="${MUTED}"`), ]; // 수평 단 경계 (등단면 / 변화단면) as dashed lines across the wing. for (const b of d.bands.slice(0, -1)) { parts.push(``); } for (const b of d.bands) { const mid = (b.zTopEl + b.zBottomEl) / 2; parts.push(text(X(d.lengthM * 0.55), Y(mid) + 4, b.name, 'middle', MUTED)); } // 치수선: 길이 (날개벽 끝 ~ 교좌면 끝), 끝단 높이 (일반형), 뒷굽 끝 (교좌면 끝 ~ 뒷굽 끝). const endTop = d.topLine[d.topLine.length - 1].el; const endBottom = d.bottomLine[d.bottomLine.length - 1].el; parts.push(dimLine([X(d.lengthM), Y(endTop)], [X(0), Y(d.parapetTopEl)], Y(elMax) + 12, 'h', `${Math.round(d.lengthM * 1000)} (교좌면 끝에서, 날개벽 축 방향)`)); if (d.form === 'general') parts.push(dimLine([X(d.lengthM), Y(endTop)], [X(d.lengthM), Y(endBottom)], X(d.lengthM) - 12, 'v', `${Math.round(d.endHeightM * 1000)}`)); parts.push(text(X(0) + 4, Y(d.parapetTopEl) + 4, `흉벽 상단 ${el(d.parapetTopEl)}`, 'start')); parts.push(text(X(pb) + 4, Y(bracketTop) - 4, `브라켓 상면 ${el(bracketTop)}`, 'start')); parts.push(text(X(wf) + 4, Y(d.seatEl) + 4, `교좌면 ${el(d.seatEl)}`, 'start')); parts.push(text(X(dmRight) + 4, Y(d.footingTopEl) + 4, el(d.footingTopEl), 'start')); parts.push(text(X(dmRight) + 4, Y(footingBottom) + 4, `기초 하단 ${el(footingBottom)}`, 'start')); parts.push(dimLine([X(0), Y(footingBottom)], [X(d.heelDistanceM), Y(footingBottom)], Y(footingBottom) + 18, 'h', '')); parts.push(text((X(0) + X(d.heelDistanceM)) / 2, Y(footingBottom) + 31, `뒷굽 끝 ${d.heelDistanceM.toFixed(3)} m`, 'middle')); return [ ``, ...parts, '', ].join(''); } export interface AbutWingCornerDrawing { parapetMm: number; bracketMm: number; wingThicknessMm: number; haunchMm: number; /** 접속슬래브 간격: 날개벽과 20, 흉벽과 5. 모따기 300 × 300 (spec §4.8). */ slabGapWingMm: number; slabGapParapetMm: number; chamferMm: number; } /** 좌 날개벽–흉벽 모서리 평면 (모식도): 흉벽, 브라켓(슬래브 아래), 날개벽, 헌치, 접속슬래브(모따기, 간격). */ export function abutWingCornerSvg(d: AbutWingCornerDrawing): string { const k = 0.14; // px per mm const x0 = 70; // 날개벽 내측면 const y0 = 60; // 흉벽 배면 const X = (mm: number) => x0 + mm * k; const Y = (mm: number) => y0 + mm * k; const span = 2200; // drawn extent from the corner (mm) const h = d.haunchMm; const c = d.chamferMm; const sx = d.slabGapWingMm; const sy = d.slabGapParapetMm; const parts = [ // 흉벽 (above the 배면 line) and the 교대축 at its front face. poly([[X(-d.wingThicknessMm), Y(-d.parapetMm)], [X(span), Y(-d.parapetMm)], [X(span), Y(0)], [X(-d.wingThicknessMm), Y(0)]], `fill="${WALL}" stroke="${MUTED}"`), ``, // 브라켓 (under the slab, dashed). ``, // 날개벽 (left, running back = down). poly([[X(-d.wingThicknessMm), Y(0)], [X(0), Y(0)], [X(0), Y(span)], [X(-d.wingThicknessMm), Y(span)]], `fill="${WING}" stroke="#9c8763"`), ]; if (h > 0) parts.push(poly([[X(0), Y(0)], [X(h), Y(0)], [X(0), Y(h)]], `fill="${WING}" stroke="#9c8763"`)); // 접속슬래브 with the corner cut (모따기) and the gaps. parts.push( poly( [[X(sx + c), Y(sy)], [X(span), Y(sy)], [X(span), Y(span)], [X(sx), Y(span)], [X(sx), Y(sy + c)]], `fill="${SLAB}" stroke="#b49a6b"`, ), ); parts.push(text(X(-d.wingThicknessMm / 2), Y(span) - 6, `날개벽 ${d.wingThicknessMm}`, 'middle')); parts.push(text(X(span / 2), Y(-d.parapetMm / 2) + 4, `흉벽 ${d.parapetMm}`, 'middle')); parts.push(text(X(span) - 4, Y(-d.parapetMm) - 6, '교대축 (흉벽 전면)', 'end', '#9b2c1f')); parts.push(text(X(span) - 4, Y(d.bracketMm) - 4, `브라켓 ${d.bracketMm} (슬래브 아래)`, 'end', MUTED)); if (h > 0) parts.push(text(X(h) + 6, Y(h / 2) + 4, `헌치 ${h} × ${h}`, 'start')); parts.push(text(X(sx + c) + 8, Y(sy + c) + 14, `슬래브 모따기 ${c} × ${c}`, 'start')); parts.push(text(X(span / 2), Y(span) - 6, `접속슬래브 · 날개벽과 ${sx} · 흉벽과 ${sy}`, 'middle')); const W = X(span) + 20; const H = Y(span) + 16; return [ ``, ...parts, '', ].join(''); }