feat(view): add 3D pier viewer and P1 sample page

- three.js scene in the pier frame (pivot at first column centre)
- coping as extruded elevation outline; columns, footings, piles, blinding, bearing blocks and pads
- four view presets, per-part visibility toggles
- workbook load (sample or file), result table, issue list and undetermined defaults
- frame orientation test and bounding-box test

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-21 17:28:02 +09:00
1 parent d963d8ed3c
commit ad8e437c99
6 files changed
+727

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
import * as THREE from 'three';
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { buildPierScene, frameMatrix } from '../src/view/scene';
import { loadP1 } from './helpers';
const model = buildPier(loadP1());
const M = frameMatrix(model);
/** Scene coordinates of a national-grid point at EL 0 (x = E − pivot E, z = −(N − pivot N)). */
function sceneOf(e: number, n: number, el: number): THREE.Vector3 {
return new THREE.Vector3(e - model.pivot.e, el, -(n - model.pivot.n));
}
describe('3D frame (view/scene.ts)', () => {
it('is a proper rotation (determinant +1) so faces keep their winding', () => {
expect(M.determinant()).toBeCloseTo(1, 9);
});
it('maps local (along-axis s, across n, EL) to the national-grid position', () => {
// Block 1 on the front row: its centre in grid coordinates must match the local-to-grid formula.
const b = model.blocks[0];
const grid = {
e: model.bp1.e + model.u.e * b.s + model.nrm.e * b.n,
n: model.bp1.n + model.u.n * b.s + model.nrm.n * b.n,
};
const mapped = new THREE.Vector3(b.s, b.bottom, b.n).applyMatrix4(M);
const expected = sceneOf(grid.e, grid.n, b.bottom);
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)', () => {
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);
});
it('builds the expected part counts for P1', () => {
const scene = buildPierScene(model);
const count = (key: keyof typeof scene.parts) => scene.parts[key].children.length;
expect(count('coping')).toBe(1);
expect(count('columns')).toBe(1);
expect(count('footings')).toBe(1);
expect(count('piles')).toBe(20);
expect(count('blocks')).toBe(10);
expect(count('pads')).toBe(10);
expect(count('blinding')).toBe(1);
expect(count('ground')).toBe(1);
});
});