From 08fb86d5ed1c2fa9068093e6d99abc7ed91478bf Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 6 Oct 2025 18:03:00 +0530 Subject: [PATCH] added focus and added asset toggles in asset properties --- .../properties/AssetProperties.tsx | 273 +++++++++++++++++- .../eventHandlers/useModelEventHandlers.ts | 45 +-- .../modules/builder/asset/models/models.tsx | 31 +- app/src/modules/builder/builder.tsx | 9 +- app/src/modules/builder/hooks/useZoomMesh.tsx | 79 +++++ app/src/modules/builder/testUi/outline.tsx | 130 +++++++-- .../assetControls/cutCopyPasteControls3D.tsx | 16 +- .../assetControls/duplicationControls3D.tsx | 16 +- .../controls/assetControls/groupControls.tsx | 14 +- .../assetControls/scaleControls3D.tsx | 1 + .../selection3D/selectionControls3D.tsx | 16 +- app/src/modules/scene/sceneContext.tsx | 13 +- .../group/assetGroup/createAssetGroupApi.ts | 53 ++++ .../group/assetGroup/createChildrenApi.ts | 42 +++ .../group/assetGroup/deleteAssetGroup.ts | 0 .../group/assetGroup/deleteChildrenApi.ts | 0 .../group/assetGroup/getAssetGroupApi.ts | 29 ++ .../group/assetGroup/getAssetGroupsApi.ts | 29 ++ .../group/assetGroup/updateAssetGroupApi.ts | 44 +++ app/src/store/builder/useAssetStore.ts | 17 +- 20 files changed, 761 insertions(+), 96 deletions(-) create mode 100644 app/src/modules/builder/hooks/useZoomMesh.tsx create mode 100644 app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts create mode 100644 app/src/services/factoryBuilder/group/assetGroup/createChildrenApi.ts create mode 100644 app/src/services/factoryBuilder/group/assetGroup/deleteAssetGroup.ts create mode 100644 app/src/services/factoryBuilder/group/assetGroup/deleteChildrenApi.ts create mode 100644 app/src/services/factoryBuilder/group/assetGroup/getAssetGroupApi.ts create mode 100644 app/src/services/factoryBuilder/group/assetGroup/getAssetGroupsApi.ts create mode 100644 app/src/services/factoryBuilder/group/assetGroup/updateAssetGroupApi.ts diff --git a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx index f15aeab..6e1f45f 100644 --- a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx @@ -1,4 +1,5 @@ -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; +import { useParams } from "react-router-dom"; import InputToggle from "../../../ui/inputs/InputToggle"; import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; import { RemoveIcon } from "../../../icons/ExportCommonIcons"; @@ -6,6 +7,12 @@ import PositionInput from "../customInput/PositionInputs"; import RotationInput from "../customInput/RotationInput"; import { useSceneContext } from "../../../../modules/scene/sceneContext"; import { useBuilderStore } from "../../../../store/builder/useBuilderStore"; +import { useContextActionStore } from "../../../../store/builder/store"; +import { useSocketStore } from "../../../../store/socket/useSocketStore"; +import useAssetResponseHandler from "../../../../modules/collaboration/responseHandler/useAssetResponseHandler"; + +import { getUserData } from "../../../../functions/getUserData"; +import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; interface UserData { id: number; @@ -15,10 +22,17 @@ interface UserData { const AssetProperties: React.FC = () => { const [userData, setUserData] = useState([]); - const { assetStore } = useSceneContext(); - const { assets, setCurrentAnimation, selectedAssets, getAssetById } = assetStore(); + const { assetStore, undoRedo3DStore, versionStore } = useSceneContext(); + const { assets, setCurrentAnimation, selectedAssets, getAssetById, peekToggleVisibility, peekToggleLock } = assetStore(); const { loopAnimation } = useBuilderStore(); const [hoveredIndex, setHoveredIndex] = useState(null); + const { setContextAction } = useContextActionStore(); + const { projectId } = useParams(); + const { push3D } = undoRedo3DStore(); + const { builderSocket } = useSocketStore(); + const { userId, organization } = getUserData(); + const { selectedVersion } = versionStore(); + const { updateAssetInScene } = useAssetResponseHandler(); const asset = getAssetById(selectedAssets[0]?.uuid); @@ -36,6 +50,228 @@ const AssetProperties: React.FC = () => { } }; + const handleAssetVisibilityUpdate = async (asset: Asset | undefined) => { + if (!asset) return; + + if (!builderSocket?.connected) { + setAssetsApi({ + modelUuid: asset.modelUuid, + modelName: asset.modelName, + assetId: asset.assetId, + position: asset.position, + rotation: asset.rotation, + scale: asset.scale, + isCollidable: asset.isCollidable, + opacity: asset.opacity, + isLocked: asset.isLocked, + isVisible: asset.isVisible, + versionId: selectedVersion?.versionId || "", + projectId: projectId || "", + }) + .then((data) => { + if (!data.message || !data.data) { + echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); + return; + } + if (data.message === "Model updated successfully" && data.data) { + const model: Asset = { + modelUuid: data.data.modelUuid, + modelName: data.data.modelName, + assetId: data.data.assetId, + position: data.data.position, + rotation: data.data.rotation, + scale: data.data.scale, + isLocked: data.data.isLocked, + isVisible: data.data.isVisible, + isCollidable: data.data.isCollidable, + opacity: data.data.opacity, + ...(data.data.eventData ? { eventData: data.data.eventData } : {}), + }; + + updateAssetInScene(model, () => { + echo.info(`${asset.isVisible ? "Hid" : "Unhid"} asset: ${model.modelName}`); + }); + } else { + echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); + } + }) + .catch(() => { + echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); + }); + } else { + const data = { + organization, + modelUuid: asset.modelUuid, + modelName: asset.modelName, + assetId: asset.assetId, + position: asset.position, + rotation: asset.rotation, + scale: asset.scale, + isCollidable: asset.isCollidable, + opacity: asset.opacity, + isLocked: asset.isLocked, + isVisible: asset.isVisible, + socketId: builderSocket?.id, + versionId: selectedVersion?.versionId || "", + projectId, + userId, + }; + + builderSocket.emit("v1:model-asset:add", data); + } + }; + + const handleAssetLockUpdate = async (asset: Asset | null) => { + if (!asset) return; + + if (!builderSocket?.connected) { + setAssetsApi({ + modelUuid: asset.modelUuid, + modelName: asset.modelName, + assetId: asset.assetId, + position: asset.position, + rotation: asset.rotation, + scale: asset.scale, + isCollidable: asset.isCollidable, + opacity: asset.opacity, + isLocked: asset.isLocked, + isVisible: asset.isVisible, + versionId: selectedVersion?.versionId || "", + projectId: projectId || "", + }) + .then((data) => { + if (!data.message || !data.data) { + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); + return; + } + if (data.message === "Model updated successfully" && data.data) { + const model: Asset = { + modelUuid: data.data.modelUuid, + modelName: data.data.modelName, + assetId: data.data.assetId, + position: data.data.position, + rotation: data.data.rotation, + scale: data.data.scale, + isLocked: data.data.isLocked, + isVisible: data.data.isVisible, + isCollidable: data.data.isCollidable, + opacity: data.data.opacity, + ...(data.data.eventData ? { eventData: data.data.eventData } : {}), + }; + + updateAssetInScene(model, () => { + echo.info(`${asset.isVisible ? "Locked" : "Unlocked"} asset: ${model.modelName}`); + }); + } else { + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); + } + }) + .catch(() => { + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); + }); + } else { + const data = { + organization, + modelUuid: asset.modelUuid, + modelName: asset.modelName, + assetId: asset.assetId, + position: asset.position, + rotation: asset.rotation, + scale: asset.scale, + isCollidable: asset.isCollidable, + opacity: asset.opacity, + isLocked: asset.isLocked, + isVisible: asset.isVisible, + socketId: builderSocket?.id, + versionId: selectedVersion?.versionId || "", + projectId, + userId, + }; + + builderSocket.emit("v1:model-asset:add", data); + } + }; + + const handleOptionClick = useCallback( + (option: string) => { + if (!asset) return; + if (option === "visibility") { + const undoActions: UndoRedo3DAction[] = []; + const assetsToUpdate: AssetData[] = []; + const assetUuid = asset.modelUuid; + + const updatedAsset = peekToggleVisibility(assetUuid); + if (!updatedAsset) return; + + assetsToUpdate.push({ + type: "Asset", + assetData: { + ...asset, + isVisible: asset.isVisible, + }, + newData: { + ...asset, + isVisible: updatedAsset.isVisible, + }, + timeStap: new Date().toISOString(), + }); + + handleAssetVisibilityUpdate(updatedAsset); + + 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, + }); + } + } else if (option === "lock") { + const undoActions: UndoRedo3DAction[] = []; + const assetsToUpdate: AssetData[] = []; + const assetUuid = asset.modelUuid; + + const updatedAsset = peekToggleLock(assetUuid); + if (!updatedAsset) return; + + assetsToUpdate.push({ + type: "Asset", + assetData: { ...asset, isLocked: asset.isLocked }, + newData: { ...asset, isLocked: updatedAsset.isLocked }, + timeStap: new Date().toISOString(), + }); + + handleAssetLockUpdate(updatedAsset); + + 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, + }); + } + } + }, + [selectedVersion, builderSocket, projectId, userId, organization, asset] + ); + if (selectedAssets.length !== 1 || !asset) return null; return ( @@ -45,12 +281,39 @@ const AssetProperties: React.FC = () => {
{}} value1={parseFloat(asset.position[0]?.toFixed(5))} value2={parseFloat(asset.position[2]?.toFixed(5))} /> {}} value={parseFloat(asset.rotation[1]?.toFixed(5))} /> +
+
Flip 90°
+ +
Render settings
- - + { + handleOptionClick("visibility"); + }} + value={asset.isVisible} + /> + { + handleOptionClick("lock"); + }} + value={asset.isLocked} + />
diff --git a/app/src/modules/builder/asset/models/model/eventHandlers/useModelEventHandlers.ts b/app/src/modules/builder/asset/models/model/eventHandlers/useModelEventHandlers.ts index 861b185..86f5367 100644 --- a/app/src/modules/builder/asset/models/model/eventHandlers/useModelEventHandlers.ts +++ b/app/src/modules/builder/asset/models/model/eventHandlers/useModelEventHandlers.ts @@ -1,5 +1,4 @@ import * as THREE from "three"; -import { CameraControls } from "@react-three/drei"; import { ThreeEvent, useThree } from "@react-three/fiber"; import { useCallback, useEffect, useRef } from "react"; @@ -13,12 +12,13 @@ import { useLeftData, useTopData } from "../../../../../../store/visualization/u import { useSelectedAction, useSelectedAsset, useSelectedEventData } from "../../../../../../store/simulation/useSimulationStore"; import { useBuilderStore } from "../../../../../../store/builder/useBuilderStore"; import useAssetResponseHandler from "../../../../../collaboration/responseHandler/useAssetResponseHandler"; +import useZoomMesh from "../../../../hooks/useZoomMesh"; import { updateEventToBackend } from "../../../../../../components/layout/sidebarRight/properties/eventProperties/functions/handleUpdateEventToBackend"; import { deleteFloorAssetApi } from "../../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorAssetApi"; export function useModelEventHandlers({ boundingBox, groupRef, asset }: { boundingBox: THREE.Box3 | null; groupRef: React.RefObject; asset: Asset }) { - const { controls, gl, camera } = useThree(); + const { gl } = useThree(); const { activeTool } = useActiveTool(); const { toolMode } = useToolMode(); const { activeModule } = useModuleStore(); @@ -37,6 +37,7 @@ export function useModelEventHandlers({ boundingBox, groupRef, asset }: { boundi const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset(); const { deletableFloorAsset, setDeletableFloorAsset } = useBuilderStore(); const { removeAssetFromScene } = useAssetResponseHandler(); + const { zoomMeshes } = useZoomMesh(); const { selectedVersion } = versionStore(); const { projectId } = useParams(); const { userId, organization } = getUserData(); @@ -96,45 +97,7 @@ export function useModelEventHandlers({ boundingBox, groupRef, asset }: { boundi groupRef.current && (activeModule === "builder" || (activeModule === "simulation" && resourceManagementId)) ) { - const frontView = false; - if (frontView) { - const size = boundingBox.getSize(new THREE.Vector3()); - const center = boundingBox.getCenter(new THREE.Vector3()); - - const front = new THREE.Vector3(0, 0, 1); - groupRef.current.localToWorld(front); - front.sub(groupRef.current.position).normalize(); - - const distance = Math.max(size.x, size.y, size.z) * 2; - const newPosition = center.clone().addScaledVector(front, distance); - - (controls as CameraControls).setPosition(newPosition.x, newPosition.y, newPosition.z, true); - (controls as CameraControls).setTarget(center.x, center.y, center.z, true); - (controls as CameraControls).fitToBox(groupRef.current, true, { - cover: true, - paddingTop: 5, - paddingLeft: 5, - paddingBottom: 5, - paddingRight: 5, - }); - } else { - const collisionPos = new THREE.Vector3(); - groupRef.current.getWorldPosition(collisionPos); - const size = boundingBox.getSize(new THREE.Vector3()); - - const currentPos = new THREE.Vector3().copy(camera.position); - - const target = new THREE.Vector3(); - if (!controls) return; - (controls as CameraControls).getTarget(target); - const direction = new THREE.Vector3().subVectors(target, currentPos).normalize(); - - const offsetDistance = 5; - const newCameraPos = new THREE.Vector3().copy(collisionPos).sub(direction.multiplyScalar(offsetDistance)); - - camera.position.copy(newCameraPos); - (controls as CameraControls).setLookAt(newCameraPos.x, size.y > newCameraPos.y ? size.y : newCameraPos.y, newCameraPos.z, collisionPos.x, 0, collisionPos.z, true); - } + zoomMeshes([asset.modelUuid]); if (groupRef.current) { toggleSelectedAsset(groupRef.current); diff --git a/app/src/modules/builder/asset/models/models.tsx b/app/src/modules/builder/asset/models/models.tsx index 79b4ae3..588a617 100644 --- a/app/src/modules/builder/asset/models/models.tsx +++ b/app/src/modules/builder/asset/models/models.tsx @@ -1,13 +1,15 @@ import { useEffect, useRef, useState } from "react"; -import { useThree, useFrame } from "@react-three/fiber"; import { Group, Vector3 } from "three"; import { CameraControls } from "@react-three/drei"; -import { useLimitDistance, useRenderDistance } from "../../../../store/builder/store"; +import { GLTFLoader } from "three/examples/jsm/Addons"; +import { useThree, useFrame } from "@react-three/fiber"; +import { useContextActionStore, useLimitDistance, useRenderDistance } from "../../../../store/builder/store"; import { useSelectedAsset } from "../../../../store/simulation/useSimulationStore"; import { useSceneContext } from "../../../scene/sceneContext"; +import useZoomMesh from "../../hooks/useZoomMesh"; +import useCallBackOnKey from "../../../../utils/hooks/useCallBackOnKey"; import Model from "./model/model"; -import { GLTFLoader } from "three/examples/jsm/Addons"; const distanceWorker = new Worker(new URL("../../../../services/factoryBuilder/webWorkers/distanceWorker.js", import.meta.url)); @@ -15,17 +17,36 @@ function Models({ loader }: { readonly loader: GLTFLoader }) { const { controls, camera } = useThree(); const assetGroupRef = useRef(null); const { assetStore } = useSceneContext(); - const { assets, selectedAssets } = assetStore(); + const { assets, selectedAssets, getSelectedAssetUuids } = assetStore(); const { selectedAsset, clearSelectedAsset } = useSelectedAsset(); + const { contextAction, setContextAction } = useContextActionStore(); const { limitDistance } = useLimitDistance(); const { renderDistance } = useRenderDistance(); const [renderMap, setRenderMap] = useState>({}); + const { zoomMeshes } = useZoomMesh(); const cameraPos = useRef(new Vector3()); useEffect(() => { // console.log(assets); }, [assets]); + useEffect(() => { + if (contextAction === "focusAsset") { + zoomMeshes(getSelectedAssetUuids()); + setContextAction(null); + } + }, [contextAction]); + + useCallBackOnKey( + () => { + if (selectedAssets.length > 0) { + zoomMeshes(getSelectedAssetUuids()); + } + }, + ".", + { dependencies: [selectedAssets.length], noRepeat: true } + ); + useEffect(() => { distanceWorker.onmessage = (e) => { const { shouldRender, modelUuid } = e.data; @@ -57,7 +78,7 @@ function Models({ loader }: { readonly loader: GLTFLoader }) { ref={assetGroupRef} onPointerMissed={(e) => { e.stopPropagation(); - if (selectedAssets.length === 1) { + if (selectedAssets.length > 0) { const target = (controls as CameraControls).getTarget(new Vector3()); (controls as CameraControls).setTarget(target.x, 0, target.z, true); } diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index 5e9c941..a5e6a64 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -4,6 +4,7 @@ import * as THREE from "three"; import { useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import { Geometry } from "@react-three/csg"; +import { CameraControls } from "@react-three/drei"; import { useFrame, useThree } from "@react-three/fiber"; ////////// Zustand State Imports ////////// @@ -30,7 +31,7 @@ import Decal from "./Decal/decal"; import { findEnvironment } from "../../services/factoryBuilder/environment/findEnvironment"; export default function Builder() { - const { gl, scene } = useThree(); + const { gl, scene, camera, controls } = useThree(); const plane = useRef(null); const csgRef = useRef(null); @@ -42,12 +43,14 @@ export default function Builder() { const { setRenderDistance } = useRenderDistance(); const { setLimitDistance } = useLimitDistance(); const { projectId } = useParams(); - const { scene: storeScene } = useSceneContext(); + const { scene: storeScene, camera: storeCamera, controls: storeControls } = useSceneContext(); const { setHoveredPoint, setHoveredLine } = useBuilderStore(); useEffect(() => { storeScene.current = scene; - }, [scene]); + storeCamera.current = camera; + storeControls.current = controls as CameraControls; + }, [scene, camera, controls]); useEffect(() => { if (!toggleView) { diff --git a/app/src/modules/builder/hooks/useZoomMesh.tsx b/app/src/modules/builder/hooks/useZoomMesh.tsx new file mode 100644 index 0000000..f59819e --- /dev/null +++ b/app/src/modules/builder/hooks/useZoomMesh.tsx @@ -0,0 +1,79 @@ +import { Box3, Object3D, Vector3, PerspectiveCamera } from "three"; +import { useCallback } from "react"; +import { useSceneContext } from "../../scene/sceneContext"; + +export function useZoomMesh() { + const { scene, camera, controls } = useSceneContext(); + + const zoomMeshes = useCallback( + (meshUuids: string[]) => { + if (!scene.current || !camera.current || !controls.current) return; + if (meshUuids.length === 0) return; + + const models: Object3D[] = meshUuids.map((uuid) => scene.current!.getObjectByProperty("uuid", uuid)).filter((m): m is Object3D => m !== undefined); + + if (models.length === 0) return; + + const box = new Box3(); + models.forEach((model) => { + box.expandByObject(model); + }); + + const cam = camera.current; + if (!(cam instanceof PerspectiveCamera)) { + console.warn("Only PerspectiveCamera is supported for directional zoom."); + return; + } + + const camPos = new Vector3(); + cam.getWorldPosition(camPos); + const camDir = new Vector3(0, 0, -1).transformDirection(cam.matrixWorld); + const boxCenter = box.getCenter(new Vector3()); + const fov = (cam.fov * Math.PI) / 180; + const aspect = cam.aspect; + const halfFovY = fov / 2; + const halfFovX = Math.atan(Math.tan(halfFovY) * aspect); + const camRight = new Vector3(1, 0, 0).transformDirection(cam.matrixWorld); + const camUp = new Vector3(0, 1, 0).transformDirection(cam.matrixWorld); + + const corners = []; + for (let i = 0; i < 8; i++) { + const corner = new Vector3(); + corner.x = i & 1 ? box.max.x : box.min.x; + corner.y = i & 2 ? box.max.y : box.min.y; + corner.z = i & 4 ? box.max.z : box.min.z; + corners.push(corner); + } + + let maxRight = 0; + let maxUp = 0; + for (const corner of corners) { + const toCorner = corner.clone().sub(boxCenter); + const projRight = Math.abs(toCorner.dot(camRight)); + const projUp = Math.abs(toCorner.dot(camUp)); + maxRight = Math.max(maxRight, projRight); + maxUp = Math.max(maxUp, projUp); + } + + const distX = maxRight / Math.sin(halfFovX); + const distY = maxUp / Math.sin(halfFovY); + const requiredDist = Math.max(distX, distY); + const toBox = boxCenter.clone().sub(camPos); + const currentDistAlongView = toBox.dot(camDir); + + if (currentDistAlongView <= 0) { + const newPos = boxCenter.clone().add(camDir.clone().multiplyScalar(requiredDist)); + controls.current.setLookAt(newPos.x, newPos.y, newPos.z, boxCenter.x, boxCenter.y, boxCenter.z, true); + return; + } + + const targetCamPos = boxCenter.clone().sub(camDir.clone().multiplyScalar(requiredDist)); + controls.current.setLookAt(targetCamPos.x, targetCamPos.y, targetCamPos.z, boxCenter.x, boxCenter.y, boxCenter.z, true); + }, + [scene, camera, controls] + ); + + return { zoomMeshes }; +} + +export default useZoomMesh; diff --git a/app/src/modules/builder/testUi/outline.tsx b/app/src/modules/builder/testUi/outline.tsx index eb8b0fc..b59e7cf 100644 --- a/app/src/modules/builder/testUi/outline.tsx +++ b/app/src/modules/builder/testUi/outline.tsx @@ -46,6 +46,7 @@ const TreeNode = ({ const { isGroup, getGroupsContainingAsset, getGroupsContainingGroup } = assetGroupStore(); const isGroupNode = isGroup(item); + const itemId = isGroupNode ? item.groupUuid : item.modelUuid; const itemName = isGroupNode ? item.groupName : item.modelName; const isVisible = item.isVisible; const isLocked = item.isLocked; @@ -110,7 +111,7 @@ const TreeNode = ({ const shouldShowHighlight = isDropTarget(); return ( -
+
{ - const [isOpen, setIsOpen] = useState(true); + const [isOpen, setIsOpen] = useState(false); const lastSelectedRef = useRef<{ item: AssetGroupChild; index: number } | null>(null); const dragStateRef = useRef({ draggedItem: null, @@ -200,7 +201,7 @@ export const Outline = () => { }); const [_, forceUpdate] = useState({}); const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext(); - const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, toggleSelectedAsset, selectedAssets } = assetStore(); + const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, peekToggleLock, toggleSelectedAsset, selectedAssets } = assetStore(); const { groupHierarchy, isGroup, getGroupsContainingAsset, getFlatGroupChildren, setGroupExpanded, addChildToGroup, removeChildFromGroup, getGroupsContainingGroup } = assetGroupStore(); const { projectId } = useParams(); const { push3D } = undoRedo3DStore(); @@ -296,6 +297,77 @@ export const Outline = () => { } }; + const handleAssetLockUpdate = async (asset: Asset | null) => { + if (!asset) return; + + if (!builderSocket?.connected) { + setAssetsApi({ + modelUuid: asset.modelUuid, + modelName: asset.modelName, + assetId: asset.assetId, + position: asset.position, + rotation: asset.rotation, + scale: asset.scale, + isCollidable: asset.isCollidable, + opacity: asset.opacity, + isLocked: asset.isLocked, + isVisible: asset.isVisible, + versionId: selectedVersion?.versionId || "", + projectId: projectId || "", + }) + .then((data) => { + if (!data.message || !data.data) { + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); + return; + } + if (data.message === "Model updated successfully" && data.data) { + const model: Asset = { + modelUuid: data.data.modelUuid, + modelName: data.data.modelName, + assetId: data.data.assetId, + position: data.data.position, + rotation: data.data.rotation, + scale: data.data.scale, + isLocked: data.data.isLocked, + isVisible: data.data.isVisible, + isCollidable: data.data.isCollidable, + opacity: data.data.opacity, + ...(data.data.eventData ? { eventData: data.data.eventData } : {}), + }; + + updateAssetInScene(model, () => { + echo.info(`${asset.isVisible ? "Locked" : "Unlocked"} asset: ${model.modelName}`); + }); + } else { + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); + } + }) + .catch(() => { + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); + }); + } else { + const data = { + organization, + modelUuid: asset.modelUuid, + modelName: asset.modelName, + assetId: asset.assetId, + position: asset.position, + rotation: asset.rotation, + scale: asset.scale, + isCollidable: asset.isCollidable, + opacity: asset.opacity, + isLocked: asset.isLocked, + isVisible: asset.isVisible, + socketId: builderSocket?.id, + versionId: selectedVersion?.versionId || "", + projectId, + userId, + }; + + builderSocket.emit("v1:model-asset:add", data); + } + }; + const handleToggleExpand = useCallback( (groupUuid: string, newExpanded: boolean) => { setGroupExpanded(groupUuid, newExpanded); @@ -577,14 +649,8 @@ export const Outline = () => { assetsToUpdate.push({ type: "Asset", - assetData: { - ...asset, - isVisible: asset.isVisible, - }, - newData: { - ...asset, - isVisible: updatedAsset.isVisible, - }, + assetData: { ...asset, isVisible: asset.isVisible }, + newData: { ...asset, isVisible: updatedAsset.isVisible }, timeStap: new Date().toISOString(), }); @@ -592,17 +658,9 @@ export const Outline = () => { if (assetsToUpdate.length > 0) { if (assetsToUpdate.length === 1) { - undoActions.push({ - module: "builder", - actionType: "Asset-Update", - asset: assetsToUpdate[0], - }); + undoActions.push({ module: "builder", actionType: "Asset-Update", asset: assetsToUpdate[0] }); } else { - undoActions.push({ - module: "builder", - actionType: "Assets-Update", - assets: assetsToUpdate, - }); + undoActions.push({ module: "builder", actionType: "Assets-Update", assets: assetsToUpdate }); } push3D({ @@ -614,6 +672,36 @@ export const Outline = () => { } else if (option === "lock") { if (isGroup(item)) { } else { + const undoActions: UndoRedo3DAction[] = []; + const assetsToUpdate: AssetData[] = []; + const assetUuid = item.modelUuid; + const asset = getAssetById(assetUuid); + if (!asset) return; + + const updatedAsset = peekToggleLock(assetUuid); + if (!updatedAsset) return; + + assetsToUpdate.push({ + type: "Asset", + assetData: { ...asset, isLocked: asset.isLocked }, + newData: { ...asset, isLocked: updatedAsset.isLocked }, + timeStap: new Date().toISOString(), + }); + + handleAssetLockUpdate(updatedAsset); + + 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, + }); + } } } else if (option === "kebab") { if (isGroup(item)) { diff --git a/app/src/modules/scene/controls/assetControls/cutCopyPasteControls3D.tsx b/app/src/modules/scene/controls/assetControls/cutCopyPasteControls3D.tsx index d2a63ae..55b0f8b 100644 --- a/app/src/modules/scene/controls/assetControls/cutCopyPasteControls3D.tsx +++ b/app/src/modules/scene/controls/assetControls/cutCopyPasteControls3D.tsx @@ -42,7 +42,7 @@ const CutCopyPasteControls3D = () => { rotatedObjects, setRotatedObjects, } = assetStore(); - const { updateAssetInScene, removeAssetFromScene } = useAssetResponseHandler(); + const { addAssetToScene, removeAssetFromScene } = useAssetResponseHandler(); const { selectedVersion } = versionStore(); const { userId, organization } = getUserData(); @@ -369,13 +369,13 @@ const CutCopyPasteControls3D = () => { isVisible: asset.isVisible, }; - if (asset.eventData) { - let updatedEventData = JSON.parse(JSON.stringify(asset.eventData)) as EventsSchema; + if (pastedAsset.userData.eventData) { + let updatedEventData = JSON.parse(JSON.stringify(pastedAsset.userData.eventData)) as EventsSchema; updatedEventData.modelUuid = newFloorItem.modelUuid; const eventData: any = { - type: asset.eventData.type, - subType: asset.eventData.subType || "", + type: pastedAsset.userData.eventData.type, + subType: pastedAsset.userData.eventData.subType || "", }; let points: THREE.Vector3[] = []; @@ -439,7 +439,7 @@ const CutCopyPasteControls3D = () => { opacity: newFloorItem.opacity, isLocked: newFloorItem.isLocked, isVisible: newFloorItem.isVisible, - eventData: eventData, + eventData: newFloorItem.eventData, versionId: selectedVersion?.versionId || "", projectId: projectId || "", }) @@ -463,7 +463,7 @@ const CutCopyPasteControls3D = () => { ...(data.data.eventData ? { eventData: data.data.eventData } : {}), }; - updateAssetInScene(model, () => { + addAssetToScene(model, () => { echo.info(`Pasted asset: ${model.modelName}`); }); } else { @@ -537,7 +537,7 @@ const CutCopyPasteControls3D = () => { ...(data.data.eventData ? { eventData: data.data.eventData } : {}), }; - updateAssetInScene(model, () => { + addAssetToScene(model, () => { echo.info(`Pasted asset: ${model.modelUuid}`); }); } else { diff --git a/app/src/modules/scene/controls/assetControls/duplicationControls3D.tsx b/app/src/modules/scene/controls/assetControls/duplicationControls3D.tsx index 80f1d15..27da22a 100644 --- a/app/src/modules/scene/controls/assetControls/duplicationControls3D.tsx +++ b/app/src/modules/scene/controls/assetControls/duplicationControls3D.tsx @@ -25,7 +25,7 @@ const DuplicationControls3D = () => { const { projectId } = useParams(); const { assets, addAsset, removeAsset, getAssetById, selectedAssets, duplicatedObjects, setDuplicatedObjects, setPastedObjects, movedObjects, setMovedObjects, rotatedObjects, setRotatedObjects } = assetStore(); - const { updateAssetInScene, removeAssetFromScene } = useAssetResponseHandler(); + const { addAssetToScene, removeAssetFromScene } = useAssetResponseHandler(); const { selectedVersion } = versionStore(); const { userId, organization } = getUserData(); @@ -296,13 +296,13 @@ const DuplicationControls3D = () => { isVisible: asset.isVisible, }; - if (asset.eventData) { - let updatedEventData = JSON.parse(JSON.stringify(asset.eventData)) as EventsSchema; + if (duplicatedAsset.userData.eventData) { + let updatedEventData = JSON.parse(JSON.stringify(duplicatedAsset.userData.eventData)) as EventsSchema; updatedEventData.modelUuid = newFloorItem.modelUuid; const eventData: any = { - type: asset.eventData.type, - subType: asset.eventData.subType || "", + type: duplicatedAsset.userData.eventData.type, + subType: duplicatedAsset.userData.eventData.subType || "", }; let points: THREE.Vector3[] = []; @@ -364,7 +364,7 @@ const DuplicationControls3D = () => { opacity: newFloorItem.opacity, isLocked: newFloorItem.isLocked, isVisible: newFloorItem.isVisible, - eventData: eventData, + eventData: newFloorItem.eventData, versionId: selectedVersion?.versionId || "", projectId: projectId || "", }) @@ -388,7 +388,7 @@ const DuplicationControls3D = () => { ...(data.data.eventData ? { eventData: data.data.eventData } : {}), }; - updateAssetInScene(model, () => { + addAssetToScene(model, () => { echo.info(`Duplicated asset: ${model.modelName}`); }); } else { @@ -460,7 +460,7 @@ const DuplicationControls3D = () => { ...(data.data.eventData ? { eventData: data.data.eventData } : {}), }; - updateAssetInScene(model, () => { + addAssetToScene(model, () => { echo.info(`Duplicated asset: ${model.modelUuid}`); }); } else { diff --git a/app/src/modules/scene/controls/assetControls/groupControls.tsx b/app/src/modules/scene/controls/assetControls/groupControls.tsx index 2aeb465..ba04658 100644 --- a/app/src/modules/scene/controls/assetControls/groupControls.tsx +++ b/app/src/modules/scene/controls/assetControls/groupControls.tsx @@ -10,6 +10,7 @@ import useCallBackOnKey from "../../../../utils/hooks/useCallBackOnKey"; import generateUniqueAssetGroupName from "../../../builder/asset/functions/generateUniqueAssetGroupName"; import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; +import { getAssetGroupsApi } from "../../../../services/factoryBuilder/group/assetGroup/getAssetGroupsApi"; function GroupControls() { const { projectId } = useParams(); @@ -24,8 +25,17 @@ function GroupControls() { const { userId, organization } = getUserData(); useEffect(() => { - console.log("assetGroups: ", assetGroups); - console.log("hierarchy: ", buildHierarchy(assets, assetGroups)); + if (!projectId || !selectedVersion) return; + + getAssetGroupsApi(projectId, selectedVersion.versionId).then((data) => { + console.log("data: ", data); + }); + }, [projectId, selectedVersion]); + + useEffect(() => { + const hierarchy = buildHierarchy(assets, assetGroups); + // console.log("assetGroups: ", assetGroups); + // console.log("hierarchy: ", hierarchy); }, [assetGroups, assets]); useEffect(() => { diff --git a/app/src/modules/scene/controls/assetControls/scaleControls3D.tsx b/app/src/modules/scene/controls/assetControls/scaleControls3D.tsx index 04e50e8..b87d066 100644 --- a/app/src/modules/scene/controls/assetControls/scaleControls3D.tsx +++ b/app/src/modules/scene/controls/assetControls/scaleControls3D.tsx @@ -81,6 +81,7 @@ function ScaleControls3D() { } } + console.log('builderSocket: ', builderSocket); if (!builderSocket?.connected) { setAssetsApi({ modelUuid: updatedAsset.modelUuid, diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx index 3d67dc5..e8f8607 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx @@ -15,6 +15,7 @@ import useAssetResponseHandler from "../../../../collaboration/responseHandler/u import { deleteFloorAssetApi } from "../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorAssetApi"; import { updateEventToBackend } from "../../../../../components/layout/sidebarRight/properties/eventProperties/functions/handleUpdateEventToBackend"; +import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; const SelectionControls3D: React.FC = () => { const { camera, controls, gl, scene, pointer } = useThree(); @@ -152,14 +153,21 @@ const SelectionControls3D: React.FC = () => { }; const onKeyDown = (event: KeyboardEvent) => { - if (movedObjects.length > 0 || rotatedObjects.length > 0) return; + if (movedObjects.length > 0 || rotatedObjects.length > 0 || event.repeat) return; if (event.key.toLowerCase() === "escape") { event.preventDefault(); clearSelection(); + return; } if (event.key.toLowerCase() === "delete") { event.preventDefault(); deleteSelection(); + return; + } + const modifier = detectModifierKeys(event); + + if (modifier === "Ctrl+A") { + selectedAll(); } }; @@ -224,6 +232,7 @@ const SelectionControls3D: React.FC = () => { selectedAisle, selectedFloor, selectedWallAsset, + assets, ]); useEffect(() => { @@ -267,6 +276,11 @@ const SelectionControls3D: React.FC = () => { setSelectedAssets(selected); }, [selectionBox, pointer, controls, selectedAssets, setSelectedAssets]); + const selectedAll = useCallback(() => { + const assetMeshes: THREE.Object3D[] = assets.map((asset) => scene.getObjectByProperty("uuid", asset.modelUuid)).filter((obj): obj is THREE.Object3D => obj !== undefined); + setSelectedAssets(assetMeshes); + }, [assets]); + const clearSelection = () => { setPastedObjects([]); setDuplicatedObjects([]); diff --git a/app/src/modules/scene/sceneContext.tsx b/app/src/modules/scene/sceneContext.tsx index 87a8661..2245ab2 100644 --- a/app/src/modules/scene/sceneContext.tsx +++ b/app/src/modules/scene/sceneContext.tsx @@ -1,4 +1,5 @@ -import { Scene } from "three"; +import { Scene, PerspectiveCamera, OrthographicCamera } from "three"; +import { CameraControls } from "@react-three/drei"; import { createContext, MutableRefObject, useContext, useMemo, useRef } from "react"; import { createVersionStore, VersionStoreType } from "../../store/builder/useVersionStore"; @@ -33,6 +34,8 @@ import { createCollabusersStore, CollabUsersStoreType } from "../../store/collab type SceneContextValue = { scene: MutableRefObject; + controls: MutableRefObject; + camera: MutableRefObject; versionStore: VersionStoreType; @@ -86,6 +89,8 @@ export function SceneProvider({ readonly layoutType: "default" | "useCase" | "tutorial" | null; }) { const scene = useRef(null); + const controls = useRef(null); + const camera = useRef(null); const versionStore = useMemo(() => createVersionStore(), []); @@ -123,6 +128,8 @@ export function SceneProvider({ const clearStores = useMemo( () => () => { scene.current = null; + controls.current = null; + camera.current = null; versionStore.getState().clearVersions(); assetStore.getState().clearAssets(); wallAssetStore.getState().clearWallAssets(); @@ -177,6 +184,8 @@ export function SceneProvider({ const contextValue = useMemo( () => ({ scene, + controls, + camera, versionStore, assetStore, wallAssetStore, @@ -207,6 +216,8 @@ export function SceneProvider({ }), [ scene, + controls, + camera, versionStore, assetStore, wallAssetStore, diff --git a/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts b/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts new file mode 100644 index 0000000..d4b49cd --- /dev/null +++ b/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts @@ -0,0 +1,53 @@ +const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +interface Props { + projectId: string; + versionId: string; + groupUuid: string; + groupName: string; + isVisible: string; + isExpanded: string; + isLocked: string; + childrens: { + type: "Asset" | "Group"; + childrenUuid: string; + }[]; +} + +export const createAssetGroupApi = async (props: Props) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetgroup/${props.projectId}/${props.versionId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token") || ""}`, + token: localStorage.getItem("token") || "", + refresh_token: localStorage.getItem("refreshToken") || "", + body: JSON.stringify({ + groupUuid: props.groupUuid, + groupName: props.groupName, + isVisible: props.isVisible, + isExpanded: props.isExpanded, + isLocked: props.isLocked, + childrens: props.childrens, + }), + }, + }); + + const newAccessToken = response.headers.get("x-access-token"); + if (newAccessToken) { + localStorage.setItem("token", newAccessToken); + } + + if (!response.ok) { + console.error("Failed to create asset group"); + return null; + } + + const result = await response.json(); + return result; + } catch (error) { + console.error("Failed to create asset group", error); + return null; + } +}; diff --git a/app/src/services/factoryBuilder/group/assetGroup/createChildrenApi.ts b/app/src/services/factoryBuilder/group/assetGroup/createChildrenApi.ts new file mode 100644 index 0000000..bfeb6a6 --- /dev/null +++ b/app/src/services/factoryBuilder/group/assetGroup/createChildrenApi.ts @@ -0,0 +1,42 @@ +const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +interface Props { + projectId: string; + versionId: string; + groupsData: { + groupUuid: string; + childUuid: string; + type: string; + }[]; +} + +export const createAssetGroupChildrenApi = async ({ projectId, versionId, groupsData }: Props) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetgroup/child/${projectId}/${versionId}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token") || ""}`, + token: localStorage.getItem("token") || "", + refresh_token: localStorage.getItem("refreshToken") || "", + data: JSON.parse(JSON.stringify({ groupsData: groupsData })), + }, + }); + + const newAccessToken = response.headers.get("x-access-token"); + if (newAccessToken) { + localStorage.setItem("token", newAccessToken); + } + + if (!response.ok) { + console.error("Failed to update asset group"); + return null; + } + + const result = await response.json(); + return result; + } catch (error) { + console.error("Failed to update asset group", error); + return null; + } +}; diff --git a/app/src/services/factoryBuilder/group/assetGroup/deleteAssetGroup.ts b/app/src/services/factoryBuilder/group/assetGroup/deleteAssetGroup.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/src/services/factoryBuilder/group/assetGroup/deleteChildrenApi.ts b/app/src/services/factoryBuilder/group/assetGroup/deleteChildrenApi.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupApi.ts b/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupApi.ts new file mode 100644 index 0000000..49de18b --- /dev/null +++ b/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupApi.ts @@ -0,0 +1,29 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const getAssetGroupApi = async (projectId: string, versionId: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetgroup/${projectId}/${versionId}`, { + method: "GET", + headers: { + Authorization: "Bearer ", + "Content-Type": "application/json", + token: localStorage.getItem("token") || "", + refresh_token: localStorage.getItem("refreshToken") || "", + }, + }); + + const newAccessToken = response.headers.get("x-access-token"); + if (newAccessToken) { + localStorage.setItem("token", newAccessToken); + } + + if (!response.ok) { + echo.error("Failed to get asset group"); + } + + const result = await response.json(); + return result; + } catch { + echo.error("Failed to get asset group"); + } +}; diff --git a/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupsApi.ts b/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupsApi.ts new file mode 100644 index 0000000..7ad4a08 --- /dev/null +++ b/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupsApi.ts @@ -0,0 +1,29 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const getAssetGroupsApi = async (projectId: string, versionId: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetGroups/${projectId}/${versionId}`, { + method: "GET", + headers: { + Authorization: "Bearer ", + "Content-Type": "application/json", + token: localStorage.getItem("token") || "", + refresh_token: localStorage.getItem("refreshToken") || "", + }, + }); + + const newAccessToken = response.headers.get("x-access-token"); + if (newAccessToken) { + localStorage.setItem("token", newAccessToken); + } + + if (!response.ok) { + echo.error("Failed to get asset groups"); + } + + const result = await response.json(); + return result; + } catch { + echo.error("Failed to get asset groups"); + } +}; diff --git a/app/src/services/factoryBuilder/group/assetGroup/updateAssetGroupApi.ts b/app/src/services/factoryBuilder/group/assetGroup/updateAssetGroupApi.ts new file mode 100644 index 0000000..da5424c --- /dev/null +++ b/app/src/services/factoryBuilder/group/assetGroup/updateAssetGroupApi.ts @@ -0,0 +1,44 @@ +const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +interface Props { + projectId: string; + versionId: string; + groupsData: { + groupUuid: string; + groupName: string; + isVisible: string; + isExpanded: string; + isLocked: string; + }[]; +} + +export const updateAssetGroupApi = async (props: Props) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetgroup/${props.projectId}/${props.versionId}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token") || ""}`, + token: localStorage.getItem("token") || "", + refresh_token: localStorage.getItem("refreshToken") || "", + data: JSON.parse(JSON.stringify({ groupsData: props.groupsData })), + }, + }); + + const newAccessToken = response.headers.get("x-access-token"); + if (newAccessToken) { + localStorage.setItem("token", newAccessToken); + } + + if (!response.ok) { + console.error("Failed to update asset group"); + return null; + } + + const result = await response.json(); + return result; + } catch (error) { + console.error("Failed to update asset group", error); + return null; + } +}; diff --git a/app/src/store/builder/useAssetStore.ts b/app/src/store/builder/useAssetStore.ts index d0e7d2b..7e9a591 100644 --- a/app/src/store/builder/useAssetStore.ts +++ b/app/src/store/builder/useAssetStore.ts @@ -42,6 +42,7 @@ interface AssetsStore { setCollision: (modelUuid: string, isCollidable: boolean) => void; setVisibility: (modelUuid: string, isVisible: boolean) => void; peekToggleVisibility: (modelUuid: string) => Asset | undefined; + peekToggleLock: (modelUuid: string) => Asset | undefined; setOpacity: (modelUuid: string, opacity: number) => void; // Animation controls @@ -63,6 +64,7 @@ interface AssetsStore { getInvisibleAssets: () => Asset[]; getVisibleAssets: () => Asset[]; hasAsset: (modelUuid: string) => boolean; + getSelectedAssetUuids: () => string[]; } export const createAssetStore = () => { @@ -276,7 +278,16 @@ export const createAssetStore = () => { const asset = get().assets.find((a) => a.modelUuid === modelUuid); if (!asset) return undefined; - const updatedAsset = { ...asset, isVisible: !asset.isVisible }; + const updatedAsset: Asset = { ...asset, isVisible: !asset.isVisible }; + + return updatedAsset; + }, + + peekToggleLock: (modelUuid: string): Asset | undefined => { + const asset = get().assets.find((a) => a.modelUuid === modelUuid); + if (!asset) return undefined; + + const updatedAsset: Asset = { ...asset, isLocked: !asset.isLocked }; return updatedAsset; }, @@ -410,6 +421,10 @@ export const createAssetStore = () => { hasAsset: (modelUuid) => { return get().assets.some((a) => a.modelUuid === modelUuid); }, + + getSelectedAssetUuids: () => { + return get().selectedAssets.map((o) => o.uuid); + }, })) ); };