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:
1 parent
b1b2ad1ddb
commit
1d31d2f3a9
44 files changed
+2571
-120
No files matched your search
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 교대 날개벽 · 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 }[];
|
||||
/** 헌치 300 × 300 (위·아래), 평면 삼각형과 높이 범위. */
|
||||
haunches: Haunch[];
|
||||
/** 수평 단 (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 interface Haunch {
|
||||
kind: 'upper' | 'lower';
|
||||
/** 평면 삼각형 꼭짓점 (E, N): 모서리, 날개벽 쪽 점, 벽체·흉벽 쪽 점. */
|
||||
corners: [AbutPoint, AbutPoint, AbutPoint];
|
||||
zBottomEl: number;
|
||||
zTopEl: 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 },
|
||||
];
|
||||
|
||||
// 헌치: 위 헌치는 흉벽 배면(−0.5), 아래 헌치는 벽체 배면(−0.8)의 모서리에 둔다.
|
||||
const pointAt = (d: number) => ({ e: p0.e + w.e * d, n: p0.n + w.n * d });
|
||||
const shift = (q: AbutPoint, v: AbutPoint, f: number) => ({ e: q.e + v.e * f, n: q.n + v.n * f });
|
||||
const bracketTop = topStart - members.shape.bracketHeightM;
|
||||
const haunchAt = (kind: 'upper' | 'lower', t: number): Haunch => {
|
||||
const d = Math.abs(t) / k;
|
||||
const c = pointAt(d);
|
||||
const inwardUnit = { e: model.u.e * a, n: model.u.n * a };
|
||||
return {
|
||||
kind,
|
||||
corners: [c, shift(c, w, HAUNCH_LEG_M), shift(c, inwardUnit, HAUNCH_LEG_M)],
|
||||
zBottomEl: kind === 'upper' ? bracketTop : footingTop,
|
||||
zTopEl: kind === 'upper' ? topAt(d) : bracketTop,
|
||||
};
|
||||
};
|
||||
const haunches = [haunchAt('upper', members.shape.parapetBackM), haunchAt('lower', members.shape.wallBackM)];
|
||||
|
||||
// 수평 단 (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,
|
||||
haunches,
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user