import * as THREE from 'three'; import { describe, expect, it } from 'vitest'; import { buildPier } from '../src/core/geometry'; import type { TerrainGrid } from '../src/core/ground-model'; import { buildPierScene, frameMatrix } from '../src/view/scene'; import { terrainGeometry } from '../src/view/terrain'; import { loadP1 } from './helpers'; const model = buildPier(loadP1()); function grid(over: Partial): TerrainGrid { return { origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [30, 31, 32, 33], ...over }; } describe('지형 격자 → 장면 메시 (terrain.ts)', () => { it('a node maps to the same scene point as the pier frame (x = E − pivot E, z = −(N − pivot N))', () => { // A national-grid point on the first stretch: origin + 3·u − 2·nrm (the frame used by the pier scene). const st = model.stretches[0]; const q = { e: st.origin.e + 3 * st.u.e - 2 * st.nrm.e, n: st.origin.n + 3 * st.u.n - 2 * st.nrm.n }; const expected = new THREE.Vector3(3, 0, -2).applyMatrix4(frameMatrix(model)); const g = terrainGeometry(grid({ origin: [q.n, q.e], z: [0, 0, 0, 0] }), model.pivot); const p = new THREE.Vector3().fromBufferAttribute(g.getAttribute('position') as THREE.BufferAttribute, 0); expect(p.x).toBeCloseTo(expected.x, 6); expect(p.y).toBeCloseTo(expected.y, 6); expect(p.z).toBeCloseTo(expected.z, 6); }); it('the grid node at the pier centre sits at the scene origin, with the sampled EL as height', () => { const centre = grid({ origin: [model.pivot.n - 2.5, model.pivot.e - 2.5], step: 5, nx: 2, ny: 2, z: [28.9, 1, 1, 1] }); const g = terrainGeometry(centre, model.pivot); const p = new THREE.Vector3().fromBufferAttribute(g.getAttribute('position') as THREE.BufferAttribute, 0); expect(p.x).toBeCloseTo(-2.5, 9); expect(p.z).toBeCloseTo(2.5, 9); expect(p.y).toBeCloseTo(28.9, 5); // stored as Float32 }); it('a cell with a missing corner is not drawn; a full cell gives two triangles', () => { expect(terrainGeometry(grid({ z: [30, null, 32, 33] }), model.pivot).getIndex()!.count).toBe(0); expect(terrainGeometry(grid({}), model.pivot).getIndex()!.count).toBe(6); }); }); describe('장면에 지형을 넣은 뒤', () => { it('the camera size follows the members, not the extent of the terrain', () => { const small = grid({ origin: [model.pivot.e - 20, model.pivot.n - 20], step: 10, nx: 5, ny: 5, z: new Array(25).fill(28) }); const wide = grid({ origin: [model.pivot.e - 200, model.pivot.n - 200], step: 10, nx: 41, ny: 41, z: new Array(41 * 41).fill(28) }); const a = buildPierScene(model, small).size; const b = buildPierScene(model, wide).size; expect(b).toBeCloseTo(a, 6); expect(buildPierScene(model, wide).parts.ground.children.length).toBe(1); }); it('uses the pier ground pad when the imported grid has no complete terrain cells', () => { const empty = grid({ nx: 2, ny: 2, z: [null, null, null, null] }); const scene = buildPierScene(model, empty); expect(scene.parts.ground.children).toHaveLength(1); const pad = scene.parts.ground.children[0] as THREE.Mesh; expect(pad.geometry.getIndex()!.count).toBeGreaterThan(0); }); it('places the fallback ground pad at the imported pier ground elevation', () => { const scene = buildPierScene(model, undefined, 31.25); const pad = scene.parts.ground.children[0] as THREE.Mesh; pad.geometry.computeBoundingBox(); expect(pad.geometry.boundingBox!.getCenter(new THREE.Vector3()).y).toBeCloseTo(31.25, 5); }); it('does not draw a fallback ground pad when the imported pier has no terrain elevation', () => { expect(buildPierScene(model, undefined, null).parts.ground.children).toHaveLength(0); }); });