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:
1 parent
d963d8ed3c
commit
ad8e437c99
6 files changed
+727
No files matched your search
@@ -0,0 +1,137 @@
|
||||
import * as THREE from 'three';
|
||||
import type { PierModel } from '../core/geometry';
|
||||
|
||||
export type PartName = 'coping' | 'columns' | 'footings' | 'piles' | 'blinding' | 'blocks' | 'pads' | 'ground';
|
||||
|
||||
export const PART_LABELS: Record<PartName, string> = {
|
||||
coping: '코핑',
|
||||
columns: '기둥',
|
||||
footings: '확대기초',
|
||||
piles: '말뚝',
|
||||
blinding: '버림콘크리트',
|
||||
blocks: '받침블럭',
|
||||
pads: '모르타르 패드',
|
||||
ground: '지반',
|
||||
};
|
||||
|
||||
const COLORS: Record<PartName, number> = {
|
||||
coping: 0xb8c2cc,
|
||||
columns: 0x9aa7b4,
|
||||
footings: 0x7d8a99,
|
||||
piles: 0x5b6878,
|
||||
blinding: 0xe6e2d8,
|
||||
blocks: 0xd6dde5,
|
||||
pads: 0x8898aa,
|
||||
ground: 0x6b8f4e,
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the model's local frame (x = along axis s, y = absolute EL, z = across axis n) to the scene.
|
||||
* The scene uses y as up and places the first column centre at the origin (spec §1).
|
||||
* Basis vectors: X = (u.e, 0, -u.n), Z = (nrm.e, 0, -nrm.n). The determinant is +1, so faces keep their winding.
|
||||
*/
|
||||
export function frameMatrix(m: PierModel): THREE.Matrix4 {
|
||||
const X = new THREE.Vector3(m.u.e, 0, -m.u.n);
|
||||
const Y = new THREE.Vector3(0, 1, 0);
|
||||
const Z = new THREE.Vector3(m.nrm.e, 0, -m.nrm.n);
|
||||
const origin = new THREE.Vector3(m.bp1.e - m.pivot.e, 0, -(m.bp1.n - m.pivot.n));
|
||||
return new THREE.Matrix4().makeBasis(X, Y, Z).setPosition(origin);
|
||||
}
|
||||
|
||||
export interface PierScene {
|
||||
root: THREE.Group;
|
||||
parts: Record<PartName, THREE.Group>;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export function buildPierScene(m: PierModel): PierScene {
|
||||
const M = frameMatrix(m);
|
||||
const parts = {} as Record<PartName, THREE.Group>;
|
||||
for (const key of Object.keys(PART_LABELS) as PartName[]) {
|
||||
parts[key] = new THREE.Group();
|
||||
parts[key].name = key;
|
||||
}
|
||||
|
||||
const add = (part: PartName, geometry: THREE.BufferGeometry, opacity = 1) => {
|
||||
geometry.applyMatrix4(M);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: COLORS[part],
|
||||
roughness: 0.75,
|
||||
metalness: 0.05,
|
||||
transparent: opacity < 1,
|
||||
opacity,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
parts[part].add(new THREE.Mesh(geometry, material));
|
||||
};
|
||||
|
||||
// Coping: the elevation outline extruded across the width (no boolean operations).
|
||||
const shape = new THREE.Shape(m.profile.map(([s, z]) => new THREE.Vector2(s, z)));
|
||||
const coping = new THREE.ExtrudeGeometry(shape, { depth: m.copingB, bevelEnabled: false });
|
||||
coping.translate(0, 0, -m.copingB / 2);
|
||||
add('coping', coping);
|
||||
|
||||
for (const c of m.columns) {
|
||||
const g = new THREE.CylinderGeometry(c.diameter / 2, c.diameter / 2, c.top - c.bottom, 48);
|
||||
g.translate(c.s, (c.top + c.bottom) / 2, 0);
|
||||
add('columns', g);
|
||||
}
|
||||
|
||||
for (const f of m.footings) {
|
||||
const g = new THREE.BoxGeometry(f.length, f.top - f.bottom, f.width);
|
||||
g.translate(f.s, (f.top + f.bottom) / 2, 0);
|
||||
add('footings', g);
|
||||
}
|
||||
|
||||
for (const b of m.blindings) {
|
||||
const g = new THREE.BoxGeometry(b.length, b.thickness, b.width);
|
||||
g.translate(b.s, b.top - b.thickness / 2, 0);
|
||||
add('blinding', g);
|
||||
}
|
||||
|
||||
for (const p of m.piles) {
|
||||
const g = new THREE.CylinderGeometry(p.diameter / 2, p.diameter / 2, p.top - p.bottom, 20);
|
||||
g.translate(p.s, (p.top + p.bottom) / 2, p.n);
|
||||
add('piles', g);
|
||||
}
|
||||
|
||||
for (const b of m.blocks) {
|
||||
const h = b.top - b.bottom;
|
||||
const g = new THREE.BoxGeometry(1.2, h, 1.2);
|
||||
g.translate(b.s, b.bottom + h / 2, b.n);
|
||||
add('blocks', g);
|
||||
const pad = new THREE.BoxGeometry(1.0, 0.05, 1.0);
|
||||
pad.translate(b.s, b.padTop - 0.025, b.n);
|
||||
add('pads', pad);
|
||||
}
|
||||
|
||||
// Ground: a translucent plane at the ground EL, used only to judge soil cover (not a computed input).
|
||||
const ground = new THREE.PlaneGeometry(60, 60);
|
||||
ground.rotateX(-Math.PI / 2);
|
||||
ground.translate(m.copingL / 2, m.groundEl, 0);
|
||||
add('ground', ground, 0.35);
|
||||
|
||||
const root = new THREE.Group();
|
||||
for (const key of Object.keys(parts) as PartName[]) root.add(parts[key]);
|
||||
const box = new THREE.Box3().setFromObject(root);
|
||||
const size = Math.max(...box.getSize(new THREE.Vector3()).toArray());
|
||||
return { root, parts, size };
|
||||
}
|
||||
|
||||
export type ViewKind = 'iso' | 'front' | 'side' | 'top';
|
||||
|
||||
/** Camera position for one of the four view presets, looking at the centre of the model. */
|
||||
export function viewPosition(scene: PierScene, kind: ViewKind, m: PierModel): { position: THREE.Vector3; target: THREE.Vector3 } {
|
||||
const box = new THREE.Box3().setFromObject(scene.root);
|
||||
const target = box.getCenter(new THREE.Vector3());
|
||||
const dist = scene.size * 1.6;
|
||||
const X = new THREE.Vector3(m.u.e, 0, -m.u.n);
|
||||
const Z = new THREE.Vector3(m.nrm.e, 0, -m.nrm.n);
|
||||
const dirs: Record<ViewKind, THREE.Vector3> = {
|
||||
iso: new THREE.Vector3(1, 0.7, 1).normalize(),
|
||||
front: Z.clone().add(new THREE.Vector3(0, 0.15, 0)).normalize(),
|
||||
side: X.clone().add(new THREE.Vector3(0, 0.15, 0)).normalize(),
|
||||
top: new THREE.Vector3(0, 1, 0.001).normalize(),
|
||||
};
|
||||
return { position: target.clone().add(dirs[kind].multiplyScalar(dist)), target };
|
||||
}
|
||||
Reference in new issue
Block a user