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 0e65678..27e9402 100644 --- a/app/src/modules/builder/testUi/outline.tsx +++ b/app/src/modules/builder/testUi/outline.tsx @@ -55,6 +55,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; @@ -120,7 +121,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, @@ -226,24 +227,8 @@ export const Outline = () => { }); const [_, forceUpdate] = useState({}); const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext(); - const { - addSelectedAsset, - clearSelectedAssets, - getAssetById, - peekToggleVisibility, - toggleSelectedAsset, - selectedAssets, - } = assetStore(); - const { - groupHierarchy, - isGroup, - getGroupsContainingAsset, - getFlatGroupChildren, - setGroupExpanded, - addChildToGroup, - removeChildFromGroup, - getGroupsContainingGroup, - } = assetGroupStore(); + 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(); const { builderSocket } = useSocketStore(); @@ -350,6 +335,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); @@ -646,14 +702,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(), }); @@ -661,17 +711,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({ @@ -683,6 +725,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)) { @@ -699,7 +771,7 @@ export const Outline = () => {
-

Scene Hierarchy

+

Assets