feat(bearing): plan of a pair with block outline and overlaid drawing values (승인 2026-10-26)

- C4·C5 measured across the coping width (교각축 직각): the two coping edges
  at ±B/2 to the bearing centre. The former along-the-axis definition is replaced.
- view/bearing-plan-svg.ts: the plan of one pair with the model's solplates
  and the block outline(s) (일체: one 2500 × 1200; 분리: two 1200 × 1200), and
  the drawing A/B overlaid as dashed outlines.
- ② bearing card: 상세 보기 pair chips (①·⑥ …) and the overlay toggle.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 12:19:14 +09:00
1 parent 43a18182f3
commit b3a62850ab
5 files changed
+168 -12

No files matched your search

+7 -7
View File
@@ -2,7 +2,7 @@ import type { BlockModel, PierModel } from './geometry';
/**
* 받침 배치 규칙 (승인 2026-10-26).
* - C4·C5: 코핑 가장자리와 받침 중심(받침좌표) 사이 거리, 교각축 방향 (s). 모델값을 쓰고 도면값과 비교한다.
* - C4·C5: 코핑 가장자리(교각축 직각, 폭 B의 두 가장자리)와 받침 중심 사이 거리. 모델값을 쓰고 도면값과 비교한다.
* - 일체: 같은 순서끼리 (전열 1 · 후열 1, 전열 2 · 후열 2 …, 각 열의 번호 순) 두 받침의 편심거리가 100 mm를 넘으면 분리를 우선한다.
* (P1 은 전열 1–5, 후열 6–10 으로 번호가 매겨져 있어서 번호 자체가 아니라 열 안의 순서로 짝짓는다.)
* 편심거리 = 두 받침 중심의 교각축 방향 차이 |s(전열) − s(후열)|.
@@ -12,9 +12,9 @@ export const PAIR_ECCENTRICITY_LIMIT_MM = 100;
export interface EdgeDistance {
no: number;
row: BlockModel['row'];
/** 코핑 시작(s = 0)에서 받침 중심까지 (mm). */
/** 교각축 직각 방향: 코핑 한쪽 가장자리(n = −B/2)에서 받침 중심까지 (mm) = C4. */
leftMm: number;
/** 받침 중심에서 코핑 끝(s = L)까지 (mm). */
/** 받침 중심에서 코핑 다른 가장자리(n = +B/2)까지 (mm) = C5. */
rightMm: number;
}
@@ -27,14 +27,14 @@ export interface PairEccentricity {
allowed: boolean;
}
/** C4·C5 from the model: distances from each coping edge to each bearing centre, along the coping length (mm). */
/** C4·C5 from the model: distances from the two coping edges (±B/2, across the coping width) to each bearing centre (mm). */
export function bearingEdgeDistances(m: PierModel): EdgeDistance[] {
const L = m.copingL * 1000;
const half = m.copingB / 2;
return m.blocks.map((b) => ({
no: b.no,
row: b.row,
leftMm: b.s * 1000,
rightMm: L - b.s * 1000,
leftMm: (b.n + half) * 1000,
rightMm: (half - b.n) * 1000,
}));
}
+54
View File
@@ -19,6 +19,7 @@ import { parseDrawingRows } from './core/drawing-import';
import { groundElFor, parseGroundModel, strataRowsFor, type GroundModelFile } from './core/ground-model';
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 { groundSectionSvg } from './view/ground-svg';
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
import { columnDiameterIssues } from './core/column-check';
@@ -1515,8 +1516,61 @@ function renderBearingBasis(wrap: HTMLDivElement, pier: Pier): void {
* 받침 배치 (승인 2026-10-26, read-only): C4·C5 from the model (coping edge ↔ bearing centre, 교각축 방향) and the
* eccentricity of each front–back pair (일체 allowed up to 100 mm, otherwise 분리 first).
*/
/** 상세 보기 pair shown in the plan, and whether the drawing values are overlaid (screen state only, 승인 2026-10-26). */
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. */
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;
}
function renderBearingLayout(wrap: HTMLDivElement, pier: Pier): void {
if (!pier.model) return;
// 상세 보기 (승인 2026-10-26): pair chips (①·⑥ …) and the plan with the drawing values overlaid as dashed outlines.
const pairList = pairEccentricities(pier.model.blocks);
if (bearingPairIndex >= pairList.length) bearingPairIndex = 0;
wrap.appendChild(el('p', '상세 보기 (전열 · 후열 쌍)', 'lbl'));
const chips = el('div', undefined, 'opts');
pairList.forEach((p, i) => {
const chip = el('button', `${PAIR_CHIP_LABELS[p.frontNo - 1] ?? p.frontNo}·${PAIR_CHIP_LABELS[p.backNo - 1] ?? p.backNo}`, 'btn small');
chip.type = 'button';
chip.setAttribute('aria-pressed', String(i === bearingPairIndex));
chip.addEventListener('click', () => {
bearingPairIndex = i;
renderAll();
});
chips.appendChild(chip);
});
wrap.appendChild(chips);
const overlayCheck = el('input');
overlayCheck.type = 'checkbox';
overlayCheck.checked = bearingOverlayOn;
overlayCheck.id = 'bearing-overlay';
overlayCheck.addEventListener('change', () => {
bearingOverlayOn = overlayCheck.checked;
renderAll();
});
const overlayLabel = el('label', ' 도면값 겹쳐 보기 (점선)');
overlayLabel.prepend(overlayCheck);
wrap.appendChild(overlayLabel);
const current = pairList[bearingPairIndex];
if (current) {
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'));
wrap.appendChild(fig);
}
wrap.appendChild(el('p', `배치 규칙 (모델값 · 편심 한계 ${PAIR_ECCENTRICITY_LIMIT_MM} mm)`, 'lbl'));
const edges = bearingEdgeDistances(pier.model);
const table = el('table', undefined, 'grid-table rebar-table');
+70
View File
@@ -0,0 +1,70 @@
import type { PierModel } from '../core/geometry';
import { bearingEdgeDistances, pairEccentricities } from '../core/bearing-layout';
/** Drawing values (A, B of the solplate, mm) of the front and back bearings, to overlay on the model (승인 2026-10-26). */
export interface BearingPlanOverlay {
front: { A: number; B: number };
back: { A: number; B: number };
}
/** Number of pairs the 상세 보기 chips can show (①–⑩ in the mockup). */
export const PAIR_CHIP_LABELS = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
/**
* 교량받침 상세 · 평면 for one pair (승인 2026-10-26): the model's solplates and blocks, with the drawing values overlaid
* as dashed outlines when given. 일체 draws the pair's block as one outline (2500 across the 교축 × 1200 along the 교각축);
* 분리 draws two 1200 × 1200 outlines. x is the 교축 (n), y the 교각축 (s, downward); units mm.
*/
export function bearingPlanSvg(m: PierModel, pairIndex: number, overlay?: BearingPlanOverlay): 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 sc = (f.s + k.s) / 2;
const X = (n: number) => (n - nc) * 1000;
const Y = (s: number) => (s - sc) * 1000;
const parts: string[] = [];
// Block outline(s): 일체 = one 2500 × 1200 outline; 분리 = two 1200 × 1200 outlines.
const ib = m.integratedBlocks.find((x) => x.frontNo === pair.frontNo);
if (ib) {
parts.push(`<rect x="${X(ib.n - 1.25)}" y="${Y(ib.s - 0.6)}" width="2500" height="1200" fill="none" stroke="#6E8496" stroke-width="12"></rect>`);
parts.push(`<text x="${X(ib.n)}" y="${Y(ib.s - 0.6) - 60}" text-anchor="middle" font-size="90" fill="#55606E">C3 2500 (일체 블록 윤곽)</text>`);
} else {
for (const b of [f, k]) {
parts.push(`<rect x="${X(b.n) - 600}" y="${Y(b.s) - 600}" width="1200" height="1200" fill="none" stroke="#6E8496" stroke-width="12"></rect>`);
}
}
// Solplates: the model's plan (A across the 교축, B along the 교각축), centred on each bearing.
for (const b of [f, k]) {
if (b.plan) {
const { A, B } = b.plan;
parts.push(`<rect x="${X(b.n) - A / 2}" y="${Y(b.s) - B / 2}" width="${A}" height="${B}" fill="#EEF3F7" stroke="#2F6DB5" stroke-width="10"></rect>`);
}
}
// Drawing values overlaid (dashed red) on the same bearings (#1 겹쳐 보이게).
if (overlay) {
for (const [b, v] of [[f, overlay.front], [k, overlay.back]] as const) {
parts.push(`<rect x="${X(b.n) - v.A / 2}" y="${Y(b.s) - v.B / 2}" width="${v.A}" height="${v.B}" fill="none" stroke="#B4531F" stroke-width="10" stroke-dasharray="40 25"></rect>`);
}
}
// Bearing centres and labels.
for (const b of [f, k]) {
parts.push(`<circle cx="${X(b.n)}" cy="${Y(b.s)}" r="40" fill="#18212B"></circle>`);
parts.push(`<text x="${X(b.n) + 70}" y="${Y(b.s) - 60}" font-size="90" fill="#18212B">${b.no}</text>`);
}
// Dimensions: C2 between the bearing centres, C4 and C5 from the coping edges (교각축 직각), C1 to the block edges.
const dist = bearingEdgeDistances(m);
const d = (no: number) => dist.find((x) => x.no === no)!;
const half = m.copingB / 2;
parts.push(`<text x="${(X(f.n) + X(k.n)) / 2}" y="${Y(f.s) + 180}" text-anchor="middle" font-size="90" fill="#18212B">C2 ${Math.round(Math.abs(k.n - f.n) * 1000)}</text>`);
parts.push(`<text x="${X(f.n) - 760}" y="${Y(f.s)}" text-anchor="end" font-size="90" fill="#18212B">C4 ${Math.round(d(f.no).leftMm)}</text>`);
parts.push(`<text x="${X(k.n) + 760}" y="${Y(k.s)}" font-size="90" fill="#18212B">C5 ${Math.round(d(k.no).rightMm)}</text>`);
parts.push(`<text x="${X(half)}" y="${Y(f.s) - 760}" text-anchor="end" font-size="80" fill="#55606E">코핑 폭 B ${Math.round(m.copingB * 1000)}</text>`);
return `<svg viewBox="-1800 -1000 3600 2000" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="교량받침 상세 평면: 쌍 ${pair.frontNo}·${pair.backNo}, 블록 윤곽, 솔플레이트, 도면값 겹쳐 보기">${parts.join('')}</svg>`;
}
+5 -5
View File
@@ -12,14 +12,14 @@ function block(no: number, row: '전열' | '후열', s: number): BlockModel {
}
describe('받침 배치 규칙 (승인 2026-10-26)', () => {
it('C4 and C5 from the model: distances from each coping edge to the bearing centre (P1 ≈ 943 / 950 mm)', () => {
it('C4 and C5 from the model: distances from the two coping edges across the width (교각축 직각), P1 front 852 / 2248', () => {
const d = bearingEdgeDistances(model);
expect(d).toHaveLength(10);
const first = d.find((x) => x.no === 1 && x.row === '전열')!;
expect(first.leftMm).toBeCloseTo(943, 0);
expect(first.rightMm).toBeCloseTo(12000 - 943, 0);
// the two ends of one bearing sum to the coping length
for (const x of d) expect(x.leftMm + x.rightMm).toBeCloseTo(12000, 6);
expect(first.leftMm).toBeCloseTo(852, 0);
expect(first.rightMm).toBeCloseTo(2248, 0);
// the two edges are one coping width (3100) apart
for (const x of d) expect(x.leftMm + x.rightMm).toBeCloseTo(3100, 6);
});
it('the front and back bearings of one number are about 85 mm apart on P1, so 일체 is allowed', () => {
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { bearingPlanSvg } from '../src/view/bearing-plan-svg';
import { loadP1 } from './helpers';
const separate = buildPier(loadP1());
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' });
describe('교량받침 상세 평면 (승인 2026-10-26)', () => {
it('shows C4 and C5 across the coping width (P1 front 852 / 2248)', () => {
const svg = bearingPlanSvg(separate, 0);
expect(svg).toContain('C4 852');
expect(svg).toContain('C5 950');
});
it('분리 draws two 1200 outlines; 일체 draws one 2500 outline (block outline only)', () => {
expect((bearingPlanSvg(separate, 0).match(/width="1200" height="1200"/g) ?? []).length).toBe(2);
expect(bearingPlanSvg(integrated, 0)).toContain('width="2500" height="1200"');
});
it('the drawing values are overlaid as dashed outlines only when given', () => {
const plain = bearingPlanSvg(separate, 0);
expect(plain).not.toContain('stroke-dasharray="40 25"');
const overlay = { front: { A: 700, B: 640 }, back: { A: 700, B: 540 } };
const svg = bearingPlanSvg(separate, 0, overlay);
expect((svg.match(/stroke-dasharray="40 25"/g) ?? []).length).toBe(2);
});
it('an unknown pair index gives an empty drawing', () => {
expect(bearingPlanSvg(separate, 99)).toBe('');
});
});