pillar jib animator added

This commit is contained in:
2025-08-07 14:53:20 +05:30
parent a08cec33ab
commit a1557001e6
3 changed files with 290 additions and 103 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;