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 = { coping: '코핑', columns: '기둥', footings: '확대기초', piles: '말뚝', blinding: '버림콘크리트', blocks: '받침블럭', pads: '모르타르 패드', ground: '지반', }; const COLORS: Record = { 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; size: number; } export function buildPierScene(m: PierModel): PierScene { const Ms = m.stretches.map((_, k) => stretchMatrix(m, k)); const parts = {} as Record; 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 = { 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 }; }