feat(abut): 교대 형상·입력 패널과 교각 받침·솔플레이트 개편

- 교대 형상(src/core/abut, 3D 장면, 목록·카메라·맞춤)과 교대 입력 패널 추가
- 교각 받침 기준 카드 제거, 솔플레이트 규칙을 교각별 입력으로 변경
- 받침 ELEVATION·솔플레이트 TABLE을 모델값으로 연결
- 교대 입력값 카드: 형식 행에 시점·종점 표시, 제목을 "A1 입력값 [입력]"으로 변경
- 테스트 614건 통과, 타입 검사 통과

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-03 14:51:34 +09:00
1 parent b1b2ad1ddb
commit 1d31d2f3a9
44 files changed
+2571 -120

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
/**
* 교대 부재 · AA3 (2026-11-03, spec_abut §4.3–4.6): 벽체, 흉벽 상단, 저판, 말뚝의 치수와 위치. Pure.
*
* - 교대축 기준 t: 전면 쪽이 +. 벽체 전면 +1.6 m (교좌면 끝), 배면 −0.8 m (브라켓 배면). 두께 2.4 m.
* - 흉벽: t −0.5 ~ 0 m. 상단 EL(s) = 교좌면 EL(구간 ①) + 흉벽 높이 2.5 m + i_b × (s − s_1), i_b = 받침 z의 축 방향 경사.
* - 저판: 하면 = 기초 하단 EL, 두께 1.1 m. 폭은 t −1.8 ~ +2.8 m (앞굽 1.2, 뒷굽 1.0, 벽체 2.4). 길이 = 교좌면 길이 L.
* - 말뚝: 열 위치 t = −1.15 / +0.85 / +2.15 m. 열 안 본수 = int((L − 1.3)/2.0) + 1, 양끝 0.65 m 띄움, 균등 간격.
* 말뚝 선단 EL = 기초 하단 EL − 말뚝길이. (skew 평행사변형에서 열 끝이 밀리는 보정은 아직 없다 — QA-5 이후.)
* - 벽체 높이 = 구간 교좌면 EL − (기초 하단 EL + 저판 두께).
*/
import type { AbutModel } from './geometry';
import { resolveShape, type AbutShape, type ResolvedShape } from './shape';
export interface WallSegment {
/** 축 위 구간 범위 (IP1에서 m). */
sFrom: number;
sTo: number;
/** 구간의 교좌면 EL (구간 i). */
seatEl: number;
/** 벽체 높이 (m) = seat − (footing bottom + footing thickness). */
heightM: number;
}
export interface PileRow {
/** 횡방향 위치 t (m, 전면 +). */
t: number;
/** 축 위 말뚝 위치 (IP1에서 m). */
s: number[];
}
export interface AbutMembers {
/** The shape values in use (defaults merged with the user's changes), in m. */
shape: ResolvedShape;
wallSegments: WallSegment[];
/** 흉벽 상단 EL: s = 0 (IP1 끝)과 s = L (IP3 끝), 축 방향 경사 i_b (소수, + = IP3 쪽으로 올라감). */
parapetTopStartEl: number;
parapetTopEndEl: number;
parapetSlope: number;
footingTopEl: number;
footingBottomEl: number | undefined;
piles: PileRow[];
pileBottomEl: number | undefined;
}
/** 흉벽 상단 EL at axial position s (m). */
export function parapetTopEl(seatFirst: number, bearingSFirst: number, slope: number, s: number, parapetHeightM: number): number {
return seatFirst + parapetHeightM + slope * (s - bearingSFirst);
}
export function buildMembers(model: AbutModel, foundation: { bottomEl?: number; pileLengthMm?: number } = {}, shapeChanges?: Partial<AbutShape>): AbutMembers {
const L = model.length;
const shape = resolveShape(shapeChanges);
const sorted = [...model.bearings].sort((a, b) => a.s - b.s);
const bounds = [0, ...model.stepBounds, L];
const footingBottomEl = foundation.bottomEl;
const footingTopEl = footingBottomEl === undefined ? NaN : footingBottomEl + shape.footingThicknessM;
const wallSegments: WallSegment[] = sorted.map((b, k) => ({
sFrom: bounds[k],
sTo: bounds[k + 1],
seatEl: b.seatEl,
heightM: b.seatEl - footingTopEl,
}));
// 받침 ①(첫 입력)과 마지막 입력 받침이 흉벽 상단선을 정한다 (spec §4.4).
const first = model.bearings[0];
const last = model.bearings[model.bearings.length - 1];
const slope = (last.z - first.z) / (last.s - first.s);
const parapetTopStartEl = parapetTopEl(first.seatEl, first.s, slope, 0, shape.parapetHeightM);
const parapetTopEndEl = parapetTopEl(first.seatEl, first.s, slope, L, shape.parapetHeightM);
const count = Math.floor((L - 2 * shape.pileEdgeM) / shape.pileSpacingM) + 1;
const spacing = count > 1 ? (L - 2 * shape.pileEdgeM) / (count - 1) : 0;
const piles: PileRow[] = shape.pileRowsT.map((t) => ({
t,
s: Array.from({ length: count }, (_, k) => shape.pileEdgeM + k * spacing),
}));
const pileBottomEl = footingBottomEl === undefined || foundation.pileLengthMm === undefined ? undefined : footingBottomEl - foundation.pileLengthMm / 1000;
return {
shape,
wallSegments,
parapetTopStartEl,
parapetTopEndEl,
parapetSlope: slope,
footingTopEl,
footingBottomEl,
piles,
pileBottomEl,
};
}