Files
Pier_v1/tests/abut-blocks-slab.test.ts
T
ypshimandClaude Haiku 5.5 3aa7cc06b6 fix(abut): 날개벽을 삽도대로 흉벽·벽체 배면에 맞닿게 나눔
- 날개벽을 상·하 두 부분으로 나눔. 브라켓 상면 위는 흉벽 배면(−0.5)에, 아래는 벽체 배면(−0.8)에 맞닿음 (삽도 A-A와 평면)
- 브라켓 상면이 s 방향으로 기울어 있으므로, 두께 0.45 m 안에서 가장 높은 브라켓 상면을 기준으로 자름
- 헌치는 기존 위치 유지: 위 헌치는 흉벽 배면 모서리, 아래 헌치는 벽체 배면 모서리
- 테스트 갱신: 날개벽 상부·하부 경계, 날개벽 개수 4. 전체 638건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-03 16:42:10 +09:00

105 lines
5.9 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 { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as XLSX from 'xlsx';
import * as THREE from 'three';
import { describe, expect, it } from 'vitest';
import { buildAbut } from '../src/core/abut/geometry';
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
import { buildMembers } from '../src/core/abut/members';
import { buildWings } from '../src/core/abut/wings';
import { buildAbutGroup } from '../src/view/abut-scene';
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
const input = toAbutInput(rec);
const model = buildAbut(input);
const members = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm });
const wings = buildWings(model, members);
const group = buildAbutGroup(model, members, wings);
const named = (name: string) => group.getObjectByName(name) as THREE.Group | undefined;
describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => {
it('has a block and a pad for each of the 5 bearings', () => {
expect(named('blocks')?.children).toHaveLength(2 * model.bearings.length);
});
it('draws the 접속슬래브 as one slab', () => {
expect(named('slab')?.children).toHaveLength(1);
});
it('the slab does not overlap either wing in plan (spec §4.8: 날개벽 안쪽면 + 20)', () => {
// Axis frame: s along IP1 → IP3, t positive to the 전면, as toAxis() in abut-scene.ts.
const axis = (p: { e: number; n: number }) => {
const d = { e: p.e - model.ip1.e, n: p.n - model.ip1.n };
const f = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide };
return { s: d.e * model.u.e + d.n * model.u.n, t: d.e * f.e + d.n * f.n };
};
const inside = (x: number, y: number, poly: { s: number; t: number }[]) => {
let c = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
if (poly[i].t > y !== poly[j].t > y && x < ((poly[j].s - poly[i].s) * (y - poly[i].t)) / (poly[j].t - poly[i].t) + poly[i].s) c = !c;
}
return c;
};
const pos = (named('slab') as THREE.Group).children[0] as THREE.Mesh;
const attr = pos.geometry.getAttribute('position');
const polys = wings.map((w) => w.corners.map(axis));
for (let i = 0; i < attr.count; i++) {
for (const poly of polys) expect(inside(attr.getX(i), attr.getZ(i), poly)).toBe(false);
}
});
it('the slab reaches 7 m behind the 흉벽 back face and is 450 thick', () => {
const box = new THREE.Box3().setFromObject(named('slab') as THREE.Group);
expect(box.min.z).toBeCloseTo(members.shape.parapetBackM - 0.005 - 7, 2);
expect(box.max.y - box.min.y).toBeGreaterThan(0.45);
});
it('the 흉벽 and the 벽체 only touch (no shared volume) and the back ledge is vertical-step free', () => {
const volume = (a: THREE.Object3D, b: THREE.Object3D) => {
const box = new THREE.Box3().setFromObject(a).intersect(new THREE.Box3().setFromObject(b));
return Math.max(0, box.max.x - box.min.x) * Math.max(0, box.max.y - box.min.y) * Math.max(0, box.max.z - box.min.z);
};
for (const w of (named('wall') as THREE.Group).children) {
for (const p of (named('parapet') as THREE.Group).children) expect(volume(w, p)).toBeLessThan(1e-9);
}
// back face: the ledge (t −0.8 … −0.5) and the 흉벽 back (−0.5) meet at one horizontal edge, not a vertical offset
const backs = (named('wall') as THREE.Group).children.map((w) => new THREE.Box3().setFromObject(w).min.z);
expect(Math.min(...backs)).toBeCloseTo(members.shape.wallBackM, 6);
});
it('the 날개벽 meets the 흉벽 back face above the 브라켓 and the 벽체 back face below it, and never enters them', () => {
// All 벽체 and 흉벽 parts lie at t ≥ 벽체 배면 (−0.8). A wing vertex with t ≤ −0.8 therefore has no volume in common with them.
const z = (g: THREE.Object3D) => {
const pos = (g as THREE.Mesh).geometry.getAttribute('position');
let max = -Infinity;
for (let i = 0; i < pos.count; i++) max = Math.max(max, pos.getZ(i));
return max;
};
// Upper part (above the 브라켓 상면) meets the 흉벽 back face (−0.5); the lower part meets the 벽체 back face (−0.8).
const backTop = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / model.length - members.shape.bracketHeightM;
for (const w of (named('wings') as THREE.Group).children) {
expect(z(w)).toBeLessThanOrEqual(members.shape.parapetBackM + 1e-6);
const pos = (w as THREE.Mesh).geometry.getAttribute('position');
for (let i = 0; i < pos.count; i++) {
if (pos.getZ(i) > members.shape.wallBackM + 1e-6) expect(pos.getY(i)).toBeGreaterThanOrEqual(backTop(pos.getX(i)) - 1e-6);
}
}
for (const w of (named('wall') as THREE.Group).children) expect(new THREE.Box3().setFromObject(w).min.z).toBeGreaterThanOrEqual(members.shape.wallBackM - 1e-6);
});
it('the upper 헌치 and the 접속슬래브 stay on the 브라켓 상면 over the back strip, not in it', () => {
const backTop = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / model.length - members.shape.bracketHeightM;
for (const h of (named('haunches') as THREE.Group).children) {
const b = new THREE.Box3().setFromObject(h);
if (b.max.z < members.shape.parapetBackM + 1e-6) continue; // lower 헌치, behind the 벽체
expect(b.min.y).toBeGreaterThanOrEqual(Math.max(backTop(b.min.x), backTop(b.max.x)) - 1e-6);
}
const pos = ((named('slab') as THREE.Group).children[0] as THREE.Mesh).geometry.getAttribute('position');
for (let i = 0; i < pos.count; i++) {
if (pos.getZ(i) >= members.shape.wallBackM) expect(pos.getY(i)).toBeGreaterThanOrEqual(backTop(pos.getX(i)) - 1e-6);
}
});
});