feat(abut): ② 날개벽 카드를 시안대로 작성 (좌우 같게 · 측별 치수 · 삽도 3개)

- 선택 줄 (타일): 좌우 같게(기본) / 따로 정하기, 편집할 측 (좌 · ① 쪽 / 우 · ⑤ 쪽), 형식 일반형(기본) / 단축형 / 없음
- 측별 치수 입력 (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>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-04 09:20:29 +09:00
1 parent a025a61209
commit 80e5302192
12 files changed
+610 -53

No files matched your search

+65 -17
View File
@@ -21,11 +21,43 @@ 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;
@@ -66,7 +98,12 @@ export interface Haunch {
export const HAUNCH_LEG_M = 0.3;
export function buildWings(model: AbutModel, members: AbutMembers, forms?: { left: WingForm; right: WingForm }): WingSolid[] {
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;
@@ -89,26 +126,30 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
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 * 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 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: WING_LENGTH_M, el: topEnd },
{ dM: length, el: topEnd },
];
const bottomLine =
form === 'general' && heelDistanceM < WING_LENGTH_M
form === 'general' && heelDistanceM < length
? [
{ dM: 0, el: footingTop },
{ dM: heelDistanceM, el: footingTop },
{ dM: WING_LENGTH_M, el: bottomEnd },
{ dM: length, el: bottomEnd },
]
: [
{ dM: 0, el: footingTop },
{ dM: WING_LENGTH_M, el: form === 'general' ? bottomEnd : footingTop },
{ dM: length, el: form === 'general' ? bottomEnd : footingTop },
];
// 헌치: 위 헌치는 흉벽 배면(−0.5), 아래 헌치는 벽체 배면(−0.8)의 모서리에 둔다.
@@ -122,8 +163,8 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
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, WING_THICKNESS_M);
const corners: [AbutPoint, AbutPoint, AbutPoint] = [c, shift(c, w, HAUNCH_LEG_M), shift(c, inwardUnit, HAUNCH_LEG_M)];
const c = shift(pointAt(d), inwardUnit, thick);
const corners: [AbutPoint, AbutPoint, AbutPoint] = [c, shift(c, w, leg), shift(c, inwardUnit, leg)];
return {
kind,
corners,
@@ -132,14 +173,17 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
};
};
// 연직 헌치 (2026-11-03, 내측면 모서리): upper at the 흉벽 배면, lower at the 벽체 배면.
const haunches = [haunchAt('upper', members.shape.parapetBackM), haunchAt('lower', members.shape.wallBackM)];
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 = {
dMFrom: Math.abs(members.shape.wallBackM) / k,
dMTo: heelDistanceM,
el: footingTop,
};
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은 날개벽 끝에 가장 가까운 받침의 교좌면이다.
@@ -157,6 +201,10 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef
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,