diff --git a/src/main.ts b/src/main.ts index 6ebffdd..58c8dd3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,7 @@ 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 { footingPlanSvg, footingSideSvg } from './view/footing-svg'; 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'; @@ -716,6 +717,21 @@ function footingLengthOf(p: Pier): number | undefined { return p.model?.footings[0]?.length; } +/** 확대기초·말뚝 그림 (2단계, 승인 2026-10-23): plan and side views drawn from the model; θ is shown as a note. */ +function renderFootingDrawings(box: HTMLDivElement, pier: Pier): void { + if (!pier.model) return; + const wrap = el('div', undefined, 'footing-drawings'); + const plan = el('figure', undefined, 'footing-fig'); + plan.innerHTML = footingPlanSvg(pier.model); + plan.appendChild(el('figcaption', '평면 · 1열이 아래, 열은 교축과 직각 방향 줄', 'muted small')); + const side = el('figure', undefined, 'footing-fig'); + side.innerHTML = footingSideSvg(pier.model); + side.appendChild(el('figcaption', '정면 · 점선 = 경사 선택 시 상면 (L2, h2)', 'muted small')); + wrap.append(plan, side); + wrap.appendChild(el('p', '기초 회전각 θ는 교각축 대비 회전으로, 생성 규칙은 spec에 추가 후 적용합니다 (지금은 0°).', 'muted small')); + box.appendChild(wrap); +} + /** 말뚝 규격 (승인 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 ?? '강관'; @@ -1645,6 +1661,7 @@ function renderPlan(): void { if (member.id === 'footing') { renderPileSpec(fields, pier); renderPileRows(fields, pier); + renderFootingDrawings(fields, pier); } if (member.id === 'coping') renderCopingInfo(fields, pier); if (member.id === 'bearing') renderBearingRef(fields, pier); diff --git a/src/style.css b/src/style.css index 516d0ce..cf82b55 100644 --- a/src/style.css +++ b/src/style.css @@ -794,3 +794,8 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif; .bearing-ref .table-scroll { overflow-x: auto; margin-bottom: 8px; } .bearing-ref table { font-size: 12px; } .bearing-ref input.changed-input { border: 2px solid #e8833a; background: #fff6ee; } + +/* ② 확대기초·말뚝 그림 (2단계) */ +.footing-drawings { display: grid; gap: 10px; margin-top: 12px; } +.footing-fig { margin: 0; } +.footing-fig svg { width: 100%; height: auto; max-height: 260px; background: #ffffff; border: 1px solid #d5dde6; border-radius: 6px; } diff --git a/src/view/footing-svg.ts b/src/view/footing-svg.ts new file mode 100644 index 0000000..b6ec64a --- /dev/null +++ b/src/view/footing-svg.ts @@ -0,0 +1,65 @@ +/** + * 확대기초·말뚝 그림 (2단계, 승인 2026-10-23): plan and side views as SVG strings, drawn from the model (footings, piles, + * columns) and the input values. Pure: no DOM, so that it can be tested. Units: m in the model; the labels show mm. + */ +import type { PierModel } from '../core/geometry'; + +const mm = (v: number) => Math.round(v * 1000); + +/** Plan view: the footing rectangle (length along the 교축, width across), the pile circles, the column circle, the axis. */ +export function footingPlanSvg(model: PierModel): string { + const f = model.footings[0]; + if (!f) return ''; + const pad = 0.8; + const x0 = f.s - f.length / 2 - pad; + const x1 = f.s + f.length / 2 + pad; + const y0 = -f.width / 2 - pad; + const y1 = f.width / 2 + pad; + const piles = model.piles.filter((p) => Math.abs(p.s - f.s) <= f.length / 2 + 1e-6); + const parts: string[] = []; + parts.push(``); + for (const p of piles) { + parts.push(``); + } + for (const c of model.columns) { + parts.push(``); + } + // Pier axis (dash-dot) through the footing centre. + parts.push(``); + // Dimension labels: length and width, in mm. + parts.push(`${mm(f.length)}`); + parts.push(`${mm(f.width)}`); + return `${parts.join('')}`; +} + +/** Side view (elevation): the footing, the piles down to their tips, and the column above the footing (EL up). */ +export function footingSideSvg(model: PierModel): string { + const f = model.footings[0]; + if (!f) return ''; + const piles = model.piles.filter((p) => Math.abs(p.s - f.s) <= f.length / 2 + 1e-6); + const tipEl = piles.length ? Math.min(...piles.map((p) => p.bottom)) : f.bottom; + const topEl = model.levelBottom; + const pad = 0.8; + const x0 = f.s - f.length / 2 - pad; + const x1 = f.s + f.length / 2 + pad; + const y0 = -(topEl + pad); + const y1 = -(tipEl - pad); + const parts: string[] = []; + // Elevation: y = −EL so that the level grows upward on the screen. + parts.push(``); + for (const p of piles) { + parts.push(``); + } + // Column: from the footing top up to the Level underside. + const col = model.columns[0]; + if (col) { + parts.push(``); + } + // Labels: the footing height and the pile length, in mm. + parts.push(`h1 ${mm(f.top - f.bottom)}`); + if (piles.length) { + const pileLen = piles[0].top - piles[0].bottom; + parts.push(`말뚝 ${mm(pileLen)}`); + } + return `${parts.join('')}`; +} diff --git a/tests/footing-svg.test.ts b/tests/footing-svg.test.ts new file mode 100644 index 0000000..eb3721c --- /dev/null +++ b/tests/footing-svg.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier } from '../src/core/geometry'; +import { footingPlanSvg, footingSideSvg } from '../src/view/footing-svg'; +import { loadP1 } from './helpers'; + +const m = buildPier(loadP1()); +const count = (svg: string, tag: string) => (svg.match(new RegExp(`<${tag} `, 'g')) ?? []).length; + +describe('확대기초·말뚝 그림 (2단계, 승인 2026-10-23)', () => { + it('the plan draws one circle per pile under the footing, and labels the footing length and width in mm', () => { + const svg = footingPlanSvg(m); + const piles = m.piles.filter((p) => Math.abs(p.s - m.footings[0].s) <= m.footings[0].length / 2 + 1e-6).length; + // The column circles are dashed; the pile circles are the solid ones. + expect(count(svg, 'circle') - m.columns.length).toBe(piles); + expect(svg).toContain('10400'); + expect(svg).toContain('8150'); + }); + + it('the side view shows the footing rectangle and one line per pile, with the pile length label', () => { + const svg = footingSideSvg(m); + expect(count(svg, 'line')).toBeGreaterThanOrEqual(20); + expect(svg).toContain('h1 2000'); + expect(svg).toContain('말뚝 24000'); + }); + + it('the drawings follow the footing inputs (width 9000 mm shows in the plan label)', () => { + const wide = buildPier({ ...loadP1(), footingWidthMm: 9000 }); + expect(footingPlanSvg(wide)).toContain('9000'); + }); +});