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 { useSelectedFloorItem, useObjectPosition, useObjectRotation } from "../../../../store/builder/store";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import { useBuilderStore } from "../../../../store/builder/useBuilderStore";
interface UserData {
id: number; // Unique identifier for the user data
label: string; // Label of the user data field
value: string; // Value of the user data field
id: number;
label: string;
value: string;
}
const AssetProperties: React.FC = () => {
const [userData, setUserData] = useState<UserData[]>([]); // State to track user data
const [nextId, setNextId] = useState(1); // Unique ID for new entries
const { selectedFloorItem } = useSelectedFloorItem();
const { objectPosition } = useObjectPosition();
const { objectRotation } = useObjectRotation();
const { assetStore } = useSceneContext();
const { assets, setCurrentAnimation } = assetStore()
const [hoveredIndex, setHoveredIndex] = useState<any>(null);
const [isPlaying, setIsplaying] = useState(false);
// Function to handle adding new user data
const handleAddUserData = () => {
const newUserData: UserData = {
id: nextId,
label: `Property ${nextId}`,
value: "",
const [userData, setUserData] = useState<UserData[]>([]);
const { selectedFloorItem } = useSelectedFloorItem();
const { objectPosition } = useObjectPosition();
const { objectRotation } = useObjectRotation();
const { assetStore } = useSceneContext();
const { assets, setCurrentAnimation } = assetStore();
const { loopAnimation } = useBuilderStore();
const [hoveredIndex, setHoveredIndex] = useState<any>(null);
const handleAddUserData = () => {
};
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) => {
setUserData((prevUserData) =>
prevUserData.map((data) =>
data.id === id ? { ...data, value: newValue } : data
)
);
};
const handleUserDataChange = (id: number, newValue: string) => {
};
// Remove user data
const handleRemoveUserData = (id: number) => {
setUserData((prevUserData) =>
prevUserData.filter((data) => data.id !== id)
);
};
const handleRemoveUserData = (id: number) => {
};
const handleAnimationClick = (animation: string) => {
if (selectedFloorItem) {
setCurrentAnimation(selectedFloorItem.uuid, animation, true);
const handleAnimationClick = (animation: string) => {
if (selectedFloorItem) {
setCurrentAnimation(selectedFloorItem.uuid, animation, true, loopAnimation);
}
}
}
if (!selectedFloorItem) return null;
if (!selectedFloorItem) return null;
return (
<div className="asset-properties-container">
{/* Name */}
<div className="header">{selectedFloorItem.userData.modelName}</div>
<section>
{objectPosition.x && objectPosition.z &&
<PositionInput
onChange={() => { }}
value1={parseFloat(objectPosition.x.toFixed(5))}
value2={parseFloat(objectPosition.z.toFixed(5))}
/>
}
{objectRotation.y &&
<RotationInput
onChange={() => { }}
value={parseFloat(objectRotation.y.toFixed(5))}
/>
}
</section>
return (
<div className="asset-properties-container">
{/* Name */}
<div className="header">{selectedFloorItem.userData.modelName}</div>
<section>
{objectPosition &&
<PositionInput
onChange={() => { }}
value1={parseFloat(objectPosition.x.toFixed(5))}
value2={parseFloat(objectPosition.z.toFixed(5))}
/>
}
{objectRotation &&
<RotationInput
onChange={() => { }}
value={parseFloat(objectRotation.y.toFixed(5))}
/>
}
</section>
<section>
<div className="header">Render settings</div>
<InputToggle inputKey="visible" label="Visible" />
<InputToggle inputKey="frustumCull" label="Frustum cull" />
</section>
<section>
<div className="header">Render settings</div>
<InputToggle inputKey="visible" label="Visible" />
<InputToggle inputKey="frustumCull" label="Frustum cull" />
</section>
<section>
<div className="header">User Data</div>
{userData.map((data) => (
<div className="input-container">
<InputWithDropDown
key={data.id}
label={data.label}
value={data.value}
editableLabel
onChange={(newValue) => handleUserDataChange(data.id, newValue)} // Pass the change handler
/>
<div
className="remove-button"
onClick={() => handleRemoveUserData(data.id)}
>
<RemoveIcon />
</div>
</div>
))}
<section>
<div className="header">User Data</div>
{userData.map((data) => (
<div className="input-container">
<InputWithDropDown
key={data.id}
label={data.label}
value={data.value}
editableLabel
onChange={(newValue) => handleUserDataChange(data.id, newValue)}
/>
<div
className="remove-button"
onClick={() => handleRemoveUserData(data.id)}
>
<RemoveIcon />
</div>
</div>
))}
{/* Add new user data */}
<div className="optimize-button" onClick={handleAddUserData}>
+ 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>
{/* Add new user data */}
<div className="optimize-button" onClick={handleAddUserData}>
+ Add
</div>
))}
</div>
))}
</div>
</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>
))}
</div>
</div>
);
};
export default AssetProperties;

View File

@@ -1,4 +1,4 @@
import * as THREE from "three"
import * as THREE from "three"
import { useEffect } from 'react'
import { getFloorAssets } from '../../../services/factoryBuilder/asset/floorAsset/getFloorItemsApi';
import { useLoadingProgress, useRenameModeStore, useSelectedFloorItem, useSelectedItem, useSocketStore } from '../../../store/builder/store';
@@ -226,7 +226,8 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
modelUuid: item.modelUuid,
modelName: item.modelName,
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",
point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
@@ -242,6 +243,36 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
}
};
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 {
assets.push({

View File

@@ -162,6 +162,7 @@ async function handleModelLoad(
// SOCKET
if (selectedItem.type) {
console.log('selectedItem: ', selectedItem);
const data = PointsCalculator(
selectedItem.type,
gltf.scene.clone(),
@@ -170,7 +171,7 @@ async function handleModelLoad(
if (!data || !data.points) return;
const eventData: any = { type: selectedItem.type, };
const eventData: any = { type: selectedItem.type };
if (selectedItem.type === "Conveyor") {
const ConveyorEvent: ConveyorEventSchema = {
@@ -378,6 +379,7 @@ async function handleModelLoad(
actionName: "Action 1",
actionType: "animation",
animation: null,
loopAnimation: true,
loadCapacity: 1,
travelPoints: {
startPoint: null,
@@ -416,6 +418,7 @@ async function handleModelLoad(
userId: userId,
};
console.log('completeData: ', completeData);
socket.emit("v1:model-asset:add", completeData);
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 { useActiveTool, useDeletableFloorItem, useLimitDistance, useRenderDistance, useSelectedFloorItem, useSocketStore, useToggleView, useToolMode } from '../../../../../store/builder/store';
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 { useLeftData, useTopData } from '../../../../../store/visualization/useZone3DWidgetStore';
import { useSelectedAsset } from '../../../../../store/simulation/useSimulationStore';
@@ -15,6 +15,7 @@ import { useParams } from 'react-router-dom';
import { getUserData } from '../../../../../functions/getUserData';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useVersionContext } from '../../../version/versionContext';
import { SkeletonUtils } from 'three-stdlib';
function Model({ asset }: { readonly asset: Asset }) {
const { camera, controls, gl } = useThree();
@@ -23,7 +24,7 @@ function Model({ asset }: { readonly asset: Asset }) {
const { subModule } = useSubModuleStore();
const { activeModule } = useModuleStore();
const { assetStore, eventStore, productStore } = useSceneContext();
const { assets, removeAsset, setAnimations } = assetStore();
const { removeAsset, setAnimations, resetAnimation } = assetStore();
const { setTop } = useTopData();
const { setLeft } = useLeftData();
const { getIsEventInProduct } = productStore();
@@ -33,7 +34,7 @@ function Model({ asset }: { readonly asset: Asset }) {
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
const { socket } = useSocketStore();
const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem();
const { setSelectedFloorItem } = useSelectedFloorItem();
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
const { limitDistance } = useLimitDistance();
const { renderDistance } = useRenderDistance();
const [isRendered, setIsRendered] = useState(false);
@@ -46,13 +47,15 @@ function Model({ asset }: { readonly asset: Asset }) {
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
const { userId, organization } = getUserData();
const [animationNames, setAnimationNames] = useState<string[]>([]);
const mixerRef = useRef<THREE.AnimationMixer>();
const actions = useRef<{ [name: string]: THREE.AnimationAction }>({});
useEffect(() => {
setDeletableFloorItem(null);
}, [activeModule, toolMode])
if (selectedFloorItem === null) {
resetAnimation(asset.modelUuid);
}
}, [activeModule, toolMode, selectedFloorItem])
useEffect(() => {
const loader = new GLTFLoader();
@@ -62,40 +65,21 @@ function Model({ asset }: { readonly asset: Asset }) {
loader.setDRACOLoader(dracoLoader);
const loadModel = async () => {
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
// 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 cachedModel = THREE.Cache.get(assetId);
if (cachedModel) {
const clonedScene = cachedModel.scene.clone();
clonedScene.animations = cachedModel.animations || [];
setGltfScene(clonedScene);
calculateBoundingBox(clonedScene);
if (cachedModel.animations && clonedScene.animations.length > 0) {
const animationName = clonedScene.animations.map((clip: any) => clip.name);
setAnimationNames(animationName)
const clone: any = SkeletonUtils.clone(cachedModel.scene);
clone.animations = cachedModel.animations || [];
setGltfScene(clone);
calculateBoundingBox(clone);
if (cachedModel.animations && clone.animations.length > 0) {
const animationName = clone.animations.map((clip: any) => clip.name);
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);
actions.current[animation.name] = action;
});
@@ -293,28 +277,27 @@ function Model({ asset }: { readonly asset: Asset }) {
clearSelectedAsset()
}
}
useFrame((_, delta) => {
if (mixerRef.current) {
mixerRef.current.update(delta);
}
});
useEffect(() => {
if (asset.animationState && asset.animationState.playing) {
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?.playing) {
action.reset().setLoop(THREE.LoopOnce, 1).play();
if (action && asset.animationState?.isPlaying) {
const loopMode = asset.animationState.loopAnimation ? THREE.LoopRepeat : THREE.LoopOnce;
action.reset().setLoop(loopMode, loopMode === THREE.LoopRepeat ? Infinity : 1).play();
}
} else {
Object.values(actions.current).forEach((action) => action.stop());
}
}, [asset.animationState])
return (

View File

@@ -22,7 +22,8 @@ interface AssetsStore {
// Animation controls
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;
removeAnimation: (modelUuid: string, animation: string) => void;
@@ -149,18 +150,28 @@ export const createAssetStore = () => {
if (asset) {
asset.animations = animations;
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) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid);
if (asset?.animationState) {
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) {
asset.animations = asset.animations.filter(a => a !== animation);
if (asset.animationState?.current === animation) {
asset.animationState.playing = false;
asset.animationState.isPlaying = false;
asset.animationState.current = '';
}
}

View File

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

View File

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

View File

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