Files
Pier_v1/tests/abut-scene.test.ts
T
ypshimandClaude Haiku 5.5 43edcdc3c5 fix(abut): 흉벽과 벽체가 간섭하지 않도록 벽체 배면 단차를 나눔
- 벽체를 전면부(t −0.5 … 1.6, 교좌면 단차까지)와 배면 단차부(t −0.8 … −0.5, 브라켓 상면까지)로 나눔. 배면 단차 위가 접속슬래브 받침 (흉벽 상단 − 0.85)
- 흉벽은 교좌면 단차 위(t −0.5 … 0)에서 흉벽 상단까지. 벽체와 겹치지 않음
- 배면은 연직 단차 없이 같은 면(−0.8, −0.5)을 따라 올라가고, 바뀌는 곳은 수평 단차 1개
- 테스트: 벽체와 흉벽 사이 공통 체적 0, 벽체 배면 위치. 전체 636건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

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

135 lines
6.5 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 { abutPlacement, buildAbutGroup, cameraForBox, covers } from '../src/view/abut-scene';
import type { TerrainGrid } from '../src/core/ground-model';
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const input = toAbutInput(parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0]);
const GROUND_EL = 33.984; // 운곡교 A1 지반고 (입력 파일, spec_abut §10.1)
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 count = (name: string) => (group.getObjectByName(name) as THREE.Group | undefined)?.children.length ?? 0;
describe('교대 A1 3D 장면 (AA3·AA3b·AA3c)', () => {
it('has the members: 5 wall segments (front + back ledge), 5 parapet parts, 1 footing, 18 piles, 2 wings, 4 haunches', () => {
expect(count('wall')).toBe(10); // 5 front parts + 5 back ledges (도면 type-1)
expect(count('parapet')).toBe(5); // one per 교좌면 step, from the step up
expect(count('footing')).toBe(1);
expect(count('piles')).toBe(18);
expect(count('wings')).toBe(2);
expect(count('haunches')).toBe(4);
});
it('spans from the pile tip (19.295 m) up to the highest parapet end (47.18 m)', () => {
const box = new THREE.Box3().setFromObject(group);
expect(box.min.y).toBeCloseTo(19.295, 2);
expect(box.max.y).toBeGreaterThan(47.0);
expect(box.max.y).toBeLessThan(48.0);
});
it('spans the axis from IP1 (s = 0) to IP3 (s = L) along x', () => {
const box = new THREE.Box3().setFromObject(group);
expect(box.min.x).toBeLessThanOrEqual(0.01);
expect(box.max.x).toBeGreaterThanOrEqual(model.length - 0.01);
});
it('a group without foundation (no footing or piles) still builds', () => {
const bare = buildAbutGroup(model, buildMembers(model), buildWings(model, buildMembers(model)));
expect((bare.getObjectByName('footing') as THREE.Group | undefined)).toBeUndefined();
expect(bare.getObjectByName('wall')).toBeDefined();
});
});
describe('교대 배치 (교각 장면과 같은 좌표계)', () => {
const pivot = { e: 179138.0886505, n: 259865.6772855 };
it('the IP3 end maps to the pier-scene position of IP3 (E − pivot.E, EL, −(N − pivot.N))', () => {
const v = new THREE.Vector3(model.length, 0, 0).applyMatrix4(abutPlacement(model, pivot));
expect(v.x).toBeCloseTo(input.ip3.e - pivot.e, 4);
expect(v.z).toBeCloseTo(-(input.ip3.n - pivot.n), 4);
});
it('the IP1 end maps to the pier-scene position of IP1', () => {
const v = new THREE.Vector3(0, 0, 0).applyMatrix4(abutPlacement(model, pivot));
expect(v.x).toBeCloseTo(model.ip1.e - pivot.e, 4);
expect(v.z).toBeCloseTo(-(model.ip1.n - pivot.n), 4);
});
it('a placed group keeps the local parts: the parapet end at IP3 stays at 46.835 m', () => {
const placed = buildAbutGroup(model, members, wings, pivot);
const box = new THREE.Box3().setFromObject(placed);
expect(box.max.y).toBeGreaterThan(47.0);
expect(placed.matrixAutoUpdate).toBe(true);
placed.updateMatrixWorld(true);
// The placed group must sit at IP3 in the pier frame (x = E − pivot.E, z = −(N − pivot.N)).
const ip3 = new THREE.Vector3(input.ip3.e - pivot.e, 0, -(input.ip3.n - pivot.n));
const boxAtIp3 = new THREE.Box3().setFromObject(placed);
expect(boxAtIp3.containsPoint(new THREE.Vector3(ip3.x, 40, ip3.z))).toBe(true);
});
});
describe('교대 지반 (지반고 pad)', () => {
const pivot = { e: 179138.0886505, n: 259865.6772855 };
it('the ground pad sits at 지반고 33.984 m and covers the axis and the wings', () => {
const g = buildAbutGroup(model, members, wings, pivot, GROUND_EL);
const ground = g.getObjectByName('ground') as THREE.Group;
expect(ground).toBeDefined();
const box = new THREE.Box3().setFromObject(ground);
expect(box.min.y).toBeCloseTo(GROUND_EL - 0.025, 3);
expect(box.max.y).toBeCloseTo(GROUND_EL + 0.025, 3);
// Covers the axis length plus the margin on both ends (x is the axis direction).
expect(box.max.x - box.min.x).toBeGreaterThan(model.length);
});
it('without a 지반고 there is no ground pad', () => {
const g = buildAbutGroup(model, members, wings, pivot);
expect(g.getObjectByName('ground')).toBeUndefined();
});
});
describe('교대 지형 모델', () => {
const pivot = { e: 179138.0886505, n: 259865.6772855 };
// A flat terrain grid at 33.984 m: N (origin[0]) 259800…259899, E (origin[1]) 179100…179199, 1 m step.
const grid: TerrainGrid = { origin: [259800, 179100], step: 1, nx: 100, ny: 100, z: Array(100 * 100).fill(33.984) };
const small: TerrainGrid = { origin: [259800, 179100], step: 1, nx: 10, ny: 10, z: Array(100).fill(33.984) };
it('covers() is true when the footprint lies inside the grid, false outside', () => {
expect(covers(grid, [model.ip1, { e: model.ip1.e, n: model.ip1.n }], 2)).toBe(true);
expect(covers(small, [model.ip1], 2)).toBe(false);
});
it('a covering terrain replaces the flat pad', () => {
const g = buildAbutGroup(model, members, wings, pivot, GROUND_EL, grid);
const ground = g.getObjectByName('ground') as THREE.Group;
const mesh = ground.children[0] as THREE.Mesh;
expect(mesh.geometry.type).toBe('BufferGeometry');
});
it('a terrain that does not cover the abutment falls back to the flat pad', () => {
const g = buildAbutGroup(model, members, wings, pivot, 33.984, small);
const mesh = (g.getObjectByName('ground') as THREE.Group).children[0] as THREE.Mesh;
expect(mesh.geometry.type).toBe('BoxGeometry');
});
});
describe('교대 선택 시 카메라', () => {
it('the camera target is the centre of the abutment box and the camera stands back from it', () => {
const box = new THREE.Box3(new THREE.Vector3(0, 19, -2), new THREE.Vector3(12, 47, 2));
const cam = cameraForBox(box);
expect(cam.target.x).toBeCloseTo(6, 6);
expect(cam.target.y).toBeCloseTo(33, 6);
expect(cam.position.distanceTo(cam.target)).toBeGreaterThan(12);
});
});