Feat: Add panning, grid snap, and view control buttons

This commit is contained in:
2025-12-21 23:02:25 +09:00
parent 30b5f94856
commit 182c364e37
2 changed files with 132 additions and 65 deletions

View File

@@ -2,13 +2,13 @@
import React, { useRef } from 'react';
import { TransformControls, Text } from '@react-three/drei';
import * as THREE from 'three';
import {
SimObject,
ObjectType,
AxisObject,
CylinderObject,
LedObject,
SwitchObject
import {
SimObject,
ObjectType,
AxisObject,
CylinderObject,
LedObject,
SwitchObject
} from '../types';
interface ObjectProps {
@@ -31,8 +31,8 @@ export const LinearAxis: React.FC<ObjectProps> = ({ data, isSelected, isPlaying,
const safePos = isNaN(normalizedPos) ? 0 : Math.max(0, Math.min(railLength, normalizedPos));
return (
<group
position={[axis.position.x, axis.position.y, axis.position.z]}
<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); }}
>
@@ -44,7 +44,7 @@ export const LinearAxis: React.FC<ObjectProps> = ({ data, isSelected, isPlaying,
<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'} />
@@ -66,12 +66,12 @@ export const RotaryAxis: React.FC<ObjectProps> = ({ data, isSelected, isPlaying,
const rotationAngle = (axis.currentValue % 360) * (Math.PI / 180);
return (
<group
position={[axis.position.x, axis.position.y, axis.position.z]}
<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">
<Text position={[0, 1.5, 0]} fontSize={0.3} color="white" anchorX="center" anchorY="bottom">
{axis.name} ({axis.currentValue.toFixed(0)}°)
</Text>
@@ -91,10 +91,10 @@ export const RotaryAxis: React.FC<ObjectProps> = ({ data, isSelected, isPlaying,
</mesh>
</group>
{isSelected && !isPlaying && (
{isSelected && !isPlaying && (
<mesh position={[0, 0.4, 0]}>
<cylinderGeometry args={[0.9, 0.9, 1, 16]} />
<primitive object={selectedMaterial} attach="material" />
<primitive object={selectedMaterial} attach="material" />
</mesh>
)}
</group>
@@ -108,34 +108,34 @@ export const Cylinder: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, o
const extension = Math.min(cyl.stroke, Math.max(0, cyl.currentPosition));
return (
<group
position={[cyl.position.x, cyl.position.y, cyl.position.z]}
<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">
<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]}>
<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]}
<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>
<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]}>
<mesh position={[housingLen / 2 + extension / 2, 0, 0]}>
<boxGeometry args={[housingLen + extension + 0.5, 0.7, 0.7]} />
<primitive object={selectedMaterial} attach="material" />
</mesh>
@@ -147,21 +147,21 @@ export const Cylinder: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, o
// -- 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]}
<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);
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" />
@@ -185,17 +185,17 @@ export const Switch: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onS
// -- 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]}
<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" />
@@ -203,14 +203,14 @@ export const Led: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSele
<mesh position={[0, 0.3, 0]}>
<sphereGeometry args={[0.2, 16, 16]} />
<meshStandardMaterial
color={led.isOn ? led.color : '#334155'}
<meshStandardMaterial
color={led.isOn ? led.color : '#334155'}
emissive={led.isOn ? led.color : '#000000'}
emissiveIntensity={led.isOn ? 2 : 0}
/>
</mesh>
{isSelected && !isPlaying && (
{isSelected && !isPlaying && (
<mesh position={[0, 0.2, 0]}>
<boxGeometry args={[0.6, 0.6, 0.6]} />
<primitive object={selectedMaterial} attach="material" />
@@ -220,37 +220,39 @@ export const Led: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSele
);
};
export const EditableObject: React.FC<ObjectProps & { enableTransform: boolean }> = (props) => {
const { data, enableTransform, isPlaying, onUpdate } = props;
export const EditableObject: React.FC<ObjectProps & { enableTransform: boolean, snap?: number | null }> = (props) => {
const { data, enableTransform, isPlaying, onUpdate, snap } = 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 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 =
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;
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"
<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"
translationSnap={snap ?? undefined}
rotationSnap={snap ? Math.PI / 12 : undefined}
/>
)}
</>