pillar jib animator added
This commit is contained in:
@@ -1,82 +1,40 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { Sphere, Box } from '@react-three/drei';
|
||||
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
||||
|
||||
function PillarJibAnimator({ crane }: { crane: CraneStatus }) {
|
||||
function PillarJibAnimator({ crane, points }: { crane: CraneStatus; points: [THREE.Vector3, THREE.Vector3]; }) {
|
||||
const { scene } = useThree();
|
||||
const { assetStore } = useSceneContext();
|
||||
const { resetAsset } = assetStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
|
||||
const [clampedPoints, setClampedPoints] = useState<[THREE.Vector3, THREE.Vector3]>();
|
||||
const [isInside, setIsInside] = useState<[boolean, boolean]>([false, false]);
|
||||
const [animationPhase, setAnimationPhase] = useState<string>('idle'); // 0: idle, 1: init-hook-adjust, 2: 'rotate-base', 3: 'move-trolley', 4: 'final-hook-adjust'
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) {
|
||||
resetAsset(crane.modelUuid);
|
||||
setAnimationPhase('idle');
|
||||
} else if (animationPhase === 'idle') {
|
||||
setAnimationPhase('init-hook-adjust');
|
||||
}
|
||||
}, [isPlaying, scene, crane.modelUuid]);
|
||||
|
||||
useEffect(() => {
|
||||
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
|
||||
|
||||
if (model) {
|
||||
const base = model.getObjectByName('base');
|
||||
const trolley = model.getObjectByName('trolley');
|
||||
const hook = model.getObjectByName('hook');
|
||||
|
||||
if (base && trolley && hook) {
|
||||
let trolleyDir = 1;
|
||||
let hookDir = 1;
|
||||
|
||||
const trolleySpeed = 0.01;
|
||||
const hookSpeed = 0.01;
|
||||
const rotationSpeed = 0.005;
|
||||
|
||||
const trolleyMinOffset = -1;
|
||||
const trolleyMaxOffset = 1.75;
|
||||
const hookMinOffset = 0.25;
|
||||
const hookMaxOffset = -1.5;
|
||||
|
||||
const originalTrolleyX = trolley.position.x;
|
||||
const originalHookY = hook.position.y;
|
||||
|
||||
const animate = () => {
|
||||
if (base) {
|
||||
base.rotation.y += rotationSpeed;
|
||||
}
|
||||
|
||||
if (trolley) {
|
||||
trolley.position.x += trolleyDir * trolleySpeed;
|
||||
if (trolley.position.x >= originalTrolleyX + trolleyMaxOffset ||
|
||||
trolley.position.x <= originalTrolleyX + trolleyMinOffset) {
|
||||
trolleyDir *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (hook) {
|
||||
hook.position.y += hookDir * hookSpeed;
|
||||
if (hook.position.y >= originalHookY + hookMinOffset ||
|
||||
hook.position.y <= originalHookY + hookMaxOffset) {
|
||||
hookDir *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
}
|
||||
}, [crane, scene]);
|
||||
|
||||
return (
|
||||
<PillarJibHelper crane={crane} />
|
||||
);
|
||||
}
|
||||
|
||||
export default PillarJibAnimator;
|
||||
|
||||
function PillarJibHelper({ crane }: { crane: CraneStatus }) {
|
||||
const { scene } = useThree();
|
||||
|
||||
const { geometry, position } = useMemo(() => {
|
||||
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
|
||||
if (!model) return { geometry: null, position: null };
|
||||
if (!model) return;
|
||||
|
||||
const base = model.getObjectByName('base');
|
||||
const trolley = model.getObjectByName('trolley');
|
||||
const hook = model.getObjectByName('hook');
|
||||
|
||||
if (!base || !trolley || !hook) return { geometry: null, position: null };
|
||||
if (!base || !trolley || !hook) return;
|
||||
|
||||
const baseWorld = new THREE.Vector3();
|
||||
base.getWorldPosition(baseWorld);
|
||||
@@ -87,47 +45,186 @@ function PillarJibHelper({ crane }: { crane: CraneStatus }) {
|
||||
const hookWorld = new THREE.Vector3();
|
||||
hook.getWorldPosition(hookWorld);
|
||||
|
||||
const trolleyMinOffset = -1;
|
||||
const trolleyMaxOffset = 1.75;
|
||||
const hookMinOffset = 0.25;
|
||||
const hookMaxOffset = -1.5;
|
||||
|
||||
const distFromBase = new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length();
|
||||
const outerRadius = distFromBase + 1.75;
|
||||
const innerRadius = Math.max(distFromBase - 1, 0.05);
|
||||
const height = (0.25 - (-1.5));
|
||||
const cylinderYPosition = hookWorld.y + (height / 2) + (-1.5 + 0.25) / 2;
|
||||
const innerRadius = Math.max(distFromBase + trolleyMinOffset, 0.05);
|
||||
const outerRadius = Math.max(
|
||||
new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length() + trolleyMaxOffset,
|
||||
innerRadius
|
||||
);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.absarc(0, 0, outerRadius, 0, Math.PI * 2, false);
|
||||
const yMin = hookWorld.y + hookMaxOffset;
|
||||
const yMax = hookWorld.y + hookMinOffset;
|
||||
|
||||
const hole = new THREE.Path();
|
||||
hole.absarc(0, 0, innerRadius, 0, Math.PI * 2, true);
|
||||
shape.holes.push(hole);
|
||||
function clampToCylinder(pos: THREE.Vector3) {
|
||||
const xzDist = new THREE.Vector2(pos.x - baseWorld.x, pos.z - baseWorld.z);
|
||||
const distance = xzDist.length();
|
||||
|
||||
const extrudeSettings = {
|
||||
depth: height,
|
||||
bevelEnabled: false,
|
||||
steps: 1
|
||||
let clampedDistance = THREE.MathUtils.clamp(distance, innerRadius, outerRadius);
|
||||
if (distance > 0) {
|
||||
clampedDistance = Math.max(innerRadius, Math.min(distance, outerRadius));
|
||||
}
|
||||
|
||||
const clampedXZ = xzDist.normalize().multiplyScalar(clampedDistance);
|
||||
const y = THREE.MathUtils.clamp(pos.y, yMin, yMax);
|
||||
|
||||
return new THREE.Vector3(baseWorld.x + clampedXZ.x, y, baseWorld.z + clampedXZ.y);
|
||||
}
|
||||
|
||||
const newClampedPoints: [THREE.Vector3, THREE.Vector3] = [new THREE.Vector3(), new THREE.Vector3()];
|
||||
const newIsInside: [boolean, boolean] = [false, false];
|
||||
|
||||
points.forEach((point, i) => {
|
||||
const xzDist = new THREE.Vector2(point.x - baseWorld.x, point.z - baseWorld.z).length();
|
||||
const insideXZ = xzDist >= innerRadius && xzDist <= outerRadius;
|
||||
const insideY = point.y >= yMin && point.y <= yMax;
|
||||
newIsInside[i] = insideXZ && insideY;
|
||||
|
||||
newClampedPoints[i] = newIsInside[i] ? point.clone() : clampToCylinder(point);
|
||||
});
|
||||
|
||||
setClampedPoints(newClampedPoints);
|
||||
setIsInside(newIsInside);
|
||||
}, [scene, points, crane.modelUuid]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!isPlaying || isPaused || !points || animationPhase === 'idle') return;
|
||||
|
||||
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
|
||||
if (!model) return;
|
||||
|
||||
const base = model.getObjectByName('base');
|
||||
const trolley = model.getObjectByName('trolley');
|
||||
const hook = model.getObjectByName('hook');
|
||||
|
||||
if (!base || !trolley || !hook || !clampedPoints || !trolley.parent) return;
|
||||
|
||||
const baseWorld = new THREE.Vector3();
|
||||
base.getWorldPosition(baseWorld);
|
||||
|
||||
const hookWorld = new THREE.Vector3();
|
||||
hook.getWorldPosition(hookWorld);
|
||||
|
||||
if (!model.userData.animationData) {
|
||||
model.userData.animationData = {
|
||||
originalHookY: hook.position.y,
|
||||
targetHookY: points[0].y - baseWorld.y + 0.5,
|
||||
targetDirection: new THREE.Vector2(),
|
||||
targetTrolleyX: 0,
|
||||
targetWorldPosition: points[0].clone(),
|
||||
finalHookTargetY: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const position: [number, number, number] = [baseWorld.x, cylinderYPosition, baseWorld.z];
|
||||
const { animationData } = model.userData;
|
||||
const hookSpeed = 0.01 * speed;
|
||||
const rotationSpeed = 0.005 * speed;
|
||||
const trolleySpeed = 0.01 * speed;
|
||||
|
||||
return { geometry, position };
|
||||
}, [scene, crane.modelUuid]);
|
||||
switch (animationPhase) {
|
||||
case 'init-hook-adjust': {
|
||||
const direction = Math.sign(animationData.targetHookY - hook.position.y);
|
||||
hook.position.y += direction * hookSpeed;
|
||||
|
||||
if (!geometry || !position) return null;
|
||||
if (parseFloat(Math.abs(hook.position.y - animationData.targetHookY).toFixed(2)) < 0.05) {
|
||||
hook.position.y = animationData.targetHookY;
|
||||
setAnimationPhase('rotate-base');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'rotate-base': {
|
||||
const baseWorld = new THREE.Vector3();
|
||||
base.getWorldPosition(baseWorld);
|
||||
|
||||
const baseForward = new THREE.Vector3(1, 0, 0);
|
||||
base.localToWorld(baseForward);
|
||||
baseForward.sub(baseWorld);
|
||||
|
||||
const currentDir = new THREE.Vector2(baseForward.x, baseForward.z).normalize();
|
||||
|
||||
const targetWorld = clampedPoints[0];
|
||||
const targetDir = new THREE.Vector2(
|
||||
targetWorld.x - baseWorld.x,
|
||||
targetWorld.z - baseWorld.z
|
||||
).normalize();
|
||||
|
||||
const currentAngle = Math.atan2(currentDir.y, currentDir.x);
|
||||
const targetAngle = Math.atan2(targetDir.y, targetDir.x);
|
||||
let angleDiff = currentAngle - targetAngle;
|
||||
|
||||
angleDiff = Math.atan2(Math.sin(angleDiff), Math.cos(angleDiff));
|
||||
|
||||
if (parseFloat(Math.abs(angleDiff).toFixed(2)) > 0.05) {
|
||||
base.rotation.y += Math.sign(angleDiff) * rotationSpeed;
|
||||
} else {
|
||||
base.rotation.y += angleDiff;
|
||||
const localTarget = trolley.parent.worldToLocal(clampedPoints[0].clone());
|
||||
animationData.targetTrolleyX = localTarget?.x;
|
||||
setAnimationPhase('move-trolley');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'move-trolley': {
|
||||
const dx = animationData.targetTrolleyX - trolley.position.x;
|
||||
const direction = Math.sign(dx);
|
||||
trolley.position.x += direction * trolleySpeed;
|
||||
|
||||
if (parseFloat(Math.abs(dx).toFixed(2)) < 0.05) {
|
||||
trolley.position.x = animationData.targetTrolleyX;
|
||||
|
||||
animationData.finalHookTargetY = hook.position.y - 0.5;
|
||||
setAnimationPhase('final-hook-adjust');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'final-hook-adjust': {
|
||||
const dy = animationData.finalHookTargetY - hook.position.y;
|
||||
const direction = Math.sign(dy);
|
||||
hook.position.y += direction * hookSpeed;
|
||||
|
||||
if (parseFloat(Math.abs(dy).toFixed(2)) < 0.05) {
|
||||
hook.position.y = animationData.finalHookTargetY;
|
||||
setAnimationPhase('idle');
|
||||
console.log('hii');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!clampedPoints) return null;
|
||||
|
||||
return (
|
||||
<mesh
|
||||
geometry={geometry}
|
||||
position={position}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
<>
|
||||
{points.map((point, i) => (
|
||||
<Box
|
||||
key={`original-${i}`}
|
||||
position={point}
|
||||
args={[0.15, 0.15, 0.15]}
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={0x888888}
|
||||
metalness={0.5}
|
||||
roughness={0.4}
|
||||
side={THREE.DoubleSide}
|
||||
transparent={true}
|
||||
opacity={0.3}
|
||||
/>
|
||||
</mesh>
|
||||
<meshStandardMaterial color="yellow" />
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{clampedPoints.map((point, i) => (
|
||||
<Sphere
|
||||
key={`clamped-${i}`}
|
||||
position={point}
|
||||
args={[0.1, 16, 16]}
|
||||
>
|
||||
<meshStandardMaterial color={isInside[i] ? 'lightgreen' : 'orange'} />
|
||||
</Sphere>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PillarJibAnimator;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
|
||||
function PillarJibHelper({ crane }: { crane: CraneStatus }) {
|
||||
const { scene } = useThree();
|
||||
|
||||
const { geometry, position } = useMemo(() => {
|
||||
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
|
||||
if (!model) return { geometry: null, position: null };
|
||||
|
||||
const base = model.getObjectByName('base');
|
||||
const trolley = model.getObjectByName('trolley');
|
||||
const hook = model.getObjectByName('hook');
|
||||
|
||||
if (!base || !trolley || !hook) return { geometry: null, position: null };
|
||||
|
||||
const baseWorld = new THREE.Vector3();
|
||||
base.getWorldPosition(baseWorld);
|
||||
|
||||
const trolleyWorld = new THREE.Vector3();
|
||||
trolley.getWorldPosition(trolleyWorld);
|
||||
|
||||
const hookWorld = new THREE.Vector3();
|
||||
hook.getWorldPosition(hookWorld);
|
||||
|
||||
const trolleyMinOffset = -1;
|
||||
const trolleyMaxOffset = 1.75;
|
||||
const hookMinOffset = 0.25;
|
||||
const hookMaxOffset = -1.5;
|
||||
|
||||
const distFromBase = new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length();
|
||||
const outerRadius = distFromBase + trolleyMaxOffset;
|
||||
const innerRadius = Math.max(distFromBase + trolleyMinOffset, 0.05);
|
||||
const height = (hookMinOffset - (hookMaxOffset));
|
||||
const cylinderYPosition = hookWorld.y + (height / 2) + (hookMaxOffset + hookMinOffset) / 2;
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.absarc(0, 0, outerRadius, 0, Math.PI * 2, false);
|
||||
|
||||
const hole = new THREE.Path();
|
||||
hole.absarc(0, 0, innerRadius, 0, Math.PI * 2, true);
|
||||
shape.holes.push(hole);
|
||||
|
||||
const extrudeSettings = {
|
||||
depth: height,
|
||||
bevelEnabled: false,
|
||||
steps: 1
|
||||
};
|
||||
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const position: [number, number, number] = [baseWorld.x, cylinderYPosition, baseWorld.z];
|
||||
|
||||
return { geometry, position };
|
||||
}, [scene, crane.modelUuid]);
|
||||
|
||||
if (!geometry || !position) return null;
|
||||
|
||||
return (
|
||||
<mesh
|
||||
geometry={geometry}
|
||||
position={position}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={0x888888}
|
||||
metalness={0.5}
|
||||
roughness={0.4}
|
||||
side={THREE.DoubleSide}
|
||||
transparent={true}
|
||||
opacity={0.3}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export default PillarJibHelper;
|
||||
@@ -1,14 +1,27 @@
|
||||
import * as THREE from 'three'
|
||||
import PillarJibAnimator from '../animator/pillarJibAnimator'
|
||||
import PillarJibHelper from '../helper/pillarJibHelper'
|
||||
|
||||
function PillarJibInstance({ crane }: { crane: CraneStatus }) {
|
||||
|
||||
const position1: [number, number, number] = [5, 1, -4];
|
||||
const position2: [number, number, number] = [-2, 2, -2];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<PillarJibAnimator crane={crane} />
|
||||
<PillarJibAnimator
|
||||
crane={crane}
|
||||
points={[
|
||||
new THREE.Vector3(...position1),
|
||||
new THREE.Vector3(...position2)
|
||||
]}
|
||||
/>
|
||||
|
||||
<PillarJibHelper crane={crane} />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PillarJibInstance
|
||||
export default PillarJibInstance;
|
||||
Reference in New Issue
Block a user