feat(view): colour the ③ 3D rebar by diameter or by member, with a legend

Role colours stay the default (spec §6.7). A control on ③ switches to diameter (nominal size, H and S of one size
share a colour, Okabe–Ito palette) or member (받침블럭 · 코핑 · 기둥 · 확대기초). The legend lists only the items
the model has. Highlighting and the state colours are unchanged. Approved 2026-10-23.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-23 09:10:20 +09:00
1 parent 5d5ad7e997
commit d35e27e7de
6 files changed
+176 -3

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest';
import * as THREE from 'three';
import { buildPier } from '../src/core/geometry';
import { DIAMETER_GROUPS, diameterGroupIndex, memberOfBar } from '../src/core/rebar/color';
import { buildRebar3D } from '../src/view/rebar3d';
import { loadP1 } from './helpers';
const rb = buildRebar3D(buildPier(loadP1()));
const colorOf = (name: string) => (rb.byName.get(name)![0].material as THREE.MeshStandardMaterial).color.getHex();
describe('③ 3D 색 기준 (spec §6.7 승인 2026-10-23)', () => {
it('every bar of P1 belongs to a known member (none falls into 기타)', () => {
const names = [...rb.byName.keys()];
expect(names.filter((n) => memberOfBar(n) === 'other')).toEqual([]);
});
it('H and S of the same size share one diameter group; a non-rebar diameter has none', () => {
expect(DIAMETER_GROUPS[diameterGroupIndex(15.9)].label).toBe('H16 · S16');
expect(DIAMETER_GROUPS[diameterGroupIndex(25.4)].label).toBe('H25 · S25');
expect(DIAMETER_GROUPS).toHaveLength(8);
expect(diameterGroupIndex(60)).toBe(-1);
});
it('role is the default colour; diameter and member modes recolour every bar and can switch back', () => {
const role = colorOf('C1');
rb.setColorMode('member');
expect(colorOf('C1')).not.toBe(role);
rb.setColorMode('diameter');
expect(colorOf('C1')).not.toBe(role);
rb.setColorMode('role');
expect(colorOf('C1')).toBe(role);
});
it('member mode: all bars of one member share one colour', () => {
rb.setColorMode('member');
expect(colorOf('C1')).toBe(colorOf('C14'));
expect(colorOf('P1')).toBe(colorOf('P6'));
expect(colorOf('C1')).not.toBe(colorOf('P1'));
rb.setColorMode('role');
});
it('diameter mode: bars of one size share a colour, a different size does not (C1 and C7 are H29, C9 is H25)', () => {
rb.setColorMode('diameter');
expect(colorOf('C1')).toBe(colorOf('C7'));
expect(colorOf('C1')).not.toBe(colorOf('C9'));
rb.setColorMode('role');
});
it('legend lists only the items the model has; role mode has none', () => {
expect(rb.legend('role')).toEqual([]);
expect(rb.legend('member').map((i) => i.label)).toEqual(['받침블럭', '코핑', '기둥', '확대기초']);
const sizes = rb.legend('diameter').map((i) => i.label);
expect(sizes.length).toBeGreaterThan(0);
expect(sizes.length).toBeLessThanOrEqual(8);
});
});