164 lines
6.8 KiB
TypeScript
164 lines
6.8 KiB
TypeScript
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<typeof buildAxis>, 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 배치');
|
||
});
|
||
});
|