Files
Pier_v1/src/core/sole-plate.ts
T
ypshimandClaude Opus 5.5 c7acfccd51 feat(bearing): 방위각 입력 · 계산 비교와 계산값 적용 버튼
교각·교대 솔플레이트 표에 받침별 '방위각 입력 (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>
2026-11-06 15:55:24 +09:00

155 lines
7.8 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;
/** 입력 방위각 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;
}