feat(core,view): bent coping stage 2 — mitred coping, stretch-local bearings, blocks and columns

- buildPier builds on buildAxis: each object carries its stretch k and a local
  s from that stretch's base; coping parts are per stretch with the mitre plane
  trimming the overlap in the scene (renderer.localClippingEnabled).
- Straight P1 output is unchanged (regression tests pass).
- Bent coping is supported for 현장타설말뚝 only; footings and piles on a bent
  axis remain 미실시 until stage 3. Kink outside the coping and columns within
  the Level half-width of the kink are refused.
- W03/W04 use coping coordinates (g).
- Tests: constructed 60° bend — world coordinates of bearings, blocks and the
  column survive the stretch frames; mitre sides; scene mesh count; refusals.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 09:22:57 +09:00
1 parent 46709be532
commit 5371932cca
6 files changed
+295 -78

No files matched your search

+33 -13
View File
@@ -30,14 +30,32 @@ const COLORS: Record<PartName, number> = {
* 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);
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(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));
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.
*/
/** 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>;
@@ -45,15 +63,15 @@ export interface PierScene {
}
export function buildPierScene(m: PierModel): PierScene {
const M = frameMatrix(m);
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) => {
geometry.applyMatrix4(M);
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,
@@ -61,22 +79,24 @@ export function buildPierScene(m: PierModel): PierScene {
transparent: opacity < 1,
opacity,
side: THREE.DoubleSide,
clippingPlanes,
});
parts[part].add(new THREE.Mesh(geometry, material));
};
// Coping: each part's elevation outline extruded across its width (no boolean operations).
// 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);
add('coping', coping);
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);
add('columns', g, 1, c.k);
}
for (const f of m.footings) {
@@ -94,17 +114,17 @@ export function buildPierScene(m: PierModel): PierScene {
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);
add('piles', g, 1, p.k);
}
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);
add('blocks', g, 1, b.k);
const pad = new THREE.BoxGeometry(1.0, 0.05, 1.0);
pad.translate(b.s, b.padTop - 0.025, b.n);
add('pads', pad);
add('pads', pad, 1, b.k);
}
// Ground: a translucent plane at the ground EL, used only to judge soil cover (not a computed input).