Each block takes the skew of its bearing row against the perpendicular of its stretch axis (rowSkewDeg, from the bearing azimuth). The blocks and pads are turned to that skew, and the S1/S2 bars are drawn in the block frame and turned with it, so their lengths and embedments do not change. ② shows the row skew (P1: front −4.42°, back −3.11°). Approved 2026-10-23 with the drawing as reference. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
189 lines
7.3 KiB
TypeScript
189 lines
7.3 KiB
TypeScript
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 stretchMatrix(m: PierModel, k: number): THREE.Matrix4 {
|
||
const st = m.stretches[k];
|
||
const X = new THREE.Vector3(st.u.e, 0, -st.u.n);
|
||
const Y = new THREE.Vector3(0, 1, 0);
|
||
const Z = new THREE.Vector3(st.nrm.e, 0, -st.nrm.n);
|
||
const origin = new THREE.Vector3(st.origin.e - m.pivot.e, 0, -(st.origin.n - m.pivot.n));
|
||
return new THREE.Matrix4().makeBasis(X, Y, Z).setPosition(origin);
|
||
}
|
||
|
||
/**
|
||
* Mitre plane at the bend point. Stretch 0 keeps the side (p − kink)·mitre ≤ 0 and stretch 1 the other side,
|
||
* so each stretch's full-length slab is trimmed to its own half and the two meet at the mitred joint.
|
||
* Three.js keeps fragments with a non-negative distance from the plane, hence the sign flip for stretch 0.
|
||
*/
|
||
/** Mitre plane for one stretch as a list, or undefined on a straight axis. */
|
||
export function planesOf(m: PierModel, k: number): THREE.Plane[] | undefined {
|
||
const plane = mitrePlane(m, k);
|
||
return plane ? [plane] : undefined;
|
||
}
|
||
|
||
/** Straight-frame matrix (stretch 0). */
|
||
export function frameMatrix(m: PierModel): THREE.Matrix4 {
|
||
return stretchMatrix(m, 0);
|
||
}
|
||
|
||
export function mitrePlane(m: PierModel, k: number): THREE.Plane | undefined {
|
||
if (!m.kink || !m.mitre) return undefined;
|
||
const normal = new THREE.Vector3(m.mitre.e, 0, -m.mitre.n).multiplyScalar(k === 0 ? -1 : 1);
|
||
const point = new THREE.Vector3(m.kink.e - m.pivot.e, 0, -(m.kink.n - m.pivot.n));
|
||
return new THREE.Plane().setFromNormalAndCoplanarPoint(normal, point);
|
||
}
|
||
|
||
export interface PierScene {
|
||
root: THREE.Group;
|
||
parts: Record<PartName, THREE.Group>;
|
||
size: number;
|
||
}
|
||
|
||
export function buildPierScene(m: PierModel): PierScene {
|
||
const Ms = m.stretches.map((_, k) => stretchMatrix(m, k));
|
||
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, k = 0, clippingPlanes?: THREE.Plane[]) => {
|
||
geometry.applyMatrix4(Ms[k]);
|
||
const material = new THREE.MeshStandardMaterial({
|
||
color: COLORS[part],
|
||
roughness: 0.75,
|
||
metalness: 0.05,
|
||
transparent: opacity < 1,
|
||
opacity,
|
||
side: THREE.DoubleSide,
|
||
clippingPlanes,
|
||
});
|
||
material.userData.baseOpacity = opacity;
|
||
parts[part].add(new THREE.Mesh(geometry, material));
|
||
};
|
||
|
||
// Coping: each part's elevation outline extruded across its width (no boolean operations), trimmed by the mitre plane.
|
||
for (const part of m.copingParts) {
|
||
const shape = new THREE.Shape(part.profile.map(([s, z]) => new THREE.Vector2(s, z)));
|
||
const coping = new THREE.ExtrudeGeometry(shape, { depth: part.nTo - part.nFrom, bevelEnabled: false });
|
||
coping.translate(0, 0, part.nFrom);
|
||
const plane = mitrePlane(m, part.k);
|
||
add('coping', coping, 1, part.k, plane ? [plane] : undefined);
|
||
}
|
||
|
||
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, 1, c.k);
|
||
}
|
||
|
||
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, 1, f.k, planesOf(m, f.k));
|
||
}
|
||
|
||
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, 1, b.k, planesOf(m, b.k));
|
||
}
|
||
|
||
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, 1, p.k, planesOf(m, p.k));
|
||
}
|
||
|
||
for (const b of m.blocks) {
|
||
// Turn the block so its sides follow the bearing row (rowSkewDeg); the 3D turn is rotateY(−skew).
|
||
const turn = (-b.rowSkewDeg * Math.PI) / 180;
|
||
const h = b.top - b.bottom;
|
||
const g = new THREE.BoxGeometry(1.2, h, 1.2);
|
||
g.rotateY(turn);
|
||
g.translate(b.s, b.bottom + h / 2, b.n);
|
||
add('blocks', g, 1, b.k);
|
||
const pad = new THREE.BoxGeometry(1.0, 0.05, 1.0);
|
||
pad.rotateY(turn);
|
||
pad.translate(b.s, b.padTop - 0.025, b.n);
|
||
add('pads', pad, 1, b.k);
|
||
}
|
||
|
||
// 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 };
|
||
}
|
||
|
||
/** ③ rebar view: members and the ground at half opacity so the bars show through (승인 2026-10-23). */
|
||
const REBAR_VIEW_ALPHA = 0.5;
|
||
|
||
/** Turns the ③ rebar view on or off. Off restores each part's own opacity. Depth writes stop while on, so bars behind a member stay visible. */
|
||
export function setRebarView(scene: PierScene, on: boolean): void {
|
||
for (const group of Object.values(scene.parts)) {
|
||
group.traverse((o) => {
|
||
if (!(o instanceof THREE.Mesh)) return;
|
||
const mat = o.material as THREE.MeshStandardMaterial;
|
||
const base = mat.userData.baseOpacity as number;
|
||
mat.opacity = on ? REBAR_VIEW_ALPHA : base;
|
||
mat.transparent = on || base < 1;
|
||
mat.depthWrite = !on;
|
||
mat.needsUpdate = true;
|
||
});
|
||
}
|
||
}
|
||
|
||
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 };
|
||
}
|