diff --git a/src/core/changes.ts b/src/core/changes.ts index 0355b43..ca20510 100644 --- a/src/core/changes.ts +++ b/src/core/changes.ts @@ -8,6 +8,13 @@ export const EDITABLE_FIELDS = [ { key: 'copingWidthMm', label: '코핑 폭 B', unit: 'mm' }, { key: 'copingLengthMm', label: '코핑 길이 L', unit: 'mm' }, { key: 'columnDiameterMm', label: '기둥 직경 D', unit: 'mm' }, + { key: 'footingLengthMm', label: '확대기초 길이 L', unit: 'mm' }, + { key: 'footingWidthMm', label: '확대기초 폭 W', unit: 'mm' }, + { key: 'footingHeightMm', label: '확대기초 높이 h1', unit: 'mm' }, + { key: 'blindingMarginMm', label: '비림 여유 (한쪽)', unit: 'mm' }, + { key: 'blindingThicknessMm', label: '비림 두께', unit: 'mm' }, + { key: 'pileOuterMm', label: '말뚝 외경 D', unit: 'mm' }, + { key: 'pileThicknessMm', label: '말뚝 두께 t', unit: 'mm' }, { key: 'copingH1Mm', label: '끝단 높이 h1', unit: 'mm' }, { key: 'copingH2Mm', label: '테이퍼 높이 h2', unit: 'mm' }, { key: 'levelWidthMm', label: 'Level 폭', unit: 'mm' }, @@ -23,6 +30,13 @@ export const FIELD_LABELS: Record = { copingWidthMm: '코핑 폭 B', copingLengthMm: '코핑 길이 L', columnDiameterMm: '기둥 직경 D', + footingLengthMm: '확대기초 길이 L', + footingWidthMm: '확대기초 폭 W', + footingHeightMm: '확대기초 높이 h1', + blindingMarginMm: '비림 여유 (한쪽)', + blindingThicknessMm: '비림 두께', + pileOuterMm: '말뚝 외경 D', + pileThicknessMm: '말뚝 두께 t', copingH1Mm: '끝단 높이 h1', copingH2Mm: '테이퍼 높이 h2', levelWidthMm: 'Level 폭', diff --git a/src/core/geometry.ts b/src/core/geometry.ts index f99e5a8..7040f65 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -357,15 +357,16 @@ export function buildPier(input: PierInput): PierModel { } } + const fd = footingDims(input); // Footings are grouped per stretch so that a 일체기초 never crosses the kink; each group is aligned with its stretch. const footingSets: { s: number; k: number; length: number }[] = []; for (let k = 0; k < stretchCount; k++) { - for (const g of footingGroups(colG.filter((_, i) => colK[i] === k))) { + for (const g of footingGroups(colG.filter((_, i) => colK[i] === k), fd.L)) { footingSets.push({ s: g.s - base[k], k, length: g.length }); } } - const footingTop = input.foundationType === '현장타설말뚝' ? input.footingBottomEl : input.footingBottomEl + DIM.footingH; + const footingTop = input.foundationType === '현장타설말뚝' ? input.footingBottomEl : input.footingBottomEl + fd.H; const columns: ColumnModel[] = colG.map((g, i) => ({ s: g - base[colK[i]], k: colK[i], @@ -386,19 +387,19 @@ export function buildPier(input: PierInput): PierModel { }); } else { for (const f of footingSets) { - footings.push({ s: f.s, k: f.k, top: footingTop, bottom: input.footingBottomEl, length: f.length, width: DIM.footingW }); + footings.push({ s: f.s, k: f.k, top: footingTop, bottom: input.footingBottomEl, length: f.length, width: fd.W }); blindings.push({ s: f.s, k: f.k, top: input.footingBottomEl, - length: f.length + DIM.blindingMargin, - width: DIM.footingW + DIM.blindingMargin, - thickness: DIM.blindingT, + length: f.length + fd.blM, + width: fd.W + fd.blM, + thickness: fd.blT, }); if (input.foundationType === '말뚝기초') { - const g = pileGeometry(f.length); + const g = pileGeometry(f.length, fd.W); const counts = resolveRowCounts(input.pileRows, g); - piles.push(...pilesFor(f.s, input.footingBottomEl, pileLength, g, counts).map((p) => ({ ...p, k: f.k }))); + piles.push(...pilesFor(f.s, input.footingBottomEl, pileLength, g, counts, fd.pileD).map((p) => ({ ...p, k: f.k }))); } } } @@ -464,7 +465,7 @@ export function buildPier(input: PierInput): PierModel { columnHeight: levelBottom - columns[0].bottom, soilCover: input.groundEl - (input.foundationType === '현장타설말뚝' ? input.footingBottomEl : footingTop), pileCount: piles.length, - pileLayout: input.foundationType === '말뚝기초' ? layoutText(resolveRowCounts(input.pileRows, pileGeometry(footings[0].length)), pileGeometry(footings[0].length)) : null, + pileLayout: input.foundationType === '말뚝기초' ? layoutText(resolveRowCounts(input.pileRows, pileGeometry(footings[0].length, fd.W)), pileGeometry(footings[0].length, fd.W)) : null, pileTipEl: piles.length ? pileTip : null, blockHeightFront, blockHeightBack, @@ -488,16 +489,16 @@ function segmentTop(segments: Segment[], s: number): number { * Footings (spec §4.6). Adjacent columns closer than one footing length share a 일체기초 whose length is * the distance between its outer columns + DIM.footingL, centred between them. Other columns get their own footing. */ -export function footingGroups(colS: number[]): { s: number; length: number }[] { +export function footingGroups(colS: number[], footL: number = DIM.footingL): { s: number; length: number }[] { const groups: number[][] = []; for (const s of [...colS].sort((a, b) => a - b)) { const last = groups[groups.length - 1]; - if (last && s - last[last.length - 1] < DIM.footingL) last.push(s); + if (last && s - last[last.length - 1] < footL) last.push(s); else groups.push([s]); } return groups.map((g) => ({ s: (g[0] + g[g.length - 1]) / 2, - length: g[g.length - 1] - g[0] + DIM.footingL, + length: g[g.length - 1] - g[0] + footL, })); } @@ -506,15 +507,28 @@ export function footingGroups(colS: number[]): { s: number; length: number }[] { * both with an end distance of 0.7 m and an even spacing that never exceeds 2.25 m. */ /** Pile geometry of one footing, from the 1단계 constants (spec §4.6). */ -export function pileGeometry(footingLength: number): PileGeometry { - return { footingLength, footingWidth: DIM.footingW, end: DIM.pileEnd, minSpacing: DIM.pileSpacing }; +export function pileGeometry(footingLength: number, footingWidth: number = DIM.footingW): PileGeometry { + return { footingLength, footingWidth, end: DIM.pileEnd, minSpacing: DIM.pileSpacing }; +} + +/** Footing and pile dimensions in m: the input when given, else the 1단계 constants (승인 2026-10-23). */ +export function footingDims(input: PierInput): { L: number; W: number; H: number; blM: number; blT: number; pileD: number } { + return { + L: (input.footingLengthMm ?? DIM.footingL * 1000) / 1000, + W: (input.footingWidthMm ?? DIM.footingW * 1000) / 1000, + H: (input.footingHeightMm ?? DIM.footingH * 1000) / 1000, + // 비림 여유 is the margin on each side (삽도 한쪽 100); the blinding grows by twice that on each length and width. + blM: (2 * (input.blindingMarginMm ?? DIM.blindingMargin * 500)) / 1000, + blT: (input.blindingThicknessMm ?? DIM.blindingT * 1000) / 1000, + pileD: (input.pileOuterMm ?? DIM.pileDiameter * 1000) / 1000, + }; } /** * Piles under one footing (spec §4.6): one 열 per row across the width, each with its own count along the length. * counts holds the 본수 of each 열 (see core/piles.ts). */ -export function pilesFor(s: number, topEl: number, length: number, g: PileGeometry, counts: number[]): PileModel[] { +export function pilesFor(s: number, topEl: number, length: number, g: PileGeometry, counts: number[], diameter: number = DIM.pileDiameter): PileModel[] { const rows = rowLayout(counts, g); const spacingN = (g.footingWidth - 2 * g.end) / (counts.length - 1); const out: PileModel[] = []; @@ -526,7 +540,7 @@ export function pilesFor(s: number, topEl: number, length: number, g: PileGeomet n: (j - (counts.length - 1) / 2) * spacingN, top: topEl, bottom: topEl - length, - diameter: DIM.pileDiameter, + diameter, }); } }); diff --git a/src/core/pile-spec.ts b/src/core/pile-spec.ts new file mode 100644 index 0000000..e31fb30 --- /dev/null +++ b/src/core/pile-spec.ts @@ -0,0 +1,41 @@ +/** + * 말뚝 규격 목록 (승인 2026-10-23): the outer diameter D and wall t are chosen from the list; the inner diameter, area and + * unit weight are read from it. 강관 and PHC are separate lists; 복합 말뚝 is entered as 강관 + PHC later (1·2단계 후). + */ +export interface PileSpec { + kind: '강관' | 'PHC'; + /** 외경 D (mm). */ + D: number; + /** 두께 t (mm). */ + t: number; + /** 내경 (mm). */ + inner: number; + /** 단면적 (cm²). */ + area: number; + /** 단위중량 (kg/m for 강관, kN/m for PHC as in the sheet). */ + unitWeight: number; +} + +export const STEEL_PILES: PileSpec[] = [ + { kind: '강관', D: 406.4, t: 9, inner: 388.4, area: 112, unitWeight: 88.2 }, + { kind: '강관', D: 406.4, t: 12, inner: 382.4, area: 149, unitWeight: 116.7 }, + { kind: '강관', D: 508.0, t: 9, inner: 490.0, area: 141, unitWeight: 110.7 }, + { kind: '강관', D: 508.0, t: 12, inner: 484.0, area: 187, unitWeight: 146.8 }, + { kind: '강관', D: 508.0, t: 14, inner: 480.0, area: 217, unitWeight: 170.5 }, + { kind: '강관', D: 609.6, t: 9, inner: 591.6, area: 170, unitWeight: 133.3 }, + { kind: '강관', D: 609.6, t: 12, inner: 585.6, area: 225, unitWeight: 176.8 }, + { kind: '강관', D: 609.6, t: 14, inner: 581.6, area: 262, unitWeight: 205.6 }, + { kind: '강관', D: 609.6, t: 16, inner: 577.6, area: 298, unitWeight: 234.2 }, +]; + +export const PHC_PILES: PileSpec[] = [ + { kind: 'PHC', D: 400, t: 65, inner: 270, area: 684, unitWeight: 1.74 }, + { kind: 'PHC', D: 450, t: 70, inner: 310, area: 836, unitWeight: 2.13 }, + { kind: 'PHC', D: 500, t: 80, inner: 340, area: 1056, unitWeight: 2.69 }, + { kind: 'PHC', D: 600, t: 90, inner: 420, area: 1442, unitWeight: 3.67 }, +]; + +/** The spec of an outer diameter and wall from the list of its kind, or undefined when the pair is not in the list. */ +export function findPileSpec(kind: '강관' | 'PHC', D: number, t: number): PileSpec | undefined { + return (kind === '강관' ? STEEL_PILES : PHC_PILES).find((p) => p.D === D && p.t === t); +} diff --git a/src/core/types.ts b/src/core/types.ts index cc1a6e6..7e110cb 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -44,6 +44,16 @@ export interface PierInput { bearings: Bearing[]; /** 솔플레이트 plan per bearing (승인 2026-10-23): A, B (mm) and the slope S (%). Absent = no solplate. */ bearingPlans?: { no: number; A: number; B: number; S: number }[]; + /** 확대기초 L (축 방향), W, 높이 h1 (mm); 비림 여유 and 비림 두께 (mm); 말뚝 외경 D and 두께 t (mm). Absent = the 1단계 constants. */ + footingLengthMm?: number; + footingWidthMm?: number; + footingHeightMm?: number; + blindingMarginMm?: number; + blindingThicknessMm?: number; + pileOuterMm?: number; + pileThicknessMm?: number; + /** 말뚝 종류 (규격 목록): 강관 (default) or PHC. */ + pileKind?: '강관' | 'PHC'; /** 공통 규칙 for the solplate: centre thickness T, 솔플 여유 margin (mm), bearing height h3 (mm). */ solplate?: { T: number; margin: number; h3: number }; } diff --git a/src/main.ts b/src/main.ts index dcc99a0..6ebffdd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,7 @@ import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from '. import { parseDrawingRows } from './core/drawing-import'; import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table'; import { columnDiameterIssues } from './core/column-check'; +import { findPileSpec, PHC_PILES, STEEL_PILES } from './core/pile-spec'; 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'; @@ -71,7 +72,7 @@ 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: ['columnDiameterMm'], note: '기둥 직경 D를 입력합니다 (1200–3000 mm, 50 mm 단위). 높이와 위치는 계산값입니다.' }, - { id: 'footing', label: '확대기초 · 말뚝', keys: ['pileLengthMm'], note: '말뚝 길이를 입력합니다. 열별 본수는 다음 단계에서 추가합니다.' }, + { id: 'footing', label: '확대기초 · 말뚝', keys: ['footingLengthMm', 'footingWidthMm', 'footingHeightMm', 'blindingMarginMm', 'blindingThicknessMm', 'pileLengthMm'], note: '확대기초 치수, 비림, 말뚝 길이와 규격을 입력합니다. 규칙 미정 항목(모따기, 경사, 복합 말뚝)은 다음 단계에서 추가합니다.' }, { id: 'ground', label: '지반·지층', keys: [], note: '지반고는 입력값을 표시만 합니다. 지층 입력은 다음 단계에서 추가합니다.' }, ]; @@ -198,6 +199,14 @@ function withCopingDefaults(input: PierInput): PierInput { return { ...input, columnDiameterMm: input.columnDiameterMm ?? 2000, + footingLengthMm: input.footingLengthMm ?? 10400, + footingWidthMm: input.footingWidthMm ?? 8150, + footingHeightMm: input.footingHeightMm ?? 2000, + blindingMarginMm: input.blindingMarginMm ?? 100, + blindingThicknessMm: input.blindingThicknessMm ?? 100, + pileKind: input.pileKind ?? '강관', + pileOuterMm: input.pileOuterMm ?? 609.6, + pileThicknessMm: input.pileThicknessMm ?? 12, copingH1Mm: input.copingH1Mm ?? P1_COPING_GEOM.h1, copingH2Mm: input.copingH2Mm ?? P1_COPING_GEOM.h2, levelWidthMm: input.levelWidthMm ?? P1_COPING_GEOM.levelWidth, @@ -707,6 +716,52 @@ function footingLengthOf(p: Pier): number | undefined { return p.model?.footings[0]?.length; } +/** 말뚝 규격 (승인 2026-10-23): 강관 and PHC from their lists by outer diameter and wall; the inner diameter, area and weight are read. */ +function renderPileSpec(box: HTMLDivElement, pier: Pier): void { + const kind = pier.draft.pileKind ?? '강관'; + const list = kind === '강관' ? STEEL_PILES : PHC_PILES; + const cur = findPileSpec(kind, pier.draft.pileOuterMm ?? 609.6, pier.draft.pileThicknessMm ?? 12); + const wrap = el('div', undefined, 'pile-spec'); + const kindSel = el('select'); + kindSel.id = 'pile-kind'; + for (const k of ['강관', 'PHC'] as const) { + const o = el('option', k === '강관' ? '강관 말뚝' : 'PHC 말뚝'); + o.value = k; + kindSel.appendChild(o); + } + kindSel.value = kind; + kindSel.addEventListener('change', () => { + const first = (kindSel.value === '강관' ? STEEL_PILES : PHC_PILES)[0]; + setPileSpec(pier.input.name, kindSel.value as '강관' | 'PHC', first.D, first.t); + }); + const sizeSel = el('select'); + sizeSel.id = 'pile-size'; + for (const p of list) { + const o = el('option', `${p.D} × ${p.t}`); + o.value = `${p.D}|${p.t}`; + sizeSel.appendChild(o); + } + if (cur) sizeSel.value = `${cur.D}|${cur.t}`; + sizeSel.addEventListener('change', () => { + const [D, t] = sizeSel.value.split('|').map(Number); + setPileSpec(pier.input.name, kind, D, t); + }); + const info = cur + ? `내경 ${cur.inner} mm · 단면적 ${cur.area} cm² · 단위중량 ${cur.unitWeight} ${kind === '강관' ? 'kg/m' : 'kN/m'}` + : '목록에 없는 규격입니다.'; + wrap.append(el('label', '말뚝 종류 '), kindSel, el('label', ' 외경 × 두께 '), sizeSel, el('p', info, 'muted small')); + box.appendChild(wrap); +} + +function setPileSpec(name: string, kind: '강관' | 'PHC', D: number, t: number): void { + const p = state.piers.find((q) => q.input.name === name); + if (!p) return; + p.draft = { ...p.draft, pileKind: kind, pileOuterMm: D, pileThicknessMm: t }; + state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q)); + setupScene(); + renderAll(); +} + /** 열별 본수 −/+ per 열 (spec §5.4). Shows the spacing, the total, and a warning below the minimum spacing. */ function renderPileRows(box: HTMLDivElement, pier: Pier): void { const footingLength = footingLengthOf(pier); @@ -1587,7 +1642,10 @@ function renderPlan(): void { regen.disabled = true; } else { fieldRows(fields, pier, member.keys); - if (member.id === 'footing') renderPileRows(fields, pier); + if (member.id === 'footing') { + renderPileSpec(fields, pier); + renderPileRows(fields, pier); + } if (member.id === 'coping') renderCopingInfo(fields, pier); if (member.id === 'bearing') renderBearingRef(fields, pier); if (FCK_MEMBERS.includes(member.id as FckMember)) renderMemberFck(fields, member.id as FckMember); diff --git a/tests/footing-inputs.test.ts b/tests/footing-inputs.test.ts new file mode 100644 index 0000000..f3db6c1 --- /dev/null +++ b/tests/footing-inputs.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier, footingDims } from '../src/core/geometry'; +import { loadP1 } from './helpers'; + +describe('확대기초·말뚝 입력 (입력만, 승인 2026-10-23)', () => { + it('without the new inputs the footing dims are the 1단계 constants (P1 unchanged)', () => { + const d = footingDims(loadP1()); + expect(d.L).toBeCloseTo(10.4, 9); + expect(d.W).toBeCloseTo(8.15, 9); + expect(d.H).toBeCloseTo(2.0, 9); + expect(d.blM).toBeCloseTo(0.2, 9); + expect(d.blT).toBeCloseTo(0.1, 9); + expect(d.pileD).toBeCloseTo(0.6096, 9); + }); + + it('the footing width, height and blinding follow the inputs in the model', () => { + const m = buildPier({ ...loadP1(), footingWidthMm: 9000, footingHeightMm: 2200, blindingMarginMm: 150, blindingThicknessMm: 120 }); + expect(m.footings[0].width).toBeCloseTo(9.0, 9); + expect(m.blindings[0].width).toBeCloseTo(9.0 + 0.3, 9); + expect(m.blindings[0].thickness).toBeCloseTo(0.12, 9); + expect(m.footings[0].top - m.footings[0].bottom).toBeCloseTo(2.2, 9); + }); + + it('a steel pile of 609.6 × 16 gives the pile diameter 0.6096 m (the wall is in the list, not the diameter)', () => { + const m = buildPier({ ...loadP1(), pileOuterMm: 609.6, pileThicknessMm: 16 }); + expect(m.piles[0].diameter).toBeCloseTo(0.6096, 9); + }); +}); diff --git a/tests/pile-spec.test.ts b/tests/pile-spec.test.ts new file mode 100644 index 0000000..9384c17 --- /dev/null +++ b/tests/pile-spec.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { findPileSpec, PHC_PILES, STEEL_PILES } from '../src/core/pile-spec'; + +describe('말뚝 규격 목록 (승인 2026-10-23)', () => { + it('the steel list has the nine rows of the sheet, and 609.6 × 12 gives inner 585.6 and area 225', () => { + expect(STEEL_PILES.length).toBe(9); + const p = findPileSpec('강관', 609.6, 12)!; + expect(p.inner).toBe(585.6); + expect(p.area).toBe(225); + }); + + it('the PHC list has the four rows of the sheet; a pair not in the list is not found', () => { + expect(PHC_PILES.length).toBe(4); + expect(findPileSpec('PHC', 500, 80)!.inner).toBe(340); + expect(findPileSpec('강관', 609.6, 10)).toBeUndefined(); + }); +});