feat(abut): ② 벽체·흉벽 카드를 시안대로 작성 (삽도 2개, 브라켓 폭 입력, 확대기초·말뚝 분리)
- 삽도 2개 (src/view/abut-wall-svg.ts): 교대축 직각 단면(① 구간, "300 + 500 + 1600 = 2400", 흉벽 상단·브라켓 상면·교좌면·저판 EL)과 정면·배면도(교좌면 계단, 흉벽 상단 경사, 받침, 수축줄눈). 생성본 값, 가로·세로 같은 축척 - 벽체 두께는 자동 (= 브라켓 폭 + 흉벽 두께 + 교좌면 폭). 입력은 브라켓 폭(기본 300). 예전 저장 파일의 벽체 두께는 브라켓 폭으로 바꿔 읽음 (normalizeShape) - 인접 무시 기준(기본 5 mm) 입력: 이웃 받침의 교좌면 EL 차이가 이 값보다 작으면 한 구간으로 묶음 (A1 단차 71 mm, 변화 없음) - 카드 구성: 벽체 형식(기본형만, QA-8) → 삽도 → 입력 격자(시안 순서, QA-4 표시) → 자동 값(벽체 두께, 흉벽 상단 IP3/IP1, 교대축) → 구간 표 → 확인 필요 → 시안 메모 - 저판·말뚝 입력 5개를 새 "확대기초 · 말뚝" 카드로 분리. 부재 목록 6행(시안 순서), 변경 표시도 카드별로 나눔 - 테스트 갱신·추가. 전체 655건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
796a267205
commit
55b679932e
11 files changed
+401
-64
No files matched your search
@@ -7,9 +7,9 @@ const base = {
|
||||
};
|
||||
|
||||
describe('교대 부재 목록 (시안 ② 부재 · 교대)', () => {
|
||||
it('has the five rows of the mockup, in order', () => {
|
||||
it('has the six rows of the mockup, in order', () => {
|
||||
const rows = abutMemberRows(base, '11.253 m');
|
||||
expect(rows.map((r) => r.id)).toEqual(['bearing', 'shape', 'wing', 'slab', 'ground']);
|
||||
expect(rows.map((r) => r.id)).toEqual(['bearing', 'shape', 'footing', 'wing', 'slab', 'ground']);
|
||||
expect(rows[0].label).toBe('교량받침·받침블럭·솔플레이트');
|
||||
});
|
||||
|
||||
@@ -20,7 +20,11 @@ describe('교대 부재 목록 (시안 ② 부재 · 교대)', () => {
|
||||
it('marks the shape when a value differs from the default', () => {
|
||||
const rows = abutMemberRows({ ...base, shape: { parapetHeightMm: 2200 } }, '—');
|
||||
expect(rows.find((r) => r.id === 'shape')?.changed).toBe(true);
|
||||
expect(rows.find((r) => r.id === 'shape')?.summary).toBe('벽체 두께 2400 · 흉벽 2200');
|
||||
expect(rows.find((r) => r.id === 'shape')?.summary).toBe('벽체 두께 2400 (자동) · 흉벽 2200');
|
||||
expect(rows.find((r) => r.id === 'footing')?.changed).toBe(false);
|
||||
const foot = abutMemberRows({ ...base, shape: { footingThicknessMm: 1300 } }, '—');
|
||||
expect(foot.find((r) => r.id === 'footing')?.changed).toBe(true);
|
||||
expect(foot.find((r) => r.id === 'shape')?.changed).toBe(false);
|
||||
});
|
||||
|
||||
it('marks the wing when a side is not the general form, and names both sides', () => {
|
||||
@@ -41,7 +45,7 @@ describe('교대 부재 목록 (시안 ② 부재 · 교대)', () => {
|
||||
expect(rows.find((r) => r.id === 'ground')?.summary).toBe('지반고 33.984 m');
|
||||
});
|
||||
|
||||
it('only the bearing, shape and wing cards take input in this step', () => {
|
||||
expect(ABUT_MEMBER_EDITABLE).toEqual({ bearing: true, shape: true, wing: true, slab: false, ground: false });
|
||||
it('only the bearing, shape, footing and wing cards take input in this step', () => {
|
||||
expect(ABUT_MEMBER_EDITABLE).toEqual({ bearing: true, shape: true, footing: true, wing: true, slab: false, ground: false });
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ABUT_SHAPE_DEFAULT, resolveShape, shapeIssues } from '../src/core/abut/shape';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, resolveShape, shapeIssues, wallThicknessMm, type AbutShape } from '../src/core/abut/shape';
|
||||
|
||||
describe('교대 형상 입력 (시안 ② 벽체·흉벽 · 확대기초·말뚝)', () => {
|
||||
it('the defaults equal the spec values (wall +1.6 … −0.8, footing +2.8 … −1.8, piles 650 / 2000)', () => {
|
||||
@@ -11,17 +11,21 @@ describe('교대 형상 입력 (시안 ② 벽체·흉벽 · 확대기초·말
|
||||
expect(r.footingBackM).toBeCloseTo(-1.8, 9);
|
||||
expect(r.pileEdgeM).toBeCloseTo(0.65, 9);
|
||||
expect(shapeIssues({})).toEqual([]);
|
||||
expect(ABUT_SHAPE_DEFAULT.wallThicknessMm).toBe(2400);
|
||||
expect(ABUT_SHAPE_DEFAULT.bracketWidthMm).toBe(300);
|
||||
expect(wallThicknessMm(ABUT_SHAPE_DEFAULT)).toBe(2400); // 300 + 500 + 1600 (자동)
|
||||
});
|
||||
|
||||
it('a changed wall thickness moves the back face (front − thickness)', () => {
|
||||
expect(resolveShape({ wallThicknessMm: 2000 }).wallBackM).toBeCloseTo(-0.4, 9);
|
||||
it('a changed 브라켓 폭 moves the back face; an old saved 벽체 두께 still opens as the same wall', () => {
|
||||
expect(resolveShape({ bracketWidthMm: 0 }).wallBackM).toBeCloseTo(-0.5, 9);
|
||||
expect(normalizeShape({ wallThicknessMm: 2000 } as Partial<AbutShape>)).toEqual({ bracketWidthMm: -100 });
|
||||
expect(resolveShape({ wallThicknessMm: 2000 } as Partial<AbutShape>).wallBackM).toBeCloseTo(-0.4, 9);
|
||||
});
|
||||
|
||||
it('refuses a footing that does not cover the wall, and non-positive sizes', () => {
|
||||
expect(shapeIssues({ footingBackMm: -500 }).join(' ')).toContain('뒷굽');
|
||||
expect(shapeIssues({ footingFrontMm: 1000 }).join(' ')).toContain('앞굽');
|
||||
expect(shapeIssues({ wallThicknessMm: 0 }).join(' ')).toContain('벽체 두께');
|
||||
expect(shapeIssues({ bracketWidthMm: -1 }).join(' ')).toContain('브라켓 폭');
|
||||
expect(shapeIssues({ seatStepIgnoreMm: -1 }).join(' ')).toContain('인접 무시 기준');
|
||||
expect(shapeIssues({ pileSpacingMm: 0 }).join(' ')).toContain('말뚝');
|
||||
});
|
||||
});
|
||||
@@ -40,7 +44,8 @@ const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abu
|
||||
describe('형상 입력이 계산에 반영됨 (시안 ② 카드)', () => {
|
||||
it('the card has one field per shape value, in the mockup order', () => {
|
||||
expect(SHAPE_EDIT_FIELDS.map((f) => f.key)[0]).toBe('wallFrontMm');
|
||||
expect(SHAPE_EDIT_FIELDS).toHaveLength(13);
|
||||
expect(SHAPE_EDIT_FIELDS).toHaveLength(14);
|
||||
expect(SHAPE_EDIT_FIELDS.filter((f) => f.group === 'footing').map((f) => f.key)).toEqual(['footingThicknessMm', 'footingFrontMm', 'footingBackMm', 'pileEdgeMm', 'pileSpacingMm']);
|
||||
});
|
||||
|
||||
it('the new drawing values do not change the geometry (the model ignores them)', () => {
|
||||
@@ -71,4 +76,11 @@ describe('형상 입력이 계산에 반영됨 (시안 ② 카드)', () => {
|
||||
const wider = buildMembers(model, {}, { pileSpacingMm: 3000 });
|
||||
expect(wider.piles[0].s.length).toBeLessThan(6);
|
||||
});
|
||||
|
||||
it('인접 무시 기준: steps smaller than the value join one segment (A1 steps are 71 mm)', () => {
|
||||
const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) });
|
||||
const foundation = { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm };
|
||||
expect(buildMembers(model, foundation).wallSegments).toHaveLength(5);
|
||||
expect(buildMembers(model, foundation, { seatStepIgnoreMm: 100 }).wallSegments).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
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 { abutWallElevationSvg, abutWallSectionSvg, type AbutWallDrawing } from '../src/view/abut-wall-svg';
|
||||
|
||||
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
|
||||
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
|
||||
const input = toAbutInput(rec);
|
||||
const model = buildAbut(input);
|
||||
const m = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm });
|
||||
const d: AbutWallDrawing = {
|
||||
lengthM: model.length,
|
||||
segments: m.wallSegments,
|
||||
bearings: model.bearings,
|
||||
footingTopEl: m.footingTopEl,
|
||||
footingBottomEl: m.footingBottomEl!,
|
||||
parapetTopStartEl: m.parapetTopStartEl,
|
||||
parapetTopEndEl: m.parapetTopEndEl,
|
||||
wallFrontM: m.shape.wallFrontM,
|
||||
parapetBackM: m.shape.parapetBackM,
|
||||
wallBackM: m.shape.wallBackM,
|
||||
footingFrontM: m.shape.footingFrontM,
|
||||
footingBackM: m.shape.footingBackM,
|
||||
bracketHeightM: m.shape.bracketHeightM,
|
||||
shrinkJointCount: 1,
|
||||
bracketWidthMm: 300,
|
||||
parapetThicknessMm: 500,
|
||||
seatWidthMm: 1600,
|
||||
};
|
||||
|
||||
describe('교대 벽체·흉벽 삽도 (시안 ② 벽체·흉벽 카드)', () => {
|
||||
it('단면 (① 구간): the width line and the levels of the generated model', () => {
|
||||
const svg = abutWallSectionSvg(d);
|
||||
for (const s of ['300 + 500 + 1600 = 2400', '교좌면 44.367', '벽체 2.372', '상면 41.995', '기초 하단 40.895', '교대축', '← 배면', '전면 →']) {
|
||||
expect(svg).toContain(s);
|
||||
}
|
||||
expect(svg).toMatch(/흉벽 상단 4[67]\.\d{3}/);
|
||||
expect(svg).toMatch(/브라켓 상면 4[56]\.\d{3}/);
|
||||
});
|
||||
|
||||
it('정면·배면도: five steps, five bearings, the sloped 흉벽 top (IP3 46.835 → IP1 47.180) and the 수축줄눈', () => {
|
||||
const svg = abutWallElevationSvg(d);
|
||||
for (const s of ['IP3', 'IP1', `${model.length.toFixed(3)} m`, '수축줄눈 1', '46.835', '47.180', '①', '⑤']) expect(svg).toContain(s);
|
||||
expect(svg).toContain('교좌면 계단 5구간');
|
||||
});
|
||||
|
||||
it('the 수축줄눈 count follows the input', () => {
|
||||
const svg = abutWallElevationSvg({ ...d, shrinkJointCount: 2 });
|
||||
expect(svg).toContain('수축줄눈 2');
|
||||
expect(svg.match(/stroke-dasharray="5 3"/g)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user