Files
Pier_v1/tests/bent-coping.test.ts
T
ypshimandClaude Haiku 5.5 c9dae4ff9b fix(core): refuse bends that double back; use a valid synthetic bend in tests
The earlier synthetic bend (IP3 at (6, 2)) put IP4 ahead of IP3 on the turned
line, so the axis folded back on itself. buildPier now refuses a kink behind
IP1 along IP1→IP2 or an IP3 behind the kink along the turned direction. Tests
use IP3 (9, −3.464), a valid 60° right turn with kink (7, 0).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-22 09:33:09 +09:00

144 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'] = '현장타설말뚝'): PierInput {
const base = loadP1();
const axis = buildAxis(IP1, IP3, solveIp4(IP1, { e: 1, n: 0 }, IP3, ANGLE));
const sStart = copingSpan(axis, L).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: 'T형',
columnCount: 1,
foundationType,
copingLengthMm: L * 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('bent coping with footings or piles on the axis is refused until stage 3', () => {
expect(() => buildPier(bentInput('말뚝기초'))).toThrow('미실시');
expect(() => buildPier(bentInput('직접기초'))).toThrow('미실시');
});
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 배치');
});
});