import { describe, expect, it } from 'vitest'; import * as THREE from 'three'; import { buildPier, solveIp4, type PierModel } from '../src/core/geometry'; import { buildAxis, copingSpan, fromAxis, kinkArc } from '../src/core/axis'; import { buildPierScene, mitrePlane } from '../src/view/scene'; import type { PierInput } from '../src/core/types'; import { loadP1 } from './helpers'; // Constructed bend: IP1 (0, 0), IP2 (1, 0), 60° right turn, kink (7, 0), IP3 (9, −3.464) four metres ahead on the turned line. // Axis length 11 m; a 6 m coping centred on it has its kink at g = 4.5, and the T형 column (g = 3) stays 1.5 m clear of it. const IP1 = { e: 0, n: 0 }; const IP3 = { e: 9, n: -3.4641016 }; const ANGLE = 60; const L = 6; const B = 3.1; const FRONT_N = -0.7; const BACK_N = 0.6; const GS = [0.6, 1.9, 3.2]; function bentInput(foundationType: PierInput['foundationType'] = '현장타설말뚝', type: PierInput['type'] = 'T형', coping = L): PierInput { const base = loadP1(); const axis = buildAxis(IP1, IP3, solveIp4(IP1, { e: 1, n: 0 }, IP3, ANGLE)); const sStart = copingSpan(axis, coping).sStart; const z = base.copingTopEl + 0.15; const bearingsAt = (row: '전열' | '후열', n: number) => GS.map((g, i) => { const w = world(axis, sStart + g, n); return { no: i + 1, row, x: w.e, y: w.n, z, azimuthDeg: 0 }; }); return { ...base, type, columnCount: type === '2주형' ? 2 : 1, foundationType, copingLengthMm: coping * 1000, copingWidthMm: B * 1000, ips: [ { name: 'IP1', x: IP1.e, y: IP1.n, angleDeg: 0 }, { name: 'IP2', x: 1, y: 0, angleDeg: 0 }, { name: 'IP3', x: IP3.e, y: IP3.n, angleDeg: 0 }, { name: 'IP4', x: 0, y: 0, angleDeg: ANGLE }, ], bearings: [...bearingsAt('전열', FRONT_N), ...bearingsAt('후열', BACK_N)], }; } function world(axis: ReturnType, arc: number, n: number) { const k = kinkArc(axis) !== null && arc > kinkArc(axis)! ? 1 : 0; return fromAxis(axis, { k, s: arc, n }); } /** World point of a model object: stretch origin + s·u + n·nrm (inverse of the scene placement). */ function modelWorld(m: PierModel, k: number, s: number, n: number) { const st = m.stretches[k]; return { e: st.origin.e + s * st.u.e + n * st.nrm.e, n: st.origin.n + s * st.u.n + n * st.nrm.n }; } const toScene = (m: PierModel, p: { e: number; n: number }) => new THREE.Vector3(p.e - m.pivot.e, 0, -(p.n - m.pivot.n)); describe('bent coping model (stage 2)', () => { const input = bentInput(); const m = buildPier(input); const axis = buildAxis(IP1, IP3, solveIp4(IP1, { e: 1, n: 0 }, IP3, ANGLE)); it('two stretches split at the kink, inside the coping', () => { expect(m.stretches).toHaveLength(2); expect(m.kink).not.toBeNull(); const a = kinkArc(axis)!; const kinkG = a - copingSpan(axis, L).sStart; expect(m.stretches[1].base).toBeCloseTo(kinkG, 9); expect(kinkG).toBeGreaterThan(0); expect(kinkG).toBeLessThan(L); }); it('coping has one part per stretch (no lateral step in this input)', () => { expect(m.copingParts.map((p) => p.k)).toEqual([0, 1]); }); it('every bearing and block keeps its input world coordinates through its stretch frame', () => { for (const b of m.bearings) { const want = input.bearings.find((x) => x.row === b.row && x.no === b.no)!; const got = modelWorld(m, b.k, b.s, b.n); expect(got.e).toBeCloseTo(want.x, 6); expect(got.n).toBeCloseTo(want.y, 6); } for (const blk of m.blocks) { const want = input.bearings.find((x) => x.row === blk.row && x.no === blk.no)!; const got = modelWorld(m, blk.k, blk.s, blk.n); expect(got.e).toBeCloseTo(want.x, 6); expect(got.n).toBeCloseTo(want.y, 6); } }); it('the column sits on the centre line in stretch 0 and keeps clear of the kink', () => { const c = m.columns[0]; expect(c.k).toBe(0); const world = modelWorld(m, c.k, c.s, 0); const arc = copingSpan(axis, L).sStart + L / 2; const expected = fromAxis(axis, { k: 0, s: arc, n: 0 }); expect(world.e).toBeCloseTo(expected.e, 6); expect(world.n).toBeCloseTo(expected.n, 6); }); it('mitre plane trims stretch 0 before the kink and keeps stretch 1 after it', () => { const a = kinkArc(axis)!; const plane0 = mitrePlane(m, 0)!; const plane1 = mitrePlane(m, 1)!; const before = toScene(m, fromAxis(axis, { k: 0, s: a - 0.5, n: 0 })); const after = toScene(m, fromAxis(axis, { k: 0, s: a + 0.5, n: 0 })); expect(plane0.distanceToPoint(before)).toBeGreaterThan(0); expect(plane0.distanceToPoint(after)).toBeLessThan(0); expect(plane1.distanceToPoint(after)).toBeGreaterThan(0); expect(plane1.distanceToPoint(before)).toBeLessThan(0); }); it('scene builds one coping mesh per part, with mitre clipping on each', () => { const scene = buildPierScene(m); const coping = scene.parts.coping.children as THREE.Mesh[]; expect(coping).toHaveLength(2); for (const mesh of coping) { const mat = mesh.material as THREE.MeshStandardMaterial; expect(mat.clippingPlanes?.length ?? 0).toBe(1); } }); it('footings are aligned with their stretch: a T형 footing crossing the kink stays in stretch 0 and is trimmed there', () => { const piled = buildPier(bentInput('말뚝기초')); expect(piled.footings).toHaveLength(1); expect(piled.footings[0].k).toBe(0); expect(piled.footings[0].length).toBeCloseTo(10.4, 6); expect(piled.blindings[0].k).toBe(0); expect(piled.piles).toHaveLength(20); expect(piled.piles.every((p) => p.k === 0)).toBe(true); expect(buildPier(bentInput('직접기초')).piles).toHaveLength(0); }); it('2주형 bent with columns on both sides of the kink: separate footings, one per stretch', () => { const m = buildPier(bentInput('말뚝기초', '2주형', 10)); expect(m.footings.map((f) => f.k)).toEqual([0, 1]); expect(m.footings.every((f) => Math.abs(f.length - 10.4) < 1e-9)).toBe(true); expect(m.piles).toHaveLength(40); expect(m.piles.filter((p) => p.k === 1)).toHaveLength(20); const scene = buildPierScene(m); const footings = scene.parts.footings.children as THREE.Mesh[]; expect(footings).toHaveLength(2); for (const mesh of footings) { expect(((mesh.material as THREE.MeshStandardMaterial).clippingPlanes ?? []).length).toBe(1); } }); it('a kink outside the coping is refused', () => { const bad = { ...input, copingLengthMm: 2000 }; expect(() => buildPier(bad)).toThrow('꺾임점이 코핑 범위 밖'); }); }); describe('bend geometry must move forward', () => { it('IP3 behind the kink on the turned line is refused', () => { const base = bentInput(); const ips = base.ips.map((p) => (p.name === 'IP3' ? { ...p, x: 6, y: 2 } : p)); expect(() => buildPier({ ...base, ips })).toThrow('꺾임각과 IP 배치'); }); });