feat(bearing): 솔플레이트 규칙을 교각별 입력으로 바꾸고 도면 불러오기 제거 (C안)

- 받침 기준 카드, 도면 불러오기(xlsx·csv·dxf), 도면 참조 표·오버레이 제거
- 교각마다 솔플레이트 규칙 입력: 받침높이 H(솔플레이트 포함) 211, 교직 L 1000,
  교축 W 1100, 종단 기울기 S −0.5 % (기본값, 가정 표시)
- 중심 두께 T = ⌈22 + L·|S|/200⌉ 로 계산해 얇은 쪽 T2가 22 mm 이상이 되게 함
  (기본값 T 25, T2 22.5, h3 = H − T = 186)
- geometry·scene·SVG는 교각 규칙에서 만든 같은 솔플레이트 계획을 모든 받침에 사용

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-03 11:07:51 +09:00
1 parent f25e36c050
commit 36b26209df
23 files changed
+257 -1081

No files matched your search

-35
View File
@@ -1,35 +0,0 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { BEARING_ELEVATION_REF, DRAWING_TOL_MM, drawingCopingCheck, withDrawingBasis, type DrawingBearing } from '../src/core/bearing-ref';
import { loadP1 } from './helpers';
const drawing: DrawingBearing[] = BEARING_ELEVATION_REF.map((r) => ({ no: r.no, elx: r.elx, h1: r.h1, h2: r.h2 }));
describe('받침 기준: 도면 EL.X (Q16 option 2, 승인 2026-10-23)', () => {
it('the default basis (엑셀 z) keeps the model as before: the drawing check still reports the EL.X gap', () => {
const excel = buildPier(loadP1());
expect(excel.blocks.filter((b) => b.row === '전열')[0].padTop).toBeCloseTo(43.561, 3);
});
it('with the drawing basis the pads sit at the drawing EL.X and the coping tops stay within the 5 mm tolerance', () => {
const m = buildPier(withDrawingBasis(loadP1(), drawing));
expect(m.blocks.filter((b) => b.row === '전열')[0].padTop).toBeCloseTo(43.625, 3);
for (const c of drawingCopingCheck(m, drawing)) {
expect(Math.abs(c.diffMm), `No ${c.no}`).toBeLessThanOrEqual(DRAWING_TOL_MM);
}
});
it('with the drawing basis the front block (h1) is 164 mm and the back block 139 mm, excluding the 50 mm pad', () => {
const m = buildPier(withDrawingBasis(loadP1(), drawing));
const front = m.blocks.filter((b) => b.row === '전열')[0];
const back = m.blocks.filter((b) => b.row === '후열')[0];
expect(Math.round((front.top - front.bottom) * 1000)).toBe(164);
expect(Math.round((back.top - back.bottom) * 1000)).toBe(139);
});
it('a drawing row that is off by more than 5 mm is flagged', () => {
const bad = drawing.map((d) => (d.no === 3 ? { ...d, elx: d.elx + 0.02 } : d));
const c = drawingCopingCheck(buildPier(loadP1()), bad).find((x) => x.no === 3)!;
expect(c.ok).toBe(false);
});
});
+2 -6
View File
@@ -18,12 +18,8 @@ describe('교량받침 상세 평면 (승인 2026-10-26)', () => {
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('draws the solplate of each bearing from the pier plan (L 1000 across, W 1100 along)', () => {
expect((bearingPlanSvg(separate, 0).match(/width="1000" height="1100"/g) ?? []).length).toBe(2);
});
it('an unknown pair index gives an empty drawing', () => {
-68
View File
@@ -1,68 +0,0 @@
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 solplate tapers along the axis: T1 (−측) + T2 (+측) = 2·T, with T = 30 at the centre', () => {
for (const r of SOLPLATE_REF) {
expect(r.T, `No ${r.no}`).toBe(SOLPLATE_T_RULE);
expect(r.T1 + r.T2, `No ${r.no}`).toBe(2 * r.T);
expect(r.T1, `No ${r.no}`).toBeGreaterThan(r.T2);
}
});
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 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);
});
});
});
import { applyBearingRules, BEARING_RULES_DEFAULT } from '../src/core/bearing-ref';
describe('교량받침 공통 규칙 (2단계)', () => {
it('with the drawing values, the rules give back the drawing E, L, W, T1, T2 on every bearing', () => {
BEARING_ELEVATION_REF.forEach((r, i) => {
const v = applyBearingRules(r, BEARING_RULES_DEFAULT);
expect(v.E, `No ${r.no}`).toBe(r.E);
expect(v.L, `No ${r.no}`).toBe(SOLPLATE_REF[i].L);
expect(v.W, `No ${r.no}`).toBe(SOLPLATE_REF[i].W);
expect(v.T1, `No ${r.no}`).toBe(SOLPLATE_REF[i].T1);
expect(v.T2, `No ${r.no}`).toBe(SOLPLATE_REF[i].T2);
});
});
it('a 10 mm larger mortar h2 raises E by 10 and leaves L, W and T1, T2 unchanged', () => {
const r = BEARING_ELEVATION_REF[0];
const v = applyBearingRules(r, { ...BEARING_RULES_DEFAULT, h2: BEARING_RULES_DEFAULT.h2 + 10 });
expect(v.E).toBe(r.E + 10);
expect(v.L).toBe(r.A + 100);
expect(v.T1).toBe(SOLPLATE_REF[0].T1);
});
});
+8 -17
View File
@@ -6,21 +6,14 @@ import { loadP1 } from './helpers';
const model = buildPier(loadP1());
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' });
const ref = { h1: 150, h2: 50, h3: 181, h4: 30, E: 425, A: 700, B: 640 };
describe('교량받침 상세 단면 (승인 2026-10-26)', () => {
describe('교량받침 상세 단면 (승인 2026-10-26, C안 2026-11-03)', () => {
it('labels EL.X at the mortar top and the block height h1 for each bearing', () => {
const svg = bearingSectionSvg(model, 0);
expect(svg).toContain('EL.X');
expect(svg).toContain('h1 150');
});
it('draws the drawing heights dashed only when given', () => {
expect(bearingSectionSvg(model, 0)).not.toContain('stroke-dasharray="40 25"');
const svg = bearingSectionSvg(model, 0, { front: ref, back: ref });
expect((svg.match(/stroke-dasharray="40 25"/g) ?? []).length).toBeGreaterThan(0);
});
it('일체 draws one stepped outline (a polygon) for the pair', () => {
expect(bearingSectionSvg(integrated, 0)).toContain('<polygon');
});
@@ -30,15 +23,13 @@ describe('교량받침 상세 단면 (승인 2026-10-26)', () => {
});
});
describe('솔플레이트 상세 (승인 2026-10-26)', () => {
it('shows L, W, T1 and T2 of the model plan, with the drawing dashed when given', () => {
const withPlan = buildPier({ ...loadP1(), bearingPlans: [{ no: 1, A: 700, B: 640, S: -1.9 }] });
const block = withPlan.blocks.find((b) => b.no === 1)!;
describe('솔플레이트 상세 (승인 2026-10-26, C안 2026-11-03)', () => {
it('shows L, W, T1 and T2 of the pier plan', () => {
const block = model.blocks.find((b) => b.no === 1)!;
const svg = solplateDetailSvg(block);
expect(svg).toContain('L ');
expect(svg).toContain('T1 ');
expect(svg).toContain('T2 ');
const withRef = solplateDetailSvg(block, { L: 800, W: 748, T1: 38, T2: 22, S: 1.9 });
expect(withRef).toContain('stroke-dasharray="40 25"');
expect(svg).toContain('L 1000');
expect(svg).toContain('W 1100');
expect(svg).toContain('T1 27.5');
expect(svg).toContain('T2 22.5');
});
});
-32
View File
@@ -1,32 +0,0 @@
import { describe, expect, it } from 'vitest';
import { parseDrawingRows } from '../src/core/drawing-import';
const header = ['No', '열', 'EL.X (m)', 'h1 (mm)', 'h2 (mm)'];
describe('도면 불러오기 (승인 2026-10-23)', () => {
it('reads No, EL.X, h1, h2 from a sheet with a header and ignores other columns', () => {
const rows = [['도면 받침', '', '', '', ''], header, [1, '전열', 43.625, 164, 50], [2, '전열', 43.697, 164, 50]];
const r = parseDrawingRows(rows);
expect(r.errors).toEqual([]);
expect(r.rows).toEqual([
{ no: 1, elx: 43.625, h1: 164, h2: 50 },
{ no: 2, elx: 43.697, h1: 164, h2: 50 },
]);
});
it('accepts the header spelled as "EL. X" and in a different order', () => {
const rows = [['h2', 'No', 'h1', 'EL. X'], [50, 1, 164, 43.625]];
const r = parseDrawingRows(rows);
expect(r.errors).toEqual([]);
expect(r.rows[0]).toEqual({ no: 1, elx: 43.625, h1: 164, h2: 50 });
});
it('reports a missing header, a duplicate No, and an EL.X given in mm', () => {
expect(parseDrawingRows([['x', 'y']]).errors[0]).toContain('머리글');
const dup = parseDrawingRows([header, [1, '', 43.6, 164, 50], [1, '', 43.7, 164, 50]]);
expect(dup.errors.some((e) => e.includes('중복'))).toBe(true);
const mm = parseDrawingRows([header, [1, '', 43625, 164, 50]]);
expect(mm.errors.some((e) => e.includes('m 단위'))).toBe(true);
expect(mm.rows).toEqual([]);
});
});
-70
View File
@@ -1,70 +0,0 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { BEARING_ELEVATION_REF } from '../src/core/bearing-ref';
import { parseBearingElevationTable, readDxfTexts, type DxfText } from '../src/core/dxf-table';
const texts = JSON.parse(readFileSync(new URL('./fixtures/bearing-table-texts.json', import.meta.url), 'utf-8')) as DxfText[];
describe('도면 받침 ELEVATION TABLE 읽기 (DXF 텍스트, 승인 2026-10-23)', () => {
it('reads all ten bearings from the 운곡교 교각도(2) table', () => {
const r = parseBearingElevationTable(texts);
expect(r.errors).toEqual([]);
expect(r.drawing.map((d) => d.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
it('the values equal the reference table used for the 도면 비교 (EL.X, h1, h2 on every bearing)', () => {
const r = parseBearingElevationTable(texts);
for (const ref of BEARING_ELEVATION_REF) {
const got = r.drawing.find((d) => d.no === ref.no)!;
expect(got.elx, `No ${ref.no} EL.X`).toBeCloseTo(ref.elx, 3);
expect(got.h1, `No ${ref.no} h1`).toBe(ref.h1);
expect(got.h2, `No ${ref.no} h2`).toBe(ref.h2);
}
});
it('a leader-line "EL. X" label placed before the table does not replace the table header', () => {
const distractor = { x: 11102.8, y: 11521.7, text: 'EL. "X"' };
const r = parseBearingElevationTable([distractor, ...texts]);
expect(r.errors).toEqual([]);
expect(r.drawing.map((d) => d.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
it('a stray "NO" label elsewhere on the drawing does not move the No column', () => {
const r = parseBearingElevationTable([{ x: 100, y: 100, text: 'NO.' }, ...texts]);
expect(r.errors).toEqual([]);
expect(r.drawing.map((d) => d.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
it('reads TEXT entities from DXF text (x, y, string)', () => {
const dxf = ['0', 'SECTION', '2', 'ENTITIES', '0', 'TEXT', '10', '100.5', '20', '200.25', '1', 'EL. "X"', '0', 'ENDSEC'].join('\n');
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('a stray "S(%)" label elsewhere on the drawing does not replace the solplate header', () => {
const r = parseSolplateTable([{ x: 16000, y: 16200, text: 'S(%)' }, ...sp]);
expect(r.errors).toEqual([]);
expect(r.rows.length).toBe(10);
});
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);
}
});
});
+2 -2
View File
@@ -29,11 +29,11 @@ describe('3D frame (view/scene.ts)', () => {
expect(mapped.distanceTo(expected)).toBeLessThan(1e-6);
});
it('bounding box spans pile tip (2.208 m) to the highest pad top (back bearing 5, 43.898 m = sheet E41)', () => {
it('bounding box spans pile tip (2.208 m) to the highest solplate top (back bearing 5: pad top 43.898 m + h3 0.186 m + T1 0.0275 m)', () => {
const scene = buildPierScene(model);
const box = new THREE.Box3().setFromObject(scene.root);
expect(box.min.y).toBeCloseTo(2.208, 2);
expect(box.max.y).toBeCloseTo(43.898, 2);
expect(box.max.y).toBeCloseTo(43.898 + 0.186 + 0.0275, 3);
});
it('builds the expected part counts for P1', () => {
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { SOLPLATE_RULE_DEFAULT, solplateCenterT, solplatePlan, solplateRuleIssues } from '../src/core/solplate-rule';
describe('솔플레이트 규칙 (C안, 2026-11-03)', () => {
it('default 211 · 1000 × 1100 · −0.5 %: T = 25, T1 = 27.5, T2 = 22.5, h3 = 211 − 25 = 186', () => {
const p = solplatePlan(SOLPLATE_RULE_DEFAULT);
expect(p.T).toBe(25);
expect(p.T1).toBeCloseTo(27.5, 9);
expect(p.T2).toBeCloseTo(22.5, 9);
expect(p.h3).toBe(186);
expect(p.L).toBe(1000);
expect(p.W).toBe(1100);
});
it('the centre thickness is the smallest whole mm that keeps T2 at 22 mm or more', () => {
for (const [L, S] of [[1000, -0.5], [1100, -0.5], [1000, -2], [800, -1.91], [1200, -3]] as const) {
const T = solplateCenterT(L, S);
expect(T - (L * Math.abs(S)) / 200).toBeGreaterThanOrEqual(22);
expect(solplateCenterT(L, S) - 1 - (L * Math.abs(S)) / 200).toBeLessThan(22);
}
});
it('T1 + T2 = 2T for any rule', () => {
const p = solplatePlan({ heightMm: 220, lengthMm: 900, widthMm: 800, slopePct: -1.2 });
expect(p.T1 + p.T2).toBeCloseTo(2 * p.T, 9);
});
it('the default rule has no input issues', () => {
expect(solplateRuleIssues(SOLPLATE_RULE_DEFAULT)).toEqual([]);
});
it('reports a bearing height that does not exceed the solplate centre thickness', () => {
expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, heightMm: 25 })).toHaveLength(1);
});
it('reports non-positive plan sizes and a non-numeric slope', () => {
expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, lengthMm: 0 }).length).toBeGreaterThan(0);
expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, widthMm: -1 }).length).toBeGreaterThan(0);
expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, slopePct: NaN }).length).toBeGreaterThan(0);
});
});
+17 -23
View File
@@ -2,44 +2,38 @@ 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);
describe('솔플레이트 모델과 3D (C안, 2026-11-03)', () => {
it('every pier has a solplate plan from its rule; the default gives T 25 and h3 186', () => {
const m = buildPier(loadP1());
expect(m.blocks.every((b) => b.plan.T === 25 && b.plan.h3 === 186)).toBe(true);
});
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 } });
it('a pier rule changes its own plan: 받침높이 211 and slope −1 % give T 27 and h3 184', () => {
const m = buildPier({ ...loadP1(), solplate: { heightMm: 211, lengthMm: 1000, widthMm: 1100, slopePct: -1 } });
expect(m.blocks[0].plan.T).toBe(27);
expect(m.blocks[0].plan.h3).toBe(184);
});
it('the 3D scene has one solplate wedge per bearing, and its highest point is the thick end of the tallest one', () => {
const m = buildPier(loadP1());
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));
const top = Math.max(...m.blocks.map((b) => solplateGeometry(b, b.plan).bottom + solplateGeometry(b, b.plan).T1));
expect(box.max.y).toBeCloseTo(top, 3);
});
it('without plans the scene has an empty solplate part', () => {
it('the 교량받침 본체 is one box per bearing: the plan less 100 mm on each side, height h3 (186 mm)', () => {
const scene = buildPierScene(buildPier(loadP1()));
expect(scene.parts.solplates.children.length).toBe(0);
});
it('the 교량받침 본체 is one box per bearing: the solplate plan less 100 mm on each side, height h3 (181 mm)', () => {
const m = buildPier({ ...loadP1(), bearingPlans: plans, solplate: { T: 30, margin: 100, h3: 181 } });
const scene = buildPierScene(m);
const bodies = scene.parts.bearings.children;
expect(bodies.length).toBe(10);
const one = new THREE.Box3().setFromObject(bodies[0]);
expect(one.max.y - one.min.y).toBeCloseTo(0.181, 3);
// Body 1: L = 800 − 200 = 600 across the 교각축 직각, W = 740 − 200 = 540 along the 교축; the row skew turns it a few degrees.
expect(Math.max(one.max.x - one.min.x, one.max.z - one.min.z)).toBeGreaterThan(0.54);
expect(one.max.y - one.min.y).toBeCloseTo(0.186, 3);
// Body: L − 200 = 800 across the 교각축 직각, W − 200 = 900 along the 교축; the row skew turns it a few degrees.
expect(Math.max(one.max.x - one.min.x, one.max.z - one.min.z)).toBeGreaterThan(0.85);
});
});
+19 -18
View File
@@ -1,41 +1,42 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { solplateGeometry } from '../src/core/solplate';
import { SOLPLATE_RULE_DEFAULT, solplatePlan } from '../src/core/solplate-rule';
import { loadP1 } from './helpers';
const model = buildPier(loadP1());
const front1 = model.blocks.filter((b) => b.row === '전열')[0];
const params = { A: 700, B: 640, margin: 100, T: 30, slopePct: -1.91 };
const plan = solplatePlan(SOLPLATE_RULE_DEFAULT);
describe('솔플레이트 기하 (1단계, 승인 2026-10-23)', () => {
it('L = A + 100 and W = B + 100 along the 교축 and the 교각축 직각', () => {
const g = solplateGeometry(front1, 181, params);
expect(g.L).toBeCloseTo(0.8, 6);
expect(g.W).toBeCloseTo(0.74, 6);
describe('솔플레이트 기하 (C안, 2026-11-03)', () => {
it('L = 교직 1000 across the 교각축 직각 (n) and W = 교축 1100 along the 교축 (s)', () => {
const g = solplateGeometry(front1, plan);
expect(g.L).toBeCloseTo(1.0, 6);
expect(g.W).toBeCloseTo(1.1, 6);
});
it('the thick end T1 is on the 시점 side (−s): T1 ≈ 38, T2 ≈ 22 mm, and T1 + T2 = 2T', () => {
const g = solplateGeometry(front1, 181, params);
expect(g.T1 * 1000).toBeCloseTo(37.64, 1);
expect(g.T2 * 1000).toBeCloseTo(22.36, 1);
expect((g.T1 + g.T2) * 1000).toBeCloseTo(60, 6);
// The top at the 시점 corners is the thick side.
it('the thick end T1 is on the (−) side: T1 = 27.5 mm, T2 = 22.5 mm, and T1 + T2 = 2T', () => {
const g = solplateGeometry(front1, plan);
expect(g.T1 * 1000).toBeCloseTo(27.5, 6);
expect(g.T2 * 1000).toBeCloseTo(22.5, 6);
expect((g.T1 + g.T2) * 1000).toBeCloseTo(50, 6);
// The top at the (−) corners is the thick side.
const topAtStart = g.corners.filter((c) => Math.abs(c[1] - (g.bottom + g.T1)) < 1e-9);
expect(topAtStart.length).toBe(2);
});
it('the underside is the bearing top: block top (EL.X) + h3', () => {
const g = solplateGeometry(front1, 181, params);
expect(g.bottom).toBeCloseTo(front1.padTop + 0.181, 6);
it('the underside is the bearing top: block top (EL.X) + h3 (186 mm)', () => {
const g = solplateGeometry(front1, plan);
expect(g.bottom).toBeCloseTo(front1.padTop + 0.186, 6);
});
it('volume = L·W·T (the taper averages to the centre thickness)', () => {
const g = solplateGeometry(front1, 181, params);
expect(g.volume).toBeCloseTo(g.L * g.W * 0.03, 9);
const g = solplateGeometry(front1, plan);
expect(g.volume).toBeCloseTo(g.L * g.W * 0.025, 9);
});
it('the solplate is centred on the block and follows the row direction', () => {
const g = solplateGeometry(front1, 181, params);
const g = solplateGeometry(front1, plan);
const c = g.corners.reduce((acc, p) => [acc[0] + p[0] / 8, acc[1] + p[2] / 8] as [number, number], [0, 0]);
expect(c[0]).toBeCloseTo(front1.s, 6);
expect(c[1]).toBeCloseTo(front1.n, 6);