- 선택 줄 (타일): 좌우 같게(기본) / 따로 정하기, 편집할 측 (좌 · ① 쪽 / 우 · ⑤ 쪽), 형식 일반형(기본) / 단축형 / 없음 - 측별 치수 입력 (WingDims, 교대 기록 wingDims): 길이 6500, 두께 450, 끝 높이 1500(일반형), 헌치 300. 단축형 길이 기본 = 뒷굽 끝(1.807 m). 좌우 같게이면 양쪽에 함께 적용, 켤 때 좌측 값을 우측에 복사 - 연동: 두께 → 날개벽 3D · 접속슬래브 폭(geometry slabWidthMm) · 슬래브 평면, 헌치 → 연직 헌치 · 저판 상면 헌치 (0이면 없음) - 자동 값: 경사 (A1 = −i), 등단면 · 변화단면 경계(뒷굽 끝), 끝 EL 상단 / 하단 (가정), 단축형 하단 = 확대기초 상면 (뒷굽 밖은 AI02 안내) - 삽도 3개 (src/view/abut-wing-svg.ts): 평면(좌우 날개벽 · 접속슬래브, abutPlanOutline 공유), 측면(수평 단 · 뒷굽 끝), 날개벽–흉벽 모서리 모식도(헌치 · 브라켓 · 슬래브 모따기 · 간격) - "생성되는 날개벽" 줄과 시안 메모. 프로젝트 파일 검사에 wingDims · wingsLinked 추가 - 테스트 7건 추가 · 갱신. 전체 669건 통과, 타입 검사 · 빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
11 KiB
TypeScript
226 lines
11 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;
|
||
|
||
/** 날개벽 치수 입력 (측별, mm, 2026-11-04 시안 ② 날개벽 카드). 단축형 길이의 기본값은 뒷굽 끝까지의 거리다. */
|
||
export interface WingDims {
|
||
lengthMm: number;
|
||
thicknessMm: number;
|
||
endHeightMm: number;
|
||
haunchMm: number;
|
||
}
|
||
|
||
export const WING_DIMS_DEFAULT: WingDims = { lengthMm: 6500, thicknessMm: 450, endHeightMm: 1500, haunchMm: 300 };
|
||
|
||
/** The dims of one side: the user's values over the defaults; 단축형 without a length input runs to the 뒷굽 끝. */
|
||
export function resolveWingDims(form: WingForm, changes: Partial<WingDims> | undefined, heelDistanceM: number): WingDims {
|
||
const base = { ...WING_DIMS_DEFAULT, ...(changes ?? {}) };
|
||
if (form === 'short' && changes?.lengthMm === undefined) base.lengthMm = Math.round(heelDistanceM * 1000);
|
||
return base;
|
||
}
|
||
|
||
/** Input problems of one side's dims, in Korean; empty when the wing can be built. */
|
||
export function wingDimsIssues(d: WingDims): string[] {
|
||
const out: string[] = [];
|
||
if (!(d.lengthMm > 0)) out.push('날개벽 길이는 0보다 커야 합니다.');
|
||
if (!(d.thicknessMm > 0)) out.push('날개벽 두께는 0보다 커야 합니다.');
|
||
if (!(d.endHeightMm > 0)) out.push('날개벽 끝 높이는 0보다 커야 합니다.');
|
||
if (!(d.haunchMm >= 0)) out.push('헌치는 0 이상이어야 합니다.');
|
||
return out;
|
||
}
|
||
|
||
export interface WingSolid {
|
||
side: 'left' | 'right';
|
||
form: Exclude<WingForm, 'none'>;
|
||
/** 날개벽이 시작하는 축 끝: 0 (IP1 끝) 또는 L (IP3 끝). */
|
||
sEnd: number;
|
||
/** 이 측의 치수 (m): 길이, 두께, 끝 높이, 헌치 다리. */
|
||
lengthM: number;
|
||
thicknessM: number;
|
||
endHeightM: number;
|
||
haunchM: 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[];
|
||
/**
|
||
* 저판 상면 헌치 (수평, 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 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 },
|
||
dims?: { left?: Partial<WingDims>; right?: Partial<WingDims> },
|
||
): 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 dm = resolveWingDims(form, dims?.[side], heelDistanceM);
|
||
const length = dm.lengthMm / 1000;
|
||
const thick = dm.thicknessMm / 1000;
|
||
const leg = dm.haunchMm / 1000;
|
||
const p0 = at(sEnd);
|
||
const p1 = { e: p0.e + w.e * length, n: p0.n + w.n * length };
|
||
const inward = { e: model.u.e * a * thick, n: model.u.n * a * thick };
|
||
const topEnd = topStart + slope * length;
|
||
const bottomEnd = form === 'general' ? topEnd - dm.endHeightMm / 1000 : footingTop;
|
||
const topAt = (d: number) => topStart + slope * d;
|
||
const topLine = [
|
||
{ dM: 0, el: topStart },
|
||
{ dM: length, el: topEnd },
|
||
];
|
||
const bottomLine =
|
||
form === 'general' && heelDistanceM < length
|
||
? [
|
||
{ dM: 0, el: footingTop },
|
||
{ dM: heelDistanceM, el: footingTop },
|
||
{ dM: length, el: bottomEnd },
|
||
]
|
||
: [
|
||
{ dM: 0, el: footingTop },
|
||
{ dM: length, 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;
|
||
// The 브라켓 상면 falls along s (the 흉벽 top slopes), so the upper haunch rests on the highest point of its own footprint.
|
||
const sAt = (q: AbutPoint) => (q.e - model.ip1.e) * model.u.e + (q.n - model.ip1.n) * model.u.n;
|
||
const backTopAt = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / model.length - members.shape.bracketHeightM;
|
||
const haunchAt = (kind: 'upper' | 'lower', t: number): Haunch => {
|
||
const d = Math.abs(t) / k;
|
||
// The corner lies on the 날개벽 내측면 (the face between the two wings), WING_THICKNESS_M in from the axis end.
|
||
const inwardUnit = { e: model.u.e * a, n: model.u.n * a };
|
||
const c = shift(pointAt(d), inwardUnit, thick);
|
||
const corners: [AbutPoint, AbutPoint, AbutPoint] = [c, shift(c, w, leg), shift(c, inwardUnit, leg)];
|
||
return {
|
||
kind,
|
||
corners,
|
||
zBottomEl: kind === 'upper' ? Math.max(...corners.map((q) => backTopAt(sAt(q)))) : footingTop,
|
||
zTopEl: kind === 'upper' ? topAt(d) : bracketTop,
|
||
};
|
||
};
|
||
// 연직 헌치 (2026-11-03, 내측면 모서리): upper at the 흉벽 배면, lower at the 벽체 배면.
|
||
const haunches = leg > 0 ? [haunchAt('upper', members.shape.parapetBackM), haunchAt('lower', members.shape.wallBackM)] : [];
|
||
|
||
// 저판 상면 헌치 (수평): from the 벽체 배면 to the 뒷굽, on the 저판 상면 next to the 날개벽 내측면.
|
||
const footingHaunch =
|
||
leg > 0
|
||
? {
|
||
dMFrom: Math.abs(members.shape.wallBackM) / k,
|
||
dMTo: Math.min(heelDistanceM, length),
|
||
el: footingTop,
|
||
}
|
||
: undefined;
|
||
|
||
// 수평 단 (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,
|
||
lengthM: length,
|
||
thicknessM: thick,
|
||
endHeightM: dm.endHeightMm / 1000,
|
||
haunchM: leg,
|
||
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,
|
||
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;
|
||
}
|