import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import * as XLSX from 'xlsx'; import { parseWorkbook } from '../src/core/parseWorkbook'; import { buildPier, DIM, defaultLevelWidthMm } from '../src/core/geometry'; import type { PierInput } from '../src/core/types'; // 산외교 P11 (샘플01) with the coping bent 8.767° at IP4 (2026-11-10 검토): back ⑥ g 11.64, ⑦ g 18.49, kink g 15.14. const SAMPLE = fileURLToPath(new URL('../project_data/01_엑셀/샘플01_산외교_교각입력데이터.xlsx', import.meta.url)); function p11(): PierInput { const wb = XLSX.read(readFileSync(SAMPLE), { type: 'buffer' }); wb.Sheets['IP']['F46'] = { t: 'n', v: 8.767 }; const input = parseWorkbook(wb).piers.find((p) => p.name === 'P11')!; return { ...input, levelWidthMm: input.levelWidthMm ?? defaultLevelWidthMm(input.type, 2000) }; } const kinkOf = (m: ReturnType) => m.stretches[1].base; const gOf = (m: ReturnType, no: number) => m.bearings.find((b) => b.no === no)!.g; describe('꺾임점 근처 계단 경계 (A, 2026-11-10)', () => { it('a back boundary 0.07 m from the kink moves onto the kink (⑥|⑦)', () => { const m = buildPier(p11()); const kink = kinkOf(m); const own = (gOf(m, 6) + gOf(m, 7)) / 2; expect(Math.abs(own - kink)).toBeLessThan(0.1); expect(m.backSegments![2].s0).toBeCloseTo(kink, 9); expect(m.backSegments![1].s1).toBeCloseTo(kink, 9); }); it('boundaries far from the kink stay midway between their bearings', () => { const m = buildPier(p11()); expect(m.backSegments![1].s0).toBeCloseTo((gOf(m, 5) + gOf(m, 6)) / 2, 9); expect(m.backSegments![3].s0).toBeCloseTo((gOf(m, 7) + gOf(m, 8)) / 2, 9); m.segments.slice(1).forEach((seg) => expect(Math.abs(seg.s0 - kinkOf(m))).toBeGreaterThan(DIM.blockSize / 2)); }); it('no snap when a bearing would end up closer than half a block to the kink', () => { const input = p11(); const base = buildPier(input); // ⑦ 0.4 m past the kink, ⑥ 0.6 m before it: midpoint 0.1 m before the kink, but ⑦ is within half a block. const u1 = base.stretches[1].u; const u0 = base.stretches[0].u; const kink = kinkOf(base); const d7 = kink + 0.4 - gOf(base, 7); const d6 = kink - 0.6 - gOf(base, 6); const bearings = input.bearings.map((b) => b.no === 7 ? { ...b, x: b.x + d7 * u1.e, y: b.y + d7 * u1.n } : b.no === 6 ? { ...b, x: b.x + d6 * u0.e, y: b.y + d6 * u0.n } : b); const m = buildPier({ ...input, bearings }); const own = (gOf(m, 6) + gOf(m, 7)) / 2; expect(kinkOf(m) - own).toBeCloseTo(0.1, 3); expect(m.backSegments![2].s0).toBeCloseTo(own, 9); }); }); describe('전·후열 나눔선 하나 (B, 2026-11-10)', () => { const split = (m: ReturnType, k: number) => { const [front] = m.copingParts.filter((p) => p.k === k); return Math.abs(front.nFrom) < Math.abs(front.nTo) ? front.nFrom : front.nTo; }; const meanN = (m: ReturnType, row: string, k?: number) => { const rows = m.bearings.filter((b) => b.row === row && (k === undefined || b.k === k)); return rows.reduce((s, b) => s + b.n, 0) / rows.length; }; it('both stretches use the same line, the mean of the two rows over the whole pier (no jog at the kink)', () => { const m = buildPier(p11()); expect(m.copingParts).toHaveLength(4); const nAll = (meanN(m, '전열') + meanN(m, '후열')) / 2; expect(split(m, 0)).toBeCloseTo(nAll, 9); expect(split(m, 1)).toBeCloseTo(nAll, 9); // Per stretch the lines would differ by about 0.16 m. const per = (k: number) => (meanN(m, '전열', k) + meanN(m, '후열', k)) / 2; expect(Math.abs(per(0) - per(1))).toBeGreaterThan(0.1); }); it('falls back to a line per stretch when a bearing would be closer than half a block to the common line', () => { const input = p11(); const base = buildPier(input); const nrm = base.stretches[1].nrm; // Move back ⑦ towards the axis (+n side here is the front), to n ≈ +0.1. const b7 = base.bearings.find((b) => b.no === 7)!; const d = 0.1 - b7.n; const bearings = input.bearings.map((b) => (b.no === 7 ? { ...b, x: b.x + d * nrm.e, y: b.y + d * nrm.n } : b)); const m = buildPier({ ...input, bearings }); const nAll = (meanN(m, '전열') + meanN(m, '후열')) / 2; expect(m.bearings.some((b) => Math.abs(b.n - nAll) < DIM.blockSize / 2)).toBe(true); for (const k of [0, 1]) expect(split(m, k)).toBeCloseTo((meanN(m, '전열', k) + meanN(m, '후열', k)) / 2, 9); }); });