From df19cc47e9ea1de90e219f339ad221e5059fadc9 Mon Sep 17 00:00:00 2001 From: ypshim Date: Fri, 23 Oct 2026 16:01:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(view):=20=ED=99=95=EB=8C=80=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=C2=B7=EB=A7=90=EB=9A=9D=20inputs=20on=20=E2=91=A1=20(?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EB=A7=8C):=20footing=20L,=20W,=20h1,=20blind?= =?UTF-8?q?ing=20margin=20and=20thickness,=20steel=20and=20PHC=20pile=20li?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The footing card takes L, W, h1 (default 2000), the blinding margin on each side (100) and thickness (100). The pile is picked from the 강관 or PHC list by outer diameter and wall; the inner diameter, area and weight are read from the list. Defaults are the 1단계 constants, so P1 is unchanged. The model reads them (footing width, height, blinding, pile diameter; the footing length for grouping). 복합 말뚝 and the rule-pending items stay disabled; the footing rebar is not yet tied to these inputs (1·2단계 후). Co-Authored-By: Claude Haiku 5.5 --- src/core/changes.ts | 14 ++++++++ src/core/geometry.ts | 46 ++++++++++++++++---------- src/core/pile-spec.ts | 41 ++++++++++++++++++++++++ src/core/types.ts | 10 ++++++ src/main.ts | 62 ++++++++++++++++++++++++++++++++++-- tests/footing-inputs.test.ts | 28 ++++++++++++++++ tests/pile-spec.test.ts | 17 ++++++++++ 7 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 src/core/pile-spec.ts create mode 100644 tests/footing-inputs.test.ts create mode 100644 tests/pile-spec.test.ts 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(); + }); +});