Files
Pier_v1/src/view/footing-section-l-svg.ts
T
ypshimandClaude Haiku 5.5 0e9d89951a feat(rebar): 확대기초 교각축 단면(길이 L) 탭 추가
- view/footing-section-l-svg.ts: 길이 방향 단면 (B1–B4 점, B5–B8 선, B9 세로선, B10-2 점·B10-1 선, B11 점, B12 선)
- 탭 전환: 교각축 직각 단면(폭 W) ↔ 교각축 단면(길이 L), 다리 배치 상세·B10 모서리는 다음 단계
- 테스트: 점·선 개수가 철근표 개수와 같음

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-27 13:56:05 +09:00

145 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
/**
* 확대기초 · 교각축 단면 (길이 L) (승인 2026-10-27, 2단계 탭). A cut across the footing length: horizontal = L, vertical = height from the 하면.
* Bars that run along W cross this plane as points; bars that run along L lie in it as lines (방향 교환 후 배치).
* - B1–B4 (along W, spaced along L): points at the L grid (odd → B1·B2·B3, even → B4)
* - B5–B8 (along L): one line per family from cover to cover
* - B9: vertical lines at the L grid columns
* - B10-2 (on the L faces): points per level (lowest removed); B10-1 (on the W faces, along L): lines per level (lowest removed)
* - B11 (along W, spaced along L): points on the L grid per assembly level; B12 (along L, spaced along W): lines per assembly level
* Every mark comes from the same functions as the rebar table and the 3D view.
*/
const COLOR = { outline: '#55606E', b5: '#0F5C66', b14: '#B4531F', b9: '#6B3FA0', b10: '#2E7D32', b11: '#55606E', b12: '#55606E', dim: '#B42318', column: '#B08D57', pile: '#8898aa' };
const L_PX = 380;
const X0 = 60;
const Y0 = 230;
export function footingSectionLSvg(model: PierModel): string {
const foot = model.footings[0];
if (!foot) return '<svg viewBox="0 0 600 430" width="600" height="430" role="img" aria-label="확대기초 없음"><text x="20" y="180" font-size="12">확대기초 없음</text></svg>';
const fs = model.footingRebar ?? DEFAULT_FOOTING_REBAR;
const L = foot.length * 1000;
const H = (foot.top - foot.bottom) * 1000;
const k = L_PX / L;
const cx = X0 + L_PX / 2;
const x = (mm: number) => cx + mm * k;
const y = (z: number) => Y0 - z * k;
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 = '') =>
`<line class="${cls}" data-bar="${bar}" x1="${x1.toFixed(1)}" x2="${x2.toFixed(1)}" y1="${yy.toFixed(1)}" y2="${yy.toFixed(1)}" stroke="${color}" stroke-width="${width}"${dash ? ` stroke-dasharray="${dash}"` : ''}/>`;
const dot = (cls: string, bar: string, xx: number, yy: number, color: string, r = 2.6) =>
`<circle class="${cls}" data-bar="${bar}" cx="${xx.toFixed(1)}" cy="${yy.toFixed(1)}" r="${r}" fill="${color}"/>`;
const vline = (cls: string, bar: string, xx: number, y1: number, y2: number, color: string) =>
`<line class="${cls}" data-bar="${bar}" x1="${xx.toFixed(1)}" x2="${xx.toFixed(1)}" y1="${y1.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${color}" stroke-width="1"/>`;
const text = (xx: number, yy: number, s: string, anchor = 'start', fill = COLOR.outline) =>
`<text x="${xx.toFixed(1)}" y="${yy.toFixed(1)}" font-size="10" font-family="JetBrains Mono, monospace" fill="${fill}" stroke="#fff" stroke-width="3" paint-order="stroke" text-anchor="${anchor}">${s}</text>`;
const tick = (xx: number, yy: number) => `<line x1="${(xx - 3).toFixed(1)}" x2="${(xx + 3).toFixed(1)}" y1="${(yy + 3).toFixed(1)}" y2="${(yy - 3).toFixed(1)}" stroke="${COLOR.dim}" stroke-width="0.8"/>`;
// Context: the column (centred, above the footing) and the piles (below, one stub per distinct L position).
const colD = model.columnDiameterMm;
parts.push(`<rect class="column" x="${x(-colD / 2).toFixed(1)}" y="${y(H) - 36}" width="${(colD * k).toFixed(1)}" height="36" fill="#F2E8D5" stroke="${COLOR.column}" stroke-width="0.8"/>`);
const piles = model.piles.filter((p) => Math.abs(p.s - foot.s) <= foot.length / 2 + 1e-6);
for (const s of [...new Set(piles.map((p) => Math.round((p.s - foot.s) * 1000)))]) {
const d = (piles.find((p) => Math.round((p.s - foot.s) * 1000) === s)?.diameter ?? 0.6) * 1000;
parts.push(`<rect class="pile" x="${(x(s) - d * k / 2).toFixed(1)}" y="${Y0}" width="${Math.max(3, d * k).toFixed(1)}" height="26" fill="${COLOR.pile}" stroke="#3b4656" stroke-width="0.6"/>`);
}
// Footing outline and the 하면 line.
parts.push(`<rect x="${x(-L / 2).toFixed(1)}" y="${y(H).toFixed(1)}" width="${(L * k).toFixed(1)}" height="${(H * k).toFixed(1)}" fill="none" stroke="${COLOR.outline}" stroke-width="1.2"/>`);
parts.push(line('base', '', x(-L / 2), x(L / 2), y(0), COLOR.outline, 1));
// Cover lines on the L faces (측면 피복 c_s).
const xa = x(-L / 2 + fs.cSide);
const xb = x(L / 2 - fs.cSide);
// B5–B8 (along L): one line per family from cover to cover.
parts.push(line('b5', 'B5', xa, xb, y(z.B5), COLOR.b5, 1.6));
parts.push(line('b6', 'B6', xa, xb, y(z.B68), COLOR.b5, 1.2));
parts.push(line('b7', 'B7', xa, xb, y(z.B7), COLOR.b5, 1.2));
parts.push(line('b8', 'B8', xa, xb, y(z.B68), COLOR.b5, 1.2));
// B1–B4 (along W, spaced along L): points on the L grid. Odd positions carry B1, B2, B3; even carry B4 (as bottomBars).
const xsL = gridPositions(L, fs, 'sL');
xsL.forEach((mm, i) => {
const xx = x(mm);
if (i % 2 === 1) {
parts.push(dot('b1', 'B1', xx, y(z.B1), COLOR.b14));
parts.push(dot('b2', 'B2', xx, y(z.B24), COLOR.b14));
parts.push(dot('b3', 'B3', xx, y(z.B3), COLOR.b14));
} else {
parts.push(dot('b4', 'B4', xx, y(z.B24), COLOR.b14));
}
});
// B9: one vertical line per L column of the B9 grid.
const b9 = b9Bars(L, foot.width * 1000, [], 2000, fs);
for (const xm of [...new Set(b9.bars.map((b) => b.x))]) parts.push(vline('b9', 'B9', x(xm), y(z.B24), y(z.B5), COLOR.b9));
// B10-2 (on the L faces, points) and B10-1 (on the W faces, lines along L), per level above the lowest.
const levels = b10Levels(H, fs);
const dB10 = NOMINAL_DIAMETER.S19;
const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19, fs);
for (let j = 1; j < levels; j++) {
const zj = fs.cBottom + j * fs.sV;
parts.push(dot('b10', 'B10-2', x(-L / 2 + depth2), y(zj), COLOR.b10, 2.2));
parts.push(dot('b10', 'B10-2', x(L / 2 - depth2), y(zj), COLOR.b10, 2.2));
parts.push(line('b10', 'B10-1', x(-L / 2 + depth2), x(L / 2 - depth2), y(zj + dB10), COLOR.b10, 1.2, '2 2'));
}
// B11 (along W, spaced along L): points on the L grid, per assembly level. B12 (along L, spaced along W): lines per level.
const js = assemblyJs(H, fs);
const nL = grid(L, fs, 'sL').positions - 1;
js.forEach((j, idx) => {
const zA = fs.cBottom + j * fs.sV;
for (const i of assemblyGridIndices(nL, fs.kB11, idx)) parts.push(dot('b11', 'B11', x(xsL[i]), y(zA), COLOR.b11, 2.2));
parts.push(line('b12', 'B12', xa, xb, y(zA + dB10), COLOR.b12, 1.2, '6 3'));
});
// Dimensions: the length line with ticks and the grid string, and the right-hand heights.
const g = grid(L, fs, 'sL');
const yDim = Y0 + 40;
parts.push(`<line x1="${x(-L / 2).toFixed(1)}" x2="${x(L / 2).toFixed(1)}" y1="${yDim}" y2="${yDim}" stroke="${COLOR.dim}" stroke-width="0.8"/>`);
parts.push(tick(x(-L / 2), yDim) + tick(x(L / 2), yDim));
parts.push(text(x(-L / 2) - 5, yDim + 14, `${fs.cSide}`, 'end', COLOR.dim));
parts.push(text(x(L / 2) + 5, yDim + 14, `${fs.cSide}`, 'start', COLOR.dim));
parts.push(text(x(-L / 2 + fs.cSide) + 2, yDim + 14, `${g.e.toFixed(1)}`, 'start', COLOR.dim));
parts.push(text(x(L / 2 - fs.cSide) - 2, yDim + 14, `${g.e.toFixed(1)}`, 'end', COLOR.dim));
parts.push(text(cx + 150, yDim - 6, `${g.n}@${fs.sL}=${g.G}`, 'middle', COLOR.dim));
parts.push(text(cx, 20, `교각축 단면 · 길이 ${L.toFixed(0)}`, 'middle', '#1F2937'));
const xr = X0 + L_PX + 18;
parts.push(`<line x1="${xr}" x2="${xr}" y1="${y(H).toFixed(1)}" y2="${y(0).toFixed(1)}" stroke="${COLOR.dim}" stroke-width="0.8"/>`);
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));
// Bubbles outside the outline with a leader to a point of their family.
const bubble = (name: string, bx: number, by: number, tx: number, ty: number, dotColor: string) => {
const edgeY = by < ty ? by + 11 : by - 11;
return `<g class="bubble" data-bubble="${name}"><line class="leader" data-bar="${name}" x1="${bx.toFixed(1)}" x2="${tx.toFixed(1)}" y1="${edgeY.toFixed(1)}" y2="${ty.toFixed(1)}" stroke="${COLOR.outline}" stroke-width="0.7"/><circle class="leader-point" data-bar="${name}" cx="${tx.toFixed(1)}" cy="${ty.toFixed(1)}" r="2.6" fill="${dotColor}"/><circle cx="${bx.toFixed(1)}" cy="${by.toFixed(1)}" r="11" fill="#fff" stroke="${COLOR.outline}"/><text x="${bx.toFixed(1)}" y="${(by + 3).toFixed(1)}" font-size="9" text-anchor="middle" fill="${COLOR.outline}">${name}</text></g>`;
};
const yTop = 40;
const zB11 = js.length > 0 ? fs.cBottom + js[0] * fs.sV : fs.cBottom;
const zB12 = zB11 + dB10;
parts.push(bubble('B5', xa + 12, yTop, xa + 12, y(z.B5), COLOR.b5));
parts.push(bubble('B12', xb - 40, yTop, xb, y(zB12), COLOR.b12));
parts.push(bubble('B11', xb - 12, yTop + 0, x(xsL[xsL.length - 2]), y(zB11), COLOR.b11));
parts.push(bubble('B10-2', X0 - 18, y(fs.cBottom + (levels - 1) * fs.sV / 2), x(-L / 2 + depth2), y(fs.cBottom + (levels - 1) * fs.sV / 2), COLOR.b10));
// Legend and units note.
const yLeg = 352;
parts.push(`<line x1="20" x2="44" y1="${yLeg}" y2="${yLeg}" stroke="${COLOR.b5}" stroke-width="1.6"/>${text(50, yLeg + 3, '주철근 (B5–B8)', 'start', COLOR.outline)}`);
parts.push(`<circle cx="180" cy="${yLeg}" r="2.6" fill="${COLOR.b14}"/>${text(188, yLeg + 3, '직각·보조 (B1–B4)', 'start', COLOR.outline)}`);
parts.push(`<line x1="330" x2="330" y1="${yLeg - 8}" y2="${yLeg + 8}" stroke="${COLOR.b9}" stroke-width="1"/>${text(338, yLeg + 3, '전단·띠 (B9)', 'start', COLOR.outline)}`);
parts.push(`<circle cx="20" cy="${yLeg + 22}" r="2.2" fill="${COLOR.b10}"/>${text(28, yLeg + 25, '측면 수평 (B10-1 · B10-2)', 'start', COLOR.outline)}`);
parts.push(`<line x1="250" x2="274" y1="${yLeg + 22}" y2="${yLeg + 22}" stroke="${COLOR.b11}" stroke-width="1.2" stroke-dasharray="6 3"/>${text(280, yLeg + 25, '조립 (B11·B12)', 'start', COLOR.outline)}`);
parts.push(text(20, yLeg + 48, '치수 mm · 높이 방향은 실제 비율 · 다리 생략 · 기준: 확대기초 하면 0', 'start', COLOR.outline));
return `<svg viewBox="0 0 600 430" width="600" height="430" role="img" aria-label="확대기초 교각축 단면 (길이 L)"><g transform="translate(0 20)">${parts.join('')}</g></svg>`;
}