feat(rebar): 확대기초 교각축 직각 단면(폭 W) 삽도 1단계

- view/footing-section-svg.ts: B5–B8 선, B1–B4 점, B9 행 선, B10-1 측면 점, B11 선, B12 점을 계산 함수 결과로 그림
- 치수: 폭 W, 격자(e · n@s=G · e), 피복, B10 단 수 @ s_v, 높이 방향 축소 표시
- 풍선(B1–B5, B11, B2–B4, B10-1)을 누르면 그 철근만 강조, 다시 누르면 해제
- 테스트: 점·선 개수가 철근표 개수와 같음 (B1·B2·B4, B9 행, B10-1, B11 단, B12)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 17:42:03 +09:00
1 parent 6957054f4c
commit c91fe74d9a
3 files changed
+168

No files matched your search

+16
View File
@@ -32,6 +32,7 @@ import { COLUMN_REBAR_RANGES, type ColumnRebarSettings, columnSettingsIssues, DE
import { columnInputFor, columnRebar } from './core/rebar/column';
import { DEFAULT_FOOTING_REBAR, FOOTING_REBAR_RANGES, type FootingRebarSettings, footingSettingsIssues } from './core/rebar/footing-settings';
import { footingRuleValues } from './core/rebar/footing';
import { footingSectionSvg } from './view/footing-section-svg';
import { columnElevationSvg } from './view/column-elevation-svg';
import { type ColumnSection, columnSectionSvg } from './view/column-section-svg';
import { groundSectionSvg } from './view/ground-svg';
@@ -1372,9 +1373,24 @@ function renderFootingSettings(box: HTMLDivElement, pier: Pier): void {
applyFootingRebar(pier.input.name, next);
});
wrap.append(msg, apply);
// 교각축 직각 단면 (폭 W) (승인 2026-10-26, 추천안 1단계): a bubble click highlights that bar family; a second click clears it.
const sectionFig = el('figure', undefined, 'ground-fig');
sectionFig.innerHTML = footingSectionSvg(pier.model);
sectionFig.appendChild(el('figcaption', '교각축 직각 단면 (폭 W) · 풍선을 누르면 그 철근만 강조 · 높이 방향 축소 · 다리 생략', 'muted small'));
sectionFig.querySelectorAll<SVGGElement>('[data-bubble]').forEach((g) => g.addEventListener('click', () => highlightFootingBar(sectionFig, g.dataset.bubble ?? '')));
wrap.appendChild(sectionFig);
box.appendChild(wrap);
}
/** Highlights the marks of one bar family in a footing section (data-bar); the others dim. A second click on the same family clears it. */
function highlightFootingBar(fig: HTMLElement, name: string): void {
const active = fig.dataset.active === name ? '' : name;
fig.dataset.active = active;
fig.querySelectorAll<SVGElement>('[data-bar]').forEach((m) => {
m.style.opacity = active === '' || m.dataset.bar === active ? '1' : '0.2';
});
}
function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
if (!pier.model || !rebar3d) return;
const L = pier.model.copingL;
+111
View File
@@ -0,0 +1,111 @@
import type { PierModel } from '../core/geometry';
import { NOMINAL_DIAMETER } from '../core/rebar/common';
import { 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단계). A cut across the footing width: horizontal = W, vertical = height
* from the 하면 (drawn compressed, 높이 방향 축소). Every mark comes from the same functions as the rebar table and the 3D view:
* - B5–B8 are lines (bars along W), B1–B4 are points on the grid along W, B9 are vertical lines per row, B10-1 are side points per level,
* - B11 are lines per assembly level, B12 are points on its W grid (every k·s). Legs are not drawn in this view.
* Each mark carries data-bar="<철근 번호>" so a bubble can highlight its bars (철근표 강조와 같은 방식).
*/
const COLOR = { outline: '#55606E', axis: '#0F5C66', b1: '#0F5C66', b5: '#6B3FA0', b9: '#2E7D32', b10: '#B4531F', b11: '#1F2937', b12: '#7C3AED', dim: '#B42318' };
const W_PX = 400;
const X0 = 60;
const Y_BOTTOM = 200;
const SCALE_Y = 0.8;
export function footingSectionSvg(model: PierModel): string {
const foot = model.footings[0];
if (!foot) return '<svg viewBox="0 0 520 260" width="520" height="260" role="img" aria-label="확대기초 없음"><text x="20" y="130" 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 * SCALE_Y;
const cx = X0 + W_PX / 2;
const x = (mm: number) => cx + mm * kX;
const y = (z: number) => Y_BOTTOM - 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) =>
`<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}"/>`;
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}" text-anchor="${anchor}">${s}</text>`;
// Footing outline (W × H) and the 하면 / 상면 levels.
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: 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));
// B1–B4 on the grid along W (교각축, 점): odd grid positions carry B1, B2 and B3, even carry B4 (same parity 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.b1));
parts.push(dot('b2', 'B2', xx, y(z.B24), COLOR.axis));
parts.push(dot('b3', 'B3', xx, y(z.B3), COLOR.axis));
} else {
parts.push(dot('b4', 'B4', xx, y(z.B24), COLOR.axis));
}
});
// B9: one 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: side points at both W faces, one per level (9 levels).
const levels = b10Levels(H, fs);
const dB1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, fs); // B10-1 depth from the W face, as in the table and 3D
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: lines per assembly level (along L, shown as a line in this section). B12: points on its W grid, every k·s, 2-step stagger per level.
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));
const i0 = idx % 2 === 0 ? 0 : 2;
const xsB12 = gridPositions(W, fs, 'sAxis');
for (let i = i0; i < xsB12.length; i += fs.kB12) parts.push(dot('b12', 'B12', x(xsB12[i]), y(zA + dB10), COLOR.b12, 2.2));
});
// Dimensions: width, grid, covers, and the B10 levels.
const g = grid(W, fs, 'sAxis');
const yDim = Y_BOTTOM + 20;
parts.push(text(cx, 16, `교각축 직각 단면 · 폭 ${W.toFixed(0)}`, 'middle', '#1F2937'));
parts.push(text(X0, yDim, `${fs.cSide}`, 'start', COLOR.dim));
parts.push(text(cx, yDim, `${g.e.toFixed(1)} · ${g.n}@${fs.sAxis}=${g.G} · ${g.e.toFixed(1)}`, 'middle', COLOR.dim));
parts.push(text(X0 + W_PX, yDim, `${fs.cSide}`, 'end', COLOR.dim));
const xr = X0 + W_PX + 14;
parts.push(text(xr, y(H) + 10, `${fs.cTop}`, 'start', COLOR.dim));
parts.push(text(xr, (y(H) + y(0)) / 2, `${levels - 1}@${fs.sV}=${(levels - 1) * fs.sV}`, 'start', COLOR.dim));
parts.push(text(xr, y(0) - 4, `${fs.cBottom}`, 'start', COLOR.dim));
parts.push(text(X0, y(0) + 36, `높이 방향 축소 (×${SCALE_Y}) · 다리 생략 · 기준: 확대기초 하면 0`, 'start', COLOR.outline));
// Bubbles: one per bar family, linked to its marks through data-bar (click to highlight, in the panel).
const bubbles: [string, number, number][] = [['B5', xa + 6, 24], ['B1', x(xs[1]), 40], ['B11', xb - 6, 56], ['B2', x(xs[1]), y(0) + 40], ['B4', x(xs[2]), y(0) + 40], ['B3', x(xs[3]), y(0) + 40], ['B10-1', X0 - 16, y(fs.cBottom) - 6]];
for (const [name, bx, by] of bubbles) {
parts.push(`<g class="bubble" data-bubble="${name}" style="cursor:pointer"><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>`);
}
return `<svg viewBox="0 0 520 260" width="520" height="260" role="img" aria-label="확대기초 교각축 직각 단면">${parts.join('')}</svg>`;
}