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>
This commit is contained in:
1 parent
d3c761dcd1
commit
0e9d89951a
3 files changed
+211
-15
No files matched your search
@@ -0,0 +1,144 @@
|
||||
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>`;
|
||||
}
|
||||
Reference in new issue
Block a user