교각·교대 솔플레이트 상세 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>
187 lines
9.4 KiB
TypeScript
187 lines
9.4 KiB
TypeScript
/**
|
||
* 교대 받침 카드 (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 ? '없음' : '있음 (블럭 철근을 교좌면 아래로)'}`,
|
||
];
|
||
}
|