diff --git a/src/core/bearing-ref.ts b/src/core/bearing-ref.ts new file mode 100644 index 0000000..ad4ed44 --- /dev/null +++ b/src/core/bearing-ref.ts @@ -0,0 +1,79 @@ +/** + * 교량받침 reference tables from the P1 상세도 (도면값, 읽기 전용, 2026-10-23). They are not used in the model: + * the model keeps the imported values (Q16). EL.X is the drawing's bearing level; E = h1 + h2 + h3 + h4. + */ +export interface BearingRefRow { + no: number; + row: '전열' | '후열'; + /** EL.X (m). */ + elx: number; + /** 블럭 높이 h1 (mm, 도면). */ + h1: number; + /** 모르타르 h2 (mm). */ + h2: number; + /** 받침 h3 (mm). */ + h3: number; + /** 솔플레이트 h4 (mm). */ + h4: number; + /** E (mm, 도면). */ + E: number; + A: number; + B: number; + C1: number; + C2: number | null; + C3: number | null; + C4: number | null; + C5: number | null; + C6: number | null; + S1: number; +} + +export interface SolplateRefRow { + no: number; + /** 기울기 S (%), negative as drawn. */ + s: number; + L: number; + W: number; + /** 솔플레이트 T (mm). */ + T: number; + T1: number; + T2: number; +} + +const FRONT = { h1: 164, h2: 50, h3: 181, h4: 30, E: 425 }; +const BACK = { h1: 139, h2: 50, h3: 181, h4: 30, E: 400 }; + +export const BEARING_ELEVATION_REF: BearingRefRow[] = [ + { no: 1, row: '전열', elx: 43.625, ...FRONT, A: 700, B: 640, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 }, + { no: 2, row: '전열', elx: 43.697, ...FRONT, A: 700, B: 640, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 }, + { no: 3, row: '전열', elx: 43.768, ...FRONT, A: 700, B: 790, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 }, + { no: 4, row: '전열', elx: 43.840, ...FRONT, A: 700, B: 640, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 }, + { no: 5, row: '전열', elx: 43.912, ...FRONT, A: 700, B: 640, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 }, + { no: 6, row: '후열', elx: 43.600, ...BACK, A: 700, B: 540, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 }, + { no: 7, row: '후열', elx: 43.672, ...BACK, A: 700, B: 540, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 }, + { no: 8, row: '후열', elx: 43.744, ...BACK, A: 700, B: 650, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 }, + { no: 9, row: '후열', elx: 43.816, ...BACK, A: 700, B: 540, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 }, + { no: 10, row: '후열', elx: 43.887, ...BACK, A: 700, B: 540, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 }, +]; + +export const SOLPLATE_REF: SolplateRefRow[] = [ + { no: 1, s: -1.910, L: 800, W: 740, T: 38, T1: 38, T2: 22 }, + { no: 2, s: -1.907, L: 800, W: 740, T: 38, T1: 38, T2: 22 }, + { no: 3, s: -1.905, L: 800, W: 890, T: 38, T1: 38, T2: 22 }, + { no: 4, s: -1.903, L: 800, W: 740, T: 38, T1: 38, T2: 22 }, + { no: 5, s: -1.901, L: 800, W: 740, T: 38, T1: 38, T2: 22 }, + { no: 6, s: -1.910, L: 800, W: 640, T: 38, T1: 38, T2: 22 }, + { no: 7, s: -1.907, L: 800, W: 640, T: 38, T1: 38, T2: 22 }, + { no: 8, s: -1.905, L: 800, W: 750, T: 38, T1: 38, T2: 22 }, + { no: 9, s: -1.903, L: 800, W: 640, T: 38, T1: 38, T2: 22 }, + { no: 10, s: -1.901, L: 800, W: 640, T: 38, T1: 38, T2: 22 }, +]; + +/** 솔플레이트 T of the 공통 규칙 (30 mm). T1 and T2 are computed from this value, not from the table's T column. */ +export const SOLPLATE_T_RULE = 30; + +/** T1 / T2 by the drawing's rule: T ± L·|S|/2, rounded to mm (S in %). */ +export function solplateTs(T: number, L: number, s: number): { T1: number; T2: number } { + const half = (L * Math.abs(s)) / 200; + return { T1: Math.round(T + half), T2: Math.round(T - half) }; +} diff --git a/src/main.ts b/src/main.ts index f3eb15d..030b20d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,7 @@ import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar import { DEFAULT_MATERIAL, FCK_MEMBERS, fyOf, GRADE_CHOICES, isAssumedMaterial, materialFrom, type BendFactors, type FckByMember, type FckMember, type GradeKey, type Material } from './core/rebar/common'; import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from './core/rebar/quantity'; import { P1_COPING_GEOM } from './core/coping-geom'; +import { BEARING_ELEVATION_REF, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs } from './core/bearing-ref'; import { allVisibleChecked, setVisibleChecked, type TypeFilter } from './core/selection'; import { choose, defaultSummary, isSelectable, SHAPE_GROUPS, UNDEFINED_REASON, type MemberId } from './core/shape-options'; import { parseProject, SCHEMA_VERSION, serializeProject, type ProjectFile, type ProjectInfo } from './core/project'; @@ -1246,6 +1247,56 @@ function setMaterial(key: 'fck' | GradeKey, raw: string): void { renderAll(); } +/** ② 교량받침 reference (1단계, 읽기 전용): the drawing's ELEVATION and 솔플레이트 tables, not used in the model (Q16). */ +function renderBearingRef(box: HTMLDivElement, pier: Pier): void { + const m = pier.model; + const wrap = el('div', undefined, 'bearing-ref'); + wrap.appendChild(el('p', '교량받침 ELEVATION TABLE (도면값 · 읽기 전용, mm · EL m)', 'lbl')); + wrap.appendChild(el('p', '도면값은 모델에 쓰지 않습니다(Q16). 아래 "차이"는 도면과 모델의 코핑 상면 차이입니다.', 'muted small')); + const front = m?.segments ?? []; + const backBlocks = m?.blocks.filter((b) => b.row === '후열') ?? []; + const et = el('table', undefined, 'grid-table rebar-table'); + const eh = el('thead'); + const ehr = el('tr'); + for (const h of ['No', '열', 'EL.X', 'E', 'A', 'B', 'C1', 'S1', '도면 상면 (EL.X − E)', '모델 코핑 상면', '차이 (m)']) ehr.appendChild(el('th', h)); + eh.appendChild(ehr); + et.appendChild(eh); + const eb = el('tbody'); + for (const r of BEARING_ELEVATION_REF) { + const drawingTop = r.elx - r.E / 1000; + const modelTop = r.row === '전열' ? front[r.no - 1]?.top : backBlocks[r.no - 6]?.bottom; + const diff = modelTop === undefined ? NaN : modelTop - drawingTop; + eb.appendChild(el('tr')).append( + el('td', String(r.no)), el('td', r.row), el('td', r.elx.toFixed(3)), el('td', String(r.E)), + el('td', String(r.A)), el('td', String(r.B)), el('td', String(r.C1)), el('td', String(r.S1)), + el('td', drawingTop.toFixed(3)), el('td', modelTop === undefined ? '—' : modelTop.toFixed(3)), + el('td', Number.isNaN(diff) ? '—' : diff.toFixed(3)), + ); + } + et.appendChild(eb); + wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(et); + + wrap.appendChild(el('p', '솔플레이트 TABLE (도면값, mm)', 'lbl')); + const st = el('table', undefined, 'grid-table rebar-table'); + const sh = el('thead'); + const shr = el('tr'); + for (const h of ['No', 'S (%)', 'L', 'W', 'T', 'T1', 'T2', '식 검산']) shr.appendChild(el('th', h)); + sh.appendChild(shr); + st.appendChild(sh); + const sb = el('tbody'); + for (const r of SOLPLATE_REF) { + const calc = solplateTs(SOLPLATE_T_RULE, r.L, r.s); + const ok = calc.T1 === r.T1 && calc.T2 === r.T2; + sb.appendChild(el('tr')).append( + el('td', String(r.no)), el('td', r.s.toFixed(3)), el('td', String(r.L)), el('td', String(r.W)), + el('td', String(r.T)), el('td', String(r.T1)), el('td', String(r.T2)), el('td', ok ? '일치' : `식 ${calc.T1} / ${calc.T2}`), + ); + } + st.appendChild(sb); + wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(st); + box.appendChild(wrap); +} + /** ② 코핑 상세 (2단계, 읽기 전용): d_IP2 and the 계단 구간 표 from the model (승인 2026-10-23). */ function renderCopingInfo(box: HTMLDivElement, pier: Pier): void { const m = pier.model; @@ -1321,6 +1372,7 @@ function renderPlan(): void { fieldRows(fields, pier, member.keys); if (member.id === 'footing') renderPileRows(fields, pier); if (member.id === 'coping') renderCopingInfo(fields, pier); + if (member.id === 'bearing') renderBearingRef(fields, pier); if (FCK_MEMBERS.includes(member.id as FckMember)) renderMemberFck(fields, member.id as FckMember); regen.textContent = `다시 생성 · 변경 ${pendingKeys(pier).length}`; regen.disabled = pendingKeys(pier).length === 0; diff --git a/src/style.css b/src/style.css index 36243db..a586984 100644 --- a/src/style.css +++ b/src/style.css @@ -789,3 +789,7 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif; #viewer-block .origin-wrap { align-self: flex-start; } #viewer-block .viewer-stack-right { position: static; width: auto; } } + +/* ② 교량받침 reference tables (읽기 전용) */ +.bearing-ref .table-scroll { overflow-x: auto; margin-bottom: 8px; } +.bearing-ref table { font-size: 12px; } diff --git a/tests/bearing-ref.test.ts b/tests/bearing-ref.test.ts new file mode 100644 index 0000000..841fcf0 --- /dev/null +++ b/tests/bearing-ref.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier } from '../src/core/geometry'; +import { BEARING_ELEVATION_REF, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs } from '../src/core/bearing-ref'; +import { loadP1 } from './helpers'; + +describe('교량받침 reference (도면값, 읽기 전용, 1단계)', () => { + it('E = h1 + h2 + h3 + h4 on every row', () => { + for (const r of BEARING_ELEVATION_REF) expect(r.h1 + r.h2 + r.h3 + r.h4, `No ${r.no}`).toBe(r.E); + }); + + it('solplate L = A + 100 and W = B + 100 against the elevation row', () => { + BEARING_ELEVATION_REF.forEach((e, i) => { + expect(SOLPLATE_REF[i].L, `No ${e.no}`).toBe(e.A + 100); + expect(SOLPLATE_REF[i].W, `No ${e.no}`).toBe(e.B + 100); + }); + }); + + it('T1 and T2 follow T ± L·|S|/2 on every row (the 검산 column)', () => { + for (const r of SOLPLATE_REF) { + const c = solplateTs(SOLPLATE_T_RULE, r.L, r.s); + expect(c.T1, `No ${r.no}`).toBe(r.T1); + expect(c.T2, `No ${r.no}`).toBe(r.T2); + } + }); + + it('the drawing EL.X − E sits a constant 0.211 m below the model coping top on all ten bearings (Q16 diagnostic)', () => { + const m = buildPier(loadP1()); + const back = m.blocks.filter((b) => b.row === '후열'); + const diffs = BEARING_ELEVATION_REF.map((r) => { + const modelTop = r.row === '전열' ? m.segments[r.no - 1].top : back[r.no - 6].bottom; + return modelTop - (r.elx - r.E / 1000); + }); + for (const d of diffs) expect(d).toBeCloseTo(0.211, 2); + }); +});