feat(rebar): 기둥 단면 A·B·C (각 그룹 첫 단, 커플러 θ+Δθ/2, P6 현 4본)
- view/column-section-svg.ts: A 일반(P3, 커플러 없음), B 단부(P5, 커플러 1), C 확대기초 안(P4, 반원 2·커플러 2) - P1·P2는 같은 각도에 r 875·843.2로 배치, 띠철근 둘레·겹침·반원 길이 표시 - P6은 P6 단이 그 높이에 있을 때만 중심을 지나는 현 4본 (0 · 46.96 · 90 · 133.04°) - 기둥 패널에서 정면 ↔ 단면 A·B·C 전환 (단면 높이는 각 그룹의 첫 단, 새 계산 없음) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c82aff3713
commit
baaec9d7e2
3 files changed
+141
-5
No files matched your search
+32
-5
@@ -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<ColumnSection, string> = {
|
||||
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';
|
||||
|
||||
@@ -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<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';
|
||||
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;
|
||||
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 [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).
|
||||
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(`<line class="p6" x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${COLOR.crossTie}" stroke-width="1.2"/>`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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(`<circle class="p1" cx="${x1.toFixed(1)}" cy="${y1.toFixed(1)}" r="3" fill="${COLOR.main}"/>`);
|
||||
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));
|
||||
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 `<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" role="img" aria-label="단면 ${kind}">${parts.join('')}</svg>`;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user