feat(rebar): 기둥 정면 삽도 (P 번호, 단부 배치구간·기초 안 깊이 계산값 표시)

- view/column-elevation-svg.ts: 기둥 정면 SVG. 띠철근 P3·P5·P4·P6, 주철근 P1·P2, 커플러를 계산 결과 그대로 그림
- 번호는 P1–P6 (승인), 간섭(B11–P4)은 표시하지 않음 (검토 제외)
- 단부 배치구간 l_e = max(l_e,min, n·s + s/2)와 기초 안 깊이(0.5·H_f → m4·s)는 기존 계산값을 그림과 캡션에 표시
- 기둥 입력 라벨 K → P 번호

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 16:37:36 +09:00
1 parent f619b9ea20
commit c82aff3713
3 files changed
+124 -7

No files matched your search

+14 -7
View File
@@ -30,6 +30,7 @@ import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/beari
import { COPING_REBAR_RANGES, copingSettingsIssues, type CopingRebarSettings, DEFAULT_COPING_REBAR, DEFAULT_TOP_SPLICE, type TopSpliceSettings } from './core/rebar/coping-settings';
import { COLUMN_REBAR_RANGES, type ColumnRebarSettings, columnSettingsIssues, DEFAULT_COLUMN_REBAR } from './core/rebar/column-settings';
import { columnInputFor, columnRebar } from './core/rebar/column';
import { columnElevationSvg } from './view/column-elevation-svg';
import { groundSectionSvg } from './view/ground-svg';
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
import { columnDiameterIssues } from './core/column-check';
@@ -1187,11 +1188,11 @@ function renderCopingSettings(box: HTMLDivElement, pier: Pier): void {
/** 기둥 배근 입력 (승인 2026-10-26, 시안 ③ 기둥 삽도). The 주철근 하단 갈고리 is fixed. The 띠철근 간격 is three inputs (K3 · K5 · K4). */
const COLUMN_REBAR_FIELDS: { key: keyof ColumnRebarSettings; label: string }[] = [
{ key: 'c', label: 'K1 중심 피복 c (mm)' },
{ key: 'ctcP3', label: '띠철근 간격 K3 (mm)' },
{ key: 'ctcP5', label: '띠철근 간격 K5 (mm)' },
{ key: 'ctcP4', label: '띠철근 간격 K4 (mm)' },
{ key: 'ctcP6', label: '크로스타이 K6 간격 (mm)' },
{ key: 'c', label: 'P1 중심 피복 c (mm)' },
{ key: 'ctcP3', label: '띠철근 간격 P3 (mm)' },
{ key: 'ctcP5', label: '띠철근 간격 P5 (mm)' },
{ key: 'ctcP4', label: '띠철근 간격 P4 (mm)' },
{ key: 'ctcP6', label: '크로스타이 P6 간격 (mm)' },
{ key: 'ctcCoupler', label: '커플러 간격 ctcOfcoupler (mm)' },
{ key: 'dzCp2', label: 'P2 커플러 상승 Δz (mm)' },
{ key: 'nDiv', label: '주철근 원주 등분 수 n_div' },
@@ -1229,8 +1230,14 @@ function renderColumnSettings(box: HTMLDivElement, pier: Pier): void {
}
wrap.appendChild(grid);
// Read-only results of the current model (계산 값).
const col = columnRebar(columnInputFor(pier.model));
wrap.appendChild(el('p', `K2 내측 r ${col.r2} mm · P1 ${col.mainP1}본 · P2 ${col.mainP2}본 · 주철근 커플러 ${col.couplers}개`, 'muted small'));
const input = columnInputFor(pier.model);
const col = columnRebar(input);
wrap.appendChild(el('p', `P1 외측 r ${col.r1} · P2 내측 r ${col.r2} mm · 주철근 P1 ${col.mainP1}본 · P2 ${col.mainP2}본 · 주철근 커플러 ${col.couplers}개`, 'muted small'));
// 정면 (승인 2026-10-26, 시안 ③ 기둥): 치수는 입력과 계산 결과, 간섭(B11–P4)은 검토 제외.
const fig = el('figure', undefined, 'ground-fig');
fig.innerHTML = columnElevationSvg(input, col);
fig.appendChild(el('figcaption', '정면 · 높이 기준 = 확대기초 상면 (mm) · 간섭(B11–P4)은 검토 제외', 'muted small'));
wrap.appendChild(fig);
const msg = el('p', undefined, 'muted small');
const apply = el('button', '적용', 'btn small');
apply.type = 'button';
+74
View File
@@ -0,0 +1,74 @@
import type { ColumnInput, columnRebar } from '../core/rebar/column';
/**
* 기둥 배근 · 정면 (승인 2026-10-26, 시안 ③ 기둥 삽도). y = height from the 확대기초 상면 (mm, up +), drawn to scale in y.
* Names are the P numbers of the 철근표 (P1–P6). Every value is read from the column model and columnRebar; nothing is recomputed here.
* 간섭 (B11–P4) is not checked and not shown (승인 2026-10-26).
*/
type ColumnResult = ReturnType<typeof columnRebar>;
const COLOR = { main: '#0F5C66', tie: '#6B3FA0', crossTie: '#B4531F', coupler: '#0F5C66', guide: '#55606E' };
export function columnElevationSvg(input: ColumnInput, col: ColumnResult): string {
const W = 520;
const H = 600;
const top = input.Hc + input.hcop + 300;
const bottom = Math.min(col.zBot, ...col.p4, 0) - 400;
const k = (H - 60) / (top - bottom);
const y = (z: number) => 30 + (top - z) * k;
const cx = 230;
const half = (input.D / 2) * k;
const r1 = col.r1 * k;
const r2 = col.r2 * k;
const xl = (r: number) => cx - r;
const xr = (r: number) => cx + r;
const lines: string[] = [];
const text = (x: number, yy: number, s: string, anchor = 'start', fill = COLOR.guide) =>
`<text x="${x.toFixed(1)}" y="${yy.toFixed(1)}" font-size="11" font-family="JetBrains Mono, monospace" fill="${fill}" text-anchor="${anchor}">${s}</text>`;
const hline = (z: number, cls: string, color: string, dash = '') =>
`<line class="${cls}" x1="${(cx - half).toFixed(1)}" x2="${(cx + half).toFixed(1)}" y1="${y(z).toFixed(1)}" y2="${y(z).toFixed(1)}" stroke="${color}" stroke-width="1.2"${dash ? ` stroke-dasharray="${dash}"` : ''}/>`;
// Column outline (기둥 상단 = Hc) and the coping top (Hc + h_cop).
lines.push(`<rect x="${(cx - half).toFixed(1)}" y="${y(input.Hc).toFixed(1)}" width="${(2 * half).toFixed(1)}" height="${(y(0) - y(input.Hc)).toFixed(1)}" fill="none" stroke="${COLOR.guide}" stroke-width="1"/>`);
lines.push(`<line x1="20" x2="${W - 20}" y1="${y(0).toFixed(1)}" y2="${y(0).toFixed(1)}" stroke="${COLOR.guide}" stroke-width="1.5"/>`);
lines.push(text(20, y(0) - 4, '확대기초 상면 0'));
lines.push(`<line x1="20" x2="${W - 20}" y1="${y(input.Hc).toFixed(1)}" y2="${y(input.Hc).toFixed(1)}" stroke="${COLOR.guide}" stroke-dasharray="4 3"/>`);
lines.push(text(20, y(input.Hc) - 4, `기둥 상단 ${input.Hc}`));
lines.push(`<line x1="20" x2="${W - 20}" y1="${y(input.Hc + input.hcop).toFixed(1)}" y2="${y(input.Hc + input.hcop).toFixed(1)}" stroke="${COLOR.guide}"/>`);
lines.push(text(20, y(input.Hc + input.hcop) - 4, `코핑 상면 ${input.Hc + input.hcop}`));
// P3 (일반 구간), P5 (단부), P4 (기초 안) horizontal ties; P6 crosstie levels.
for (const z of col.p3) lines.push(hline(z, 'p3', COLOR.tie));
for (const z of col.p5) lines.push(hline(z, 'p5', COLOR.tie));
for (const z of col.p4) lines.push(hline(z, 'p4', COLOR.tie, '3 2'));
for (const z of col.p6Levels) lines.push(hline(z, 'p6', COLOR.crossTie, '6 3'));
// P1 (outer) and P2 (inner) main bars, from the bottom hook to their top ends.
for (const x of [xl(r1), xr(r1)]) lines.push(`<line class="p1" x1="${x.toFixed(1)}" x2="${x.toFixed(1)}" y1="${y(col.zBot).toFixed(1)}" y2="${y(col.zTop1).toFixed(1)}" stroke="${COLOR.main}" stroke-width="2"/>`);
for (const x of [xl(r2), xr(r2)]) lines.push(`<line class="p2" x1="${x.toFixed(1)}" x2="${x.toFixed(1)}" y1="${y(col.zBot).toFixed(1)}" y2="${y(col.zTop2).toFixed(1)}" stroke="${COLOR.main}" stroke-width="1.5"/>`);
// Main-bar couplers on P1 (P2 is offset by dzCp2).
for (const z of col.couplerP1) for (const x of [xl(r1), xr(r1)]) lines.push(`<circle class="coupler" cx="${x.toFixed(1)}" cy="${y(z).toFixed(1)}" r="3" fill="${COLOR.coupler}"/>`);
// Labels on the right, one line per bar group, with the values that produced them.
const lx = cx + half + 30;
const lab: [string, string][] = [
['P3', `일반 @${input.ctcP3} · ${col.p3.length}개`],
['P5', `단부 @${input.ctcP5} · ${col.p5.length}개`],
['P4', `기초 안 @${input.ctcP4} · ${col.p4.length}개`],
['P6', `크로스타이 @${input.ctcP6} · ${col.p6Levels.length}단`],
['P1', `주철근 외측 r ${col.r1} · ${col.mainP1}본`],
['P2', `주철근 내측 r ${col.r2} · P2 위 끝 = 기둥 상단 + l_d ${Math.round(col.zTop2 - input.Hc)}`],
['커플러', `@${input.ctcCoupler} · P2 +${input.dzCp2}`],
];
lab.forEach(([name, s], i) => {
const yy = 40 + i * 44;
lines.push(`<circle cx="${lx}" cy="${yy}" r="12" fill="#fff" stroke="${COLOR.tie}"/>`);
lines.push(text(lx, yy + 4, name, 'middle', COLOR.tie));
lines.push(text(lx + 18, yy + 4, s, 'start', '#1F2937'));
});
lines.push(text(lx, 40 + lab.length * 44 + 10, `단부 배치구간 l_e ${col.lE} = max(${col.lEmin}, ${Math.floor(col.lEmin / input.ctcP5)}@${input.ctcP5} + ${input.ctcP5 / 2})`, 'start', '#1F2937'));
lines.push(text(lx, 40 + lab.length * 44 + 26, `기초 안 깊이 ${Math.round(input.rP4 * input.Hf)} (0.5 × H_f) → ${col.p4.length}@${input.ctcP4}`, 'start', '#1F2937'));
return `<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" role="img" aria-label="기둥 배근 정면">${lines.join('')}</svg>`;
}
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { columnInputFor, columnRebar } from '../src/core/rebar/column';
import { columnElevationSvg } from '../src/view/column-elevation-svg';
import { loadP1 } from './helpers';
const count = (svg: string, cls: string) => svg.split(`class="${cls}"`).length - 1;
describe('기둥 정면 삽도 (승인 2026-10-26)', () => {
const m = buildPier(loadP1());
const input = columnInputFor(m);
const col = columnRebar(input);
const svg = columnElevationSvg(input, col);
it('draws one line per computed tie level (P3, P5, P4, P6)', () => {
expect(count(svg, 'p3')).toBe(col.p3.length);
expect(count(svg, 'p5')).toBe(col.p5.length);
expect(count(svg, 'p4')).toBe(col.p4.length);
expect(count(svg, 'p6')).toBe(col.p6Levels.length);
});
it('labels use the P numbers and the values that produced them', () => {
expect(svg).toContain('@300');
expect(svg).toContain('@120');
expect(svg).toContain('@3000');
expect(svg).toContain(`l_e ${col.lE} = max(${col.lEmin}, 19@120 + 60)`);
expect(svg).toContain(`기초 안 깊이 ${Math.round(input.rP4 * input.Hf)}`);
expect(svg).not.toMatch(/\bK[1-6]\b/);
});
it('draws the P1 and P2 bars on both sides (4 vertical lines)', () => {
expect(count(svg, 'p1')).toBe(2);
expect(count(svg, 'p2')).toBe(2);
expect(count(svg, 'coupler')).toBe(col.couplerP1.length * 2);
});
});