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

+16 -4
View File
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as XLSX from 'xlsx';
import { describe, expect, it } from 'vitest';
import { applyAbutEdit, applyBearingEdit, restoreAbutDefault, setWingForm } from '../src/core/abut/edit';
import { applyAbutEdit, applyBearingEdit, restoreAbutDefault, setWingForm, setWingsLinked, applyWingDim } from '../src/core/abut/edit';
import { parseAbutments } from '../src/core/abut/parse';
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
@@ -25,11 +25,23 @@ describe('교대 입력 편집 (교대 편집 카드)', () => {
expect(() => applyAbutEdit(rec, 'seatEl', rec.foundationBottomEl)).toThrow('기초 하단 EL보다 높아야');
});
it('a wing form change sets one side and keeps the other', () => {
const next = setWingForm(rec, 'left', 'short');
expect(next.wings).toEqual({ left: 'short', right: 'general' });
it('a wing form change sets both sides while 좌우 같게 (default), one side once it is off', () => {
expect(setWingForm(rec, 'left', 'short').wings).toEqual({ left: 'short', right: 'short' });
const apart = setWingsLinked(rec, false);
expect(setWingForm(apart, 'left', 'short').wings).toEqual({ left: 'short', right: 'general' });
expect(rec.wings).toBeUndefined();
});
it('wing dims: 좌우 같게 applies to both sides, off applies to one; turning it on copies the left side', () => {
const both = applyWingDim(rec, 'left', 'lengthMm', 6000);
expect(both.wingDims).toEqual({ left: { lengthMm: 6000 }, right: { lengthMm: 6000 } });
const apart = applyWingDim(setWingsLinked(rec, false), 'right', 'thicknessMm', 500);
expect(apart.wingDims).toEqual({ left: {}, right: { thicknessMm: 500 } });
const relinked = setWingsLinked(applyWingDim(apart, 'left', 'lengthMm', 5000), true);
expect(relinked.wingDims).toEqual({ left: { lengthMm: 5000 }, right: { lengthMm: 5000 } });
expect(() => applyWingDim(rec, 'left', 'thicknessMm', 0)).toThrow('두께');
expect(() => applyWingDim(rec, 'left', 'haunchMm', -1)).toThrow('헌치');
});
});
describe('교대 기본값 복원과 받침 편집', () => {
+68
View File
@@ -0,0 +1,68 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as XLSX from 'xlsx';
import { describe, expect, it } from 'vitest';
import { buildAbut } from '../src/core/abut/geometry';
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
import { buildMembers } from '../src/core/abut/members';
import { buildWings } from '../src/core/abut/wings';
import { abutPlanOutline } from '../src/view/abut-scene';
import { abutWingCornerSvg, abutWingPlanSvg, abutWingSideSvg } from '../src/view/abut-wing-svg';
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
const build = (r = rec) => {
const model = buildAbut(toAbutInput(r));
const members = buildMembers(model, { bottomEl: r.foundationBottomEl, pileLengthMm: r.pileLengthMm }, r.shape);
return { model, members, wings: buildWings(model, members, r.wings, r.wingDims) };
};
describe('날개벽 치수 입력 (측별) 과 연동', () => {
it('defaults: 6500 · 450 · 1500 · haunch 300 on both sides; 단축형 runs to the 뒷굽 끝 (1.807 m)', () => {
const { wings } = build();
for (const w of wings) expect([w.lengthM, w.thicknessM, w.endHeightM, w.haunchM]).toEqual([6.5, 0.45, 1.5, 0.3]);
const short = build({ ...rec, wings: { left: 'short', right: 'short' } }).wings[0];
expect(short.lengthM).toBeCloseTo(short.heelDistanceM, 3);
expect(short.heelDistanceM).toBeCloseTo(1.807, 3);
});
it('a thicker wing narrows the 접속슬래브 (slab width = L − Σ(t/|sin Δ| + 20))', () => {
const base = build().model.wing!.slabWidthMm;
const thick = build({ ...rec, wingDims: { left: { thicknessMm: 550 }, right: { thicknessMm: 550 } } });
expect(base - thick.model.wing!.slabWidthMm).toBeGreaterThan(195); // 2 × 100 / |sin Δ| ≈ 200.8
expect(thick.wings[0].thicknessM).toBeCloseTo(0.55, 9);
});
it('haunch 0 drops the 연직 헌치 and the 저판 상면 헌치 of that wing', () => {
const w = build({ ...rec, wingDims: { left: { haunchMm: 0 }, right: {} } }).wings.find((x) => x.side === 'left')!;
expect(w.haunches).toHaveLength(0);
expect(w.footingHaunch).toBeUndefined();
});
});
describe('날개벽 삽도 (시안 ② 날개벽 카드)', () => {
it('평면: both wings, the slab between them and its width', () => {
const { model, members, wings } = build();
const svg = abutWingPlanSvg({
lengthM: model.length, outline: abutPlanOutline(model, members, wings), leftLabel: '좌 · 일반형', rightLabel: '우 · 일반형',
leftSEnd: wings.find((w) => w.side === 'left')!.sEnd, slabWidthM: model.wing!.slabWidthMm / 1000,
});
for (const s of ['좌 · 일반형', '우 · 일반형', `접속슬래브 폭 ${(model.wing!.slabWidthMm / 1000).toFixed(3)} m`]) expect(svg).toContain(s);
expect(svg.match(/<polygon /g)).toHaveLength(4); // slab, wall, two wings
});
it('측면: the bands, the length, the end height and the 뒷굽 끝', () => {
const { members, wings } = build();
const w = wings[0];
const svg = abutWingSideSvg({
name: '좌 날개벽', form: w.form, lengthM: w.lengthM, endHeightM: w.endHeightM, heelDistanceM: w.heelDistanceM,
topLine: w.topLine, bottomLine: w.bottomLine, bands: w.bands, seatEl: rec.seatEl, footingTopEl: members.footingTopEl,
});
for (const s of ['6500 (교좌면 끝에서, 날개벽 축 방향)', '1500', '등단면', '변화단면1', '변화단면2', '뒷굽 끝 1.807 m', '교좌면 44.367']) expect(svg).toContain(s);
});
it('모서리: haunch, bracket, slab chamfer and gaps from the inputs', () => {
const svg = abutWingCornerSvg({ parapetMm: 500, bracketMm: 300, wingThicknessMm: 450, haunchMm: 300, slabGapWingMm: 20, slabGapParapetMm: 5, chamferMm: 300 });
for (const s of ['헌치 300 × 300', '브라켓 300 (슬래브 아래)', '슬래브 모따기 300 × 300', '날개벽과 20 · 흉벽과 5', '흉벽 500', '날개벽 450']) expect(svg).toContain(s);
});
});