import { describe, expect, it } from 'vitest'; import { castShape, castShapeIssues, castLowerZone } from '../src/core/cast-in-place-shape'; import { castRebarDefaults } from '../src/core/rebar/cast-in-place'; import { buildPier } from '../src/core/geometry'; import { changedKeys } from '../src/core/changes'; import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project'; import { loadP1 } from './helpers'; describe('2026-11-05 cast-in-place drawing and user decisions', () => { const input = { ...loadP1(), foundationType: '현장타설말뚝' as const, groundEl: 30, footingBottomEl: 10, pileLengthMm: 24000 }; it('uses ground level for the pile head and ground minus length for its tip', () => { const model = buildPier(input); expect(model.piles[0].top).toBe(30); expect(model.piles[0].bottom).toBe(6); expect(model.columns[0].bottom).toBe(30); expect(model.footings).toEqual([]); expect(model.derived.soilCover).toBe(0); }); it('uses the clarified positive rock offsets, 9000 mm beta and casing defaults', () => { expect(castShape(input)).toMatchObject({ pileBottomEl: 6, betaLengthMm: 9000, betaEl: 21, weatheredRockEl: 11, bedrockEl: 7, casingOn: false, casingTopEl: 33, casingBottomEl: 7, clayBottomEl: undefined }); }); it('rejects a pile head above the coping instead of generating an inverted column', () => { expect(() => buildPier({ ...input, groundEl: 100 })).toThrow('지반고'); }); it('rejects zero or negative cast-in-place pile length', () => { for (const pileLengthMm of [0, -1]) expect(() => buildPier({ ...input, pileLengthMm })).toThrow('말뚝 길이'); }); it('keeps automatic values linked to geometry changes and manual overrides fixed', () => { const auto = castShape({ ...input, groundEl: 40 }); expect(auto.bedrockEl).toBe(17); expect(auto.casingBottomEl).toBe(17); expect(auto.casingTopEl).toBe(43); const manual = castShape({ ...input, groundEl: 40, castInPlaceShape: { bedrockEl: -2, casingTopEl: 35, weatheredRockEl: 3 } }); expect(manual.casingBottomEl).toBe(-2); expect(manual.casingTopEl).toBe(35); expect(manual.weatheredRockEl).toBe(3); }); it('requires independent clay EL and reports reversed lower zone instead of silently swapping it', () => { expect(castLowerZone(input)).toBeNull(); expect(castLowerZone({ ...input, castInPlaceShape: { clayBottomEl: 16 } })).toEqual({ bottomEl: 15, topEl: 18, valid: true }); expect(castLowerZone({ ...input, castInPlaceShape: { clayBottomEl: 4 } })?.valid).toBe(false); }); it('inherits both round-column main layers, hoop spacing, cover convention and couplers', () => { const s = castRebarDefaults(input); expect(s.column).toMatchObject({ mainDiameterMm: 31.8, innerMainDiameterMm: 31.8, mainCount: 46, innerMainCount: 46, coverMm: 125, coverReference: 'main-center', tieSpacingMm: 300, endZoneSpacingMm: 120, tieKind: 'hoop' }); expect(s.pile?.endCoverMm).toBe(250); expect(s.connection).toEqual({ mode: 'coupler', couplerLengthMm: 150 }); }); it('preserves old explicit clear cover and independent edits', () => { const s = castRebarDefaults({ ...input, columnRebar: { nDiv: 48, c: 140 }, castInPlaceRebar: { column: { coverMm: 100, mainCount: 44 }, pile: { endCoverMm: 300 } } }); expect(s.column?.coverReference).toBe('clear'); expect(s.column?.coverMm).toBe(100); expect(s.column?.mainCount).toBe(44); expect(s.pile?.coverMm).toBe(140); expect(s.pile?.innerMainCount).toBe(48); expect(s.pile?.endCoverMm).toBe(300); }); it('persists explicit shape values without turning auto defaults into fixed values', () => { const next = { ...input, castInPlaceShape: { casingOn: true, clayBottomEl: 16, betaLengthMm: 10000 } }; const p: ProjectFile = { schemaVersion: SCHEMA_VERSION, project: { name: '', bridge: '', client: '', author: '', crs: '', memo: '' }, source: { fileName: '', sheets: [] }, history: [], piers: [{ id: 'P1', input: next, defaults: input, draft: next }] }; expect(parseProject(serializeProject(p))).toEqual(p); expect(changedKeys(input, next)).toEqual(['castInPlaceShape']); const invalid = JSON.parse(serializeProject(p)); invalid.piers[0].draft.castInPlaceShape.betaLengthMm = 0; expect(() => parseProject(JSON.stringify(invalid))).toThrow('현장타설'); }); it('permits negative elevations but rejects nonfinite elevations and invalid switches', () => { expect(castShapeIssues({ bedrockEl: -20, casingBottomEl: -30 })).toEqual([]); expect(castShapeIssues({ casingOn: 1 })).toHaveLength(1); expect(castShapeIssues({ clayBottomEl: Infinity })).toHaveLength(1); }); });