Files
Pier_v1/src/core/sole-plate.ts
T
ypshimandClaude Opus 5.5 3bd2340609 feat(bearing): 솔플레이트 표에 '방위각 검사 (″)' 열 (Q-SH4)
교각·교대 솔플레이트 상세 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>
2026-11-06 15:39:42 +09:00

144 lines
7.1 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.
/**
* 솔플레이트 (결정 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;
}