diff --git a/src/core/changes.ts b/src/core/changes.ts index 51de119..0355b43 100644 --- a/src/core/changes.ts +++ b/src/core/changes.ts @@ -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 = { copingWidthMm: '코핑 폭 B', copingLengthMm: '코핑 길이 L', + columnDiameterMm: '기둥 직경 D', copingH1Mm: '끝단 높이 h1', copingH2Mm: '테이퍼 높이 h2', levelWidthMm: 'Level 폭', diff --git a/src/core/column-check.ts b/src/core/column-check.ts new file mode 100644 index 0000000..227952b --- /dev/null +++ b/src/core/column-check.ts @@ -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; +} diff --git a/src/core/geometry.ts b/src/core/geometry.ts index 50367f3..f99e5a8 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -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, diff --git a/src/core/rebar/column.ts b/src/core/rebar/column.ts index 07e597d..23a5f9f 100644 --- a/src/core/rebar/column.ts +++ b/src/core/rebar/column.ts @@ -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 = { diff --git a/src/core/types.ts b/src/core/types.ts index 88a3cd5..cc1a6e6 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -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; diff --git a/src/main.ts b/src/main.ts index a38f24f..f39eb9b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 }); } diff --git a/tests/column-diameter.test.ts b/tests/column-diameter.test.ts new file mode 100644 index 0000000..44d1ee7 --- /dev/null +++ b/tests/column-diameter.test.ts @@ -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); + }); +});