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 `${parts.join('')}`; +} 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(''); + }); +});