Files
Pier_v1/tests/validate-and-foundation.test.ts
ypshimandClaude Haiku 5.5 9e1e25f8fe feat(core,view): bent coping stage 3 — footings and piles in stretch frames
- Footings, blindings and piles on a bent axis are placed in their stretch's
  frame; 일체기초 is grouped per stretch, so a combined footing never crosses
  the kink (separate footings, each aligned with its stretch).
- Footings, blindings and piles are trimmed at the mitre plane like the coping.
- The stage-2 refusal for bent foundations is removed; the Level-half-width
  refusal for columns at the kink remains.
- Tests: T형 footing crossing the kink stays in stretch 0; 2주형 with columns
  on both sides gives one footing per stretch, with mitre clipping in the scene.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-22 09:38:01 +09:00

190 lines
8.3 KiB
TypeScript
Raw Permalink 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('E02 when front and back bearing counts differ', () => {
const p1 = loadP1();
const issues: Issue[] = [];
const bad = { ...p1, bearings: p1.bearings.filter((b) => !(b.row === '후열' && b.no === 10)) };
expect(validateInput(bad, issues)).toBe(false);
expect(issues.map((i) => i.code)).toContain('E02');
});
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 배치');
});
});