교각·교대 솔플레이트 표에 받침별 '방위각 입력 (CAD)' · '방위각 계산 (CAD)' · '차이 (″, 계산 − 입력)' · [적용] 열을 두고, 표 위에 '계산 방위각 모두 적용 / 모두 입력값으로' 버튼을 둔다 (사용자 요청 2026-11-06, Q-SH4 "입력값 유지"를 사용자가 고르게 바꿈). - 계산 방위각: 거더 짝이면 두 받침을 잇는 방향, 교대만의 현 짝이면 원곡선 접선(현 방향 ± 방위각 변화의 절반) - 적용값은 교각 input.azimuthApplied / 교대 record.azimuthApplied에 받침번호별로 저장, 엑셀 값은 그대로. 블럭 · 받침 · 솔플레이트 회전과 오르막 방향에 쓰고, 짝 찾기는 엑셀 입력값으로 - 차이 표시: 거더 짝 ≤ 10″ / WS01, 현 짝은 '현 · 참고'. 도·분·초 CAD 각(엑셀과 같은 표기) - 테스트: P1 전열 계산값 ≤ 0.5″ · 후열 없음, 적용한 받침만 블럭 회전 변화, 원곡선 현 접선 = 입력, 교대 toAbutInput 반영 - 브라우저: 운곡교AA.xlsx A1-광주 ① 적용 → 블럭 회전 4.39° → 4.36°, 모두 적용 5개 · 모두 해제, 예외 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
7.8 KiB
TypeScript
155 lines
7.8 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;
|
||
/** 입력 방위각 and the one computed from the pair (deg, model 진북); the second is absent without a pair. */
|
||
azimuthInputDeg: number;
|
||
azimuthCalcDeg?: number;
|
||
/** girder (같은 거더) or chord (교대만, A1 ↔ A2 현). */
|
||
pairKind?: 'girder' | 'chord';
|
||
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;
|
||
/** The azimuth in use (an applied 계산 방위각 when chosen) — for the 오르막 direction. */
|
||
azimuthDeg: number;
|
||
/** The workbook azimuth, when it differs from the one in use. */
|
||
inputAzimuthDeg?: 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,
|
||
pairKind: pair?.kind,
|
||
azimuthInputDeg: b.inputAzimuthDeg ?? b.azimuthDeg,
|
||
azimuthCalcDeg: pair ? (pair.from.structure === structure && pair.from.no === b.no ? pair.fromCalcDeg : pair.toCalcDeg) : 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;
|
||
}
|