feat: Enhance asset and human event handling with animation and loop capabilities

This commit is contained in:
2025-07-02 17:31:17 +05:30
parent 2f0acbda3c
commit 424df54ff7
8 changed files with 192 additions and 170 deletions

View File

@@ -6,145 +6,128 @@ import PositionInput from "../customInput/PositionInputs";
import RotationInput from "../customInput/RotationInput"; import RotationInput from "../customInput/RotationInput";
import { useSelectedFloorItem, useObjectPosition, useObjectRotation } from "../../../../store/builder/store"; import { useSelectedFloorItem, useObjectPosition, useObjectRotation } from "../../../../store/builder/store";
import { useSceneContext } from "../../../../modules/scene/sceneContext"; import { useSceneContext } from "../../../../modules/scene/sceneContext";
import { useBuilderStore } from "../../../../store/builder/useBuilderStore";
interface UserData { interface UserData {
id: number; // Unique identifier for the user data id: number;
label: string; // Label of the user data field label: string;
value: string; // Value of the user data field value: string;
} }
const AssetProperties: React.FC = () => { const AssetProperties: React.FC = () => {
const [userData, setUserData] = useState<UserData[]>([]); // State to track user data const [userData, setUserData] = useState<UserData[]>([]);
const [nextId, setNextId] = useState(1); // Unique ID for new entries const { selectedFloorItem } = useSelectedFloorItem();
const { selectedFloorItem } = useSelectedFloorItem(); const { objectPosition } = useObjectPosition();
const { objectPosition } = useObjectPosition(); const { objectRotation } = useObjectRotation();
const { objectRotation } = useObjectRotation(); const { assetStore } = useSceneContext();
const { assetStore } = useSceneContext(); const { assets, setCurrentAnimation } = assetStore();
const { assets, setCurrentAnimation } = assetStore() const { loopAnimation } = useBuilderStore();
const [hoveredIndex, setHoveredIndex] = useState<any>(null); const [hoveredIndex, setHoveredIndex] = useState<any>(null);
const [isPlaying, setIsplaying] = useState(false);
// Function to handle adding new user data const handleAddUserData = () => {
const handleAddUserData = () => {
const newUserData: UserData = {
id: nextId,
label: `Property ${nextId}`,
value: "",
}; };
setUserData([...userData, newUserData]);
setNextId(nextId + 1); // Increment the ID for the next entry
};
// Function to update the value of a user data entry const handleUserDataChange = (id: number, newValue: string) => {
const handleUserDataChange = (id: number, newValue: string) => { };
setUserData((prevUserData) =>
prevUserData.map((data) =>
data.id === id ? { ...data, value: newValue } : data
)
);
};
// Remove user data const handleRemoveUserData = (id: number) => {
const handleRemoveUserData = (id: number) => { };
setUserData((prevUserData) =>
prevUserData.filter((data) => data.id !== id)
);
};
const handleAnimationClick = (animation: string) => { const handleAnimationClick = (animation: string) => {
if (selectedFloorItem) { if (selectedFloorItem) {
setCurrentAnimation(selectedFloorItem.uuid, animation, true); setCurrentAnimation(selectedFloorItem.uuid, animation, true, loopAnimation);
}
} }
}
if (!selectedFloorItem) return null; if (!selectedFloorItem) return null;
return ( return (
<div className="asset-properties-container"> <div className="asset-properties-container">
{/* Name */} {/* Name */}
<div className="header">{selectedFloorItem.userData.modelName}</div> <div className="header">{selectedFloorItem.userData.modelName}</div>
<section> <section>
{objectPosition.x && objectPosition.z && {objectPosition &&
<PositionInput <PositionInput
onChange={() => { }} onChange={() => { }}
value1={parseFloat(objectPosition.x.toFixed(5))} value1={parseFloat(objectPosition.x.toFixed(5))}
value2={parseFloat(objectPosition.z.toFixed(5))} value2={parseFloat(objectPosition.z.toFixed(5))}
/> />
} }
{objectRotation.y && {objectRotation &&
<RotationInput <RotationInput
onChange={() => { }} onChange={() => { }}
value={parseFloat(objectRotation.y.toFixed(5))} value={parseFloat(objectRotation.y.toFixed(5))}
/> />
} }
</section> </section>
<section> <section>
<div className="header">Render settings</div> <div className="header">Render settings</div>
<InputToggle inputKey="visible" label="Visible" /> <InputToggle inputKey="visible" label="Visible" />
<InputToggle inputKey="frustumCull" label="Frustum cull" /> <InputToggle inputKey="frustumCull" label="Frustum cull" />
</section> </section>
<section> <section>
<div className="header">User Data</div> <div className="header">User Data</div>
{userData.map((data) => ( {userData.map((data) => (
<div className="input-container"> <div className="input-container">
<InputWithDropDown <InputWithDropDown
key={data.id} key={data.id}
label={data.label} label={data.label}
value={data.value} value={data.value}
editableLabel editableLabel
onChange={(newValue) => handleUserDataChange(data.id, newValue)} // Pass the change handler onChange={(newValue) => handleUserDataChange(data.id, newValue)}
/> />
<div <div
className="remove-button" className="remove-button"
onClick={() => handleRemoveUserData(data.id)} onClick={() => handleRemoveUserData(data.id)}
> >
<RemoveIcon /> <RemoveIcon />
</div> </div>
</div> </div>
))} ))}
{/* Add new user data */} {/* Add new user data */}
<div className="optimize-button" onClick={handleAddUserData}> <div className="optimize-button" onClick={handleAddUserData}>
+ Add + Add
</div>
</section>
<div style={{ display: "flex", flexDirection: "column", outline: "1px solid var(--border-color)" }}>
{selectedFloorItem.uuid && <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>Animations</div>}
{assets.map((asset) => (
<div key={asset.modelUuid} className="asset-item">
{asset.modelUuid === selectedFloorItem.uuid &&
asset.animations &&
asset.animations.length > 0 &&
asset.animations.map((animation, index) => (
<div
key={index}
style={{ gap: "15px", cursor: "pointer", padding: "5px" }}
>
<div
onClick={() => handleAnimationClick(animation)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
style={{
height: "20px",
width: "100%",
borderRadius: "5px",
background:
hoveredIndex === index
? "#7b4cd3"
: "transparent",
}}
>
{animation.charAt(0).toUpperCase() +
animation.slice(1).toLowerCase()}
</div>
</div> </div>
))} </section>
</div> <div style={{ display: "flex", flexDirection: "column", outline: "1px solid var(--border-color)" }}>
))} {selectedFloorItem.uuid && <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>Animations</div>}
</div> {assets.map((asset) => (
</div> <div key={asset.modelUuid} className="asset-item">
); {asset.modelUuid === selectedFloorItem.uuid &&
asset.animations &&
asset.animations.length > 0 &&
asset.animations.map((animation, index) => (
<div
key={index}
style={{ gap: "15px", cursor: "pointer", padding: "5px" }}
>
<div
onClick={() => handleAnimationClick(animation)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
style={{
height: "20px",
width: "100%",
borderRadius: "5px",
background:
hoveredIndex === index
? "#7b4cd3"
: "transparent",
}}
>
{animation.charAt(0).toUpperCase() +
animation.slice(1).toLowerCase()}
</div>
</div>
))}
</div>
))}
</div>
</div>
);
}; };
export default AssetProperties; export default AssetProperties;

View File

@@ -1,4 +1,4 @@
import * as THREE from "three" import * as THREE from "three"
import { useEffect } from 'react' import { useEffect } from 'react'
import { getFloorAssets } from '../../../services/factoryBuilder/asset/floorAsset/getFloorItemsApi'; import { getFloorAssets } from '../../../services/factoryBuilder/asset/floorAsset/getFloorItemsApi';
import { useLoadingProgress, useRenameModeStore, useSelectedFloorItem, useSelectedItem, useSocketStore } from '../../../store/builder/store'; import { useLoadingProgress, useRenameModeStore, useSelectedFloorItem, useSelectedItem, useSocketStore } from '../../../store/builder/store';
@@ -226,7 +226,8 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
modelUuid: item.modelUuid, modelUuid: item.modelUuid,
modelName: item.modelName, modelName: item.modelName,
position: item.position, position: item.position,
rotation: [item.rotation.x, item.rotation.y, item.rotation.z], state: "idle", rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "storageUnit", type: "storageUnit",
point: { point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(), uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
@@ -242,6 +243,36 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
} }
}; };
addEvent(storageEvent); addEvent(storageEvent);
} else if (item.eventData.type === 'Human') {
const humanEvent: HumanEventSchema = {
modelUuid: item.modelUuid,
modelName: item.modelName,
position: item.position,
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "human",
point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],
rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0],
actions: [
{
actionUuid: THREE.MathUtils.generateUUID(),
actionName: "Action 1",
actionType: "animation",
animation: null,
loopAnimation: true,
loadCapacity: 1,
travelPoints: {
startPoint: null,
endPoint: null,
},
triggers: []
}
]
}
}
addEvent(humanEvent);
} }
} else { } else {
assets.push({ assets.push({

View File

@@ -162,6 +162,7 @@ async function handleModelLoad(
// SOCKET // SOCKET
if (selectedItem.type) { if (selectedItem.type) {
console.log('selectedItem: ', selectedItem);
const data = PointsCalculator( const data = PointsCalculator(
selectedItem.type, selectedItem.type,
gltf.scene.clone(), gltf.scene.clone(),
@@ -170,7 +171,7 @@ async function handleModelLoad(
if (!data || !data.points) return; if (!data || !data.points) return;
const eventData: any = { type: selectedItem.type, }; const eventData: any = { type: selectedItem.type };
if (selectedItem.type === "Conveyor") { if (selectedItem.type === "Conveyor") {
const ConveyorEvent: ConveyorEventSchema = { const ConveyorEvent: ConveyorEventSchema = {
@@ -378,6 +379,7 @@ async function handleModelLoad(
actionName: "Action 1", actionName: "Action 1",
actionType: "animation", actionType: "animation",
animation: null, animation: null,
loopAnimation: true,
loadCapacity: 1, loadCapacity: 1,
travelPoints: { travelPoints: {
startPoint: null, startPoint: null,
@@ -416,6 +418,7 @@ async function handleModelLoad(
userId: userId, userId: userId,
}; };
console.log('completeData: ', completeData);
socket.emit("v1:model-asset:add", completeData); socket.emit("v1:model-asset:add", completeData);
const asset: Asset = { const asset: Asset = {

View File

@@ -6,7 +6,7 @@ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { ThreeEvent, useFrame, useThree } from '@react-three/fiber'; import { ThreeEvent, useFrame, useThree } from '@react-three/fiber';
import { useActiveTool, useDeletableFloorItem, useLimitDistance, useRenderDistance, useSelectedFloorItem, useSocketStore, useToggleView, useToolMode } from '../../../../../store/builder/store'; import { useActiveTool, useDeletableFloorItem, useLimitDistance, useRenderDistance, useSelectedFloorItem, useSocketStore, useToggleView, useToolMode } from '../../../../../store/builder/store';
import { AssetBoundingBox } from '../../functions/assetBoundingBox'; import { AssetBoundingBox } from '../../functions/assetBoundingBox';
import { CameraControls, Html } from '@react-three/drei'; import { CameraControls } from '@react-three/drei';
import useModuleStore, { useSubModuleStore } from '../../../../../store/useModuleStore'; import useModuleStore, { useSubModuleStore } from '../../../../../store/useModuleStore';
import { useLeftData, useTopData } from '../../../../../store/visualization/useZone3DWidgetStore'; import { useLeftData, useTopData } from '../../../../../store/visualization/useZone3DWidgetStore';
import { useSelectedAsset } from '../../../../../store/simulation/useSimulationStore'; import { useSelectedAsset } from '../../../../../store/simulation/useSimulationStore';
@@ -15,6 +15,7 @@ import { useParams } from 'react-router-dom';
import { getUserData } from '../../../../../functions/getUserData'; import { getUserData } from '../../../../../functions/getUserData';
import { useSceneContext } from '../../../../scene/sceneContext'; import { useSceneContext } from '../../../../scene/sceneContext';
import { useVersionContext } from '../../../version/versionContext'; import { useVersionContext } from '../../../version/versionContext';
import { SkeletonUtils } from 'three-stdlib';
function Model({ asset }: { readonly asset: Asset }) { function Model({ asset }: { readonly asset: Asset }) {
const { camera, controls, gl } = useThree(); const { camera, controls, gl } = useThree();
@@ -23,7 +24,7 @@ function Model({ asset }: { readonly asset: Asset }) {
const { subModule } = useSubModuleStore(); const { subModule } = useSubModuleStore();
const { activeModule } = useModuleStore(); const { activeModule } = useModuleStore();
const { assetStore, eventStore, productStore } = useSceneContext(); const { assetStore, eventStore, productStore } = useSceneContext();
const { assets, removeAsset, setAnimations } = assetStore(); const { removeAsset, setAnimations, resetAnimation } = assetStore();
const { setTop } = useTopData(); const { setTop } = useTopData();
const { setLeft } = useLeftData(); const { setLeft } = useLeftData();
const { getIsEventInProduct } = productStore(); const { getIsEventInProduct } = productStore();
@@ -33,7 +34,7 @@ function Model({ asset }: { readonly asset: Asset }) {
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset(); const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem(); const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem();
const { setSelectedFloorItem } = useSelectedFloorItem(); const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
const { limitDistance } = useLimitDistance(); const { limitDistance } = useLimitDistance();
const { renderDistance } = useRenderDistance(); const { renderDistance } = useRenderDistance();
const [isRendered, setIsRendered] = useState(false); const [isRendered, setIsRendered] = useState(false);
@@ -46,13 +47,15 @@ function Model({ asset }: { readonly asset: Asset }) {
const { selectedVersion } = selectedVersionStore(); const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams(); const { projectId } = useParams();
const { userId, organization } = getUserData(); const { userId, organization } = getUserData();
const [animationNames, setAnimationNames] = useState<string[]>([]);
const mixerRef = useRef<THREE.AnimationMixer>(); const mixerRef = useRef<THREE.AnimationMixer>();
const actions = useRef<{ [name: string]: THREE.AnimationAction }>({}); const actions = useRef<{ [name: string]: THREE.AnimationAction }>({});
useEffect(() => { useEffect(() => {
setDeletableFloorItem(null); setDeletableFloorItem(null);
}, [activeModule, toolMode]) if (selectedFloorItem === null) {
resetAnimation(asset.modelUuid);
}
}, [activeModule, toolMode, selectedFloorItem])
useEffect(() => { useEffect(() => {
const loader = new GLTFLoader(); const loader = new GLTFLoader();
@@ -62,40 +65,21 @@ function Model({ asset }: { readonly asset: Asset }) {
loader.setDRACOLoader(dracoLoader); loader.setDRACOLoader(dracoLoader);
const loadModel = async () => { const loadModel = async () => {
try { try {
// Check Cache
// const assetId = asset.assetId;
// const cachedModel = THREE.Cache.get(assetId);
// if (cachedModel) {
// setGltfScene(cachedModel.scene.clone());
// calculateBoundingBox(cachedModel.scene);
// return;
// }
// Check Cache // Check Cache
// const assetId = asset.assetId;
// console.log('assetId: ', assetId);
// const cachedModel = THREE.Cache.get(assetId);
// console.log('cachedModel: ', cachedModel);
// if (cachedModel) {
// setGltfScene(cachedModel.scene.clone());
// calculateBoundingBox(cachedModel.scene);
// return;
// }
const assetId = asset.assetId; const assetId = asset.assetId;
const cachedModel = THREE.Cache.get(assetId); const cachedModel = THREE.Cache.get(assetId);
if (cachedModel) { if (cachedModel) {
const clonedScene = cachedModel.scene.clone(); const clone: any = SkeletonUtils.clone(cachedModel.scene);
clonedScene.animations = cachedModel.animations || []; clone.animations = cachedModel.animations || [];
setGltfScene(clonedScene); setGltfScene(clone);
calculateBoundingBox(clonedScene); calculateBoundingBox(clone);
if (cachedModel.animations && clonedScene.animations.length > 0) { if (cachedModel.animations && clone.animations.length > 0) {
const animationName = clonedScene.animations.map((clip: any) => clip.name); const animationName = clone.animations.map((clip: any) => clip.name);
setAnimationNames(animationName)
setAnimations(asset.modelUuid, animationName) setAnimations(asset.modelUuid, animationName)
mixerRef.current = new THREE.AnimationMixer(clonedScene); mixerRef.current = new THREE.AnimationMixer(clone);
clonedScene.animations.forEach((animation: any) => { clone.animations.forEach((animation: any) => {
const action = mixerRef.current!.clipAction(animation); const action = mixerRef.current!.clipAction(animation);
actions.current[animation.name] = action; actions.current[animation.name] = action;
}); });
@@ -293,28 +277,27 @@ function Model({ asset }: { readonly asset: Asset }) {
clearSelectedAsset() clearSelectedAsset()
} }
} }
useFrame((_, delta) => { useFrame((_, delta) => {
if (mixerRef.current) { if (mixerRef.current) {
mixerRef.current.update(delta); mixerRef.current.update(delta);
} }
}); });
useEffect(() => { useEffect(() => {
if (asset.animationState && asset.animationState.isPlaying) {
if (asset.animationState && asset.animationState.playing) {
if (!mixerRef.current) return; if (!mixerRef.current) return;
Object.values(actions.current).forEach((action) => action.stop()); Object.values(actions.current).forEach((action) => action.stop());
const action = actions.current[asset.animationState.current]; const action = actions.current[asset.animationState.current];
if (action && asset.animationState?.playing) { if (action && asset.animationState?.isPlaying) {
action.reset().setLoop(THREE.LoopOnce, 1).play(); const loopMode = asset.animationState.loopAnimation ? THREE.LoopRepeat : THREE.LoopOnce;
action.reset().setLoop(loopMode, loopMode === THREE.LoopRepeat ? Infinity : 1).play();
} }
} else { } else {
Object.values(actions.current).forEach((action) => action.stop()); Object.values(actions.current).forEach((action) => action.stop());
} }
}, [asset.animationState]) }, [asset.animationState])
return ( return (

View File

@@ -22,7 +22,8 @@ interface AssetsStore {
// Animation controls // Animation controls
setAnimations: (modelUuid: string, animations: string[]) => void; setAnimations: (modelUuid: string, animations: string[]) => void;
setCurrentAnimation: (modelUuid: string, current: string, isPlaying: boolean) => void; setCurrentAnimation: (modelUuid: string, current: string, isisPlaying: boolean, loopAnimation: boolean) => void;
resetAnimation: (modelUuid: string) => void;
addAnimation: (modelUuid: string, animation: string) => void; addAnimation: (modelUuid: string, animation: string) => void;
removeAnimation: (modelUuid: string, animation: string) => void; removeAnimation: (modelUuid: string, animation: string) => void;
@@ -149,18 +150,28 @@ export const createAssetStore = () => {
if (asset) { if (asset) {
asset.animations = animations; asset.animations = animations;
if (!asset.animationState) { if (!asset.animationState) {
asset.animationState = { current: '', playing: false }; asset.animationState = { current: '', isPlaying: false, loopAnimation: true };
} }
} }
}); });
}, },
setCurrentAnimation: (modelUuid, current, isPlaying) => { setCurrentAnimation: (modelUuid, current, isisPlaying, loopAnimation) => {
set((state) => { set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid); const asset = state.assets.find(a => a.modelUuid === modelUuid);
if (asset?.animationState) { if (asset?.animationState) {
asset.animationState.current = current; asset.animationState.current = current;
asset.animationState.playing = isPlaying; asset.animationState.isPlaying = isisPlaying;
asset.animationState.loopAnimation = loopAnimation;
}
});
},
resetAnimation: (modelUuid) => {
set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid);
if (asset?.animationState) {
asset.animationState = { current: '', isPlaying: false, loopAnimation: true };
} }
}); });
}, },
@@ -184,7 +195,7 @@ export const createAssetStore = () => {
if (asset?.animations) { if (asset?.animations) {
asset.animations = asset.animations.filter(a => a !== animation); asset.animations = asset.animations.filter(a => a !== animation);
if (asset.animationState?.current === animation) { if (asset.animationState?.current === animation) {
asset.animationState.playing = false; asset.animationState.isPlaying = false;
asset.animationState.current = ''; asset.animationState.current = '';
} }
} }

View File

@@ -15,6 +15,7 @@ interface BuilderState {
// Floor Asset // Floor Asset
selectedFloorAsset: Object3D | null; selectedFloorAsset: Object3D | null;
loopAnimation: boolean;
// Wall Settings // Wall Settings
selectedWall: Object3D | null; selectedWall: Object3D | null;
@@ -64,6 +65,7 @@ interface BuilderState {
// Setters - Floor Asset // Setters - Floor Asset
setSelectedFloorAsset: (asset: Object3D | null) => void; setSelectedFloorAsset: (asset: Object3D | null) => void;
setLoopAnimation: (loop: boolean) => void;
// Setters - Wall // Setters - Wall
setSelectedWall: (wall: Object3D | null) => void; setSelectedWall: (wall: Object3D | null) => void;
@@ -118,6 +120,7 @@ export const useBuilderStore = create<BuilderState>()(
deletableWallAsset: null, deletableWallAsset: null,
selectedFloorAsset: null, selectedFloorAsset: null,
loopAnimation: true,
selectedWall: null, selectedWall: null,
wallThickness: 0.5, wallThickness: 0.5,
@@ -197,6 +200,12 @@ export const useBuilderStore = create<BuilderState>()(
}); });
}, },
setLoopAnimation(loopAnimation: boolean) {
set((state) => {
state.loopAnimation = loopAnimation;
});
},
// === Setters: Wall === // === Setters: Wall ===
setSelectedWall: (wall: Object3D | null) => { setSelectedWall: (wall: Object3D | null) => {

View File

@@ -26,7 +26,8 @@ interface Asset {
animations?: string[]; animations?: string[];
animationState?: { animationState?: {
current: string; current: string;
playing: boolean; isPlaying: boolean;
loopAnimation: boolean;
}; };
eventData?: { eventData?: {
type: string; type: string;

View File

@@ -74,6 +74,7 @@ interface HumanAction {
actionName: string; actionName: string;
actionType: "animation" | "animatedTravel"; actionType: "animation" | "animatedTravel";
animation: string | null; animation: string | null;
loopAnimation: boolean;
loadCapacity: number; loadCapacity: number;
travelPoints?: { startPoint: [number, number, number] | null; endPoint: [number, number, number] | null; } travelPoints?: { startPoint: [number, number, number] | null; endPoint: [number, number, number] | null; }
triggers: TriggerSchema[]; triggers: TriggerSchema[];