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 0000000..844fc9b Binary files /dev/null and b/app/src/assets/gltf-glb/ui/human-ui-assembly.glb differ diff --git a/app/src/assets/gltf-glb/ui/human-ui-green.glb b/app/src/assets/gltf-glb/ui/human-ui-green.glb new file mode 100644 index 0000000..3dac9d6 Binary files /dev/null and b/app/src/assets/gltf-glb/ui/human-ui-green.glb differ 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 0000000..9624cb2 Binary files /dev/null and b/app/src/assets/gltf-glb/ui/human-ui-orange.glb differ 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) => {