From 36421fa70a6aa1aa608e742478eb9c1595721d37 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 7 Jul 2025 10:11:37 +0530 Subject: [PATCH 1/4] added new human ui --- .../assets/gltf-glb/ui/human-ui-assembly.glb | Bin 0 -> 18908 bytes app/src/assets/gltf-glb/ui/human-ui-green.glb | Bin 0 -> 18904 bytes .../assets/gltf-glb/ui/human-ui-orange.glb | Bin 0 -> 18904 bytes .../builder/asset/models/model/model.tsx | 27 +++++- .../selectionControls/copyPasteControls.tsx | 6 +- .../selectionControls/duplicationControls.tsx | 6 +- .../selectionControls/selectionControls.tsx | 30 +++++- .../instances/instance/humanInstance.tsx | 3 + .../human/instances/instance/humanUi.tsx | 58 ++++++++---- app/src/store/simulation/useProductStore.ts | 86 +++++++++++++++++- 10 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 app/src/assets/gltf-glb/ui/human-ui-assembly.glb create mode 100644 app/src/assets/gltf-glb/ui/human-ui-green.glb create mode 100644 app/src/assets/gltf-glb/ui/human-ui-orange.glb diff --git a/app/src/assets/gltf-glb/ui/human-ui-assembly.glb b/app/src/assets/gltf-glb/ui/human-ui-assembly.glb new file mode 100644 index 0000000000000000000000000000000000000000..844fc9bc4aa059bd25fa3964aeddcf5171286985 GIT binary patch literal 18908 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 literal 0 HcmV?d00001 diff --git a/app/src/assets/gltf-glb/ui/human-ui-orange.glb b/app/src/assets/gltf-glb/ui/human-ui-orange.glb new file mode 100644 index 0000000000000000000000000000000000000000..9624cb2ba8d4812450415346238c5bc41f89cba3 GIT binary patch literal 18904 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 af*^realkRXiiyxykiI^dJ0RIQ4#uf_z literal 0 HcmV?d00001 diff --git a/app/src/modules/builder/asset/models/model/model.tsx b/app/src/modules/builder/asset/models/model/model.tsx index c7bca83..75ad55b 100644 --- a/app/src/modules/builder/asset/models/model/model.tsx +++ b/app/src/modules/builder/asset/models/model/model.tsx @@ -17,6 +17,7 @@ import { useSceneContext } from '../../../../scene/sceneContext'; import { useVersionContext } from '../../../version/versionContext'; import { SkeletonUtils } from 'three-stdlib'; import { useAnimationPlaySpeed } from '../../../../../store/usePlayButtonStore'; +import { upsertProductOrEventApi } from '../../../../../services/simulation/products/UpsertProductOrEventApi'; function Model({ asset }: { readonly asset: Asset }) { const { camera, controls, gl } = useThree(); @@ -55,6 +56,21 @@ function Model({ asset }: { readonly asset: Asset }) { const blendFactor = useRef(0); const blendDuration = 0.5; + const updateBackend = ( + productName: string, + productUuid: string, + projectId: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productUuid: productUuid, + projectId: projectId, + eventDatas: eventData, + versionId: selectedVersion?.versionId || '', + }); + }; + useEffect(() => { setDeletableFloorItem(null); if (selectedFloorItem === null || selectedFloorItem.modelUuid !== asset.modelUuid) { @@ -217,7 +233,16 @@ function Model({ asset }: { readonly asset: Asset }) { const response = socket.emit('v1:model-asset:delete', data) eventStore.getState().removeEvent(asset.modelUuid); - productStore.getState().deleteEvent(asset.modelUuid); + const updatedEvents = productStore.getState().deleteEvent(asset.modelUuid); + + updatedEvents.forEach((event) => { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + }) if (response) { diff --git a/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx b/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx index 29a3007..e8c8886 100644 --- a/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx @@ -1,8 +1,8 @@ import * as THREE from "three"; import { useEffect, useMemo } from "react"; import { useFrame, useThree } from "@react-three/fiber"; +import { SkeletonUtils } from "three-stdlib"; import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/builder/store"; -// import { setAssetsApi } from '../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; import * as Types from "../../../../types/world/worldTypes"; import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; import { useParams } from "react-router-dom"; @@ -10,6 +10,8 @@ import { getUserData } from "../../../../functions/getUserData"; import { useSceneContext } from "../../sceneContext"; import { useVersionContext } from "../../../builder/version/versionContext"; +// import { setAssetsApi } from '../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; + const CopyPasteControls = ({ copiedObjects, setCopiedObjects, @@ -109,7 +111,7 @@ const CopyPasteControls = ({ const copySelection = () => { if (selectedAssets.length > 0) { const newClones = selectedAssets.map((asset: any) => { - const clone = asset.clone(); + const clone = SkeletonUtils.clone(asset); clone.position.copy(asset.position); return clone; }); diff --git a/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx b/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx index b4a1916..3b52cdf 100644 --- a/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx @@ -1,8 +1,8 @@ import * as THREE from "three"; import { useEffect, useMemo } from "react"; import { useFrame, useThree } from "@react-three/fiber"; +import { SkeletonUtils } from "three-stdlib"; import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/builder/store"; -// import { setAssetsApi } from '../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; import * as Types from "../../../../types/world/worldTypes"; import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; import { useParams } from "react-router-dom"; @@ -10,6 +10,8 @@ import { getUserData } from "../../../../functions/getUserData"; import { useSceneContext } from "../../sceneContext"; import { useVersionContext } from "../../../builder/version/versionContext"; +// import { setAssetsApi } from '../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; + const DuplicationControls = ({ duplicatedObjects, setDuplicatedObjects, @@ -104,7 +106,7 @@ const DuplicationControls = ({ const duplicateSelection = () => { if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { const newClones = selectedAssets.map((asset: any) => { - const clone = asset.clone(); + const clone = SkeletonUtils.clone(asset); clone.position.copy(asset.position); return clone; }); diff --git a/app/src/modules/scene/controls/selectionControls/selectionControls.tsx b/app/src/modules/scene/controls/selectionControls/selectionControls.tsx index 645ed1c..49c94dd 100644 --- a/app/src/modules/scene/controls/selectionControls/selectionControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/selectionControls.tsx @@ -17,6 +17,8 @@ import { useParams } from "react-router-dom"; import { getUserData } from "../../../../functions/getUserData"; import { useSceneContext } from "../../sceneContext"; import { useVersionContext } from "../../../builder/version/versionContext"; +import { useProductContext } from "../../../simulation/products/productContext"; +import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi"; const SelectionControls: React.FC = () => { const { camera, controls, gl, scene, raycaster, pointer } = useThree(); @@ -37,6 +39,8 @@ const SelectionControls: React.FC = () => { const { toolMode } = useToolMode(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct } = selectedProductStore(); const { projectId } = useParams(); const isDragging = useRef(false); @@ -48,6 +52,21 @@ const SelectionControls: React.FC = () => { const isShiftSelecting = useRef(false); const { userId, organization } = getUserData(); + const updateBackend = ( + productName: string, + productUuid: string, + projectId: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productUuid: productUuid, + projectId: projectId, + eventDatas: eventData, + versionId: selectedVersion?.versionId || '', + }); + }; + useEffect(() => { if (!camera || !scene || toggleView) return; @@ -284,7 +303,16 @@ const SelectionControls: React.FC = () => { const response = socket.emit("v1:model-asset:delete", data); eventStore.getState().removeEvent(selectedMesh.uuid); - productStore.getState().deleteEvent(selectedMesh.uuid); + const updatedEvents = productStore.getState().deleteEvent(selectedMesh.uuid); + + updatedEvents.forEach((event) => { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + }) if (response) { diff --git a/app/src/modules/simulation/human/instances/instance/humanInstance.tsx b/app/src/modules/simulation/human/instances/instance/humanInstance.tsx index 12d2d61..5ba757d 100644 --- a/app/src/modules/simulation/human/instances/instance/humanInstance.tsx +++ b/app/src/modules/simulation/human/instances/instance/humanInstance.tsx @@ -253,6 +253,9 @@ function HumanInstance({ human }: { human: HumanStatus }) { function startUnloadingProcess() { const humanAsset = getAssetById(human.modelUuid); + if (humanAsset?.animationState?.current !== 'drop') { + setCurrentAnimation(human.modelUuid, 'drop', true, false, false); + } if (humanAsset?.animationState?.current === 'drop' && humanAsset?.animationState?.isCompleted) { if (human.point.action.triggers.length > 0) { const trigger = getTriggerByUuid(selectedProduct.productUuid, human.point.action.triggers[0]?.triggerUuid); diff --git a/app/src/modules/simulation/human/instances/instance/humanUi.tsx b/app/src/modules/simulation/human/instances/instance/humanUi.tsx index 64b1f84..68d7e5a 100644 --- a/app/src/modules/simulation/human/instances/instance/humanUi.tsx +++ b/app/src/modules/simulation/human/instances/instance/humanUi.tsx @@ -4,11 +4,11 @@ import { useFrame, useThree } from '@react-three/fiber'; import { useIsDragging, useIsRotating, useSelectedAction, useSelectedEventSphere } from '../../../../../store/simulation/useSimulationStore'; import { useProductContext } from '../../../products/productContext'; import { useSceneContext } from '../../../../scene/sceneContext'; -import { Group, Plane, Vector3 } from 'three'; +import { Group, Plane, Vector2, Vector3 } from 'three'; import { useVersionContext } from '../../../../builder/version/versionContext'; import { useParams } from 'react-router-dom'; -import startPoint from "../../../../../assets/gltf-glb/ui/arrow_green.glb"; -import startEnd from "../../../../../assets/gltf-glb/ui/arrow_red.glb"; +import startPoint from "../../../../../assets/gltf-glb/ui/human-ui-green.glb"; +import startEnd from "../../../../../assets/gltf-glb/ui/human-ui-orange.glb"; import { upsertProductOrEventApi } from '../../../../../services/simulation/products/UpsertProductOrEventApi'; function HumanUi() { @@ -18,7 +18,7 @@ function HumanUi() { const endMarker = useRef(null); const outerGroup = useRef(null); const prevMousePos = useRef({ x: 0, y: 0 }); - const { controls, raycaster } = useThree(); + const { controls, raycaster, camera } = useThree(); const { selectedEventSphere } = useSelectedEventSphere(); const { selectedProductStore } = useProductContext(); const { humanStore, productStore } = useSceneContext(); @@ -27,11 +27,12 @@ function HumanUi() { const { updateEvent } = productStore(); const [startPosition, setStartPosition] = useState<[number, number, number]>([0, 1, 0]); const [endPosition, setEndPosition] = useState<[number, number, number]>([0, 1, 0]); - const [startRotation, setStartRotation] = useState<[number, number, number]>([0, 0, 0]); + const [startRotation, setStartRotation] = useState<[number, number, number]>([0, Math.PI, 0]); const [endRotation, setEndRotation] = useState<[number, number, number]>([0, 0, 0]); const { isDragging, setIsDragging } = useIsDragging(); const { isRotating, setIsRotating } = useIsRotating(); const plane = useRef(new Plane(new Vector3(0, 1, 0), 0)); + const dragOffset = useRef(new Vector3()); const [selectedHumanData, setSelectedHumanData] = useState<{ position: [number, number, number]; @@ -74,15 +75,15 @@ function HumanUi() { if (action.pickUpPoint?.position && outerGroup.current) { const worldPos = new Vector3(...action.pickUpPoint.position); const localPosition = outerGroup.current.worldToLocal(worldPos.clone()); - setStartPosition([localPosition.x, 0.5, localPosition.z]); + setStartPosition([localPosition.x, 1, localPosition.z]); setStartRotation(action.pickUpPoint.rotation || [0, 0, 0]); } if (action.dropPoint?.position && outerGroup.current) { const worldPos = new Vector3(...action.dropPoint.position); const localPosition = outerGroup.current.worldToLocal(worldPos.clone()); - setEndPosition([localPosition.x, 0.5, localPosition.z]); - setEndRotation(action.dropPoint.rotation || [0, 0, 0]); + setEndPosition([localPosition.x, 1, localPosition.z]); + setEndRotation(action.dropPoint.rotation || [0, Math.PI, 0]); } }, [selectedEventSphere, outerGroup.current, selectedAction, humans]); @@ -91,20 +92,39 @@ function HumanUi() { state: "start" | "end", rotation: "start" | "end" ) => { - if (e.object.name === "handle") { + e.stopPropagation(); + const intersection = new Vector3(); + const pointer = new Vector2((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1); + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.ray.intersectPlane(plane.current, intersection); + + if (e.object.parent.name === "handle") { const normalizedX = (e.clientX / window.innerWidth) * 2 - 1; const normalizedY = -(e.clientY / window.innerHeight) * 2 + 1; prevMousePos.current = { x: normalizedX, y: normalizedY }; setIsRotating(rotation); - if (controls) (controls as any).enabled = false; setIsDragging(null); } else { setIsDragging(state); setIsRotating(null); - if (controls) (controls as any).enabled = false; } + + if (intersects) { + let localPoint: Vector3 | null = null; + if (outerGroup.current) { + localPoint = outerGroup.current.worldToLocal(intersection.clone()); + } + const marker = state === "start" ? startMarker.current : endMarker.current; + if (marker && localPoint) { + const markerPos = new Vector3().copy(marker.position); + dragOffset.current.copy(markerPos.sub(localPoint)); + } + } + + if (controls) (controls as any).enabled = false; }; + const handlePointerUp = () => { (controls as any).enabled = true; setIsDragging(null); @@ -159,16 +179,15 @@ function HumanUi() { useFrame(() => { if (!isDragging || !plane.current || !raycaster || !outerGroup.current) return; const intersectPoint = new Vector3(); - const intersects = raycaster.ray.intersectPlane( - plane.current, - intersectPoint - ); + const intersects = raycaster.ray.intersectPlane(plane.current, intersectPoint); if (!intersects) return; - const localPoint = outerGroup?.current.worldToLocal(intersectPoint.clone()); + + const localPoint = outerGroup.current.worldToLocal(intersectPoint.clone()).add(dragOffset.current); + if (isDragging === "start") { - setStartPosition([localPoint.x, 0.5, localPoint.z]); + setStartPosition([localPoint.x, 1, localPoint.z]); } else if (isDragging === "end") { - setEndPosition([localPoint.x, 0.5, localPoint.z]); + setEndPosition([localPoint.x, 1, localPoint.z]); } }); @@ -177,7 +196,7 @@ function HumanUi() { const currentPointerX = state.pointer.x; const deltaX = currentPointerX - prevMousePos.current.x; prevMousePos.current.x = currentPointerX; - const marker =isRotating === "start" ? startMarker.current : endMarker.current; + const marker = isRotating === "start" ? startMarker.current : endMarker.current; if (marker) { const rotationSpeed = 10; marker.rotation.y += deltaX * rotationSpeed; @@ -220,6 +239,7 @@ function HumanUi() { void; removeEvent: (productUuid: string, modelUuid: string) => void; - deleteEvent: (modelUuid: string) => void; + deleteEvent: (modelUuid: string) => EventsSchema[]; updateEvent: (productUuid: string, modelUuid: string, updates: Partial) => EventsSchema | undefined; // Point-level actions @@ -145,11 +145,93 @@ export const createProductStore = () => { }, deleteEvent: (modelUuid) => { + let updatedEvents: EventsSchema[] = []; set((state) => { + const actionsToDelete = new Set(); + for (const product of state.products) { - product.eventDatas = product.eventDatas.filter(e => 'modelUuid' in e && e.modelUuid !== modelUuid); + const eventIndex = product.eventDatas.findIndex(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (eventIndex !== -1) { + const event = product.eventDatas[eventIndex]; + + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action) { + actionsToDelete.add(point.action.actionUuid); + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action) { + actionsToDelete.add(point.action.actionUuid); + } else if ('actions' in point) { + for (const action of point.actions) { + actionsToDelete.add(action.actionUuid); + } + } + } + + product.eventDatas.splice(eventIndex, 1); + } + } + + for (const product of state.products) { + for (const event of product.eventDatas) { + let eventModified = false; + + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action?.triggers) { + const originalLength = point.action.triggers.length; + point.action.triggers = point.action.triggers.filter(trigger => { + return !( + (trigger.triggeredAsset?.triggeredModel?.modelUuid === modelUuid) || + (actionsToDelete.has(trigger.triggeredAsset?.triggeredAction?.actionUuid || '')) + ); + }); + if (point.action.triggers.length !== originalLength) { + eventModified = true; + } + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action?.triggers) { + const originalLength = point.action.triggers.length; + point.action.triggers = point.action.triggers.filter((trigger: TriggerSchema) => { + return !( + (trigger.triggeredAsset?.triggeredModel?.modelUuid === modelUuid) || + (actionsToDelete.has(trigger.triggeredAsset?.triggeredAction?.actionUuid || '')) + ); + }); + if (point.action.triggers.length !== originalLength) { + eventModified = true; + } + } else if ('actions' in point) { + for (const action of point.actions) { + if (action.triggers) { + const originalLength = action.triggers.length; + action.triggers = action.triggers.filter((trigger: TriggerSchema) => { + return !( + (trigger.triggeredAsset?.triggeredModel?.modelUuid === modelUuid) || + (actionsToDelete.has(trigger.triggeredAsset?.triggeredAction?.actionUuid || '')) + ); + }); + if (action.triggers.length !== originalLength) { + eventModified = true; + } + } + } + } + } + + if (eventModified) { + updatedEvents.push(JSON.parse(JSON.stringify(event))); + } + } } }); + return updatedEvents; }, updateEvent: (productUuid, modelUuid, updates) => { From b7f29bf5db5c63da9dc4bc52ed1f98f7f195cc58 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 7 Jul 2025 10:18:15 +0530 Subject: [PATCH 2/4] fix: Update position and rotation handling for selected human in HumanUi --- .../human/instances/instance/humanUi.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/modules/simulation/human/instances/instance/humanUi.tsx b/app/src/modules/simulation/human/instances/instance/humanUi.tsx index 68d7e5a..12ee8d5 100644 --- a/app/src/modules/simulation/human/instances/instance/humanUi.tsx +++ b/app/src/modules/simulation/human/instances/instance/humanUi.tsx @@ -70,6 +70,14 @@ function HumanUi() { rotation: selectedHuman.rotation, }); + if (outerGroup.current) { + outerGroup.current.position.set( + selectedHuman.position[0], + selectedHuman.position[1], + selectedHuman.position[2] + ); + } + const action = selectedHuman.point.action; if (action.pickUpPoint?.position && outerGroup.current) { @@ -77,6 +85,9 @@ function HumanUi() { const localPosition = outerGroup.current.worldToLocal(worldPos.clone()); setStartPosition([localPosition.x, 1, localPosition.z]); setStartRotation(action.pickUpPoint.rotation || [0, 0, 0]); + } else { + setStartPosition([0, 1, 0]); + setStartRotation([0, Math.PI, 0]); } if (action.dropPoint?.position && outerGroup.current) { @@ -84,6 +95,9 @@ function HumanUi() { const localPosition = outerGroup.current.worldToLocal(worldPos.clone()); setEndPosition([localPosition.x, 1, localPosition.z]); setEndRotation(action.dropPoint.rotation || [0, Math.PI, 0]); + } else { + setEndPosition([0, 1, 0]); + setEndRotation([0, 0, 0]); } }, [selectedEventSphere, outerGroup.current, selectedAction, humans]); From 74094aee9f1f2028a57f65dcff8c487f9c3c63c0 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 7 Jul 2025 15:00:16 +0530 Subject: [PATCH 3/4] feat: Add assembly action handling and UI components - Implemented `useAssemblyHandler` to manage assembly actions for humans. - Enhanced `useHumanActions` to include assembly action handling. - Updated `HumanInstance` to support assembly processes and animations. - Modified `HumanUi` to allow for assembly point configuration and rotation. - Created `AssemblyAction` component for setting process time and material swap options. - Updated simulation types to include assembly action properties. - Adjusted existing action handlers to accommodate assembly actions alongside worker actions. - Refactored `MaterialAnimator` and `VehicleAnimator` to manage attachment states and visibility based on load. - Updated product store types to include human point actions. --- .../actions/assemblyAction.tsx | 44 +++ .../mechanics/humanMechanics.tsx | 96 +++++- app/src/components/ui/inputs/InputRange.tsx | 1 - .../geomentries/floors/addFloorToScene.ts | 62 ---- .../geomentries/floors/loadOnlyFloors.ts | 190 ------------ .../modules/builder/groups/wallItemsGroup.tsx | 291 ------------------ .../builder/groups/wallsAndWallItems.tsx | 74 ----- app/src/modules/builder/groups/wallsMesh.tsx | 82 ----- .../human/actionHandler/useAssemblyHandler.ts | 36 +++ .../actions/human/useHumanActions.ts | 11 +- .../simulation/actions/useActionHandler.ts | 2 +- .../instances/animator/materialAnimator.tsx | 22 +- .../instances/instance/humanInstance.tsx | 119 ++++++- .../human/instances/instance/humanUi.tsx | 210 ++++++++----- .../spatialUI/vehicle/vehicleUI.tsx | 1 - .../triggerHandler/useTriggerHandler.ts | 92 +++++- .../instances/animator/materialAnimator.tsx | 49 +-- .../instances/animator/vehicleAnimator.tsx | 54 ++-- app/src/store/simulation/useProductStore.ts | 10 +- app/src/types/simulationTypes.d.ts | 5 +- 20 files changed, 592 insertions(+), 859 deletions(-) create mode 100644 app/src/components/layout/sidebarRight/properties/eventProperties/actions/assemblyAction.tsx delete mode 100644 app/src/modules/builder/geomentries/floors/addFloorToScene.ts delete mode 100644 app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts delete mode 100644 app/src/modules/builder/groups/wallItemsGroup.tsx delete mode 100644 app/src/modules/builder/groups/wallsAndWallItems.tsx delete mode 100644 app/src/modules/builder/groups/wallsMesh.tsx create mode 100644 app/src/modules/simulation/actions/human/actionHandler/useAssemblyHandler.ts diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/actions/assemblyAction.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/actions/assemblyAction.tsx new file mode 100644 index 0000000..cc7be80 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/actions/assemblyAction.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import InputRange from "../../../../../ui/inputs/InputRange"; +import SwapAction from "./SwapAction"; + +interface AssemblyActionProps { + processTime: { + value: number; + min: number; + max: number; + disabled?: boolean, + onChange: (value: number) => void; + }; + swapOptions: string[]; + swapDefaultOption: string; + onSwapSelect: (value: string) => void; +} + +const AssemblyAction: React.FC = ({ + processTime, + swapOptions, + swapDefaultOption, + onSwapSelect, +}) => { + return ( + <> + { }} + onChange={processTime.onChange} + /> + + + ); +}; + +export default AssemblyAction; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx index 9f2fd2d..b979a91 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx @@ -5,18 +5,21 @@ import RenameInput from "../../../../../ui/inputs/RenameInput"; import LabledDropdown from "../../../../../ui/inputs/LabledDropdown"; import Trigger from "../trigger/Trigger"; import ActionsList from "../components/ActionsList"; -import { useSelectedEventData, useSelectedAction, useSelectedAnimation } from "../../../../../../store/simulation/useSimulationStore"; +import { useSelectedEventData, useSelectedAction } from "../../../../../../store/simulation/useSimulationStore"; import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi"; import { useProductContext } from "../../../../../../modules/simulation/products/productContext"; import { useVersionContext } from "../../../../../../modules/builder/version/versionContext"; import { useSceneContext } from "../../../../../../modules/scene/sceneContext"; import { useParams } from "react-router-dom"; import WorkerAction from "../actions/workerAction"; +import AssemblyAction from "../actions/assemblyAction"; function HumanMechanics() { - const [activeOption, setActiveOption] = useState<"worker">("worker"); + const [activeOption, setActiveOption] = useState<"worker" | "assembly">("worker"); const [speed, setSpeed] = useState("0.5"); const [loadCapacity, setLoadCapacity] = useState("1"); + const [processTime, setProcessTime] = useState(10); + const [swappedMaterial, setSwappedMaterial] = useState("Default material"); const [currentAction, setCurrentAction] = useState(); const [selectedPointData, setSelectedPointData] = useState(); const { selectedEventData } = useSelectedEventData(); @@ -48,6 +51,8 @@ function HumanMechanics() { ) as HumanEventSchema | undefined )?.speed?.toString() || "1"); setLoadCapacity(point.action.loadCapacity.toString()); + setProcessTime(point.action.processTime || 10); + setSwappedMaterial(point.action.swapMaterial || "Default material"); } } else { clearSelectedAction(); @@ -158,6 +163,52 @@ function HumanMechanics() { setLoadCapacity(value); }; + const handleProcessTimeChange = (value: number) => { + if (!currentAction || !selectedPointData || !selectedAction.actionId) return; + + const updatedAction = { ...currentAction }; + updatedAction.processTime = value + + const updatedPoint = { ...selectedPointData, action: updatedAction }; + + const event = updateAction( + selectedProduct.productUuid, + selectedAction.actionId, + updatedAction + ); + + if (event) { + updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event); + } + + setCurrentAction(updatedAction); + setSelectedPointData(updatedPoint); + setProcessTime(value); + }; + + const handleMaterialChange = (value: string) => { + if (!currentAction || !selectedPointData || !selectedAction.actionId) return; + + const updatedAction = { ...currentAction }; + updatedAction.swapMaterial = value + + const updatedPoint = { ...selectedPointData, action: updatedAction }; + + const event = updateAction( + selectedProduct.productUuid, + selectedAction.actionId, + updatedAction + ); + + if (event) { + updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event); + } + + setCurrentAction(updatedAction); + setSelectedPointData(updatedPoint); + setSwappedMaterial(value); + }; + const handleClearPoints = () => { if (!currentAction || !selectedPointData || !selectedAction.actionId) return; @@ -262,23 +313,38 @@ function HumanMechanics() { - + {currentAction.actionType === 'worker' && + + } + {currentAction.actionType === 'assembly' && + + }
diff --git a/app/src/components/ui/inputs/InputRange.tsx b/app/src/components/ui/inputs/InputRange.tsx index 51c8b1a..1a2f6f5 100644 --- a/app/src/components/ui/inputs/InputRange.tsx +++ b/app/src/components/ui/inputs/InputRange.tsx @@ -80,7 +80,6 @@ const InputRange: React.FC = ({ disabled={disabled} onKeyUp={(e) => { if (e.key === "ArrowUp" || e.key === "ArrowDown") { - console.log("e.key: ", e.key); handlekey(e); } }} diff --git a/app/src/modules/builder/geomentries/floors/addFloorToScene.ts b/app/src/modules/builder/geomentries/floors/addFloorToScene.ts deleted file mode 100644 index 5360d44..0000000 --- a/app/src/modules/builder/geomentries/floors/addFloorToScene.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as THREE from 'three'; -import * as Types from "../../../../types/world/worldTypes"; -import * as CONSTANTS from "../../../../types/world/worldConstants"; - -import texturePath from "../../../../assets/textures/floor/white1.png"; -import texturePathDark from "../../../../assets/textures/floor/black.png"; - -// Cache for materials -const materialCache = new Map(); - -export default function addFloorToScene( - shape: THREE.Shape, - layer: number, - floorGroup: Types.RefGroup, - userData: any, -) { - const savedTheme: string | null = localStorage.getItem('theme'); - - const textureLoader = new THREE.TextureLoader(); - - const textureScale = CONSTANTS.floorConfig.textureScale; - - const materialKey = `floorMaterial_${textureScale}`; - - let material: THREE.Material; - - if (materialCache.has(materialKey)) { - material = materialCache.get(materialKey) as THREE.Material; - } else { - const floorTexture = textureLoader.load(savedTheme === "dark" ? texturePathDark : texturePath); - // const floorTexture = textureLoader.load(texturePath); - - floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping; - floorTexture.repeat.set(textureScale, textureScale); - floorTexture.colorSpace = THREE.SRGBColorSpace; - - material = new THREE.MeshStandardMaterial({ - map: floorTexture, - side: THREE.DoubleSide, - }); - - materialCache.set(materialKey, material); - } - - const extrudeSettings = { - depth: CONSTANTS.floorConfig.height, - bevelEnabled: false, - }; - - const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); - const mesh = new THREE.Mesh(geometry, material); - - mesh.receiveShadow = true; - mesh.position.y = (layer) * CONSTANTS.wallConfig.height; - mesh.rotateX(Math.PI / 2); - mesh.name = `Floor_Layer_${layer}`; - - // Store UUIDs for debugging or future processing - mesh.userData.uuids = userData; - - floorGroup.current.add(mesh); -} diff --git a/app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts b/app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts deleted file mode 100644 index 1c199de..0000000 --- a/app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts +++ /dev/null @@ -1,190 +0,0 @@ -import * as THREE from 'three'; -import * as turf from '@turf/turf'; -import * as CONSTANTS from '../../../../types/world/worldConstants'; -import * as Types from "../../../../types/world/worldTypes"; - -// temp -import blueFloorImage from "../../../../assets/textures/floor/blue.png" - -function loadOnlyFloors( - floorGroup: Types.RefGroup, - linesByLayer: any, - layer: any, -): void { - - ////////// Creating polygon floor based on the onlyFloorlines.current which does not add roof to it, The lines are still stored in Lines.current as well ////////// - - let floorsInLayer = linesByLayer[layer]; - floorsInLayer = floorsInLayer.filter((line: any) => line[0][3] && line[1][3] === CONSTANTS.lineConfig.floorName); - const floorResult = floorsInLayer.map((pair: [THREE.Vector3, string, number, string][]) => - pair.map((point) => ({ - position: [point[0].x, point[0].z], - uuid: point[1] - })) - ); - const FloorLineFeatures = floorResult.map((line: any) => turf.lineString(line.map((p: any) => p.position))); - - function identifyPolygonsAndConnectedLines(FloorLineFeatures: any) { - const floorpolygons = []; - const connectedLines = []; - const unprocessedLines = [...FloorLineFeatures]; // Copy the features - - while (unprocessedLines.length > 0) { - const currentLine = unprocessedLines.pop(); - const coordinates = currentLine.geometry.coordinates; - - // Check if the line is closed (forms a polygon) - if ( - coordinates[0][0] === coordinates[coordinates.length - 1][0] && - coordinates[0][1] === coordinates[coordinates.length - 1][1] - ) { - floorpolygons.push(turf.polygon([coordinates])); // Add as a polygon - continue; - } - - // Check if the line connects to another line - let connected = false; - for (let i = unprocessedLines.length - 1; i >= 0; i--) { - const otherCoordinates = unprocessedLines[i].geometry.coordinates; - - // Check if lines share a start or end point - if ( - coordinates[0][0] === otherCoordinates[otherCoordinates.length - 1][0] && - coordinates[0][1] === otherCoordinates[otherCoordinates.length - 1][1] - ) { - // Merge lines - const mergedCoordinates = [...otherCoordinates, ...coordinates.slice(1)]; - unprocessedLines[i] = turf.lineString(mergedCoordinates); - connected = true; - break; - } else if ( - coordinates[coordinates.length - 1][0] === otherCoordinates[0][0] && - coordinates[coordinates.length - 1][1] === otherCoordinates[0][1] - ) { - // Merge lines - const mergedCoordinates = [...coordinates, ...otherCoordinates.slice(1)]; - unprocessedLines[i] = turf.lineString(mergedCoordinates); - connected = true; - break; - } - } - - if (!connected) { - connectedLines.push(currentLine); // Add unconnected line as-is - } - } - - return { floorpolygons, connectedLines }; - } - - const { floorpolygons, connectedLines } = identifyPolygonsAndConnectedLines(FloorLineFeatures); - - function convertConnectedLinesToPolygons(connectedLines: any) { - return connectedLines.map((line: any) => { - const coordinates = line.geometry.coordinates; - - // If the line has more than two points, close the polygon - if (coordinates.length > 2) { - const firstPoint = coordinates[0]; - const lastPoint = coordinates[coordinates.length - 1]; - - // Check if already closed; if not, close it - if (firstPoint[0] !== lastPoint[0] || firstPoint[1] !== lastPoint[1]) { - coordinates.push(firstPoint); - } - - // Convert the closed line into a polygon - return turf.polygon([coordinates]); - } - - // If not enough points for a polygon, return the line unchanged - return line; - }); - } - - const convertedConnectedPolygons = convertConnectedLinesToPolygons(connectedLines); - - if (convertedConnectedPolygons.length > 0) { - const validPolygons = convertedConnectedPolygons.filter( - (polygon: any) => polygon.geometry?.type === "Polygon" - ); - - if (validPolygons.length > 0) { - floorpolygons.push(...validPolygons); - } - } - - function convertPolygonsToOriginalFormat(floorpolygons: any, originalLines: [THREE.Vector3, string, number, string][][]) { - return floorpolygons.map((polygon: any) => { - const coordinates = polygon.geometry.coordinates[0]; // Extract the coordinates array (assume it's a single polygon) - - // Map each coordinate back to its original structure - const mappedPoints = coordinates.map((coord: [number, number]) => { - const [x, z] = coord; - - // Find the original point matching this coordinate - const originalPoint = originalLines.flat().find(([point]) => point.x === x && point.z === z); - - if (!originalPoint) { - throw new Error(`Original point for coordinate [${x}, ${z}] not found.`); - } - - return originalPoint; - }); - - // Create pairs of consecutive points - const pairs: typeof originalLines = []; - for (let i = 0; i < mappedPoints.length - 1; i++) { - pairs.push([mappedPoints[i], mappedPoints[i + 1]]); - } - - return pairs; - }); - } - - const convertedFloorPolygons: Types.OnlyFloorLines = convertPolygonsToOriginalFormat(floorpolygons, floorsInLayer); - - convertedFloorPolygons.forEach((floor) => { - const points: THREE.Vector3[] = []; - - floor.forEach((lineSegment) => { - const startPoint = lineSegment[0][0]; - points.push(new THREE.Vector3(startPoint.x, startPoint.y, startPoint.z)); - }); - - const lastLine = floor[floor.length - 1]; - const endPoint = lastLine[1][0]; - points.push(new THREE.Vector3(endPoint.x, endPoint.y, endPoint.z)); - - const shape = new THREE.Shape(); - shape.moveTo(points[0].x, points[0].z); - - points.forEach(point => shape.lineTo(point.x, point.z)); - shape.closePath(); - - const extrudeSettings = { - depth: 0.3, - bevelEnabled: false - }; - - const texture = new THREE.TextureLoader().load(blueFloorImage); - texture.wrapS = texture.wrapT = THREE.RepeatWrapping; - texture.colorSpace = THREE.SRGBColorSpace; - - const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); - const material = new THREE.MeshStandardMaterial({ color: CONSTANTS.floorConfig.defaultColor, side: THREE.DoubleSide, map: texture }); - const mesh = new THREE.Mesh(geometry, material); - - mesh.castShadow = true; - mesh.receiveShadow = true; - - mesh.position.y = (floor[0][0][2] - 0.99) * CONSTANTS.wallConfig.height; - mesh.rotateX(Math.PI / 2); - mesh.name = `Only_Floor_Line_${floor[0][0][2]}`; - - mesh.userData = floor; - floorGroup?.current?.add(mesh); - }); -} - -export default loadOnlyFloors; diff --git a/app/src/modules/builder/groups/wallItemsGroup.tsx b/app/src/modules/builder/groups/wallItemsGroup.tsx deleted file mode 100644 index 28f4f4b..0000000 --- a/app/src/modules/builder/groups/wallItemsGroup.tsx +++ /dev/null @@ -1,291 +0,0 @@ -// import { useEffect } from "react"; -// import { -// useObjectPosition, -// useObjectRotation, -// useSelectedWallItem, -// useSocketStore, -// useWallItems, -// useSelectedItem, -// useToolMode, -// } from "../../../store/builder/store"; -// import { Csg } from "../csg/csg"; -// import * as Types from "../../../types/world/worldTypes"; -// import * as CONSTANTS from "../../../types/world/worldConstants"; -// import * as THREE from "three"; -// import { useThree } from "@react-three/fiber"; -// import handleMeshMissed from "../eventFunctions/handleMeshMissed"; -// import DeleteWallItems from "../geomentries/walls/deleteWallItems"; -// import loadInitialWallItems from "../IntialLoad/loadInitialWallItems"; -// import AddWallItems from "../geomentries/walls/addWallItems"; -// import useModuleStore from "../../../store/useModuleStore"; -// import { useParams } from "react-router-dom"; -// import { getUserData } from "../../../functions/getUserData"; -// import { useVersionContext } from "../version/versionContext"; - -// const WallItemsGroup = ({ -// currentWallItem, -// hoveredDeletableWallItem, -// selectedItemsIndex, -// setSelectedItemsIndex, -// CSGGroup, -// }: any) => { -// const state = useThree(); -// const { socket } = useSocketStore(); -// const { pointer, camera, raycaster } = state; -// const { toolMode } = useToolMode(); -// const { wallItems, setWallItems } = useWallItems(); -// const { setObjectPosition } = useObjectPosition(); -// const { setObjectRotation } = useObjectRotation(); -// const { setSelectedWallItem } = useSelectedWallItem(); -// const { activeModule } = useModuleStore(); -// const { selectedItem } = useSelectedItem(); -// const { selectedVersionStore } = useVersionContext(); -// const { selectedVersion } = selectedVersionStore(); -// const { projectId } = useParams(); -// const { userId, organization } = getUserData(); - -// useEffect(() => { -// // Load Wall Items from the backend -// if (!projectId || !selectedVersion) return; -// loadInitialWallItems(setWallItems, projectId, selectedVersion?.versionId); -// }, [selectedVersion?.versionId]); - -// ////////// Update the Position value changes in the selected item ////////// - -// useEffect(() => { -// const canvasElement = state.gl.domElement; -// function handlePointerMove(e: any) { -// if (selectedItemsIndex !== null && toolMode === 'cursor' && e.buttons === 1) { -// const Raycaster = state.raycaster; -// const intersects = Raycaster.intersectObjects(CSGGroup.current?.children[0].children!, true); -// const Object = intersects.find((child) => child.object.name.includes("WallRaycastReference")); - -// if (Object) { -// (state.controls as any)!.enabled = false; -// setWallItems((prevItems: any) => { -// const updatedItems = [...prevItems]; -// let position: [number, number, number] = [0, 0, 0]; - -// if (updatedItems[selectedItemsIndex].type === "fixed-move") { -// position = [ -// Object!.point.x, -// Math.floor(Object!.point.y / CONSTANTS.wallConfig.height) * -// CONSTANTS.wallConfig.height, -// Object!.point.z, -// ]; -// } else if (updatedItems[selectedItemsIndex].type === "free-move") { -// position = [Object!.point.x, Object!.point.y, Object!.point.z]; -// } - -// requestAnimationFrame(() => { -// setObjectPosition(new THREE.Vector3(...position)); -// setObjectRotation({ -// x: THREE.MathUtils.radToDeg(Object!.object.rotation.x), -// y: THREE.MathUtils.radToDeg(Object!.object.rotation.y), -// z: THREE.MathUtils.radToDeg(Object!.object.rotation.z), -// }); -// }); - -// updatedItems[selectedItemsIndex] = { -// ...updatedItems[selectedItemsIndex], -// position: position, -// quaternion: Object!.object.quaternion.clone() as Types.QuaternionType, -// }; - -// return updatedItems; -// }); -// } -// } -// } - -// async function handlePointerUp() { -// const Raycaster = state.raycaster; -// const intersects = Raycaster.intersectObjects( -// CSGGroup.current?.children[0].children!, -// true -// ); -// const Object = intersects.find((child) => -// child.object.name.includes("WallRaycastReference") -// ); -// if (Object) { -// if (selectedItemsIndex !== null) { -// let currentItem: any = null; -// setWallItems((prevItems: any) => { -// const updatedItems = [...prevItems]; -// const WallItemsForStorage = updatedItems.map((item) => { -// const { model, ...rest } = item; -// return { -// ...rest, -// modelUuid: model?.uuid, -// }; -// }); - -// currentItem = updatedItems[selectedItemsIndex]; -// localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); -// return updatedItems; -// }); - -// setTimeout(async () => { - -// //REST - -// // await setWallItem( -// // organization, -// // currentItem?.model?.uuid, -// // currentItem.modelName, -// // currentItem.assetId, -// // currentItem.type!, -// // currentItem.csgposition!, -// // currentItem.csgscale!, -// // currentItem.position, -// // currentItem.quaternion, -// // currentItem.scale!, -// // ) - -// //SOCKET - -// const data = { -// organization, -// modelUuid: currentItem.model?.uuid!, -// assetId: currentItem.assetId, -// modelName: currentItem.modelName!, -// type: currentItem.type!, -// csgposition: currentItem.csgposition!, -// csgscale: currentItem.csgscale!, -// position: currentItem.position!, -// quaternion: currentItem.quaternion, -// scale: currentItem.scale!, -// socketId: socket.id, -// versionId: selectedVersion?.versionId || '', -// projectId, -// userId -// }; - -// // console.log('data: ', data); -// socket.emit("v1:wallItems:set", data); -// }, 0); -// (state.controls as any)!.enabled = true; -// } -// } -// } - -// canvasElement.addEventListener("pointermove", handlePointerMove); -// canvasElement.addEventListener("pointerup", handlePointerUp); - -// return () => { -// canvasElement.removeEventListener("pointermove", handlePointerMove); -// canvasElement.removeEventListener("pointerup", handlePointerUp); -// }; -// }, [selectedItemsIndex, selectedVersion?.versionId]); - -// useEffect(() => { -// const canvasElement = state.gl.domElement; -// let drag = false; -// let isLeftMouseDown = false; - -// const onMouseDown = (evt: any) => { -// if (evt.button === 0) { -// isLeftMouseDown = true; -// drag = false; -// } -// }; - -// const onMouseUp = (evt: any) => { -// if (evt.button === 0) { -// isLeftMouseDown = false; -// if (!drag && toolMode === '3D-Delete' && activeModule === "builder") { -// DeleteWallItems( -// hoveredDeletableWallItem, -// setWallItems, -// wallItems, -// socket, -// projectId, -// selectedVersion?.versionId || '', -// ); -// } -// } -// }; - -// const onMouseMove = () => { -// if (isLeftMouseDown) { -// drag = true; -// } -// }; - -// const onDrop = (event: any) => { -// if (selectedItem.category !== 'Fenestration') return; - -// pointer.x = (event.clientX / window.innerWidth) * 2 - 1; -// pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; - -// raycaster.setFromCamera(pointer, camera); - -// if (selectedItem.id && selectedVersion && projectId) { -// if (selectedItem.subCategory) { -// AddWallItems( -// selectedItem, -// raycaster, -// CSGGroup, -// setWallItems, -// socket, -// projectId, -// selectedVersion?.versionId || '', -// ); -// } -// event.preventDefault(); -// } -// }; - -// const onDragOver = (event: any) => { -// event.preventDefault(); -// }; - -// canvasElement.addEventListener("mousedown", onMouseDown); -// canvasElement.addEventListener("mouseup", onMouseUp); -// canvasElement.addEventListener("mousemove", onMouseMove); -// canvasElement.addEventListener("drop", onDrop); -// canvasElement.addEventListener("dragover", onDragOver); - -// return () => { -// canvasElement.removeEventListener("mousedown", onMouseDown); -// canvasElement.removeEventListener("mouseup", onMouseUp); -// canvasElement.removeEventListener("mousemove", onMouseMove); -// canvasElement.removeEventListener("drop", onDrop); -// canvasElement.removeEventListener("dragover", onDragOver); -// }; -// }, [toolMode, wallItems, selectedItem, camera, selectedVersion?.versionId]); - -// useEffect(() => { -// if (toolMode && activeModule === "builder") { -// handleMeshMissed( -// currentWallItem, -// setSelectedWallItem, -// setSelectedItemsIndex -// ); -// setSelectedWallItem(null); -// setSelectedItemsIndex(null); -// } -// }, [toolMode]); - -// return ( -// <> -// {wallItems.map((item: Types.WallItem, index: number) => ( -// -// -// -// ))} -// -// ); -// }; - -// export default WallItemsGroup; diff --git a/app/src/modules/builder/groups/wallsAndWallItems.tsx b/app/src/modules/builder/groups/wallsAndWallItems.tsx deleted file mode 100644 index 6961cf6..0000000 --- a/app/src/modules/builder/groups/wallsAndWallItems.tsx +++ /dev/null @@ -1,74 +0,0 @@ -// import { Geometry } from "@react-three/csg"; -// import { -// useSelectedWallItem, -// useToggleView, -// useToolMode, -// useWallItems, -// useWalls, -// } from "../../../store/builder/store"; -// import handleMeshDown from "../eventFunctions/handleMeshDown"; -// import handleMeshMissed from "../eventFunctions/handleMeshMissed"; -// import WallsMesh from "./wallsMesh"; -// import WallItemsGroup from "./wallItemsGroup"; - -// const WallsAndWallItems = ({ -// CSGGroup, -// setSelectedItemsIndex, -// selectedItemsIndex, -// currentWallItem, -// csg, -// lines, -// hoveredDeletableWallItem, -// }: any) => { -// const { walls } = useWalls(); -// const { wallItems } = useWallItems(); -// const { toggleView } = useToggleView(); -// const { toolMode } = useToolMode(); -// const { setSelectedWallItem } = useSelectedWallItem(); - -// return ( -// { -// if (toolMode === "cursor") { -// handleMeshDown( -// event, -// currentWallItem, -// setSelectedWallItem, -// setSelectedItemsIndex, -// wallItems, -// toggleView -// ); -// } -// }} -// onPointerMissed={() => { -// if (toolMode === "cursor") { -// handleMeshMissed( -// currentWallItem, -// setSelectedWallItem, -// setSelectedItemsIndex -// ); -// setSelectedWallItem(null); -// setSelectedItemsIndex(null); -// } -// }} -// > -// -// -// -// -// -// ); -// }; - -// export default WallsAndWallItems; diff --git a/app/src/modules/builder/groups/wallsMesh.tsx b/app/src/modules/builder/groups/wallsMesh.tsx deleted file mode 100644 index 80468e5..0000000 --- a/app/src/modules/builder/groups/wallsMesh.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// import * as THREE from "three"; -// import * as Types from "../../../types/world/worldTypes"; -// import * as CONSTANTS from "../../../types/world/worldConstants"; -// import { Base } from "@react-three/csg"; -// import { MeshDiscardMaterial } from "@react-three/drei"; -// import { useUpdateScene, useWalls } from "../../../store/builder/store"; -// import React, { useEffect } from "react"; -// import { getLines } from "../../../services/factoryBuilder/lines/getLinesApi"; -// import objectLinesToArray from "../geomentries/lines/lineConvertions/objectLinesToArray"; -// import loadWalls from "../geomentries/walls/loadWalls"; -// import texturePath from "../../../assets/textures/floor/wall-tex.png"; -// import { useParams } from "react-router-dom"; -// import { getUserData } from "../../../functions/getUserData"; -// import { useVersionContext } from "../version/versionContext"; - -// const WallsMeshComponent = ({ lines }: any) => { -// const { walls, setWalls } = useWalls(); -// const { updateScene, setUpdateScene } = useUpdateScene(); -// const { projectId } = useParams(); -// const { selectedVersionStore } = useVersionContext(); -// const { selectedVersion } = selectedVersionStore(); -// const { organization } = getUserData(); - -// useEffect(() => { -// if (updateScene) { -// if (!selectedVersion) { -// setUpdateScene(false); -// return; -// }; -// getLines(organization, projectId, selectedVersion?.versionId || '').then((data) => { -// const Lines: Types.Lines = objectLinesToArray(data); -// localStorage.setItem("Lines", JSON.stringify(Lines)); - -// if (Lines) { -// loadWalls(lines, setWalls); -// } -// }); -// setUpdateScene(false); -// } -// }, [updateScene, selectedVersion?.versionId]); - -// const textureLoader = new THREE.TextureLoader(); -// const wallTexture = textureLoader.load(texturePath); - -// wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping; -// wallTexture.repeat.set(0.1, 0.1); -// wallTexture.colorSpace = THREE.SRGBColorSpace; - -// return ( -// <> -// {walls.map((wall: Types.Wall, index: number) => ( -// -// -// -// -// -// -// -// -// ))} -// -// ); -// }; - -// const WallsMesh = React.memo(WallsMeshComponent); -// export default WallsMesh; diff --git a/app/src/modules/simulation/actions/human/actionHandler/useAssemblyHandler.ts b/app/src/modules/simulation/actions/human/actionHandler/useAssemblyHandler.ts new file mode 100644 index 0000000..a654b35 --- /dev/null +++ b/app/src/modules/simulation/actions/human/actionHandler/useAssemblyHandler.ts @@ -0,0 +1,36 @@ +import { useCallback } from "react"; +import { useSceneContext } from "../../../../scene/sceneContext"; +import { useProductContext } from "../../../products/productContext"; + +export function useAssemblyHandler() { + const { materialStore, humanStore, productStore } = useSceneContext(); + const { getMaterialById } = materialStore(); + const { getModelUuidByActionUuid } = productStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct } = selectedProductStore(); + const { incrementHumanLoad, addCurrentMaterial } = humanStore(); + + const assemblyLogStatus = (materialUuid: string, status: string) => { + echo.info(`${materialUuid}, ${status}`); + } + + const handleAssembly = useCallback((action: HumanAction, materialId?: string) => { + if (!action || action.actionType !== 'assembly' || !materialId) return; + + const material = getMaterialById(materialId); + if (!material) return; + + const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid); + if (!modelUuid) return; + + incrementHumanLoad(modelUuid, 1); + addCurrentMaterial(modelUuid, material.materialType, material.materialId); + + assemblyLogStatus(material.materialName, `performing assembly action`); + + }, [getMaterialById]); + + return { + handleAssembly, + }; +} \ No newline at end of file diff --git a/app/src/modules/simulation/actions/human/useHumanActions.ts b/app/src/modules/simulation/actions/human/useHumanActions.ts index 9713f11..ba29a1b 100644 --- a/app/src/modules/simulation/actions/human/useHumanActions.ts +++ b/app/src/modules/simulation/actions/human/useHumanActions.ts @@ -1,13 +1,19 @@ import { useEffect, useCallback } from 'react'; import { useWorkerHandler } from './actionHandler/useWorkerHandler'; +import { useAssemblyHandler } from './actionHandler/useAssemblyHandler'; export function useHumanActions() { const { handleWorker } = useWorkerHandler(); + const { handleAssembly } = useAssemblyHandler(); const handleWorkerAction = useCallback((action: HumanAction, materialId: string) => { handleWorker(action, materialId); }, [handleWorker]); + const handleAssemblyAction = useCallback((action: HumanAction, materialId: string) => { + handleAssembly(action, materialId); + }, [handleAssembly]); + const handleHumanAction = useCallback((action: HumanAction, materialId: string) => { if (!action) return; @@ -15,10 +21,13 @@ export function useHumanActions() { case 'worker': handleWorkerAction(action, materialId); break; + case 'assembly': + handleAssemblyAction(action, materialId); + break; default: console.warn(`Unknown Human action type: ${action.actionType}`); } - }, [handleWorkerAction]); + }, [handleWorkerAction, handleAssemblyAction]); const cleanup = useCallback(() => { }, []); diff --git a/app/src/modules/simulation/actions/useActionHandler.ts b/app/src/modules/simulation/actions/useActionHandler.ts index 3bb95bf..7352ae1 100644 --- a/app/src/modules/simulation/actions/useActionHandler.ts +++ b/app/src/modules/simulation/actions/useActionHandler.ts @@ -39,7 +39,7 @@ export function useActionHandler() { case 'store': case 'retrieve': handleStorageAction(action as StorageAction, materialId as string); break; - case 'worker': + case 'worker': case 'assembly': handleHumanAction(action as HumanAction, materialId as string); break; default: diff --git a/app/src/modules/simulation/human/instances/animator/materialAnimator.tsx b/app/src/modules/simulation/human/instances/animator/materialAnimator.tsx index ffea67a..398bb2a 100644 --- a/app/src/modules/simulation/human/instances/animator/materialAnimator.tsx +++ b/app/src/modules/simulation/human/instances/animator/materialAnimator.tsx @@ -6,6 +6,7 @@ import { MaterialModel } from '../../../materials/instances/material/materialMod const MaterialAnimator = ({ human }: { human: HumanStatus }) => { const meshRef = useRef(null!); const [hasLoad, setHasLoad] = useState(false); + const [isAttached, setIsAttached] = useState(false); const { scene } = useThree(); useEffect(() => { @@ -13,32 +14,45 @@ const MaterialAnimator = ({ human }: { human: HumanStatus }) => { }, [human.currentLoad]); useEffect(() => { - if (!hasLoad || !meshRef.current) return; + if (!hasLoad || !meshRef.current) { + setIsAttached(false); + return; + } const humanModel = scene.getObjectByProperty("uuid", human.modelUuid) as THREE.Object3D; if (!humanModel) return; + meshRef.current.visible = false; + const bone = humanModel.getObjectByName('PlaceObjectRefBone') as THREE.Bone; if (bone) { + if (meshRef.current.parent) { + meshRef.current.parent.remove(meshRef.current); + } + bone.add(meshRef.current); meshRef.current.position.set(0, 0, 0); meshRef.current.rotation.set(0, 0, 0); meshRef.current.scale.set(1, 1, 1); + + meshRef.current.visible = true; + setIsAttached(true); } - }, [hasLoad, human.modelUuid]); + }, [hasLoad, human.modelUuid, scene]); return ( <> - {hasLoad && human.currentMaterials.length > 0 && ( + {hasLoad && human.point.action.actionType === 'worker' && human.currentMaterials.length > 0 && ( )} ); }; -export default MaterialAnimator; +export default MaterialAnimator; \ No newline at end of file diff --git a/app/src/modules/simulation/human/instances/instance/humanInstance.tsx b/app/src/modules/simulation/human/instances/instance/humanInstance.tsx index 5ba757d..104ffff 100644 --- a/app/src/modules/simulation/human/instances/instance/humanInstance.tsx +++ b/app/src/modules/simulation/human/instances/instance/humanInstance.tsx @@ -16,7 +16,7 @@ function HumanInstance({ human }: { human: HumanStatus }) { const { isPlaying } = usePlayButtonStore(); const { scene } = useThree(); const { assetStore, materialStore, armBotStore, conveyorStore, machineStore, vehicleStore, humanStore, storageUnitStore, productStore } = useSceneContext(); - const { removeMaterial, setEndTime } = materialStore(); + const { removeMaterial, setEndTime, setMaterial } = materialStore(); const { getStorageUnitById } = storageUnitStore(); const { getArmBotById } = armBotStore(); const { getConveyorById } = conveyorStore(); @@ -41,6 +41,13 @@ function HumanInstance({ human }: { human: HumanStatus }) { const previousTimeRef = useRef(null); const animationFrameIdRef = useRef(null); const humanAsset = getAssetById(human.modelUuid); + const processStartTimeRef = useRef(null); + const processTimeRef = useRef(0); + const processAnimationIdRef = useRef(null); + const accumulatedPausedTimeRef = useRef(0); + const lastPauseTimeRef = useRef(null); + const hasLoggedHalfway = useRef(false); + const hasLoggedCompleted = useRef(false); useEffect(() => { isPausedRef.current = isPaused; @@ -94,6 +101,16 @@ function HumanInstance({ human }: { human: HumanStatus }) { cancelAnimationFrame(animationFrameIdRef.current) animationFrameIdRef.current = null } + if (processAnimationIdRef.current) { + cancelAnimationFrame(processAnimationIdRef.current); + processAnimationIdRef.current = null; + } + processStartTimeRef.current = null; + processTimeRef.current = 0; + accumulatedPausedTimeRef.current = 0; + lastPauseTimeRef.current = null; + hasLoggedHalfway.current = false; + hasLoggedCompleted.current = false; const object = scene.getObjectByProperty('uuid', human.modelUuid); if (object && human) { object.position.set(human.position[0], human.position[1], human.position[2]); @@ -103,7 +120,103 @@ function HumanInstance({ human }: { human: HumanStatus }) { useEffect(() => { if (isPlaying) { - if (!human.point.action.pickUpPoint || !human.point.action.dropPoint) return; + if (!human.point.action.assemblyPoint || human.point.action.actionType === 'worker') return; + + if (!human.isActive && human.state === 'idle' && currentPhase === 'init') { + setHumanState(human.modelUuid, 'idle'); + setCurrentPhase('waiting'); + setHumanPicking(human.modelUuid, false); + setHumanActive(human.modelUuid, false); + setCurrentAnimation(human.modelUuid, 'idle', true, true, true); + humanStatus(human.modelUuid, 'Human is waiting for material in assembly'); + } else if (!human.isActive && human.state === 'idle' && currentPhase === 'waiting') { + if (human.currentMaterials.length > 0 && humanAsset && humanAsset.animationState?.current !== 'working_standing') { + setCurrentAnimation(human.modelUuid, 'working_standing', true, true, false); + setHumanState(human.modelUuid, 'running'); + setCurrentPhase('assembling'); + setHumanPicking(human.modelUuid, true); + setHumanActive(human.modelUuid, true); + + processStartTimeRef.current = performance.now(); + processTimeRef.current = human.point.action.processTime || 0; + accumulatedPausedTimeRef.current = 0; + lastPauseTimeRef.current = null; + hasLoggedHalfway.current = false; + hasLoggedCompleted.current = false; + + if (!processAnimationIdRef.current) { + processAnimationIdRef.current = requestAnimationFrame(trackAssemblyProcess); + } + } + } else if (human.isActive && human.state === 'running' && human.currentMaterials.length > 0 && humanAsset && humanAsset.animationState?.current === 'working_standing' && humanAsset.animationState?.isCompleted) { + if (human.point.action.assemblyPoint && currentPhase === 'assembling') { + setHumanState(human.modelUuid, 'idle'); + setCurrentPhase('waiting'); + setHumanPicking(human.modelUuid, false); + setHumanActive(human.modelUuid, false); + setCurrentAnimation(human.modelUuid, 'idle', true, true, true); + humanStatus(human.modelUuid, 'Human is waiting for material in assembly'); + + decrementHumanLoad(human.modelUuid, 1); + const material = removeLastMaterial(human.modelUuid); + if (material) { + triggerPointActions(human.point.action, material.materialId); + } + } + } + + } else { + reset() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [human, currentPhase, path, isPlaying, humanAsset?.animationState?.isCompleted]); + + const trackAssemblyProcess = useCallback(() => { + const now = performance.now(); + + if (!processStartTimeRef.current || !human.point.action.processTime) { + return; + } + + if (isPausedRef.current) { + if (!lastPauseTimeRef.current) { + lastPauseTimeRef.current = now; + } + processAnimationIdRef.current = requestAnimationFrame(trackAssemblyProcess); + return; + } else if (lastPauseTimeRef.current) { + accumulatedPausedTimeRef.current += now - lastPauseTimeRef.current; + lastPauseTimeRef.current = null; + } + + const elapsed = (now - processStartTimeRef.current - accumulatedPausedTimeRef.current) * isSpeedRef.current; + const totalProcessTimeMs = human.point.action.processTime * 1000; + + if (elapsed >= totalProcessTimeMs / 2 && !hasLoggedHalfway.current) { + hasLoggedHalfway.current = true; + if (human.currentMaterials.length > 0) { + setMaterial(human.currentMaterials[0].materialId, human.point.action.swapMaterial || 'Default Material'); + } + humanStatus(human.modelUuid, `🟡 Human ${human.modelUuid} reached halfway in assembly.`); + } + + if (elapsed >= totalProcessTimeMs && !hasLoggedCompleted.current) { + hasLoggedCompleted.current = true; + setCurrentAnimation(human.modelUuid, 'working_standing', true, true, true); + if (processAnimationIdRef.current) { + cancelAnimationFrame(processAnimationIdRef.current); + processAnimationIdRef.current = null; + } + humanStatus(human.modelUuid, `✅ Human ${human.modelUuid} completed assembly process.`); + return; + } + + processAnimationIdRef.current = requestAnimationFrame(trackAssemblyProcess); + }, [human.modelUuid, human.point.action.processTime, human.currentMaterials]); + + useEffect(() => { + if (isPlaying) { + if (!human.point.action.pickUpPoint || !human.point.action.dropPoint || human.point.action.actionType === 'assembly') return; if (!human.isActive && human.state === 'idle' && currentPhase === 'init') { const toPickupPath = computePath( @@ -145,7 +258,7 @@ function HumanInstance({ human }: { human: HumanStatus }) { setCurrentAnimation(human.modelUuid, 'walk_with_box', true, true, true); humanStatus(human.modelUuid, 'Started from pickup point, heading to drop point'); } - } else if (human.currentLoad === human.point.action.loadCapacity && human.currentMaterials.length > 0) { + } else if (human.currentLoad === human.point.action.loadCapacity && human.currentMaterials.length > 0 && humanAsset?.animationState?.current !== 'pickup') { setCurrentAnimation(human.modelUuid, 'pickup', true, false, false); } } else if (!human.isActive && human.state === 'idle' && currentPhase === 'dropping' && human.currentLoad === 0) { diff --git a/app/src/modules/simulation/human/instances/instance/humanUi.tsx b/app/src/modules/simulation/human/instances/instance/humanUi.tsx index 12ee8d5..da53336 100644 --- a/app/src/modules/simulation/human/instances/instance/humanUi.tsx +++ b/app/src/modules/simulation/human/instances/instance/humanUi.tsx @@ -9,13 +9,16 @@ import { useVersionContext } from '../../../../builder/version/versionContext'; import { useParams } from 'react-router-dom'; import startPoint from "../../../../../assets/gltf-glb/ui/human-ui-green.glb"; import startEnd from "../../../../../assets/gltf-glb/ui/human-ui-orange.glb"; +import assembly from "../../../../../assets/gltf-glb/ui/human-ui-assembly.glb"; import { upsertProductOrEventApi } from '../../../../../services/simulation/products/UpsertProductOrEventApi'; function HumanUi() { const { scene: startScene } = useGLTF(startPoint) as any; const { scene: endScene } = useGLTF(startEnd) as any; + const { scene: assemblyScene } = useGLTF(assembly) as any; const startMarker = useRef(null); const endMarker = useRef(null); + const assemblyMarker = useRef(null); const outerGroup = useRef(null); const prevMousePos = useRef({ x: 0, y: 0 }); const { controls, raycaster, camera } = useThree(); @@ -28,6 +31,7 @@ function HumanUi() { const [startPosition, setStartPosition] = useState<[number, number, number]>([0, 1, 0]); const [endPosition, setEndPosition] = useState<[number, number, number]>([0, 1, 0]); const [startRotation, setStartRotation] = useState<[number, number, number]>([0, Math.PI, 0]); + const [assemblyRotation, setAssemblyRotation] = useState<[number, number, number]>([0, 0, 0]); const [endRotation, setEndRotation] = useState<[number, number, number]>([0, 0, 0]); const { isDragging, setIsDragging } = useIsDragging(); const { isRotating, setIsRotating } = useIsRotating(); @@ -44,6 +48,10 @@ function HumanUi() { const { selectedVersion } = selectedVersionStore(); const { projectId } = useParams(); + const selectedHuman = selectedEventSphere ? getHumanById(selectedEventSphere.userData.modelUuid) : null; + const actionType = selectedHuman?.point?.action?.actionType || null; + const isAssembly = actionType === 'assembly'; + const updateBackend = ( productName: string, productUuid: string, @@ -99,6 +107,13 @@ function HumanUi() { setEndPosition([0, 1, 0]); setEndRotation([0, 0, 0]); } + + if (action.assemblyPoint?.rotation) { + setAssemblyRotation(action.assemblyPoint.rotation); + } else { + setAssemblyRotation([0, 0, 0]); + } + }, [selectedEventSphere, outerGroup.current, selectedAction, humans]); const handlePointerDown = ( @@ -106,6 +121,7 @@ function HumanUi() { state: "start" | "end", rotation: "start" | "end" ) => { + if (isAssembly) return; e.stopPropagation(); const intersection = new Vector3(); const pointer = new Vector2((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1); @@ -144,54 +160,68 @@ function HumanUi() { setIsDragging(null); setIsRotating(null); - if (selectedEventSphere?.userData.modelUuid && selectedAction.actionId) { - const selectedHuman = getHumanById(selectedEventSphere.userData.modelUuid); + if (!selectedEventSphere?.userData.modelUuid || !selectedAction?.actionId) return; - if (selectedHuman && outerGroup.current && startMarker.current && endMarker.current) { - const worldPosStart = new Vector3(...startPosition); - const globalStartPosition = outerGroup.current.localToWorld(worldPosStart.clone()); + const selectedHuman = getHumanById(selectedEventSphere.userData.modelUuid); + if (!selectedHuman || !outerGroup.current) return; - const worldPosEnd = new Vector3(...endPosition); - const globalEndPosition = outerGroup.current.localToWorld(worldPosEnd.clone()); + const isAssembly = selectedHuman.point?.action?.actionType === 'assembly'; - const updatedAction = { - ...selectedHuman.point.action, - pickUpPoint: { - position: [globalStartPosition.x, globalStartPosition.y, globalStartPosition.z] as [number, number, number], - rotation: startRotation - }, - dropPoint: { - position: [globalEndPosition.x, globalEndPosition.y, globalEndPosition.z] as [number, number, number], - rotation: endRotation - } - }; + let updatedAction; - const event = updateEvent( - selectedProduct.productUuid, - selectedEventSphere.userData.modelUuid, - { - ...selectedHuman, - point: { - ...selectedHuman.point, - action: updatedAction - } - } - ); + if (isAssembly) { + updatedAction = { + ...selectedHuman.point.action, + assemblyPoint: { + rotation: assemblyRotation + }, + }; + } else { + if (!startMarker.current || !endMarker.current) return; - if (event) { - updateBackend( - selectedProduct.productName, - selectedProduct.productUuid, - projectId || '', - event - ); - } + const worldPosStart = new Vector3(...startPosition); + const globalStartPosition = outerGroup.current.localToWorld(worldPosStart.clone()); + + const worldPosEnd = new Vector3(...endPosition); + const globalEndPosition = outerGroup.current.localToWorld(worldPosEnd.clone()); + + updatedAction = { + ...selectedHuman.point.action, + pickUpPoint: { + position: [globalStartPosition.x, globalStartPosition.y, globalStartPosition.z] as [number, number, number], + rotation: startRotation, + }, + dropPoint: { + position: [globalEndPosition.x, globalEndPosition.y, globalEndPosition.z] as [number, number, number], + rotation: endRotation, + }, + }; + } + + const event = updateEvent( + selectedProduct.productUuid, + selectedEventSphere.userData.modelUuid, + { + ...selectedHuman, + point: { + ...selectedHuman.point, + action: updatedAction, + }, } + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); } }; useFrame(() => { - if (!isDragging || !plane.current || !raycaster || !outerGroup.current) return; + if (isAssembly || !isDragging || !plane.current || !raycaster || !outerGroup.current) return; const intersectPoint = new Vector3(); const intersects = raycaster.ray.intersectPlane(plane.current, intersectPoint); if (!intersects) return; @@ -210,11 +240,17 @@ function HumanUi() { const currentPointerX = state.pointer.x; const deltaX = currentPointerX - prevMousePos.current.x; prevMousePos.current.x = currentPointerX; - const marker = isRotating === "start" ? startMarker.current : endMarker.current; + const marker = isRotating === "start" ? isAssembly ? assemblyMarker.current : startMarker.current : isAssembly ? assemblyMarker.current : endMarker.current; if (marker) { const rotationSpeed = 10; marker.rotation.y += deltaX * rotationSpeed; - if (isRotating === "start") { + if (isAssembly && isRotating === "start") { + setAssemblyRotation([ + marker.rotation.x, + marker.rotation.y, + marker.rotation.z, + ]); + } else if (isRotating === "start") { setStartRotation([ marker.rotation.x, marker.rotation.y, @@ -245,7 +281,7 @@ function HumanUi() { return () => { window.removeEventListener("pointerup", handleGlobalPointerUp); }; - }, [isDragging, isRotating, startPosition, startRotation, endPosition, endRotation]); + }, [isDragging, isRotating, startPosition, startRotation, endPosition, endRotation, assemblyRotation]); return ( <> @@ -255,43 +291,69 @@ function HumanUi() { ref={outerGroup} rotation={[0, Math.PI, 0]} > - { - e.stopPropagation(); - handlePointerDown(e, "start", "start"); - }} - onPointerMissed={() => { - (controls as any).enabled = true; - setIsDragging(null); - setIsRotating(null); - }} - /> + {isAssembly ? ( + { + if (e.object.parent.name === "handle") { + e.stopPropagation(); + const normalizedX = (e.clientX / window.innerWidth) * 2 - 1; + prevMousePos.current.x = normalizedX; + setIsRotating("start"); + setIsDragging(null); + if (controls) (controls as any).enabled = false; + } + }} + onPointerMissed={() => { + setIsDragging(null); + setIsRotating(null); + if (controls) (controls as any).enabled = true; + }} + /> + ) : ( + <> + { + e.stopPropagation(); + handlePointerDown(e, "start", "start"); + }} + onPointerMissed={() => { + setIsDragging(null); + setIsRotating(null); + if (controls) (controls as any).enabled = true; + }} + /> - { - e.stopPropagation(); - handlePointerDown(e, "end", "end"); - }} - onPointerMissed={() => { - (controls as any).enabled = true; - setIsDragging(null); - setIsRotating(null); - }} - /> + { + e.stopPropagation(); + handlePointerDown(e, "end", "end"); + }} + onPointerMissed={() => { + setIsDragging(null); + setIsRotating(null); + if (controls) (controls as any).enabled = true; + }} + /> + + )} )} - ) + ); } export default HumanUi \ No newline at end of file diff --git a/app/src/modules/simulation/spatialUI/vehicle/vehicleUI.tsx b/app/src/modules/simulation/spatialUI/vehicle/vehicleUI.tsx index 02eb2ec..9bd36b5 100644 --- a/app/src/modules/simulation/spatialUI/vehicle/vehicleUI.tsx +++ b/app/src/modules/simulation/spatialUI/vehicle/vehicleUI.tsx @@ -301,7 +301,6 @@ const VehicleUI = () => { return selectedVehicleData ? ( { - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } ) @@ -354,7 +362,9 @@ export function useTriggerHandler() { if (vehicle) { if (vehicle.isActive === false && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) { // Handle current action from vehicle - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } setIsPaused(materialId, true); handleAction(action, materialId); @@ -365,7 +375,9 @@ export function useTriggerHandler() { addVehicleToMonitor(vehicle.modelUuid, () => { - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } ) @@ -382,7 +394,9 @@ export function useTriggerHandler() { if (conveyor) { if (!conveyor.isPaused) { // Handle current action from vehicle - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } setIsPaused(materialId, true); handleAction(action, materialId); @@ -393,7 +407,9 @@ export function useTriggerHandler() { addConveyorToMonitor(conveyor.modelUuid, () => { - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } ) @@ -406,7 +422,9 @@ export function useTriggerHandler() { if (conveyor) { if (!conveyor.isPaused) { // Handle current action from vehicle - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } setIsPaused(materialId, true); handleAction(action, materialId); @@ -417,7 +435,9 @@ export function useTriggerHandler() { addConveyorToMonitor(conveyor.modelUuid, () => { - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } ) @@ -434,7 +454,9 @@ export function useTriggerHandler() { if (machine) { if (machine.isActive === false && machine.state === 'idle' && !machine.currentAction) { setIsPaused(materialId, true); - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } else { @@ -443,7 +465,9 @@ export function useTriggerHandler() { addMachineToMonitor(machine.modelUuid, () => { - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } ) @@ -456,7 +480,9 @@ export function useTriggerHandler() { if (machine) { if (machine.isActive === false && machine.state === 'idle' && !machine.currentAction) { setIsPaused(materialId, true); - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } else { @@ -465,7 +491,9 @@ export function useTriggerHandler() { addMachineToMonitor(machine.modelUuid, () => { - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } ) @@ -480,7 +508,9 @@ export function useTriggerHandler() { // Handle current action from arm bot setIsPaused(materialId, true); - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } else { @@ -489,7 +519,9 @@ export function useTriggerHandler() { setIsPaused(materialId, true); addHumanToMonitor(human.modelUuid, () => { - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId) } ); @@ -501,7 +533,9 @@ export function useTriggerHandler() { // Handle current action from arm bot setIsPaused(materialId, true); - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId); } else { @@ -510,7 +544,9 @@ export function useTriggerHandler() { setIsPaused(materialId, true); addHumanToMonitor(human.modelUuid, () => { - setIsVisible(materialId, false); + if (action.actionType === 'worker') { + setIsVisible(materialId, false); + } handleAction(action, materialId) } ); @@ -1280,7 +1316,7 @@ export function useTriggerHandler() { if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) { const material = getMaterialById(materialId); - if (material) { + if (material && action.actionType === 'worker') { setPreviousLocation(material.materialId, { modelUuid: material.current.modelUuid, @@ -1410,6 +1446,28 @@ export function useTriggerHandler() { handleAction(action, material.materialId); } + } else if (material && action.actionType === 'assembly') { + + setPreviousLocation(material.materialId, { + modelUuid: material.current.modelUuid, + pointUuid: material.current.pointUuid, + actionUuid: material.current.actionUuid, + }) + + setCurrentLocation(material.materialId, { + modelUuid: material.current.modelUuid, + pointUuid: material.current.pointUuid, + actionUuid: material.current.actionUuid, + }); + + setNextLocation(material.materialId, { + modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid, + pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid, + }) + + setIsPaused(material.materialId, false); + setIsVisible(material.materialId, true); + } } diff --git a/app/src/modules/simulation/vehicle/instances/animator/materialAnimator.tsx b/app/src/modules/simulation/vehicle/instances/animator/materialAnimator.tsx index 6a6a32a..8b943d9 100644 --- a/app/src/modules/simulation/vehicle/instances/animator/materialAnimator.tsx +++ b/app/src/modules/simulation/vehicle/instances/animator/materialAnimator.tsx @@ -6,40 +6,53 @@ import { MaterialModel } from '../../../materials/instances/material/materialMod const MaterialAnimator = ({ agvDetail }: { agvDetail: VehicleStatus }) => { const meshRef = useRef(null!); const [hasLoad, setHasLoad] = useState(false); + const [isAttached, setIsAttached] = useState(false); const { scene } = useThree(); const offset = new THREE.Vector3(0, 0.85, 0); useEffect(() => { - setHasLoad(agvDetail.currentLoad > 0); + const loadState = agvDetail.currentLoad > 0; + setHasLoad(loadState); + + if (!loadState) { + setIsAttached(false); + if (meshRef.current?.parent) { + meshRef.current.parent.remove(meshRef.current); + } + } }, [agvDetail.currentLoad]); useFrame(() => { - if (!hasLoad || !meshRef.current) return; + if (!hasLoad || !meshRef.current || isAttached) return; const agvModel = scene.getObjectByProperty("uuid", agvDetail.modelUuid) as THREE.Object3D; - if (agvModel) { - const worldPosition = offset.clone().applyMatrix4(agvModel.matrixWorld); - meshRef.current.position.copy(worldPosition); - meshRef.current.rotation.copy(agvModel.rotation); + if (agvModel && !isAttached) { + if (meshRef.current.parent) { + meshRef.current.parent.remove(meshRef.current); + } + + agvModel.add(meshRef.current); + + meshRef.current.position.copy(offset); + meshRef.current.rotation.set(0, 0, 0); + meshRef.current.scale.set(1, 1, 1); + + setIsAttached(true); } }); return ( <> - {hasLoad && ( - <> - {agvDetail.currentMaterials.length > 0 && - - } - + {hasLoad && agvDetail.currentMaterials.length > 0 && ( + )} ); }; - -export default MaterialAnimator; +export default MaterialAnimator; \ No newline at end of file diff --git a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx index a5255e0..be10213 100644 --- a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx +++ b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx @@ -80,7 +80,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai const distances = []; let accumulatedDistance = 0; let index = 0; - const rotationSpeed = 1; + const rotationSpeed = 0.75; for (let i = 0; i < currentPath.length - 1; i++) { const start = new THREE.Vector3(...currentPath[i]); @@ -100,17 +100,25 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai const end = new THREE.Vector3(...currentPath[index + 1]); const segmentDistance = distances[index]; - const currentDirection = new THREE.Vector3().subVectors(end, start).normalize(); - const targetAngle = Math.atan2(currentDirection.x, currentDirection.z); - const currentAngle = object.rotation.y; + const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0))); + const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI); + targetQuaternion.multiply(y180); - let angleDifference = targetAngle - currentAngle; - if (angleDifference > Math.PI) angleDifference -= 2 * Math.PI; - if (angleDifference < -Math.PI) angleDifference += 2 * Math.PI; + const angle = object.quaternion.angleTo(targetQuaternion); + if (angle < 0.01) { + object.quaternion.copy(targetQuaternion); + } else { + const step = rotationSpeed * delta * speed * agvDetail.speed; + const angle = object.quaternion.angleTo(targetQuaternion); - const maxRotationStep = (rotationSpeed * speed * agvDetail.speed) * delta; - object.rotation.y += Math.sign(angleDifference) * Math.min(Math.abs(angleDifference), maxRotationStep); - const isAligned = Math.abs(angleDifference) < 0.01; + if (angle < step) { + object.quaternion.copy(targetQuaternion); + } else { + object.quaternion.rotateTowards(targetQuaternion, step); + } + } + + const isAligned = angle < 0.01; if (isAligned) { progressRef.current += delta * (speed * agvDetail.speed); @@ -122,17 +130,25 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai if (progressRef.current >= totalDistance) { if (restRotation && objectRotation) { - const targetEuler = new THREE.Euler( - objectRotation.x, - objectRotation.y - agvDetail.point.action.steeringAngle, - objectRotation.z - ); - const targetQuaternion = new THREE.Quaternion().setFromEuler(targetEuler); - object.quaternion.slerp(targetQuaternion, delta * (rotationSpeed * speed * agvDetail.speed)); - if (object.quaternion.angleTo(targetQuaternion) < 0.01) { + const targetEuler = new THREE.Euler(0, objectRotation.y - agvDetail.point.action.steeringAngle, 0); + + const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler); + const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI); + const targetQuaternion = baseQuaternion.multiply(y180); + + const angle = object.quaternion.angleTo(targetQuaternion); + if (angle < 0.01) { object.quaternion.copy(targetQuaternion); - object.rotation.copy(targetEuler); setRestingRotation(false); + } else { + const step = rotationSpeed * delta * speed * agvDetail.speed; + const angle = object.quaternion.angleTo(targetQuaternion); + + if (angle < step) { + object.quaternion.copy(targetQuaternion); + } else { + object.quaternion.rotateTowards(targetQuaternion, step); + } } return; } diff --git a/app/src/store/simulation/useProductStore.ts b/app/src/store/simulation/useProductStore.ts index 8e4a306..1a37964 100644 --- a/app/src/store/simulation/useProductStore.ts +++ b/app/src/store/simulation/useProductStore.ts @@ -18,13 +18,13 @@ type ProductsStore = { updateEvent: (productUuid: string, modelUuid: string, updates: Partial) => EventsSchema | undefined; // Point-level actions - addPoint: (productUuid: string, modelUuid: string, point: ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema) => void; + addPoint: (productUuid: string, modelUuid: string, point: ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema | HumanPointSchema) => void; removePoint: (productUuid: string, modelUuid: string, pointUuid: string) => void; updatePoint: ( productUuid: string, modelUuid: string, pointUuid: string, - updates: Partial + updates: Partial ) => EventsSchema | undefined; // Action-level actions @@ -65,9 +65,9 @@ type ProductsStore = { getEventByActionUuid: (productUuid: string, actionUuid: string) => EventsSchema | undefined; getEventByTriggerUuid: (productUuid: string, triggerUuid: string) => EventsSchema | undefined; getEventByPointUuid: (productUuid: string, pointUuid: string) => EventsSchema | undefined; - getPointByUuid: (productUuid: string, modelUuid: string, pointUuid: string) => ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema | undefined; - getActionByUuid: (productUuid: string, actionUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']) | undefined; - getActionByPointUuid: (productUuid: string, pointUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']) | undefined; + getPointByUuid: (productUuid: string, modelUuid: string, pointUuid: string) => ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema | HumanPointSchema | undefined; + getActionByUuid: (productUuid: string, actionUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action'] | HumanPointSchema['action']) | undefined; + getActionByPointUuid: (productUuid: string, pointUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action'] | HumanPointSchema['action']) | undefined; getModelUuidByPointUuid: (productUuid: string, actionUuid: string) => (string) | undefined; getModelUuidByActionUuid: (productUuid: string, actionUuid: string) => (string) | undefined; getPointUuidByActionUuid: (productUuid: string, actionUuid: string) => (string) | undefined; diff --git a/app/src/types/simulationTypes.d.ts b/app/src/types/simulationTypes.d.ts index 678f117..133cb74 100644 --- a/app/src/types/simulationTypes.d.ts +++ b/app/src/types/simulationTypes.d.ts @@ -72,7 +72,10 @@ interface StorageAction { interface HumanAction { actionUuid: string; actionName: string; - actionType: "worker"; + actionType: "worker" | "assembly"; + processTime?: number; + swapMaterial?: string; + assemblyPoint?: { rotation: [number, number, number] | null; } pickUpPoint?: { position: [number, number, number] | null; rotation: [number, number, number] | null; } dropPoint?: { position: [number, number, number] | null; rotation: [number, number, number] | null; } loadCapacity: number; From 5070ff4060e00df462c51a67017e1d9c4ae5bf15 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 7 Jul 2025 15:11:49 +0530 Subject: [PATCH 4/4] feat: Update human UI assets with new GLB models --- app/src/assets/gltf-glb/ui/human-ui-green.glb | Bin 18904 -> 19428 bytes .../assets/gltf-glb/ui/human-ui-orange.glb | Bin 18904 -> 19428 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/assets/gltf-glb/ui/human-ui-green.glb b/app/src/assets/gltf-glb/ui/human-ui-green.glb index 3dac9d616271f766f8de7c0e119c39cc5e9a1ee6..faa8ce7ba3b9d2b1edfb5827ec0893070763724d 100644 GIT binary patch literal 19428 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 diff --git a/app/src/assets/gltf-glb/ui/human-ui-orange.glb b/app/src/assets/gltf-glb/ui/human-ui-orange.glb index 9624cb2ba8d4812450415346238c5bc41f89cba3..d352c13c4f50a600a8edd8f619c8d1a0b8f920b5 100644 GIT binary patch literal 19428 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 af*^realkRXiiyxykiI^dJ0RIQ4#uf_z