Files
Pier_v1/src/core/abut/bearing-table.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

187 lines
9.4 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-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<AbutBearingRule> & Record<string, unknown>): 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 ? '없음' : '있음 (블럭 철근을 교좌면 아래로)'}`,
];
}