import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import * as XLSX from 'xlsx'; import * as THREE from 'three'; import { it, expect } from 'vitest'; import { parseGroundModel } from '../src/core/ground-model'; import { parseWorkbook } from '../src/core/parseWorkbook'; import { buildPier } from '../src/core/geometry'; import { parseAbutments, toAbutInput } from '../src/core/abut/parse'; import { buildAbut } from '../src/core/abut/geometry'; import { buildMembers } from '../src/core/abut/members'; import { buildWings } from '../src/core/abut/wings'; import { buildAbutGroup } from '../src/view/abut-scene'; import { terrainGeometry } from '../src/view/terrain'; import { terrainMapGeometry } from '../src/view/terrain-map'; it('records terrain and map placement against the actual Okseong project', () => { const folder = '/mnt/c/Users/User/Downloads/NAVER WORKS'; const ground = parseGroundModel(readFileSync(path.join(folder, '옥성교 지형.json'), 'utf8')); const wb = XLSX.read(readFileSync(path.join(folder, '옥성교 데이터_261106.xlsx')), { type: 'buffer' }); const piers = parseWorkbook(wb).piers; const abuts = parseAbutments(wb, piers.map(p => p.name)).abutments; const pivot = buildPier(piers[0]).pivot, grid = ground.terrainGrid!; const terrain = terrainGeometry(grid, pivot), pos = terrain.getAttribute('position'); const layers = ground.mapLayers!.map(layer => { const g = terrainMapGeometry(layer, pivot, 0, grid); return { id: layer.id, corners: layer.survey_corners, vertices: g.getAttribute('position').count, triangles: g.getIndex()!.count / 3 }; }); const duplicates = abuts.map(rec => { const model = buildAbut(toAbutInput(rec)); const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape); const root = buildAbutGroup(model, members, buildWings(model, members, rec.wings, rec.wingDims), pivot, rec.groundEl, grid, rec.slab); const shared = buildAbutGroup(model, members, buildWings(model, members, rec.wings, rec.wingDims), pivot, rec.groundEl, grid, rec.slab, undefined, undefined, true); expect(shared.getObjectByName('ground')).toBeUndefined(); root.updateMatrixWorld(true); const meshes: object[] = []; root.getObjectByName('ground')?.traverse(obj => { if (!(obj instanceof THREE.Mesh)) return; const vertices = obj.geometry.getAttribute('position'); let maxDisplacement = 0; if (vertices.count === pos.count) for (let i = 0; i < vertices.count; i++) { const actual = new THREE.Vector3().fromBufferAttribute(vertices, i).applyMatrix4(obj.matrixWorld); const expected = new THREE.Vector3().fromBufferAttribute(pos, i); maxDisplacement = Math.max(maxDisplacement, actual.distanceTo(expected)); } meshes.push({ vertices: vertices.count, triangles: obj.geometry.getIndex()?.count! / 3, sameGrid: vertices.count === pos.count, maxDisplacementM: maxDisplacement }); }); const gx = (rec.ip2.n - grid.origin[0]) / grid.step, gy = (rec.ip2.e - grid.origin[1]) / grid.step; const i = Math.floor(gx), j = Math.floor(gy); const inside = i >= 0 && j >= 0 && i + 1 < grid.nx && j + 1 < grid.ny; const heights = inside ? [grid.z[j * grid.nx + i], grid.z[j * grid.nx + i + 1], grid.z[(j + 1) * grid.nx + i], grid.z[(j + 1) * grid.nx + i + 1]] : []; const valid = heights.length === 4 && heights.every(h => h !== null); const u = gx - i, v = gy - j; const sampledAtIp2 = valid ? heights[0]! * (1-u) * (1-v) + heights[1]! * u * (1-v) + heights[2]! * (1-u) * v + heights[3]! * u * v : null; return { abutment: rec.name, excelGroundEl: rec.groundEl, ip2: rec.ip2, modelEntry: ground.piers.some(p => p.id === rec.name), ip2GridSample: sampledAtIp2, meshes }; }); const values = grid.z.filter((v): v is number => v !== null); const report = { grid: { origin: grid.origin, step: grid.step, nx: grid.nx, ny: grid.ny, valid: values.length, total: grid.z.length, min: Math.min(...values), max: Math.max(...values) }, terrain: { vertices: pos.count, triangles: terrain.getIndex()!.count / 3 }, layers, duplicates }; const out = path.resolve('.verify/terrain-review-20261109'); mkdirSync(out, { recursive: true }); writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2)); console.log(JSON.stringify(report)); expect(layers.every(layer => layer.triangles === report.terrain.triangles)).toBe(true); expect(duplicates.every(d => d.meshes.every(m => (m as { maxDisplacementM: number }).maxDisplacementM < 0.001))).toBe(true); });