/** * 교대 날개벽 · AA3b (2026-11-03, spec_abut §4.7): 날개벽 평면, 상단선, 단부 하단. 순수 함수. * * - 평면: 교좌면 끝(IP1 끝 s = 0 또는 IP3 끝 s = L)에서 날개벽 선(AA2)을 따라 길이 6.5 m. 두께 0.45 m는 교대 안쪽으로 둔다. * - 상단: 시작점 = 흉벽 상단 EL(그 끝의 s), 끝까지 날개벽 경사(i)로 직선. * - 하단 (일반형): 단부 하단 = 끝점 상단 − 1.5 m. 단축형: 하단 = 저판 상면 높이(평평하게). * - 좌(①쪽) 날개벽은 받침 ①에 가까운 끝, 우(n쪽)는 반대 끝 (spec_abut §4.7). * - 하단 선 (AA3c): 저판 뒷굽 끝(배면 −1.8 m)까지는 저판 상면, 그 밖은 뒷굽 끝 상단 모서리에서 단부 하단까지 직선 * (변화단면). 뒷굽 끝까지의 날개벽 거리 = 1.8 / |sin Δ| (단축형 1.807 m와 같은 식). * - 헌치 300 × 300 (AA3c): 흉벽 배면(−0.5) 모서리에 위 헌치, 벽체 배면(−0.8) 모서리에 아래 헌치. 수직 삼각기둥. * 위 헌치는 브라켓 상면(흉벽 상단 − 0.85)부터 날개벽 상단까지, 아래 헌치는 저판 상면부터 브라켓 상면까지다. * - 아직 없음: 등단면·변화단면 수평 단 구분 (QA-12), 저판 측면 윤곽의 정밀 모서리. * * 수치는 spec_abut §12 A2 합성 예제(좌 46.838 → 46.714, 하단 45.214 / 우 47.183 → 47.059, 하단 45.559)와 맞춘다. */ import type { AbutModel, AbutPoint, WingForm } from './geometry'; import type { AbutMembers } from './members'; export const WING_LENGTH_M = 6.5; export const WING_THICKNESS_M = 0.45; /** 단부 높이 (끝점 상단에서 단부 하단까지, m). */ export const WING_END_HEIGHT_M = 1.5; export interface WingSolid { side: 'left' | 'right'; form: Exclude; /** 날개벽이 시작하는 축 끝: 0 (IP1 끝) 또는 L (IP3 끝). */ sEnd: number; /** 평면 네 꼭짓점 (E, N), 순서: 시작, 끝, 끝 안쪽, 시작 안쪽. */ corners: [AbutPoint, AbutPoint, AbutPoint, AbutPoint]; topStartEl: number; topEndEl: number; /** 단부 하단 EL (일반형: 끝 상단 − 1.5; 단축형: 저판 상면). */ bottomEndEl: number; /** 축 끝에서 날개벽 선을 따라 잰 뒷굽 끝까지의 거리 (m). */ heelDistanceM: number; /** 상단선 (d = 축 끝에서 날개벽 방향 거리 m, EL m). */ topLine: { dM: number; el: number }[]; /** 하단선: 저판 상면 → 뒷굽 끝 → 단부 하단 (일반형), 또는 저판 상면 평탄 (단축형). */ bottomLine: { dM: number; el: number }[]; /** * 저판 상면 헌치 (수평, 2026-11-03): between the 저판 상면 and the 날개벽 내측면. The triangle lies in the plane EL–s, * with legs HAUNCH_LEG_M along the 저판 상면 and along the 내측면; it runs along the wing from dMFrom (벽체 배면) to dMTo (뒷굽). */ footingHaunch?: { dMFrom: number; dMTo: number; el: number }; /** 수평 단 (QA-12, spec_abut §4.7): 등단면 / 변화단면1 / 변화단면2. 경계는 단부 높이 1.5 m와 교좌면 EL. */ bands: WingBand[]; } export interface WingBand { name: '등단면' | '변화단면1' | '변화단면2' | '단축형 전구간'; /** 높이 범위 (EL m). */ zTopEl: number; zBottomEl: number; } export const HAUNCH_LEG_M = 0.3; export function buildWings(model: AbutModel, members: AbutMembers, forms?: { left: WingForm; right: WingForm }): WingSolid[] { if (!model.wing) return []; const L = model.length; const wings = forms ?? model.wing.wings; // 좌 = 받침 ① 쪽 끝 (① 에 가까운 축 끝). const firstS = model.bearings[0].s; const leftS = firstS < L / 2 ? 0 : L; const rightS = leftS === 0 ? L : 0; const slope = model.wing.slopePct / 100; const dirRad = (model.wing.lineDeg * Math.PI) / 180; const w = { e: Math.cos(dirRad), n: Math.sin(dirRad) }; const ip1 = model.ip1; // 축 끝에서 안쪽 방향: s = 0이면 +u, s = L이면 −u. const at = (s: number) => ({ e: ip1.e + model.u.e * s, n: ip1.n + model.u.n * s }); // 배면 쪽으로 가는 날개벽 방향의 횡방향 성분 k = |w · (전면 방향)|: 날개벽 거리 d는 횡방향 t = −d·k. const frontVec = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide }; const k = Math.abs(w.e * frontVec.e + w.n * frontVec.n); const footingTop = members.footingTopEl; const heelDistanceM = Math.abs(members.shape.footingBackM) / k; const build = (side: 'left' | 'right', sEnd: number, topStart: number, form: Exclude): WingSolid => { const a = sEnd === 0 ? 1 : -1; const p0 = at(sEnd); const p1 = { e: p0.e + w.e * WING_LENGTH_M, n: p0.n + w.n * WING_LENGTH_M }; const inward = { e: model.u.e * a * WING_THICKNESS_M, n: model.u.n * a * WING_THICKNESS_M }; const topEnd = topStart + slope * WING_LENGTH_M; const bottomEnd = form === 'general' ? topEnd - WING_END_HEIGHT_M : footingTop; const topAt = (d: number) => topStart + slope * d; const topLine = [ { dM: 0, el: topStart }, { dM: WING_LENGTH_M, el: topEnd }, ]; const bottomLine = form === 'general' && heelDistanceM < WING_LENGTH_M ? [ { dM: 0, el: footingTop }, { dM: heelDistanceM, el: footingTop }, { dM: WING_LENGTH_M, el: bottomEnd }, ] : [ { dM: 0, el: footingTop }, { dM: WING_LENGTH_M, el: form === 'general' ? bottomEnd : footingTop }, ]; // 저판 상면 헌치 (수평): from the 벽체 배면 to the 뒷굽, on the 저판 상면 next to the 날개벽 내측면. const footingHaunch = { dMFrom: Math.abs(members.shape.wallBackM) / k, dMTo: heelDistanceM, el: footingTop, }; // 수평 단 (QA-12): 등단면 = 끝 상단 − 1.5 m 위, 변화단면1 = 그 아래 ~ 교좌면 EL, 변화단면2 = 교좌면 EL ~ 저판 상면. // 교좌면 EL은 날개벽 끝에 가장 가까운 받침의 교좌면이다. const nearest = model.bearings.reduce((best, b) => (Math.abs(b.s - sEnd) < Math.abs(best.s - sEnd) ? b : best)); const bands: WingBand[] = form === 'general' ? [ { name: '등단면', zTopEl: topStart, zBottomEl: bottomEnd }, { name: '변화단면1', zTopEl: bottomEnd, zBottomEl: nearest.seatEl }, { name: '변화단면2', zTopEl: nearest.seatEl, zBottomEl: footingTop }, ] : [{ name: '단축형 전구간', zTopEl: topStart, zBottomEl: footingTop }]; return { side, form, sEnd, corners: [p0, p1, { e: p1.e + inward.e, n: p1.n + inward.n }, { e: p0.e + inward.e, n: p0.n + inward.n }], topStartEl: topStart, topEndEl: topEnd, bottomEndEl: bottomEnd, heelDistanceM, topLine, bottomLine, footingHaunch, bands, }; }; const out: WingSolid[] = []; if (wings.left !== 'none') out.push(build('left', leftS, leftS === 0 ? members.parapetTopStartEl : members.parapetTopEndEl, wings.left)); if (wings.right !== 'none') out.push(build('right', rightS, rightS === 0 ? members.parapetTopStartEl : members.parapetTopEndEl, wings.right)); return out; }