feat(bearing): section and solplate detail drawings with the drawing values overlaid (승인 2026-10-26)

- view/bearing-section-svg.ts: the pair's elevation — blocks (일체: one stepped
  outline), mortar, bearing body and solplate per bearing from the model; the
  drawing heights overlaid dashed. EL.X at the mortar top, E = h1 + h2 + h3 + h4.
- view/solplate-svg.ts: per bearing, the plan L × W and the taper T1 · T2
  (S %), with the drawing values dashed.
- ② bearing card: section figure and the two solplate figures under the plan.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 12:33:05 +09:00
1 parent 0708aa1354
commit 0b847e7547
4 files changed
+217 -12

No files matched your search

+44 -12
View File
@@ -20,6 +20,8 @@ import { groundElFor, parseGroundModel, strataRowsFor, type GroundModelFile } fr
import { CORROSION_ALLOWANCE_MM, parseNValue, SEA_DATUM, STRATA_NAMES } from './core/ground';
import { bearingEdgeDistances, integratedCheck, PAIR_ECCENTRICITY_LIMIT_MM, pairEccentricities } from './core/bearing-layout';
import { bearingPlanSvg, type BearingPlanOverlay, PAIR_CHIP_LABELS } from './view/bearing-plan-svg';
import { bearingSectionSvg, type BearingDrawingRef } from './view/bearing-section-svg';
import { solplateDetailSvg, type SolplateDrawingRef } from './view/solplate-svg';
import { groundSectionSvg } from './view/ground-svg';
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
import { columnDiameterIssues } from './core/column-check';
@@ -1520,18 +1522,30 @@ function renderBearingBasis(wrap: HTMLDivElement, pier: Pier): void {
let bearingPairIndex = 0;
let bearingOverlayOn = true;
/** Drawing A, B of the front and back bearings of a pair: the imported drawing rows if any, else the reference table. */
/**
* Drawing values of one bearing for the overlays (승인 2026-10-26): the imported drawing row where it has the value
* (elx, h1, h2, A, B), otherwise the reference table (h3, h4, E).
*/
function drawingRefFor(pier: Pier, no: number): BearingDrawingRef | undefined {
const r = BEARING_ELEVATION_REF.find((x) => x.no === no);
if (!r) return undefined;
const d = (bearingDrawing[pier.input.name] ?? []).find((x) => x.no === no);
const A = d?.A ?? r.A;
const B = d?.B ?? r.B;
return { elx: d?.elx ?? r.elx, h1: d?.h1 ?? r.h1, h2: d?.h2 ?? r.h2, h3: r.h3, h4: r.h4, E: r.E, A, B };
}
/** Drawing A, B of the front and back bearings of a pair, for the plan overlay. */
function overlayFor(pier: Pier, frontNo: number, backNo: number): BearingPlanOverlay | undefined {
const rows = bearingDrawing[pier.input.name] ?? [];
const pick = (no: number) => {
const d = rows.find((r) => r.no === no);
if (d?.A !== undefined && d?.B !== undefined) return { A: d.A, B: d.B };
const r = BEARING_ELEVATION_REF.find((x) => x.no === no);
return r ? { A: r.A, B: r.B } : undefined;
};
const f = pick(frontNo);
const b = pick(backNo);
return f && b ? { front: f, back: b } : undefined;
const f = drawingRefFor(pier, frontNo);
const b = drawingRefFor(pier, backNo);
return f && b ? { front: { A: f.A, B: f.B }, back: { A: b.A, B: b.B } } : undefined;
}
/** Drawing solplate values of one bearing (reference table), for the solplate detail overlay. */
function solplateRefFor(no: number): SolplateDrawingRef | undefined {
const r = SOLPLATE_REF.find((x) => x.no === no);
return r ? { L: r.L, W: r.W, T1: r.T1, T2: r.T2, S: r.s } : undefined;
}
function renderBearingLayout(wrap: HTMLDivElement, pier: Pier): void {
@@ -1568,8 +1582,26 @@ function renderBearingLayout(wrap: HTMLDivElement, pier: Pier): void {
const fig = el('figure', undefined, 'ground-fig');
const overlay = bearingOverlayOn ? overlayFor(pier, current.frontNo, current.backNo) : undefined;
fig.innerHTML = bearingPlanSvg(pier.model, bearingPairIndex, overlay);
fig.appendChild(el('figcaption', '평면 · 실선 = 모델(솔플레이트, 블록 윤곽) · 점선 = 도면값 · C4·C5는 교각축 직각 방향 코핑 가장자리에서 받침까지', 'muted small'));
fig.appendChild(el('figcaption', '평면 · 실선 = 모델(솔플레이트, 블록 윤곽) · 점선 = 도면값 · C4·C5는 교축 방향, 받침 중심에서 블록 바깥 연단까지', 'muted small'));
wrap.appendChild(fig);
// 단면 (elevation): the blocks, mortar, bearing body and solplates; the drawing heights dashed.
const section = el('figure', undefined, 'ground-fig');
const refs = bearingOverlayOn ? { front: drawingRefFor(pier, current.frontNo), back: drawingRefFor(pier, current.backNo) } : undefined;
section.innerHTML = bearingSectionSvg(pier.model, bearingPairIndex, refs);
section.appendChild(el('figcaption', '단면 · EL.X는 모르타르 상단, E = h1 + h2 + h3 + h4 · 점선 = 도면 높이', 'muted small'));
wrap.appendChild(section);
// 솔플레이트 상세 for the two bearings of the pair (plan and taper, drawing dashed).
const solGrid = el('div', undefined, 'footing-grid');
for (const no of [current.frontNo, current.backNo]) {
const block = pier.model.blocks.find((b) => b.no === no)!;
const fig2 = el('figure', undefined, 'footing-fig');
fig2.innerHTML = solplateDetailSvg(block, bearingOverlayOn ? solplateRefFor(no) : undefined);
fig2.appendChild(el('figcaption', `솔플레이트 상세 · ${no}번 · 두께 과장 · 점선 = 도면값`, 'muted small'));
solGrid.appendChild(fig2);
}
wrap.appendChild(solGrid);
}
wrap.appendChild(el('p', `배치 규칙 (모델값 · 편심 한계 ${PAIR_ECCENTRICITY_LIMIT_MM} mm)`, 'lbl'));
const edges = bearingEdgeDistances(pier.model);
+82
View File
@@ -0,0 +1,82 @@
import type { PierModel } from '../core/geometry';
import { pairEccentricities } from '../core/bearing-layout';
/**
* Drawing values of one bearing (도면값), overlaid as dashed outlines (승인 2026-10-26).
* All lengths in mm; elx in m.
*/
export interface BearingDrawingRef {
elx?: number;
h1: number;
h2: number;
h3: number;
h4: number;
E: number;
A: number;
B: number;
}
/**
* 교량받침 상세 · 단면 (elevation) for one pair (승인 2026-10-26): the model's blocks, mortar, bearing body,
* and solplate per bearing, with the drawing heights overlaid as dashed outlines. x is the 교축 (n), y the EL (up).
* Labels: EL.X (mortar top), h1 (block), h2 (mortar), h3 (bearing), h4 (solplate), E = h1 + h2 + h3 + h4.
*/
export function bearingSectionSvg(m: PierModel, pairIndex: number, refs?: { front?: BearingDrawingRef; back?: BearingDrawingRef }): string {
const pair = pairEccentricities(m.blocks)[pairIndex];
if (!pair) return '';
const f = m.blocks.find((b) => b.no === pair.frontNo && b.row === '전열')!;
const k = m.blocks.find((b) => b.no === pair.backNo && b.row === '후열')!;
const nc = (f.n + k.n) / 2;
const elc = (f.bottom + k.bottom) / 2;
const X = (n: number) => (n - nc) * 1000;
const Y = (el: number) => -(el - elc) * 1000;
const parts: string[] = [];
// Blocks: one stepped outline for 일체, else one per bearing.
const ib = m.integratedBlocks.find((x) => x.frontNo === pair.frontNo);
if (ib) {
const x0 = X(ib.n - 1.25);
const x1 = X(ib.n + 1.25);
const xm = X(ib.n);
parts.push(`<polygon points="${x0},${Y(ib.bottom)} ${x1},${Y(ib.bottom)} ${x1},${Y(ib.topBack)} ${xm},${Y(ib.topBack)} ${xm},${Y(ib.topFront)} ${x0},${Y(ib.topFront)}" fill="#D6DDE5" stroke="#6E8496" stroke-width="10"></polygon>`);
} else {
for (const b of [f, k]) {
parts.push(`<rect x="${X(b.n) - 600}" y="${Y(b.top)}" width="1200" height="${(b.top - b.bottom) * 1000}" fill="#D6DDE5" stroke="#6E8496" stroke-width="10"></rect>`);
}
}
// Mortar, bearing body and solplate per bearing, from the model.
for (const [b, ref] of [[f, refs?.front], [k, refs?.back]] as const) {
const pad = 1000;
parts.push(`<rect x="${X(b.n) - pad / 2}" y="${Y(b.padTop)}" width="${pad}" height="${50}" fill="#B7C7D3" stroke="#6E8496" stroke-width="6"></rect>`);
if (b.plan) {
const { A, margin, h3, T } = b.plan;
const L = A + margin;
const bodyTop = b.padTop + h3 / 1000;
parts.push(`<rect x="${X(b.n) - (L - 200) / 2}" y="${Y(bodyTop)}" width="${L - 200}" height="${h3}" fill="#8898AA" stroke="#5B6676" stroke-width="6"></rect>`);
const drop = (L * Math.abs(b.plan.S)) / 200;
const t1 = T + drop;
const t2 = T - drop;
const x0 = X(b.n) - L / 2;
const x1 = X(b.n) + L / 2;
parts.push(`<polygon points="${x0},${Y(bodyTop)} ${x1},${Y(bodyTop)} ${x1},${Y(bodyTop + t2 / 1000)} ${x0},${Y(bodyTop + t1 / 1000)}" fill="#F3D9B8" stroke="#B4531F" stroke-width="6"></polygon>`);
}
if (ref) {
// Drawing heights from the block bottom, dashed (overlay).
const base = b.bottom;
const dBlockTop = base + (ref.h1 ?? 0) / 1000;
const dPad = dBlockTop + ref.h2 / 1000;
const dBody = dPad + ref.h3 / 1000;
const dSol = dBody + ref.h4 / 1000;
parts.push(`<rect x="${X(b.n) - 600}" y="${Y(dBlockTop)}" width="1200" height="${(dBlockTop - base) * 1000}" fill="none" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></rect>`);
parts.push(`<line x1="${X(b.n) - 700}" y1="${Y(dSol)}" x2="${X(b.n) + 700}" y2="${Y(dSol)}" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></line>`);
parts.push(`<line x1="${X(b.n) - 700}" y1="${Y(dBody)}" x2="${X(b.n) + 700}" y2="${Y(dBody)}" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></line>`);
}
// EL.X at the mortar top, bearing number.
parts.push(`<circle cx="${X(b.n)}" cy="${Y(b.padTop)}" r="30" fill="#18212B"></circle>`);
parts.push(`<text x="${X(b.n) + 60}" y="${Y(b.padTop) - 60}" font-size="90" fill="#18212B">${b.no} · EL.X ${b.padTop.toFixed(3)}</text>`);
parts.push(`<text x="${X(b.n)}" y="${Y(b.bottom) + 160}" text-anchor="middle" font-size="90" fill="#55606E">h1 ${Math.round((b.top - b.bottom) * 1000)}</text>`);
}
return `<svg viewBox="-2000 -1200 4000 2000" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="교량받침 상세 단면: 블록, 모르타르, 받침, 솔플레이트, EL.X, 도면 높이 점선">${parts.join('')}</svg>`;
}
+47
View File
@@ -0,0 +1,47 @@
import type { BlockModel } from '../core/geometry';
/** Drawing values of one solplate (도면값), overlaid as dashed outlines. mm. */
export interface SolplateDrawingRef {
L: number;
W: number;
T1: number;
T2: number;
S: number;
}
/**
* 솔플레이트 상세 (승인 2026-10-26) for one bearing: the plan (L across the 교축, W along the 교각축) and the
* taper in side view (T1 at the (−) side, T2 at the (+) side, S % · T). Model solid, drawing dashed.
*/
export function solplateDetailSvg(block: BlockModel, ref?: SolplateDrawingRef): string {
const plan = block.plan;
if (!plan) return '';
const L = plan.A + plan.margin;
const W = plan.B + plan.margin;
const drop = (L * Math.abs(plan.S)) / 200;
const t1 = plan.T + drop;
const t2 = plan.T - drop;
const parts: string[] = [];
// Plan (left): L across, W along.
parts.push(`<rect x="${-L / 2}" y="${-W / 2}" width="${L}" height="${W}" fill="#F3D9B8" stroke="#B4531F" stroke-width="8"></rect>`);
parts.push(`<text x="0" y="${-W / 2 - 40}" text-anchor="middle" font-size="60" fill="#18212B">L ${L.toFixed(0)}</text>`);
parts.push(`<text x="${L / 2 + 40}" y="0" font-size="60" fill="#18212B">W ${W.toFixed(0)}</text>`);
if (ref) {
parts.push(`<rect x="${-ref.L / 2}" y="${-ref.W / 2}" width="${ref.L}" height="${ref.W}" fill="none" stroke="#B4531F" stroke-width="6" stroke-dasharray="40 25"></rect>`);
}
// Side view (right): thickness T1 at x = −L/2 and T2 at x = +L/2, shown with the taper.
const ox = L / 2 + 500;
const sc = 4; // exaggerated thickness for legibility, labelled
parts.push(`<polygon points="${ox - L / 2},0 ${ox + L / 2},0 ${ox + L / 2},${-t2 * sc} ${ox - L / 2},${-t1 * sc}" fill="#F3D9B8" stroke="#B4531F" stroke-width="8"></polygon>`);
parts.push(`<text x="${ox - L / 2}" y="${-t1 * sc - 40}" font-size="60" fill="#18212B">T1 ${t1.toFixed(0)}</text>`);
parts.push(`<text x="${ox + L / 2}" y="${-t2 * sc - 40}" text-anchor="end" font-size="60" fill="#18212B">T2 ${t2.toFixed(0)}</text>`);
parts.push(`<text x="${ox}" y="120" text-anchor="middle" font-size="60" fill="#55606E">S ${plan.S} % · T ${plan.T} (두께 과장 ×${sc})</text>`);
if (ref) {
const r1 = ref.T1;
const r2 = ref.T2;
parts.push(`<polygon points="${ox - ref.L / 2},0 ${ox + ref.L / 2},0 ${ox + ref.L / 2},${-r2 * sc} ${ox - ref.L / 2},${-r1 * sc}" fill="none" stroke="#B4531F" stroke-width="6" stroke-dasharray="40 25"></polygon>`);
}
const minX = -L / 2 - 200;
const maxX = ox + L / 2 + 200;
return `<svg viewBox="${minX} -900 ${maxX - minX} 1300" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="솔플레이트 상세: 평면 L × W, 두께 T1 · T2 (두께 과장)">${parts.join('')}</svg>`;
}
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { bearingSectionSvg } from '../src/view/bearing-section-svg';
import { solplateDetailSvg } from '../src/view/solplate-svg';
import { loadP1 } from './helpers';
const model = buildPier(loadP1());
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' });
const ref = { h1: 150, h2: 50, h3: 181, h4: 30, E: 425, A: 700, B: 640 };
describe('교량받침 상세 단면 (승인 2026-10-26)', () => {
it('labels EL.X at the mortar top and the block height h1 for each bearing', () => {
const svg = bearingSectionSvg(model, 0);
expect(svg).toContain('EL.X');
expect(svg).toContain('h1 150');
});
it('draws the drawing heights dashed only when given', () => {
expect(bearingSectionSvg(model, 0)).not.toContain('stroke-dasharray="40 25"');
const svg = bearingSectionSvg(model, 0, { front: ref, back: ref });
expect((svg.match(/stroke-dasharray="40 25"/g) ?? []).length).toBeGreaterThan(0);
});
it('일체 draws one stepped outline (a polygon) for the pair', () => {
expect(bearingSectionSvg(integrated, 0)).toContain('<polygon');
});
it('an unknown pair gives an empty drawing', () => {
expect(bearingSectionSvg(model, 99)).toBe('');
});
});
describe('솔플레이트 상세 (승인 2026-10-26)', () => {
it('shows L, W, T1 and T2 of the model plan, with the drawing dashed when given', () => {
const withPlan = buildPier({ ...loadP1(), bearingPlans: [{ no: 1, A: 700, B: 640, S: -1.9 }] });
const block = withPlan.blocks.find((b) => b.no === 1)!;
const svg = solplateDetailSvg(block);
expect(svg).toContain('L ');
expect(svg).toContain('T1 ');
expect(svg).toContain('T2 ');
const withRef = solplateDetailSvg(block, { L: 800, W: 748, T1: 38, T2: 22, S: 1.9 });
expect(withRef).toContain('stroke-dasharray="40 25"');
});
});