added move and rotate tool using tranformControls

This commit is contained in:
2025-08-26 12:02:46 +05:30
parent ed099c7f75
commit b4b412ce14
11 changed files with 671 additions and 245 deletions

View File

@@ -32,9 +32,9 @@ const AssetProperties: React.FC = () => {
setUserData([]); 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) => { const handleAnimationClick = (animation: string) => {
if (selectedFloorItem) { if (selectedFloorItem) {
@@ -57,14 +57,14 @@ const AssetProperties: React.FC = () => {
<section> <section>
{objectPosition && ( {objectPosition && (
<PositionInput <PositionInput
onChange={() => {}} onChange={() => { }}
value1={parseFloat(objectPosition.x.toFixed(5))} value1={parseFloat(objectPosition.x.toFixed(5))}
value2={parseFloat(objectPosition.z.toFixed(5))} value2={parseFloat(objectPosition.z.toFixed(5))}
/> />
)} )}
{objectRotation && ( {objectRotation && (
<RotationInput <RotationInput
onChange={() => {}} onChange={() => { }}
value={parseFloat(objectRotation.y.toFixed(5))} value={parseFloat(objectRotation.y.toFixed(5))}
/> />
)} )}
@@ -107,7 +107,7 @@ const AssetProperties: React.FC = () => {
if (asset.modelUuid !== selectedFloorItem.uuid || !asset.animations) if (asset.modelUuid !== selectedFloorItem.uuid || !asset.animations)
return ( return (
i === 0 && ( i === 0 && (
<div className="no-animation"> <div className="no-animation" key={i}>
Looks like there are no preset animations yet. Stay tuned for Looks like there are no preset animations yet. Stay tuned for
future additions! future additions!
</div> </div>

View File

@@ -146,6 +146,12 @@ const Tools: React.FC = () => {
case "draw-floor": case "draw-floor":
is2D && setToolMode("Floor"); is2D && setToolMode("Floor");
break; break;
case "move":
if (!is2D) setToolMode("Move-Asset");
break;
case "rotate":
if (!is2D) setToolMode("Rotate-Asset");
break;
case "measure": case "measure":
setToolMode("MeasurementScale"); setToolMode("MeasurementScale");
break; break;

View File

@@ -34,7 +34,7 @@ export function useModelEventHandlers({
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { eventStore, productStore, assetStore, undoRedo3DStore } = useSceneContext(); const { eventStore, productStore, assetStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore(); const { push3D } = undoRedo3DStore();
const { getAssetById, removeAsset } = assetStore(); const { removeAsset } = assetStore();
const { zoneAssetId, setZoneAssetId } = useZoneAssetId(); const { zoneAssetId, setZoneAssetId } = useZoneAssetId();
const { resourceManagementId, setResourceManagementId } = useResourceManagementId(); const { resourceManagementId, setResourceManagementId } = useResourceManagementId();
const { removeEvent, getEventByModelUuid } = eventStore(); const { removeEvent, getEventByModelUuid } = eventStore();
@@ -77,11 +77,12 @@ export function useModelEventHandlers({
} }
}, [zoneAssetId]) }, [zoneAssetId])
useEffect(() => { useEffect(() => {
if (!resourceManagementId) return if (!resourceManagementId) return
if (resourceManagementId === asset.modelUuid) { if (resourceManagementId === asset.modelUuid) {
handleDblClick(asset); handleDblClick(asset);
} }

View File

@@ -1,41 +1,14 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useThree } from "@react-three/fiber"; import { useThree } from "@react-three/fiber";
import { CameraControls, Html, ScreenSpace } from "@react-three/drei"; import { CameraControls, Html, ScreenSpace } from "@react-three/drei";
import { import { useContextActionStore, useRenameModeStore, useSelectedAssets, useToggleView, useToolMode, } from "../../../../store/builder/store";
useContextActionStore,
useRenameModeStore,
useSelectedAssets,
} from "../../../../store/builder/store";
import ContextMenu from "../../../../components/ui/menu/contextMenu"; import ContextMenu from "../../../../components/ui/menu/contextMenu";
import useModuleStore from "../../../../store/useModuleStore";
function ContextControls() { function ContextControls() {
const { gl, controls } = useThree(); const { gl, controls } = useThree();
const [canRender, setCanRender] = useState(false); const [canRender, setCanRender] = useState(false);
const [visibility, setVisibility] = useState({ 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({
rename: true, rename: true,
focus: true, focus: true,
flipX: true, flipX: true,
@@ -49,195 +22,219 @@ function ContextControls() {
group: false, group: false,
array: false, array: false,
delete: true, delete: true,
}); });
} else if (selectedAssets.length > 1) { const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
setVisibility({ const { toggleView } = useToggleView();
rename: false, const { activeModule } = useModuleStore();
focus: true, const { toolMode } = useToolMode();
flipX: true, const { selectedAssets } = useSelectedAssets();
flipZ: true, const { setContextAction } = useContextActionStore();
move: true, const { setIsRenameMode } = useRenameModeStore();
rotate: true, const rightDrag = useRef(false);
duplicate: true, const isRightMouseDown = useRef(false);
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]);
useEffect(() => { useEffect(() => {
const canvasElement = gl.domElement; if (selectedAssets.length === 1) {
setVisibility({
const onPointerDown = (evt: any) => { rename: true,
if (evt.button === 2) { focus: true,
isRightMouseDown.current = true; flipX: true,
rightDrag.current = false; flipZ: true,
} move: true,
}; rotate: true,
duplicate: true,
const onPointerMove = () => { copy: true,
if (isRightMouseDown.current) { paste: true,
rightDrag.current = true; modifier: false,
} group: false,
}; array: false,
delete: true,
const onPointerUp = (evt: any) => { });
if (evt.button === 2) { } else if (selectedAssets.length > 1) {
isRightMouseDown.current = false; setVisibility({
} rename: false,
}; focus: true,
flipX: true,
const handleContextClick = (event: MouseEvent) => { flipZ: true,
event.preventDefault(); move: true,
if (rightDrag.current) return; rotate: true,
if (selectedAssets.length > 0) { duplicate: true,
setMenuPosition({ copy: true,
x: event.clientX - gl.domElement.width / 2, paste: true,
y: event.clientY - gl.domElement.height / 2, modifier: false,
}); group: true,
setCanRender(true); array: false,
if (controls) { delete: true,
(controls as CameraControls).enabled = false; });
} 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); setCanRender(false);
if (controls) { 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) { return (
canvasElement.addEventListener("pointerdown", onPointerDown); <>
canvasElement.addEventListener("pointermove", onPointerMove); {canRender && (
canvasElement.addEventListener("pointerup", onPointerUp); <ScreenSpace depth={1}>
canvasElement.addEventListener("contextmenu", handleContextClick); <Html
} else { style={{
setCanRender(false); position: "fixed",
if (controls) { top: menuPosition.y,
(controls as CameraControls).enabled = true; left: menuPosition.x,
} zIndex: 1000,
setMenuPosition({ x: 0, y: 0 }); }}
} >
<ContextMenu
return () => { visibility={visibility}
canvasElement.removeEventListener("pointerdown", onPointerDown); onRename={() => handleAssetRename()}
canvasElement.removeEventListener("pointermove", onPointerMove); onFocus={() => handleAssetFocus()}
canvasElement.removeEventListener("pointerup", onPointerUp); onFlipX={() => console.log("Flip to X")}
canvasElement.removeEventListener("contextmenu", handleContextClick); onFlipZ={() => console.log("Flip to Z")}
}; onMove={() => handleAssetMove()}
}, [controls, gl, selectedAssets]); onRotate={() => handleAssetRotate()}
onDuplicate={() => handleAssetDuplicate()}
const handleAssetRename = () => { onCopy={() => handleAssetCopy()}
setCanRender(false); onPaste={() => handleAssetPaste()}
if (controls) { onGroup={() => console.log("Group")}
(controls as CameraControls).enabled = true; onArray={() => console.log("Array")}
} onDelete={() => handleAssetDelete()}
setContextAction("renameAsset"); />
setIsRenameMode(true); </Html>
}; </ScreenSpace>
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 && (
<ScreenSpace depth={1}>
<Html
style={{
position: "fixed",
top: menuPosition.y,
left: menuPosition.x,
zIndex: 1000,
}}
>
<ContextMenu
visibility={visibility}
onRename={() => 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()}
/>
</Html>
</ScreenSpace>
)}
</>
);
} }
export default ContextControls; export default ContextControls;

View File

@@ -1,7 +1,7 @@
import * as THREE from "three"; import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber"; 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 * as Types from "../../../../../types/world/worldTypes";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
@@ -12,6 +12,7 @@ import { useProductContext } from "../../../../simulation/products/productContex
import { getUserData } from "../../../../../functions/getUserData"; import { getUserData } from "../../../../../functions/getUserData";
import { useSceneContext } from "../../../sceneContext"; import { useSceneContext } from "../../../sceneContext";
import { useVersionContext } from "../../../../builder/version/versionContext"; import { useVersionContext } from "../../../../builder/version/versionContext";
import useModuleStore from "../../../../../store/useModuleStore";
// import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; // 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 plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const { toolMode } = useToolMode();
const { activeModule } = useModuleStore();
const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const { selectedAssets, setSelectedAssets } = useSelectedAssets();
const { selectedProductStore } = useProductContext(); const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore(); const { selectedProduct } = selectedProductStore();
@@ -162,7 +165,7 @@ function MoveControls3D({ boundingBoxRef }: any) {
} }
}; };
if (!toggleView) { if (!toggleView && toolMode === 'cursor') {
canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointerdown", onPointerDown);
canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointermove", onPointerMove);
canvasElement.addEventListener("pointerup", onPointerUp); canvasElement.addEventListener("pointerup", onPointerUp);
@@ -178,7 +181,14 @@ function MoveControls3D({ boundingBoxRef }: any) {
canvasElement?.removeEventListener("keyup", onKeyUp); canvasElement?.removeEventListener("keyup", onKeyUp);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => {
const pointPosition = new THREE.Vector3().copy(point.position); const pointPosition = new THREE.Vector3().copy(point.position);

View File

@@ -1,7 +1,7 @@
import * as THREE from "three"; import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber"; 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 * as Types from "../../../../../types/world/worldTypes";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
@@ -11,6 +11,7 @@ import { useSceneContext } from "../../../sceneContext";
import { useVersionContext } from "../../../../builder/version/versionContext"; import { useVersionContext } from "../../../../builder/version/versionContext";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { handleAssetRotationSnap } from "./functions/handleAssetRotationSnap"; import { handleAssetRotationSnap } from "./functions/handleAssetRotationSnap";
import useModuleStore from "../../../../../store/useModuleStore";
// import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; // import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi';
@@ -18,6 +19,8 @@ function RotateControls3D() {
const { camera, gl, scene, pointer, raycaster } = useThree(); const { camera, gl, scene, pointer, raycaster } = useThree();
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const { toolMode } = useToolMode();
const { activeModule } = useModuleStore();
const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const { selectedAssets, setSelectedAssets } = useSelectedAssets();
const { selectedProductStore } = useProductContext(); const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore(); const { selectedProduct } = selectedProductStore();
@@ -137,7 +140,7 @@ function RotateControls3D() {
prevPointerPosition.current = currentPointer.clone(); prevPointerPosition.current = currentPointer.clone();
}; };
if (!toggleView) { if (!toggleView && toolMode === 'cursor') {
canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointerdown", onPointerDown);
canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointermove", onPointerMove);
canvasElement.addEventListener("pointerup", onPointerUp); canvasElement.addEventListener("pointerup", onPointerUp);
@@ -152,7 +155,14 @@ function RotateControls3D() {
canvasElement.removeEventListener("keydown", onKeyDown); canvasElement.removeEventListener("keydown", onKeyDown);
canvasElement?.removeEventListener("keyup", onKeyUp); 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(() => { const resetToInitialRotations = useCallback(() => {
setTimeout(() => { setTimeout(() => {
@@ -385,7 +395,7 @@ function RotateControls3D() {
setIsRotating(false); setIsRotating(false);
clearSelection(); 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 = () => { const clearSelection = () => {
setPastedObjects([]); setPastedObjects([]);

View File

@@ -16,22 +16,23 @@ import DuplicationControls3D from "./duplicationControls3D";
import CopyPasteControls3D from "./copyPasteControls3D"; import CopyPasteControls3D from "./copyPasteControls3D";
import MoveControls3D from "./moveControls3D"; import MoveControls3D from "./moveControls3D";
import RotateControls3D from "./rotateControls3D"; import RotateControls3D from "./rotateControls3D";
import TransformControls3D from "./transformControls3D";
// import { deleteFloorItem } from '../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorItemApi'; // import { deleteFloorItem } from '../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorItemApi';
const SelectionControls3D: React.FC = () => { const SelectionControls3D: React.FC = () => {
const { camera, controls, gl, scene, raycaster, pointer } = useThree(); const { camera, controls, gl, scene, raycaster, pointer } = useThree();
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const { activeModule } = useModuleStore();
const { toolMode } = useToolMode();
const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const { selectedAssets, setSelectedAssets } = useSelectedAssets();
const boundingBoxRef = useRef<THREE.Mesh>(); const boundingBoxRef = useRef<THREE.Mesh>();
const { activeModule } = useModuleStore();
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { contextAction, setContextAction } = useContextActionStore() const { contextAction, setContextAction } = useContextActionStore()
const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext(); const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore(); const { push3D } = undoRedo3DStore();
const { removeAsset, getAssetById, movedObjects, rotatedObjects, copiedObjects, pastedObjects, duplicatedObjects, setPastedObjects, setDuplicatedObjects } = assetStore(); const { removeAsset, getAssetById, movedObjects, rotatedObjects, copiedObjects, pastedObjects, duplicatedObjects, setPastedObjects, setDuplicatedObjects } = assetStore();
const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]); const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]);
const { toolMode } = useToolMode();
const { selectedVersionStore } = useVersionContext(); const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore(); const { selectedVersion } = selectedVersionStore();
const { selectedProductStore } = useProductContext(); const { selectedProductStore } = useProductContext();
@@ -202,7 +203,7 @@ const SelectionControls3D: React.FC = () => {
rightClickMoved.current = false; rightClickMoved.current = false;
}; };
if (!toggleView && activeModule === "builder" && toolMode === 'cursor') { if (!toggleView && activeModule === "builder" && (toolMode === 'cursor' || toolMode === 'Move-Asset' || toolMode === 'Rotate-Asset')) {
helper.enabled = true; helper.enabled = true;
canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointermove", onPointerMove);
canvasElement.addEventListener("pointerup", onPointerUp); 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]); }, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, rotatedObjects, activeModule, toolMode]);
useEffect(() => { useEffect(() => {
if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) { if (activeModule !== "builder" || (toolMode !== 'cursor' && toolMode !== 'Move-Asset' && toolMode !== 'Rotate-Asset') || toggleView) {
clearSelection(); clearSelection();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -381,6 +382,8 @@ const SelectionControls3D: React.FC = () => {
<CopyPasteControls3D /> <CopyPasteControls3D />
<TransformControls3D />
</> </>
); );
}; };

View File

@@ -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<any>(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<THREE.Vector3>(new THREE.Vector3());
const initialPositionsRef = useRef<Map<string, THREE.Vector3>>(new Map());
const initialRotationsRef = useRef<Map<string, THREE.Euler>>(new Map());
const initialPivotPositionRef = useRef<THREE.Vector3>(new THREE.Vector3());
const initialQuaternionsRef = useRef<Map<string, THREE.Quaternion>>(new Map());
const tempObjectRef = useRef<THREE.Object3D>(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 (
<TransformControls
space="world"
translationSnap={keyEvent === 'Ctrl' ? 0.5 : keyEvent === 'Ctrl+Shift' ? 0.1 : 0}
rotationSnap={keyEvent === 'Ctrl' ? THREE.MathUtils.degToRad(15) : keyEvent === 'Ctrl+Shift' ? THREE.MathUtils.degToRad(5) : 0}
ref={transformControlsRef}
camera={camera}
showX
showY
showZ
/>
);
}
export default TransformControls3D;

View File

@@ -1,17 +1,17 @@
import { TransformControls } from "@react-three/drei";
import * as THREE from "three"; 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 { 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 { useProductContext } from "../../../simulation/products/productContext";
import { getUserData } from "../../../../functions/getUserData"; import { getUserData } from "../../../../functions/getUserData";
import { useSceneContext } from "../../sceneContext"; import { useSceneContext } from "../../sceneContext";
import { useVersionContext } from "../../../builder/version/versionContext"; 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() { export default function TransformControl() {
const state = useThree(); const state = useThree();

View File

@@ -20,7 +20,7 @@ function UndoRedo3DControls() {
const { selectedVersion } = selectedVersionStore(); const { selectedVersion } = selectedVersionStore();
useEffect(() => { useEffect(() => {
// console.log(undoStack, redoStack); console.log(undoStack, redoStack);
}, [undoStack, redoStack]); }, [undoStack, redoStack]);
useEffect(() => { useEffect(() => {

View File

@@ -13,6 +13,9 @@ type UndoRedo3DStore = {
peekUndo3D: () => UndoRedo3DTypes | undefined; peekUndo3D: () => UndoRedo3DTypes | undefined;
peekRedo3D: () => 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 = () => { export const createUndoRedo3DStore = () => {
@@ -20,15 +23,14 @@ export const createUndoRedo3DStore = () => {
immer((set, get) => ({ immer((set, get) => ({
undoStack: [], undoStack: [],
redoStack: [], redoStack: [],
_listeners: [],
push3D: (entry) => { push3D: (entry) => {
set((state) => { set((state) => {
state.undoStack.push(entry); state.undoStack.push(entry);
if (state.undoStack.length > undoRedoConfig.undoRedoCount) { if (state.undoStack.length > undoRedoConfig.undoRedoCount) {
state.undoStack.shift(); state.undoStack.shift();
} }
state.redoStack = []; state.redoStack = [];
}); });
}, },
@@ -41,6 +43,9 @@ export const createUndoRedo3DStore = () => {
state.redoStack.unshift(lastAction); state.redoStack.unshift(lastAction);
} }
}); });
if (lastAction) {
get()._listeners.forEach((cb) => cb({ type: "undo", entry: lastAction! }));
}
return lastAction; return lastAction;
}, },
@@ -52,6 +57,9 @@ export const createUndoRedo3DStore = () => {
state.undoStack.push(redoAction); state.undoStack.push(redoAction);
} }
}); });
if (redoAction) {
get()._listeners.forEach((cb) => cb({ type: "redo", entry: redoAction! }));
}
return redoAction; return redoAction;
}, },
@@ -71,8 +79,19 @@ export const createUndoRedo3DStore = () => {
const stack = get().redoStack; const stack = get().redoStack;
return stack.length > 0 ? stack[0] : undefined; 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<typeof createUndoRedo3DStore>; export type UndoRedo3DStoreType = ReturnType<typeof createUndoRedo3DStore>;