feat: Add human mechanics and event handling, including UI components and action management

This commit is contained in:
2025-07-03 12:09:31 +05:30
parent 424df54ff7
commit b5c69f3335
13 changed files with 469 additions and 48 deletions

View File

@@ -8,6 +8,7 @@ import VehicleMechanics from "./mechanics/vehicleMechanics";
import RoboticArmMechanics from "./mechanics/roboticArmMechanics"; import RoboticArmMechanics from "./mechanics/roboticArmMechanics";
import MachineMechanics from "./mechanics/machineMechanics"; import MachineMechanics from "./mechanics/machineMechanics";
import StorageMechanics from "./mechanics/storageMechanics"; import StorageMechanics from "./mechanics/storageMechanics";
import HumanMechanics from "./mechanics/humanMechanics";
import { AddIcon } from "../../../../icons/ExportCommonIcons"; import { AddIcon } from "../../../../icons/ExportCommonIcons";
import { handleAddEventToProduct } from "../../../../../modules/simulation/events/points/functions/handleAddEventToProduct"; import { handleAddEventToProduct } from "../../../../../modules/simulation/events/points/functions/handleAddEventToProduct";
import { useProductContext } from "../../../../../modules/simulation/products/productContext"; import { useProductContext } from "../../../../../modules/simulation/products/productContext";
@@ -61,6 +62,8 @@ const EventProperties: React.FC = () => {
return "machine"; return "machine";
case "storageUnit": case "storageUnit":
return "storageUnit"; return "storageUnit";
case "human":
return "human";
default: default:
return null; return null;
} }
@@ -80,6 +83,7 @@ const EventProperties: React.FC = () => {
{assetType === "roboticArm" && <RoboticArmMechanics />} {assetType === "roboticArm" && <RoboticArmMechanics />}
{assetType === "machine" && <MachineMechanics />} {assetType === "machine" && <MachineMechanics />}
{assetType === "storageUnit" && <StorageMechanics />} {assetType === "storageUnit" && <StorageMechanics />}
{assetType === "human" && <HumanMechanics />}
</> </>
)} )}
{!currentEventData && selectedEventSphere && ( {!currentEventData && selectedEventSphere && (

View File

@@ -0,0 +1,373 @@
import { useEffect, useState } from "react";
import { MathUtils } from "three";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
import RenameInput from "../../../../../ui/inputs/RenameInput";
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
import Trigger from "../trigger/Trigger";
import { useSelectedEventData, useSelectedAction } from "../../../../../../store/simulation/useSimulationStore";
import PickAndPlaceAction from "../actions/PickAndPlaceAction";
import ActionsList from "../components/ActionsList";
import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi";
import { useProductContext } from "../../../../../../modules/simulation/products/productContext";
import { useParams } from "react-router-dom";
import { useVersionContext } from "../../../../../../modules/builder/version/versionContext";
import { useSceneContext } from "../../../../../../modules/scene/sceneContext";
import InputToggle from "../../../../../ui/inputs/InputToggle";
function HumanMechanics() {
const [activeOption, setActiveOption] = useState<"animation" | "animatedTravel">("animation");
const [activeAnimationOption, setActiveAnimationOption] = useState("");
const [animationOptions, setAnimationOptions] = useState<string[]>([]);
const [speed, setSpeed] = useState("0.5");
const [currentAction, setCurrentAction] = useState<HumanAction | undefined>();
const [isLoopAnimation, setIsLoopAnimation] = useState(false);
const [selectedPointData, setSelectedPointData] = useState<HumanPointSchema | undefined>();
const { selectedEventData } = useSelectedEventData();
const { productStore, assetStore } = useSceneContext();
const { getAssetById } = assetStore();
const { getPointByUuid, getEventByModelUuid, updateEvent, updateAction, addAction, removeAction } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { selectedAction, setSelectedAction, clearSelectedAction } = useSelectedAction();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
useEffect(() => {
if (selectedEventData) {
const point = getPointByUuid(
selectedProduct.productUuid,
selectedEventData.data.modelUuid,
selectedEventData.selectedPoint
) as HumanPointSchema | undefined;
if (point?.actions) {
setSelectedPointData(point);
if (point.actions.length > 0) {
setSelectedAction(point.actions[0].actionUuid, point.actions[0].actionName);
const asset = getAssetById(selectedEventData.data.modelUuid);
if (asset && asset.animations) {
setAnimationOptions(asset.animations)
}
}
}
} else {
clearSelectedAction();
}
}, [selectedEventData, selectedProduct]);
useEffect(() => {
if (selectedEventData && selectedProduct.productUuid) {
const event = getEventByModelUuid(
selectedProduct.productUuid,
selectedEventData.data.modelUuid
) as HumanEventSchema | undefined;
if (event?.speed !== undefined) {
setSpeed(event.speed.toString());
}
const point = getPointByUuid(
selectedProduct.productUuid,
selectedEventData.data.modelUuid,
selectedEventData.selectedPoint
) as HumanPointSchema | undefined;
if (point?.actions) {
setSelectedPointData(point);
const action = point.actions.find((a) => a.actionUuid === selectedAction.actionId);
if (action) {
setCurrentAction(action);
setIsLoopAnimation(action.loopAnimation ?? false);
setActiveOption(action.actionType as "animation" | "animatedTravel");
setActiveAnimationOption(action.animation || '')
}
}
} else {
clearSelectedAction();
setCurrentAction(undefined);
setSpeed("0.5");
}
}, [selectedEventData, selectedProduct, selectedAction]);
const updateBackend = (
productName: string,
productUuid: string,
projectId: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName,
productUuid,
projectId,
eventDatas: eventData,
versionId: selectedVersion?.versionId || "",
});
};
const handleSelectActionType = (actionType: string) => {
if (!selectedAction.actionId) return;
setActiveOption(actionType as "animation" | "animatedTravel");
const event = updateAction(
selectedProduct.productUuid,
selectedAction.actionId,
{ actionType: actionType as "animation" | "animatedTravel" }
);
if (selectedPointData) {
const updatedActions = selectedPointData.actions.map((action) =>
action.actionUuid === selectedAction.actionId
? { ...action, actionType: actionType as "animation" | "animatedTravel" }
: action
);
setSelectedPointData({ ...selectedPointData, actions: updatedActions });
}
if (event) {
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
}
}
const handleSelectAnimation = (animationOption: string) => {
if (!selectedAction.actionId) return;
setActiveAnimationOption(animationOption);
const event = updateAction(
selectedProduct.productUuid,
selectedAction.actionId,
{ animation: animationOption }
);
if (selectedPointData) {
const updatedActions = selectedPointData.actions.map((action) =>
action.actionUuid === selectedAction.actionId
? { ...action, animation: animationOption }
: action
);
setSelectedPointData({ ...selectedPointData, actions: updatedActions });
}
if (event) {
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
}
}
const handleLoopAnimationChange = () => {
if (!selectedAction.actionId || !currentAction) return;
const updatedValue = !isLoopAnimation;
setIsLoopAnimation(updatedValue);
const event = updateAction(
selectedProduct.productUuid,
selectedAction.actionId,
{ loopAnimation: updatedValue }
);
if (selectedPointData) {
const updatedActions = selectedPointData.actions.map((action) =>
action.actionUuid === selectedAction.actionId
? { ...action, loopAnimation: updatedValue }
: action
);
setSelectedPointData({ ...selectedPointData, actions: updatedActions });
setCurrentAction(updatedActions.find((a) => a.actionUuid === selectedAction.actionId));
}
if (event) {
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
}
};
const handleRenameAction = (newName: string) => {
if (!selectedAction.actionId) return;
const event = updateAction(
selectedProduct.productUuid,
selectedAction.actionId,
{ actionName: newName }
);
if (selectedPointData) {
const updatedActions = selectedPointData.actions.map((action) =>
action.actionUuid === selectedAction.actionId
? { ...action, actionName: newName }
: action
);
setSelectedPointData({ ...selectedPointData, actions: updatedActions });
}
if (event) {
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
}
};
const handleSpeedChange = (value: string) => {
if (!selectedEventData) return;
const event = updateEvent(
selectedProduct.productUuid,
selectedEventData.data.modelUuid,
{ speed: parseFloat(value) }
);
if (event) {
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
}
};
const handleClearPoints = () => {
if (!selectedAction.actionId || !selectedPointData) return;
const event = updateAction(
selectedProduct.productUuid,
selectedAction.actionId,
{
travelPoints: {
startPoint: null,
endPoint: null,
},
}
);
if (event) {
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
}
};
const handleAddAction = () => {
if (!selectedEventData || !selectedPointData) return;
const newAction: HumanAction = {
actionUuid: MathUtils.generateUUID(),
actionName: `Action ${selectedPointData.actions.length + 1}`,
actionType: "animation",
animation: null,
loadCapacity: 1,
loopAnimation: true,
travelPoints: {
startPoint: null,
endPoint: null,
},
triggers: [],
};
const event = addAction(
selectedProduct.productUuid,
selectedEventData.data.modelUuid,
selectedEventData.selectedPoint,
newAction
);
if (event) {
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
}
const updatedPoint = { ...selectedPointData, actions: [...selectedPointData.actions, newAction] };
setSelectedPointData(updatedPoint);
setSelectedAction(newAction.actionUuid, newAction.actionName);
};
const handleDeleteAction = (actionUuid: string) => {
if (!selectedPointData) return;
const event = removeAction(selectedProduct.productUuid, actionUuid);
if (event) {
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
}
const index = selectedPointData.actions.findIndex((a) => a.actionUuid === actionUuid);
const newActions = selectedPointData.actions.filter((a) => a.actionUuid !== actionUuid);
const updatedPoint = { ...selectedPointData, actions: newActions };
setSelectedPointData(updatedPoint);
if (selectedAction.actionId === actionUuid) {
const nextAction = newActions[index] || newActions[index - 1];
if (nextAction) {
setSelectedAction(nextAction.actionUuid, nextAction.actionName);
} else {
clearSelectedAction();
}
}
};
const availableActions = {
defaultOption: "animatedTravel",
options: ["animation", "animatedTravel"],
};
return (
<>
<div className="global-props section">
<div className="property-list-container">
<div className="property-item">
<InputWithDropDown
label="Speed"
value={speed}
min={0}
step={0.1}
defaultValue="0.5"
max={10}
activeOption="m/s"
onClick={() => { }}
onChange={handleSpeedChange}
/>
</div>
</div>
</div>
<section>
<ActionsList
selectedPointData={selectedPointData}
multipleAction
handleAddAction={handleAddAction}
handleDeleteAction={handleDeleteAction}
/>
{selectedAction.actionId && currentAction && (
<div className="selected-actions-details">
<div className="selected-actions-header">
<RenameInput
value={selectedAction.actionName || ""}
onRename={handleRenameAction}
/>
</div>
<div className="selected-actions-list">
<LabledDropdown
label="Action Type"
defaultOption={activeOption}
options={availableActions.options}
onSelect={handleSelectActionType}
disabled={true}
/>
<LabledDropdown
label="Animation"
defaultOption={activeAnimationOption}
options={animationOptions}
onSelect={handleSelectAnimation}
disabled={true}
/>
<InputToggle
value={isLoopAnimation}
inputKey=""
label="Loop Animation"
onClick={handleLoopAnimationChange}
/>
{activeOption === 'animatedTravel' &&
<PickAndPlaceAction clearPoints={handleClearPoints} />
}
</div>
<div className="tirgger">
<Trigger
selectedPointData={selectedPointData as any}
type={"Human"}
/>
</div>
</div>
)}
</section>
</>
);
}
export default HumanMechanics;

View File

@@ -13,7 +13,7 @@ import { useSceneContext } from "../../../../../../modules/scene/sceneContext";
type TriggerProps = { type TriggerProps = {
selectedPointData?: PointsScheme | undefined; selectedPointData?: PointsScheme | undefined;
type?: "Conveyor" | "Vehicle" | "RoboticArm" | "Machine" | "StorageUnit"; type?: "Conveyor" | "Vehicle" | "RoboticArm" | "Machine" | "StorageUnit" | "Human";
}; };
const Trigger = ({ selectedPointData, type }: TriggerProps) => { const Trigger = ({ selectedPointData, type }: TriggerProps) => {
@@ -38,7 +38,7 @@ const Trigger = ({ selectedPointData, type }: TriggerProps) => {
if (type === "Conveyor" || type === "Vehicle" || type === "Machine" || type === "StorageUnit") { if (type === "Conveyor" || type === "Vehicle" || type === "Machine" || type === "StorageUnit") {
actionUuid = (selectedPointData as | ConveyorPointSchema | VehiclePointSchema | MachinePointSchema | StoragePointSchema).action?.actionUuid; actionUuid = (selectedPointData as | ConveyorPointSchema | VehiclePointSchema | MachinePointSchema | StoragePointSchema).action?.actionUuid;
} else if (type === "RoboticArm" && selectedAction.actionId) { } else if ((type === "RoboticArm" || type === "Human") && selectedAction.actionId) {
actionUuid = selectedAction.actionId; actionUuid = selectedAction.actionId;
} }
@@ -365,18 +365,16 @@ const Trigger = ({ selectedPointData, type }: TriggerProps) => {
} }
/> />
</button> </button>
{triggers.length > 1 && ( <button
<button id="remove-trigger-button"
id="remove-trigger-button" className="remove-button"
className="remove-button" onClick={(e) => {
onClick={(e) => { e.stopPropagation();
e.stopPropagation(); handleRemoveTrigger(trigger.triggerUuid);
handleRemoveTrigger(trigger.triggerUuid); }}
}} >
> <RemoveIcon />
<RemoveIcon /> </button>
</button>
)}
</div> </div>
))} ))}
</div> </div>

View File

@@ -251,6 +251,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
rotation: [item.rotation.x, item.rotation.y, item.rotation.z], rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle", state: "idle",
type: "human", type: "human",
speed: 1,
point: { point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(), 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], position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],

View File

@@ -369,6 +369,7 @@ async function handleModelLoad(
rotation: newFloorItem.rotation, rotation: newFloorItem.rotation,
state: "idle", state: "idle",
type: "human", type: "human",
speed: 1,
point: { point: {
uuid: THREE.MathUtils.generateUUID(), uuid: THREE.MathUtils.generateUUID(),
position: [data.points[0].x, data.points[0].y, data.points[0].z], position: [data.points[0].x, data.points[0].y, data.points[0].z],

View File

@@ -30,7 +30,7 @@ export const handleAddEventToProduct = ({
projectId: projectId || '', projectId: projectId || '',
eventDatas: event eventDatas: event
}).then((data) => { }).then((data) => {
// console.log(data); console.log(data);
}) })
if (clearSelectedAsset) { if (clearSelectedAsset) {

View File

@@ -1,6 +1,27 @@
import HumanInstances from './instances/humanInstances' import HumanInstances from './instances/humanInstances'
import { useEffect, useState } from "react";
import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import { useSceneContext } from "../../scene/sceneContext";
function Human() { function Human() {
const { humanStore } = useSceneContext();
const { getHumanById } = humanStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { isPlaying } = usePlayButtonStore();
const [isHumanSelected, setIsHumanSelected] = useState(false);
useEffect(() => {
if (selectedEventSphere) {
const selectedVehicle = getHumanById(selectedEventSphere.userData.modelUuid);
if (selectedVehicle) {
setIsHumanSelected(true);
} else {
setIsHumanSelected(false);
}
}
}, [getHumanById, selectedEventSphere])
return ( return (
<> <>

View File

@@ -1,4 +1,5 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import HumanUi from './humanUi';
function HumanInstance({ human }: { human: HumanStatus }) { function HumanInstance({ human }: { human: HumanStatus }) {
@@ -8,6 +9,9 @@ function HumanInstance({ human }: { human: HumanStatus }) {
return ( return (
<> <>
<HumanUi human={human} />
</> </>
) )
} }

View File

@@ -0,0 +1,10 @@
import React from 'react'
function HumanUi({ human }: { human: HumanStatus }) {
return (
<>
</>
)
}
export default HumanUi

View File

@@ -1,38 +1,37 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const upsertProductOrEventApi = async (body: any) => { export const upsertProductOrEventApi = async (body: any) => {
try { try {
const response = await fetch( const response = await fetch(
`${url_Backend_dwinzo}/api/V1/ProductUpsert`, `${url_Backend_dwinzo}/api/V1/ProductUpsert`,
{ {
method: "POST", method: "POST",
headers: { headers: {
Authorization: "Bearer <access_token>", Authorization: "Bearer <access_token>",
"Content-Type": "application/json", "Content-Type": "application/json",
token: localStorage.getItem("token") || "", token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "", refresh_token: localStorage.getItem("refreshToken") || "",
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
} }
); );
const newAccessToken = response.headers.get("x-access-token"); const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) { if (newAccessToken) {
//console.log("New token received:", newAccessToken); localStorage.setItem("token", newAccessToken);
localStorage.setItem("token", newAccessToken); }
}
if (!response.ok) { if (!response.ok) {
console.error("Failed to add product or event"); console.error("Failed to add product or event");
} }
const result = await response.json(); const result = await response.json();
return result; return result;
} catch (error) { } catch (error) {
echo.error("Failed to upsert product Or eventApi"); echo.error("Failed to upsert product Or eventApi");
if (error instanceof Error) { if (error instanceof Error) {
console.log(error.message); console.log(error.message);
} else { } else {
console.log("An unknown error occurred"); console.log("An unknown error occurred");
}
} }
}
}; };

View File

@@ -32,13 +32,13 @@ type ProductsStore = {
productUuid: string, productUuid: string,
modelUuid: string, modelUuid: string,
pointUuid: string, pointUuid: string,
action: ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action'] action: ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action'] | HumanPointSchema['actions'][0]
) => EventsSchema | undefined; ) => EventsSchema | undefined;
removeAction: (productUuid: string, actionUuid: string) => EventsSchema | undefined; removeAction: (productUuid: string, actionUuid: string) => EventsSchema | undefined;
updateAction: ( updateAction: (
productUuid: string, productUuid: string,
actionUuid: string, actionUuid: string,
updates: Partial<ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']> updates: Partial<ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action'] | HumanPointSchema['actions'][0]>
) => EventsSchema | undefined; ) => EventsSchema | undefined;
// Trigger-level actionss // Trigger-level actionss
@@ -276,6 +276,15 @@ export const createProductStore = () => {
return; return;
} }
} }
} else if (event.type === "human") {
if ('actions' in point) {
const index = point.actions.findIndex((a: any) => a.actionUuid === actionUuid);
if (index !== -1) {
point.actions.splice(index, 1);
updatedEvent = JSON.parse(JSON.stringify(event));
return;
}
}
} else if ('action' in point && point.action?.actionUuid === actionUuid) { } else if ('action' in point && point.action?.actionUuid === actionUuid) {
point.action = undefined; point.action = undefined;
updatedEvent = JSON.parse(JSON.stringify(event)); updatedEvent = JSON.parse(JSON.stringify(event));

View File

@@ -158,6 +158,7 @@ interface StorageEventSchema extends AssetEventSchema {
interface HumanEventSchema extends AssetEventSchema { interface HumanEventSchema extends AssetEventSchema {
type: "human"; type: "human";
speed: number;
point: HumanPointSchema; point: HumanPointSchema;
} }