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>
This commit is contained in:
1 parent
e0e220f35c
commit
398fa997e2
4 files changed
+52
-3
No files matched your search
+3
-1
@@ -19,7 +19,7 @@ import { choose, defaultSummary, isSelectable, SHAPE_GROUPS, UNDEFINED_REASON, t
|
||||
import { parseProject, SCHEMA_VERSION, serializeProject, type ProjectFile, type ProjectInfo } from './core/project';
|
||||
import { clearBackup, readBackup, writeBackup, type Backup } from './view/backup';
|
||||
import type { Issue, PierInput, PierType } from './core/types';
|
||||
import { buildPierScene, viewPosition, type PartName, type PierScene, type ViewKind } from './view/scene';
|
||||
import { buildPierScene, setRebarView, viewPosition, type PartName, type PierScene, type ViewKind } from './view/scene';
|
||||
|
||||
/** One pier as shown in the list. `checked` is the multi-select state (시안 ①). */
|
||||
interface Pier extends PierValues {
|
||||
@@ -238,6 +238,7 @@ function setupScene(): void {
|
||||
scene.parts[key].visible = state.visible[key];
|
||||
}
|
||||
world.add(scene.root);
|
||||
setRebarView(scene, state.stage === 'rebar');
|
||||
state.scene = scene;
|
||||
setView(state.view);
|
||||
}
|
||||
@@ -746,6 +747,7 @@ function wireRebarPicking(): void {
|
||||
|
||||
function setStage(stage: 'shape' | 'plan' | 'rebar'): void {
|
||||
state.stage = stage;
|
||||
if (state.scene) setRebarView(state.scene, stage === 'rebar');
|
||||
$<HTMLElement>('view-shape').hidden = stage !== 'shape';
|
||||
$<HTMLElement>('view-plan').hidden = stage !== 'plan';
|
||||
$<HTMLElement>('view-rebar').hidden = stage !== 'rebar';
|
||||
|
||||
+5
-2
@@ -33,6 +33,9 @@ const OTHER_COLOR = 0x8c8c8c;
|
||||
const MEMBER_COLORS: Record<RebarMemberKey, number> = { bearing: 0xe69f00, coping: 0x0072b2, column: 0x009e73, footing: 0xcc79a7, other: OTHER_COLOR };
|
||||
const MEMBER_LABELS: Record<RebarMemberKey, string> = { bearing: '받침블럭', coping: '코핑', column: '기둥', footing: '확대기초', other: '기타' };
|
||||
const hex = (v: number) => `#${v.toString(16).padStart(6, '0')}`;
|
||||
/** Rebar is drawn a little deeper than the 6.7 colours so it reads through the half-transparent members (승인 2026-10-23). */
|
||||
const REBAR_SHADE = 0.8;
|
||||
const rebarColor = (v: number) => new THREE.Color(v).multiplyScalar(REBAR_SHADE);
|
||||
|
||||
const COLOR = {
|
||||
coping: 0xb4531f,
|
||||
@@ -66,7 +69,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
|
||||
geo.applyQuaternion(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize()));
|
||||
geo.translate((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2);
|
||||
geo.applyMatrix4(Ms[k]);
|
||||
const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.6, metalness: 0.1 });
|
||||
const mat = new THREE.MeshStandardMaterial({ color: rebarColor(color), roughness: 0.45, metalness: 0.1 });
|
||||
const mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.userData = { name, roleColor: color, dMm, member: memberOfBar(name) };
|
||||
group.add(mesh);
|
||||
@@ -459,7 +462,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
|
||||
};
|
||||
const setColorMode = (mode: RebarColorMode) => {
|
||||
for (const meshes of byName.values()) {
|
||||
for (const mesh of meshes) (mesh.material as THREE.MeshStandardMaterial).color.setHex(colorOf(mesh, mode));
|
||||
for (const mesh of meshes) (mesh.material as THREE.MeshStandardMaterial).color.copy(rebarColor(colorOf(mesh, mode)));
|
||||
}
|
||||
};
|
||||
const legend = (mode: RebarColorMode) => {
|
||||
|
||||
@@ -87,6 +87,7 @@ export function buildPierScene(m: PierModel): PierScene {
|
||||
side: THREE.DoubleSide,
|
||||
clippingPlanes,
|
||||
});
|
||||
material.userData.baseOpacity = opacity;
|
||||
parts[part].add(new THREE.Mesh(geometry, material));
|
||||
};
|
||||
|
||||
@@ -146,6 +147,24 @@ export function buildPierScene(m: PierModel): PierScene {
|
||||
return { root, parts, size };
|
||||
}
|
||||
|
||||
/** ③ rebar view: members and the ground at half opacity so the bars show through (승인 2026-10-23). */
|
||||
const REBAR_VIEW_ALPHA = 0.5;
|
||||
|
||||
/** Turns the ③ rebar view on or off. Off restores each part's own opacity. Depth writes stop while on, so bars behind a member stay visible. */
|
||||
export function setRebarView(scene: PierScene, on: boolean): void {
|
||||
for (const group of Object.values(scene.parts)) {
|
||||
group.traverse((o) => {
|
||||
if (!(o instanceof THREE.Mesh)) return;
|
||||
const mat = o.material as THREE.MeshStandardMaterial;
|
||||
const base = mat.userData.baseOpacity as number;
|
||||
mat.opacity = on ? REBAR_VIEW_ALPHA : base;
|
||||
mat.transparent = on || base < 1;
|
||||
mat.depthWrite = !on;
|
||||
mat.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type ViewKind = 'iso' | 'front' | 'side' | 'top';
|
||||
|
||||
/** Camera position for one of the four view presets, looking at the centre of the model. */
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
|
||||
@@ -119,3 +120,27 @@ describe('3D 철근 모델은 ③ 공통 설정 fck를 따른다', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user