Files
Pier_v1/tests/abut-rebar3d.test.ts
ypshimandClaude Haiku 5.5 91c4a4ad63 feat(abut-rebar): ③ 교대 받침블럭 3D 철근 (S1 · S2)
- 받침블럭 배근 경로를 교대 3D 철근에 추가 (선택 · 색 기준 공통), 방위각 회전 반영
- 콘크리트 안 검사에 받침블럭 포함 (다리 묻힘 포함 모두 안)
- 테스트: 블럭 5개 × 8본씩 3조각, 콘크리트 안

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

176 lines
8.7 KiB
TypeScript
Raw Permalink 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 { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing';
import { skewAngle } from '../src/core/abut/rebar/skew-layout';
import { abutSkewK } from '../src/view/abut-scene';
import { buildAbutRebar3D } from '../src/view/abut-rebar3d';
import { abutWallGeom, abutWallRebar } from '../src/core/abut/rebar/wall';
import { abutParapetGeom, abutParapetRebar } from '../src/core/abut/rebar/parapet';
import { abutWingGeom, abutWingRebar } from '../src/core/abut/rebar/wing';
import { buildWings } from '../src/core/abut/wings';
// 예제 A1 (교대확대기초철근.md §13).
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
const model = buildAbut(toAbutInput(rec));
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm });
const geom = abutFootingGeom(model.length, members.shape, skewAngle(rec.skewDeg));
const k = abutSkewK(model);
const back = members.shape.footingBackM;
const bottom = members.footingBottomEl!;
const build = (dir: 'skew' | 'perp') => {
const r = abutFootingRebar(geom, { dir });
return { r, scene: buildAbutRebar3D(geom, r, k, back, bottom) };
};
// Segments per bar: ⊓ / ⊔ have 3, the others 1.
const PIECES: Record<string, number> = { B1: 3, B2: 3, B5: 3, B6: 3 };
describe('교대 저판 3D 철근 (③, 2026-11-04)', () => {
for (const dir of ['skew', 'perp'] as const) {
it(`draws every 철근표 bar with its count (${dir})`, () => {
const { r, scene } = build(dir);
for (const b of r.bars) expect(scene.byName.get(b.mark)?.length, b.mark).toBe(b.count * (PIECES[b.mark] ?? 1));
});
it(`keeps every bar inside the 저판 (${dir})`, () => {
const { scene } = build(dir);
const top = bottom + geom.H / 1000;
const front = back + geom.W / 1000;
const box = new THREE.Box3();
for (const mesh of scene.group.children as THREE.Mesh[]) {
mesh.geometry.computeBoundingBox();
box.copy(mesh.geometry.boundingBox!);
const tMid = (box.min.z + box.max.z) / 2;
const s0 = k * tMid;
expect(box.min.y).toBeGreaterThan(bottom);
expect(box.max.y).toBeLessThan(top);
expect(box.min.z).toBeGreaterThan(back);
expect(box.max.z).toBeLessThan(front);
// Along the axis, within the skew ends at that t (a bar diameter of slack for the cylinder's sides).
expect(box.min.x).toBeGreaterThan(s0 + Math.min(0, k * (box.min.z - tMid), k * (box.max.z - tMid)) - 0.03);
expect(box.max.x).toBeLessThan(s0 + geom.L / 1000 + Math.max(0, k * (box.min.z - tMid), k * (box.max.z - tMid)) + 0.03);
}
});
}
it('dims or hides the bars not selected (선택만 보기)', () => {
const { scene } = build('skew');
scene.show(new Set(['B9']), true);
expect(scene.byName.get('B9')!.every((m) => m.visible)).toBe(true);
expect(scene.byName.get('B1')!.some((m) => m.visible)).toBe(false);
scene.show(null, false);
expect(scene.byName.get('B1')!.every((m) => m.visible)).toBe(true);
});
});
describe('교대 벽체 3D 철근 (③, 2026-11-04)', () => {
const footing = abutFootingRebar(geom);
const wGeom = abutWallGeom(model.length, members, skewAngle(rec.skewDeg));
const wall = abutWallRebar(wGeom, footing);
const scene = buildAbutRebar3D(geom, footing, k, back, bottom, wall);
it('draws every wall bar of the 철근표 (one tube per straight piece)', () => {
for (const b of wall.bars) {
const pieces = wall.paths.filter((p) => p.mark === b.mark).reduce((a, p) => a + p.pts.length - 1, 0);
expect(scene.byName.get(b.mark)?.length, b.mark).toBe(pieces);
}
expect(scene.byName.get('B1')?.length).toBeGreaterThan(0); // the 저판 bars stay
});
it('keeps the wall bars between the wall faces and the end faces, above the 저판 하면 철근망', () => {
const top = bottom + (wGeom.HF + Math.max(...wGeom.segments.map((s) => s.H)) + wall.ld.H19) / 1000;
const box = new THREE.Box3();
for (const b of wall.bars) {
for (const mesh of scene.byName.get(b.mark)!) {
mesh.geometry.computeBoundingBox();
box.copy(mesh.geometry.boundingBox!);
expect(box.min.z, b.mark).toBeGreaterThan(wGeom.xb / 1000 - 0.3); // 갈고리 296 바깥쪽까지
expect(box.max.z, b.mark).toBeLessThan(wGeom.xf / 1000 + 0.3);
expect(box.min.y, b.mark).toBeGreaterThan(bottom + footing.z.B68 / 1000);
expect(box.max.y, b.mark).toBeLessThan(top + 0.01);
const t = (box.min.z + box.max.z) / 2;
expect(box.min.x, b.mark).toBeGreaterThan(k * t - 0.05 - Math.abs(k) * 0.3);
expect(box.max.x, b.mark).toBeLessThan(k * t + geom.L / 1000 + 0.05 + Math.abs(k) * 0.3);
}
}
});
});
describe('교대 흉벽 · 브라켓 3D 철근 (③, 2026-11-04)', () => {
const footing = abutFootingRebar(geom);
const wGeom = abutWallGeom(model.length, members, skewAngle(rec.skewDeg));
const wall = abutWallRebar(wGeom, footing);
const pGeom = abutParapetGeom(wGeom, members.shape, model.bearings[0].s, { B: 11254, a: 300, b: 11554, nearT: -505, chamferA: 300, chamferB: 300 });
const para = abutParapetRebar(pGeom, wall);
const scene = buildAbutRebar3D(geom, footing, k, back, bottom, wall, para);
it('draws every 흉벽 bar of the 철근표 (one tube per straight piece), next to the 저판 · 벽체 bars', () => {
for (const b of para.bars) {
const pieces = para.paths.filter((p) => p.mark === b.mark).reduce((a, p) => a + p.pts.length - 1, 0);
expect(scene.byName.get(b.mark)?.length, b.mark).toBe(pieces);
}
expect(scene.byName.get('W1')?.length).toBeGreaterThan(0);
expect(scene.byName.get('B1')?.length).toBeGreaterThan(0);
});
it('keeps the 흉벽 bars between the bracket back and the 흉벽 전면, below the 흉벽 상단 (P10 into the slab by 300)', () => {
const topMax = bottom + Math.max(...pGeom.top) / 1000;
const box = new THREE.Box3();
for (const b of para.bars) {
for (const mesh of scene.byName.get(b.mark)!) {
mesh.geometry.computeBoundingBox();
box.copy(mesh.geometry.boundingBox!);
expect(box.min.z, b.mark).toBeGreaterThan(wGeom.xb / 1000);
expect(box.max.z, b.mark).toBeLessThan(0);
expect(box.max.y, b.mark).toBeLessThan(topMax + (b.mark === 'P10' ? 0 : -0.05));
}
}
});
});
describe('교대 날개벽 3D 철근 (③, 2026-11-04)', () => {
const footing = abutFootingRebar(geom);
const wingsR = buildWings(model, members).map((w) => abutWingRebar(abutWingGeom(model, members, w, footing.z.B68)));
const scene = buildAbutRebar3D(geom, footing, k, back, bottom, undefined, undefined, wingsR);
it('draws both wings: every WR bar (one tube per straight piece), 좌 + 우', () => {
expect(wingsR.length).toBe(2);
const marks = new Set(wingsR.flatMap((r) => r.bars.map((b) => b.mark)));
for (const m of marks) {
const pieces = wingsR.flatMap((r) => r.paths.filter((p) => p.mark === m)).reduce((a, p) => a + p.pts.length - 1, 0);
expect(scene.byName.get(m)?.length, m).toBe(pieces);
}
});
it('wing bars lie behind the 벽체 전면 and above the 저판 하면망', () => {
const box = new THREE.Box3();
for (const r of wingsR) for (const b of r.bars) for (const mesh of scene.byName.get(b.mark)!) {
mesh.geometry.computeBoundingBox();
box.copy(mesh.geometry.boundingBox!);
expect(box.max.z, b.mark).toBeLessThan(members.shape.wallFrontM);
expect(box.min.y, b.mark).toBeGreaterThan(bottom + footing.z.B68 / 1000 - (b.mark === 'WR6' ? 0.8 : 0.4));
}
});
});
import { abutBlockRebar } from '../src/core/abut/rebar/block';
describe('교대 받침블럭 3D 철근 (③, 2026-11-05)', () => {
it('S1 · S2: 블럭 5개 × 8본 = 40본씩, 본마다 다리 · 직선 · 다리 3조각 (120 조각)', () => {
const blockR = abutBlockRebar(
model.bearings.map((b) => ({ no: b.no, s: b.s, t: model.frontSide * b.lateral, rotDeg: b.rotDeg, seatEl: b.seatEl, blockHeightMm: b.blockHeightMm })),
bottom,
);
const footing = abutFootingRebar(geom);
const scene = buildAbutRebar3D(geom, footing, k, back, bottom, undefined, undefined, [], undefined, blockR);
expect(scene.byName.get('S1')?.length).toBe(120);
expect(scene.byName.get('S2')?.length).toBe(120);
});
});