Files
Pier_v1/tests/abut-scene.test.ts
T
ypshimandClaude Haiku 5.5 5e64a4f2ed feat(abut): 벽체 높이 H_w = 받침 ① 교좌면 ~ 흉벽 상단 연직 (기본 3326)
- 사용자 도면(2026-11-04): 받침 ① 위치에서 받침 ① 교좌면부터 흉벽 상단까지 연직 거리. 모든 교대 기본값 3326 (QA-4 가정 2500 대체)
- 입력 이름 "벽체 높이 · 받침 ① 기준 연직", ② 정면도에 받침 ① 기준선과 H_w 치수
- ③ 흉벽 철근표 대표 단면 = 받침 ① 위치 (P1–P3는 그 단면에서 정확히 계산)
- 테스트: 흉벽 상단 IP1 48.006 / IP3 47.661 등 + 0.826 반영, 교대흉벽철근.md §11 재현은 2500을 넣어 유지, 3326 기본 확인 (P1 3945)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-04 17:20:05 +09:00

171 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { setRebarViewOn } from '../src/view/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, 6 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(6); // per wing: upper and lower 연직 헌치, and the 저판 상면 헌치
});
it('spans from the pile tip (19.295 m) up to the highest parapet end (48.006 m)', () => {
const box = new THREE.Box3().setFromObject(group);
expect(box.min.y).toBeCloseTo(19.295, 2);
// 벽체 높이 H_w 3326 (받침 ① 기준, 2026-11-04): 이전 2500 기준 상한 47.0 ~ 48.0에서 + 0.826.
expect(box.max.y).toBeGreaterThan(47.826);
expect(box.max.y).toBeLessThan(48.826);
});
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 47.661 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);
});
});
describe('③ 배근 보기 반투명 (교각과 같음, 2026-11-04)', () => {
const g = buildAbutGroup(model, members, wings, undefined, GROUND_EL);
const mats = () => {
const out: THREE.MeshStandardMaterial[] = [];
g.traverse((o) => {
if (o instanceof THREE.Mesh) out.push(o.material as THREE.MeshStandardMaterial);
});
return out;
};
const ground = () => (g.getObjectByName('ground') as THREE.Group).children[0] as THREE.Mesh;
it('turns every member and the ground to half opacity with depth writes off', () => {
setRebarViewOn(g, true);
for (const m of mats()) {
expect(m.opacity).toBe(0.5);
expect(m.transparent).toBe(true);
expect(m.depthWrite).toBe(false);
}
});
it('restores each part: members opaque, the ground at 0.35 without depth writes', () => {
setRebarViewOn(g, false);
const groundMat = ground().material as THREE.MeshStandardMaterial;
for (const m of mats()) {
if (m === groundMat) continue;
expect(m.opacity).toBe(1);
expect(m.transparent).toBe(false);
expect(m.depthWrite).toBe(true);
}
expect(groundMat.opacity).toBe(0.35);
expect(groundMat.depthWrite).toBe(false);
});
});