- 위 연직 헌치(흉벽 배면)와 아래 연직 헌치(벽체 배면) 삭제. 관련 코드와 Haunch 타입 제거 - 저판 상면 헌치는 유지 - 테스트 갱신. 전체 637건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
144 lines
7.0 KiB
TypeScript
144 lines
7.0 KiB
TypeScript
/**
|
||
* 교대 날개벽 · 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<WingForm, 'none'>;
|
||
/** 날개벽이 시작하는 축 끝: 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<WingForm, 'none'>): 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;
|
||
}
|