/** * 교대 받침 카드 (2026-11-03, 시안 ② 교량받침·받침블럭·솔플레이트, 교각 카드와 같은 형식). 순수 함수. * * - 솔플레이트 (결정 2026-11-06, spec_abut §5·QA-15, src/core/sole-plate.ts): S = 같은 거더의 짝 받침(A1 ↔ P1 전열, * A2 ↔ Pn)으로 계산, 짝이 없으면 교대 종단경사 i(엑셀 Abut). L = A + 100, W = B + 100 (A × B 기본 700 × 640, 교각 P1 * 전열 값 가정). 받침이 1열이라 T는 교대 전체에 하나: max(22, ⌈22 + L·|S|/2⌉). 거더 하면 고정 → 블럭 상면 = z − (T − T_ref). * - h3 기본값 = 받침 시트의 받침높이 − T_ref (A1 275 − 30 = 245): 엑셀 받침높이가 정한 거더 하면을 그대로 둔다 * (시안은 교각 P1의 h3 181을 가정했으나 교대 시트에 받침높이가 있어 그 값을 지킨다). * - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 + h4 (= T). z = 패드 상면 = EL.X. */ import type { BearingPair } from '../bearing-pair'; import { type BearingSole, SOLE_DEFAULTS, structureSoles } from '../sole-plate'; import { ABUT_PAD_MM, type AbutModel } from './geometry'; import type { AbutMembers } from './members'; /** 교대 솔플레이트 입력 (2026-11-06). Every value optional; absent = the default. */ export interface AbutBearingRule { /** 받침 본체 높이 h3 (mm). Absent = 받침높이 − T_ref. */ h3Mm?: number; tRefMm: number; tMinMm: number; /** 솔플 여유 L − A = W − B (mm). */ marginMm: number; /** 받침 평면 A (거더 방향) × B (mm), every bearing. */ aMm: number; bMm: number; /** T typed (mm). Absent = 자동. */ tMm?: number; /** S typed for every bearing (%). Absent = 짝 받침 계산. */ slopePct?: number; } export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { tRefMm: SOLE_DEFAULTS.tRefMm, tMinMm: SOLE_DEFAULTS.tMinMm, marginMm: SOLE_DEFAULTS.marginMm, aMm: SOLE_DEFAULTS.aMm, bMm: SOLE_DEFAULTS.bMm }; /** 받침블럭 평면 (mm, 정사각) and 몰탈패드 평면 (mm), spec_abut §4.2. */ export const ABUT_BLOCK_MM = 1200; export const ABUT_PAD_PLAN_MM = 1000; /** I01: 블럭 콘크리트 높이가 피복 50 + 철근 지름 13보다 작으면 블럭 철근을 교좌면 아래로 묻는다 (spec_abut §6.6). */ const I01_MIN_MM = 50 + 13; const CIRCLED = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩']; /** Keys the 2026-11-06 rule reads; values stored by the C안 (lengthMm, widthMm, kind) are ignored. */ const RULE_KEYS = ['h3Mm', 'tRefMm', 'tMinMm', 'marginMm', 'aMm', 'bMm', 'tMm', 'slopePct'] as const; export function resolveBearingRule(changes?: Partial & Record): AbutBearingRule { const own = Object.fromEntries(Object.entries(changes ?? {}).filter(([k]) => (RULE_KEYS as readonly string[]).includes(k))); return { ...ABUT_BEARING_RULE_DEFAULT, ...own }; } /** Bridge context of the abutment card: the girder pairs and the 교대 종단경사 i (decimal). */ export interface AbutSoleContext { pairs?: BearingPair[]; slopeI?: number; } /** Solplate of every bearing of the abutment (one row). */ export function abutSoles(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): BearingSole[] { const plates = Object.fromEntries(model.bearings.map((b) => [String(b.no), { aMm: rule.aMm, bMm: rule.bMm }])); return structureSoles( model.name, model.bearings.map((b) => ({ no: b.no, row: '1열', azimuthDeg: b.azimuthDeg ?? 0 })), { h3Mm: rule.h3Mm ?? heightMm - rule.tRefMm, tRefMm: rule.tRefMm, tMinMm: rule.tMinMm, marginMm: rule.marginMm, plates, tByRow: rule.tMm === undefined ? undefined : { '1열': rule.tMm }, slopePct: rule.slopePct === undefined ? undefined : Object.fromEntries(model.bearings.map((b) => [String(b.no), rule.slopePct!])), }, ctx.pairs ?? [], ctx.slopeI === undefined ? undefined : ctx.slopeI * 100, ); } export interface AbutElevationRow { no: number; /** IP3(① 쪽)에서 잰 위치 (m). */ sFromIp3M: number; seg: string; seatEl: number; h1: number; h2: number; /** 패드 상면 = EL.X (m). */ z: number; h3: number; h4: number; E: number; /** 솔플레이트 상면 EL (m) = 교좌면 + E. */ solplateTopEl: number; /** 블럭 끝에서 가장 가까운 계단 경계까지 (m). 경계가 없으면 null. */ stepGapM: number | null; /** 받침 방위각 (deg): CAD (엑셀 값, 동 = 0° 반시계) / 진북 (북에서 시계). 입력이 없으면 null. */ azimuthCadDeg: number | null; azimuthNorthDeg: number | null; /** 블럭 평면 회전 (deg), 교대축 직각 기준. */ rotDeg: number; } export function abutElevationRows(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): AbutElevationRow[] { const half = ABUT_BLOCK_MM / 2000; const soles = new Map(abutSoles(model, rule, heightMm, ctx).map((x) => [x.no, x])); return model.bearings.map((b) => { const plan = soles.get(b.no)!; // 거더 하면 고정: the block concrete loses T − T_ref (2026-11-06, Q-SH7). const h1 = b.blockHeightMm - ABUT_PAD_MM - plan.dropMm; const E = h1 + ABUT_PAD_MM + plan.h3 + plan.T; const gaps = model.stepBounds.map((x) => Math.abs(x - b.s) - half); return { no: b.no, sFromIp3M: model.length - b.s, seg: CIRCLED[b.no - 1] ?? String(b.no), seatEl: b.seatEl, h1, h2: ABUT_PAD_MM, z: b.z - plan.dropMm / 1000, h3: plan.h3, h4: plan.T, E, solplateTopEl: b.seatEl + E / 1000, stepGapM: gaps.length ? Math.min(...gaps) : null, azimuthCadDeg: b.azimuthDeg === undefined ? null : (((90 - b.azimuthDeg) % 360) + 360) % 360, azimuthNorthDeg: b.azimuthDeg === undefined ? null : ((b.azimuthDeg % 360) + 360) % 360, rotDeg: b.rotDeg, }; }); } export interface AbutSolplateRow { no: number; /** 기울기 (%). */ S: number; /** S 근거 (계산 / 입력 / 가정 / 수정됨 / 없음) and the 짝 받침, e.g. "P1-①". */ source: string; pair?: string; /** 방위각 검사 (″): input azimuth against the pair direction; absent without a pair. */ azimuthSec?: number; L: number; W: number; T: number; T1: number; T2: number; /** 검산 문구 "T1 − T2 ≈ L·|S|/100" (W08, 1 mm 이내). */ check: string; warnings: string[]; } export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): AbutSolplateRow[] { return abutSoles(model, rule, heightMm, ctx).map((p) => { const rhs = (p.L * Math.abs(p.slopePct)) / 100; return { no: p.no, S: p.slopePct, source: p.slopeSource, azimuthSec: p.azimuthCheckSec, pair: p.pair ? `${p.pair.structure}-${CIRCLED[p.pair.no - 1] ?? p.pair.no}` : undefined, L: p.L, W: p.W, T: p.T, T1: p.T1, T2: p.T2, check: `${p.T1 - p.T2} ≈ ${rhs.toFixed(1)} ${Math.abs(p.T1 - p.T2 - rhs) <= 1 + 1e-9 ? '일치' : '불일치 (W08)'}`, warnings: p.warnings, }; }); } /** Input problems of the 교대 솔플레이트 입력. Empty when it can build a solplate. */ export function abutBearingRuleIssues(rule: AbutBearingRule, heightMm: number): string[] { const out: string[] = []; for (const k of ['tRefMm', 'tMinMm', 'aMm', 'bMm'] as const) if (!(Number.isFinite(rule[k]) && rule[k] > 0)) out.push(`${k}는 0보다 커야 합니다.`); if (!(Number.isFinite(rule.marginMm) && rule.marginMm >= 0)) out.push('솔플 여유는 0 이상이어야 합니다.'); if (rule.tMm !== undefined && !(Number.isFinite(rule.tMm) && rule.tMm > 0)) out.push('T는 0보다 커야 합니다.'); if (rule.slopePct !== undefined && !Number.isFinite(rule.slopePct)) out.push('S는 숫자여야 합니다.'); const h3 = rule.h3Mm ?? heightMm - rule.tRefMm; if (!(Number.isFinite(h3) && h3 > 0)) out.push(`받침 높이 h3 (${h3})는 0보다 커야 합니다. 받침높이 ${heightMm}와 T_ref를 확인하세요.`); return out; } /** 검사 줄 (시안): W02 블럭 콘크리트, W04 블럭이 교좌면 안, W03 블럭 끝 ~ 계단 경계, I01 블럭 철근. 문제 없으면 "없음". */ export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): string[] { const rows = abutElevationRows(model, rule, heightMm, ctx); const minH1 = Math.min(...rows.map((r) => r.h1)); const half = ABUT_BLOCK_MM / 2000; // Block plan across the wall: centre t = frontSide × lateral, between the 흉벽 front (t 0) and the 벽체 front. const tCentres = model.bearings.map((b) => model.frontSide * b.lateral); const backGap = Math.min(...tCentres.map((t) => t - half)); const frontGap = Math.min(...tCentres.map((t) => members.shape.wallFrontM - (t + half))); const stepGaps = rows.map((r) => r.stepGapM).filter((g): g is number => g !== null); const minStep = stepGaps.length ? Math.min(...stepGaps) : null; return [ `블럭 콘크리트 ${minH1} > 0 (W02 ${minH1 > 0 ? '없음' : '있음'})`, `블럭이 교좌면 안 (흉벽 ${Math.round(backGap * 1000)} · 전면 ${Math.round(frontGap * 1000)}, W04 ${backGap >= 0 && frontGap >= 0 ? '없음' : '있음'})`, minStep === null ? '블럭 끝 ~ 계단 경계: 경계 없음 (W03 없음)' : `블럭 끝 ~ 계단 경계 최소 ${minStep.toFixed(3)} m (W03 ${minStep >= 0 ? '없음' : '있음'})`, `${minH1} ≥ ${I01_MIN_MM} → I01 ${minH1 >= I01_MIN_MM ? '없음' : '있음 (블럭 철근을 교좌면 아래로)'}`, ]; }