157 lines
6.8 KiB
TypeScript
157 lines
6.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import * as THREE from 'three';
|
||
import { buildPier } from '../src/core/geometry';
|
||
import { buildRebar3D } from '../src/view/rebar3d';
|
||
import { buildPierScene, setRebarView } from '../src/view/scene';
|
||
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
|
||
import { loadP1 } from './helpers';
|
||
|
||
const rb = buildRebar3D(buildPier(loadP1()));
|
||
|
||
describe('3D 철근 모델 (③)', () => {
|
||
it('every 철근표 family has bars, named as in the table', () => {
|
||
for (const n of ['C1', 'C4', 'C7', 'C9', 'C10', 'C11', 'C12', 'C13', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'B1', 'B4', 'B5', 'B6', 'B7', 'B8', 'S1', 'S2']) {
|
||
expect(rb.byName.get(n)?.length ?? 0, n).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
it('B1–B4 and B5–B8 all exist in 3D (B2 was missing)', () => {
|
||
for (const n of ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8']) {
|
||
expect(rb.byName.get(n)?.length ?? 0, n).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
it('B2 legs reach the B1 height (1873 above the footing bottom), B1 legs hang 304.8 below B1', () => {
|
||
const f = buildPier(loadP1()).footings[0];
|
||
const box = (name: string) => {
|
||
const b = new THREE.Box3();
|
||
for (const mesh of rb.byName.get(name)!) b.expandByObject(mesh);
|
||
return b;
|
||
};
|
||
expect(box('B2').max.y - f.bottom).toBeCloseTo(1.873, 2);
|
||
expect(box('B1').min.y - f.bottom).toBeCloseTo(1.873 - 0.3048, 2);
|
||
});
|
||
|
||
it('C12 / C13 in 3D: per row two 일자 and two ㄷ (body + 2 legs each end)', () => {
|
||
expect(rb.byName.get('C12')!.length).toBe(5 * (2 + 2 * 3));
|
||
expect(rb.byName.get('C13')!.length).toBe(2 * (2 + 2 * 3));
|
||
});
|
||
|
||
it('C1 legs turn down 472 at the ends (to EL 42.812 at the end face)', () => {
|
||
const model = buildPier(loadP1());
|
||
const b = new THREE.Box3();
|
||
for (const mesh of rb.byName.get('C1')!) b.expandByObject(mesh);
|
||
expect(b.min.y).toBeCloseTo(model.copingRefTop - 0.127 - 0.472, 2);
|
||
});
|
||
|
||
it('C9 / C10 / C11: three 1740-wide 폐합 per position, each a U plus a cap (18 pieces each)', () => {
|
||
expect(rb.byName.get('C10')!.length).toBe(9 * 18);
|
||
expect(rb.byName.get('C9')!.length).toBe(18 * 18);
|
||
expect(rb.byName.get('C11')!.length).toBe(18 * 18);
|
||
});
|
||
|
||
it('C7 / C8 follow the underside: lowest at the Level (+127 − r), vertical legs up to the C1 level', () => {
|
||
const model = buildPier(loadP1());
|
||
const b = new THREE.Box3();
|
||
for (const n of ['C7', 'C8']) for (const mesh of rb.byName.get(n)!) b.expandByObject(mesh);
|
||
const r = 28.6 / 2000;
|
||
expect(b.min.y).toBeCloseTo(model.levelBottom + 0.127 - r, 2);
|
||
// Vertical legs end flat, so no radius on top. C1 is the top row at copingRefTop − 0.127.
|
||
expect(b.max.y).toBeCloseTo(model.copingRefTop - 0.127, 2);
|
||
});
|
||
|
||
it('P1 has one bar per 원주 등분 (46) plus its outward hook tail, P2 one per 등분 (46)', () => {
|
||
expect(rb.byName.get('P1')!.length).toBe(46 * 2);
|
||
expect(rb.byName.get('P2')!.length).toBe(46);
|
||
});
|
||
|
||
it('P6 crossties in 3D: 8 levels × 4 bars each with main bar + 2 downward hooks (3 pieces per bar = 96 pieces)', () => {
|
||
expect(rb.byName.get('P6')!.length).toBe(8 * 4 * 3);
|
||
});
|
||
|
||
it('couplers in 3D: main bar couplers (368) and tie couplers (35: P5 19 + P4 16)', () => {
|
||
expect(rb.byName.get('주철근 커플러')!.length).toBe(368);
|
||
expect(rb.byName.get('띠철근 커플러')!.length).toBe(35);
|
||
});
|
||
|
||
it('show dims the other bars and keeps the chosen one', () => {
|
||
rb.show(new Set(['P3']), false);
|
||
const p3 = (rb.byName.get('P3')![0].material as THREE.MeshStandardMaterial).opacity;
|
||
const c1 = (rb.byName.get('C1')![0].material as THREE.MeshStandardMaterial).opacity;
|
||
expect(p3).toBe(1);
|
||
expect(c1).toBeLessThan(1);
|
||
rb.show(null, false);
|
||
expect((rb.byName.get('C1')![0].material as THREE.MeshStandardMaterial).opacity).toBe(1);
|
||
});
|
||
|
||
it('선택만 보기: with isolate on, only the selected names stay visible; several names can be selected', () => {
|
||
rb.show(new Set(['P3', 'C1']), true);
|
||
expect(rb.byName.get('P3')!.every((m) => m.visible)).toBe(true);
|
||
expect(rb.byName.get('C1')!.every((m) => m.visible)).toBe(true);
|
||
expect(rb.byName.get('C9')!.every((m) => !m.visible)).toBe(true);
|
||
rb.show(null, true);
|
||
expect(rb.byName.get('C9')!.every((m) => m.visible)).toBe(true);
|
||
rb.show(null, false);
|
||
});
|
||
});
|
||
|
||
describe('받침블럭 S1 · S2 in 3D (받침블럭철근.md §3–5)', () => {
|
||
it('per block: 8 S1 and 8 S2, each a bar with two 700 legs (3 pieces)', () => {
|
||
const blocks = buildPier(loadP1()).blocks.length;
|
||
expect(rb.byName.get('S1')!.length).toBe(blocks * 8 * 3);
|
||
expect(rb.byName.get('S2')!.length).toBe(blocks * 8 * 3);
|
||
});
|
||
});
|
||
|
||
describe('확대기초 B9 · B10 · B11 · B12 in 3D (확대기초철근 (1).md §7–9)', () => {
|
||
it('B9: 306 vertical bars (column zone removed), each three pieces (bar + two hook tails)', () => {
|
||
expect(rb.byName.get('B9')!.length).toBe(306 * 3);
|
||
});
|
||
|
||
it('B10-1: 16 (8 levels × 2 faces, lowest removed); B10-2: 16 (8 levels × 2 faces, lowest removed); B11 30, B12 24 at k = 8 — one tube per bar (bends rounded, 수정 2026-10-23)', () => {
|
||
expect(rb.byName.get('B10-1')!.length).toBe(16);
|
||
expect(rb.byName.get('B10-2')!.length).toBe(16);
|
||
expect(rb.byName.get('B11')!.length).toBe(30);
|
||
expect(rb.byName.get('B12')!.length).toBe(24);
|
||
});
|
||
});
|
||
|
||
describe('3D 철근 모델은 ③ 공통 설정 fck를 따른다', () => {
|
||
const model = buildPier(loadP1());
|
||
const height = (scene: ReturnType<typeof buildRebar3D>, name: string) => {
|
||
const b = new THREE.Box3();
|
||
for (const mesh of scene.byName.get(name)!) b.expandByObject(mesh);
|
||
return b.max.y - b.min.y;
|
||
};
|
||
|
||
it('C15 stair bars are shorter with a higher fck, as in the 철근표 (stepDevLength)', () => {
|
||
const base = height(buildRebar3D(model), 'C15');
|
||
const high = height(buildRebar3D(model, { ...DEFAULT_MATERIAL, fck: 35 }), 'C15');
|
||
expect(high).toBeLessThan(base - 0.05);
|
||
});
|
||
});
|
||
|
||
describe('③ 부재 반투명 (승인 2026-10-23)', () => {
|
||
const model = buildPier(loadP1());
|
||
const opacities = (scene: ReturnType<typeof buildPierScene>) => {
|
||
const out: Record<string, number[]> = {};
|
||
for (const [key, group] of Object.entries(scene.parts)) {
|
||
group.traverse((o) => {
|
||
if (o instanceof THREE.Mesh) (out[key] ??= []).push((o.material as THREE.MeshStandardMaterial).opacity);
|
||
});
|
||
}
|
||
return out;
|
||
};
|
||
|
||
it('members and the ground go to 50% on ③ and back to their own opacity off ③', () => {
|
||
const scene = buildPierScene(model);
|
||
const before = opacities(scene);
|
||
setRebarView(scene, true);
|
||
for (const key of ['coping', 'columns', 'footings', 'piles', 'blinding', 'blocks', 'pads', 'ground']) {
|
||
expect(opacities(scene)[key].every((o) => o === 0.5), key).toBe(true);
|
||
}
|
||
setRebarView(scene, false);
|
||
expect(opacities(scene)).toEqual(before);
|
||
});
|
||
});
|