135 lines
6.6 KiB
TypeScript
135 lines
6.6 KiB
TypeScript
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, 2 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(2); // per wing: the 저판 상면 헌치
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|