feat(view,rebar): column diameter D as a free input on ② (1200–3000 mm, 50 mm steps), with three checks

D is an input on the column card (default 2000 mm). The 3D column, the piles of a cast-in pier and the column rebar follow it;
n_div stays 46. Three checks run on each regenerate and appear in 알림: D below the Level width (2100), D + 2c within the
coping width, and the 주철근 arc spacing ≥ max(1.5·d_b, 40). Spec §5.2 records the rule (approved 2026-10-23).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-23 15:50:52 +09:00
1 parent a6b92ae341
commit 65c53f2b5d
7 files changed
+80 -4

No files matched your search

+2
View File
@@ -7,6 +7,7 @@ import type { PierInput } from './types';
export const EDITABLE_FIELDS = [
{ key: 'copingWidthMm', label: '코핑 폭 B', unit: 'mm' },
{ key: 'copingLengthMm', label: '코핑 길이 L', unit: 'mm' },
{ key: 'columnDiameterMm', label: '기둥 직경 D', unit: 'mm' },
{ key: 'copingH1Mm', label: '끝단 높이 h1', unit: 'mm' },
{ key: 'copingH2Mm', label: '테이퍼 높이 h2', unit: 'mm' },
{ key: 'levelWidthMm', label: 'Level 폭', unit: 'mm' },
@@ -21,6 +22,7 @@ export type EditableKey = (typeof TRACKED_KEYS)[number];
export const FIELD_LABELS: Record<EditableKey, string> = {
copingWidthMm: '코핑 폭 B',
copingLengthMm: '코핑 길이 L',
columnDiameterMm: '기둥 직경 D',
copingH1Mm: '끝단 높이 h1',
copingH2Mm: '테이퍼 높이 h2',
levelWidthMm: 'Level 폭',
+30
View File
@@ -0,0 +1,30 @@
/**
* 기둥 직경 D (승인 2026-10-23): free input 1200–3000 mm in 50 mm steps. Checks, all in mm:
* - D < Level 폭 (the column meets the Level zone);
* - D + 2·c ≤ 코핑 폭 B (c = 125, 기둥철근.md: 기둥 면 ~ P1 중심 피복);
* - the arc spacing of the 주철근 (2π·r1 / n_div, n_div = 46 kept) ≥ max(1.5·d_b, 40), with d_b = 32 (S32).
* Pure: gives the messages for the 알림 list.
*/
import type { PierModel } from './geometry';
export const COLUMN_D_RANGE = { min: 1200, max: 3000, step: 50 } as const;
export const COLUMN_COVER_MM = 125;
const N_DIV = 46;
const DB_MAIN = 31.8;
export function columnDiameterIssues(dMm: number, model: PierModel): string[] {
const out: string[] = [];
if (!Number.isFinite(dMm) || dMm < COLUMN_D_RANGE.min || dMm > COLUMN_D_RANGE.max || Math.round(dMm) % COLUMN_D_RANGE.step !== 0) {
out.push(`기둥 직경은 ${COLUMN_D_RANGE.min}~${COLUMN_D_RANGE.max} mm, ${COLUMN_D_RANGE.step} mm 단위여야 합니다.`);
return out;
}
const levelW = model.copingGeom.levelWidth;
if (dMm >= levelW) out.push(`기둥 직경 ${dMm} mm가 Level 폭 ${levelW} mm 이상입니다.`);
const copingW = model.copingB * 1000;
if (dMm + 2 * COLUMN_COVER_MM > copingW) out.push(`기둥 직경 ${dMm} mm와 피복이 코핑 폭 ${copingW} mm를 넘습니다.`);
const r1 = dMm / 2 - COLUMN_COVER_MM;
const arc = (2 * Math.PI * r1) / N_DIV;
const minArc = Math.max(1.5 * DB_MAIN, 40);
if (arc < minArc) out.push(`주철근 순간격 ${arc.toFixed(1)} mm가 최소 ${minArc.toFixed(1)} mm보다 작습니다.`);
return out;
}
+7 -2
View File
@@ -166,6 +166,8 @@ export interface PierModel {
endBottom: number;
/** Coping heights and Level width in mm, as used by the coping rebar (1단계). */
copingGeom: CopingGeom;
/** Column diameter D (mm) used by the model and the column rebar. */
columnDiameterMm: number;
bearings: BearingLocal[];
blocks: BlockModel[];
columns: ColumnModel[];
@@ -225,6 +227,8 @@ export function buildPier(input: PierInput): PierModel {
const copingH1 = (input.copingH1Mm ?? P1_COPING_GEOM.h1) / 1000;
const copingH2 = (input.copingH2Mm ?? P1_COPING_GEOM.h2) / 1000;
const levelW = (input.levelWidthMm ?? P1_COPING_GEOM.levelWidth) / 1000;
// Column diameter (m): the input, with 2.0 m when absent (승인 2026-10-23).
const colD = (input.columnDiameterMm ?? DIM.columnDiameter * 1000) / 1000;
const planHalfB = B / 2;
const ip1v: Vec2 = { e: ip1.x, n: ip1.y };
const ip2v: Vec2 = { e: ip2.x, n: ip2.y };
@@ -367,7 +371,7 @@ export function buildPier(input: PierInput): PierModel {
k: colK[i],
top: levelBottom,
bottom: footingTop,
diameter: DIM.columnDiameter,
diameter: colD,
}));
const footings: FootingModel[] = [];
@@ -378,7 +382,7 @@ export function buildPier(input: PierInput): PierModel {
if (input.foundationType === '현장타설말뚝') {
columns.forEach((c) => {
piles.push({ s: c.s, k: c.k, n: 0, top: input.footingBottomEl, bottom: pileTip, diameter: DIM.columnDiameter });
piles.push({ s: c.s, k: c.k, n: 0, top: input.footingBottomEl, bottom: pileTip, diameter: colD });
});
} else {
for (const f of footingSets) {
@@ -448,6 +452,7 @@ export function buildPier(input: PierInput): PierModel {
levelBottom,
endBottom,
copingGeom: { h1: copingH1 * 1000, h2: copingH2 * 1000, levelWidth: levelW * 1000 },
columnDiameterMm: colD * 1000,
bearings: [...frontPlaced, ...backPlaced].map((b) => ({ no: b.no, row: b.row, k: b.k, s: b.s, g: b.g, n: b.n, z: b.z })),
blocks,
columns,
+1 -1
View File
@@ -54,7 +54,7 @@ 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 };
return { ...base, Hc, hcop, eP1, eTop, D: model.columnDiameterMm };
}
export const COLUMN_P1: ColumnInput = {
+2
View File
@@ -30,6 +30,8 @@ export interface PierInput {
foundationType: FoundationType;
pileLengthMm: number;
copingWidthMm: number;
/** 기둥 직경 D (mm, 1200–3000 in 50 mm steps, 승인 2026-10-23). Absent = 2000. */
columnDiameterMm?: number;
copingLengthMm: number;
/** 끝단 높이 h1, 테이퍼 높이 h2, Level 폭 (mm). Absent = the P1 values (1000 · 1500 · 2100). */
copingH1Mm?: number;
+6 -1
View File
@@ -17,6 +17,7 @@ import { DEFAULT_MATERIAL, FCK_MEMBERS, fyOf, GRADE_CHOICES, isAssumedMaterial,
import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from './core/rebar/quantity';
import { parseDrawingRows } from './core/drawing-import';
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
import { columnDiameterIssues } from './core/column-check';
import { P1_COPING_GEOM } from './core/coping-geom';
import { applyBearingRules, BEARING_ELEVATION_REF, BEARING_RULES_DEFAULT, type BearingBasis, type BearingRules, type DrawingBearing, DRAWING_TOL_MM, drawingCopingCheck, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs, withDrawingBasis } from './core/bearing-ref';
@@ -69,7 +70,7 @@ const CRS = '국토좌표계 (x = 동, y = 북)';
const MEMBERS: { id: MemberId; label: string; keys: EditableKey[]; note: string }[] = [
{ id: 'bearing', label: '교량받침·받침블럭·솔플레이트', keys: [], note: '받침 상세(C3)는 Q16 결정 후 추가합니다. 지금은 표시만 합니다.' },
{ id: 'coping', label: '코핑', keys: ['copingWidthMm', 'copingLengthMm', 'copingH1Mm', 'copingH2Mm', 'levelWidthMm'], note: '폭, 길이, 끝단 높이 h1, 테이퍼 높이 h2, Level 폭을 입력합니다. 계단 위치·단차선 입력은 다음 단계에서 추가합니다.' },
{ id: 'column', label: '기둥', keys: [], note: '기둥 지름 입력은 다음 단계에서 추가합니다. 높이와 위치는 계산값입니다.' },
{ id: 'column', label: '기둥', keys: ['columnDiameterMm'], note: '기둥 직경 D를 입력합니다 (1200–3000 mm, 50 mm 단위). 높이와 위치는 계산값입니다.' },
{ id: 'footing', label: '확대기초 · 말뚝', keys: ['pileLengthMm'], note: '말뚝 길이를 입력합니다. 열별 본수는 다음 단계에서 추가합니다.' },
{ id: 'ground', label: '지반·지층', keys: [], note: '지반고는 입력값을 표시만 합니다. 지층 입력은 다음 단계에서 추가합니다.' },
];
@@ -196,6 +197,7 @@ animate();
function withCopingDefaults(input: PierInput): PierInput {
return {
...input,
columnDiameterMm: input.columnDiameterMm ?? 2000,
copingH1Mm: input.copingH1Mm ?? P1_COPING_GEOM.h1,
copingH2Mm: input.copingH2Mm ?? P1_COPING_GEOM.h2,
levelWidthMm: input.levelWidthMm ?? P1_COPING_GEOM.levelWidth,
@@ -218,6 +220,9 @@ function buildEntry(rawInput: PierInput, checked: boolean): Pier {
try {
model = buildPier(modelInput);
validatePier(input, model, issues);
for (const message of columnDiameterIssues(modelInput.columnDiameterMm ?? 2000, model)) {
issues.push({ code: 'D01', level: 'E', pier: input.name, message });
}
} catch (err) {
issues.push({ code: '미실시', level: 'E', pier: input.name, message: (err as Error).message });
}
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { columnDiameterIssues } from '../src/core/column-check';
import { columnInputFor, columnRebar } from '../src/core/rebar/column';
import { loadP1 } from './helpers';
describe('기둥 직경 D 입력 (승인 2026-10-23)', () => {
it('the default (absent) is 2000 mm and gives the P1 results', () => {
const m = buildPier(loadP1());
expect(m.columnDiameterMm).toBe(2000);
expect(columnInputFor(m).D).toBe(2000);
expect(columnRebar(columnInputFor(m)).mainP1).toBe(230);
});
it('a larger D (2500) changes the column radius and the rebar, and the model follows the input', () => {
const m = buildPier({ ...loadP1(), columnDiameterMm: 2500 });
expect(m.columnDiameterMm).toBe(2500);
expect(m.columns[0].diameter).toBeCloseTo(2.5, 6);
const col = columnRebar(columnInputFor(m));
expect(col.r1).toBeCloseTo(2500 / 2 - 125 - 0, 0); // r1 = D/2 − c, the outer bar radius
});
it('the checks: range and 50 mm steps, D below the Level width, D + 2c within the coping width, arc spacing', () => {
const m = buildPier(loadP1());
expect(columnDiameterIssues(2000, m)).toEqual([]);
expect(columnDiameterIssues(2050, m)).toEqual([]);
expect(columnDiameterIssues(3200, m).length).toBe(1);
expect(columnDiameterIssues(2025, m).length).toBe(1);
expect(columnDiameterIssues(2100, m).some((x) => x.includes('Level 폭'))).toBe(true);
expect(columnDiameterIssues(2050, m).some((x) => x.includes('코핑 폭'))).toBe(false);
});
});