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:
1 parent
43a18182f3
commit
b3a62850ab
5 files changed
+168
-12
No files matched your search
@@ -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
@@ -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');
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user