fix: improve 3D digital twin visual clarity

- Increase item visual scale ×2.5 with glow ring
- Move camera closer [4.0, 3.0, 4.0]
- Improve scene contrast (brighter conveyor, floor)
- Add glow to active routes
- Enhance gate/pusher visibility
- Add highlight rings to active zones
- Brighten sensors with detection effects

Based on REAL_3D_AUDIT_REPORT.md P0/P1 fixes.
This commit is contained in:
root
2026-07-04 19:48:57 +02:00
parent 29035dad5f
commit f9eb0819c0
9 changed files with 811 additions and 125 deletions

View File

@@ -15,70 +15,81 @@ export default function Actuator3D({ gate, actuators, machineState }: Props) {
// Gate vertical lift: closed (Y = 0.2) → open (Y = 0.65)
const gateY = gateOpen ? 0.65 : 0.2;
// Support post color - brighter for visibility
const postColor = '#7a8a9f';
return (
<group>
{/* Stop-gate — vertical lift mechanism */}
<group position={[TWIN_LAYOUT.gateX, TWIN_LAYOUT.beltY, 0]}>
{/* Gate support posts (left and right) */}
<mesh position={[0, 0.35, -0.35]}>
<boxGeometry args={[0.04, 0.7, 0.04]} />
<meshStandardMaterial color="#475569" metalness={0.3} roughness={0.7} />
{/* Gate support posts (left and right) - larger and brighter */}
<mesh position={[0, 0.38, -0.4]}>
<boxGeometry args={[0.06, 0.76, 0.06]} />
<meshStandardMaterial color={postColor} metalness={0.4} roughness={0.6} />
</mesh>
<mesh position={[0, 0.35, 0.35]}>
<boxGeometry args={[0.04, 0.7, 0.04]} />
<meshStandardMaterial color="#475569" metalness={0.3} roughness={0.7} />
<mesh position={[0, 0.38, 0.4]}>
<boxGeometry args={[0.06, 0.76, 0.06]} />
<meshStandardMaterial color={postColor} metalness={0.4} roughness={0.6} />
</mesh>
{/* Gate plate — moves up/down */}
{/* Cross bar at top */}
<mesh position={[0, 0.72, 0]}>
<boxGeometry args={[0.08, 0.05, 0.88]} />
<meshStandardMaterial color={postColor} metalness={0.4} roughness={0.6} />
</mesh>
{/* Gate plate — moves up/down - larger and brighter */}
<mesh position={[0, gateY, 0]}>
<boxGeometry args={[0.08, 0.05, 0.65]} />
<boxGeometry args={[0.1, 0.08, 0.72]} />
<meshStandardMaterial
color={fault ? '#fb3d4e' : gateOpen ? '#4ade80' : '#f87171'}
emissive={fault ? '#fb3d4e' : '#000000'}
emissiveIntensity={fault ? 0.4 : gateOpen ? 0.15 : 0}
emissive={fault ? '#fb3d4e' : gateOpen ? '#4ade80' : '#f87171'}
emissiveIntensity={fault ? 0.6 : gateOpen ? 0.4 : 0.3}
metalness={0.3}
roughness={0.5}
/>
</mesh>
</group>
{/* Pusher C → roll-cage C (extended plate) - brighter orange */}
<group position={[TWIN_LAYOUT.gateX - 0.1, TWIN_LAYOUT.beltY, 0]}>
{/* Pusher base (stationary) - visible base */}
<mesh position={[0, 0.12, 0.12]}>
<boxGeometry args={[0.2, 0.2, 0.15]} />
<meshStandardMaterial color="#b45309" roughness={0.7} metalness={0.2} />
</mesh>
{/* Pusher plate (moves) - larger and brighter */}
<mesh position={[0, 0.14, pusherC + 0.15]}>
<boxGeometry args={[0.35, 0.2, 0.25]} />
<meshStandardMaterial
color={actuators.pusherC === 'extended' ? '#fbbf24' : '#d97706'}
emissive={actuators.pusherC === 'extended' ? '#fbbf24' : '#d97706'}
emissiveIntensity={actuators.pusherC === 'extended' ? 0.6 : 0.2}
roughness={0.5}
metalness={0.2}
roughness={0.6}
/>
</mesh>
</group>
{/* Pusher C → roll-cage C (extended plate) */}
{/* Pusher D → roll-cage D (extended plate) - brighter purple */}
<group position={[TWIN_LAYOUT.gateX - 0.1, TWIN_LAYOUT.beltY, 0]}>
{/* Pusher base (stationary) */}
<mesh position={[0, 0.12, 0.1]}>
<boxGeometry args={[0.15, 0.18, 0.12]} />
<meshStandardMaterial color="#78350f" roughness={0.8} />
{/* Pusher base (stationary) - visible base */}
<mesh position={[0, 0.12, -0.12]}>
<boxGeometry args={[0.2, 0.2, 0.15]} />
<meshStandardMaterial color="#7c3aed" roughness={0.7} metalness={0.2} />
</mesh>
{/* Pusher plate (moves) */}
<mesh position={[0, 0.12, pusherC]}>
<boxGeometry args={[0.3, 0.15, 0.2]} />
{/* Pusher plate (moves) - larger and brighter */}
<mesh position={[0, 0.14, -pusherD - 0.15]}>
<boxGeometry args={[0.35, 0.2, 0.25]} />
<meshStandardMaterial
color={actuators.pusherC === 'extended' ? '#f59e0b' : '#92400e'}
emissive={actuators.pusherC === 'extended' ? '#f59e0b' : '#000000'}
emissiveIntensity={actuators.pusherC === 'extended' ? 0.35 : 0}
roughness={0.65}
/>
</mesh>
</group>
{/* Pusher D → roll-cage D (extended plate) */}
<group position={[TWIN_LAYOUT.gateX - 0.1, TWIN_LAYOUT.beltY, 0]}>
{/* Pusher base (stationary) */}
<mesh position={[0, 0.12, -0.1]}>
<boxGeometry args={[0.15, 0.18, 0.12]} />
<meshStandardMaterial color="#4c1d95" roughness={0.8} />
</mesh>
{/* Pusher plate (moves) */}
<mesh position={[0, 0.12, -pusherD]}>
<boxGeometry args={[0.3, 0.15, 0.2]} />
<meshStandardMaterial
color={actuators.pusherD === 'extended' ? '#c084fc' : '#581c87'}
emissive={actuators.pusherD === 'extended' ? '#c084fc' : '#000000'}
emissiveIntensity={actuators.pusherD === 'extended' ? 0.35 : 0}
roughness={0.65}
color={actuators.pusherD === 'extended' ? '#d8b4fe' : '#a78bfa'}
emissive={actuators.pusherD === 'extended' ? '#d8b4fe' : '#a78bfa'}
emissiveIntensity={actuators.pusherD === 'extended' ? 0.6 : 0.2}
roughness={0.5}
metalness={0.2}
/>
</mesh>
</group>

View File

@@ -5,40 +5,42 @@ interface Props {
}
export default function Conveyor3D({ stopped }: Props) {
const color = stopped ? '#7f1d1d' : '#1e3a54';
const beltColor = stopped ? '#991b1b' : '#334155';
const accColor = stopped ? '#7f1d1d' : '#0f766e';
const sideGuardColor = '#475569';
// Brighter colors for better visibility
const color = stopped ? '#7f1d1d' : '#3b5998';
const beltColor = stopped ? '#991b1b' : '#5a6577';
const accColor = stopped ? '#7f1d1d' : '#14b8a6';
const sideGuardColor = '#6b7a8f';
const rollerColor = '#8b9cb0';
return (
<group>
{/* Feed conveyor body */}
<mesh position={[0, TWIN_LAYOUT.beltY - 0.12, 0]} receiveShadow={false} castShadow={false}>
<boxGeometry args={[8.6, 0.18, 0.55]} />
<meshStandardMaterial color={color} roughness={0.85} metalness={0.15} />
<boxGeometry args={[8.6, 0.18, 0.58]} />
<meshStandardMaterial color={color} roughness={0.7} metalness={0.25} />
</mesh>
{/* Belt surface */}
<mesh position={[0, TWIN_LAYOUT.beltY - 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[8.4, 0.48]} />
<meshStandardMaterial color={beltColor} roughness={0.9} />
<planeGeometry args={[8.4, 0.5]} />
<meshStandardMaterial color={beltColor} roughness={0.75} />
</mesh>
{/* Conveyor side guards (borders) */}
<mesh position={[0, TWIN_LAYOUT.beltY + 0.05, 0.28]}>
<boxGeometry args={[8.4, 0.1, 0.03]} />
<meshStandardMaterial color={sideGuardColor} metalness={0.2} roughness={0.8} />
{/* Conveyor side guards (borders) - brighter for visibility */}
<mesh position={[0, TWIN_LAYOUT.beltY + 0.06, 0.3]}>
<boxGeometry args={[8.4, 0.12, 0.04]} />
<meshStandardMaterial color={sideGuardColor} metalness={0.3} roughness={0.7} />
</mesh>
<mesh position={[0, TWIN_LAYOUT.beltY + 0.05, -0.28]}>
<boxGeometry args={[8.4, 0.1, 0.03]} />
<meshStandardMaterial color={sideGuardColor} metalness={0.2} roughness={0.8} />
<mesh position={[0, TWIN_LAYOUT.beltY + 0.06, -0.3]}>
<boxGeometry args={[8.4, 0.12, 0.04]} />
<meshStandardMaterial color={sideGuardColor} metalness={0.3} roughness={0.7} />
</mesh>
{/* Rollers (visual indication of conveyor mechanism) — spaced every 1.2m */}
{[-3.6, -2.4, -1.2, 0, 1.2].map((x) => (
<mesh key={x} position={[x, TWIN_LAYOUT.beltY - 0.08, 0]} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[0.04, 0.04, 0.5, 8]} />
<meshStandardMaterial color="#334155" metalness={0.4} roughness={0.6} />
<cylinderGeometry args={[0.05, 0.05, 0.52, 12]} />
<meshStandardMaterial color={rollerColor} metalness={0.5} roughness={0.5} />
</mesh>
))}

View File

@@ -5,6 +5,13 @@ import { DIMENSION_LIMITS } from '../../domain/classifier';
import { getModelAsset } from '../../data/modelAssets';
import STLModel from './STLModel';
/**
* Visual scale multiplier for 3D demo visibility.
* Physical dimensions remain unchanged in data/proof panel.
* This only affects the rendered mesh size so item is visible in scene.
*/
const VISUAL_SCALE_MULTIPLIER = 2.5;
interface Props {
simulation: SimulationState;
currentItem?: SimulatedItem;
@@ -18,6 +25,7 @@ function FallbackPrimitive({
color,
emissive,
roughness,
emissiveIntensity = 0.35,
}: {
primitive: 'box' | 'cylinder' | 'sphere';
sx: number;
@@ -26,15 +34,16 @@ function FallbackPrimitive({
color: string;
emissive: string;
roughness: number;
emissiveIntensity?: number;
}) {
if (primitive === 'cylinder') {
return (
<mesh>
<cylinderGeometry args={[Math.min(sx, sz) / 2, Math.min(sx, sz) / 2, sy, 16]} />
<cylinderGeometry args={[Math.min(sx, sz) / 2, Math.min(sx, sz) / 2, sy, 24]} />
<meshStandardMaterial
color={color}
emissive={emissive}
emissiveIntensity={0.2}
emissiveIntensity={emissiveIntensity}
roughness={roughness}
/>
</mesh>
@@ -44,11 +53,11 @@ function FallbackPrimitive({
if (primitive === 'sphere') {
return (
<mesh>
<sphereGeometry args={[Math.max(sx, sy, sz) / 2, 16, 12]} />
<sphereGeometry args={[Math.max(sx, sy, sz) / 2, 24, 16]} />
<meshStandardMaterial
color={color}
emissive={emissive}
emissiveIntensity={0.2}
emissiveIntensity={emissiveIntensity}
roughness={roughness}
/>
</mesh>
@@ -62,7 +71,7 @@ function FallbackPrimitive({
<meshStandardMaterial
color={color}
emissive={emissive}
emissiveIntensity={0.2}
emissiveIntensity={emissiveIntensity}
roughness={roughness}
/>
</mesh>
@@ -79,19 +88,29 @@ export default function Item3D({ simulation, currentItem }: Props) {
const color = ROUTE_COLORS[category] ?? '#38bdf8';
const dims = currentItem.item.dimensionsMm;
// Scale to Three.js units (1 unit = 1 meter, dimensions in mm)
const sx = Math.max(0.12, Math.min(0.45, dims.width / 1000));
const sy = Math.max(0.1, Math.min(0.4, dims.height / 1000));
const sz = Math.max(0.12, Math.min(0.45, dims.depth / 1000));
// Physical scale to Three.js units (1 unit = 1 meter, dimensions in mm)
const physicalSx = Math.max(0.12, Math.min(0.45, dims.width / 1000));
const physicalSy = Math.max(0.1, Math.min(0.4, dims.height / 1000));
const physicalSz = Math.max(0.12, Math.min(0.45, dims.depth / 1000));
// Apply visual multiplier for demo visibility (does not affect proof panel data)
const sx = physicalSx * VISUAL_SCALE_MULTIPLIER;
const sy = physicalSy * VISUAL_SCALE_MULTIPLIER;
const sz = physicalSz * VISUAL_SCALE_MULTIPLIER;
const detecting = simulation.machineState === 'DETECTING';
const moving = simulation.machineState.startsWith('ROUTE_TO_') ||
simulation.machineState === 'MOVING_TO_CAMERA' ||
simulation.machineState === 'MOVING_TO_GATE';
const fault = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
// Get model asset for this item
const asset = getModelAsset(currentItem.item.id);
const finalColor = fault ? '#fb3d4e' : color;
const finalEmissive = fault ? '#fb3d4e' : color;
const finalRoughness = asset?.loaderType === 'stl' ? 0.45 : 0.5;
const finalRoughness = asset?.loaderType === 'stl' ? 0.35 : 0.4;
// Brighter emissive when moving for visibility
const emissiveBoost = moving ? 0.55 : 0.35;
// Fallback primitive
const fallbackPrimitive = asset?.fallbackPrimitive ?? 'box';
@@ -109,6 +128,7 @@ export default function Item3D({ simulation, currentItem }: Props) {
color={finalColor}
emissive={finalEmissive}
roughness={finalRoughness}
emissiveIntensity={emissiveBoost}
/>
}
>
@@ -119,7 +139,7 @@ export default function Item3D({ simulation, currentItem }: Props) {
position={[0, 0, 0]}
color={finalColor}
emissive={finalEmissive}
emissiveIntensity={0.2}
emissiveIntensity={emissiveBoost}
roughness={finalRoughness}
fallback={
<FallbackPrimitive
@@ -130,6 +150,7 @@ export default function Item3D({ simulation, currentItem }: Props) {
color={finalColor}
emissive={finalEmissive}
roughness={finalRoughness}
emissiveIntensity={emissiveBoost}
/>
}
/>
@@ -143,13 +164,26 @@ export default function Item3D({ simulation, currentItem }: Props) {
color={finalColor}
emissive={finalEmissive}
roughness={finalRoughness}
emissiveIntensity={emissiveBoost}
/>
)}
{/* Glow ring under item for visibility */}
<mesh position={[0, -sy / 2 + 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[Math.max(sx, sz) * 0.4, Math.max(sx, sz) * 0.6, 32]} />
<meshStandardMaterial
color={finalColor}
emissive={finalColor}
emissiveIntensity={moving ? 0.8 : 0.4}
transparent
opacity={moving ? 0.7 : 0.4}
/>
</mesh>
{detecting ? (
<mesh>
<boxGeometry args={[sx + 0.08, sy + 0.08, sz + 0.08]} />
<meshStandardMaterial color="#38bdf8" wireframe transparent opacity={0.8} />
<boxGeometry args={[sx + 0.12, sy + 0.12, sz + 0.12]} />
<meshStandardMaterial color="#38bdf8" wireframe transparent opacity={0.9} />
</mesh>
) : null}
</group>

View File

@@ -27,17 +27,38 @@ function RouteBeam({
const length = Math.sqrt(dx * dx + dz * dz) || 0.1;
const angle = Math.atan2(dz, dx);
// Much thicker and brighter active route
const thickness = active ? 0.12 : 0.04;
const width = active ? 0.22 : 0.08;
return (
<mesh position={mid} rotation={[0, -angle, 0]}>
<boxGeometry args={[length, active ? 0.06 : 0.04, active ? 0.14 : 0.08]} />
<meshStandardMaterial
color={color}
transparent
opacity={active ? 1 : 0.45}
emissive={color}
emissiveIntensity={active ? 0.55 : 0.18}
/>
</mesh>
<group>
{/* Main route beam */}
<mesh position={mid} rotation={[0, -angle, 0]}>
<boxGeometry args={[length, thickness, width]} />
<meshStandardMaterial
color={color}
transparent
opacity={active ? 1 : 0.3}
emissive={color}
emissiveIntensity={active ? 0.8 : 0.12}
/>
</mesh>
{/* Glow layer for active route */}
{active && (
<mesh position={[mid[0], mid[1] - 0.02, mid[2]]} rotation={[0, -angle, 0]}>
<boxGeometry args={[length, 0.02, width + 0.1]} />
<meshStandardMaterial
color={color}
transparent
opacity={0.5}
emissive={color}
emissiveIntensity={1.0}
/>
</mesh>
)}
</group>
);
}

View File

@@ -18,17 +18,30 @@ function SensorPole({
height?: number;
}) {
return (
<group position={[x, 0, -0.55]}>
<group position={[x, 0, -0.6]}>
{/* Pole - brighter */}
<mesh position={[0, height / 2, 0]}>
<boxGeometry args={[0.08, height, 0.08]} />
<meshStandardMaterial color="#475569" />
<boxGeometry args={[0.1, height, 0.1]} />
<meshStandardMaterial color="#6b7a8f" metalness={0.3} roughness={0.7} />
</mesh>
<mesh position={[0, height, 0.2]}>
<boxGeometry args={[0.28, 0.16, 0.22]} />
{/* Sensor head - larger and brighter */}
<mesh position={[0, height, 0.22]}>
<boxGeometry args={[0.32, 0.2, 0.26]} />
<meshStandardMaterial
color={active ? labelColor : '#1e293b'}
color={active ? labelColor : '#3d4a5c'}
emissive={active ? labelColor : '#000000'}
emissiveIntensity={active ? 0.55 : 0}
emissiveIntensity={active ? 0.7 : 0}
metalness={0.2}
roughness={0.6}
/>
</mesh>
{/* Lens indicator */}
<mesh position={[0, height, 0.36]}>
<cylinderGeometry args={[0.06, 0.06, 0.02, 16]} />
<meshStandardMaterial
color={active ? '#ffffff' : '#2a3444'}
emissive={active ? labelColor : '#000000'}
emissiveIntensity={active ? 0.9 : 0}
/>
</mesh>
</group>
@@ -44,11 +57,20 @@ export default function SensorRig3D({ sensors, machineState }: Props) {
<SensorPole x={TWIN_LAYOUT.laserX} active={sensors.laser.active} labelColor="#22d3ee" />
<SensorPole x={TWIN_LAYOUT.ultrasonicX} active={sensors.ultrasound.active} labelColor="#67e8f9" height={0.75} />
{/* Detection beam - more visible */}
{detecting ? (
<mesh position={[TWIN_LAYOUT.cameraX, TWIN_LAYOUT.beltY + 0.35, 0]}>
<boxGeometry args={[0.7, 0.02, 0.55]} />
<meshStandardMaterial color="#38bdf8" transparent opacity={0.35} emissive="#38bdf8" emissiveIntensity={0.4} />
</mesh>
<group>
{/* Main detection plane */}
<mesh position={[TWIN_LAYOUT.cameraX, TWIN_LAYOUT.beltY + 0.4, 0]}>
<boxGeometry args={[0.8, 0.03, 0.6]} />
<meshStandardMaterial color="#38bdf8" transparent opacity={0.5} emissive="#38bdf8" emissiveIntensity={0.7} />
</mesh>
{/* Scan line effect */}
<mesh position={[TWIN_LAYOUT.cameraX, TWIN_LAYOUT.beltY + 0.2, 0]}>
<boxGeometry args={[0.02, 0.4, 0.6]} />
<meshStandardMaterial color="#67e8f9" transparent opacity={0.6} emissive="#67e8f9" emissiveIntensity={0.9} />
</mesh>
</group>
) : null}
</group>
);

View File

@@ -67,27 +67,32 @@ function TwinScene({
return (
<>
<color attach="background" args={['#050910']} />
<ambientLight intensity={0.55} />
<directionalLight position={[5, 8, 4]} intensity={0.9} />
<directionalLight position={[-4, 4, -2]} intensity={0.25} />
{/* Darker blue-gray background for industrial feel */}
<color attach="background" args={['#0a1520']} />
{/* Improved lighting for better visibility */}
<ambientLight intensity={0.7} />
<directionalLight position={[5, 8, 4]} intensity={1.1} />
<directionalLight position={[-4, 4, -2]} intensity={0.4} />
<hemisphereLight args={['#b8d4e8', '#1a2836', 0.3]} />
<Grid
args={[16, 16]}
cellSize={0.5}
cellThickness={0.6}
cellColor="#1e3a54"
cellThickness={0.7}
cellColor="#2a4a6a"
sectionSize={2}
sectionThickness={1}
sectionColor="#284762"
fadeDistance={18}
sectionThickness={1.2}
sectionColor="#3a6080"
fadeDistance={16}
infiniteGrid={false}
position={[0, 0.001, 0]}
/>
{/* Floor with better contrast */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>
<planeGeometry args={[14, 10]} />
<meshStandardMaterial color="#07111d" />
<meshStandardMaterial color="#0f1a2a" />
</mesh>
<Conveyor3D stopped={stopped} />
@@ -120,9 +125,9 @@ function TwinScene({
enablePan={!simplified}
enableZoom
maxPolarAngle={Math.PI / 2.1}
minDistance={4}
maxDistance={14}
target={[0.4, 0.3, 0]}
minDistance={2.5}
maxDistance={10}
target={[0.6, 0.4, 0]}
/>
<FpsMeter onFps={onFps} />
</>
@@ -164,7 +169,7 @@ export default function SorterDigitalTwin({
<div className="digital-twin-canvas">
<Canvas
camera={{ position: simplified ? [5.5, 4.2, 5.5] : [6.2, 4.8, 6.2], fov: 42 }}
camera={{ position: simplified ? [3.8, 2.6, 3.8] : [4.0, 3.0, 4.0], fov: 45 }}
dpr={simplified ? [1, 1.25] : [1, 1.75]}
gl={{ antialias: !simplified, powerPreference: 'high-performance' }}
onCreated={({ gl }) => {

View File

@@ -19,16 +19,32 @@ function ZoneBox({
active: boolean;
}) {
return (
<mesh position={position}>
<boxGeometry args={size} />
<meshStandardMaterial
color={color}
transparent
opacity={active ? 0.5 : 0.22}
emissive={color}
emissiveIntensity={active ? 0.4 : 0.12}
/>
</mesh>
<group position={position}>
<mesh>
<boxGeometry args={size} />
<meshStandardMaterial
color={color}
transparent
opacity={active ? 0.6 : 0.2}
emissive={color}
emissiveIntensity={active ? 0.7 : 0.1}
/>
</mesh>
{/* Highlight ring for active zone */}
{active && (
<mesh position={[0, size[1] / 2 + 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[size[0] * 0.4, size[0] * 0.55, 32]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={1.0}
transparent
opacity={0.8}
side={2}
/>
</mesh>
)}
</group>
);
}
@@ -52,19 +68,19 @@ function RollCage({
<meshStandardMaterial
color={color}
transparent
opacity={active ? 0.25 : 0.1}
opacity={active ? 0.4 : 0.15}
emissive={color}
emissiveIntensity={active ? 0.3 : 0.08}
emissiveIntensity={active ? 0.6 : 0.1}
/>
</mesh>
{/* Cage wireframe edges (visible frame) */}
<lineSegments>
<edgesGeometry args={[new THREE.BoxGeometry(...size)]} />
<lineBasicMaterial color={color} linewidth={active ? 2 : 1} opacity={active ? 1 : 0.6} transparent />
<lineBasicMaterial color={color} linewidth={active ? 3 : 1} opacity={active ? 1 : 0.5} transparent />
</lineSegments>
{/* Vertical posts (corners) for visual emphasis */}
{/* Vertical posts (corners) for visual emphasis - thicker */}
{[
[-size[0] / 2, 0, -size[2] / 2],
[size[0] / 2, 0, -size[2] / 2],
@@ -72,10 +88,30 @@ function RollCage({
[size[0] / 2, 0, size[2] / 2],
].map((offset, i) => (
<mesh key={i} position={offset as [number, number, number]}>
<boxGeometry args={[0.04, size[1], 0.04]} />
<meshStandardMaterial color={color} metalness={0.3} roughness={0.7} />
<boxGeometry args={[0.06, size[1], 0.06]} />
<meshStandardMaterial
color={color}
metalness={0.4}
roughness={0.6}
emissive={active ? color : '#000000'}
emissiveIntensity={active ? 0.3 : 0}
/>
</mesh>
))}
{/* Floor highlight for active cage */}
{active && (
<mesh position={[0, -size[1] / 2 + 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[size[0] * 0.9, size[2] * 0.9]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={0.6}
transparent
opacity={0.5}
/>
</mesh>
)}
</group>
);
}