Files
Pier_v1/tests/abut-blocks-slab.test.ts
T
ypshimandClaude Haiku 5.5 686d87bad9 fix(abut): 연직 헌치를 날개벽 내측면(두 날개벽 사이 면) 위에 놓음
- 헌치 모서리를 축 끝에서 날개벽 두께(0.45 m)만큼 안쪽, 즉 내측면으로 옮김. 이전에는 날개벽 위치와 겹쳤음
- 헌치는 내측면에서 중앙 방향으로만 0.3 m 뻗음
- 테스트: 헌치 정점이 날개벽 내측면의 중앙 쪽에 있음 (이전 배치에서는 실패)
- 전체 테스트 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

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

130 lines
7.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 { 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);
}
});
it('the 헌치 sits on the 날개벽 내측면 (the face between the two wings), on the centre side, and does not enter the wing', () => {
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 };
};
// Inner face of each wing: s = sEnd + a·0.45 + (ws/wt)·t (a = +1 at IP1, −1 at IP3). Haunch vertices must lie on the centre side of it.
const inner = wings.map((w) => {
const p0 = axis(w.corners[0]);
const p1 = axis(w.corners[1]);
const a = w.sEnd === 0 ? 1 : -1;
return { sEnd: w.sEnd, a, k: (p1.s - p0.s) / (p1.t - p0.t), tRoot: p0.t };
});
for (const h of (named('haunches') as THREE.Group).children) {
const pos = (h as THREE.Mesh).geometry.getAttribute('position');
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
const w = x < model.length / 2 ? inner[0] : inner[1];
const sIn = w.sEnd + w.a * 0.45 + w.k * (z - w.tRoot);
expect(w.a * (x - sIn)).toBeGreaterThanOrEqual(-1e-6);
}
}
});
});