diff --git a/src/core/changes.ts b/src/core/changes.ts index ce00698..53e85c8 100644 --- a/src/core/changes.ts +++ b/src/core/changes.ts @@ -23,7 +23,7 @@ export const EDITABLE_FIELDS = [ ] as const; /** Inputs tracked for overrides and pending changes: the numeric cards plus 열별 본수 (an array, compared by value). */ -export const TRACKED_KEYS = [...EDITABLE_FIELDS.map((f) => f.key), 'pileRows', 'groundPosition', 'groundWaterOn', 'groundWaterEl', 'floodLevelEl', 'scourDepthM', 'highWaterEl', 'lowWaterEl', 'strataRows', 'bearingType', 'bearingLayout', 'blockRebar', 'copingRebar', 'topSplice'] as const; +export const TRACKED_KEYS = [...EDITABLE_FIELDS.map((f) => f.key), 'pileRows', 'groundPosition', 'groundWaterOn', 'groundWaterEl', 'floodLevelEl', 'scourDepthM', 'highWaterEl', 'lowWaterEl', 'strataRows', 'bearingType', 'bearingLayout', 'blockRebar', 'copingRebar', 'topSplice', 'columnRebar'] as const; export type EditableKey = (typeof TRACKED_KEYS)[number]; @@ -56,6 +56,7 @@ export const FIELD_LABELS: Record = { blockRebar: '받침블럭 철근 설정', copingRebar: '코핑 배근 입력', topSplice: '상면 주철근 이음', + columnRebar: '기둥 배근 입력', strataRows: '지층 행', }; diff --git a/src/core/geometry.ts b/src/core/geometry.ts index 1198c2e..b705042 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -2,6 +2,7 @@ import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './ax import { buildIntegratedBlocks, type IntegratedBlock } from './integrated-block'; import { type BlockRebarSettings, DEFAULT_BLOCK_REBAR } from './rebar/bearing-block'; import { type CopingRebarSettings, DEFAULT_COPING_REBAR, DEFAULT_TOP_SPLICE, type TopSpliceSettings } from './rebar/coping-settings'; +import { type ColumnRebarSettings, DEFAULT_COLUMN_REBAR } from './rebar/column-settings'; import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles'; import type { PierInput, Row } from './types'; import { type CopingGeom, P1_COPING_GEOM } from './coping-geom'; @@ -181,6 +182,8 @@ export interface PierModel { copingRebar: CopingRebarSettings; /** 상면 주철근 이음 used by the coping rebar (승인 2026-10-26). */ topSplice: TopSpliceSettings; + /** 기둥 배근 입력 used by the column rebar (승인 2026-10-26). */ + columnRebar: ColumnRebarSettings; columns: ColumnModel[]; footings: FootingModel[]; blindings: BlindingModel[]; @@ -471,6 +474,7 @@ export function buildPier(input: PierInput): PierModel { blockRebar: { ...DEFAULT_BLOCK_REBAR, ...input.blockRebar }, copingRebar: { ...DEFAULT_COPING_REBAR, ...input.copingRebar }, topSplice: { ...DEFAULT_TOP_SPLICE, ...input.topSplice }, + columnRebar: { ...DEFAULT_COLUMN_REBAR, ...input.columnRebar }, columns, footings, blindings, diff --git a/src/core/rebar/column-settings.ts b/src/core/rebar/column-settings.ts new file mode 100644 index 0000000..a4bf367 --- /dev/null +++ b/src/core/rebar/column-settings.ts @@ -0,0 +1,70 @@ +/** + * 기둥 배근 입력 (승인 2026-10-26, 시안 ③ 기둥 삽도). The inputs of the 기둥 panel; the names match ColumnInput. + * Defaults are the 기둥철근 (1).md P1 values; the ranges are proposed and pending the design-standard check. + * The 주철근 하단 갈고리 is fixed (no input). + */ +export interface ColumnRebarSettings { + /** K1 중심 피복 c (기둥 면 ~ P1 중심, mm). */ + c: number; + /** 띠철근 간격 K3 (일반 구간, mm). */ + ctcP3: number; + /** 띠철근 간격 K5 (단부, mm). */ + ctcP5: number; + /** 띠철근 간격 K4 (기초 안, mm). */ + ctcP4: number; + /** 크로스타이 K6 간격 (단 간격, mm). */ + ctcP6: number; + /** 주철근 커플러 간격 (기초 상면부터, mm). */ + ctcCoupler: number; + /** P2 커플러를 P1보다 올리는 높이 (mm). */ + dzCp2: number; + /** 주철근 원주 등분 수 n_div (P1·P2 공통, 짝수). */ + nDiv: number; +} + +export const DEFAULT_COLUMN_REBAR: ColumnRebarSettings = { + c: 125, + ctcP3: 300, + ctcP5: 120, + ctcP4: 120, + ctcP6: 1800, + ctcCoupler: 3000, + dzCp2: 150, + nDiv: 46, +}; + +/** Allowed range per input (inclusive, mm). */ +export const COLUMN_REBAR_RANGES: Record = { + c: [100, 150], + ctcP3: [200, 400], + ctcP5: [80, 200], + ctcP4: [80, 200], + ctcP6: [1200, 2400], + ctcCoupler: [2000, 4000], + dzCp2: [0, 300], + nDiv: [40, 50], +}; + +export interface ColumnSettingsIssue { + code: 'CL01' | 'CL02'; + key: keyof ColumnRebarSettings; + message: string; +} + +/** + * CL01: a value is not a finite number or is out of range (W). CL02: n_div is odd (the main bars come in pairs around the circle). + */ +export function columnSettingsIssues(s: ColumnRebarSettings): ColumnSettingsIssue[] { + const out: ColumnSettingsIssue[] = []; + for (const key of Object.keys(COLUMN_REBAR_RANGES) as (keyof ColumnRebarSettings)[]) { + const v = s[key]; + const [lo, hi] = COLUMN_REBAR_RANGES[key]; + if (!Number.isFinite(v) || v < lo || v > hi) { + out.push({ code: 'CL01', key, message: `CL01 ${key} ${v} 범위 밖 (${lo}–${hi})` }); + } + } + if (Number.isFinite(s.nDiv) && s.nDiv % 2 !== 0) { + out.push({ code: 'CL02', key: 'nDiv', message: `CL02 주철근 등분 수 ${s.nDiv}는 홀수 (짝수만 허용)` }); + } + return out; +} diff --git a/src/core/rebar/column.ts b/src/core/rebar/column.ts index abc3e6a..33ebd9f 100644 --- a/src/core/rebar/column.ts +++ b/src/core/rebar/column.ts @@ -1,6 +1,7 @@ import type { PierModel } from '../geometry'; import { DEFAULT_MATERIAL, developmentLength, fyOf, hookLength, Material, NOMINAL_DIAMETER, spliceLength } from './common'; import { layers } from './footing'; +import { DEFAULT_COLUMN_REBAR } from './column-settings'; /** * 기둥 철근 C4 (plan C4). Source: RAG/기둥철근 (1).md, 2026-10-22 최종 정리. Type-1 원형 중실 기둥. @@ -54,26 +55,19 @@ export function columnInputFor(model: PierModel, base: ColumnInput = COLUMN_P1): const eP1 = Math.round((seg.top - (c3 - 0.1)) * 1000); // e_top: the coping middle level (Level 하면 + 계단 상면)/2 is the top of P3 and P6 (승인 2026-10-23). const eTop = Math.round((seg.top - (model.levelBottom + seg.top) / 2) * 1000); - return { ...base, Hc, hcop, eP1, eTop, D: model.columnDiameterMm }; + return { ...base, ...model.columnRebar, Hc, hcop, eP1, eTop, D: model.columnDiameterMm }; } export const COLUMN_P1: ColumnInput = { D: 2000, Hc: 12703, hcop: 2644, - c: 125, + ...DEFAULT_COLUMN_REBAR, eP1: 485, // Coping middle level: (Level 하면 + 계단 상면)/2 on P1 = 42.233 → e_top 1322 (승인 2026-10-23, Q-C3 대체). eTop: 1322, Hf: 2000, - nDiv: 46, - ctcCoupler: 3000, - dzCp2: 150, - ctcP3: 300, - ctcP5: 120, - ctcP4: 120, rP4: 0.5, - ctcP6: 1800, }; /** A bar in the 확대기초 top layer, height from the 확대기초 상면 (mm, negative = below). */ diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index d1d38ec..88cf10f 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -1,6 +1,7 @@ import type { PierModel } from '../geometry'; import { barLength, bearingBlockRebar, DEFAULT_BLOCK_REBAR, totals } from './bearing-block'; import { DEFAULT_COPING_REBAR } from './coping-settings'; +import { DEFAULT_COLUMN_REBAR } from './column-settings'; import { bottomPiece, topBarLength, widthLayout, topSplicePieces } from './coping-main'; import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, stairBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping'; import { sideRowLevels } from '../coping-geom'; @@ -147,6 +148,17 @@ export function linkedSpacing(name: string, model: PierModel): Pick ({ length, count: col.mainP1 })), note: `외측 r ${col.r1}, 마디 ${col.p1Pieces.join(' / ')} (첫 마디에 갈고리 ${col.p1Hook}, 기둥철근.md §3.2)` }); rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, pieces: col.p2Pieces.map((length) => ({ length, count: col.mainP2 })), note: `내측 r ${col.r2}, 마디 ${col.p2Pieces.join(' / ')}` }); - rows.push({ member: 'column', name: '주철근 커플러', size: 'D32용 (L150 Ø60)', count: col.couplers, length: 150, note: '@3000, P2는 +150 mm 상향 엇갈림' }); + rows.push({ member: 'column', name: '주철근 커플러', size: 'D32용 (L150 Ø60)', count: col.couplers, length: 150, note: `@${model.columnRebar.ctcCoupler}, P2는 +${model.columnRebar.dzCp2} mm 상향 엇갈림` }); const tieCouplerCount = col.p5Count + col.p4Count * 2; rows.push({ member: 'column', name: '띠철근 커플러', size: 'D19용 (L100 Ø40)', count: tieCouplerCount, length: 100, note: `단부 P5 1개(${col.p5Count}) · 기초안 P4 2개(${col.p4Count * 2}) 폐합용` }); - rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @300, 겹침 ${col.lapP3} (추천안, Q-C6)` }); - rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: '단부 띠철근 @120, 커플러 1개' }); + rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @${model.columnRebar.ctcP3}, 겹침 ${col.lapP3} (추천안, Q-C6)` }); + rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: `단부 띠철근 @${model.columnRebar.ctcP5}, 커플러 1개` }); rows.push({ member: 'column', name: 'P4', size: 'H19', count: col.p4Count, length: col.p4SemicircleLength * 2, note: '확대기초 안, 반원 2개 (길이는 반원 2개 합) · B11 간섭 무시' }); rows.push({ member: 'column', name: 'P6', size: 'H16', count: col.p6Count, length: col.p6Length, note: '크로스타이, 8단 × 4' }); diff --git a/src/core/types.ts b/src/core/types.ts index d4e13c6..b2ade85 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,4 +1,5 @@ import type { CopingRebarSettings, TopSpliceSettings } from './rebar/coping-settings'; +import type { ColumnRebarSettings } from './rebar/column-settings'; export type PierType = 'T형' | '2주형' | '3주형'; export type FoundationType = '말뚝기초' | '직접기초' | '현장타설말뚝'; @@ -80,6 +81,8 @@ export interface PierInput { copingRebar?: Partial; /** 상면 주철근(C1–C6) 이음 (승인 2026-10-26). Absent = 이음 무시, 기둥 중심. */ topSplice?: Partial; + /** 기둥 배근 입력 (승인 2026-10-26, 시안 ③ 기둥). Absent = the 기둥철근 (1).md defaults. */ + columnRebar?: Partial; strataRows?: StrataRow[]; } diff --git a/src/main.ts b/src/main.ts index 8847b53..23c1767 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,6 +28,8 @@ import { copingEndStation, copingFrontSvg, copingSectionSvg } from './view/copin import { BLOCK_REBAR_RANGES } from './core/rebar/block-check'; import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/bearing-block'; 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 { groundSectionSvg } from './view/ground-svg'; import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table'; import { columnDiameterIssues } from './core/column-check'; @@ -1183,6 +1185,70 @@ function renderCopingSettings(box: HTMLDivElement, pier: Pier): void { box.appendChild(wrap); } +/** 기둥 배근 입력 (승인 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: 'ctcCoupler', label: '커플러 간격 ctcOfcoupler (mm)' }, + { key: 'dzCp2', label: 'P2 커플러 상승 Δz (mm)' }, + { key: 'nDiv', label: '주철근 원주 등분 수 n_div' }, +]; + +/** 적용: the values go to the draft and the model is rebuilt only when every check passes (CL01·CL02). */ +function applyColumnRebar(name: string, next: ColumnRebarSettings): void { + const p = state.piers.find((q) => q.input.name === name); + if (!p) return; + p.draft = { ...p.draft, columnRebar: next }; + regenerateOne(name); +} + +function renderColumnSettings(box: HTMLDivElement, pier: Pier): void { + if (!pier.model) return; + const wrap = el('div', undefined, 'bearing-ref'); + const current: ColumnRebarSettings = { ...DEFAULT_COLUMN_REBAR, ...pier.draft.columnRebar }; + wrap.appendChild(el('p', '기둥 배근 입력 (기본값 = 기둥철근 (1).md · 적용을 눌러야 반영)', 'lbl')); + const grid = el('div', undefined, 'edit-fields'); + const inputs = new Map(); + for (const f of COLUMN_REBAR_FIELDS) { + const row = el('div', undefined, 'edit-row'); + const label = el('label', f.label); + const input = el('input'); + input.type = 'number'; + input.step = 'any'; + input.value = String(current[f.key]); + input.id = `column-${f.key}`; + label.htmlFor = input.id; + inputs.set(f.key, input); + const [lo, hi] = COLUMN_REBAR_RANGES[f.key]; + const changed = current[f.key] !== DEFAULT_COLUMN_REBAR[f.key] ? ' · 기본과 다름' : ''; + row.append(label, input, el('span', `범위 ${lo}–${hi} · 기본 ${DEFAULT_COLUMN_REBAR[f.key]}${changed}`, 'muted small')); + grid.appendChild(row); + } + 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 msg = el('p', undefined, 'muted small'); + const apply = el('button', '적용', 'btn small'); + apply.type = 'button'; + apply.addEventListener('click', () => { + const next = { ...current }; + for (const f of COLUMN_REBAR_FIELDS) next[f.key] = Number(inputs.get(f.key)?.value); + const issues = columnSettingsIssues(next); + if (issues.length > 0) { + msg.textContent = issues.map((i) => i.message).join(' · '); + setError('기둥 배근 입력을 확인하세요. 값은 반영되지 않았습니다.'); + return; + } + applyColumnRebar(pier.input.name, next); + }); + wrap.append(msg, apply); + box.appendChild(wrap); +} + function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void { if (!pier.model || !rebar3d) return; const L = pier.model.copingL; @@ -1258,6 +1324,7 @@ function renderRebar(): void { if (state.rebarMember === 'bearing') renderBlockRebarSettings(box, pier); if (state.rebarMember === 'coping' && rebar3d) renderCopingDrawings(box, pier); if (state.rebarMember === 'coping') renderCopingSettings(box, pier); + if (state.rebarMember === 'column') renderColumnSettings(box, pier); const shown = rows.filter((r) => r.member === state.rebarMember); box.appendChild(rebarSelectBar(shown.map((r) => r.name))); const table = el('table', undefined, 'grid-table rebar-table'); diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index a627585..c660a93 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -4,7 +4,7 @@ import { assemblyEndOffset, assemblyJs, b10Levels, b9Bars, FOOTING_DEFAULTS, gri import { bottomPiece, MAIN, topSplicePieces, widthLayout } from '../core/rebar/coping-main'; import { C14_INSET, c14Bars, CAP, COPING_REBAR, SHEAR_LOOP_STEP, shearBars, shearBarsMulti, shearUHeight, copingSideBars, SIDE_HOOK_LEG, stairBars, undersideAt } from '../core/rebar/coping'; import { sideRowLevels } from '../core/coping-geom'; -import { columnRebar, columnInputFor, COLUMN_P1 } from '../core/rebar/column'; +import { columnRebar, columnInputFor } from '../core/rebar/column'; import { barDepths, barsAcross } from '../core/rebar/bearing-block'; import { blockFramePoint } from '../core/geometry'; import { INTEGRATED_BLOCK } from '../core/integrated-block'; @@ -294,8 +294,8 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, const sc = c.s; const base = c.bottom; const rt = mm(col.tieRadius); - for (let i = 0; i < COLUMN_P1.nDiv; i++) { - const th = (2 * Math.PI * i) / COLUMN_P1.nDiv; + for (let i = 0; i < colInput.nDiv; i++) { + const th = (2 * Math.PI * i) / colInput.nDiv; const r1 = mm(col.r1); addBar('P1', c.k, [sc + r1 * Math.cos(th), base + mm(zBot), r1 * Math.sin(th)], [sc + r1 * Math.cos(th), base + mm(col.zTop1), r1 * Math.sin(th)], NOMINAL_DIAMETER.S32, COLOR.column); // 90° bottom hook of P1, outward (기둥철근.md §3.2, Q-C5): the 12·d tail is drawn, the bend is approximated by the tail's start. @@ -312,8 +312,8 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, // Main bar couplers (D32, L150 Ø60, 4단 × 46줄 × 2열 = 368개): P1 at col.couplerP1, P2 at col.couplerP2 (+150 mm) const r1 = mm(col.r1); const r2 = mm(col.r2); - for (let i = 0; i < COLUMN_P1.nDiv; i++) { - const th = (2 * Math.PI * i) / COLUMN_P1.nDiv; + for (let i = 0; i < colInput.nDiv; i++) { + const th = (2 * Math.PI * i) / colInput.nDiv; const cosTh = Math.cos(th); const sinTh = Math.sin(th); // P1 couplers (184) @@ -334,7 +334,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, // Tie couplers (D19, L100 Ø40, 총 35개): P5 1개 (19개) · P4 2개 (16개) col.p5.forEach((z, j) => { - const th = (2 * Math.PI * (j * 7 + 0.5)) / COLUMN_P1.nDiv; + const th = (2 * Math.PI * (j * 7 + 0.5)) / colInput.nDiv; const cx = sc + rt * Math.cos(th); const cz = rt * Math.sin(th); const cy = base + mm(z); @@ -343,7 +343,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, addBar('띠철근 커플러', c.k, [cx - dx, cy, cz - dz], [cx + dx, cy, cz + dz], 40, COLOR.couplerTie); }); col.p4.forEach((z) => { - const angles = [(2 * Math.PI * 0.5) / COLUMN_P1.nDiv, (2 * Math.PI * 0.5) / COLUMN_P1.nDiv + Math.PI]; + const angles = [(2 * Math.PI * 0.5) / colInput.nDiv, (2 * Math.PI * 0.5) / colInput.nDiv + Math.PI]; angles.forEach((th) => { const cx = sc + rt * Math.cos(th); const cz = rt * Math.sin(th); diff --git a/tests/column-rebar-settings.test.ts b/tests/column-rebar-settings.test.ts new file mode 100644 index 0000000..e2154bf --- /dev/null +++ b/tests/column-rebar-settings.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier } from '../src/core/geometry'; +import { columnInputFor, columnRebar } from '../src/core/rebar/column'; +import { COLUMN_REBAR_RANGES, columnSettingsIssues, DEFAULT_COLUMN_REBAR } from '../src/core/rebar/column-settings'; +import { rebarSummary } from '../src/core/rebar/summary'; +import { loadP1 } from './helpers'; + +describe('기둥 배근 입력 (승인 2026-10-26, 시안 ③ 기둥)', () => { + it('the defaults are inside every range and raise no issue', () => { + expect(columnSettingsIssues(DEFAULT_COLUMN_REBAR)).toEqual([]); + for (const [key, [lo, hi]] of Object.entries(COLUMN_REBAR_RANGES)) { + const v = DEFAULT_COLUMN_REBAR[key as keyof typeof DEFAULT_COLUMN_REBAR]; + expect(v >= lo && v <= hi, key).toBe(true); + } + }); + + it('a value outside its range gives CL01 on that key; an odd n_div gives CL02', () => { + expect(columnSettingsIssues({ ...DEFAULT_COLUMN_REBAR, c: 151 })).toEqual([expect.objectContaining({ code: 'CL01', key: 'c' })]); + expect(columnSettingsIssues({ ...DEFAULT_COLUMN_REBAR, nDiv: 45 })).toEqual([expect.objectContaining({ code: 'CL02', key: 'nDiv' })]); + }); + + it('P1 defaults: P1 원주 46등분, P3 간격 300 · 기본값 근거', () => { + const rows = rebarSummary(buildPier(loadP1())).rows; + const p1 = rows.find((r) => r.name === 'P1')!; + expect([p1.spacing, p1.basis]).toEqual(['원주 46등분', 'default']); + const p3 = rows.find((r) => r.name === 'P3')!; + expect([p3.spacing, p3.actual, p3.basis]).toEqual(['300', '300', 'default']); + }); + + it('changed inputs reach the table (간격·근거·비고) and the column input', () => { + const m = buildPier({ ...loadP1(), columnRebar: { ctcP3: 320, nDiv: 40 } }); + expect(columnInputFor(m).ctcP3).toBe(320); + expect(columnInputFor(m).nDiv).toBe(40); + const rows = rebarSummary(m).rows; + const p3 = rows.find((r) => r.name === 'P3')!; + expect([p3.spacing, p3.basis]).toEqual(['320', 'input']); + expect(p3.note).toContain('@320'); + const p1 = rows.find((r) => r.name === 'P1')!; + expect([p1.spacing, p1.basis]).toEqual(['원주 40등분', 'input']); + // mainP1 = n_div × (couplers + 1): 40 bars per level. + const col = columnRebar(columnInputFor(m)); + expect(col.mainP1).toBe(40 * (col.couplerP1.length + 1)); + }); +});