- 받침블럭 배근 경로를 교대 3D 철근에 추가 (선택 · 색 기준 공통), 방위각 회전 반영 - 콘크리트 안 검사에 받침블럭 포함 (다리 묻힘 포함 모두 안) - 테스트: 블럭 5개 × 8본씩 3조각, 콘크리트 안 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
176 lines
8.7 KiB
TypeScript
176 lines
8.7 KiB
TypeScript
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);
|
||
});
|
||
});
|