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>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 09:33:09 +09:00
1 parent 5371932cca
commit c9dae4ff9b
3 files changed
+29 -9

No files matched your search

+12
View File
@@ -203,6 +203,18 @@ export function buildPier(input: PierInput): PierModel {
const ip3v: Vec2 = { e: ip3.x, n: ip3.y };
const kink = bent ? solveIp4(ip1v, ip2v, ip3v, ip4.angleDeg) : null;
const axis = buildAxis(ip1v, ip3v, kink);
if (kink) {
// The bend must move the axis forward: the kink ahead of IP1 along IP1→IP2, and IP3 ahead of the kink along the turned direction.
const d1 = { e: ip2v.e - ip1v.e, n: ip2v.n - ip1v.n };
const len1 = Math.hypot(d1.e, d1.n);
const rad = (-ip4.angleDeg * Math.PI) / 180;
const d2 = { e: (d1.e * Math.cos(rad) - d1.n * Math.sin(rad)) / len1, n: (d1.e * Math.sin(rad) + d1.n * Math.cos(rad)) / len1 };
const kinkAhead = (kink.e - ip1v.e) * d1.e + (kink.n - ip1v.n) * d1.n > 0;
const ip3Ahead = (ip3v.e - kink.e) * d2.e + (ip3v.n - kink.n) * d2.n > 0;
if (!kinkAhead || !ip3Ahead) {
throw new Error('꺾임각과 IP 배치가 맞지 않습니다. IP4가 축 뒤로 넘어갑니다. IP3이 꺾인 방향의 앞쪽에 있는지 확인하세요.');
}
}
const lengthIp = axis.length;
const sStart = copingSpan(axis, L).sStart;
const arcA = kinkArc(axis);
+5 -5
View File
@@ -37,21 +37,21 @@ describe('straight axis (regression against spec §12)', () => {
});
});
// Synthetic bend: IP1 (0, 0), IP2 (1, 0), IP3 (6, 2), 60° right turn. The P1 sheet is straight (IP1, IP2, IP3 collinear),
// so P1 cannot exercise a bend. The kink falls at about s = 7.15 m, inside a 6 m coping centred on the axis.
// 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: 6, n: 2 };
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.15, and the arc length adds the two stretches', () => {
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.15, 1);
expect(a).toBeCloseTo(7, 6);
const len2 = Math.hypot(ip3.e - kink.e, ip3.n - kink.n);
expect(axis.length).toBeCloseTo(a + len2, 9);
});
+12 -4
View File
@@ -6,16 +6,16 @@ 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.
// 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: 6, n: 2 };
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.8, 1.9, 3.0, 4.1, 5.2];
const GS = [0.6, 1.9, 3.2];
function bentInput(foundationType: PierInput['foundationType'] = '현장타설말뚝'): PierInput {
const base = loadP1();
@@ -133,3 +133,11 @@ describe('bent coping model (stage 2)', () => {
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 배치');
});
});