From 74094aee9f1f2028a57f65dcff8c487f9c3c63c0 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 7 Jul 2025 15:00:16 +0530 Subject: [PATCH] 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;