Files
Pier_v1/tests/abut-scene.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

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 blocks, 10 parapet parts (2 per step), 1 footing, 18 piles, 2 wings, 4 haunches', () => {
expect(count('wall')).toBe(5); // one 벽체 block per 교좌면 step (삽도)
expect(count('parapet')).toBe(10); // per step: back strip (흉벽 배면 단차) and the 흉벽 (삽도)
expect(count('footing')).toBe(1);
expect(count('piles')).toBe(18);
expect(count('wings')).toBe(4); // upper and lower part per wing
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);
});
});