The solplate's L (A + margin) runs across the 교각축 직각 and its W (B + margin) along the 교축; the top tapers across L with T1 at the (−) side (T1 = T + L·|S|/200). The 교량받침 본체 is one box per bearing: the solplate plan less 100 mm on each side, from the mortar top (EL.X) up h3 = 181 mm, turned with the bearing row. It is shown with the bearing group. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
227 lines
9.3 KiB
TypeScript
227 lines
9.3 KiB
TypeScript
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' | 'bearings' | 'solplates' | 'ground';
|
||
|
||
export const PART_LABELS: Record<PartName, string> = {
|
||
coping: '코핑',
|
||
columns: '기둥',
|
||
footings: '확대기초',
|
||
piles: '말뚝',
|
||
blinding: '버림콘크리트',
|
||
blocks: '받침블럭',
|
||
pads: '모르타르 패드',
|
||
bearings: '교량받침 본체',
|
||
solplates: '솔플레이트',
|
||
ground: '지반',
|
||
};
|
||
|
||
const COLORS: Record<PartName, number> = {
|
||
coping: 0xb8c2cc,
|
||
columns: 0x9aa7b4,
|
||
footings: 0x7d8a99,
|
||
piles: 0x5b6878,
|
||
blinding: 0xe6e2d8,
|
||
blocks: 0xd6dde5,
|
||
pads: 0x8898aa,
|
||
bearings: 0x5b6676,
|
||
solplates: 0x6f7c8a,
|
||
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);
|
||
if (b.plan) {
|
||
// 교량받침 본체 (승인 2026-10-23): the solplate plan less 100 mm on each side, from the mortar top up to the solplate.
|
||
const body = new THREE.BoxGeometry((b.plan.B + b.plan.margin) / 1000 - 0.2, b.plan.h3 / 1000, (b.plan.A + b.plan.margin) / 1000 - 0.2);
|
||
body.rotateY(turn);
|
||
body.translate(b.s, b.padTop + b.plan.h3 / 2000, b.n);
|
||
add('bearings', body, 1, b.k);
|
||
// Solplate: a wedge that tapers across the 교각축 직각 (T1 at 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).
|
||
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 };
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|