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>
100 lines
4.1 KiB
TypeScript
100 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { DIM, solveIp4 } from '../src/core/geometry';
|
||
import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from '../src/core/axis';
|
||
import type { PierInput } from '../src/core/types';
|
||
import { loadP1 } from './helpers';
|
||
|
||
const pt = (p: PierInput['ips'][number]) => ({ e: p.x, n: p.y });
|
||
const ip = (p: PierInput, name: string) => pt(p.ips.find((q) => q.name === name)!);
|
||
|
||
describe('straight axis (regression against spec §12)', () => {
|
||
const p1 = loadP1();
|
||
const axis = buildAxis(ip(p1, 'IP1'), ip(p1, 'IP3'), null);
|
||
const bp1 = copingSpan(axis, p1.copingLengthMm / 1000).sStart;
|
||
|
||
it('bearing s and n relative to BP1 match spec §12', () => {
|
||
const front = p1.bearings.filter((b) => b.row === '전열').sort((a, b) => a.no - b.no);
|
||
const back = p1.bearings.filter((b) => b.row === '후열').sort((a, b) => a.no - b.no);
|
||
const fs = front.map((b) => toAxis(axis, { e: b.x, n: b.y }));
|
||
const bs = back.map((b) => toAxis(axis, { e: b.x, n: b.y }));
|
||
expect(fs[0].s - bp1).toBeCloseTo(0.943, 3);
|
||
expect(fs[fs.length - 1].s - bp1).toBeCloseTo(10.965, 3);
|
||
expect(bs[0].s - bp1).toBeCloseTo(1.029, 3);
|
||
expect(bs[bs.length - 1].s - bp1).toBeCloseTo(11.05, 3);
|
||
expect(fs[0].n).toBeCloseTo(-0.698, 3);
|
||
expect(bs[0].n).toBeCloseTo(0.6, 3);
|
||
});
|
||
|
||
it('straight axis: every point is on stretch 0 and round-trips', () => {
|
||
for (const b of p1.bearings) {
|
||
const p = { e: b.x, n: b.y };
|
||
const q = toAxis(axis, p);
|
||
expect(q.k).toBe(0);
|
||
const back = fromAxis(axis, q);
|
||
expect(back.e).toBeCloseTo(p.e, 9);
|
||
expect(back.n).toBeCloseTo(p.n, 9);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Synthetic bend: IP1 (0, 0), IP2 (1, 0), kink (7, 0), IP3 (9, −3.464), a 60° right turn with IP3 ahead on the turned line.
|
||
// The P1 sheet is straight (IP1, IP2, IP3 collinear), so P1 cannot exercise a bend.
|
||
describe('bent axis IP1 → IP4 → IP3', () => {
|
||
const p1 = loadP1();
|
||
const ip1 = { e: 0, n: 0 };
|
||
const ip2 = { e: 1, n: 0 };
|
||
const ip3 = { e: 9, n: -3.4641016 };
|
||
const kink = solveIp4(ip1, ip2, ip3, 60)!;
|
||
const axis = buildAxis(ip1, ip3, kink);
|
||
const a = kinkArc(axis)!;
|
||
|
||
it('kink lies on the IP1→IP2 line, at s = 7, and the arc length adds the two stretches', () => {
|
||
const cross = (kink.e - ip1.e) * (ip2.n - ip1.n) - (kink.n - ip1.n) * (ip2.e - ip1.e);
|
||
expect(cross).toBeCloseTo(0, 9);
|
||
expect(a).toBeCloseTo(7, 6);
|
||
const len2 = Math.hypot(ip3.e - kink.e, ip3.n - kink.n);
|
||
expect(axis.length).toBeCloseTo(a + len2, 9);
|
||
});
|
||
|
||
it('the kink is the continuity point: s = a on both stretches at n = 0', () => {
|
||
const from0 = fromAxis(axis, { k: 0, s: a, n: 0 });
|
||
const from1 = fromAxis(axis, { k: 1, s: a, n: 0 });
|
||
expect(from0.e).toBeCloseTo(kink.e, 9);
|
||
expect(from0.n).toBeCloseTo(kink.n, 9);
|
||
expect(from1.e).toBeCloseTo(kink.e, 9);
|
||
expect(from1.n).toBeCloseTo(kink.n, 9);
|
||
});
|
||
|
||
it('every bearing keeps its input world coordinates through toAxis → fromAxis', () => {
|
||
for (const b of p1.bearings) {
|
||
const p = { e: b.x, n: b.y };
|
||
const back = fromAxis(axis, toAxis(axis, p));
|
||
expect(back.e).toBeCloseTo(p.e, 9);
|
||
expect(back.n).toBeCloseTo(p.n, 9);
|
||
}
|
||
});
|
||
|
||
it('a point just before the mitre plane is on stretch 0, just after it on stretch 1', () => {
|
||
const u1 = axis.segments[0].u;
|
||
const u2 = axis.segments[1].u;
|
||
const before = { e: kink.e - 0.01 * u1.e, n: kink.n - 0.01 * u1.n };
|
||
const after = { e: kink.e + 0.01 * u2.e, n: kink.n + 0.01 * u2.n };
|
||
expect(toAxis(axis, before).k).toBe(0);
|
||
expect(toAxis(axis, after).k).toBe(1);
|
||
});
|
||
|
||
it('coping centred on the axis: span is L_IP/2 ± L/2 and the kink lies inside it', () => {
|
||
const L = 6;
|
||
const span = copingSpan(axis, L);
|
||
expect(span.sStart).toBeCloseTo(axis.length / 2 - L / 2, 9);
|
||
expect(span.sStart < a && a < span.sEnd).toBe(true);
|
||
});
|
||
|
||
it('columns within the Level half-width of the kink are reported', () => {
|
||
const half = DIM.levelWidth / 2;
|
||
const columns = [a - 0.5, a + 5];
|
||
expect(nearKink(axis, columns, half)).toEqual([a - 0.5]);
|
||
expect(nearKink(axis, [a + 5], half)).toEqual([]);
|
||
});
|
||
});
|