Files
Pier_v1/tests/rebar3d.test.ts
T
ypshimandClaude Haiku 5.5 398fa997e2 feat(view): ③ rebar view at 50% for members, ground and blinding; deeper rebar colour
On ③ the members, the ground and the blinding concrete are drawn at half opacity without depth writes, so the bars show
through. Outside ③ each part returns to its own opacity. Rebar colours are shaded to 0.8 and slightly glossier so they
read through the members. Approved 2026-10-23.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 09:24:53 +09:00

147 lines
6.2 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 { 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('highlight dims the other bars and keeps the chosen one', () => {
rb.highlight('P3');
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.highlight(null);
expect((rb.byName.get('C1')![0].material as THREE.MeshStandardMaterial).opacity).toBe(1);
});
});
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 / B10-2: 18 each (9 levels × 2 faces), each three pieces; B11 60, B12 47', () => {
expect(rb.byName.get('B10-1')!.length).toBe(18 * 3);
expect(rb.byName.get('B10-2')!.length).toBe(18 * 3);
expect(rb.byName.get('B11')!.length).toBe(60 * 3);
expect(rb.byName.get('B12')!.length).toBe(47 * 3);
});
});
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);
});
});