From 550b508e7a4fb8df334b72720c67d6993b9bc21a Mon Sep 17 00:00:00 2001 From: ypshim Date: Mon, 26 Oct 2026 16:54:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(rebar):=20=EB=8B=A8=EB=A9=B4=20=EC=82=BD?= =?UTF-8?q?=EB=8F=84=20=EC=88=9C=EC=84=9C=20P3=C2=B7P4=C2=B7P5,=20?= =?UTF-8?q?=EC=B9=98=EC=88=98=EC=84=A0=C2=B7=EA=B7=9C=EA=B2=A9=20=ED=91=9C?= =?UTF-8?q?=EA=B8=B0=C2=B7=EC=BB=A4=ED=94=8C=EB=9F=AC=20=EC=A7=80=EC=8B=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단면 순서와 문자를 삽도에 맞춤: A 일반(P3), B 확대기초 안(P4, 반원 2·커플러 2), C 단부(P5, 커플러 1) - 각 단면에 지름 2000과 125 · 875 치수선 표시 (D, c, r1에서 계산) - 규격과 간격 표기: P1·P2 S32 Div.46, P3·P4·P5 H19 @ 입력값 - 커플러는 띠철근 위 점에서 지시선을 따라 원 밖의 작은 원으로 표시 - P4 형상과 기본 간격은 문서 값 유지 (입력으로 삽도 값 @100 입력 가능) Co-Authored-By: Claude Haiku 5.5 --- src/main.ts | 4 +- src/view/column-section-svg.ts | 69 ++++++++++++++++++++------------ tests/column-section-svg.test.ts | 10 +++-- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/main.ts b/src/main.ts index c1fb524..6aec065 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1251,8 +1251,8 @@ function renderColumnSettings(box: HTMLDivElement, pier: Pier): void { } const captions: Record = { A: 'A 일반 · P3 첫 단 · 주철근 P1·P2 · 띠철근 P3 (겹침 표시)', - B: 'B 단부 · P5 첫 단 · 커플러 1개 (θ + Δθ/2)', - C: 'C 확대기초 안 · P4 첫 단 · 커플러 2개 · P6 4본', + B: 'B 확대기초 안 · P4 첫 단 · 반원 2개 · 커플러 2개 · P6 4본', + C: 'C 단부 · P5 첫 단 · 커플러 1개 (θ + Δθ/2)', }; // 단면 A·B·C in one row (승인 2026-10-26): no wrapping, scrolls sideways on a narrow window. const row = el('div'); diff --git a/src/view/column-section-svg.ts b/src/view/column-section-svg.ts index 3ef4e71..f210fb6 100644 --- a/src/view/column-section-svg.ts +++ b/src/view/column-section-svg.ts @@ -1,55 +1,69 @@ 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). + * 기둥 배근 · 단면 A·B·C (승인 2026-10-26, 삽도 기준): plan views at the first level of each group, in the drawing's order. + * A 일반 = P3 첫 단 (full ring, no coupler), B 확대기초 안 = P4 첫 단 (2 semicircles, 2 couplers), C 단부 = P5 첫 단 (full ring, 1 coupler). + * P1 and P2 share each angle (r 875 and r 843.2). P6 is four chords through the centre only when a P6 level sits at the section. + * Dimensions and labels are read from the column input and 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' }; +const COLOR = { main: '#0F5C66', tie: '#6B3FA0', crossTie: '#B4531F', coupler: '#0F5C66', guide: '#55606E', dim: '#B42318' }; /** 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'; + const level = kind === 'A' ? col.p3[0] : kind === 'B' ? col.p4[0] : col.p5[0]; + const title = kind === 'A' ? 'A 일반 · P3' : kind === 'B' ? 'B 확대기초 안 · P4' : 'C 단부 · P5'; if (level === undefined) { return `단 없음`; } const cx = W / 2; const cy = H / 2 + 6; - const k = (H / 2 - 36) / col.tieRadius; + // Scale from the column outline (D/2), so the 2000 dimension and the 875 / 125 offsets fit in the frame. + const k = (H / 2 - 52) / (input.D / 2); 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 parts: string[] = []; + const text = (x: number, yy: number, s: string, anchor = 'start', fill = COLOR.guide) => + `${s}`; + + // Column outline (D) and the dimension lines: D above, c and r1 along the horizontal diameter. + const outer = input.D / 2 * k; + parts.push(``); + const dimY = cy - outer - 14; + parts.push(``); + parts.push(text(cx, dimY - 4, (input.D / 1000).toFixed(3), 'middle', COLOR.dim)); + const axisY = cy + 14; + const p1x = cx - col.r1 * k; + parts.push(text((cx - outer + p1x) / 2, axisY, `${input.D / 2 - col.r1}`, 'middle', COLOR.dim)); + parts.push(text((p1x + cx) / 2, axisY, `${col.r1}`, 'middle', COLOR.dim)); + + // Tie: a full ring (A, C) or two semicircles through the couplers (B). + if (kind === 'B') { 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). + // Couplers: a filled dot on the tie, with a leader to a small hollow circle outside the column (the 삽도 convention). + const couplerAngles = kind === 'B' ? [step / 2, step / 2 + 180] : kind === 'C' ? [step / 2] : []; + for (const deg of couplerAngles) { + const [x, y] = pt(col.tieRadius, deg); + const [ox, oy] = pt(input.D / 2 + 90, deg); + parts.push(``); + parts.push(``); + parts.push(``); + } + + // P6 chords through the centre, only at a P6 level of this section. if (col.p6Levels.some((z) => Math.abs(z - level) < 1e-6)) { for (const deg of P6_ANGLES) { const [x1, y1] = pt(col.tieRadius, deg); @@ -67,11 +81,14 @@ export function columnSectionSvg(kind: ColumnSection, input: ColumnInput, col: R parts.push(``); } - parts.push(text(8, 16, title)); + // Labels: the spec of each bar group with its live spacing, and the section level. + const spec = kind === 'A' ? `P1·P2 S32 Div.${n} · P3 H19 @${input.ctcP3}` : kind === 'B' ? `P4 H19 @${input.ctcP4}` : `P5 H19 @${input.ctcP5}`; + parts.push(text(8, 16, title, 'start', '#1F2937')); parts.push(text(8, 30, `z = ${level} mm`)); + parts.push(text(8, H - 24, spec, 'start', '#1F2937')); 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`)); + if (kind === 'B') parts.push(text(8, H - 10, `반원 ${col.p4SemicircleLength} × 2 · 커플러 2`)); + if (kind === 'C') parts.push(text(8, H - 10, `띠철근 둘레 ${col.tieCircumference} · 커플러 1`)); return `${parts.join('')}`; } diff --git a/tests/column-section-svg.test.ts b/tests/column-section-svg.test.ts index 4ac6508..8bec6ef 100644 --- a/tests/column-section-svg.test.ts +++ b/tests/column-section-svg.test.ts @@ -19,12 +19,14 @@ describe('기둥 단면 A·B·C (승인 2026-10-26, 추천)', () => { expect(count(svg, 'p6')).toBe(0); }); - it('B 단부: one coupler', () => { - expect(count(columnSectionSvg('B', input, col), 'coupler')).toBe(1); + it('C 단부 (P5): one coupler on a full ring', () => { + const svg = columnSectionSvg('C', input, col); + expect(count(svg, 'coupler')).toBe(1); + expect(count(svg, 'p5')).toBe(1); }); - it('C 확대기초 안: two semicircles, two couplers, and the four P6 chords at its P6 level', () => { - const svg = columnSectionSvg('C', input, col); + it('B 확대기초 안 (P4): two semicircles, two couplers, and the four P6 chords at its P6 level', () => { + const svg = columnSectionSvg('B', input, col); expect(count(svg, 'coupler')).toBe(2); expect(count(svg, 'p4')).toBe(2); expect(count(svg, 'p6')).toBe(4);