diff --git a/src/core/bearing-layout.ts b/src/core/bearing-layout.ts
index 84fa9c7..799555e 100644
--- a/src/core/bearing-layout.ts
+++ b/src/core/bearing-layout.ts
@@ -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,
}));
}
diff --git a/src/main.ts b/src/main.ts
index 6645a42..912c424 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -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');
diff --git a/src/view/bearing-plan-svg.ts b/src/view/bearing-plan-svg.ts
new file mode 100644
index 0000000..2a3e912
--- /dev/null
+++ b/src/view/bearing-plan-svg.ts
@@ -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(``);
+ parts.push(`C3 2500 (일체 블록 윤곽)`);
+ } else {
+ for (const b of [f, k]) {
+ parts.push(``);
+ }
+ }
+
+ // 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(``);
+ }
+ }
+ // 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(``);
+ }
+ }
+
+ // Bearing centres and labels.
+ for (const b of [f, k]) {
+ parts.push(``);
+ parts.push(`${b.no}`);
+ }
+
+ // 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(`C2 ${Math.round(Math.abs(k.n - f.n) * 1000)}`);
+ parts.push(`C4 ${Math.round(d(f.no).leftMm)}`);
+ parts.push(`C5 ${Math.round(d(k.no).rightMm)}`);
+ parts.push(`코핑 폭 B ${Math.round(m.copingB * 1000)}`);
+
+ return ``;
+}
diff --git a/tests/bearing-layout.test.ts b/tests/bearing-layout.test.ts
index 916ab3d..aa64a13 100644
--- a/tests/bearing-layout.test.ts
+++ b/tests/bearing-layout.test.ts
@@ -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', () => {
diff --git a/tests/bearing-plan-svg.test.ts b/tests/bearing-plan-svg.test.ts
new file mode 100644
index 0000000..70e0925
--- /dev/null
+++ b/tests/bearing-plan-svg.test.ts
@@ -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('');
+ });
+});