feat: Enhance human event handling and animation management, including state updates and monitoring

This commit is contained in:
2025-07-03 16:55:30 +05:30
parent 8dd853dd03
commit 1e715cee50
13 changed files with 316 additions and 36 deletions

View File

@@ -162,7 +162,6 @@ async function handleModelLoad(
// SOCKET
if (selectedItem.type) {
console.log('selectedItem: ', selectedItem);
const data = PointsCalculator(
selectedItem.type,
gltf.scene.clone(),

View File

@@ -49,6 +49,10 @@ function Model({ asset }: { readonly asset: Asset }) {
const { userId, organization } = getUserData();
const mixerRef = useRef<THREE.AnimationMixer>();
const actions = useRef<{ [name: string]: THREE.AnimationAction }>({});
const [currentAnimation, setCurrentAnimation] = useState<string | null>(null);
const [previousAnimation, setPreviousAnimation] = useState<string | null>(null);
const [blendFactor, setBlendFactor] = useState(0);
const blendDuration = 0.3;
useEffect(() => {
setDeletableFloorItem(null);
@@ -278,8 +282,16 @@ function Model({ asset }: { readonly asset: Asset }) {
}
}
const handleAnimationComplete = useCallback(() => {
console.log(`Animation "${currentAnimation}" completed`);
}, [currentAnimation]);
useFrame((_, delta) => {
if (mixerRef.current) {
if (blendFactor < 1) {
setBlendFactor(prev => Math.min(prev + delta / blendDuration, 1));
}
mixerRef.current.update(delta);
}
});
@@ -288,17 +300,46 @@ function Model({ asset }: { readonly asset: Asset }) {
if (asset.animationState && asset.animationState.isPlaying) {
if (!mixerRef.current) return;
Object.values(actions.current).forEach((action) => action.stop());
const action = actions.current[asset.animationState.current];
if (action && asset.animationState?.isPlaying) {
const loopMode = asset.animationState.loopAnimation ? THREE.LoopRepeat : THREE.LoopOnce;
action.reset().setLoop(loopMode, loopMode === THREE.LoopRepeat ? Infinity : 1).play();
if (asset.animationState.current !== currentAnimation) {
setPreviousAnimation(currentAnimation);
setCurrentAnimation(asset.animationState.current);
setBlendFactor(0);
}
const currentAction = actions.current[asset.animationState.current];
const previousAction = previousAnimation ? actions.current[previousAnimation] : null;
if (currentAction) {
const loopMode = asset.animationState.loopAnimation ? THREE.LoopRepeat : THREE.LoopOnce;
currentAction.reset();
currentAction.setLoop(loopMode, loopMode === THREE.LoopRepeat ? Infinity : 1);
currentAction.play();
mixerRef.current.addEventListener('finished', handleAnimationComplete);
if (previousAction && blendFactor < 1) {
previousAction.crossFadeTo(currentAction, blendDuration, true);
}
}
Object.entries(actions.current).forEach(([name, action]) => {
if ((asset.animationState && name !== asset.animationState.current) && name !== previousAnimation) {
action.stop();
}
});
} else {
Object.values(actions.current).forEach((action) => action.stop());
setCurrentAnimation(null);
}
}, [asset.animationState])
return () => {
if (mixerRef.current) {
mixerRef.current.removeEventListener('finished', handleAnimationComplete);
}
}
}, [asset.animationState, currentAnimation, previousAnimation, handleAnimationComplete]);
return (
<group

View File

@@ -4,8 +4,8 @@ import { useWorkerHandler } from './actionHandler/useWorkerHandler';
export function useHumanActions() {
const { handleWorker } = useWorkerHandler();
const handleWorkerAction = useCallback((action: HumanAction) => {
handleWorker(action);
const handleWorkerAction = useCallback((action: HumanAction, materialId: string) => {
handleWorker(action, materialId);
}, [handleWorker]);
const handleHumanAction = useCallback((action: HumanAction, materialId: string) => {
@@ -13,7 +13,7 @@ export function useHumanActions() {
switch (action.actionType) {
case 'worker':
handleWorkerAction(action);
handleWorkerAction(action, materialId);
break;
default:
console.warn(`Unknown Human action type: ${action.actionType}`);

View File

@@ -0,0 +1,79 @@
import { useEffect, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../scene/sceneContext';
type HumanCallback = {
humanId: string;
actionId: string;
callback: () => void;
};
export function useHumanEventManager() {
const { humanStore } = useSceneContext();
const { getHumanById } = humanStore();
const callbacksRef = useRef<HumanCallback[]>([]);
const isMonitoringRef = useRef(false);
const { isPlaying } = usePlayButtonStore();
const { isPaused } = usePauseButtonStore();
const { isReset } = useResetButtonStore();
useEffect(() => {
if (isReset) {
callbacksRef.current = [];
}
}, [isReset])
// Add a new human to monitor
const addHumanToMonitor = (humanId: string, actionId: string, callback: () => void) => {
// Avoid duplicates
if (!callbacksRef.current.some((entry) => entry.humanId === humanId)) {
callbacksRef.current.push({ humanId, actionId, callback });
}
// Start monitoring if not already running
if (!isMonitoringRef.current) {
isMonitoringRef.current = true;
}
};
// Remove a human from monitoring
const removeHumanFromMonitor = (humanId: string) => {
callbacksRef.current = callbacksRef.current.filter(
(entry) => entry.humanId !== humanId
);
// Stop monitoring if no more humans to track
if (callbacksRef.current.length === 0) {
isMonitoringRef.current = false;
}
};
// Check human states every frame
useFrame(() => {
if (!isMonitoringRef.current || callbacksRef.current.length === 0 || !isPlaying || isPaused) return;
callbacksRef.current.forEach(({ humanId, actionId, callback }) => {
const human = getHumanById(humanId);
if (!human) return;
const action = human.point.actions.find((action) => action.actionUuid === actionId);
if (action && human.isActive === false && human.state === 'idle' && human.isPicking && human.currentLoad < action.loadCapacity) {
callback();
removeHumanFromMonitor(humanId); // Remove after triggering
}
});
});
// Cleanup on unmount
useEffect(() => {
return () => {
callbacksRef.current = [];
isMonitoringRef.current = false;
};
}, []);
return {
addHumanToMonitor,
removeHumanFromMonitor,
};
}

View File

@@ -1,16 +1,113 @@
import { useEffect } from 'react'
import HumanUi from './humanUi';
import { useCallback, useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { NavMeshQuery } from '@recast-navigation/core';
import { useNavMesh } from '../../../../../store/builder/store';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useProductContext } from '../../../products/productContext';
import HumanAnimator from './animator/humanAnimator';
function HumanInstance({ human }: { human: HumanStatus }) {
const { navMesh } = useNavMesh();
const { isPlaying } = usePlayButtonStore();
const { materialStore, armBotStore, conveyorStore, vehicleStore, humanStore, storageUnitStore, productStore } = useSceneContext();
const { removeMaterial, setEndTime } = materialStore();
const { getStorageUnitById } = storageUnitStore();
const { getArmBotById } = armBotStore();
const { getConveyorById } = conveyorStore();
const { getVehicleById } = vehicleStore();
const { triggerPointActions } = useTriggerHandler();
const { getActionByUuid, getEventByModelUuid, getTriggerByUuid } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { humans, setHumanActive, setHumanState, setHumanPicking, clearCurrentMaterials, setHumanLoad, decrementHumanLoad, removeLastMaterial, getLastMaterial, incrementIdleTime, incrementActiveTime, resetTime } = humanStore();
const [currentPhase, setCurrentPhase] = useState<string>('init');
const [path, setPath] = useState<[number, number, number][]>([]);
const pauseTimeRef = useRef<number | null>(null);
const idleTimeRef = useRef<number>(0);
const activeTimeRef = useRef<number>(0);
const isPausedRef = useRef<boolean>(false);
const isSpeedRef = useRef<number>(0);
let startTime: number;
let fixedInterval: number;
const { speed } = useAnimationPlaySpeed();
const { isPaused } = usePauseButtonStore();
const previousTimeRef = useRef<number | null>(null);
const animationFrameIdRef = useRef<number | null>(null);
useEffect(() => {
console.log('human: ', human);
}, [human])
isPausedRef.current = isPaused;
}, [isPaused]);
useEffect(() => {
isSpeedRef.current = speed;
}, [speed]);
const computePath = useCallback(
(start: any, end: any) => {
try {
const navMeshQuery = new NavMeshQuery(navMesh);
const { path: segmentPath } = navMeshQuery.computePath(start, end);
if (
segmentPath.length > 0 &&
Math.round(segmentPath[segmentPath.length - 1].x) == Math.round(end.x) &&
Math.round(segmentPath[segmentPath.length - 1].z) == Math.round(end.z)
) {
return segmentPath?.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || [];
} else {
console.log("There is no path here...Choose valid path")
const { path: segmentPaths } = navMeshQuery.computePath(start, start);
return segmentPaths.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || [];
}
} catch {
console.error("Failed to compute path");
return [];
}
},
[navMesh]
);
function humanStatus(modelId: string, status: string) {
// console.log(`${modelId} , ${status}`);
}
function reset() {
setCurrentPhase('init');
setHumanActive(human.modelUuid, false);
setHumanPicking(human.modelUuid, false);
setHumanState(human.modelUuid, 'idle');
setHumanLoad(human.modelUuid, 0);
setPath([]);
startTime = 0;
isPausedRef.current = false;
pauseTimeRef.current = 0;
resetTime(human.modelUuid)
activeTimeRef.current = 0
idleTimeRef.current = 0
previousTimeRef.current = null
if (animationFrameIdRef.current !== null) {
cancelAnimationFrame(animationFrameIdRef.current)
animationFrameIdRef.current = null
}
}
useEffect(() => {
if (isPlaying) {
}
else {
reset()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [human, currentPhase, path, isPlaying]);
return (
<>
<HumanUi />
<HumanAnimator />
</>
)

View File

@@ -124,7 +124,8 @@ function determineExecutionMachineSequences(products: productsSchema): EventsSch
event.type === 'vehicle' ||
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm'
event.type === 'roboticArm' ||
event.type === 'human'
) {
pointToEventMap.set(event.point.uuid, event);
allPoints.push(event.point);

View File

@@ -16,7 +16,8 @@ export async function determineExecutionMachineSequences(products: productsSchem
event.type === 'vehicle' ||
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm'
event.type === 'roboticArm' ||
event.type === 'human'
) {
pointToEventMap.set(event.point.uuid, event);
allPoints.push(event.point);

View File

@@ -19,7 +19,9 @@ export function determineExecutionOrder(products: productsSchema): PointsScheme[
} else if (event.type === 'vehicle' ||
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm') {
event.type === 'roboticArm' ||
event.type === 'human'
) {
pointMap.set(event.point.uuid, event.point);
allPoints.push(event.point);
}

View File

@@ -16,7 +16,9 @@ export async function determineExecutionSequences(products: productsSchema): Pro
} else if (event.type === 'vehicle' ||
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm') {
event.type === 'roboticArm' ||
event.type === 'human'
) {
pointMap.set(event.point.uuid, event.point);
allPoints.push(event.point);
}

View File

@@ -91,7 +91,8 @@ function determineExecutionMachineSequences(products: productsSchema): EventsSch
event.type === 'vehicle' ||
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm'
event.type === 'roboticArm' ||
event.type === 'human'
) {
pointToEventMap.set(event.point.uuid, event);
allPoints.push(event.point);

View File

@@ -6,9 +6,10 @@ import { useVehicleEventManager } from '../../vehicle/eventManager/useVehicleEve
import { useMachineEventManager } from '../../machine/eventManager/useMachineEventManager';
import { useSceneContext } from '../../../scene/sceneContext';
import { useProductContext } from '../../products/productContext';
import { useHumanEventManager } from '../../human/eventManager/useHumanEventManager';
export function useTriggerHandler() {
const { materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, productStore } = useSceneContext();
const { materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, humanStore, storageUnitStore, productStore } = useSceneContext();
const { selectedProductStore } = useProductContext();
const { handleAction } = useActionHandler();
const { selectedProduct } = selectedProductStore();
@@ -19,7 +20,9 @@ export function useTriggerHandler() {
const { addConveyorToMonitor } = useConveyorEventManager();
const { addVehicleToMonitor } = useVehicleEventManager();
const { addMachineToMonitor } = useMachineEventManager();
const { addHumanToMonitor } = useHumanEventManager();
const { getVehicleById } = vehicleStore();
const { getHumanById } = humanStore();
const { getMachineById } = machineStore();
const { getStorageUnitById } = storageUnitStore();
const { getMaterialById, setCurrentLocation, setNextLocation, setPreviousLocation, setIsPaused, setIsVisible, setEndTime } = materialStore();
@@ -256,6 +259,57 @@ export function useTriggerHandler() {
} else if (toEvent?.type === 'storageUnit') {
// Transfer to Storage Unit
} else if (toEvent?.type === 'human') {
// Transfer to Human
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
const material = getMaterialById(materialId);
if (material) {
const triggeredAction = action;
// Handle current action of the material
handleAction(action, materialId);
if (material.next) {
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
const human = getHumanById(trigger.triggeredAsset?.triggeredModel.modelUuid);
setPreviousLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
})
setCurrentLocation(material.materialId, {
modelUuid: material.next.modelUuid,
pointUuid: material.next.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});
setNextLocation(material.materialId, null);
if (action) {
if (human) {
if (human.isActive === false && human.state === 'idle' && human.isPicking && human.currentLoad < (triggeredAction as HumanAction).loadCapacity) {
setIsVisible(materialId, false);
// Handle current action from vehicle
handleAction(action, materialId);
} else {
// Handle current action using Event Manager
addHumanToMonitor(human.modelUuid, triggeredAction.actionUuid, () => {
handleAction(action, materialId);
})
}
}
}
}
}
}
}
} else if (fromEvent?.type === 'vehicle') {
if (toEvent?.type === 'transfer') {

View File

@@ -1,14 +1,15 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import VehicleAnimator from '../animator/vehicleAnimator';
import * as THREE from 'three';
import { NavMeshQuery } from '@recast-navigation/core';
import { useNavMesh } from '../../../../../store/builder/store';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
import MaterialAnimator from '../animator/materialAnimator';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useProductContext } from '../../../products/productContext';
import MaterialAnimator from '../animator/materialAnimator';
import VehicleAnimator from '../animator/vehicleAnimator';
function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) {
const { navMesh } = useNavMesh();
const { isPlaying } = usePlayButtonStore();
@@ -103,10 +104,6 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]),
agvDetail?.point?.action?.pickUpPoint?.position
);
// const toPickupPath = computePath(
// new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]),
// new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2])
// );
setPath(toPickupPath);
setCurrentPhase('stationed-pickup');
setVehicleState(agvDetail.modelUuid, 'running');
@@ -150,7 +147,6 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vehicles, currentPhase, path, isPlaying]);
function animate(currentTime: number) {
if (previousTimeRef.current === null) {
previousTimeRef.current = currentTime;
@@ -527,10 +523,4 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
);
}
export default VehicleInstance;
export default VehicleInstance;

View File

@@ -15,6 +15,10 @@ interface HumansStore {
setHumanActive: (modelUuid: string, isActive: boolean) => void;
setHumanPicking: (modelUuid: string, isPicking: boolean) => void;
setHumanLoad: (modelUuid: string, load: number) => void;
setHumanState: (
modelUuid: string,
newState: HumanStatus["state"]
) => void;
incrementHumanLoad: (modelUuid: string, incrementBy: number) => void;
decrementHumanLoad: (modelUuid: string, decrementBy: number) => void;
@@ -106,6 +110,15 @@ export const createHumanStore = () => {
});
},
setHumanState: (modelUuid, newState) => {
set((state) => {
const human = state.humans.find(h => h.modelUuid === modelUuid);
if (human) {
human.state = newState;
}
});
},
incrementHumanLoad: (modelUuid, incrementBy) => {
set((state) => {
const human = state.humans.find(h => h.modelUuid === modelUuid);