diff --git a/src/main.ts b/src/main.ts index 66aa52b..0677265 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,6 +31,7 @@ import { COPING_REBAR_RANGES, copingSettingsIssues, type CopingRebarSettings, DE 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 { type ColumnSection, columnSectionSvg } from './view/column-section-svg'; import { groundSectionSvg } from './view/ground-svg'; import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table'; import { columnDiameterIssues } from './core/column-check'; @@ -1233,11 +1234,37 @@ function renderColumnSettings(box: HTMLDivElement, pier: Pier): void { 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); + // 정면 | 단면 A·B·C (승인 2026-10-26): 치수는 입력과 계산 결과, 간섭(B11–P4)은 검토 제외. + let columnView: 'front' | 'section' = 'front'; + const figBox = el('div'); + const drawColumnFigure = (): void => { + figBox.replaceChildren(toggleRow([['front', '정면'], ['section', '단면 A·B·C']] as const, columnView, (v) => { + columnView = v; + drawColumnFigure(); + })); + if (columnView === 'front') { + const fig = el('figure', undefined, 'ground-fig'); + fig.innerHTML = columnElevationSvg(input, col); + fig.appendChild(el('figcaption', '정면 · 높이 기준 = 확대기초 상면 (mm) · 간섭(B11–P4)은 검토 제외', 'muted small')); + figBox.appendChild(fig); + return; + } + const captions: Record = { + A: 'A 일반 · P3 첫 단 · 주철근 P1·P2 · 띠철근 P3 (겹침 표시)', + B: 'B 단부 · P5 첫 단 · 커플러 1개 (θ + Δθ/2)', + C: 'C 확대기초 안 · P4 첫 단 · 커플러 2개 · P6 4본', + }; + const row = el('div', undefined, 'opts'); + for (const kind of ['A', 'B', 'C'] as const) { + const fig = el('figure', undefined, 'ground-fig'); + fig.innerHTML = columnSectionSvg(kind, input, col); + fig.appendChild(el('figcaption', captions[kind], 'muted small')); + row.appendChild(fig); + } + figBox.appendChild(row); + }; + drawColumnFigure(); + wrap.appendChild(figBox); const msg = el('p', undefined, 'muted small'); const apply = el('button', '적용', 'btn small'); apply.type = 'button'; diff --git a/src/view/column-section-svg.ts b/src/view/column-section-svg.ts new file mode 100644 index 0000000..3ef4e71 --- /dev/null +++ b/src/view/column-section-svg.ts @@ -0,0 +1,77 @@ +import type { ColumnInput, columnRebar } from '../core/rebar/column'; + +/** + * 기둥 배근 · 단면 A·B·C (승인 2026-10-26, 추천): plan views at the first level of each group. + * A 일반 = P3 첫 단 (no coupler), B 단부 = P5 첫 단 (1 coupler at θ + Δθ/2), C 확대기초 안 = P4 첫 단 (2 semicircles, 2 couplers). + * P1 and P2 share each angle (r 875 and r 843.2). P6 is drawn as four chords through the centre only when a P6 level sits at the section. + * Every value comes from the column result; the section level is the first level of its group (no new calculation). + */ +export type ColumnSection = 'A' | 'B' | 'C'; + +const COLOR = { main: '#0F5C66', tie: '#6B3FA0', crossTie: '#B4531F', coupler: '#0F5C66', guide: '#55606E' }; +/** P6 chord angles (deg) from 기둥철근 (1).md §7: 0 · 46.96 · 90 · 133.04. */ +const P6_ANGLES = [0, 46.96, 90, 133.04]; + +export function columnSectionSvg(kind: ColumnSection, input: ColumnInput, col: ReturnType): string { + const W = 300; + const H = 300; + const level = kind === 'A' ? col.p3[0] : kind === 'B' ? col.p5[0] : col.p4[0]; + const title = kind === 'A' ? 'A 일반 · P3' : kind === 'B' ? 'B 단부 · P5' : 'C 확대기초 안 · P4'; + if (level === undefined) { + return `단 없음`; + } + const cx = W / 2; + const cy = H / 2 + 6; + const k = (H / 2 - 36) / col.tieRadius; + const pt = (r: number, deg: number): [number, number] => [cx + r * k * Math.cos((deg * Math.PI) / 180), cy - r * k * Math.sin((deg * Math.PI) / 180)]; + const n = input.nDiv; + const step = 360 / n; + const parts: string[] = []; + const text = (x: number, yy: number, s: string, anchor = 'start') => + `${s}`; + + // Tie: a full circle (A, B) or two semicircles (C) through the couplers. + const R = (col.tieRadius * k).toFixed(1); + if (kind === 'C') { + const [x1, y1] = pt(col.tieRadius, step / 2); + const [x2, y2] = pt(col.tieRadius, step / 2 + 180); + parts.push(``); + parts.push(``); + for (const deg of [step / 2, step / 2 + 180]) { + const [x, y] = pt(col.tieRadius, deg); + parts.push(``); + } + } else { + parts.push(``); + if (kind === 'B') { + const [x, y] = pt(col.tieRadius, step / 2); + parts.push(``); + } + } + + // P6 chords through the centre, only at a P6 level of this section (C). + if (col.p6Levels.some((z) => Math.abs(z - level) < 1e-6)) { + for (const deg of P6_ANGLES) { + const [x1, y1] = pt(col.tieRadius, deg); + const [x2, y2] = pt(col.tieRadius, deg + 180); + parts.push(``); + } + } + + // Main bars: P1 (r1) and P2 (r2) on the same angles. + for (let i = 0; i < n; i++) { + const deg = i * step; + const [x1, y1] = pt(col.r1, deg); + const [x2, y2] = pt(col.r2, deg); + parts.push(``); + parts.push(``); + } + + parts.push(text(8, 16, title)); + parts.push(text(8, 30, `z = ${level} mm`)); + if (kind === 'A') parts.push(text(8, H - 10, `띠철근 둘레 ${col.tieCircumference} · 겹침 ${col.lapP3}`)); + if (kind === 'B') parts.push(text(8, H - 10, `띠철근 둘레 ${col.tieCircumference} · 커플러 1`)); + if (kind === 'C') parts.push(text(8, H - 10, `반원 ${col.p4SemicircleLength} × 2 · 커플러 2`)); + + return `${parts.join('')}`; +} diff --git a/tests/column-section-svg.test.ts b/tests/column-section-svg.test.ts new file mode 100644 index 0000000..4ac6508 --- /dev/null +++ b/tests/column-section-svg.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier } from '../src/core/geometry'; +import { columnInputFor, columnRebar } from '../src/core/rebar/column'; +import { columnSectionSvg } from '../src/view/column-section-svg'; +import { loadP1 } from './helpers'; + +const count = (svg: string, cls: string) => svg.split(`class="${cls}"`).length - 1; + +describe('기둥 단면 A·B·C (승인 2026-10-26, 추천)', () => { + const m = buildPier(loadP1()); + const input = columnInputFor(m); + const col = columnRebar(input); + + it('A 일반: no coupler, P1 and P2 at all 46 angles, no P6 chords', () => { + const svg = columnSectionSvg('A', input, col); + expect(count(svg, 'coupler')).toBe(0); + expect(count(svg, 'p1')).toBe(input.nDiv); + expect(count(svg, 'p2')).toBe(input.nDiv); + expect(count(svg, 'p6')).toBe(0); + }); + + it('B 단부: one coupler', () => { + expect(count(columnSectionSvg('B', input, col), 'coupler')).toBe(1); + }); + + it('C 확대기초 안: two semicircles, two couplers, and the four P6 chords at its P6 level', () => { + const svg = columnSectionSvg('C', input, col); + expect(count(svg, 'coupler')).toBe(2); + expect(count(svg, 'p4')).toBe(2); + expect(count(svg, 'p6')).toBe(4); + }); +});