diff --git a/src/core/geometry.ts b/src/core/geometry.ts index bcc22e7..1ac410c 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -138,7 +138,7 @@ export interface PierModel { /** * Bend point IP4 (spec §4.1-2): the point where the IP1→IP2 line meets the line through IP3 whose - * direction is IP1→IP2 turned by the bend angle. Positive angle = counter-clockwise [확인 필요: 부호 규약]. + * direction is IP1→IP2 turned by the bend angle. Positive angle = right (clockwise) turn when travelling IP1→IP2. * Returns null for a zero angle, where IP4 is not defined. */ export function solveIp4(ip1: Vec2, ip2: Vec2, ip3: Vec2, angleDeg: number): Vec2 | null { @@ -146,7 +146,7 @@ export function solveIp4(ip1: Vec2, ip2: Vec2, ip3: Vec2, angleDeg: number): Vec const cross = (a: Vec2, b: Vec2) => a.e * b.n - a.n * b.e; const len = Math.hypot(ip2.e - ip1.e, ip2.n - ip1.n); const d1: Vec2 = { e: (ip2.e - ip1.e) / len, n: (ip2.n - ip1.n) / len }; - const rad = (angleDeg * Math.PI) / 180; + const rad = (-angleDeg * Math.PI) / 180; // clockwise is positive, so the counter-clockwise rotation uses −θ const d2: Vec2 = { e: d1.e * Math.cos(rad) - d1.n * Math.sin(rad), n: d1.e * Math.sin(rad) + d1.n * Math.cos(rad) }; const t = cross({ e: ip3.e - ip1.e, n: ip3.n - ip1.n }, d2) / cross(d1, d2); return { e: ip1.e + t * d1.e, n: ip1.n + t * d1.n }; diff --git a/tests/validate-and-foundation.test.ts b/tests/validate-and-foundation.test.ts index 17d4057..4ea788a 100644 --- a/tests/validate-and-foundation.test.ts +++ b/tests/validate-and-foundation.test.ts @@ -167,15 +167,14 @@ function meanN(m: ReturnType, row: '전열' | '후열'): numbe describe('꺾인 코핑 IP4 (spec §4.1)', () => { const cross = (a: { e: number; n: number }, b: { e: number; n: number }) => a.e * b.n - a.n * b.e; - it('IP4 lies on the IP1→IP2 line and on the line through IP3 turned by the bend angle', () => { - const ip1 = { e: 0, n: 0 }; - const ip2 = { e: 1, n: 0 }; - const ip3 = { e: 10, n: 2 }; - const ip4 = solveIp4(ip1, ip2, ip3, 10)!; - expect(ip4.n).toBeCloseTo(0, 9); + it('right (clockwise) bend of +10° puts IP4 on the IP1→IP2 line, with IP3 on the turned line', () => { const rad = (10 * Math.PI) / 180; - const d2 = { e: Math.cos(rad), n: Math.sin(rad) }; - expect(cross({ e: ip4.e - ip3.e, n: ip4.n - ip3.n }, d2)).toBeCloseTo(0, 9); + const d2 = { e: Math.cos(rad), n: -Math.sin(rad) }; // heading east, turning right → south-east + const expectedIp4 = { e: 5, n: 0 }; + const ip3 = { e: expectedIp4.e + 5 * d2.e, n: expectedIp4.n + 5 * d2.n }; + const ip4 = solveIp4({ e: 0, n: 0 }, { e: 1, n: 0 }, ip3, 10)!; + expect(ip4.e).toBeCloseTo(5, 9); + expect(ip4.n).toBeCloseTo(0, 9); }); it('zero angle has no IP4', () => {