Files
Pier_v1/tests/validate-and-foundation.test.ts
T

216 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { buildPier, solveIp4 } from '../src/core/geometry';
import { validateInput, validatePier } from '../src/core/validate';
import type { Issue, PierInput } from '../src/core/types';
import { loadP1 } from './helpers';
function run(input: PierInput): Issue[] {
const issues: Issue[] = [];
if (validateInput(input, issues)) validatePier(input, buildPier(input), issues);
return issues;
}
describe('validation (spec §10)', () => {
it('P1 raises I01 (block 100 mm < cover + bar 119 mm) and no E/W', () => {
const issues = run(loadP1());
expect(issues.map((i) => i.code)).toContain('I01');
expect(issues.filter((i) => i.level !== 'I')).toEqual([]);
});
it('E01 when the pier type and column count disagree', () => {
const issues = run({ ...loadP1(), type: '2주형' });
expect(issues.map((i) => i.code)).toContain('E01');
});
it('allows different front/back counts with a non-blocking notice', () => {
const p1 = loadP1();
const issues: Issue[] = [];
const bad = { ...p1, bearings: p1.bearings.filter((b) => !(b.row === '후열' && b.no === 10)) };
expect(validateInput(bad, issues)).toBe(true);
expect(issues).toContainEqual(expect.objectContaining({code:'W09',level:'W'}));
const m=buildPier(bad);
expect(m.bearings).toHaveLength(9);
expect(m.blocks).toHaveLength(9);
expect(m.backSegments).toHaveLength(4);
expect(m.copingParts.every(p=>p.profile.flat().every(Number.isFinite))).toBe(true);
});
it.each(['전열','후열'] as const)('keeps all bearings when %s has more entries, even with integrated layout selected', row => {
const p=loadP1();
const input={...p,bearingLayout:'일체' as const,bearings:p.bearings.filter(b=>!(b.row===row && b.no===(row==='전열'?1:10)))};
const m=buildPier(input);
expect(m.blocks).toHaveLength(input.bearings.length);
expect(m.integratedBlocks).toHaveLength(0);
expect(m.derived.frontBackDiffSpread).toBeGreaterThanOrEqual(0);
expect(m.copingParts.every(p=>p.profile.flat().every(Number.isFinite))).toBe(true);
});
it('allows a single bearing in one row but rejects a missing row', () => {
const p=loadP1();
const input={...p,bearings:p.bearings.filter(b=>b.row==='후열'||b.no===1)};
expect(buildPier(input).bearings).toHaveLength(6);
const missing={...p,bearings:p.bearings.filter(b=>b.row==='전열')};
const issues:Issue[]=[];
expect(validateInput(missing,issues)).toBe(false);
expect(issues).toContainEqual(expect.objectContaining({code:'E02',level:'E'}));
expect(()=>buildPier(missing)).toThrow('각각 받침');
});
it('W01 when IP2 disagrees with the 1:9 recalculation by more than 1 mm', () => {
const p1 = loadP1();
const ips = p1.ips.map((p) => (p.name === 'IP2' ? { ...p, x: p.x + 0.002 } : p));
expect(run({ ...p1, ips }).map((i) => i.code)).toContain('W01');
});
});
describe('foundation types', () => {
it('직접기초: footing, no piles', () => {
const m = buildPier({ ...loadP1(), foundationType: '직접기초' });
expect(m.footings).toHaveLength(1);
expect(m.piles).toHaveLength(0);
});
it('직접기초: blinding concrete under the footing (Q4 추천안, 적용)', () => {
const m = buildPier({ ...loadP1(), foundationType: '직접기초' });
expect(m.blindings).toHaveLength(1);
expect(m.blindings[0].thickness).toBeCloseTo(0.1, 6);
});
it('말뚝기초: steel pipe pile 609.6 mm (Q5 추천안)', () => {
const m = buildPier(loadP1());
expect(m.piles.length).toBeGreaterThan(0);
expect(m.piles[0].diameter).toBeCloseTo(0.6096, 6);
});
it('2주형: underside between columns drops 300 below Level, T형 has no such point (Q3 추천안)', () => {
const t = buildPier(loadP1());
const dip = t.levelBottom - 0.3;
expect(t.copingParts[0].profile.some(([, z]) => Math.abs(z - dip) < 1e-9)).toBe(false);
const two = buildPier({ ...loadP1(), type: '2주형', columnCount: 2 });
expect(two.copingParts[0].profile.some(([, z]) => Math.abs(z - dip) < 1e-9)).toBe(true);
const outlineLow = Math.min(...two.copingParts[0].profile.map(([, z]) => z));
expect(outlineLow).toBeCloseTo(dip, 9);
});
it('3주형 builds with two gaps of haunches', () => {
const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3 });
const dips = m.copingParts[0].profile.filter(([, z]) => Math.abs(z - (m.levelBottom - 0.3)) < 1e-9);
expect(dips).toHaveLength(4);
});
it('현장타설말뚝: no footing, one pile of column diameter per column, tip = bottom EL − length', () => {
const m = buildPier({ ...loadP1(), foundationType: '현장타설말뚝' });
expect(m.footings).toHaveLength(0);
expect(m.piles).toHaveLength(1);
expect(m.piles[0].diameter).toBeCloseTo(2.0, 6);
expect(m.piles[0].bottom).toBeCloseTo(26.208 - 24, 6);
});
});
describe('bearing and block checks (spec §10 W03, W04)', () => {
it('P1 has no W03 or W04', () => {
const codes = run(loadP1()).map((i) => i.code);
expect(codes).not.toContain('W03');
expect(codes).not.toContain('W04');
});
it('W03 when a bearing centre is closer than half a block width to a step boundary', () => {
const m = buildPier(loadP1());
const boundary = m.segments[1].s0;
const bearings = m.bearings.map((b, k) => (k === 0 ? { ...b, g: boundary + 0.3 } : b));
const issues: Issue[] = [];
validatePier(loadP1(), { ...m, bearings }, issues);
expect(issues.map((i) => i.code)).toContain('W03');
});
it('W04 when a block footprint extends past the coping end', () => {
const m = buildPier(loadP1());
const blocks = m.blocks.map((b, k) => (k === 0 ? { ...b, g: 0.1 } : b));
const issues: Issue[] = [];
validatePier(loadP1(), { ...m, blocks }, issues);
expect(issues.map((i) => i.code)).toContain('W04');
});
});
describe('일체기초 (spec §4.6)', () => {
it('2주형 L 12000: columns 7200 apart share one footing of 17600 with 4 × 8 piles', () => {
const m = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 12000, foundationType: '말뚝기초' });
expect(m.footings).toHaveLength(1);
expect(m.footings[0].length).toBeCloseTo(17.6, 6);
expect(m.footings[0].s).toBeCloseTo(6.0, 6);
expect(m.blindings[0].length).toBeCloseTo(17.8, 6);
expect(m.piles).toHaveLength(32);
expect(m.derived.pileLayout).toBe('7@2314 × 3@2250 = 32본');
});
it('3주형 L 12000: three columns 3600 apart share one footing', () => {
const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 12000, foundationType: '직접기초' });
expect(m.footings).toHaveLength(1);
expect(m.footings[0].length).toBeCloseTo(17.6, 6);
});
it('2주형 with columns 18000 apart: separate footings of 10400', () => {
const m = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 30000, foundationType: '직접기초' });
expect(m.footings).toHaveLength(2);
expect(m.footings.every((f) => Math.abs(f.length - 10.4) < 1e-9)).toBe(true);
});
});
describe('횡방향 단차 (spec §4.3, Q1·Q2 추천안)', () => {
it('P1 has one coping part (no step)', () => {
expect(buildPier(loadP1()).copingParts).toHaveLength(1);
});
it('a 20 mm step in one segment splits the coping into two parts at the bearing-row midline', () => {
const p1 = loadP1();
const bearings = p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, z: b.z + 0.02 } : b));
const m = buildPier({ ...p1, bearings });
expect(m.copingParts).toHaveLength(2);
const [front, back] = m.copingParts;
expect(front.nTo).toBeCloseTo(back.nFrom, 9);
expect(front.nFrom).toBeCloseTo(-m.copingB / 2, 9);
expect(back.nTo).toBeCloseTo(m.copingB / 2, 9);
expect(front.nTo).toBeCloseTo((meanN(m, '전열') + meanN(m, '후열')) / 2, 9);
// Segment 2 (bearing no. 7) is the one raised by 20 mm on the back row only.
const topAt = (part: typeof front, s: number) => part.profile.find(([ps, pz]) => Math.abs(ps - s) < 1e-9 && pz > m.levelBottom)![1];
const s2 = m.segments[2].s0;
expect(topAt(back, s2) - topAt(front, s2)).toBeCloseTo(0.02, 6);
});
it('a 3 mm difference is below the 5 mm tolerance: no step', () => {
const p1 = loadP1();
const bearings = p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, z: b.z + 0.003 } : b));
expect(buildPier({ ...p1, bearings }).copingParts).toHaveLength(1);
});
});
function meanN(m: ReturnType<typeof buildPier>, row: '전열' | '후열'): number {
const rows = m.bearings.filter((b) => b.row === row);
return rows.reduce((sum, b) => sum + b.n, 0) / rows.length;
}
describe('꺾인 코핑 IP4 (spec §4.1)', () => {
const cross = (a: { e: number; n: number }, b: { e: number; n: number }) => a.e * b.n - a.n * b.e;
it('right (clockwise) bend of +10° puts IP4 on the IP1→IP2 line, with IP3 on the turned line', () => {
const rad = (10 * Math.PI) / 180;
const d2 = { e: Math.cos(rad), n: -Math.sin(rad) }; // heading east, turning right → south-east
const expectedIp4 = { e: 5, n: 0 };
const ip3 = { e: expectedIp4.e + 5 * d2.e, n: expectedIp4.n + 5 * d2.n };
const ip4 = solveIp4({ e: 0, n: 0 }, { e: 1, n: 0 }, ip3, 10)!;
expect(ip4.e).toBeCloseTo(5, 9);
expect(ip4.n).toBeCloseTo(0, 9);
});
it('zero angle has no IP4', () => {
expect(solveIp4({ e: 0, n: 0 }, { e: 1, n: 0 }, { e: 10, n: 2 }, 0)).toBeNull();
});
it('a bend on a straight P1 sheet (IP1, IP2, IP3 collinear) is refused: IP4 would land on IP3', () => {
const p1 = loadP1();
const ips = p1.ips.map((p) => (p.name === 'IP4' ? { ...p, angleDeg: 5 } : p));
expect(() => buildPier({ ...p1, ips })).toThrow('꺾임각과 IP 배치');
});
});