import type { PierModel } from '../core/geometry'; import { NOMINAL_DIAMETER } from '../core/rebar/common'; import { assemblyGridIndices, assemblyJs, b10Levels, b9Bars, grid, gridPositions, layers, sideDepth } from '../core/rebar/footing'; import { DEFAULT_FOOTING_REBAR } from '../core/rebar/footing-settings'; /** * 확대기초 · 교각축 직각 단면 (폭 W) (승인 2026-10-26, 추천안 1·2단계). A cut across the footing width: horizontal = W, vertical = height * from the 하면 at true scale. Every mark comes from the same functions as the rebar table and the 3D view: * - B5–B8 teal points on the grid along W (방향 교환), B9 purple lines per row, B11 grey dashed lines, B12 grey points (B1–B4 and B10-1 are not drawn in this view, 승인 2026-10-27), * Legs are not drawn; the B6 leg length is noted in the figure. The column and the piles are shown as context. * Each mark carries data-bar="<철근 번호>" so a bubble can highlight its bars (철근표 강조와 같은 방식). */ const COLOR = { outline: '#55606E', b5: '#0F5C66', b14: '#B4531F', b9: '#6B3FA0', b10: '#2E7D32', b11: '#55606E', b12: '#55606E', dim: '#B42318', column: '#B08D57', pile: '#8898aa', }; const W_PX = 380; const X0 = 60; /** Height of the 하면 (z = 0) in the view; the figure is drawn at true scale (kY = kX). */ const Y0 = 230; export function footingSectionSvg(model: PierModel): string { const foot = model.footings[0]; if (!foot) return '확대기초 없음'; const fs = model.footingRebar ?? DEFAULT_FOOTING_REBAR; const L = foot.length * 1000; const W = foot.width * 1000; const H = (foot.top - foot.bottom) * 1000; const kX = W_PX / W; const kY = kX; const cx = X0 + W_PX / 2; const x = (mm: number) => cx + mm * kX; const y = (z: number) => Y0 - z * kY; const z = layers(H, fs); const parts: string[] = []; const line = (cls: string, bar: string, x1: number, x2: number, yy: number, color: string, width = 1.4, dash = '') => ``; const dot = (cls: string, bar: string, xx: number, yy: number, color: string, r = 2.6) => ``; const vline = (cls: string, bar: string, xx: number, y1: number, y2: number, color: string) => ``; // Dimension and note text has a white halo (paint-order stroke) so that leader lines under it stay readable. const text = (xx: number, yy: number, s: string, anchor = 'start', fill = COLOR.outline) => `${s}`; const tick = (xx: number, yy: number) => ``; // Context: the column (centred on W, above the footing) and the piles (below, one stub per distinct W position). const colD = model.columnDiameterMm; parts.push(``); const piles = model.piles.filter((p) => Math.abs(p.s - foot.s) <= foot.length / 2 + 1e-6); for (const n of [...new Set(piles.map((p) => Math.round(p.n * 1000)))]) { const d = (piles.find((p) => Math.round(p.n * 1000) === n)?.diameter ?? 0.6) * 1000; parts.push(``); } // Footing outline (W × H) and the 하면 line. parts.push(``); parts.push(line('base', '', x(-W / 2), x(W / 2), y(0), COLOR.outline, 1)); // Cover lines on the W faces (측면 피복 c_s). const xa = x(-W / 2 + fs.cSide); const xb = x(W / 2 - fs.cSide); // B6 leg note: the B6 leg runs from the 하면 1단 up to the B5 height (B6 다리 → B5 높이), shown in the right margin below. // B5–B8 (방향 교환): run along L and are spaced along W (sW). In this section they are points on the W grid: // odd positions carry B5, B6, B7 and even positions carry B8 (same parity rule as crossBars). const xsW = gridPositions(W, fs, 'sW'); const last = xsW.length - 1; xsW.forEach((mm, i) => { const xx = x(mm); if (i % 2 === 1) { parts.push(dot('b5', 'B5', xx, y(z.B5), COLOR.b5)); parts.push(dot('b6', 'B6', xx, y(z.B68), COLOR.b5)); parts.push(dot('b7', 'B7', xx, y(z.B7), COLOR.b5)); } else { parts.push(dot('b8', 'B8', xx, y(z.B68), COLOR.b5)); } }); // Outermost columns (승인 2026-10-26 추천, 방향 교환 반영): the missing B5–B8 points at the first and last W position are drawn as 'ext' points. for (const i of [0, last]) { const xx = x(xsW[i]); const drawnHere = i % 2 === 1 ? ['B5', 'B6', 'B7'] : ['B8']; for (const [name, zz] of [['B5', z.B5], ['B6', z.B68], ['B7', z.B7], ['B8', z.B68]] as const) { if (!drawnHere.includes(name)) parts.push(dot('ext', name, xx, y(zz), COLOR.b5)); } } // B9: one purple vertical line per row along W (the rows of b9Bars). const b9 = b9Bars(L, W, [], 2000, fs); for (const n of [...new Set(b9.bars.map((b) => b.n))]) parts.push(vline('b9', 'B9', x(n), y(z.B24), y(z.B5), COLOR.b9)); const levels = b10Levels(H, fs); // B11: grey dashed lines per assembly level. B12: grey points on its W grid, every k·s, on the even grid (1단계 규칙). const js = assemblyJs(H, fs); const dB10 = NOMINAL_DIAMETER.S19; js.forEach((j, idx) => { const zA = fs.cBottom + j * fs.sV; parts.push(line('b11', 'B11', xa, xb, y(zA), COLOR.b11, 1.2, '6 3')); const xsB12 = gridPositions(W, fs, 'sW'); for (const i of assemblyGridIndices(xsB12.length - 1, fs.kB12, idx)) parts.push(dot('b12', 'B12', x(xsB12[i]), y(zA + dB10), COLOR.b12, 2.2)); }); // Dimensions: the width line with ticks and the grid string, and the right-hand heights. const g = grid(W, fs, 'sW'); const yDim = Y0 + 40; parts.push(``); parts.push(tick(x(-W / 2), yDim) + tick(x(W / 2), yDim)); // Edge covers sit outside the dimension ends; the grid pieces sit on the first row, spread apart so they do not run together. parts.push(text(x(-W / 2) - 5, yDim + 14, `${fs.cSide}`, 'end', COLOR.dim)); parts.push(text(x(W / 2) + 5, yDim + 14, `${fs.cSide}`, 'start', COLOR.dim)); parts.push(text(x(-W / 2 + fs.cSide) + 2, yDim + 14, `${g.e.toFixed(1)}`, 'start', COLOR.dim)); parts.push(text(x(W / 2 - fs.cSide) - 2, yDim + 14, `${g.e.toFixed(1)}`, 'end', COLOR.dim)); // The grid string sits right of centre, so the bottom leaders (at the centre and the two sides) do not cross it. parts.push(text(cx + 150, yDim - 6, `${g.n}@${fs.sW}=${g.G}`, 'middle', COLOR.dim)); parts.push(text(cx, 20, `교각축 직각 단면 · 폭 ${W.toFixed(0)}`, 'middle', '#1F2937')); const xr = X0 + W_PX + 18; parts.push(``); parts.push(tick(xr, y(H)) + tick(xr, y(0))); parts.push(text(xr + 6, y(H) + 12, `${fs.cTop}`, 'start', COLOR.dim)); parts.push(text(xr + 6, (y(H) + y(0)) / 2, `${levels - 1}@${fs.sV}=${(levels - 1) * fs.sV}`, 'start', COLOR.dim)); parts.push(text(xr + 6, y(0) - 4, `${fs.cBottom}`, 'start', COLOR.dim)); // B6 leg note in the right margin, with a leader to the B6 leg's end at the B6 height (B6 다리 → B5 높이, from the 하면 1단 up to B5). const yNote = y(H) - 14; parts.push(``); parts.push(text(xr + 6, yNote, 'B6 다리 → B5', 'start', COLOR.b5)); parts.push(text(xr + 6, yNote + 12, `높이 ${(z.B5 - z.B68).toFixed(0)}`, 'start', COLOR.b5)); // Bubbles outside the outline, spread apart, each with a leader to its bar (the leader carries the same data-bar, so it highlights too). // A leader ends on a point rebar (점철근) of its family: a small dot in the family colour at the leader end (승인 2026-10-26). const bubble = (name: string, bx: number, by: number, tx: number, ty: number, dotColor: string) => { const edgeY = by < ty ? by + 11 : by - 11; return `${name}`; }; const zB11 = js.length > 0 ? fs.cBottom + js[0] * fs.sV : fs.cBottom; const yTop = 40; const yBot = Y0 + 100; // Each bottom bubble sits straight below its own bar (B2 left of centre, B4 at centre, B3 right of centre) on a bar of the right parity. parts.push(bubble('B5', xa + 12, yTop, x(xsW[1]), y(z.B5), COLOR.b5)); parts.push(bubble('B11', xb - 20, yTop, xb, y(zB11), COLOR.b11)); // Legend (선 모양별, 삽도 기준) and the units note. const yLeg = 352; parts.push(`${text(28, yLeg + 3, '주철근 (B5–B8)', 'start', COLOR.outline)}`); parts.push(`${text(338, yLeg + 3, '전단·띠 (B9)', 'start', COLOR.outline)}`); parts.push(`${text(280, yLeg + 25, '조립 (B11·B12)', 'start', COLOR.outline)}`); parts.push(text(20, yLeg + 48, '치수 mm · 높이 방향은 실제 비율 · 다리 생략 (B6 다리는 주석) · 기준: 확대기초 하면 0', 'start', COLOR.outline)); // The whole drawing shifts down 20 px so the top bubbles are not clipped. return `${parts.join('')}`; }