pillar jib animator added
This commit is contained in:
@@ -1,82 +1,40 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useFrame, useThree } from '@react-three/fiber';
|
||||||
import { 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 { 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(() => {
|
useEffect(() => {
|
||||||
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
|
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
|
||||||
|
if (!model) return;
|
||||||
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 };
|
|
||||||
|
|
||||||
const base = model.getObjectByName('base');
|
const base = model.getObjectByName('base');
|
||||||
const trolley = model.getObjectByName('trolley');
|
const trolley = model.getObjectByName('trolley');
|
||||||
const hook = model.getObjectByName('hook');
|
const hook = model.getObjectByName('hook');
|
||||||
|
|
||||||
if (!base || !trolley || !hook) return { geometry: null, position: null };
|
if (!base || !trolley || !hook) return;
|
||||||
|
|
||||||
const baseWorld = new THREE.Vector3();
|
const baseWorld = new THREE.Vector3();
|
||||||
base.getWorldPosition(baseWorld);
|
base.getWorldPosition(baseWorld);
|
||||||
@@ -87,47 +45,186 @@ function PillarJibHelper({ crane }: { crane: CraneStatus }) {
|
|||||||
const hookWorld = new THREE.Vector3();
|
const hookWorld = new THREE.Vector3();
|
||||||
hook.getWorldPosition(hookWorld);
|
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 distFromBase = new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length();
|
||||||
const outerRadius = distFromBase + 1.75;
|
const innerRadius = Math.max(distFromBase + trolleyMinOffset, 0.05);
|
||||||
const innerRadius = Math.max(distFromBase - 1, 0.05);
|
const outerRadius = Math.max(
|
||||||
const height = (0.25 - (-1.5));
|
new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length() + trolleyMaxOffset,
|
||||||
const cylinderYPosition = hookWorld.y + (height / 2) + (-1.5 + 0.25) / 2;
|
innerRadius
|
||||||
|
);
|
||||||
|
|
||||||
const shape = new THREE.Shape();
|
const yMin = hookWorld.y + hookMaxOffset;
|
||||||
shape.absarc(0, 0, outerRadius, 0, Math.PI * 2, false);
|
const yMax = hookWorld.y + hookMinOffset;
|
||||||
|
|
||||||
const hole = new THREE.Path();
|
function clampToCylinder(pos: THREE.Vector3) {
|
||||||
hole.absarc(0, 0, innerRadius, 0, Math.PI * 2, true);
|
const xzDist = new THREE.Vector2(pos.x - baseWorld.x, pos.z - baseWorld.z);
|
||||||
shape.holes.push(hole);
|
const distance = xzDist.length();
|
||||||
|
|
||||||
const extrudeSettings = {
|
let clampedDistance = THREE.MathUtils.clamp(distance, innerRadius, outerRadius);
|
||||||
depth: height,
|
if (distance > 0) {
|
||||||
bevelEnabled: false,
|
clampedDistance = Math.max(innerRadius, Math.min(distance, outerRadius));
|
||||||
steps: 1
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
const clampedXZ = xzDist.normalize().multiplyScalar(clampedDistance);
|
||||||
const position: [number, number, number] = [baseWorld.x, cylinderYPosition, baseWorld.z];
|
const y = THREE.MathUtils.clamp(pos.y, yMin, yMax);
|
||||||
|
|
||||||
return { geometry, position };
|
return new THREE.Vector3(baseWorld.x + clampedXZ.x, y, baseWorld.z + clampedXZ.y);
|
||||||
}, [scene, crane.modelUuid]);
|
}
|
||||||
|
|
||||||
if (!geometry || !position) return null;
|
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 { animationData } = model.userData;
|
||||||
|
const hookSpeed = 0.01 * speed;
|
||||||
|
const rotationSpeed = 0.005 * speed;
|
||||||
|
const trolleySpeed = 0.01 * speed;
|
||||||
|
|
||||||
|
switch (animationPhase) {
|
||||||
|
case 'init-hook-adjust': {
|
||||||
|
const direction = Math.sign(animationData.targetHookY - hook.position.y);
|
||||||
|
hook.position.y += direction * hookSpeed;
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<mesh
|
<>
|
||||||
geometry={geometry}
|
{points.map((point, i) => (
|
||||||
position={position}
|
<Box
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
key={`original-${i}`}
|
||||||
>
|
position={point}
|
||||||
<meshStandardMaterial
|
args={[0.15, 0.15, 0.15]}
|
||||||
color={0x888888}
|
>
|
||||||
metalness={0.5}
|
<meshStandardMaterial color="yellow" />
|
||||||
roughness={0.4}
|
</Box>
|
||||||
side={THREE.DoubleSide}
|
))}
|
||||||
transparent={true}
|
|
||||||
opacity={0.3}
|
{clampedPoints.map((point, i) => (
|
||||||
/>
|
<Sphere
|
||||||
</mesh>
|
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 PillarJibAnimator from '../animator/pillarJibAnimator'
|
||||||
|
import PillarJibHelper from '../helper/pillarJibHelper'
|
||||||
|
|
||||||
function PillarJibInstance({ crane }: { crane: CraneStatus }) {
|
function PillarJibInstance({ crane }: { crane: CraneStatus }) {
|
||||||
|
|
||||||
|
const position1: [number, number, number] = [5, 1, -4];
|
||||||
|
const position2: [number, number, number] = [-2, 2, -2];
|
||||||
|
|
||||||
return (
|
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