feat(rebar): 단면 삽도 순서 P3·P4·P5, 치수선·규격 표기·커플러 지시선

- 단면 순서와 문자를 삽도에 맞춤: 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 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 16:54:27 +09:00
1 parent caadc2fe77
commit 550b508e7a
3 files changed
+51 -32

No files matched your search

+2 -2
View File
@@ -1251,8 +1251,8 @@ function renderColumnSettings(box: HTMLDivElement, pier: Pier): void {
}
const captions: Record<ColumnSection, string> = {
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');
+43 -26
View File
@@ -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<typeof columnRebar>): 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 `<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" role="img" aria-label="단면 ${kind}"><text x="20" y="150" font-size="12" fill="${COLOR.guide}">단 없음</text></svg>`;
}
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') =>
`<text x="${x.toFixed(1)}" y="${yy.toFixed(1)}" font-size="11" font-family="JetBrains Mono, monospace" fill="${COLOR.guide}" text-anchor="${anchor}">${s}</text>`;
// 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) =>
`<text x="${x.toFixed(1)}" y="${yy.toFixed(1)}" font-size="11" font-family="JetBrains Mono, monospace" fill="${fill}" text-anchor="${anchor}">${s}</text>`;
// Column outline (D) and the dimension lines: D above, c and r1 along the horizontal diameter.
const outer = input.D / 2 * k;
parts.push(`<circle cx="${cx}" cy="${cy}" r="${outer.toFixed(1)}" fill="none" stroke="${COLOR.guide}" stroke-width="1"/>`);
const dimY = cy - outer - 14;
parts.push(`<line x1="${(cx - outer).toFixed(1)}" x2="${(cx + outer).toFixed(1)}" y1="${dimY.toFixed(1)}" y2="${dimY.toFixed(1)}" stroke="${COLOR.dim}" stroke-width="0.8"/>`);
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(`<path class="p4" d="M ${x1.toFixed(1)} ${y1.toFixed(1)} A ${R} ${R} 0 0 1 ${x2.toFixed(1)} ${y2.toFixed(1)}" fill="none" stroke="${COLOR.tie}" stroke-width="1.6"/>`);
parts.push(`<path class="p4" d="M ${x2.toFixed(1)} ${y2.toFixed(1)} A ${R} ${R} 0 0 1 ${x1.toFixed(1)} ${y1.toFixed(1)}" fill="none" stroke="${COLOR.tie}" stroke-width="1.6" stroke-dasharray="4 2"/>`);
for (const deg of [step / 2, step / 2 + 180]) {
const [x, y] = pt(col.tieRadius, deg);
parts.push(`<circle class="coupler" cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3.5" fill="${COLOR.coupler}"/>`);
}
} else {
parts.push(`<circle class="${kind === 'A' ? 'p3' : 'p5'}" cx="${cx}" cy="${cy}" r="${R}" fill="none" stroke="${COLOR.tie}" stroke-width="1.6"/>`);
if (kind === 'B') {
const [x, y] = pt(col.tieRadius, step / 2);
parts.push(`<circle class="coupler" cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3.5" fill="${COLOR.coupler}"/>`);
}
}
// 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(`<line x1="${x.toFixed(1)}" y1="${y.toFixed(1)}" x2="${ox.toFixed(1)}" y2="${oy.toFixed(1)}" stroke="${COLOR.guide}" stroke-width="0.8"/>`);
parts.push(`<circle class="coupler" cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3.5" fill="${COLOR.coupler}"/>`);
parts.push(`<circle cx="${ox.toFixed(1)}" cy="${oy.toFixed(1)}" r="4" fill="none" stroke="${COLOR.tie}" stroke-width="1.2"/>`);
}
// 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(`<circle class="p2" cx="${x2.toFixed(1)}" cy="${y2.toFixed(1)}" r="2.2" fill="#fff" stroke="${COLOR.main}" stroke-width="1.2"/>`);
}
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 `<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" role="img" aria-label="단면 ${kind}">${parts.join('')}</svg>`;
}
+6 -4
View File
@@ -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);