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:
1 parent
a6b92ae341
commit
65c53f2b5d
7 files changed
+80
-4
No files matched your search
@@ -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 폭',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user