Files
Pier_v1/tests/bent-coping.test.ts
ypshimandClaude Haiku 5.5 9e1e25f8fe feat(core,view): bent coping stage 3 — footings and piles in stretch frames
- Footings, blindings and piles on a bent axis are placed in their stretch's
  frame; 일체기초 is grouped per stretch, so a combined footing never crosses
  the kink (separate footings, each aligned with its stretch).
- Footings, blindings and piles are trimmed at the mitre plane like the coping.
- The stage-2 refusal for bent foundations is removed; the Level-half-width
  refusal for columns at the kink remains.
- Tests: T형 footing crossing the kink stays in stretch 0; 2주형 with columns
  on both sides gives one footing per stretch, with mitre clipping in the scene.

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

164 lines
6.8 KiB
TypeScript
Raw Permalink 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'] = '현장타설말뚝', 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 배치');
});
});