diff --git a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx index a62abda..9c8b979 100644 --- a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx @@ -32,9 +32,9 @@ const AssetProperties: React.FC = () => { setUserData([]); }; - const handleUserDataChange = (id: number, newValue: string) => {}; + const handleUserDataChange = (id: number, newValue: string) => { }; - const handleRemoveUserData = (id: number) => {}; + const handleRemoveUserData = (id: number) => { }; const handleAnimationClick = (animation: string) => { if (selectedFloorItem) { @@ -57,14 +57,14 @@ const AssetProperties: React.FC = () => {
{objectPosition && ( {}} + onChange={() => { }} value1={parseFloat(objectPosition.x.toFixed(5))} value2={parseFloat(objectPosition.z.toFixed(5))} /> )} {objectRotation && ( {}} + onChange={() => { }} value={parseFloat(objectRotation.y.toFixed(5))} /> )} @@ -107,7 +107,7 @@ const AssetProperties: React.FC = () => { if (asset.modelUuid !== selectedFloorItem.uuid || !asset.animations) return ( i === 0 && ( -
+
Looks like there are no preset animations yet. Stay tuned for future additions!
diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index d5d49e4..b89f2ea 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -146,6 +146,12 @@ const Tools: React.FC = () => { case "draw-floor": is2D && setToolMode("Floor"); break; + case "move": + if (!is2D) setToolMode("Move-Asset"); + break; + case "rotate": + if (!is2D) setToolMode("Rotate-Asset"); + break; case "measure": setToolMode("MeasurementScale"); break; diff --git a/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts b/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts index 360c513..85b5b4b 100644 --- a/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts +++ b/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts @@ -34,7 +34,7 @@ export function useModelEventHandlers({ const { socket } = useSocketStore(); const { eventStore, productStore, assetStore, undoRedo3DStore } = useSceneContext(); const { push3D } = undoRedo3DStore(); - const { getAssetById, removeAsset } = assetStore(); + const { removeAsset } = assetStore(); const { zoneAssetId, setZoneAssetId } = useZoneAssetId(); const { resourceManagementId, setResourceManagementId } = useResourceManagementId(); const { removeEvent, getEventByModelUuid } = eventStore(); @@ -77,11 +77,12 @@ export function useModelEventHandlers({ } }, [zoneAssetId]) + useEffect(() => { if (!resourceManagementId) return if (resourceManagementId === asset.modelUuid) { - - + + handleDblClick(asset); } diff --git a/app/src/modules/scene/controls/contextControls/contextControls.tsx b/app/src/modules/scene/controls/contextControls/contextControls.tsx index 142f8d1..46801c1 100644 --- a/app/src/modules/scene/controls/contextControls/contextControls.tsx +++ b/app/src/modules/scene/controls/contextControls/contextControls.tsx @@ -1,41 +1,14 @@ import { useEffect, useRef, useState } from "react"; import { useThree } from "@react-three/fiber"; import { CameraControls, Html, ScreenSpace } from "@react-three/drei"; -import { - useContextActionStore, - useRenameModeStore, - useSelectedAssets, -} from "../../../../store/builder/store"; +import { useContextActionStore, useRenameModeStore, useSelectedAssets, useToggleView, useToolMode, } from "../../../../store/builder/store"; import ContextMenu from "../../../../components/ui/menu/contextMenu"; +import useModuleStore from "../../../../store/useModuleStore"; function ContextControls() { - const { gl, controls } = useThree(); - const [canRender, setCanRender] = useState(false); - const [visibility, setVisibility] = useState({ - rename: true, - focus: true, - flipX: true, - flipZ: true, - move: true, - rotate: true, - duplicate: true, - copy: true, - paste: true, - modifier: false, - group: false, - array: false, - delete: true, - }); - const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); - const { selectedAssets } = useSelectedAssets(); - const { setContextAction } = useContextActionStore(); - const { setIsRenameMode } = useRenameModeStore(); - const rightDrag = useRef(false); - const isRightMouseDown = useRef(false); - - useEffect(() => { - if (selectedAssets.length === 1) { - setVisibility({ + const { gl, controls } = useThree(); + const [canRender, setCanRender] = useState(false); + const [visibility, setVisibility] = useState({ rename: true, focus: true, flipX: true, @@ -49,195 +22,219 @@ function ContextControls() { group: false, array: false, delete: true, - }); - } else if (selectedAssets.length > 1) { - setVisibility({ - rename: false, - focus: true, - flipX: true, - flipZ: true, - move: true, - rotate: true, - duplicate: true, - copy: true, - paste: true, - modifier: false, - group: true, - array: false, - delete: true, - }); - } else { - setVisibility({ - rename: false, - focus: false, - flipX: false, - flipZ: false, - move: false, - rotate: false, - duplicate: false, - copy: false, - paste: false, - modifier: false, - group: false, - array: false, - delete: false, - }); - } - }, [selectedAssets]); + }); + const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); + const { toggleView } = useToggleView(); + const { activeModule } = useModuleStore(); + const { toolMode } = useToolMode(); + const { selectedAssets } = useSelectedAssets(); + const { setContextAction } = useContextActionStore(); + const { setIsRenameMode } = useRenameModeStore(); + const rightDrag = useRef(false); + const isRightMouseDown = useRef(false); - useEffect(() => { - const canvasElement = gl.domElement; - - const onPointerDown = (evt: any) => { - if (evt.button === 2) { - isRightMouseDown.current = true; - rightDrag.current = false; - } - }; - - const onPointerMove = () => { - if (isRightMouseDown.current) { - rightDrag.current = true; - } - }; - - const onPointerUp = (evt: any) => { - if (evt.button === 2) { - isRightMouseDown.current = false; - } - }; - - const handleContextClick = (event: MouseEvent) => { - event.preventDefault(); - if (rightDrag.current) return; - if (selectedAssets.length > 0) { - setMenuPosition({ - x: event.clientX - gl.domElement.width / 2, - y: event.clientY - gl.domElement.height / 2, - }); - setCanRender(true); - if (controls) { - (controls as CameraControls).enabled = false; + useEffect(() => { + if (selectedAssets.length === 1) { + setVisibility({ + rename: true, + focus: true, + flipX: true, + flipZ: true, + move: true, + rotate: true, + duplicate: true, + copy: true, + paste: true, + modifier: false, + group: false, + array: false, + delete: true, + }); + } else if (selectedAssets.length > 1) { + setVisibility({ + rename: false, + focus: true, + flipX: true, + flipZ: true, + move: true, + rotate: true, + duplicate: true, + copy: true, + paste: true, + modifier: false, + group: true, + array: false, + delete: true, + }); + } else { + setVisibility({ + rename: false, + focus: false, + flipX: false, + flipZ: false, + move: false, + rotate: false, + duplicate: false, + copy: false, + paste: false, + modifier: false, + group: false, + array: false, + delete: false, + }); } - } else { + }, [selectedAssets]); + + useEffect(() => { + const canvasElement = gl.domElement; + + const onPointerDown = (evt: any) => { + if (evt.button === 2) { + isRightMouseDown.current = true; + rightDrag.current = false; + } + }; + + const onPointerMove = () => { + if (isRightMouseDown.current) { + rightDrag.current = true; + } + }; + + const onPointerUp = (evt: any) => { + if (evt.button === 2) { + isRightMouseDown.current = false; + } + }; + + const handleContextClick = (event: MouseEvent) => { + event.preventDefault(); + if (rightDrag.current) return; + if (selectedAssets.length > 0) { + setMenuPosition({ x: event.clientX - gl.domElement.width / 2, y: event.clientY - gl.domElement.height / 2, }); + setCanRender(true); + if (controls) { + (controls as CameraControls).enabled = false; + } + } else { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + } + }; + + if (selectedAssets.length > 0 && !toggleView && activeModule === "builder" && toolMode === 'cursor') { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + canvasElement.addEventListener("contextmenu", handleContextClick); + } else { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setMenuPosition({ x: 0, y: 0 }); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("contextmenu", handleContextClick); + }; + }, [controls, gl, selectedAssets, toggleView, activeModule, toolMode]); + + const handleAssetRename = () => { setCanRender(false); if (controls) { - (controls as CameraControls).enabled = true; + (controls as CameraControls).enabled = true; } - } + setContextAction("renameAsset"); + setIsRenameMode(true); + }; + const handleAssetFocus = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("focusAsset"); + }; + const handleAssetMove = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("moveAsset"); + }; + const handleAssetRotate = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("rotateAsset"); + }; + const handleAssetCopy = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("copyAsset"); + }; + const handleAssetPaste = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("pasteAsset"); + }; + const handleAssetDelete = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("deleteAsset"); + }; + const handleAssetDuplicate = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("duplicateAsset"); }; - if (selectedAssets.length > 0) { - canvasElement.addEventListener("pointerdown", onPointerDown); - canvasElement.addEventListener("pointermove", onPointerMove); - canvasElement.addEventListener("pointerup", onPointerUp); - canvasElement.addEventListener("contextmenu", handleContextClick); - } else { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setMenuPosition({ x: 0, y: 0 }); - } - - return () => { - canvasElement.removeEventListener("pointerdown", onPointerDown); - canvasElement.removeEventListener("pointermove", onPointerMove); - canvasElement.removeEventListener("pointerup", onPointerUp); - canvasElement.removeEventListener("contextmenu", handleContextClick); - }; - }, [controls, gl, selectedAssets]); - - const handleAssetRename = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("renameAsset"); - setIsRenameMode(true); - }; - const handleAssetFocus = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("focusAsset"); - }; - const handleAssetMove = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("moveAsset"); - }; - const handleAssetRotate = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("rotateAsset"); - }; - const handleAssetCopy = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("copyAsset"); - }; - const handleAssetPaste = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("pasteAsset"); - }; - const handleAssetDelete = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("deleteAsset"); - }; - const handleAssetDuplicate = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("duplicateAsset"); - }; - - return ( - <> - {canRender && ( - - - handleAssetRename()} - onFocus={() => handleAssetFocus()} - onFlipX={() => console.log("Flip to X")} - onFlipZ={() => console.log("Flip to Z")} - onMove={() => handleAssetMove()} - onRotate={() => handleAssetRotate()} - onDuplicate={() => handleAssetDuplicate()} - onCopy={() => handleAssetCopy()} - onPaste={() => handleAssetPaste()} - onGroup={() => console.log("Group")} - onArray={() => console.log("Array")} - onDelete={() => handleAssetDelete()} - /> - - - )} - - ); + return ( + <> + {canRender && ( + + + handleAssetRename()} + onFocus={() => handleAssetFocus()} + onFlipX={() => console.log("Flip to X")} + onFlipZ={() => console.log("Flip to Z")} + onMove={() => handleAssetMove()} + onRotate={() => handleAssetRotate()} + onDuplicate={() => handleAssetDuplicate()} + onCopy={() => handleAssetCopy()} + onPaste={() => handleAssetPaste()} + onGroup={() => console.log("Group")} + onArray={() => console.log("Array")} + onDelete={() => handleAssetDelete()} + /> + + + )} + + ); } export default ContextControls; diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx index e0940c9..248ec9d 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx @@ -1,7 +1,7 @@ import * as THREE from "three"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useFrame, useThree } from "@react-three/fiber"; -import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView, } from "../../../../../store/builder/store"; +import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView, useToolMode, } from "../../../../../store/builder/store"; import * as Types from "../../../../../types/world/worldTypes"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; @@ -12,6 +12,7 @@ import { useProductContext } from "../../../../simulation/products/productContex import { getUserData } from "../../../../../functions/getUserData"; import { useSceneContext } from "../../../sceneContext"; import { useVersionContext } from "../../../../builder/version/versionContext"; +import useModuleStore from "../../../../../store/useModuleStore"; // import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; @@ -20,6 +21,8 @@ function MoveControls3D({ boundingBoxRef }: any) { const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); const { toggleView } = useToggleView(); + const { toolMode } = useToolMode(); + const { activeModule } = useModuleStore(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); @@ -162,7 +165,7 @@ function MoveControls3D({ boundingBoxRef }: any) { } }; - if (!toggleView) { + if (!toggleView && toolMode === 'cursor') { canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); @@ -178,7 +181,14 @@ function MoveControls3D({ boundingBoxRef }: any) { canvasElement?.removeEventListener("keyup", onKeyUp); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [camera, controls, scene, toggleView, selectedAssets, socket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent, initialStates]); + }, [camera, controls, scene, toggleView, toolMode, selectedAssets, socket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent, initialStates]); + + useEffect(() => { + if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) { + resetToInitialPositions(); + setMovedObjects([]); + } + }, [activeModule, toolMode, toggleView]); const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => { const pointPosition = new THREE.Vector3().copy(point.position); diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx index 5af7a3e..66904b2 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx @@ -1,7 +1,7 @@ import * as THREE from "three"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useFrame, useThree } from "@react-three/fiber"; -import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store"; +import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView, useToolMode } from "../../../../../store/builder/store"; import * as Types from "../../../../../types/world/worldTypes"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; import { useParams } from "react-router-dom"; @@ -11,6 +11,7 @@ import { useSceneContext } from "../../../sceneContext"; import { useVersionContext } from "../../../../builder/version/versionContext"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { handleAssetRotationSnap } from "./functions/handleAssetRotationSnap"; +import useModuleStore from "../../../../../store/useModuleStore"; // import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; @@ -18,6 +19,8 @@ function RotateControls3D() { const { camera, gl, scene, pointer, raycaster } = useThree(); const { toggleView } = useToggleView(); + const { toolMode } = useToolMode(); + const { activeModule } = useModuleStore(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); @@ -137,7 +140,7 @@ function RotateControls3D() { prevPointerPosition.current = currentPointer.clone(); }; - if (!toggleView) { + if (!toggleView && toolMode === 'cursor') { canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); @@ -152,7 +155,14 @@ function RotateControls3D() { canvasElement.removeEventListener("keydown", onKeyDown); canvasElement?.removeEventListener("keyup", onKeyUp); }; - }, [camera, scene, toggleView, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects, keyEvent, initialPositions, initialRotations]); + }, [camera, scene, toggleView, toolMode, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects, keyEvent, initialPositions, initialRotations]); + + useEffect(() => { + if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) { + resetToInitialRotations(); + setRotatedObjects([]); + } + }, [activeModule, toolMode, toggleView]); const resetToInitialRotations = useCallback(() => { setTimeout(() => { @@ -385,7 +395,7 @@ function RotateControls3D() { setIsRotating(false); clearSelection(); - }, [rotatedObjects, eventStore, productStore, selectedProduct, updateBackend, projectId, updateAsset, organization, socket, selectedVersion, userId]); + }, [rotatedObjects, eventStore, productStore, selectedProduct, updateBackend, projectId, updateAsset, organization, socket, selectedVersion, userId, initialPositions, initialRotations]); const clearSelection = () => { setPastedObjects([]); diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx index ed458c2..512b80f 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx @@ -16,22 +16,23 @@ import DuplicationControls3D from "./duplicationControls3D"; import CopyPasteControls3D from "./copyPasteControls3D"; import MoveControls3D from "./moveControls3D"; import RotateControls3D from "./rotateControls3D"; +import TransformControls3D from "./transformControls3D"; // import { deleteFloorItem } from '../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorItemApi'; const SelectionControls3D: React.FC = () => { const { camera, controls, gl, scene, raycaster, pointer } = useThree(); const { toggleView } = useToggleView(); + const { activeModule } = useModuleStore(); + const { toolMode } = useToolMode(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const boundingBoxRef = useRef(); - const { activeModule } = useModuleStore(); const { socket } = useSocketStore(); const { contextAction, setContextAction } = useContextActionStore() const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext(); const { push3D } = undoRedo3DStore(); const { removeAsset, getAssetById, movedObjects, rotatedObjects, copiedObjects, pastedObjects, duplicatedObjects, setPastedObjects, setDuplicatedObjects } = assetStore(); const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]); - const { toolMode } = useToolMode(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { selectedProductStore } = useProductContext(); @@ -202,7 +203,7 @@ const SelectionControls3D: React.FC = () => { rightClickMoved.current = false; }; - if (!toggleView && activeModule === "builder" && toolMode === 'cursor') { + if (!toggleView && activeModule === "builder" && (toolMode === 'cursor' || toolMode === 'Move-Asset' || toolMode === 'Rotate-Asset')) { helper.enabled = true; canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); @@ -227,7 +228,7 @@ const SelectionControls3D: React.FC = () => { }, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, rotatedObjects, activeModule, toolMode]); useEffect(() => { - if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) { + if (activeModule !== "builder" || (toolMode !== 'cursor' && toolMode !== 'Move-Asset' && toolMode !== 'Rotate-Asset') || toggleView) { clearSelection(); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -381,6 +382,8 @@ const SelectionControls3D: React.FC = () => { + + ); }; diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/transformControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/transformControls3D.tsx new file mode 100644 index 0000000..a6aa2bb --- /dev/null +++ b/app/src/modules/scene/controls/selectionControls/selection3D/transformControls3D.tsx @@ -0,0 +1,380 @@ +import * as THREE from 'three'; +import { useRef, useEffect, useState, useCallback } from 'react'; +import { useThree } from '@react-three/fiber'; +import { TransformControls } from '@react-three/drei'; +import { useSelectedAssets, useSocketStore, useToolMode } from '../../../../../store/builder/store'; +import { useProductContext } from '../../../../simulation/products/productContext'; +import { useVersionContext } from '../../../../builder/version/versionContext'; +import { useSceneContext } from '../../../sceneContext'; +import { useParams } from 'react-router-dom'; + +import { getUserData } from '../../../../../functions/getUserData'; +import { upsertProductOrEventApi } from '../../../../../services/simulation/products/UpsertProductOrEventApi'; +import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys'; +// import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; + +function TransformControls3D() { + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const { toolMode } = useToolMode(); + const { camera, scene, gl } = useThree(); + const transformControlsRef = useRef(null); + const [visible, setVisible] = useState(false); + const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); + const { socket } = useSocketStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct } = selectedProductStore(); + const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext(); + const { push3D, subscribeUndoRedo } = undoRedo3DStore(); + const { updateAsset, getAssetById } = assetStore(); + const { userId, organization } = getUserData(); + const { selectedVersionStore } = useVersionContext(); + const { selectedVersion } = selectedVersionStore(); + const { projectId } = useParams(); + const pivotPointRef = useRef(new THREE.Vector3()); + const initialPositionsRef = useRef>(new Map()); + const initialRotationsRef = useRef>(new Map()); + const initialPivotPositionRef = useRef(new THREE.Vector3()); + const initialQuaternionsRef = useRef>(new Map()); + const tempObjectRef = useRef(new THREE.Object3D()); + + const updateBackend = useCallback(( + productName: string, + productUuid: string, + projectId: string, + eventData: any + ) => { + upsertProductOrEventApi({ + productName: productName, + productUuid: productUuid, + projectId: projectId, + eventDatas: eventData, + versionId: selectedVersion?.versionId || '', + }); + }, [selectedVersion]); + + const recalcGizmo = useCallback(() => { + if (selectedAssets.length === 0) return; + + const bbox = new THREE.Box3(); + selectedAssets.forEach(obj => bbox.expandByObject(obj)); + bbox.getCenter(pivotPointRef.current); + initialPivotPositionRef.current.copy(pivotPointRef.current); + + tempObjectRef.current.position.copy(pivotPointRef.current); + tempObjectRef.current.rotation.set(0, 0, 0); + tempObjectRef.current.scale.set(1, 1, 1); + + initialPositionsRef.current.clear(); + initialRotationsRef.current.clear(); + initialQuaternionsRef.current.clear(); + + selectedAssets.forEach(obj => { + initialPositionsRef.current.set(obj.uuid, obj.position.clone()); + initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); + initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); + }); + }, [selectedAssets]); + + useEffect(() => { + const unsubscribe = subscribeUndoRedo(() => { + setTimeout(() => { + recalcGizmo(); + }, 10); + }); + return unsubscribe; + }, [subscribeUndoRedo, recalcGizmo]); + + const handleTransformationComplete = useCallback(() => { + if (selectedAssets.length === 0) return; + + const updatedAssets = [...selectedAssets]; + setSelectedAssets(updatedAssets); + + selectedAssets.forEach(obj => { + const asset = getAssetById(obj.uuid); + if (!asset) return; + + updateAsset(asset.modelUuid, { + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }); + + if (asset.eventData) { + const eventData = eventStore.getState().getEventByModelUuid(asset.modelUuid); + const productData = productStore.getState().getEventByModelUuid( + selectedProduct.productUuid, + asset.modelUuid + ); + + if (eventData) { + eventStore.getState().updateEvent(asset.modelUuid, { + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }); + } + + if (productData) { + const event = productStore + .getState() + .updateEvent( + selectedProduct.productUuid, + asset.modelUuid, + { + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + } + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + } + } + } + + //REST + + // setAssetsApi( + // organization, + // asset.modelUuid, + // asset.modelName, + // [obj.position.x, 0, obj.position.z], + // { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + // asset.assetId, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization, + modelUuid: asset.modelUuid, + modelName: asset.modelName, + assetId: asset.assetId, + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id, + versionId: selectedVersion?.versionId || '', + userId, + projectId + }; + + socket.emit("v1:model-asset:add", data); + + }); + }, [selectedAssets, setSelectedAssets, getAssetById, updateAsset, eventStore, productStore, selectedProduct, updateBackend, projectId, organization, socket, selectedVersion, userId, push3D]); + + useEffect(() => { + const temp = tempObjectRef.current; + scene.add(temp); + return () => { + scene.remove(temp); + }; + }, [scene]); + + useEffect(() => { + if (selectedAssets.length === 0) { + setVisible(false); + return; + } + + const bbox = new THREE.Box3(); + selectedAssets.forEach(obj => bbox.expandByObject(obj)); + bbox.getCenter(pivotPointRef.current); + initialPivotPositionRef.current.copy(pivotPointRef.current); + + tempObjectRef.current.position.copy(pivotPointRef.current); + tempObjectRef.current.rotation.set(0, 0, 0); + tempObjectRef.current.scale.set(1, 1, 1); + + initialPositionsRef.current.clear(); + initialRotationsRef.current.clear(); + initialQuaternionsRef.current.clear(); + selectedAssets.forEach(obj => { + initialPositionsRef.current.set(obj.uuid, obj.position.clone()); + initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); + initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); + }); + + setVisible(true); + }, [selectedAssets]); + + useEffect(() => { + const controls = transformControlsRef.current; + if (!controls || !visible) return; + + controls.attach(tempObjectRef.current); + controls.setMode(toolMode === 'Move-Asset' ? 'translate' : 'rotate'); + + const onObjectChange = () => { + const temp = tempObjectRef.current; + if (!temp) return; + + selectedAssets.forEach(obj => { + const initialPos = initialPositionsRef.current.get(obj.uuid); + const initialQuat = initialQuaternionsRef.current.get(obj.uuid); + if (!initialPos || !initialQuat) return; + + if (controls.mode === 'translate') { + const delta = new THREE.Vector3().copy(temp.position).sub(initialPivotPositionRef.current); + obj.position.copy(initialPos).add(delta); + } else if (controls.mode === 'rotate') { + const deltaQuat = new THREE.Quaternion().setFromEuler(temp.rotation); + const relPos = initialPos.clone().sub(initialPivotPositionRef.current); + relPos.applyQuaternion(deltaQuat); + obj.position.copy(initialPivotPositionRef.current).add(relPos); + obj.quaternion.copy(initialQuat).multiply(deltaQuat); + } + }); + }; + + const onMouseUp = () => { + const assetsToUpdate: AssetData[] = []; + const undoActions: UndoRedo3DAction[] = []; + + selectedAssets.forEach((obj) => { + const asset = getAssetById(obj.uuid); + if (!asset) return; + + const initialPos = initialPositionsRef.current.get(obj.uuid); + const initialRot = initialRotationsRef.current.get(obj.uuid); + if (!initialPos || !initialRot) return; + + const assetData: Asset = { + ...asset, + position: [initialPos.x, initialPos.y, initialPos.z], + rotation: [initialRot.x, initialRot.y, initialRot.z], + }; + + const newData: Asset = { + ...asset, + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }; + + assetsToUpdate.push({ + type: "Asset", + assetData, + newData, + timeStap: new Date().toISOString(), + }); + + updateAsset(asset.modelUuid, { + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }); + }); + + if (assetsToUpdate.length > 0) { + if (assetsToUpdate.length === 1) { + undoActions.push({ + module: "builder", + actionType: "Asset-Update", + asset: assetsToUpdate[0], + }); + } else { + undoActions.push({ + module: "builder", + actionType: "Assets-Update", + assets: assetsToUpdate, + }); + } + + push3D({ + type: "Scene", + actions: undoActions, + }); + } + + selectedAssets.forEach(obj => { + initialPositionsRef.current.set(obj.uuid, obj.position.clone()); + initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); + initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); + }); + + if (selectedAssets.length > 0) { + const bbox = new THREE.Box3(); + selectedAssets.forEach(obj => bbox.expandByObject(obj)); + bbox.getCenter(pivotPointRef.current); + initialPivotPositionRef.current.copy(pivotPointRef.current); + + tempObjectRef.current.position.copy(pivotPointRef.current); + tempObjectRef.current.rotation.set(0, 0, 0); + } + + recalcGizmo(); + handleTransformationComplete(); + }; + + controls.addEventListener('objectChange', onObjectChange); + controls.addEventListener('mouseUp', onMouseUp); + + return () => { + controls.removeEventListener('objectChange', onObjectChange); + controls.removeEventListener('mouseUp', onMouseUp); + controls.detach(); + }; + }, [selectedAssets, toolMode, visible, handleTransformationComplete]); + + useEffect(() => { + const canvasElement = gl.domElement; + + const handleKeyDown = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + + if (keyCombination !== keyEvent) { + if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } else { + setKeyEvent(""); + } + } + }; + + const onKeyUp = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + + if (keyCombination === "") { + setKeyEvent(""); + } else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } + } + + if (visible) { + canvasElement.addEventListener('keydown', handleKeyDown); + canvasElement.addEventListener('keyup', onKeyUp); + } + + return () => { + canvasElement.removeEventListener('keydown', handleKeyDown); + canvasElement.removeEventListener('keyup', onKeyUp); + } + }, [gl, visible, keyEvent]); + + if (!visible || (toolMode !== 'Move-Asset' && toolMode !== 'Rotate-Asset')) { + return null; + } + + return ( + + ); +} + +export default TransformControls3D; \ No newline at end of file diff --git a/app/src/modules/scene/controls/transformControls/transformControls.tsx b/app/src/modules/scene/controls/transformControls/transformControls.tsx index 415be9b..8428a63 100644 --- a/app/src/modules/scene/controls/transformControls/transformControls.tsx +++ b/app/src/modules/scene/controls/transformControls/transformControls.tsx @@ -1,17 +1,17 @@ -import { TransformControls } from "@react-three/drei"; import * as THREE from "three"; -import { useSelectedFloorItem, useObjectPosition, useObjectRotation, useActiveTool, useSocketStore } from "../../../../store/builder/store"; -import { useThree } from "@react-three/fiber"; - -import { useEffect, useRef, useState } from "react"; -import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; -import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi"; -// import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import { useParams } from "react-router-dom"; +import { useThree } from "@react-three/fiber"; +import { TransformControls } from "@react-three/drei"; +import { useEffect, useRef, useState } from "react"; import { useProductContext } from "../../../simulation/products/productContext"; import { getUserData } from "../../../../functions/getUserData"; import { useSceneContext } from "../../sceneContext"; import { useVersionContext } from "../../../builder/version/versionContext"; +import { useSelectedFloorItem, useObjectPosition, useObjectRotation, useActiveTool, useSocketStore } from "../../../../store/builder/store"; + +import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; +import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi"; +// import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; export default function TransformControl() { const state = useThree(); diff --git a/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx b/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx index 3ca5633..9331320 100644 --- a/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx +++ b/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx @@ -20,7 +20,7 @@ function UndoRedo3DControls() { const { selectedVersion } = selectedVersionStore(); useEffect(() => { - // console.log(undoStack, redoStack); + console.log(undoStack, redoStack); }, [undoStack, redoStack]); useEffect(() => { diff --git a/app/src/store/builder/useUndoRedo3DStore.ts b/app/src/store/builder/useUndoRedo3DStore.ts index e8c3ce3..d3e2b7b 100644 --- a/app/src/store/builder/useUndoRedo3DStore.ts +++ b/app/src/store/builder/useUndoRedo3DStore.ts @@ -13,6 +13,9 @@ type UndoRedo3DStore = { peekUndo3D: () => UndoRedo3DTypes | undefined; peekRedo3D: () => UndoRedo3DTypes | undefined; + + subscribeUndoRedo: (cb: (action: { type: "undo" | "redo"; entry: UndoRedo3DTypes }) => void) => () => void; + _listeners: ((action: { type: "undo" | "redo"; entry: UndoRedo3DTypes }) => void)[]; }; export const createUndoRedo3DStore = () => { @@ -20,15 +23,14 @@ export const createUndoRedo3DStore = () => { immer((set, get) => ({ undoStack: [], redoStack: [], + _listeners: [], push3D: (entry) => { set((state) => { state.undoStack.push(entry); - if (state.undoStack.length > undoRedoConfig.undoRedoCount) { state.undoStack.shift(); } - state.redoStack = []; }); }, @@ -41,6 +43,9 @@ export const createUndoRedo3DStore = () => { state.redoStack.unshift(lastAction); } }); + if (lastAction) { + get()._listeners.forEach((cb) => cb({ type: "undo", entry: lastAction! })); + } return lastAction; }, @@ -52,6 +57,9 @@ export const createUndoRedo3DStore = () => { state.undoStack.push(redoAction); } }); + if (redoAction) { + get()._listeners.forEach((cb) => cb({ type: "redo", entry: redoAction! })); + } return redoAction; }, @@ -71,8 +79,19 @@ export const createUndoRedo3DStore = () => { const stack = get().redoStack; return stack.length > 0 ? stack[0] : undefined; }, + + subscribeUndoRedo: (cb) => { + set((state) => { + state._listeners.push(cb); + }); + return () => { + set((state) => { + state._listeners = state._listeners.filter((l) => l !== cb); + }); + }; + }, })) ); -} +}; export type UndoRedo3DStoreType = ReturnType; \ No newline at end of file