Implement global axis system and advanced motion logic

This commit is contained in:
2025-12-21 23:25:23 +09:00
parent 182c364e37
commit a459b81884
7 changed files with 802 additions and 365 deletions

View File

@@ -2,119 +2,129 @@
import React, { useRef } from 'react';
import { TransformControls, Text } from '@react-three/drei';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import {
SimObject,
ObjectType,
AxisObject,
CylinderObject,
LedObject,
SwitchObject
SwitchObject,
AxisData
} from '../types';
interface ObjectProps {
data: SimObject;
isSelected: boolean;
isSelected?: boolean;
isPlaying?: boolean;
onSelect: (id: string) => void;
onUpdate: (id: string, updates: Partial<SimObject>) => void;
onSelect?: (id: string) => void;
onUpdate?: (id: string, updates: Partial<SimObject>) => void;
onInteract?: (id: string) => void;
axes?: AxisData[];
}
// -- 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));
// -- Axis Visualizer Component --
const AxisVisualizer = ({ object, isPlaying, axes, isSelected }: { object: AxisObject, isPlaying: boolean, axes?: AxisData[], isSelected?: boolean }) => {
const carriageRef = useRef<THREE.Mesh>(null);
const groupRef = useRef<THREE.Group>(null);
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>
// Resolve current value from Global Axis Channel
const axisValue = axes && object.axisIndex !== undefined && axes[object.axisIndex] ? axes[object.axisIndex].value : 0; // Fallback to 0 if axes not provided or index invalid
<mesh position={[railLength / 2, 0, 0]}>
<boxGeometry args={[railLength + 0.5, 0.2, 0.5]} />
<meshStandardMaterial color="#475569" />
</mesh>
useFrame(() => {
if (object.type === ObjectType.AXIS_LINEAR && carriageRef.current) {
const railLength = 5;
const normalizedPos = ((axisValue - object.min) / (object.max - object.min)) * railLength;
const safePos = isNaN(normalizedPos) ? 0 : Math.max(0, Math.min(railLength, normalizedPos));
carriageRef.current.position.x = safePos;
} else if (object.type === ObjectType.AXIS_ROTARY && groupRef.current) {
const rotationAngle = (axisValue % 360) * (Math.PI / 180);
// The inner group for rotary axis is rotated, not the carriage directly
const innerRotaryGroup = groupRef.current.children.find(child => child.name === 'rotary-inner-group');
if (innerRotaryGroup) {
innerRotaryGroup.rotation.y = -rotationAngle;
}
}
});
<mesh position={[safePos, 0.25, 0]}>
<boxGeometry args={[0.8, 0.3, 0.6]} />
<meshStandardMaterial color={(isSelected && !isPlaying) ? '#facc15' : '#3b82f6'} />
</mesh>
if (object.type === ObjectType.AXIS_LINEAR) {
const railLength = 5;
const normalizedPos = ((axisValue - object.min) / (object.max - object.min)) * railLength;
const safePos = isNaN(normalizedPos) ? 0 : Math.max(0, Math.min(railLength, normalizedPos));
return (
<group ref={groupRef}>
<Text position={[railLength / 2, 0.8, 0]} fontSize={0.3} color="white" anchorX="center" anchorY="bottom">
{object.name} ({axisValue.toFixed(1)})
</Text>
{isSelected && !isPlaying && (
<mesh position={[railLength / 2, 0, 0]}>
<boxGeometry args={[railLength + 0.6, 0.3, 0.6]} />
<primitive object={selectedMaterial} attach="material" />
<boxGeometry args={[railLength + 0.5, 0.2, 0.5]} />
<meshStandardMaterial color="#475569" />
</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]} />
<mesh ref={carriageRef} position={[safePos, 0.25, 0]}>
<boxGeometry args={[0.8, 0.3, 0.6]} />
<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" />
{isSelected && !isPlaying && (
<mesh position={[railLength / 2, 0, 0]}>
<boxGeometry args={[railLength + 0.6, 0.3, 0.6]} />
<primitive object={selectedMaterial} attach="material" />
</mesh>
)}
</group>
);
} else if (object.type === ObjectType.AXIS_ROTARY) {
const rotationAngle = (axisValue % 360) * (Math.PI / 180);
return (
<group ref={groupRef}>
<Text position={[0, 1.5, 0]} fontSize={0.3} color="white" anchorX="center" anchorY="bottom">
{object.name} ({axisValue.toFixed(0)}°)
</Text>
<mesh position={[0, 0.25, 0]}>
<cylinderGeometry args={[0.8, 0.8, 0.5, 32]} />
<meshStandardMaterial color="#475569" />
</mesh>
)}
</group>
);
<group name="rotary-inner-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>
);
}
return null;
};
// -- Cylinder Component --
export const Cylinder: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSelect }) => {
const cyl = data as CylinderObject;
// -- Cylinder Visualizer Component --
const CylinderVisualizer = ({ object, isPlaying, isSelected }: { object: CylinderObject, isPlaying: boolean, isSelected?: boolean }) => {
const housingLen = 2;
const extension = Math.min(cyl.stroke, Math.max(0, cyl.currentPosition));
const extension = Math.min(object.stroke, Math.max(0, object.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); }}
>
<group>
<Text position={[housingLen / 2, 0.6, 0]} fontSize={0.3} color="white" anchorX="center" anchorY="bottom">
{cyl.name}
{object.name}
</Text>
<mesh rotation={[0, 0, -Math.PI / 2]} position={[housingLen / 2, 0, 0]}>
@@ -154,7 +164,7 @@ export const Switch: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onS
rotation={[sw.rotation.x, sw.rotation.y, sw.rotation.z]}
onClick={(e) => {
e.stopPropagation();
if (!isPlaying) onSelect(sw.id);
if (!isPlaying) onSelect?.(sw.id);
if (onInteract) onInteract(sw.id);
}}
>
@@ -190,7 +200,7 @@ export const Led: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSele
<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); }}
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}
@@ -220,34 +230,50 @@ export const Led: React.FC<ObjectProps> = ({ data, isSelected, isPlaying, onSele
);
};
export const EditableObject: React.FC<ObjectProps & { enableTransform: boolean, snap?: number | null }> = (props) => {
const { data, enableTransform, isPlaying, onUpdate, snap } = props;
export const EditableObject: React.FC<ObjectProps & { enableTransform?: boolean, snap?: number | null }> = (props) => {
const { data, enableTransform, isPlaying, onUpdate, snap, axes } = props;
const handleTransformChange = (e: any) => {
if (e.target.object) {
const o = e.target.object;
onUpdate(data.id, {
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;
let ComponentToRender;
switch (data.type) {
case ObjectType.AXIS_LINEAR:
case ObjectType.AXIS_ROTARY:
ComponentToRender = <AxisVisualizer object={data as AxisObject} isPlaying={isPlaying!} axes={axes} isSelected={props.isSelected} />;
break;
case ObjectType.CYLINDER:
ComponentToRender = <CylinderVisualizer object={data as CylinderObject} isPlaying={isPlaying!} isSelected={props.isSelected} />;
break;
case ObjectType.SWITCH:
ComponentToRender = <Switch {...props} />;
break;
case ObjectType.LED:
ComponentToRender = <Led {...props} />;
break;
default:
ComponentToRender = null;
}
return (
<>
<Component {...props} />
<group
position={[data.position.x, data.position.y, data.position.z]}
rotation={[data.rotation.x, data.rotation.y, data.rotation.z]}
onClick={(e) => { e.stopPropagation(); if (!isPlaying) props.onSelect?.(data.id); }}
>
{ComponentToRender}
{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]}
object={undefined} // TransformControls will apply to its children, not a specific object prop
position={[0, 0, 0]} // Position relative to the group
rotation={[0, 0, 0]} // Rotation relative to the group
onMouseUp={handleTransformChange}
size={0.6}
mode="translate"
@@ -255,6 +281,6 @@ export const EditableObject: React.FC<ObjectProps & { enableTransform: boolean,
rotationSnap={snap ? Math.PI / 12 : undefined}
/>
)}
</>
</group>
);
};