Files
Pier_v1/tests/kink-steps.test.ts
T
ypshimandClaude Haiku 5.5 9f336e2be4 fix(shape): 꺾임점 계단면을 미터면과 일치시킨다
계단 경계를 꺾임점에 맞춰도 각 구간의 계단면은 자기 축에 직각이고 미터면은 θ/2 기울어, 코핑 폭 안에서
±(B/2)·tan(θ/2) (P11 ±0.12 m)의 삼각형 조각이 남았다. 꺾임점의 계단면을 각 구간 프로파일에서 미터 퍼짐 + 10 mm만큼
잘려 나가는 쪽으로 옮겨, 구간 0은 앞 상면을, 구간 1은 뒤 상면을 미터면까지 가져가게 한다. 3D와 도면 투영이
모두 미터면으로 자르므로 둘 다 반영된다. 퍼짐이 블럭 반폭 이상이면 바꾸지 않는다.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-10 15:57:20 +09:00

118 lines
6.2 KiB
TypeScript
Raw 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 { 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);
});
});