/** * 교대 부재 · 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 + 벽체 높이 H_w (받침 ① 기준 연직, 기본 3.326 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): 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; // 인접 무시 기준 (2026-11-03): a step smaller than shape.seatStepIgnoreM joins the segment before it (one 교좌면 EL). const wallSegments: WallSegment[] = []; sorted.forEach((b, k) => { const prev = wallSegments[wallSegments.length - 1]; // Compared with the neighbouring bearing (인접), not with the first step of the joined run. if (prev && k > 0 && Math.abs(b.seatEl - sorted[k - 1].seatEl) < shape.seatStepIgnoreM) { prev.sTo = bounds[k + 1]; return; } wallSegments.push({ 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); // 본수 규칙 int((L − 2·연단)/간격) + 1; 열별 본수가 0이 아니면 그 값 (2026-11-04, 시안 열별 말뚝 본수). const ruleCount = Math.floor((L - 2 * shape.pileEdgeM) / shape.pileSpacingM) + 1; const piles: PileRow[] = shape.pileRowsT.map((t, r) => { const count = shape.pileCounts[r] > 0 ? shape.pileCounts[r] : ruleCount; const spacing = count > 1 ? (L - 2 * shape.pileEdgeM) / (count - 1) : 0; return { 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, }; }