Initial commit
This commit is contained in:
258
components/SceneObjects.tsx
Normal file
258
components/SceneObjects.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { TransformControls, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import {
|
||||
SimObject,
|
||||
ObjectType,
|
||||
AxisObject,
|
||||
CylinderObject,
|
||||
LedObject,
|
||||
SwitchObject
|
||||
} from '../types';
|
||||
|
||||
interface ObjectProps {
|
||||
data: SimObject;
|
||||
isSelected: boolean;
|
||||
isPlaying?: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
onUpdate: (id: string, updates: Partial<SimObject>) => void;
|
||||
onInteract?: (id: string) => void;
|
||||
}
|
||||
|
||||
// -- Helper Material --
|
||||
const selectedMaterial = new THREE.MeshBasicMaterial({ color: '#4ade80', wireframe: true, transparent: true, opacity: 0.5 });
|
||||
|
||||
// -- Linear Axis Component --
|
||||
export const LinearAxis: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSelect }) => {
|
||||
const axis = data as AxisObject;
|
||||
const railLength = 5;
|
||||
const normalizedPos = ((axis.currentValue - axis.min) / (axis.max - axis.min)) * railLength;
|
||||
const safePos = isNaN(normalizedPos) ? 0 : Math.max(0, Math.min(railLength, normalizedPos));
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[axis.position.x, axis.position.y, axis.position.z]}
|
||||
rotation={[axis.rotation.x, axis.rotation.y, axis.rotation.z]}
|
||||
onClick={(e) => { e.stopPropagation(); if (!isPlaying) onSelect(axis.id); }}
|
||||
>
|
||||
<Text position={[railLength / 2, 0.8, 0]} fontSize={0.3} color="white" anchorX="center" anchorY="bottom">
|
||||
{axis.name} ({axis.currentValue.toFixed(1)})
|
||||
</Text>
|
||||
|
||||
<mesh position={[railLength / 2, 0, 0]}>
|
||||
<boxGeometry args={[railLength + 0.5, 0.2, 0.5]} />
|
||||
<meshStandardMaterial color="#475569" />
|
||||
</mesh>
|
||||
|
||||
<mesh position={[safePos, 0.25, 0]}>
|
||||
<boxGeometry args={[0.8, 0.3, 0.6]} />
|
||||
<meshStandardMaterial color={(isSelected && !isPlaying) ? '#facc15' : '#3b82f6'} />
|
||||
</mesh>
|
||||
|
||||
{isSelected && !isPlaying && (
|
||||
<mesh position={[railLength / 2, 0, 0]}>
|
||||
<boxGeometry args={[railLength + 0.6, 0.3, 0.6]} />
|
||||
<primitive object={selectedMaterial} attach="material" />
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
// -- Rotary Axis Component --
|
||||
export const RotaryAxis: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSelect }) => {
|
||||
const axis = data as AxisObject;
|
||||
const rotationAngle = (axis.currentValue % 360) * (Math.PI / 180);
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[axis.position.x, axis.position.y, axis.position.z]}
|
||||
rotation={[axis.rotation.x, axis.rotation.y, axis.rotation.z]}
|
||||
onClick={(e) => { e.stopPropagation(); if (!isPlaying) onSelect(axis.id); }}
|
||||
>
|
||||
<Text position={[0, 1.5, 0]} fontSize={0.3} color="white" anchorX="center" anchorY="bottom">
|
||||
{axis.name} ({axis.currentValue.toFixed(0)}°)
|
||||
</Text>
|
||||
|
||||
<mesh position={[0, 0.25, 0]}>
|
||||
<cylinderGeometry args={[0.8, 0.8, 0.5, 32]} />
|
||||
<meshStandardMaterial color="#475569" />
|
||||
</mesh>
|
||||
|
||||
<group rotation={[0, -rotationAngle, 0]} position={[0, 0.6, 0]}>
|
||||
<mesh>
|
||||
<cylinderGeometry args={[0.7, 0.7, 0.2, 32]} />
|
||||
<meshStandardMaterial color={(isSelected && !isPlaying) ? '#facc15' : '#3b82f6'} />
|
||||
</mesh>
|
||||
<mesh position={[0.5, 0.15, 0]}>
|
||||
<boxGeometry args={[0.3, 0.1, 0.1]} />
|
||||
<meshStandardMaterial color="white" />
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
{isSelected && !isPlaying && (
|
||||
<mesh position={[0, 0.4, 0]}>
|
||||
<cylinderGeometry args={[0.9, 0.9, 1, 16]} />
|
||||
<primitive object={selectedMaterial} attach="material" />
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
// -- Cylinder Component --
|
||||
export const Cylinder: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSelect }) => {
|
||||
const cyl = data as CylinderObject;
|
||||
const housingLen = 2;
|
||||
const extension = Math.min(cyl.stroke, Math.max(0, cyl.currentPosition));
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[cyl.position.x, cyl.position.y, cyl.position.z]}
|
||||
rotation={[cyl.rotation.x, cyl.rotation.y, cyl.rotation.z]}
|
||||
onClick={(e) => { e.stopPropagation(); if (!isPlaying) onSelect(cyl.id); }}
|
||||
>
|
||||
<Text position={[housingLen/2, 0.6, 0]} fontSize={0.3} color="white" anchorX="center" anchorY="bottom">
|
||||
{cyl.name}
|
||||
</Text>
|
||||
|
||||
<mesh rotation={[0, 0, -Math.PI/2]} position={[housingLen/2, 0, 0]}>
|
||||
<cylinderGeometry args={[0.3, 0.3, housingLen, 16]} />
|
||||
<meshStandardMaterial color="#64748b" opacity={0.8} transparent />
|
||||
</mesh>
|
||||
|
||||
<mesh
|
||||
rotation={[0, 0, -Math.PI/2]}
|
||||
position={[housingLen + (extension/2), 0, 0]}
|
||||
>
|
||||
<cylinderGeometry args={[0.15, 0.15, extension + 0.2, 16]} />
|
||||
<meshStandardMaterial color="#cbd5e1" metalness={0.8} roughness={0.2} />
|
||||
</mesh>
|
||||
<mesh position={[housingLen + extension, 0, 0]}>
|
||||
<boxGeometry args={[0.2, 0.4, 0.4]} />
|
||||
<meshStandardMaterial color="#475569" />
|
||||
</mesh>
|
||||
|
||||
{isSelected && !isPlaying && (
|
||||
<mesh position={[housingLen/2 + extension/2, 0, 0]}>
|
||||
<boxGeometry args={[housingLen + extension + 0.5, 0.7, 0.7]} />
|
||||
<primitive object={selectedMaterial} attach="material" />
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
// -- Switch Component --
|
||||
export const Switch: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSelect, onInteract }) => {
|
||||
const sw = data as SwitchObject;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[sw.position.x, sw.position.y, sw.position.z]}
|
||||
rotation={[sw.rotation.x, sw.rotation.y, sw.rotation.z]}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!isPlaying) onSelect(sw.id);
|
||||
if(onInteract) onInteract(sw.id);
|
||||
}}
|
||||
>
|
||||
<Text position={[0, 0.6, 0]} fontSize={0.25} color="white" anchorX="center" anchorY="bottom">
|
||||
{sw.name}
|
||||
</Text>
|
||||
|
||||
<mesh position={[0, 0.1, 0]}>
|
||||
<boxGeometry args={[0.6, 0.2, 0.6]} />
|
||||
<meshStandardMaterial color="#334155" />
|
||||
</mesh>
|
||||
|
||||
<mesh position={[0, 0.2 + (sw.isOn ? -0.05 : 0), 0]}>
|
||||
<cylinderGeometry args={[0.2, 0.2, 0.2, 16]} />
|
||||
<meshStandardMaterial color={sw.isOn ? '#ef4444' : '#94a3b8'} />
|
||||
</mesh>
|
||||
|
||||
{isSelected && !isPlaying && (
|
||||
<mesh position={[0, 0.15, 0]}>
|
||||
<boxGeometry args={[0.7, 0.5, 0.7]} />
|
||||
<primitive object={selectedMaterial} attach="material" />
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
// -- LED Component --
|
||||
export const Led: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSelect }) => {
|
||||
const led = data as LedObject;
|
||||
|
||||
return (
|
||||
<group
|
||||
position={[led.position.x, led.position.y, led.position.z]}
|
||||
rotation={[led.rotation.x, led.rotation.y, led.rotation.z]}
|
||||
onClick={(e) => { e.stopPropagation(); if (!isPlaying) onSelect(led.id); }}
|
||||
>
|
||||
<Text position={[0, 0.6, 0]} fontSize={0.25} color="white" anchorX="center" anchorY="bottom">
|
||||
{led.name}
|
||||
</Text>
|
||||
|
||||
<mesh position={[0, 0.1, 0]}>
|
||||
<cylinderGeometry args={[0.2, 0.25, 0.2, 16]} />
|
||||
<meshStandardMaterial color="#334155" />
|
||||
</mesh>
|
||||
|
||||
<mesh position={[0, 0.3, 0]}>
|
||||
<sphereGeometry args={[0.2, 16, 16]} />
|
||||
<meshStandardMaterial
|
||||
color={led.isOn ? led.color : '#334155'}
|
||||
emissive={led.isOn ? led.color : '#000000'}
|
||||
emissiveIntensity={led.isOn ? 2 : 0}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{isSelected && !isPlaying && (
|
||||
<mesh position={[0, 0.2, 0]}>
|
||||
<boxGeometry args={[0.6, 0.6, 0.6]} />
|
||||
<primitive object={selectedMaterial} attach="material" />
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableObject: React.FC<ObjectProps & { enableTransform: boolean }> = (props) => {
|
||||
const { data, enableTransform, isPlaying, onUpdate } = props;
|
||||
|
||||
const handleTransformChange = (e: any) => {
|
||||
if (e.target.object) {
|
||||
const o = e.target.object;
|
||||
onUpdate(data.id, {
|
||||
position: { x: o.position.x, y: o.position.y, z: o.position.z },
|
||||
rotation: { x: o.rotation.x, y: o.rotation.y, z: o.rotation.z }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const Component =
|
||||
data.type === ObjectType.AXIS_LINEAR ? LinearAxis :
|
||||
data.type === ObjectType.AXIS_ROTARY ? RotaryAxis :
|
||||
data.type === ObjectType.CYLINDER ? Cylinder :
|
||||
data.type === ObjectType.SWITCH ? Switch :
|
||||
Led;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component {...props} />
|
||||
{props.isSelected && enableTransform && !isPlaying && (
|
||||
<TransformControls
|
||||
object={undefined}
|
||||
position={[data.position.x, data.position.y, data.position.z]}
|
||||
rotation={[data.rotation.x, data.rotation.y, data.rotation.z]}
|
||||
onMouseUp={handleTransformChange}
|
||||
size={0.6}
|
||||
mode="translate"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user