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 ip3v: Vec2 = { e: ip3.x, n: ip3.y };
const kink = bent ? solveIp4(ip1v, ip2v, ip3v, ip4.angleDeg) : null; const kink = bent ? solveIp4(ip1v, ip2v, ip3v, ip4.angleDeg) : null;
const axis = buildAxis(ip1v, ip3v, kink); 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 lengthIp = axis.length;
const sStart = copingSpan(axis, L).sStart; const sStart = copingSpan(axis, L).sStart;
const arcA = kinkArc(axis); 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), // 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.
// so P1 cannot exercise a bend. The kink falls at about s = 7.15 m, inside a 6 m coping centred on the axis. // The P1 sheet is straight (IP1, IP2, IP3 collinear), so P1 cannot exercise a bend.
describe('bent axis IP1 → IP4 → IP3', () => { describe('bent axis IP1 → IP4 → IP3', () => {
const p1 = loadP1(); const p1 = loadP1();
const ip1 = { e: 0, n: 0 }; const ip1 = { e: 0, n: 0 };
const ip2 = { e: 1, 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 kink = solveIp4(ip1, ip2, ip3, 60)!;
const axis = buildAxis(ip1, ip3, kink); const axis = buildAxis(ip1, ip3, kink);
const a = kinkArc(axis)!; 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); const cross = (kink.e - ip1.e) * (ip2.n - ip1.n) - (kink.n - ip1.n) * (ip2.e - ip1.e);
expect(cross).toBeCloseTo(0, 9); 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); const len2 = Math.hypot(ip3.e - kink.e, ip3.n - kink.n);
expect(axis.length).toBeCloseTo(a + len2, 9); 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 type { PierInput } from '../src/core/types';
import { loadP1 } from './helpers'; 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, // 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.
// inside a 6 m coping centred on the axis, and a T형 column (arc ≈ 4.73) stays clear of it. // 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 IP1 = { e: 0, n: 0 };
const IP3 = { e: 6, n: 2 }; const IP3 = { e: 9, n: -3.4641016 };
const ANGLE = 60; const ANGLE = 60;
const L = 6; const L = 6;
const B = 3.1; const B = 3.1;
const FRONT_N = -0.7; const FRONT_N = -0.7;
const BACK_N = 0.6; 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 { function bentInput(foundationType: PierInput['foundationType'] = '현장타설말뚝'): PierInput {
const base = loadP1(); const base = loadP1();
@@ -133,3 +133,11 @@ describe('bent coping model (stage 2)', () => {
expect(() => buildPier(bad)).toThrow('꺾임점이 코핑 범위 밖'); 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 배치');
});
});