Files
Pier_v1/tests/bent-coping.test.ts
T
ypshimandClaude Haiku 5.5 5371932cca 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>
2026-10-22 09:22:57 +09:00

136 lines
5.4 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), IP3 (6, 2), 60° right turn. The kink sits at arc ≈ 7.15 m,
// inside a 6 m coping centred on the axis, and a T형 column (arc ≈ 4.73) stays clear of it.
const IP1 = { e: 0, n: 0 };
const IP3 = { e: 6, n: 2 };
const ANGLE = 60;
const L = 6;
const B = 3.1;
const FRONT_N = -0.7;
const BACK_N = 0.6;
const GS = [0.8, 1.9, 3.0, 4.1, 5.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('꺾임점이 코핑 범위 밖');
});
});