Files
Pier_v1/src/view/footing-section-svg.ts
T
ypshimandClaude Haiku 5.5 77ad63fcbf feat(rebar): 확대기초 단면 그림에서 B1–B4와 B10-1 제외
- B1–B4 선, B2 다리선, B1–B4 풍선 제거 (그림에서만, 철근표·3D는 그대로)
- B10-1 점과 풍선 제거, 범례의 B1–B4·B10 항목 제거
- 테스트 갱신

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-27 11:19:36 +09:00

160 lines
11 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';
/**
* 확대기초 · 교각축 직각 단면 (폭 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 '<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 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 = '') =>
`<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"/>`;
// 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) =>
`<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 on W, above the footing) and the piles (below, one stub per distinct W position).
const colD = model.columnDiameterMm;
parts.push(`<rect class="column" x="${x(-colD / 2).toFixed(1)}" y="${y(H) - 36}" width="${(colD * kX).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 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(`<rect class="pile" x="${(x(n) - d * kX / 2).toFixed(1)}" y="${Y0}" width="${Math.max(3, d * kX).toFixed(1)}" height="26" fill="${COLOR.pile}" stroke="#3b4656" stroke-width="0.6"/>`);
}
// Footing outline (W × H) and the 하면 line.
parts.push(`<rect x="${x(-W / 2).toFixed(1)}" y="${y(H).toFixed(1)}" width="${(W * kX).toFixed(1)}" height="${(H * kY).toFixed(1)}" fill="none" stroke="${COLOR.outline}" stroke-width="1.2"/>`);
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(`<line x1="${x(-W / 2).toFixed(1)}" x2="${x(W / 2).toFixed(1)}" y1="${yDim}" y2="${yDim}" stroke="${COLOR.dim}" stroke-width="0.8"/>`);
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(`<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));
// 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(`<line class="b6note" x1="${(xr + 4).toFixed(1)}" x2="${xb.toFixed(1)}" y1="${(yNote + 4).toFixed(1)}" y2="${y(z.B68).toFixed(1)}" stroke="${COLOR.b5}" stroke-width="0.7"/>`);
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 `<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 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(`<circle cx="20" cy="${yLeg}" r="2.6" fill="${COLOR.b5}"/>${text(28, yLeg + 3, '주철근 (B5–B8)', '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(`<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 · 높이 방향은 실제 비율 · 다리 생략 (B6 다리는 주석) · 기준: 확대기초 하면 0', 'start', COLOR.outline));
// The whole drawing shifts down 20 px so the top bubbles are not clipped.
return `<svg viewBox="0 0 600 430" width="600" height="430" role="img" aria-label="확대기초 교각축 직각 단면"><g transform="translate(0 20)">${parts.join('')}</g></svg>`;
}