Files
Pier_v1/tests/cast-in-place.test.ts
ypshimandClaude Opus 5.5 370bd3d7cd feat(shape): 다주형 코핑 하면을 오목형으로 (Level D+200 · 오목 500 · 헌치 1500)
spec §3·§4.4(2026-11-06 개정)와 3주형형상배근.md §2.2(2주형 DWG 실측)를 따른다.
- 기둥 사이 하면: Level 끝에서 헌치 수평 1500 동안 500 올라가 평탄, 다음 기둥 앞에서 내려옴
  (이전 Level + 300, 헌치 300 × 300 대체)
- 다주형 Level 폭 기본값 = D + 200 (입력값이 있으면 입력 우선). T형은 2100 유지
- 오목 평탄부가 없으면 RB-M01 오류
- 3주형 테스트는 오목형이 성립하는 20 m 코핑으로 바꿈 (P1 길이 12 m는 RB-M01)
- 2주형 DWG 치수(L 24200, D 3000) 하면 꺾임점 테스트 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:11:15 +09:00

95 lines
6.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { buildPierScene } from '../src/view/scene';
import { buildRebar3D } from '../src/view/rebar3d';
import { rebarSummary } from '../src/core/rebar/summary';
import { buildQuantity, quantityRows } from '../src/core/rebar/quantity';
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
import { castSettingsIssues, type CastInPlaceRebar } from '../src/core/rebar/cast-in-place';
import { castRebarLayout } from '../src/core/rebar/cast-layout';
import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project';
import { changedKeys } from '../src/core/changes';
import { validateInput } from '../src/core/validate';
import type { Issue, PierInput } from '../src/core/types';
import { loadP1 } from './helpers';
const settings: CastInPlaceRebar = { column: { mainDiameterMm: 32, mainCount: 46, coverMm: 100, mainGradeMpa: 500 }, pile: { mainDiameterMm: 32, mainCount: 46, cageLengthMm: 12000, tieKind: 'spiral' }, connection: { mode: 'continuous' } };
function project(input: PierInput): ProjectFile {
return { schemaVersion: SCHEMA_VERSION, project: { name: '', bridge: '', client: '', author: '', crs: '', memo: '' }, source: { fileName: '', sheets: [] }, history: [], piers: [{ id: 'one', input, defaults: { ...input, castInPlaceRebar: undefined }, draft: { ...input, castInPlaceRebar: settings } }] };
}
describe('cast-in-place independent rendering and incomplete quantities', () => {
for (const [type, columnCount] of [['T형', 1], ['2주형', 2], ['3주형', 3]] as const) {
for (const foundationType of ['직접기초', '말뚝기초', '현장타설말뚝'] as const) {
it(`${type} / ${foundationType}: concrete, rebar and summary remain usable`, () => {
// 3주형 needs a coping long enough for the 오목형 spans (RB-M01 otherwise).
const m = buildPier({ ...loadP1(), ...(columnCount === 3 ? { copingLengthMm: 20000 } : {}), type, columnCount, foundationType });
const concrete = buildPierScene(m);
expect(concrete.parts.coping.children.length).toBeGreaterThan(0);
const rebar = buildRebar3D(m);
const result = rebarSummary(m);
expect(rebar.byName.get('C1')!.length).toBeGreaterThan(0);
const q = buildQuantity(result.rows, DEFAULT_MATERIAL, result.incomplete);
if (foundationType === '현장타설말뚝') {
expect(m.footings).toHaveLength(0);
expect(m.piles).toHaveLength(columnCount);
m.columns.forEach((c, i) => expect(c.bottom).toBe(m.piles[i].top));
expect(rebar.byName.get('기둥-P1')!.length).toBeGreaterThan(0);
expect(rebar.byName.get('말뚝-P1')!.length).toBeGreaterThan(0);
expect(rebar.byName.get('기둥-P3')!.length).toBeGreaterThan(0);
expect(rebar.byName.get('말뚝-P3')!.length).toBeGreaterThan(0);
expect(rebar.byName.has('B1')).toBe(false);
expect(result.rows.some(r => r.member === 'column')).toBe(true);
expect(result.rows.some(r => r.member === 'pile')).toBe(true);
expect(result.rows.some(r => r.member === 'footing')).toBe(false);
expect(q.missing).toEqual(result.incomplete);
expect(q.missing).toEqual(expect.arrayContaining(castRebarLayout(m).missing));
expect(q.missing).toContain('말뚝 저부 P6 검토안 미적용');
expect(quantityRows(q).filter(r => r[0] === '미계산 · 합계 미완료')).toHaveLength(q.missing.length);
expect(q.kg).toBeGreaterThan(0);
} else {
expect(rebar.byName.get('P1')!.length).toBeGreaterThan(0);
if (columnCount === 1) expect(result.incomplete).toEqual([]);
else expect(result.incomplete).toContain('다주형 철근 번호 대응·cycle별 이음 위치·수평철근 갈고리 상세 확인 필요');
expect(q.missing).toEqual(result.incomplete);
}
rebar.group.traverse(o => { const mesh = o as import('three').Mesh; mesh.geometry?.dispose(); });
});
}
}
});
describe('cast-in-place storage and validation', () => {
it('round-trips dedicated settings in generated/default/draft inputs without merging legacy settings', () => {
const input = { ...loadP1(), foundationType: '현장타설말뚝' as const, castInPlaceRebar: settings };
const p = project(input);
expect(parseProject(serializeProject(p))).toEqual(p);
expect(changedKeys({ ...input, castInPlaceRebar: undefined }, input)).toContain('castInPlaceRebar');
});
it('legacy projects keep absent settings absent rather than inventing defaults', () => {
const p = project(loadP1());
delete p.piers[0].draft;
expect(parseProject(serializeProject(p)).piers[0].input.castInPlaceRebar).toBeUndefined();
});
for (const bad of [null, [], { pile: [] }, { column: { mainCount: 2.5 } }, { pile: { coverMm: -1 } }, { connection: { mode: ['continuous'] } }, { column: { tieKind: 'unknown' } }, { column: { mainCount: '46' } }, { pile: { extra: 1 } }]) {
it(`rejects malformed settings ${JSON.stringify(bad)} in saved draft`, () => {
const p = project(loadP1());
p.piers[0].draft!.castInPlaceRebar = bad as CastInPlaceRebar;
expect(() => parseProject(serializeProject(p))).toThrow('현장타설');
});
}
it('geometry length changes produce a warning without destroying concrete or preventing reopening', () => {
const input = { ...loadP1(), foundationType: '현장타설말뚝' as const, pileLengthMm: 10000, castInPlaceRebar: settings };
const issues: Issue[] = [];
expect(validateInput(input, issues)).toBe(true);
expect(issues.some(i => i.code === 'CIP01')).toBe(true);
expect(() => parseProject(serializeProject(project(input)))).not.toThrow();
});
it('has no numeric defaults and checks cage length and cover geometry', () => {
expect(castSettingsIssues({})).toEqual([]);
expect(castSettingsIssues({ pile: { cageLengthMm: 15000 } }, 10000)).toHaveLength(1);
expect(castSettingsIssues({ column: { coverMm: 1000 } }, 10000, 2000)).toHaveLength(1);
expect(castSettingsIssues({ pile: { mainCount: Infinity } })).toHaveLength(1);
});
});