공용 파일(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>
65 lines
4.5 KiB
TypeScript
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);
|
|
});
|