feat(view): 확대기초·말뚝 plan and side drawings on ② (2단계), drawn from the model

The footing card shows a plan (footing rectangle, pile circles, column, pier axis, mm labels) and an elevation (footing,
piles to their tips, column, h1 and pile length). The drawings are SVG strings built from the model, so they follow the
inputs. θ is noted as not yet applied. Tests check the pile count, the mm labels and the width input.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-23 16:02:37 +09:00
1 parent df19cc47e9
commit 675f8124ca
4 files changed
+117

No files matched your search

+17
View File
@@ -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);
+5
View File
@@ -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; }
+65
View File
@@ -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(`<rect x="${f.s - f.length / 2}" y="${-f.width / 2}" width="${f.length}" height="${f.width}" fill="#dfe5ec" stroke="#5b6676" stroke-width="0.02"/>`);
for (const p of piles) {
parts.push(`<circle cx="${p.s}" cy="${p.n}" r="${p.diameter / 2}" fill="#8898aa" stroke="#3b4656" stroke-width="0.01"/>`);
}
for (const c of model.columns) {
parts.push(`<circle cx="${c.s}" cy="0" r="${c.diameter / 2}" fill="none" stroke="#0f5c66" stroke-width="0.03" stroke-dasharray="0.1 0.06"/>`);
}
// Pier axis (dash-dot) through the footing centre.
parts.push(`<line x1="${x0}" y1="0" x2="${x1}" y2="0" stroke="#b4531f" stroke-width="0.02" stroke-dasharray="0.3 0.1 0.05 0.1"/>`);
// Dimension labels: length and width, in mm.
parts.push(`<text x="${f.s}" y="${-f.width / 2 - 0.35}" font-size="0.28" text-anchor="middle">${mm(f.length)}</text>`);
parts.push(`<text x="${f.s + f.length / 2 + 0.35}" y="0" font-size="0.28" text-anchor="start">${mm(f.width)}</text>`);
return `<svg viewBox="${x0} ${y0} ${x1 - x0} ${y1 - y0}" role="img" aria-label="확대기초·말뚝 평면"><g transform="scale(1,1)">${parts.join('')}</g></svg>`;
}
/** 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(`<rect x="${f.s - f.length / 2}" y="${-f.top}" width="${f.length}" height="${f.top - f.bottom}" fill="#dfe5ec" stroke="#5b6676" stroke-width="0.02"/>`);
for (const p of piles) {
parts.push(`<line x1="${p.s}" y1="${-p.top}" x2="${p.s}" y2="${-p.bottom}" stroke="#8898aa" stroke-width="${Math.max(p.diameter, 0.02)}"/>`);
}
// Column: from the footing top up to the Level underside.
const col = model.columns[0];
if (col) {
parts.push(`<rect x="${col.s - col.diameter / 2}" y="${-topEl}" width="${col.diameter}" height="${topEl - f.top}" fill="#c9d4df" stroke="#0f5c66" stroke-width="0.02"/>`);
}
// Labels: the footing height and the pile length, in mm.
parts.push(`<text x="${f.s + f.length / 2 + 0.35}" y="${-(f.top + f.bottom) / 2}" font-size="0.28" text-anchor="start">h1 ${mm(f.top - f.bottom)}</text>`);
if (piles.length) {
const pileLen = piles[0].top - piles[0].bottom;
parts.push(`<text x="${f.s}" y="${-(tipEl + 0.5)}" font-size="0.28" text-anchor="middle">말뚝 ${mm(pileLen)}</text>`);
}
return `<svg viewBox="${x0} ${y0} ${x1 - x0} ${y1 - y0}" role="img" aria-label="확대기초·말뚝 측면"><g transform="scale(1,1)">${parts.join('')}</g></svg>`;
}
+30
View File
@@ -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');
});
});