Files
Pier_v1/tests/abut-blocks-slab.test.ts
T
ypshimandClaude Haiku 5.5 7fab9b6685 fix(abut): 날개벽·헌치·접속슬래브가 벽체와 흉벽 안으로 들어가지 않도록
- 날개벽: 벽체 배면(t −0.8) 뒤로만 세움. 벽체·흉벽 앞쪽 부분은 잘라냄 (프로파일을 dM ≥ 벽체 배면 위치로 자름)
- 위 헌치: 브라켓 상면이 s 방향으로 기울므로, 헌치 면적 안에서 가장 높은 브라켓 상면 위에 놓음
- 접속슬래브: 흉벽 배면 단차 위(t ≥ 벽체 배면)는 브라켓 상면에 평평하게 얹고, 벽체 뒤쪽에서만 4 % 하향
- 테스트: 날개벽 t ≤ −0.8, 벽체 배면 위치, 위 헌치와 슬래브가 배면 단차 아래로 내려가지 않음. 전체 638건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

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

97 lines
5.3 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 날개벽 stays behind the 벽체 back face, so it does not enter the 벽체 or the 흉벽', () => {
// 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;
};
for (const w of (named('wings') as THREE.Group).children) expect(z(w)).toBeLessThanOrEqual(members.shape.wallBackM + 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);
}
});
});