Files
Pier_v1/src/view/footing-section-svg.ts
T
ypshimandClaude Haiku 5.5 665b1c948a fix(rebar): 확대기초 단면 제목과 폭 치수 글자 겹침 해소
- 제목을 그림 위(y 20)로 옮겨 말뚝과 지시선과 겹치지 않게 함
- 양끝 피복은 치수선 바깥쪽에, 격자 여유(e)는 각각 안쪽 끝에 놓아 "16216"처럼 붙던 글자를 분리
- 치수선을 한 줄 내림 (Y0+40)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-26 18:04:36 +09:00

161 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 lines (bars along W), B1–B4 orange points on the grid along W, B9 purple lines per row,
* - B10-1 green points at both W faces per level, B11 grey dashed lines per assembly level, B12 grey points on its W grid.
* 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 560 360" width="560" height="360" 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));
// B5–B8: teal lines along W between the covers. B5 at the top, B6 and B8 at the 하면 1단, B7 at the 하면 2단.
const xa = x(-W / 2 + fs.cSide);
const xb = x(W / 2 - fs.cSide);
parts.push(line('b5', 'B5', xa, xb, y(z.B5), COLOR.b5, 1.6));
parts.push(line('b5', 'B6', xa, xb, y(z.B68), COLOR.b5, 1.2));
parts.push(line('b5', 'B7', xa, xb, y(z.B7), COLOR.b5, 1.2));
parts.push(line('b5', 'B8', xa, xb, y(z.B68), COLOR.b5, 1.2));
// B6 leg note: the leg runs from the 하면 1단 up to the B5 height (B6 다리 → B5 높이).
parts.push(text(cx, (y(z.B5) + y(z.B68)) / 2 - 3, `B6 다리 → B5 높이 ${(z.B5 - z.B68).toFixed(0)}`, 'middle', COLOR.b5));
// B1–B4 on the grid along W (교각축, orange points): odd grid positions carry B1, B2 and B3; even carry B4 (as bottomBars).
const xs = gridPositions(W, fs, 'sAxis');
xs.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 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));
// B10-1: green points at both W faces, one per level (9 levels), at the side depth of the table.
const levels = b10Levels(H, fs);
const dB1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, fs);
for (let j = 0; j < levels; j++) {
const zj = fs.cBottom + j * fs.sV;
parts.push(dot('b10', 'B10-1', x(-W / 2 + dB1), y(zj), COLOR.b10, 2.2));
parts.push(dot('b10', 'B10-1', x(W / 2 - dB1), y(zj), COLOR.b10, 2.2));
}
// 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, 'sAxis');
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, 'sAxis');
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));
parts.push(text(cx, yDim + 14, `${g.n}@${fs.sAxis}=${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));
// Bubbles outside the outline, spread apart, each with a leader to its bar (the leader carries the same data-bar, so it highlights too).
const bubble = (name: string, bx: number, by: number, tx: number, ty: number) => {
const edgeY = by < ty ? by + 11 : by - 11;
return `<g class="bubble" data-bubble="${name}" style="cursor:pointer"><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 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 yBot = Y0 + 66;
const zB11 = js.length > 0 ? fs.cBottom + js[0] * fs.sV : fs.cBottom;
parts.push(bubble('B5', xa + 12, yTop, xa + 12, y(z.B5)));
parts.push(bubble('B1', x(xs[1]) + 60, yTop, x(xs[1]), y(z.B1)));
parts.push(bubble('B11', xb - 20, yTop, xb, y(zB11)));
parts.push(bubble('B10-1', X0 - 18, y(fs.cBottom + (levels - 1) * fs.sV / 2), X0 + 2, y(fs.cBottom + (levels - 1) * fs.sV / 2)));
parts.push(bubble('B2', cx - 70, yBot, x(xs[1]), y(z.B24)));
parts.push(bubble('B4', cx, yBot, x(xs[2]), y(z.B24)));
parts.push(bubble('B3', cx + 70, yBot, x(xs[3]), y(z.B3)));
// Legend (선 모양별, 삽도 기준) and the units note.
const yLeg = 330;
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 · 높이 방향은 실제 비율 · 다리 생략 (B6 다리는 주석) · 기준: 확대기초 하면 0', 'start', COLOR.outline));
return `<svg viewBox="0 0 560 360" width="560" height="360" role="img" aria-label="확대기초 교각축 직각 단면">${parts.join('')}</svg>`;
}