feat(core): lateral coping step (Q1·Q2) and IP4 bend-point solve (spec §4.1)
- Front and back coping tops that differ by 5 mm or more in any segment are drawn as two coping parts split at the midline between the bearing rows. Tolerance 5 mm and midline position are the draft assumptions (Q1·Q2). - Scene extrudes each coping part across its own width. - solveIp4 computes IP4 as the intersection of the IP1→IP2 line with the line through IP3 turned by the bend angle (positive = counter-clockwise, sign convention to be confirmed). Bent coping geometry remains 미실시. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad59c0c74d
commit
d0fa9b94eb
4 files changed
+118
-26
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
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';
|
||||
@@ -59,17 +59,17 @@ describe('foundation types', () => {
|
||||
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.profile.some(([, z]) => Math.abs(z - dip) < 1e-9)).toBe(false);
|
||||
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.profile.some(([, z]) => Math.abs(z - dip) < 1e-9)).toBe(true);
|
||||
const outlineLow = Math.min(...two.profile.map(([, z]) => z));
|
||||
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.profile.filter(([, z]) => Math.abs(z - (m.levelBottom - 0.3)) < 1e-9);
|
||||
const dips = m.copingParts[0].profile.filter(([, z]) => Math.abs(z - (m.levelBottom - 0.3)) < 1e-9);
|
||||
expect(dips).toHaveLength(4);
|
||||
});
|
||||
|
||||
@@ -130,3 +130,61 @@ describe('일체기초 (spec §4.6)', () => {
|
||||
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('IP4 lies on the IP1→IP2 line and on the line through IP3 turned by the bend angle', () => {
|
||||
const ip1 = { e: 0, n: 0 };
|
||||
const ip2 = { e: 1, n: 0 };
|
||||
const ip3 = { e: 10, n: 2 };
|
||||
const ip4 = solveIp4(ip1, ip2, ip3, 10)!;
|
||||
expect(ip4.n).toBeCloseTo(0, 9);
|
||||
const rad = (10 * Math.PI) / 180;
|
||||
const d2 = { e: Math.cos(rad), n: Math.sin(rad) };
|
||||
expect(cross({ e: ip4.e - ip3.e, n: ip4.n - ip3.n }, d2)).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('bent coping is still reported as 미실시 by buildPier', () => {
|
||||
const p1 = loadP1();
|
||||
const ips = p1.ips.map((p) => (p.name === 'IP4' ? { ...p, angleDeg: 5 } : p));
|
||||
expect(() => buildPier({ ...p1, ips })).toThrow('미실시');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user