import { describe, expect, it } from 'vitest'; import { buildPier, type BlockModel } from '../src/core/geometry'; import { bearingEdgeDistances, integratedCheck, PAIR_ECCENTRICITY_LIMIT_MM, pairEccentricities } from '../src/core/bearing-layout'; import { validatePier } from '../src/core/validate'; import type { Issue } from '../src/core/types'; import { loadP1 } from './helpers'; const model = buildPier(loadP1()); function block(no: number, row: '전열' | '후열', s: number): BlockModel { return { no, row, s, n: 0, k: 0, g: 0, bottom: 0, top: 0, padTop: 0, rowSkewDeg: 0 } as BlockModel; } describe('받침 배치 규칙 (승인 2026-10-26)', () => { it('C4 and C5 from the model: distances from each coping edge to the bearing centre (P1 ≈ 943 / 950 mm)', () => { const d = bearingEdgeDistances(model); expect(d).toHaveLength(10); const first = d.find((x) => x.no === 1 && x.row === '전열')!; expect(first.leftMm).toBeCloseTo(943, 0); expect(first.rightMm).toBeCloseTo(12000 - 943, 0); // the two ends of one bearing sum to the coping length for (const x of d) expect(x.leftMm + x.rightMm).toBeCloseTo(12000, 6); }); it('the front and back bearings of one number are about 85 mm apart on P1, so 일체 is allowed', () => { const pairs = pairEccentricities(model.blocks); expect(pairs).toHaveLength(5); for (const p of pairs) { expect(p.eccentricityMm).toBeGreaterThan(80); expect(p.eccentricityMm).toBeLessThan(90); expect(p.allowed).toBe(true); } }); it('an eccentricity above 100 mm forbids 일체 for that pair (분리 우선)', () => { expect(PAIR_ECCENTRICITY_LIMIT_MM).toBe(100); const pairs = pairEccentricities([block(1, '전열', 1.0), block(1, '후열', 1.12), block(2, '전열', 3), block(2, '후열', 3.09)]); expect(pairs.find((p) => p.frontNo === 1)!.allowed).toBe(false); expect(pairs.find((p) => p.frontNo === 2)!.allowed).toBe(true); }); it('pairs by order within each row, so 전열 1 pairs with 후열 6 on P1 (numbers run on across the rows)', () => { const pairs = pairEccentricities(model.blocks); expect(pairs[0]).toMatchObject({ frontNo: 1, backNo: 6 }); }); it('a bearing left without a partner (the rows have different counts) is not paired', () => { expect(pairEccentricities([block(1, '전열', 1), block(2, '전열', 3), block(6, '후열', 1.05)])).toHaveLength(1); }); }); describe('일체 배치 선택 (승인 2026-10-26)', () => { it('P1 allows 일체 because every pair is within 100 mm', () => { expect(integratedCheck(model.blocks)).toEqual({ allowed: true, overLimit: [] }); }); it('one pair over the limit blocks 일체 and lists that pair', () => { const r = integratedCheck([block(1, '전열', 1.0), block(1, '후열', 1.12)]); expect(r.allowed).toBe(false); expect(r.overLimit.map((p) => p.frontNo)).toEqual([1]); }); it('no pairs at all does not allow 일체', () => { expect(integratedCheck([]).allowed).toBe(false); }); it('the model keeps 분리 for now and says so (B01); a 일체 that is not allowed is refused with B02', () => { const input = loadP1(); const ok: Issue[] = []; validatePier({ ...input, bearingLayout: '일체' }, model, ok); expect(ok.map((i) => i.code)).toContain('B01'); const bad: Issue[] = []; const fake = { ...model, blocks: [block(1, '전열', 1.0), block(1, '후열', 1.12)] } as unknown as typeof model; validatePier({ ...input, bearingLayout: '일체' }, fake, bad); expect(bad.map((i) => i.code)).toContain('B02'); }); });