feat(abut): 접속슬래브 경사는 형식이 정함, 단면 · 접합 상세를 파단선 단면 하나로 합침, 흉벽 높이 입력

- 경사 선택 삭제: 매립형 = 하향 4 %, 노출형 = 종단경사 따라 (노트 표기). 예전 파일의 slope 값은 읽고 무시
- 단면 + 접합 상세: 흉벽 쪽과 먼 끝만 그리고 중간 생략, 부속재 · 치수(7000 · 450 · 850 · 295 · 헌치) · EL을 한 그림에
- 흉벽 높이 (브라켓 상면 ~ 흉벽 상단) 입력 = 벽체·흉벽 카드 브라켓 높이와 같은 값
- 자동 값: 브라켓 얹힘 길이 = 브라켓 폭 − 흉벽과 간격 (295)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-04 10:27:20 +09:00
1 parent 4b8b80ae97
commit 9835fb42b3
7 files changed
+157 -199

No files matched your search

+17 -23
View File
@@ -7,12 +7,12 @@ 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 { applyShapeEdit, applySlabEdit, setSlabForm, setSlabSlope } from '../src/core/abut/edit';
import { applyShapeEdit, applySlabEdit, setSlabForm } from '../src/core/abut/edit';
import { abutMemberRows } from '../src/core/abut/member-list';
import { resolveSlab, slabChangedKeys, slabGradePct, slabIssues, slabSection, slabSlopeLabel } from '../src/core/abut/slab';
import { checkStoredAbutments } from '../src/core/project';
import { abutPlanOutline, buildAbutGroup } from '../src/view/abut-scene';
import { abutSlabJointSvg, abutSlabPlanSvg, abutSlabSectionSvg } from '../src/view/abut-slab-svg';
import { abutSlabPlanSvg, abutSlabSectionSvg } from '../src/view/abut-slab-svg';
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
@@ -25,14 +25,13 @@ const build = (r = rec) => {
const SHAPE = { parapetBackM: -0.5, wallBackM: -0.8, bracketHeightM: 0.85 };
describe('접속슬래브 입력 (2026-11-04, spec_abut §4.8, 시안 ② 접속슬래브 카드)', () => {
it('defaults: 매립형 · 하향 4 % · 7000 × 450 · 간격 5 / 20; the slope follows the form unless set', () => {
expect(resolveSlab()).toEqual({ form: 'buried', slope: 'down4', lengthMm: 7000, thicknessMm: 450, parapetGapMm: 5, wingGapMm: 20, haunchLengthMm: 2000 });
expect(resolveSlab({ form: 'exposed' }).slope).toBe('grade');
const exposed = setSlabForm({ ...rec, slab: { slope: 'down4' } }, 'exposed');
expect(exposed.slab).toEqual({ form: 'exposed' }); // picking a form resets the slope to the form's own
expect(setSlabSlope(exposed, 'down4').slab).toEqual({ form: 'exposed', slope: 'down4' });
expect(setSlabSlope(exposed, 'grade').slab).toEqual({ form: 'exposed' });
expect(slabChangedKeys(exposed.slab)).toEqual(['form', 'slope']);
it('defaults: 매립형 · 7000 × 450 · 간격 5 / 20; the form sets the slope, an old stored slope is dropped', () => {
expect(resolveSlab()).toEqual({ form: 'buried', lengthMm: 7000, thicknessMm: 450, parapetGapMm: 5, wingGapMm: 20, haunchLengthMm: 2000 });
const old = { form: 'exposed', slope: 'down4' } as unknown as Parameters<typeof resolveSlab>[0];
expect(resolveSlab(old)).toEqual({ ...resolveSlab(), form: 'exposed' });
expect(setSlabForm({ ...rec, slab: old }, 'exposed').slab).toEqual({ form: 'exposed' });
expect(slabGradePct(resolveSlab(old), 1.91)).toBe(1.91); // 노출형 = 종단경사, whatever was stored
expect(slabChangedKeys({ form: 'exposed' })).toEqual(['form']);
expect(slabChangedKeys(undefined)).toEqual([]);
});
@@ -43,7 +42,7 @@ describe('접속슬래브 입력 (2026-11-04, spec_abut §4.8, 시안 ② 접속
expect(() => applyShapeEdit({ ...rec, slab: { thicknessMm: 600 } }, 'bracketHeightMm', 500)).toThrow('접속슬래브 두께');
});
it('slope text and value: 하향 4 % = −4; 종단경사 = the wing slope (A1 = −i)', () => {
it('slope text and value: 매립형 하향 4 % = −4; 노출형 종단경사 = the wing slope (A1 = −i)', () => {
const { model } = build();
expect(slabGradePct(resolveSlab(), model.wing!.slopePct)).toBe(-4);
expect(slabGradePct(resolveSlab({ form: 'exposed' }), 1.91)).toBe(1.91);
@@ -112,12 +111,13 @@ describe('접속슬래브 입력 (2026-11-04, spec_abut §4.8, 시안 ② 접속
it('the project file keeps the slab input and refuses a wrong form', () => {
expect(() => checkStoredAbutments([{ ...rec, slab: { form: 'exposed', lengthMm: 8000 } }])).not.toThrow();
expect(() => checkStoredAbutments([{ ...rec, slab: { form: 'exposed', slope: 'down4' } as never }])).not.toThrow(); // earlier files
expect(() => checkStoredAbutments([{ ...rec, slab: { form: 'open' } }])).toThrow('접속슬래브');
expect(() => checkStoredAbutments([{ ...rec, slab: { lengthMm: 'x' } }])).toThrow('접속슬래브');
});
});
describe('접속슬래브 삽도 (평면 · 단면 · 접합 상세)', () => {
describe('접속슬래브 삽도 (평면, 단면 + 접합 상세)', () => {
const { model, members, wings } = build();
const sh = members.shape;
const sectionOf = (form: 'buried' | 'exposed') => {
@@ -137,19 +137,13 @@ describe('접속슬래브 삽도 (평면 · 단면 · 접합 상세)', () => {
for (const s of [`폭 ${(model.wing!.slabWidthMm / 1000).toFixed(3)} m`, '>7000<', '모따기 300 × 300', '접속슬래브']) expect(svg).toContain(s);
});
it('단면 매립형: 7000, 450, 하향 4 %, EL labels; 노출형 adds 헌치 2000 and 끝 850', () => {
it('단면 + 접합 상세 (중간 생략): 7000, 450, 흉벽 높이 850, 얹힘 295, accessories and EL; 노출형 adds 헌치 2000', () => {
const buried = sectionOf('buried');
for (const s of ['>7000<', '>450<', '경사 하향 4 %', '흉벽 상단 EL.46.867', '브라켓 상면 EL.46.017', '간격 5', '다웰 D25 @400']) expect(buried).toContain(s);
for (const s of ['7000 (중간 생략)', '>450<', '>850<', '>295<', '경사 하향 4 %', '흉벽 상단 EL.46.867', '브라켓 상면 EL.46.017',
'간격 5 · 아스팔트 충진재', '스티로폼', '고무판 150 × 150 × 15', '타르페이퍼 T = 15', '다웰 D25 @400 · 강관 Φ60 L300']) expect(buried).toContain(s);
expect(buried).not.toContain('헌치');
expect(buried.match(/<polygon /g)).toHaveLength(3); // 흉벽 · 벽체, slab far piece, slab near piece
const exposed = sectionOf('exposed');
for (const s of ['헌치 2000', '끝 850', '경사 종단경사 +']) expect(exposed).toContain(s);
});
it('접합 상세: the accessories and the EL labels', () => {
const svg = abutSlabJointSvg({ form: 'buried', gradePct: -4, parapetTopEl: 46.867, bracketHeightM: 0.85, parapetGapMm: 5, thicknessMm: 450 });
for (const s of ['간격 5 · 아스팔트 충진재', '스티로폼', '고무판 150 × 150 × 15', '타르페이퍼 T = 15', '다웰 D25 · 강관 Φ60 L300', 'S = 4.00 % (하향)', '흉벽 상단 EL.46.867', '브라켓 상면 EL.46.017', '접속슬래브 450']) {
expect(svg).toContain(s);
}
expect(abutSlabJointSvg({ form: 'exposed', gradePct: 1.91, parapetTopEl: 46.867, bracketHeightM: 0.85, parapetGapMm: 5, thicknessMm: 450 })).toContain('노출형 · 끝 850');
for (const s of ['헌치 2000', '경사 종단경사 +', '>295<']) expect(exposed).toContain(s);
});
});