feat(bearing): 교량받침 ELEVATION TABLE과 솔플레이트 TABLE을 모델값으로 추가

- 모델값만 사용 (도면값 없음). 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 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-03 11:22:45 +09:00
1 parent 6cbda0e126
commit b1b2ad1ddb
3 files changed
+231

No files matched your search

+127
View File
@@ -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,
};
});
}
+47
View File
@@ -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);
+57
View File
@@ -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);
});
});