전·후열 받침 수가 같으면 받침을 번호 순서로 두어, 번호가 IP1 → IP3 반대로 매겨진 교각(산외교 P9: ① g 12.1, ③ g 1.3)에서 계단 경계가 거꾸로(9.42 → 4.02) 놓이고 코핑 상면이 틀렸다. 받침을 늘 g 순서로 정렬한다. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
134 lines
7.2 KiB
TypeScript
134 lines
7.2 KiB
TypeScript
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<typeof buildPier>) => m.stretches[1].base;
|
||
const gOf = (m: ReturnType<typeof buildPier>, 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<typeof buildPier>, 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<typeof buildPier>, 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);
|
||
});
|
||
});
|
||
|
||
describe('꺾임점 계단면 = 미터면 (2026-11-10)', () => {
|
||
// Top of a stepped profile at s: the last top point at or before s.
|
||
const topAt = (m: ReturnType<typeof buildPier>, profile: [number, number][], s: number) =>
|
||
profile.slice(0, profile.findIndex(([ps]) => ps > s + 1e-9)).filter(([, z]) => z > m.endBottom + 1e-9).pop()![1];
|
||
|
||
it('over the whole mitre spread, stretch 0 keeps the top before the kink and stretch 1 the top after it', () => {
|
||
const m = buildPier(p11());
|
||
const kink = kinkOf(m);
|
||
const spread = (m.copingB / 2) * Math.tan((8.767 * Math.PI) / 360);
|
||
// The back part carries the ⑥|⑦ step at the kink: ⑥ top (back segment 1) before, ⑦ top (segment 2) after.
|
||
expect(m.backSegments![2].s0).toBeCloseTo(kink, 9);
|
||
const before = m.backSegments![1].top, after = m.backSegments![2].top;
|
||
expect(after - before).toBeGreaterThan(0.5);
|
||
const backPart = (k: number) => {
|
||
const parts = m.copingParts.filter((p) => p.k === k);
|
||
return parts.find((p) => p.profile.some(([, z]) => Math.abs(z - after) < 1e-9))!;
|
||
};
|
||
// Stretch 0 (local s = g): just short of the far edge of the mitre spread, still the ⑥ top.
|
||
expect(topAt(m, backPart(0).profile, kink + spread - 1e-6)).toBeCloseTo(before, 9);
|
||
// Stretch 1 (local s = g − kink): just past the near edge of the spread, already the ⑦ top.
|
||
expect(topAt(m, backPart(1).profile, -spread + 1e-6)).toBeCloseTo(after, 9);
|
||
// Away from the kink the profiles are unchanged: the ⑦ top in stretch 0 a block past the kink.
|
||
expect(topAt(m, backPart(0).profile, kink + DIM.blockSize / 2 + 0.1)).toBeCloseTo(after, 9);
|
||
});
|
||
});
|
||
|
||
describe('받침 번호가 축 반대 방향인 교각 (2026-11-10)', () => {
|
||
it('steps are built in g order: bounds rise and each step top is its own bearing seat', () => {
|
||
// P1 with the bearing numbers reversed in each row (⑤ … ① along IP1 → IP3).
|
||
const p1 = (() => {
|
||
const wb = XLSX.read(readFileSync(fileURLToPath(new URL('../도면추출/samples/운곡교P1입력.xlsx', import.meta.url))), { type: 'buffer' });
|
||
return parseWorkbook(wb).piers.find((p) => p.name === 'P1')!;
|
||
})();
|
||
const renumber = (no: number) => (no <= 5 ? 6 - no : 16 - no);
|
||
const reversed = { ...p1, bearings: p1.bearings.map((b) => ({ ...b, no: renumber(b.no) })) };
|
||
const a = buildPier(p1), b = buildPier(reversed);
|
||
for (const m of [a, b]) m.segments.forEach((s) => expect(s.s1).toBeGreaterThan(s.s0));
|
||
expect(b.segments.map((s) => s.top)).toEqual(a.segments.map((s) => s.top));
|
||
expect(b.segments.map((s) => s.s0)).toEqual(a.segments.map((s) => s.s0));
|
||
});
|
||
});
|