feat(bearing): 교각 솔플레이트를 2026-11-06 결정으로 (짝 받침 S · 열 T · 거더 하면 고정)

받침방위각_솔플레이트검토.md §3과 시안(Version 110–113)을 따른다 (사용자 결정 2026-11-06: 시안 적용).
11-03 C안(받침높이 H 고정, h3 = H − T, S 입력)을 대체한다.
- S: 교량 전체(A1, P1 … Pn, A2) 거더 짝 받침의 z 차 / 수평거리. 없으면 교각 종단경사 입력 →
  다른 열 같은 순번 값(가정, WS03) → 0 %(WS03). 받침별 직접 수정(수정됨)
- 평면 L = A + 100, W = B + 100 (받침별 A × B, 기본 700 × 640 가정), h3 181
- T = 열마다 max(22, ⌈22 + L·|S|/2⌉), 열 T 직접 수정 가능(T2 < T_min → WS02).
  T1은 오르막 쪽(3D 쐐기의 두꺼운 끝도 그쪽), T2 반대쪽
- 거더 하면 고정: 블럭 상면 = z − (T − T_ref), T_ref 30
- 짝 받침은 불러오기·프로젝트 열기 때 교량 전체로 계산해 교각 생성에 넘김
- ② 카드: h3·T_ref·T_min·여유·열 T·종단경사 입력, 표에 짝 받침·S 근거·받침별 S/A/B 수정·검산 W08·검사
- P1 검증(A1과 함께): S −1.908 … −1.902 %, L 800 · W 740 · T 30 · T1 38 · T2 22, 블럭 높이 변화 없음
- 블럭·솔플레이트 테스트는 A1–P1 짝 문맥으로 실행 (짝이 없으면 S 0 · T 22 · 블럭 +8 mm)
교대(QA-15 C안)와 Pier 시트 종단경사 열 읽기는 아직 남음.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 14:48:10 +09:00
1 parent 5fec2743cb
commit a0b0929e13
19 files changed
+486 -109

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
/**
* 솔플레이트 (결정 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 };
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,
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;
}