diff --git a/src/core/bearing-pair.ts b/src/core/bearing-pair.ts index 8a0675b..268ed4c 100644 --- a/src/core/bearing-pair.ts +++ b/src/core/bearing-pair.ts @@ -52,7 +52,7 @@ const DEG = Math.PI / 180; const angleDiffDeg = (a: number, b: number) => Math.abs(((a - b + 540) % 360) - 180); /** - * Girder pairs between neighbouring structures (Q-SH1, Q-SH4). For each bearing b on structure k the candidates on k + 1 lie + * Girder pairs over the bridge (Q-SH1, Q-SH4). For each bearing b the candidates on the other structures lie * ahead along b's azimuth, within 0.1° of its azimuth and 0.1 m of its girder line. The pair is the first one reached along * the girder: on a straight bridge the next span's bearing (후열) lies on the same line further on, so "closest to the line" * alone would be a tie (2026-11-06 review). WS04 when another candidate is within 0.5 m of it along the girder. @@ -60,11 +60,13 @@ const angleDiffDeg = (a: number, b: number) => Math.abs(((a - b + 540) % 360) - */ export function girderPairs(structures: PairStructure[]): BearingPair[] { const pairs: BearingPair[] = []; - for (let k = 0; k + 1 < structures.length; k++) { - const here = structures[k], next = structures[k + 1]; + // Every other structure is searched, not only the next one in the list: with two lines (광주 · 완도) the list order + // A1-광주, A1-완도, P1-완도 … mixes the lines, and the geometry alone tells which bearing ends the girder (2026-11-06). + for (const here of structures) { + const others = structures.filter((s) => s !== here).flatMap((s) => s.bearings.map((c) => ({ ...c, structure: s.name }))); for (const b of here.bearings) { const u = { e: Math.sin(b.azimuthDeg * DEG), n: Math.cos(b.azimuthDeg * DEG) }; - const candidates = next.bearings.map((c) => { + const candidates = others.map((c) => { const d = { e: c.x - b.x, n: c.y - b.y }; return { c, along: d.e * u.e + d.n * u.n, offset: Math.abs(d.e * u.n - d.n * u.e), dAz: angleDiffDeg(c.azimuthDeg, b.azimuthDeg), d }; }).filter((t) => t.along > 0 && t.dAz <= PAIR_AZIMUTH_TOL_DEG && t.offset <= PAIR_OFFSET_TOL_M) @@ -83,9 +85,9 @@ export function girderPairs(structures: PairStructure[]): BearingPair[] { if (fromCheckSec > AZIMUTH_CHECK_SEC || toCheckSec > AZIMUTH_CHECK_SEC) warnings.push('WS01'); if (ambiguous) warnings.push('WS04'); pairs.push({ - from: { structure: here.name, no: b.no }, to: { structure: next.name, no: best.c.no }, + from: { structure: here.name, no: b.no }, to: { structure: best.c.structure, no: best.c.no }, distanceM, offsetM: best.offset, azimuthDiffDeg: best.dAz, directionDeg, fromCheckSec, toCheckSec, - slopePct, uphill: best.c.z > b.z ? next.name : here.name, warnings, + slopePct, uphill: best.c.z > b.z ? best.c.structure : here.name, warnings, }); } } diff --git a/tests/bearing-pair.test.ts b/tests/bearing-pair.test.ts index 51a2231..82ddd42 100644 --- a/tests/bearing-pair.test.ts +++ b/tests/bearing-pair.test.ts @@ -124,3 +124,13 @@ describe('교대만 있는 프로젝트 (가져오기 허용 2026-11-06)', () => expect(back.abutments?.map((a) => a.name)).toEqual(['A1']); }); }); + +describe('두 노선 (광주 · 완도) — 목록 순서와 무관하게 거더 기하로 짝 (2026-11-06)', () => { + it('A1-광주, A1-완도, P1-완도, P1-광주 in that order still pair 광주 with 광주 and 완도 with 완도', () => { + const shift = (s: PairStructure, name: string, de: number): PairStructure => ({ name, bearings: s.bearings.map((b) => ({ ...b, x: b.x + de })) }); + const list = [shift(A1, 'A1-광주', 13), shift(A1, 'A1-완도', 0), shift(P1, 'P1-완도', 0), shift(P1, 'P1-광주', 13)]; + const pairs = girderPairs(list); + expect(pairs).toHaveLength(10); + expect(pairs.every((p) => p.from.structure.slice(3) === p.to.structure.slice(3) && p.from.structure.startsWith('A1') && p.from.no === p.to.no)).toBe(true); + }); +});