diff --git a/src/core/bearing-ref.ts b/src/core/bearing-ref.ts index ca1f92f..1850034 100644 --- a/src/core/bearing-ref.ts +++ b/src/core/bearing-ref.ts @@ -114,6 +114,10 @@ export interface DrawingBearing { elx: number; h1: number; h2: number; + /** 받침 평면 A, B (mm) and solplate slope S (%), when read from the drawing (승인 2026-10-23). */ + A?: number; + B?: number; + S?: number; } /** Tolerance for the coping top check against the drawing (승인 2026-10-23: 5 mm). */ diff --git a/src/core/dxf-table.ts b/src/core/dxf-table.ts index 17c3f15..9e0e8dc 100644 --- a/src/core/dxf-table.ts +++ b/src/core/dxf-table.ts @@ -96,7 +96,49 @@ export function parseBearingElevationTable(texts: DxfText[]): BearingTableResult errors.push(`No ${r.no}: EL.X·h1·h2 중 값이 없습니다.`); continue; } - drawing.push({ no: r.no, elx, h1, h2 }); + drawing.push({ no: r.no, elx, h1, h2, A: r.values.a, B: r.values.b }); } return { rows, drawing: drawing.sort((a, b) => a.no - b.no), errors }; } + +export interface SolplateTableRow { + no: number; + /** 기울기 S (%), negative as drawn. */ + S: number; + L: number; + W: number; + /** 중심 두께 T, and the 교각축 직각 (−) and (+) side thicknesses T1, T2 (mm). */ + T: number; + T1: number; + T2: number; +} + +/** + * 솔플레이트 TABLE (승인 2026-10-23): the header row has NO, S(%), L, W, T, T1, T2; each bearing row gives the values under + * their headers, as in the bearing ELEVATION table. + */ +export function parseSolplateTable(texts: DxfText[]): { rows: SolplateTableRow[]; errors: string[] } { + const sHeader = texts.find((t) => norm(t.text) === 's(%)' || norm(t.text) === 's%'); + if (!sHeader) return { rows: [], errors: ['솔플레이트 TABLE의 S(%) 머리글을 찾지 못했습니다.'] }; + const headerY = sHeader.y; + const headerRow = texts.filter((t) => Math.abs(t.y - headerY) <= 60); + const key = (name: string) => headerRow.find((t) => norm(t.text) === name); + const cols = { S: key('s(%)') ?? key('s%'), L: key('l'), W: key('w'), T: key('t'), T1: key('t1'), T2: key('t2') }; + const noHeader = headerRow.find((t) => norm(t.text) === 'no'); + if (!cols.L || !cols.W || !cols.T || !cols.T1 || !cols.T2 || !noHeader) { + return { rows: [], errors: ['솔플레이트 TABLE의 머리글(NO, L, W, T, T1, T2)이 모두 있어야 합니다.'] }; + } + const noCells = texts.filter((t) => NUMBER.test(t.text) && Number.isInteger(Number(t.text)) && Math.abs(t.x - noHeader.x) <= 250 && t.y < headerY - 60); + const at = (col: DxfText, y: number) => { + const hit = texts.find((t) => Math.abs(t.x - col.x) <= 150 && Math.abs(t.y - y) <= 120 && NUMBER.test(t.text)); + return hit ? Number(hit.text) : NaN; + }; + const rows: SolplateTableRow[] = []; + const errors: string[] = []; + for (const cell of noCells.sort((a, b) => b.y - a.y)) { + const r = { no: Number(cell.text), S: cols.S ? at(cols.S, cell.y) : NaN, L: at(cols.L, cell.y), W: at(cols.W, cell.y), T: at(cols.T, cell.y), T1: at(cols.T1, cell.y), T2: at(cols.T2, cell.y) }; + if (Object.values(r).some((v) => Number.isNaN(v))) errors.push(`No ${r.no}: 솔플레이트 값이 빠졌습니다.`); + else rows.push(r); + } + return { rows: rows.sort((a, b) => a.no - b.no), errors }; +} diff --git a/src/core/geometry.ts b/src/core/geometry.ts index a3f21e7..50367f3 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -69,6 +69,8 @@ export interface BlockModel { padTop: number; /** Skew (deg, −90…90] of the bearing row against the perpendicular of the stretch axis; the block sides follow the row. */ rowSkewDeg: number; + /** Solplate plan of this bearing (A, B, S, T, margin, h3), when the input gives it. */ + plan?: { A: number; B: number; S: number; T: number; margin: number; h3: number }; } /** Skew of a bearing row (deg) from the bearing azimuth and the stretch axis direction u (see {@link BlockModel.rowSkewDeg}). */ @@ -400,13 +402,19 @@ export function buildPier(input: PierInput): PierModel { const blocks: BlockModel[] = []; const bearingAz = new Map(input.bearings.map((r) => [r.no, r.azimuthDeg])); const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u); + // Solplate plans from the input (승인 2026-10-23); the 공통 규칙 default is T 30, margin 100, h3 181. + const rules = input.solplate ?? { T: 30, margin: 100, h3: 181 }; + const planOf = (no: number) => { + const p = input.bearingPlans?.find((q) => q.no === no); + return p ? { A: p.A, B: p.B, S: p.S, T: rules.T, margin: rules.margin, h3: rules.h3 } : undefined; + }; frontPlaced.forEach((b) => { const bottom = segmentTop(segments, b.g); - blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k) }); + blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan: planOf(b.no) }); }); backPlaced.forEach((b) => { const bottom = b.z - h2; - blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k) }); + blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan: planOf(b.no) }); }); const blockHeightFront = blocks.find((b) => b.row === '전열')!.top - blocks.find((b) => b.row === '전열')!.bottom; diff --git a/src/core/types.ts b/src/core/types.ts index 144f1de..88a3cd5 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -40,6 +40,10 @@ export interface PierInput { pileRows?: number[]; ips: IpPoint[]; bearings: Bearing[]; + /** 솔플레이트 plan per bearing (승인 2026-10-23): A, B (mm) and the slope S (%). Absent = no solplate. */ + bearingPlans?: { no: number; A: number; B: number; S: number }[]; + /** 공통 규칙 for the solplate: centre thickness T, 솔플 여유 margin (mm), bearing height h3 (mm). */ + solplate?: { T: number; margin: number; h3: number }; } export type IssueLevel = 'E' | 'W' | 'I'; diff --git a/src/main.ts b/src/main.ts index 16819d7..37db753 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +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 { parseDrawingRows } from './core/drawing-import'; -import { parseBearingElevationTable, readDxfTexts } from './core/dxf-table'; +import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table'; import { P1_COPING_GEOM } from './core/coping-geom'; import { applyBearingRules, BEARING_ELEVATION_REF, BEARING_RULES_DEFAULT, type BearingBasis, type BearingRules, type DrawingBearing, DRAWING_TOL_MM, drawingCopingCheck, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs, withDrawingBasis } from './core/bearing-ref'; @@ -46,6 +46,7 @@ type Stage = 'shape' | 'plan' | 'rebar' | 'quantity'; const PART_GROUPS: { label: string; parts: PartName[] }[] = [ { label: '받침·받침블럭', parts: ['blocks', 'pads'] }, + { label: '솔플레이트', parts: ['solplates'] }, { label: '코핑', parts: ['coping'] }, { label: '기둥', parts: ['columns'] }, { label: '확대기초', parts: ['footings'] }, @@ -207,7 +208,12 @@ function buildEntry(rawInput: PierInput, checked: boolean): Pier { let model: PierModel | null = null; // The model uses the drawing EL.X when the project basis is 'drawing' and this pier has drawing values (Q16). const drawing = bearingBasis === 'drawing' ? bearingDrawing[input.name] : undefined; - const modelInput = drawing?.length ? withDrawingBasis(input, drawing) : input; + const baseInput = drawing?.length ? withDrawingBasis(input, drawing) : input; + // Solplate plans come from the drawing rows that have A, B and S, whatever the 받침 기준 (승인 2026-10-23). + const plans = (bearingDrawing[input.name] ?? []) + .filter((d) => d.A !== undefined && d.B !== undefined && d.S !== undefined) + .map((d) => ({ no: d.no, A: d.A as number, B: d.B as number, S: d.S as number })); + const modelInput: PierInput = { ...baseInput, bearingPlans: plans, solplate: { T: bearingRules.T, margin: bearingRules.margin, h3: bearingRules.h3 } }; if (validateInput(input, issues)) { try { model = buildPier(modelInput); @@ -1351,12 +1357,17 @@ function renderBearingBasis(wrap: HTMLDivElement, pier: Pier): void { // A DXF drawing: the 교량받침 ELEVATION TABLE is read by position. Otherwise a sheet with No, EL.X, h1, h2. let drawingRows: DrawingBearing[]; if (file.name.toLowerCase().endsWith('.dxf')) { - const r = parseBearingElevationTable(readDxfTexts(await file.text())); + const texts = readDxfTexts(await file.text()); + const r = parseBearingElevationTable(texts); if (r.errors.length) { setError(`도면 불러오기 실패: ${r.errors.slice(0, 3).join(' ')}`); return; } - drawingRows = r.drawing; + // The solplate table gives S per bearing (승인 2026-10-23); a missing S is reported, not guessed. + const sp = parseSolplateTable(texts); + const sByNo = new Map(sp.rows.map((x) => [x.no, x.S])); + drawingRows = r.drawing.map((d) => ({ ...d, S: sByNo.get(d.no) })); + if (sp.errors.length) setStatus(`솔플레이트 값 확인: ${sp.errors.slice(0, 2).join(' ')}`); } else { const wb = XLSX.read(await file.arrayBuffer(), { type: 'array' }); const sheet = wb.Sheets[wb.SheetNames[0]]; @@ -1421,6 +1432,7 @@ function renderBearingRef(box: HTMLDivElement, pier: Pier): void { return; } bearingRules = { ...bearingRules, [key]: v }; + regenerateAllPiers(); renderAll(); }); grid.append(lab, input, el('span', `기본 ${BEARING_RULES_DEFAULT[key]}`, 'muted small')); diff --git a/src/view/scene.ts b/src/view/scene.ts index 640a2bf..0db5909 100644 --- a/src/view/scene.ts +++ b/src/view/scene.ts @@ -1,7 +1,8 @@ import * as THREE from 'three'; import type { PierModel } from '../core/geometry'; +import { solplateGeometry } from '../core/solplate'; -export type PartName = 'coping' | 'columns' | 'footings' | 'piles' | 'blinding' | 'blocks' | 'pads' | 'ground'; +export type PartName = 'coping' | 'columns' | 'footings' | 'piles' | 'blinding' | 'blocks' | 'pads' | 'solplates' | 'ground'; export const PART_LABELS: Record = { coping: '코핑', @@ -11,6 +12,7 @@ export const PART_LABELS: Record = { blinding: '버림콘크리트', blocks: '받침블럭', pads: '모르타르 패드', + solplates: '솔플레이트', ground: '지반', }; @@ -22,6 +24,7 @@ const COLORS: Record = { blinding: 0xe6e2d8, blocks: 0xd6dde5, pads: 0x8898aa, + solplates: 0x6f7c8a, ground: 0x6b8f4e, }; @@ -136,6 +139,11 @@ export function buildPierScene(m: PierModel): PierScene { pad.rotateY(turn); pad.translate(b.s, b.padTop - 0.025, b.n); add('pads', pad, 1, b.k); + if (b.plan) { + // Solplate: a wedge that tapers along the 교축 (T1 on the 시점 side), on the bearing (승인 2026-10-23). + const sp = solplateGeometry(b, b.plan.h3, { A: b.plan.A, B: b.plan.B, margin: b.plan.margin, T: b.plan.T, slopePct: b.plan.S }); + add('solplates', wedgeGeometry(sp.corners), 1, b.k); + } } // Ground: a translucent plane at the ground EL, used only to judge soil cover (not a computed input). @@ -186,3 +194,26 @@ export function viewPosition(scene: PierScene, kind: ViewKind, m: PierModel): { }; return { position: target.clone().add(dirs[kind].multiplyScalar(dist)), target }; } + +/** + * A wedge from the eight corners of solplateGeometry: corner index (ai, ci, t) = (ai·2 + ci)·2 + t, with ai the 교축 side + * (0 = 시점), ci the 교각축 직각 side and t 0 = underside, 1 = top. Each face is a quad split into two triangles. + */ +function wedgeGeometry(corners: [number, number, number][]): THREE.BufferGeometry { + const idx = (ai: number, ci: number, t: number) => (ai * 2 + ci) * 2 + t; + const quads = [ + [idx(0, 0, 0), idx(0, 1, 0), idx(1, 1, 0), idx(1, 0, 0)], + [idx(0, 0, 1), idx(1, 0, 1), idx(1, 1, 1), idx(0, 1, 1)], + [idx(0, 0, 0), idx(0, 1, 0), idx(0, 1, 1), idx(0, 0, 1)], + [idx(1, 0, 0), idx(1, 0, 1), idx(1, 1, 1), idx(1, 1, 0)], + [idx(0, 0, 0), idx(0, 0, 1), idx(1, 0, 1), idx(1, 0, 0)], + [idx(0, 1, 0), idx(1, 1, 0), idx(1, 1, 1), idx(0, 1, 1)], + ]; + const index: number[] = []; + for (const [q0, q1, q2, q3] of quads) index.push(q0, q1, q2, q0, q2, q3); + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.Float32BufferAttribute(corners.flat(), 3)); + geo.setIndex(index); + geo.computeVertexNormals(); + return geo; +} diff --git a/tests/bearing-ref.test.ts b/tests/bearing-ref.test.ts index 743ad20..8d400f8 100644 --- a/tests/bearing-ref.test.ts +++ b/tests/bearing-ref.test.ts @@ -31,14 +31,16 @@ describe('교량받침 reference (도면값, 읽기 전용, 1단계)', () => { } }); - it('EL.X = block top incl. the 50 mm mortar: coping top = EL.X − h1 − h2 matches the model on all ten bearings (within 2 mm)', () => { + it('Excel z is EL.X (option 1): the model keeps z; the drawing EL.X minus Excel z is +64 mm (전열) and −11 mm (후열)', () => { 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.h1 + r.h2) / 1000); + const front = m.blocks.filter((b) => b.row === '전열'); + BEARING_ELEVATION_REF.forEach((r) => { + const z = r.row === '전열' ? front[r.no - 1].padTop : back[r.no - 6].padTop; + const diffMm = (r.elx - z) * 1000; // 도면 − 엑셀 + // About +64 mm on the front row and −11 mm on the back row (varies by a millimetre or two per bearing). + expect(Math.abs(diffMm - (r.row === '전열' ? 64 : -11)), `No ${r.no}`).toBeLessThan(2); }); - for (const d of diffs) expect(Math.abs(d)).toBeLessThan(0.002); }); }); diff --git a/tests/dxf-table.test.ts b/tests/dxf-table.test.ts index 3770708..d232afc 100644 --- a/tests/dxf-table.test.ts +++ b/tests/dxf-table.test.ts @@ -40,3 +40,25 @@ describe('도면 받침 ELEVATION TABLE 읽기 (DXF 텍스트, 승인 2026-10-23 expect(readDxfTexts(dxf)).toEqual([{ x: 100.5, y: 200.25, text: 'EL. "X"' }]); }); }); + +import { parseSolplateTable } from '../src/core/dxf-table'; +import { SOLPLATE_REF } from '../src/core/bearing-ref'; + +describe('솔플레이트 TABLE 읽기 (승인 2026-10-23)', () => { + const sp = JSON.parse(readFileSync(new URL('./fixtures/solplate-table-texts.json', import.meta.url), 'utf-8')) as DxfText[]; + + it('reads S, L, W, T, T1 and T2 for all ten bearings, and they equal the reference values', () => { + const r = parseSolplateTable(sp); + expect(r.errors).toEqual([]); + expect(r.rows.map((x) => x.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + for (const ref of SOLPLATE_REF) { + const got = r.rows.find((x) => x.no === ref.no)!; + expect(got.S, `No ${ref.no} S`).toBeCloseTo(ref.s, 3); + expect(got.L, `No ${ref.no} L`).toBe(ref.L); + expect(got.W, `No ${ref.no} W`).toBe(ref.W); + expect(got.T, `No ${ref.no} T`).toBe(30); + expect(got.T1, `No ${ref.no} T1`).toBe(38); + expect(got.T2, `No ${ref.no} T2`).toBe(22); + } + }); +}); diff --git a/tests/fixtures/solplate-table-texts.json b/tests/fixtures/solplate-table-texts.json new file mode 100644 index 0000000..fb7ab9d --- /dev/null +++ b/tests/fixtures/solplate-table-texts.json @@ -0,0 +1,407 @@ +[ +{ +"x": 6995.713250393052, +"y": 6754.627068450442, +"text": "교량받침 연단거리 검토" +}, +{ +"x": 2481.474417619672, +"y": 12500.89064770541, +"text": "NO." +}, +{ +"x": 3480.342593385318, +"y": 12500.89064770541, +"text": "S(%)" +}, +{ +"x": 4683.73806608838, +"y": 12500.89064770541, +"text": "L" +}, +{ +"x": 5583.73806608838, +"y": 12500.89064770541, +"text": "W" +}, +{ +"x": 6483.73806608838, +"y": 12500.89064770541, +"text": "T" +}, +{ +"x": 7332.606241854026, +"y": 12500.89064770541, +"text": "T1" +}, +{ +"x": 8232.606241854026, +"y": 12500.89064770541, +"text": "T2" +}, +{ +"x": 2583.73806608838, +"y": 12020.89064770541, +"text": "1" +}, +{ +"x": 3378.078944916609, +"y": 12020.89064770541, +"text": "-1.910" +}, +{ +"x": 4581.474417619672, +"y": 12020.89064770541, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 12020.89064770541, +"text": "740" +}, +{ +"x": 6432.606241854026, +"y": 12020.89064770541, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 12020.89064770541, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 12020.89064770541, +"text": "22" +}, +{ +"x": 2583.73806608838, +"y": 11540.89064770541, +"text": "2" +}, +{ +"x": 3378.078944916609, +"y": 11540.89064770541, +"text": "-1.907" +}, +{ +"x": 4581.474417619672, +"y": 11540.89064770541, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 11540.89064770541, +"text": "740" +}, +{ +"x": 6432.606241854026, +"y": 11540.89064770541, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 11540.89064770541, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 11540.89064770541, +"text": "22" +}, +{ +"x": 2583.73806608838, +"y": 11060.89064770541, +"text": "3" +}, +{ +"x": 3378.078944916609, +"y": 11060.89064770541, +"text": "-1.905" +}, +{ +"x": 4581.474417619672, +"y": 11060.89064770541, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 11060.89064770541, +"text": "890" +}, +{ +"x": 6432.606241854026, +"y": 11060.89064770541, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 11060.89064770541, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 11060.89064770541, +"text": "22" +}, +{ +"x": 2583.73806608838, +"y": 10580.89064770541, +"text": "4" +}, +{ +"x": 3378.078944916609, +"y": 10580.89064770541, +"text": "-1.903" +}, +{ +"x": 4581.474417619672, +"y": 10580.89064770541, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 10580.89064770541, +"text": "740" +}, +{ +"x": 6432.606241854026, +"y": 10580.89064770541, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 10580.89064770541, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 10580.89064770541, +"text": "22" +}, +{ +"x": 2583.73806608838, +"y": 10100.89064770541, +"text": "5" +}, +{ +"x": 3378.078944916609, +"y": 10100.89064770541, +"text": "-1.901" +}, +{ +"x": 4581.474417619672, +"y": 10100.89064770541, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 10100.89064770541, +"text": "740" +}, +{ +"x": 6432.606241854026, +"y": 10100.89064770541, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 10100.89064770541, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 10100.89064770541, +"text": "22" +}, +{ +"x": 2583.73806608838, +"y": 9620.890647705412, +"text": "6" +}, +{ +"x": 3378.078944916609, +"y": 9620.890647705412, +"text": "-1.910" +}, +{ +"x": 4581.474417619672, +"y": 9620.890647705412, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 9620.890647705412, +"text": "640" +}, +{ +"x": 6432.606241854026, +"y": 9620.890647705412, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 9620.890647705412, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 9620.890647705412, +"text": "22" +}, +{ +"x": 2583.73806608838, +"y": 9140.890647705412, +"text": "7" +}, +{ +"x": 3378.078944916609, +"y": 9140.890647705412, +"text": "-1.907" +}, +{ +"x": 4581.474417619672, +"y": 9140.890647705412, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 9140.890647705412, +"text": "640" +}, +{ +"x": 6432.606241854026, +"y": 9140.890647705412, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 9140.890647705412, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 9140.890647705412, +"text": "22" +}, +{ +"x": 2583.73806608838, +"y": 8660.890647705412, +"text": "8" +}, +{ +"x": 3378.078944916609, +"y": 8660.890647705412, +"text": "-1.905" +}, +{ +"x": 4581.474417619672, +"y": 8660.890647705412, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 8660.890647705412, +"text": "750" +}, +{ +"x": 6432.606241854026, +"y": 8660.890647705412, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 8660.890647705412, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 8660.890647705412, +"text": "22" +}, +{ +"x": 2583.73806608838, +"y": 8180.890647705412, +"text": "9" +}, +{ +"x": 3378.078944916609, +"y": 8180.890647705412, +"text": "-1.903" +}, +{ +"x": 4581.474417619672, +"y": 8180.890647705412, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 8180.890647705412, +"text": "640" +}, +{ +"x": 6432.606241854026, +"y": 8180.890647705412, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 8180.890647705412, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 8180.890647705412, +"text": "22" +}, +{ +"x": 2532.606241854026, +"y": 7700.890647705412, +"text": "10" +}, +{ +"x": 3378.078944916609, +"y": 7700.890647705412, +"text": "-1.901" +}, +{ +"x": 4581.474417619672, +"y": 7700.890647705412, +"text": "800" +}, +{ +"x": 5481.474417619672, +"y": 7700.890647705412, +"text": "640" +}, +{ +"x": 6432.606241854026, +"y": 7700.890647705412, +"text": "30" +}, +{ +"x": 7332.606241854026, +"y": 7700.890647705412, +"text": "38" +}, +{ +"x": 8232.606241854026, +"y": 7700.890647705412, +"text": "22" +}, +{ +"x": 4052.185026156749, +"y": 5992.968147136969, +"text": "구 분" +}, +{ +"x": 3694.26225651627, +"y": 5512.968147136969, +"text": "필요연단길이" +}, +{ +"x": 7809.175705251289, +"y": 5512.968147136969, +"text": "s = 200 + 5.0L = 200 + 5.0 X 50.2 = 451.2mm" +} +] \ No newline at end of file diff --git a/tests/solplate-scene.test.ts b/tests/solplate-scene.test.ts new file mode 100644 index 0000000..42bdd41 --- /dev/null +++ b/tests/solplate-scene.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import * as THREE from 'three'; +import { buildPier } from '../src/core/geometry'; +import { solplateGeometry } from '../src/core/solplate'; +import { BEARING_ELEVATION_REF, SOLPLATE_REF } from '../src/core/bearing-ref'; +import { buildPierScene } from '../src/view/scene'; +import { loadP1 } from './helpers'; + +const plans = SOLPLATE_REF.map((s, i) => ({ no: s.no, A: BEARING_ELEVATION_REF[i].A, B: BEARING_ELEVATION_REF[i].B, S: s.s })); + +describe('솔플레이트 모델과 3D (2단계, 승인 2026-10-23)', () => { + it('without plans there is no solplate; with plans every bearing has one', () => { + expect(buildPier(loadP1()).blocks.every((b) => b.plan === undefined)).toBe(true); + const m = buildPier({ ...loadP1(), bearingPlans: plans, solplate: { T: 30, margin: 100, h3: 181 } }); + expect(m.blocks.every((b) => b.plan !== undefined)).toBe(true); + expect(m.blocks[0].plan!.A).toBe(700); + }); + + it('the 3D scene has one solplate wedge per bearing, and its height is the bearing top plus T', () => { + const m = buildPier({ ...loadP1(), bearingPlans: plans, solplate: { T: 30, margin: 100, h3: 181 } }); + const scene = buildPierScene(m); + const wedges = scene.parts.solplates.children; + expect(wedges.length).toBe(10); + const box = new THREE.Box3().setFromObject(scene.parts.solplates); + // The highest point is the thick (시점) end of the tallest wedge: underside (block top + h3) + T1. + const top = Math.max(...m.blocks.map((b) => solplateGeometry(b, 181, { A: b.plan!.A, B: b.plan!.B, margin: 100, T: 30, slopePct: b.plan!.S }).bottom + solplateGeometry(b, 181, { A: b.plan!.A, B: b.plan!.B, margin: 100, T: 30, slopePct: b.plan!.S }).T1)); + expect(box.max.y).toBeCloseTo(top, 3); + }); + + it('without plans the scene has an empty solplate part', () => { + const scene = buildPierScene(buildPier(loadP1())); + expect(scene.parts.solplates.children.length).toBe(0); + }); +});