216 lines
9.6 KiB
TypeScript
216 lines
9.6 KiB
TypeScript
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 배치');
|
||
});
|
||
});
|