/** * 솔플레이트 (결정 2026-11-06, RAG/받침방위각_솔플레이트검토.md §3, spec.md §5.3, 시안 Version 110–113). Pure. * Replaces the 2026-11-03 C안 (받침높이 H 고정, h3 = H − T, S 입력) by the user's decision to follow the 시안: * * - 평면: L = A + 여유 (거더 방향 = 방위각), W = B + 여유 (여유 100). A × B per bearing; absent = 700 × 640 (P1 전열 값, 가정). * - S: 같은 거더 양끝 받침의 z 차 / 수평거리 (girderPairs). 없으면 교각 종단경사 입력 → 같은 구조물 다른 열의 같은 순번 값 * ("가정", WS03) → 0 % (WS03). 받침별 직접 수정 = "수정됨". * - T: 열마다 max(T_min, ⌈max(T_min + L·|S|/2)⌉). 직접 고치면 "수정됨", T2 < T_min이면 WS02. * T1 = round(T + L·|S|/2) 오르막 쪽, T2 = round(T − L·|S|/2). * - 높이: 거더 하면 고정 — 블럭 상면 = z − (T − T_ref); 받침 본체 h3 고정 (181, P1 상세도). */ import { type BearingPair, pairOf } from './bearing-pair'; export interface SoleSettings { /** 받침 본체 높이 h3 (mm). */ h3Mm?: number; /** T_ref: the solplate thickness the workbook z assumed (mm). */ tRefMm?: number; /** T_min: the thin-end minimum (mm). */ tMinMm?: number; /** 솔플 여유 L − A = W − B (mm). */ marginMm?: number; /** 받침 평면 A (거더 방향) × B per bearing number (mm). */ plates?: Record; /** T per row, when typed (mm). Absent = 자동. */ tByRow?: Partial>; /** S per bearing number, when typed (%). */ slopePct?: Record; } export const SOLE_DEFAULTS = { h3Mm: 181, tRefMm: 30, tMinMm: 22, marginMm: 100, aMm: 700, bMm: 640 } as const; export type SlopeSource = '계산' | '입력' | '가정' | '수정됨' | '없음'; export interface BearingSole { no: number; row: string; pair?: { structure: string; no: number }; /** 방위각 검사 (″): this bearing's input azimuth against the direction to its pair (Q-SH4, WS01 > 10″). Absent without a pair. */ azimuthCheckSec?: number; slopePct: number; slopeSource: SlopeSource; aMm: number; bMm: number; /** L along the girder, W across it (mm). */ L: number; W: number; /** Row thickness T and whether it was typed; T1 on the 오르막 쪽, T2 on the other end (mm). */ T: number; tTyped: boolean; T1: number; T2: number; /** This bearing's requirement T_min + L·|S|/2 (mm). */ tReq: number; h3: number; /** Block top drop T − T_ref (mm, + = the block gets lower). */ dropMm: number; /** Unit vector (E, N) towards the 오르막 쪽, or null when S = 0. */ uphill: { e: number; n: number } | null; warnings: ('WS01' | 'WS02' | 'WS03' | 'WS04')[]; } export interface SoleBearing { no: number; row: string; azimuthDeg: number; } const DEG = Math.PI / 180; /** * Solplates of one structure. `pairs` are the girder pairs of the whole bridge (girderPairs over A1, P1 … Pn, A2); * `fallbackSlopePct` is the structure's 종단경사 input (%), when given. */ export function structureSoles(structure: string, bearings: SoleBearing[], settings: SoleSettings = {}, pairs: BearingPair[] = [], fallbackSlopePct?: number): BearingSole[] { const s = { ...SOLE_DEFAULTS, ...settings }; const rows = [...new Set(bearings.map((b) => b.row))]; const order = (b: SoleBearing) => bearings.filter((q) => q.row === b.row).indexOf(b); const paired = new Map(bearings.map((b) => [b.no, pairOf(pairs, structure, b.no)])); const pairSlope = (b: SoleBearing) => paired.get(b.no)?.slopePct; const slopes = bearings.map((b) => { const typed = settings.slopePct?.[String(b.no)]; if (typed !== undefined) return { slopePct: typed, source: '수정됨' as SlopeSource }; const own = pairSlope(b); if (own !== undefined) return { slopePct: own, source: '계산' as SlopeSource }; if (fallbackSlopePct !== undefined) return { slopePct: fallbackSlopePct, source: '입력' as SlopeSource }; // The same position in another row that has a pair (시안: 후열 ⑥ ← 전열 ①). for (const other of rows.filter((r) => r !== b.row)) { const twin = bearings.filter((q) => q.row === other)[order(b)]; const v = twin && pairSlope(twin); if (v !== undefined) return { slopePct: v, source: '가정' as SlopeSource }; } return { slopePct: 0, source: '없음' as SlopeSource }; }); const plate = (b: SoleBearing) => s.plates?.[String(b.no)] ?? { aMm: s.aMm, bMm: s.bMm }; const req = bearings.map((b, i) => s.tMinMm + ((plate(b).aMm + s.marginMm) * Math.abs(slopes[i].slopePct)) / 200); const rowT = new Map(rows.map((r) => { const typed = settings.tByRow?.[r]; const auto = Math.max(s.tMinMm, Math.ceil(Math.max(...bearings.map((b, i) => (b.row === r ? req[i] : -Infinity))) - 1e-9)); return [r, { T: typed ?? auto, typed: typed !== undefined }]; })); return bearings.map((b, i) => { const { aMm, bMm } = plate(b); const L = aMm + s.marginMm, W = bMm + s.marginMm; const { slopePct, source } = slopes[i]; const { T, typed } = rowT.get(b.row)!; const half = (L * Math.abs(slopePct)) / 200; const T1 = Math.round(T + half), T2 = Math.round(T - half); const pair = paired.get(b.no); const warnings: BearingSole['warnings'] = [...(pair?.warnings ?? [])]; if (T2 < s.tMinMm) warnings.push('WS02'); if (source === '가정' || source === '없음') warnings.push('WS03'); // S runs along the structure order, which is the azimuth direction (girderPairs looks ahead along it). const dir = { e: Math.sin(b.azimuthDeg * DEG), n: Math.cos(b.azimuthDeg * DEG) }; const sign = Math.sign(slopePct); return { no: b.no, row: b.row, pair: pair ? (pair.from.structure === structure ? pair.to : pair.from) : undefined, azimuthCheckSec: pair ? (pair.from.structure === structure && pair.from.no === b.no ? pair.fromCheckSec : pair.toCheckSec) : undefined, slopePct, slopeSource: source, aMm, bMm, L, W, T, tTyped: typed, T1, T2, tReq: req[i], h3: s.h3Mm, dropMm: T - s.tRefMm, uphill: sign ? { e: sign * dir.e, n: sign * dir.n } : null, warnings, }; }); } /** Input problems of the settings, in Korean; empty when they can build solplates. */ export function soleSettingsIssues(settings: SoleSettings | undefined): string[] { if (!settings) return []; const out: string[] = []; const positive = (v: unknown, label: string) => { if (v !== undefined && !(typeof v === 'number' && Number.isFinite(v) && v > 0)) out.push(`${label}은(는) 0보다 커야 합니다.`); }; positive(settings.h3Mm, '받침 높이 h3'); positive(settings.tRefMm, 'T_ref'); positive(settings.tMinMm, 'T_min'); if (settings.marginMm !== undefined && !(Number.isFinite(settings.marginMm) && settings.marginMm >= 0)) out.push('솔플 여유는 0 이상이어야 합니다.'); for (const [no, p] of Object.entries(settings.plates ?? {})) { positive(p.aMm, `받침 ${no} A`); positive(p.bMm, `받침 ${no} B`); } for (const [row, t] of Object.entries(settings.tByRow ?? {})) positive(t, `${row} T`); for (const [no, v] of Object.entries(settings.slopePct ?? {})) if (!Number.isFinite(v)) out.push(`받침 ${no} S는 숫자여야 합니다.`); return out; }