fix(bearing): 짝 받침을 목록 순서가 아니라 거더 기하로 찾음 (두 노선 광주·완도)

운곡교(완도,광주) 데이터에서 구조물 목록이 A1-광주, A1-완도, P1-완도 … P6-완도, P1-광주 … P6-광주,
A2-광주, A2-완도 순서라 "바로 다음 구조물"만 보던 짝짓기가 A1-광주→P1-광주와 P6-완도→A2-완도를
놓쳤다. 이제 모든 다른 구조물의 받침 중 거더를 따라 진행 방향으로 처음 만나는 받침(방위각 ≤ 0.1°,
거더선 ≤ 0.1 m)을 짝으로 한다. 이 데이터에서 14개 연결 × 5 = 70쌍, WS01·WS04 없음, S −2.028 ~ −1.754 %.
교대만 있는 다경간 입력(A1·A2 353 m, 방위각 8° 차)은 짝 없음 → 교대 종단경사 사용 (정상).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 15:35:48 +09:00
1 parent 2668f17bf2
commit 54c855ab1a
2 files changed
+18 -6

No files matched your search

+8 -6
View File
@@ -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,
});
}
}
+10
View File
@@ -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);
});
});