교각·교대 솔플레이트 상세 TABLE에 받침별로 엑셀 방위각과 짝 받침 방향의 차이(초)를 보인다. 10″ 이하 "≤ 10″", 넘으면 WS01, 짝이 없으면 "— (짝 없음)". - BearingSole.azimuthCheckSec (짝의 from/to 쪽에 맞는 값), 표 행 azimuthSec - 테스트: P1 전열 ≤ 0.5″ · 후열 없음, A1 ≤ 0.5″, P1 ③ 방위각 75°59′ 오타 → 약 87″ · WS01 그 받침만 - 브라우저 확인(운곡교 완도·광주 데이터): P1-광주 전열 짝 A1-광주 0.0–0.3″, 후열 짝 P2-광주 0.0–0.1″ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
7.1 KiB
TypeScript
144 lines
7.1 KiB
TypeScript
/**
|
||
* 솔플레이트 (결정 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<string, { aMm: number; bMm: number }>;
|
||
/** T per row, when typed (mm). Absent = 자동. */
|
||
tByRow?: Partial<Record<string, number>>;
|
||
/** S per bearing number, when typed (%). */
|
||
slopePct?: Record<string, number>;
|
||
}
|
||
|
||
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;
|
||
}
|