From b1b2ad1ddbf869f35061f27d9b0421c71baa6f8f Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 3 Nov 2026 11:22:45 +0900 Subject: [PATCH] =?UTF-8?q?feat(bearing):=20=EA=B5=90=EB=9F=89=EB=B0=9B?= =?UTF-8?q?=EC=B9=A8=20ELEVATION=20TABLE=EA=B3=BC=20=EC=86=94=ED=94=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=8A=B8=20TABLE=EC=9D=84=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모델값만 사용 (도면값 없음). A·B 열은 제거 - ELEVATION: EL.X(엑셀 z), h1 블록, h2 몰탈, h3 받침, h4 솔플(T), E, C1–C6, S1·S2 (블록 철근의 본수·길이, 전열 행에 일체 쌍 표시) - 솔플레이트: S, L, W, T, T1, T2 (소수 한 자리), 검산 T1−T2 ≈ L·|S|/100 - 계산은 core/bearing-table.ts 순수 함수, 테스트 포함 Co-Authored-By: Claude Haiku 5.5 --- src/core/bearing-table.ts | 127 ++++++++++++++++++++++++++++++++++++ src/main.ts | 47 +++++++++++++ tests/bearing-table.test.ts | 57 ++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 src/core/bearing-table.ts create mode 100644 tests/bearing-table.test.ts diff --git a/src/core/bearing-table.ts b/src/core/bearing-table.ts new file mode 100644 index 0000000..faa7c69 --- /dev/null +++ b/src/core/bearing-table.ts @@ -0,0 +1,127 @@ +/** + * 교량받침 표 (2026-11-03, 모델값). Pure: the ELEVATION TABLE and the 솔플레이트 TABLE of a pier, every value from the + * model. No drawing values are used (C안). Lengths in mm; EL.X in m. + * + * - EL.X: the mortar top = the Excel z (padTop, Q16). + * - h1: block height (block top − block bottom). h2: mortar (pad top − block top). h3: bearing body (solplate rule). + * - h4: solplate centre thickness T. E = h1 + h2 + h3 + h4. + * - C1: bearing centre to the outer block edge along 교축. C2: between the two bearings of a pair, along 교축 (on the + * front row). C3: block span of the pair along 교축 (front row). C4·C5: 교각축 margins (bearing-layout). C6: block depth + * along 교각축. + * - S1·S2: block rebar bars from the model (bearing-block): count and bar length. For 일체 the pair's bars sit on the front row. + */ +import type { PierModel } from './geometry'; +import { bearingEdgeDistances, pairEccentricities } from './bearing-layout'; +import { INTEGRATED_BLOCK } from './integrated-block'; +import { bearingBlockRebar, type BarRow } from './rebar/bearing-block'; + +/** Depth of a 분리 bearing block along 교각축 (mm), the same value as the block rebar. */ +export const BLOCK_DEPTH_MM = 1200; + +export interface ElevationRow { + no: number; + row: '전열' | '후열'; + elx: number; + h1: number; + h2: number; + h3: number; + h4: number; + E: number; + C1: number; + /** Front row only; null on the back row. */ + C2: number | null; + C3: number | null; + C4: number; + C5: number; + C6: number; + S1: BarSummary | null; + S2: BarSummary | null; +} + +export interface BarSummary { + count: number; + length: number; +} + +export interface SolplateRow { + no: number; + /** 기울기 S (%), negative as drawn. */ + S: number; + L: number; + W: number; + T: number; + T1: number; + T2: number; + /** Check: T1 − T2 against L·|S|/100 (mm). */ + checkLhs: number; + checkRhs: number; + ok: boolean; +} + +function barSummary(bars: BarRow[], name: 'S1' | 'S2'): BarSummary | null { + const hit = bars.filter((x) => x.name === name); + if (hit.length === 0) return null; + return { count: hit.reduce((sum, x) => sum + x.count, 0), length: hit[0].length }; +} + +export function elevationRows(m: PierModel): ElevationRow[] { + const edges = bearingEdgeDistances(m); + const rebar = bearingBlockRebar(m); + const pairs = pairEccentricities(m.blocks); + return [...m.blocks] + .sort((a, b) => a.no - b.no) + .map((b) => { + const plan = b.plan; + const h1 = Math.round((b.top - b.bottom) * 1000); + const h2 = Math.round((b.padTop - b.top) * 1000); + const edge = edges.find((e) => e.no === b.no)!; + const ib = m.integratedBlocks.find((x) => (b.row === '전열' ? x.frontNo : x.backNo) === b.no); + // C1: bearing centre to the outer edge of its block along 교축 (1200 block centred on the bearing, or the 2500 block). + const C1 = ib ? Math.round(Math.abs(b.n - (b.n <= ib.n ? ib.n - INTEGRATED_BLOCK.length / 2000 : ib.n + INTEGRATED_BLOCK.length / 2000)) * 1000) : 600; + const pair = pairs.find((p) => (b.row === '전열' ? p.frontNo : p.backNo) === b.no); + const front = b.row === '전열'; + const C2 = front && pair ? Math.round(Math.abs(m.blocks.find((x) => x.no === pair.backNo && x.row === '후열')!.n - b.n) * 1000) : null; + const C3 = front && pair ? (ib ? INTEGRATED_BLOCK.length : (C2 ?? 0) + BLOCK_DEPTH_MM) : null; + const bars = rebar.find((r) => r.no === b.no && r.row === b.row)?.bars ?? []; + return { + no: b.no, + row: b.row, + elx: b.padTop, + h1, + h2, + h3: plan.h3, + h4: plan.T, + E: h1 + h2 + plan.h3 + plan.T, + C1, + C2, + C3, + C4: Math.round(edge.leftMm), + C5: Math.round(edge.rightMm), + C6: BLOCK_DEPTH_MM, + S1: barSummary(bars, 'S1'), + S2: barSummary(bars, 'S2'), + }; + }); +} + +export function solplateRows(m: PierModel): SolplateRow[] { + return [...m.blocks] + .sort((a, b) => a.no - b.no) + .map((b) => { + const p = b.plan; + const checkLhs = p.T1 - p.T2; + const checkRhs = (p.L * Math.abs(p.slopePct)) / 100; + return { + no: b.no, + S: p.slopePct, + L: p.L, + W: p.W, + T: p.T, + T1: p.T1, + T2: p.T2, + checkLhs, + checkRhs, + ok: Math.abs(checkLhs - checkRhs) < 1e-6, + }; + }); +} diff --git a/src/main.ts b/src/main.ts index 9356afc..73801e8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -48,6 +48,7 @@ import { footingPlanSvg, footingSideSvg } from './view/footing-svg'; import { P1_COPING_GEOM } from './core/coping-geom'; import { SOLPLATE_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule'; import type { SolplateRule } from './core/types'; +import { type BarSummary, elevationRows, solplateRows } from './core/bearing-table'; /** 지반 모델 (ground_model.json) for the project. Its values reach a pier's 지반고 only when the user applies them. */ let groundModel: GroundModelFile | null = null; @@ -2436,9 +2437,55 @@ function renderBearingRef(box: HTMLDivElement, pier: Pier): void { wrap.appendChild(typeInput); renderBearingLayout(wrap, pier); renderSolplateRule(wrap, pier); + renderElevationTable(wrap, pier); + renderSolplateTable(wrap, pier); box.appendChild(wrap); } +/** 교량받침 ELEVATION TABLE (모델값, mm · EL m, 2026-11-03): one row per bearing, from the model only. */ +function renderElevationTable(wrap: HTMLDivElement, pier: Pier): void { + if (!pier.model) return; + wrap.appendChild(el('p', '교량받침 ELEVATION TABLE (모델값 · mm · EL m)', 'lbl')); + const table = el('table', undefined, 'grid-table rebar-table'); + const head = el('thead'); + const hr = el('tr'); + for (const h of ['No', '열', 'EL.X', 'h1 블록', 'h2 몰탈', 'h3 받침', 'h4 솔플', 'E', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'S1 (본수·길이)', 'S2 (본수·길이)']) hr.appendChild(el('th', h)); + head.appendChild(hr); + table.appendChild(head); + const body = el('tbody'); + const dash = (v: number | null) => (v === null ? '—' : String(v)); + const bars = (b: BarSummary | null) => (b === null ? '—' : `${b.count} · ${b.length}`); + for (const r of elevationRows(pier.model)) { + const tr = el('tr'); + const cells = [String(r.no), r.row, r.elx.toFixed(3), String(r.h1), String(r.h2), String(r.h3), String(r.h4), String(r.E), String(r.C1), dash(r.C2), dash(r.C3), String(r.C4), String(r.C5), String(r.C6), bars(r.S1), bars(r.S2)]; + for (const c of cells) tr.appendChild(el('td', c)); + body.appendChild(tr); + } + table.appendChild(body); + wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table); +} + +/** 솔플레이트 TABLE (모델값, mm, 2026-11-03): S, L, W, T, T1, T2 and the check T1 − T2 ≈ L·|S|/100. */ +function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void { + if (!pier.model) return; + wrap.appendChild(el('p', '솔플레이트 TABLE (모델값 · mm)', 'lbl')); + const table = el('table', undefined, 'grid-table rebar-table'); + const head = el('thead'); + const hr = el('tr'); + for (const h of ['No', 'S (%)', 'L (교직)', 'W (교축)', 'T (중심)', 'T1 (−측)', 'T2 (+측)', '검산 T1 − T2 ≈ L·|S|/100']) hr.appendChild(el('th', h)); + head.appendChild(hr); + table.appendChild(head); + const body = el('tbody'); + for (const r of solplateRows(pier.model)) { + const tr = el('tr'); + const cells = [String(r.no), r.S.toFixed(2), String(r.L), String(r.W), String(r.T), r.T1.toFixed(1), r.T2.toFixed(1), `${r.checkLhs.toFixed(1)} ≈ ${r.checkRhs.toFixed(1)} ${r.ok ? '일치' : '불일치'}`]; + for (const c of cells) tr.appendChild(el('td', c)); + body.appendChild(tr); + } + table.appendChild(body); + wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table); +} + /** Sets one pier's solplate rule (C안, 2026-11-03) and rebuilds that pier. */ function setSolplateRule(name: string, rule: SolplateRule): void { const p = state.piers.find((q) => q.input.name === name); diff --git a/tests/bearing-table.test.ts b/tests/bearing-table.test.ts new file mode 100644 index 0000000..87c20f9 --- /dev/null +++ b/tests/bearing-table.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier } from '../src/core/geometry'; +import { elevationRows, solplateRows } from '../src/core/bearing-table'; +import { loadP1 } from './helpers'; + +const model = buildPier(loadP1()); +const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' }); + +describe('교량받침 ELEVATION TABLE (모델값, 2026-11-03)', () => { + const rows = elevationRows(model); + + it('one row per bearing; EL.X is the Excel z (mortar top)', () => { + expect(rows.length).toBe(10); + expect(rows[0].elx).toBeCloseTo(43.561, 3); + expect(rows[5].elx).toBeCloseTo(43.611, 3); + }); + + it('front block 100 mm, back block 150 mm; mortar 50; bearing 186; solplate 25; E = sum', () => { + expect(rows[0]).toMatchObject({ h1: 100, h2: 50, h3: 186, h4: 25, E: 361 }); + expect(rows[5]).toMatchObject({ h1: 150, h2: 50, h3: 186, h4: 25, E: 411 }); + }); + + it('C1 is 600 for a 분리 bearing; C6 is the 1200 block depth; C4 and C5 are 600 each', () => { + expect(rows.every((r) => r.C1 === 600)).toBe(true); + expect(rows.every((r) => r.C6 === 1200)).toBe(true); + expect(rows[0]).toMatchObject({ C4: 600, C5: 600 }); + }); + + it('C2 and C3 sit on the front row only: C2 is the bearing distance along 교축, C3 = C2 + 1200', () => { + expect(rows[0].C2).toBeGreaterThan(1000); + expect(rows[0].C3).toBe(rows[0].C2! + 1200); + expect(rows[5].C2).toBeNull(); + expect(rows[5].C3).toBeNull(); + }); + + it('S1 and S2 come from the block rebar: 8 bars of 2500 each on a 분리 block', () => { + expect(rows[0].S1).toEqual({ count: 8, length: 2500 }); + expect(rows[0].S2).toEqual({ count: 8, length: 2500 }); + }); + + it('일체: the pair block gives C3 = 2500 on the front row and the bars on the front row only', () => { + const ir = elevationRows(integrated); + expect(ir[0].C3).toBe(2500); + expect(ir[5].S1).toBeNull(); + expect(ir[0].S1).not.toBeNull(); + }); +}); + +describe('솔플레이트 TABLE (모델값, 2026-11-03)', () => { + it('10 rows with S −0.5, L 1000, W 1100, T 25, T1 27.5, T2 22.5; the check holds', () => { + const rows = solplateRows(model); + expect(rows.length).toBe(10); + expect(rows[0]).toMatchObject({ S: -0.5, L: 1000, W: 1100, T: 25, T1: 27.5, T2: 22.5, ok: true }); + expect(rows[0].checkLhs).toBeCloseTo(5, 9); + expect(rows[0].checkRhs).toBeCloseTo(5, 9); + }); +});