From 398fa997e286264622a06b3ed5955ffbf088392a Mon Sep 17 00:00:00 2001 From: ypshim Date: Fri, 23 Oct 2026 09:24:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(view):=20=E2=91=A2=20rebar=20view=20at=205?= =?UTF-8?q?0%=20for=20members,=20ground=20and=20blinding;=20deeper=20rebar?= =?UTF-8?q?=20colour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/main.ts | 4 +++- src/view/rebar3d.ts | 7 +++++-- src/view/scene.ts | 19 +++++++++++++++++++ tests/rebar3d.test.ts | 25 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 122fd7e..1fbfa52 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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'); $('view-shape').hidden = stage !== 'shape'; $('view-plan').hidden = stage !== 'plan'; $('view-rebar').hidden = stage !== 'rebar'; diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index 515d38a..2bea896 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -33,6 +33,9 @@ const OTHER_COLOR = 0x8c8c8c; const MEMBER_COLORS: Record = { bearing: 0xe69f00, coping: 0x0072b2, column: 0x009e73, footing: 0xcc79a7, other: OTHER_COLOR }; const MEMBER_LABELS: Record = { 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) => { diff --git a/src/view/scene.ts b/src/view/scene.ts index a6b55d0..cb8b4ff 100644 --- a/src/view/scene.ts +++ b/src/view/scene.ts @@ -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. */ diff --git a/tests/rebar3d.test.ts b/tests/rebar3d.test.ts index 0d04b5d..178970d 100644 --- a/tests/rebar3d.test.ts +++ b/tests/rebar3d.test.ts @@ -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) => { + const out: Record = {}; + 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); + }); +});