Files
Pier_v1/scripts/terrain-review.test.ts
T
ypshimandClaude Haiku 5.5 fc520ec03d feat: 프로젝트 공통 지반모델·교대 지반 통합·재료표/수량 진입·옥성교 랜딩 히어로 (11-06~11-09 작업분)
공용 파일(main.ts·index.html·style.css)에서 서로 얽혀 있어 한 커밋으로 묶음.
- 지반: ① 형상 프로젝트 지반모델 관리창(생성·JSON 불러오기·교각별 비교·선택 적용),
  프로젝트 JSON 내장 groundModel 추출, 교대 8행 통합 적용·공통지형 중복격자 제외
- 재료표·수량: 상단 ⑤ 메뉴·배근 바로가기, 결과 교각/탭 기억, 수량 워크북·정합 보정
- 랜딩: 옥성교 6교각 히어로 이미지·투영 강조점·좌→우 광선·지형 라운드/도로 연결
- 검증 스크립트(verify-project-ground/ground-abutments/okseong-user-files/quantity-*/landing-hero 등)
정본: DOC/04_실행/실행_*_2026-11-09_v0.1.00.md

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-09 18:01:02 +09:00

65 lines
4.5 KiB
TypeScript

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);
});