Files
Pier_v1/src/core/bearing-pair.ts
T
ypshimandClaude Opus 5.5 7c5f129712 feat(bearing): 교대만 있을 때 A1 ↔ A2 현(chord) 짝 받침
교각 없이 A1 · A2만 불러온 다경간 곡선교(운곡교 광주·완도, 358 m, 방위각 8° 차)는 거더 짝 허용오차
(방위각 ≤ 0.1°)를 넘어 짝이 없었다. 교각이 0기일 때만, A1 받침과 A2 받침을 잇는 현의 방향이 두
방위각의 평균과 0.1° 이내인 받침을 짝으로 한다(원곡선이면 정확히 같다). 같은 노선·같은 번호가 117″,
다른 후보는 1176″ 이상이라 하나로 정해진다.
- S = Δz / 현 길이 (교량 전체 평균 경사), S 근거 '계산', 짝 받침 표시 "A2-광주-① (현)"
- 방위각 검사 칸은 "현 117″ (참고)": 한 거더용 10″ 규칙이 아니므로 WS01은 내지 않음
- 교각이 하나라도 있으면 쓰지 않음. 단경간 직선(A1·A2 방위각 같음)은 기존 거더 짝으로 처리
- 테스트: 곡선 두 노선 합성 10쌍(노선·번호 일치), 교대 카드 표시, 교각 있을 때 미적용
- 브라우저: 운곡교AA.xlsx A1-광주 ①–⑤ ↔ A2-광주 ①–⑤ (현), S −1.900 ~ −1.892 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:45:33 +09:00

154 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 거더 짝 받침 · 종단경사 S · 솔플레이트 열 두께 (결정 2026-11-06, RAG/받침방위각_솔플레이트검토.md §3, Q-SH1·4·8·9).
* Pure. Used by src/core/sole-plate.ts (pier solplates, 2026-11-06) through src/core/bridge-pairs.ts.
*
* Frame: (E, N) in m as in the model (x = E, y = N). Azimuth: the model's 진북 방위각 (deg, clockwise from north), i.e. the
* workbook CAD angle θ_CAD converted by 90 − θ_CAD. A girder at azimuth θ runs along (sin θ, cos θ).
*/
export interface PairBearing {
no: number;
x: number;
y: number;
/** z (EL m). Q-SH5: both ends on the 받침블럭 상면 basis; the pier z definition is still spec Q16. */
z: number;
azimuthDeg: number;
}
export interface PairStructure {
name: string;
bearings: PairBearing[];
}
/** Q-SH1 tolerances and the Q-SH4 azimuth check. */
export const PAIR_AZIMUTH_TOL_DEG = 0.1;
export const PAIR_OFFSET_TOL_M = 0.1;
export const AZIMUTH_CHECK_SEC = 10;
/** Two candidates nearer than this along the girder are ambiguous (WS04). Less than the row spacing (block 1.2 m). */
export const PAIR_ALONG_TOL_M = 0.5;
export type PairWarning = 'WS01' | 'WS04';
export interface BearingPair {
/** The bearing on structure k and its partner on structure k + 1 (structure order A1, P1 … Pn, A2). */
from: { structure: string; no: number };
to: { structure: string; no: number };
/** Horizontal distance (m), distance off the girder line of `from` (m), azimuth difference |θ_to − θ_from| (deg). */
distanceM: number;
offsetM: number;
azimuthDiffDeg: number;
/** Direction from → to as an azimuth (deg), and its difference from each input azimuth (arc seconds). */
directionDeg: number;
fromCheckSec: number;
toCheckSec: number;
/** S = (z_to − z_from) / distance × 100 (%), sign along the structure order (− = lower towards the later structure). */
slopePct: number;
/** The structure on the higher end: the 오르막 쪽 where T1 sits for both bearings. */
uphill: string;
/** girder = the two ends of one girder; chord = A1 ↔ A2 of a bridge loaded without piers (현, chordPairs). */
kind: 'girder' | 'chord';
warnings: PairWarning[];
}
const DEG = Math.PI / 180;
const angleDiffDeg = (a: number, b: number) => Math.abs(((a - b + 540) % 360) - 180);
/**
* 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.
* WS01 when either input azimuth is more than 10″ from the pair direction.
*/
export function girderPairs(structures: PairStructure[]): BearingPair[] {
const pairs: BearingPair[] = [];
// 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 = 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)
.sort((p, q) => p.along - q.along);
if (!candidates.length) continue;
// The nearest along the girder, and among those about as near (ambiguous, WS04) the one closest to the line.
const near = candidates.filter((t) => t.along - candidates[0].along < PAIR_ALONG_TOL_M).sort((p, q) => p.offset - q.offset);
const best = near[0];
const ambiguous = near.length > 1;
const distanceM = Math.hypot(best.d.e, best.d.n);
const directionDeg = (Math.atan2(best.d.e, best.d.n) / DEG + 360) % 360;
const fromCheckSec = angleDiffDeg(directionDeg, b.azimuthDeg) * 3600;
const toCheckSec = angleDiffDeg(directionDeg, best.c.azimuthDeg) * 3600;
const slopePct = ((best.c.z - b.z) / distanceM) * 100;
const warnings: PairWarning[] = [];
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: best.c.structure, no: best.c.no },
distanceM, offsetM: best.offset, azimuthDiffDeg: best.dAz, directionDeg, fromCheckSec, toCheckSec,
slopePct, uphill: best.c.z > b.z ? best.c.structure : here.name, warnings, kind: 'girder',
});
}
}
return pairs;
}
/**
* A1 ↔ A2 pairs when no pier is loaded (교대만, 검토 2026-11-06). Over several spans the azimuths of the two ends differ
* (곡선, 운곡교 8°), so the girder test fails; the chord from b to c then runs along the mean of the two azimuths (exact on a
* circular arc). The partner is the end bearing whose chord is within 0.1° of that mean (WS04 when two are), and S is the
* whole-bridge average Δz / chord. The check value is chord − mean azimuth, for reference only (no WS01).
*/
export function chordPairs(starts: PairStructure[], ends: PairStructure[]): BearingPair[] {
const pairs: BearingPair[] = [];
for (const here of starts) for (const b of here.bearings) {
const candidates = ends.flatMap((s) => s.bearings.map((c) => {
const d = { e: c.x - b.x, n: c.y - b.y };
const directionDeg = (Math.atan2(d.e, d.n) / DEG + 360) % 360;
const mean = (b.azimuthDeg + c.azimuthDeg + (Math.abs(b.azimuthDeg - c.azimuthDeg) > 180 ? 360 : 0)) / 2 % 360;
return { c, structure: s.name, d, directionDeg, dev: angleDiffDeg(directionDeg, mean) };
})).filter((t) => t.dev <= PAIR_AZIMUTH_TOL_DEG).sort((p, q) => p.dev - q.dev);
if (!candidates.length) continue;
const best = candidates[0];
const distanceM = Math.hypot(best.d.e, best.d.n);
pairs.push({
from: { structure: here.name, no: b.no }, to: { structure: best.structure, no: best.c.no },
distanceM, offsetM: 0, azimuthDiffDeg: angleDiffDeg(best.c.azimuthDeg, b.azimuthDeg), directionDeg: best.directionDeg,
fromCheckSec: best.dev * 3600, toCheckSec: best.dev * 3600, slopePct: ((best.c.z - b.z) / distanceM) * 100,
uphill: best.c.z > b.z ? best.structure : here.name, warnings: candidates.length > 1 ? ['WS04'] : [], kind: 'chord',
});
}
return pairs;
}
/** The pair a bearing belongs to, on either end; undefined when it has none (then WS03 with a fallback S, not done here). */
export function pairOf(pairs: BearingPair[], structure: string, no: number): BearingPair | undefined {
return pairs.find((p) => (p.from.structure === structure && p.from.no === no) || (p.to.structure === structure && p.to.no === no));
}
/** Smallest allowed thin-end thickness T_min (mm, Q-SH3). */
export const SOLE_T_MIN_MM = 22;
/** Thickness one bearing needs: T_req = T_min + L·|S|/2 (L mm, S %). */
export function soleRequiredT(lengthMm: number, slopePct: number, tMinMm: number = SOLE_T_MIN_MM): number {
return tMinMm + (lengthMm * Math.abs(slopePct)) / 200;
}
/**
* Row thickness and end thicknesses (Q-SH8·9): T_row = max(T_min, ⌈max T_req⌉) over the row; per bearing
* T1 = round(T_row + L·|S|/2) on the 오르막 쪽 and T2 = round(T_row − L·|S|/2) (≥ T_min by construction).
*/
export function soleRowThickness(items: { lengthMm: number; slopePct: number }[], tMinMm: number = SOLE_T_MIN_MM) {
const T = Math.max(tMinMm, Math.ceil(Math.max(...items.map((i) => soleRequiredT(i.lengthMm, i.slopePct, tMinMm))) - 1e-9));
return {
T,
ends: items.map((i) => {
const half = (i.lengthMm * Math.abs(i.slopePct)) / 200;
return { T1: Math.round(T + half), T2: Math.round(T - half) };
}),
};
}