From c82aff3713ddc135db3bb1e03a7d5dc249684e52 Mon Sep 17 00:00:00 2001 From: ypshim Date: Mon, 26 Oct 2026 16:37:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(rebar):=20=EA=B8=B0=EB=91=A5=20=EC=A0=95?= =?UTF-8?q?=EB=A9=B4=20=EC=82=BD=EB=8F=84=20(P=20=EB=B2=88=ED=98=B8,=20?= =?UTF-8?q?=EB=8B=A8=EB=B6=80=20=EB=B0=B0=EC=B9=98=EA=B5=AC=EA=B0=84=C2=B7?= =?UTF-8?q?=EA=B8=B0=EC=B4=88=20=EC=95=88=20=EA=B9=8A=EC=9D=B4=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=EA=B0=92=20=ED=91=9C=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/main.ts | 21 ++++++--- src/view/column-elevation-svg.ts | 74 ++++++++++++++++++++++++++++++ tests/column-elevation-svg.test.ts | 36 +++++++++++++++ 3 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 src/view/column-elevation-svg.ts create mode 100644 tests/column-elevation-svg.test.ts diff --git a/src/main.ts b/src/main.ts index 23c1767..66aa52b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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'; diff --git a/src/view/column-elevation-svg.ts b/src/view/column-elevation-svg.ts new file mode 100644 index 0000000..acfd5db --- /dev/null +++ b/src/view/column-elevation-svg.ts @@ -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; + +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) => + `${s}`; + const hline = (z: number, cls: string, color: string, dash = '') => + ``; + + // Column outline (기둥 상단 = Hc) and the coping top (Hc + h_cop). + lines.push(``); + lines.push(``); + lines.push(text(20, y(0) - 4, '확대기초 상면 0')); + lines.push(``); + lines.push(text(20, y(input.Hc) - 4, `기둥 상단 ${input.Hc}`)); + lines.push(``); + 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(``); + for (const x of [xl(r2), xr(r2)]) lines.push(``); + + // 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(``); + + // 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(``); + 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 `${lines.join('')}`; +} diff --git a/tests/column-elevation-svg.test.ts b/tests/column-elevation-svg.test.ts new file mode 100644 index 0000000..4370d9c --- /dev/null +++ b/tests/column-elevation-svg.test.ts @@ -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); + }); +});