diff --git a/app/src/components/footer/Footer.tsx b/app/src/components/footer/Footer.tsx index fdac078..0ee70c5 100644 --- a/app/src/components/footer/Footer.tsx +++ b/app/src/components/footer/Footer.tsx @@ -10,7 +10,6 @@ import { import ShortcutHelper from "./shortcutHelper"; import { useShortcutStore } from "../../store/builder/store"; import { usePlayButtonStore } from "../../store/usePlayButtonStore"; -import OuterClick from "../../utils/outerClick"; const Footer: React.FC = () => { const { logs, setIsLogListVisible } = useLogger(); @@ -19,11 +18,6 @@ const Footer: React.FC = () => { const { isPlaying } = usePlayButtonStore(); const { showShortcuts, setShowShortcuts } = useShortcutStore(); - OuterClick({ - contextClassName: ["shortcut-helper-overlay"], - setMenuVisible: () => setShowShortcuts(false), - }); - return (
@@ -82,7 +76,7 @@ const Footer: React.FC = () => { showShortcuts ? "visible" : "" }`} > - +
)}
diff --git a/app/src/components/footer/shortcutHelper.tsx b/app/src/components/footer/shortcutHelper.tsx index bf8ccc1..aa927c5 100644 --- a/app/src/components/footer/shortcutHelper.tsx +++ b/app/src/components/footer/shortcutHelper.tsx @@ -29,8 +29,8 @@ import { DublicateIcon, DuplicateInstanceIcon, PlayIcon, - BrowserIcon, } from "../icons/ShortcutIcons"; +import { CloseIcon } from "../icons/ExportCommonIcons"; interface ShortcutItem { keys: string[]; @@ -44,7 +44,13 @@ interface ShortcutGroup { items: ShortcutItem[]; } -const ShortcutHelper = () => { +interface ShortcutHelperProps { + setShowShortcuts: (value: boolean) => void; +} + +const ShortcutHelper: React.FC = ({ + setShowShortcuts, +}) => { const shortcuts: ShortcutGroup[] = [ { category: "Essential", @@ -256,27 +262,26 @@ const ShortcutHelper = () => { }, ], }, - { - category: "Miscellaneous", - items: [ - { - keys: ["F5", "F11", "F12", "CTRL", "+", "R"], - name: "Browser Defaults", - description: "Reserved for browser defaults", - icon: , - }, - ], - }, ]; const [activeCategory, setActiveCategory] = React.useState("Essential"); const activeShortcuts = - shortcuts.find((group) => group.category === activeCategory)?.items || []; + shortcuts.find((group) => group.category === activeCategory)?.items ?? []; return (
+
{shortcuts.map((group) => ( @@ -313,7 +318,7 @@ const ShortcutHelper = () => { {item.keys.map((key, i) => ( {key} diff --git a/app/src/components/layout/controls/ControlsPlayer.tsx b/app/src/components/layout/controls/ControlsPlayer.tsx index 248c466..724c3bb 100644 --- a/app/src/components/layout/controls/ControlsPlayer.tsx +++ b/app/src/components/layout/controls/ControlsPlayer.tsx @@ -1,53 +1,85 @@ -import React, { useState } from "react"; -import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import React, { useEffect, useState } from "react"; +import useCameraModeStore, { + usePlayButtonStore, +} from "../../../store/usePlayButtonStore"; import useModuleStore from "../../../store/useModuleStore"; import { PlayIcon } from "../../icons/ShortcutIcons"; import InputToggle from "../../ui/inputs/InputToggle"; import { EyeCloseIcon, WalkIcon } from "../../icons/ExportCommonIcons"; import { ExitIcon } from "../../icons/SimulationIcons"; +import { useCamMode } from "../../../store/builder/store"; -const ControlsPlayer = () => { +const ControlsPlayer: React.FC = () => { const { setIsPlaying } = usePlayButtonStore(); const { activeModule } = useModuleStore(); - const [walkMode, setWalkMode] = useState(false); + const { walkMode, toggleWalkMode } = useCameraModeStore(); + const [hidePlayer, setHidePlayer] = useState(false); + const { camMode } = useCamMode(); const changeCamMode = () => { - setWalkMode(!walkMode); - console.log("switch camera mode to first person"); + toggleWalkMode(); + echo.log("switch camera mode to first person"); + // Simulate "/" keypress + const slashKeyEvent = new KeyboardEvent("keydown", { + key: "/", + code: "Slash", + keyCode: 191, // for compatibility + which: 191, + bubbles: true, + cancelable: true, + }); + document.dispatchEvent(slashKeyEvent); }; + useEffect(() => { + if (camMode === "ThirdPerson") { + toggleWalkMode(); + } else return; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [camMode]); + return ( -
-
- -
Running {activeModule}...
-
+
+ {!hidePlayer && ( +
+ +
Running {activeModule}...
+
+ )}
-
- - -
+ {!hidePlayer && activeModule === "builder" && ( +
+ + +
+ )} -
+
+ {!hidePlayer && "Hide"} +
); diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx index 05774e2..a5d4629 100644 --- a/app/src/components/layout/sidebarRight/SideBarRight.tsx +++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx @@ -89,7 +89,7 @@ const SideBarRight: React.FC = () => { {activeModule === "simulation" && ( <> - {selectedPointData.actions.length > 1 && ( + {selectedPointData?.actions?.length > 1 && (
))} - {!multipleAction && selectedPointData && ( + {!multipleAction && selectedPointData?.action && (
{ - -
-
- ); -}; - -export default SelectFloorPlan; +import React from "react"; +import useLayoutStore from "../../store/builder/uselayoutStore"; + +const SelectFloorPlan: React.FC = () => { + const { currentLayout, setLayout } = useLayoutStore(); + return ( +
+ Preset Layouts +
+ + +
+
+ ); +}; + +export default SelectFloorPlan; diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index 50f878a..bb99cc8 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -335,6 +335,7 @@ const Tools: React.FC = () => { {activeModule !== "visualization" && (
- + {!hidePlayer && ( + + )} + {!hidePlayer && ( + + )} + {subModule === "analysis" && (
-
-
-
-
- -
-
-
23 April ,25
-
04:41 PM
-
-
-
-
- {intervals.map((label, index) => { - const segmentProgress = (index / totalSegments) * 100; - const isFilled = progress >= segmentProgress; - return ( - -
-
{label} mins
-
-
- {index < intervals.length - 1 && ( -
= ((index + 1) / totalSegments) * 100 - ? "filled" - : "" - }`} - >
- )} -
- ); - })} -
-
- -
-
-
00:10:20
-
-
- -
-
-
-
-
-
- -
- Speed -
-
-
0.5X
-
-
-
-
-
-
-
-
-
-
-
- - + {!hidePlayer && ( +
+
+
+
+ +
+
+
23 April ,25
+
04:41 PM
+
+
+
+
+ {intervals.map((label, index) => { + const segmentProgress = (index / totalSegments) * 100; + const isFilled = progress >= segmentProgress; + return ( + +
+
{label} mins
+
+
+ {index < intervals.length - 1 && ( +
= ((index + 1) / totalSegments) * 100 + ? "filled" + : "" + }`} + >
+ )} +
+ ); + })} +
+
+ +
+
+
00:10:20
+
+
+ +
+
+
+
+
+
+ +
+ Speed +
+
+
0.5X
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
{MAX_SPEED}x
-
{MAX_SPEED}x
-
- {subModule === "analysis" && ( + )} + {!hidePlayer && subModule === "analysis" && (
00:00
24:00
diff --git a/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts b/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts index 0d96c8d..096665a 100644 --- a/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts +++ b/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts @@ -3,7 +3,6 @@ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; import gsap from 'gsap'; import * as THREE from 'three'; import * as CONSTANTS from '../../../types/world/worldConstants'; -import { toast } from 'react-toastify'; import * as Types from "../../../types/world/worldTypes"; import { initializeDB, retrieveGLTF, storeGLTF } from '../../../utils/indexDB/idbUtils'; import { getCamera } from '../../../services/factoryBuilder/camera/getCameraApi'; @@ -91,7 +90,7 @@ async function loadInitialFloorItems( }, undefined, (error) => { - toast.error(`[IndexedDB] Error loading ${item.modelName}:`); + echo.error(`[IndexedDB] Error loading ${item.modelName}:`); URL.revokeObjectURL(blobUrl); resolve(); } @@ -111,7 +110,7 @@ async function loadInitialFloorItems( }, undefined, (error) => { - toast.error(`[Backend] Error loading ${item.modelName}:`); + echo.error(`[Backend] Error loading ${item.modelName}:`); resolve(); } ); diff --git a/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts b/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts index 2258f3b..96395ba 100644 --- a/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts +++ b/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts @@ -33,7 +33,7 @@ async function loadInitialWallItems( const loadedWallItems = await Promise.all(items.map(async (item: Types.WallItem) => { // Check THREE.js cache first - const cachedModel = THREE.Cache.get(item.modelName!); + const cachedModel = THREE.Cache.get(item.modelfileID!); if (cachedModel) { return processModel(cachedModel, item); } @@ -45,7 +45,7 @@ async function loadInitialWallItems( return new Promise((resolve) => { loader.load(blobUrl, (gltf) => { URL.revokeObjectURL(blobUrl); - THREE.Cache.add(item.modelName!, gltf); + THREE.Cache.add(item.modelfileID!, gltf); resolve(processModel(gltf, item)); }); }); @@ -58,8 +58,8 @@ async function loadInitialWallItems( try { // Cache the model const modelBlob = await fetch(modelUrl).then((res) => res.blob()); - await storeGLTF(item.modelName!, modelBlob); - THREE.Cache.add(item.modelName!, gltf); + await storeGLTF(item.modelfileID!, modelBlob); + THREE.Cache.add(item.modelfileID!, gltf); resolve(processModel(gltf, item)); } catch (error) { console.error('Failed to cache model:', error); diff --git a/app/src/modules/builder/assetGroup/assetsGroup.tsx b/app/src/modules/builder/assetGroup/assetsGroup.tsx new file mode 100644 index 0000000..9e57fb7 --- /dev/null +++ b/app/src/modules/builder/assetGroup/assetsGroup.tsx @@ -0,0 +1,125 @@ +import * as THREE from "three" +import { useEffect } from 'react' +import { getFloorAssets } from '../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi'; +import { useLoadingProgress } from '../../../store/builder/store'; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; +import { FloorItems } from "../../../types/world/worldTypes"; +import { useAssetsStore } from "../../../store/builder/useAssetStore"; +import Models from "./models/models"; +import { useGLTF } from "@react-three/drei"; + +const gltfLoaderWorker = new Worker( + new URL( + "../../../services/factoryBuilder/webWorkers/gltfLoaderWorker.js", + import.meta.url + ) +); + +function AssetsGroup() { + const { setLoadingProgress } = useLoadingProgress(); + const { setAssets } = useAssetsStore(); + + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); + + dracoLoader.setDecoderPath( + "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/" + ); + loader.setDRACOLoader(dracoLoader); + + useEffect(() => { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; + + let totalAssets = 0; + let loadedAssets = 0; + + const updateLoadingProgress = (progress: number) => { + if (progress < 100) { + setLoadingProgress(progress); + } else if (progress === 100) { + setTimeout(() => { + setLoadingProgress(100); + setTimeout(() => { + setLoadingProgress(0); + }, 1500); + }, 1000); + } + }; + + getFloorAssets(organization).then((data) => { + if (data.length > 0) { + const uniqueItems = (data as FloorItems).filter((item, index, self) => index === self.findIndex((t) => t.modelfileID === item.modelfileID)); + totalAssets = uniqueItems.length; + if (totalAssets === 0) { + updateLoadingProgress(100); + return; + } + gltfLoaderWorker.postMessage({ floorItems: uniqueItems }); + } else { + gltfLoaderWorker.postMessage({ floorItems: [] }); + updateLoadingProgress(100); + } + }); + + gltfLoaderWorker.onmessage = async (event) => { + if (event.data.message === "gltfLoaded" && event.data.modelBlob) { + const blobUrl = URL.createObjectURL(event.data.modelBlob); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(event.data.modelID, gltf); + + loadedAssets++; + const progress = Math.round((loadedAssets / totalAssets) * 100); + updateLoadingProgress(progress); + + if (loadedAssets === totalAssets) { + const assets: Asset[] = []; + getFloorAssets(organization).then((data: FloorItems) => { + data.forEach((item) => { + if (item.eventData) { + assets.push({ + modelUuid: item.modelUuid, + modelName: item.modelName, + assetId: item.modelfileID, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + isLocked: item.isLocked, + isCollidable: false, + isVisible: item.isVisible, + opacity: 1, + eventData: item.eventData + }) + } else { + assets.push({ + modelUuid: item.modelUuid, + modelName: item.modelName, + assetId: item.modelfileID, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + isLocked: item.isLocked, + isCollidable: false, + isVisible: item.isVisible, + opacity: 1, + }) + } + }) + setAssets(assets); + }) + updateLoadingProgress(100); + } + }); + } + }; + }, []); + + return ( + <> + + + ) +} + +export default AssetsGroup; \ No newline at end of file diff --git a/app/src/modules/builder/assetGroup/models/model/model.tsx b/app/src/modules/builder/assetGroup/models/model/model.tsx new file mode 100644 index 0000000..55eb1a6 --- /dev/null +++ b/app/src/modules/builder/assetGroup/models/model/model.tsx @@ -0,0 +1,82 @@ +import { Outlines } from '@react-three/drei'; +import { useEffect, useState } from 'react'; +import { retrieveGLTF, storeGLTF } from '../../../../../utils/indexDB/idbUtils'; +import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import * as THREE from 'three'; + +function Model({ asset }: { asset: Asset }) { + const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + const [gltfScene, setGltfScene] = useState(null); + + useEffect(() => { + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); + + dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); + loader.setDRACOLoader(dracoLoader); + const loadModel = async () => { + try { + const cachedModel = THREE.Cache.get(asset.assetId!); + if (cachedModel) { + setGltfScene(cachedModel); + return; + } + + // Check IndexedDB + const indexedDBModel = await retrieveGLTF(asset.assetId!); + if (indexedDBModel) { + const blobUrl = URL.createObjectURL(indexedDBModel); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(asset.assetId!, gltf); + setGltfScene(gltf); + }, + undefined, + (error) => { + echo.error(`[IndexedDB] Error loading ${asset.modelName}:`); + URL.revokeObjectURL(blobUrl); + } + ); + return; + } + + // Fetch from Backend + const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${asset.assetId!}`; + loader.load(modelUrl, async (gltf) => { + const modelBlob = await fetch(modelUrl).then((res) => res.blob()); + await storeGLTF(asset.assetId!, modelBlob); + THREE.Cache.add(asset.assetId!, gltf); + setGltfScene(gltf); + }, + undefined, + (error) => { + echo.error(`[Backend] Error loading ${asset.modelName}:`); + } + ); + } catch (err) { + console.error("Failed to load model:", asset.assetId, err); + } + }; + + loadModel(); + + }, [asset.assetId]); + + return ( + <> + {gltfScene && + + + + } + + ); +} + +export default Model; \ No newline at end of file diff --git a/app/src/modules/builder/assetGroup/models/models.tsx b/app/src/modules/builder/assetGroup/models/models.tsx new file mode 100644 index 0000000..7378812 --- /dev/null +++ b/app/src/modules/builder/assetGroup/models/models.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { useAssetsStore } from '../../../../store/builder/useAssetStore'; +import Model from './model/model'; + +function Models() { + const { assets } = useAssetsStore(); + + return ( + <> + {assets.map((asset) => + + )} + + ) +} + +export default Models \ No newline at end of file diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index ed82590..57ead56 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -47,6 +47,7 @@ import MeasurementTool from "../scene/tools/measurementTool"; import NavMesh from "../simulation/vehicle/navMesh/navMesh"; import CalculateAreaGroup from "./groups/calculateAreaGroup"; import LayoutImage from "./layout/layoutImage"; +import AssetsGroup from "./assetGroup/assetsGroup"; export default function Builder() { const state = useThree(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements. @@ -299,6 +300,8 @@ export default function Builder() { anglesnappedPoint={anglesnappedPoint} /> + {/* */} + diff --git a/app/src/modules/builder/geomentries/assets/assetManager.ts b/app/src/modules/builder/geomentries/assets/assetManager.ts index 889d905..5d2f90f 100644 --- a/app/src/modules/builder/geomentries/assets/assetManager.ts +++ b/app/src/modules/builder/geomentries/assets/assetManager.ts @@ -78,7 +78,7 @@ export default async function assetManager( }, undefined, (error) => { - toast.error(`[IndexedDB] Error loading ${item.modelName}:`); + echo.error(`[IndexedDB] Error loading ${item.modelName}:`); resolve(); } ); @@ -97,7 +97,7 @@ export default async function assetManager( }, undefined, (error) => { - toast.error(`[Backend] Error loading ${item.modelName}:`); + echo.error(`[Backend] Error loading ${item.modelName}:`); resolve(); } ); diff --git a/app/src/modules/builder/geomentries/lines/updateDistanceText.ts b/app/src/modules/builder/geomentries/lines/updateDistanceText.ts index 2f743a9..8a5a1cc 100644 --- a/app/src/modules/builder/geomentries/lines/updateDistanceText.ts +++ b/app/src/modules/builder/geomentries/lines/updateDistanceText.ts @@ -28,7 +28,7 @@ function updateDistanceText( const textMesh = text as THREE.Mesh; if (textMesh.userData[0][1] === linePoints[0][1] && textMesh.userData[1][1] === linePoints[1][1]) { textMesh.position.set(position.x, 1, position.z); - const className = `Distance line-${textMesh.userData[0][1]}_${textMesh.userData[1][1]}_${linePoints[0][2]}`; + const className = `distance line-${textMesh.userData[0][1]}_${textMesh.userData[1][1]}_${linePoints[0][2]}`; const element = document.getElementsByClassName(className)[0] as HTMLElement; if (element) { element.innerHTML = `${distance} m`; diff --git a/app/src/modules/builder/groups/floorItemsGroup.tsx b/app/src/modules/builder/groups/floorItemsGroup.tsx index ad0c86e..aacd04c 100644 --- a/app/src/modules/builder/groups/floorItemsGroup.tsx +++ b/app/src/modules/builder/groups/floorItemsGroup.tsx @@ -104,7 +104,7 @@ const FloorItemsGroup = ({ updateLoadingProgress(100); return; } - gltfLoaderWorker.postMessage({ floorItems: data }); + gltfLoaderWorker.postMessage({ floorItems: uniqueItems }); } else { gltfLoaderWorker.postMessage({ floorItems: [] }); loadInitialFloorItems( diff --git a/app/src/modules/market/Card.tsx b/app/src/modules/market/Card.tsx index d70d2b4..2b878bb 100644 --- a/app/src/modules/market/Card.tsx +++ b/app/src/modules/market/Card.tsx @@ -103,7 +103,7 @@ const Card: React.FC = ({
+ - {/* Extra Buttons */} - {selectedZone.activeSides.includes(side) && ( -
= ({ : "" } `} + > + {/* Hide Panel */} + - {/* Clean Panel */} -
cleanPanel(side)} - style={{ - cursor: - hiddenPanels[selectedZone.zoneId]?.includes(side) || - selectedZone.lockedPanels.includes(side) - ? "not-allowed" - : "pointer", - }} - > - -
- - {/* Lock/Unlock Panel */} -
cleanPanel(side)} + style={{ + cursor: + hiddenPanels[selectedZone.zoneId]?.includes(side) || selectedZone.lockedPanels.includes(side) - ? "Unlock Panel" - : "Lock Panel" + ? "not-allowed" + : "pointer", + }} + > + + + + {/* Lock/Unlock Panel */} +
-
- )} -
- ))} -
- + /> + +
+ )} + + ))} + ); }; diff --git a/app/src/modules/visualization/widgets/panel/Panel.tsx b/app/src/modules/visualization/widgets/panel/Panel.tsx index f0fb86b..4b510b4 100644 --- a/app/src/modules/visualization/widgets/panel/Panel.tsx +++ b/app/src/modules/visualization/widgets/panel/Panel.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { arrayMove } from "@dnd-kit/sortable"; -import { useAsset3dWidget, useSocketStore } from "../../../../store/builder/store"; +import { useSocketStore } from "../../../../store/builder/store"; import { usePlayButtonStore } from "../../../../store/usePlayButtonStore"; import { useWidgetStore } from "../../../../store/useWidgetStore"; import { DraggableWidget } from "../2d/DraggableWidget"; @@ -47,7 +47,7 @@ interface PanelProps { } const generateUniqueId = () => - `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; const Panel: React.FC = ({ selectedZone, @@ -56,7 +56,6 @@ const Panel: React.FC = ({ setZonesData, waitingPanels, }) => { - const { widgetSelect, setWidgetSelect } = useAsset3dWidget(); const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({}); const [panelDimensions, setPanelDimensions] = useState<{ [side in Side]?: { width: number; height: number }; @@ -183,7 +182,7 @@ const Panel: React.FC = ({ // Add widget to panel const addWidgetToPanel = async (asset: any, panel: Side) => { - const email = localStorage.getItem("email") || ""; + const email = localStorage.getItem("email") ?? ""; const organization = email?.split("@")[1]?.split(".")[0]; const newWidget = { @@ -285,7 +284,7 @@ const Panel: React.FC = ({ {selectedZone.activeSides.map((side) => (
= ({ }} >
{ const { toggleThreeD } = useThreeDStore(); // simulation store - const { isPlaying, setIsPlaying } = usePlayButtonStore(); + const { isPlaying } = usePlayButtonStore(); // collaboration store const { selectedUser } = useSelectedUserStore(); diff --git a/app/src/services/visulization/zone/panel.ts b/app/src/services/visulization/zone/panel.ts index 97934ce..1f1911b 100644 --- a/app/src/services/visulization/zone/panel.ts +++ b/app/src/services/visulization/zone/panel.ts @@ -1,5 +1,4 @@ let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; -// let url_Backend_dwinzo = `http://192.168.0.102:5000`; type Side = "top" | "bottom" | "left" | "right"; export const panelData = async ( diff --git a/app/src/store/builder/useAssetStore.ts b/app/src/store/builder/useAssetStore.ts new file mode 100644 index 0000000..326a877 --- /dev/null +++ b/app/src/store/builder/useAssetStore.ts @@ -0,0 +1,221 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; + +interface AssetsStore { + assets: Assets; + + // Asset CRUD operations + addAsset: (asset: Asset) => void; + removeAsset: (modelUuid: string) => void; + updateAsset: (modelUuid: string, updates: Partial) => void; + setAssets: (assets: Assets) => void; + + // Asset properties + setPosition: (modelUuid: string, position: [number, number, number]) => void; + setRotation: (modelUuid: string, rotation: [number, number, number]) => void; + setLock: (modelUuid: string, isLocked: boolean) => void; + setCollision: (modelUuid: string, isCollidable: boolean) => void; + setVisibility: (modelUuid: string, isVisible: boolean) => void; + setOpacity: (modelUuid: string, opacity: number) => void; + + // Animation controls + setAnimation: (modelUuid: string, animation: string) => void; + setCurrentAnimation: (modelUuid: string, current: string, isPlaying: boolean) => void; + addAnimation: (modelUuid: string, animation: string) => void; + removeAnimation: (modelUuid: string, animation: string) => void; + + // Event data operations + addEventData: (modelUuid: string, eventData: Asset['eventData']) => void; + updateEventData: (modelUuid: string, updates: Partial) => void; + removeEventData: (modelUuid: string) => void; + + // Helper functions + getAssetById: (modelUuid: string) => Asset | undefined; + getAssetByPointUuid: (pointUuid: string) => Asset | undefined; + hasAsset: (modelUuid: string) => boolean; +} + +export const useAssetsStore = create()( + immer((set, get) => ({ + assets: [], + + // Asset CRUD operations + addAsset: (asset) => { + set((state) => { + if (!state.assets.some(a => a.modelUuid === asset.modelUuid)) { + state.assets.push(asset); + } + }); + }, + + removeAsset: (modelUuid) => { + set((state) => { + state.assets = state.assets.filter(a => a.modelUuid !== modelUuid); + }); + }, + + updateAsset: (modelUuid, updates) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + Object.assign(asset, updates); + } + }); + }, + + setAssets: (assets) => { + set((state) => { + state.assets = assets; + }); + }, + + // Asset properties + setPosition: (modelUuid, position) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.position = position; + } + }); + }, + + setRotation: (modelUuid, rotation) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.rotation = rotation; + } + }); + }, + + setLock: (modelUuid, isLocked) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.isLocked = isLocked; + } + }); + }, + + setCollision: (modelUuid, isCollidable) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.isCollidable = isCollidable; + } + }); + }, + + setVisibility: (modelUuid, isVisible) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.isVisible = isVisible; + } + }); + }, + + setOpacity: (modelUuid, opacity) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.opacity = opacity; + } + }); + }, + + // Animation controls + setAnimation: (modelUuid, animation) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + if (!asset.animationState) { + asset.animationState = { current: animation, playing: false }; + } else { + asset.animationState.current = animation; + } + } + }); + }, + + setCurrentAnimation: (modelUuid, current, isPlaying) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset?.animationState) { + asset.animationState.current = current; + asset.animationState.playing = isPlaying; + } + }); + }, + + addAnimation: (modelUuid, animation) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + if (!asset.animations) { + asset.animations = [animation]; + } else if (!asset.animations.includes(animation)) { + asset.animations.push(animation); + } + } + }); + }, + + removeAnimation: (modelUuid, animation) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset?.animations) { + asset.animations = asset.animations.filter(a => a !== animation); + if (asset.animationState?.current === animation) { + asset.animationState.playing = false; + asset.animationState.current = ''; + } + } + }); + }, + + // Event data operations + addEventData: (modelUuid, eventData) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.eventData = eventData; + } + }); + }, + + updateEventData: (modelUuid, updates) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset?.eventData) { + asset.eventData = { ...asset.eventData, ...updates }; + } + }); + }, + + removeEventData: (modelUuid) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + delete asset.eventData; + } + }); + }, + + // Helper functions + getAssetById: (modelUuid) => { + return get().assets.find(a => a.modelUuid === modelUuid); + }, + + getAssetByPointUuid: (pointUuid) => { + return get().assets.find(asset => + asset.eventData?.point?.uuid === pointUuid || + asset.eventData?.points?.some(p => p.uuid === pointUuid) + ); + }, + + hasAsset: (modelUuid) => { + return get().assets.some(a => a.modelUuid === modelUuid); + } + })) +); \ No newline at end of file diff --git a/app/src/store/usePlayButtonStore.ts b/app/src/store/usePlayButtonStore.ts index bed2bd7..fe85d6a 100644 --- a/app/src/store/usePlayButtonStore.ts +++ b/app/src/store/usePlayButtonStore.ts @@ -33,3 +33,17 @@ export const useAnimationPlaySpeed = create((set) => ({ speed: 1, setSpeed: (value) => set({ speed: value }), })); + +interface CameraModeState { + walkMode: boolean; + setWalkMode: (enabled: boolean) => void; + toggleWalkMode: () => void; +} + +const useCameraModeStore = create((set) => ({ + walkMode: false, + setWalkMode: (enabled) => set({ walkMode: enabled }), + toggleWalkMode: () => set((state) => ({ walkMode: !state.walkMode })), +})); + +export default useCameraModeStore; diff --git a/app/src/styles/components/footer/footer.scss b/app/src/styles/components/footer/footer.scss index 5b6467b..7bf951a 100644 --- a/app/src/styles/components/footer/footer.scss +++ b/app/src/styles/components/footer/footer.scss @@ -1,7 +1,6 @@ @use "../../abstracts/variables" as *; @use "../../abstracts/mixins" as *; - .footer-container { width: 100%; position: fixed; @@ -34,7 +33,7 @@ .selector { color: var(--text-color); - font-size: var(--font-size-small) + font-size: var(--font-size-small); } .icon { @@ -48,7 +47,7 @@ gap: 6px; position: relative; pointer-events: all; - // dummy + // dummy .bg-dummy { background: var(--background-color-solid); position: absolute; @@ -166,11 +165,23 @@ min-height: 320px; height: 320px; border-radius: 18px; - - + pointer-events: all; background: var(--background-color); backdrop-filter: blur(20px); - + .close-button { + position: absolute; + @include flex-center; + height: 26px; + width: 26px; + right: 12px; + top: 10px; + background: var(--background-color); + border-radius: #{$border-radius-medium}; + outline: 1px solid var(--border-color); + &:hover{ + background: var(--background-color-solid); + } + } .header { display: flex; justify-content: center; @@ -192,7 +203,7 @@ padding-left: 10px; &::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; @@ -244,7 +255,6 @@ .shortcut-intro { display: flex; - // align-items: center; gap: 6px; .value-wrapper { @@ -254,7 +264,6 @@ .description { font-size: var(--font-size-tiny); - } } } @@ -265,14 +274,22 @@ gap: 6px; flex-wrap: wrap; - .key { - background: linear-gradient(135.11deg, #656DC2 3.48%, #9526E5 91.33%); + background: linear-gradient( + 135.11deg, + #656dc2 3.48%, + #9526e5 91.33% + ); + @include flex-center; padding: 4px 10px; + height: 25px; border-radius: 4px; font-family: monospace; font-size: var(--font-size-tiny); color: var(--icon-default-color-active); + &:last-child{ + background: var(--background-color-button); + } } .key.add { @@ -295,19 +312,12 @@ align-items: flex-start; /* or center if vertical centering is desired */ } - - - } - - - } .shortcut-helper-overlay { max-height: 0; overflow: hidden; - // opacity: 0; transform: translateY(20px); transition: all 0.3s ease-in-out; @@ -316,4 +326,4 @@ opacity: 1; transform: translateY(0); } -} \ No newline at end of file +} diff --git a/app/src/styles/components/simulation/simulation.scss b/app/src/styles/components/simulation/simulation.scss index c57bfb0..b7ac080 100644 --- a/app/src/styles/components/simulation/simulation.scss +++ b/app/src/styles/components/simulation/simulation.scss @@ -8,7 +8,16 @@ z-index: 2; transform: translate(-50%, 0); width: 70vw; - + transition: all 0.3s; + &.hide { + width: fit-content; + .simulation-player-container + .controls-container + .simulation-button-container { + width: 32px; + height: 24px; + } + } .simulation-player-container { background: var(--background-color); padding: 7px; @@ -90,11 +99,12 @@ @include flex-center; gap: 2px; padding: 4px 8px; - min-width: 64px; + width: 64px; background: var(--background-color); border-radius: #{$border-radius-extra-large}; height: fit-content; cursor: pointer; + transition: all 0.2s; &:hover { outline: 1px solid var(--border-color); @@ -347,6 +357,86 @@ } } +.controls-player-container { + min-width: 26vw; + max-width: 80vw; + border-radius: 15px; + gap: 40px; + background: var(--background-color); + backdrop-filter: blur(20px); + cursor: pointer; + @include flex-center; + justify-content: space-between; + position: fixed; + bottom: 32px; + left: 50%; + transform: translate(-50%, 0); + color: var(--accent-color); + z-index: 100; + isolation: isolate; + font-weight: 700; + padding: 8px; + transition: all 0.2s; + + &.hide { + min-width: auto; + width: 92px; + } + + .controls-left, + .controls-right { + display: flex; + align-items: center; + gap: 12px; + font-size: var(--font-size-small); + + .label { + text-transform: capitalize; + font-size: var(--font-size-small); + } + + .walkMode-wrapper { + display: flex; + align-items: center; + gap: 4px; + + .input-toggle-container { + padding: 0; + gap: 4px; + + .label { + font-size: var(--font-size-small); + } + } + } + + .btn-wrapper { + @include flex-center; + gap: 2px; + padding: 4px 8px; + width: 64px; + background: var(--background-color); + border-radius: 20px; + height: fit-content; + cursor: pointer; + transition: all 0.2s; + outline: 1px solid transparent; + &:hover { + outline: 1px solid var(--border-color); + color: var(--accent-color); + } + &.hide { + width: 32px; + } + .icon { + width: 16px; + height: 16px; + @include flex-center; + } + } + } +} + .processDisplayer { border-radius: #{$border-radius-large}; outline: 1px solid var(--border-color); @@ -454,7 +544,7 @@ } } } - .storage-container{ + .storage-container { font-size: var(--font-size-tiny); color: var(--highlight-text-color); } diff --git a/app/src/styles/components/tools.scss b/app/src/styles/components/tools.scss index ba4bb9f..c07f11a 100644 --- a/app/src/styles/components/tools.scss +++ b/app/src/styles/components/tools.scss @@ -12,12 +12,16 @@ box-shadow: #{$box-shadow-medium}; border-radius: #{$border-radius-large}; width: fit-content; - transition: width 0.2s; background: var(--background-color); backdrop-filter: blur(20px); z-index: 2; outline: 1px solid var(--border-color); outline-offset: -1px; + transition: transform 0.4s ease-in-out 0.01s; + + &.visible { + transform: translate(-50%, -310px); + } .split { height: 20px; @@ -47,9 +51,11 @@ position: relative; &:hover { - background: color-mix(in srgb, - var(--highlight-accent-color) 60%, - transparent); + background: color-mix( + in srgb, + var(--highlight-accent-color) 60%, + transparent + ); .tooltip { opacity: 1; @@ -78,9 +84,11 @@ position: relative; &:hover { - background: color-mix(in srgb, - var(--highlight-accent-color) 60%, - transparent); + background: color-mix( + in srgb, + var(--highlight-accent-color) 60%, + transparent + ); } .drop-down-container { @@ -178,88 +186,6 @@ } -.tools-container { - transition: transform 0.4s ease-in-out 0.01s; - - &.visible { - transform: translate(-50%, -310px); - } -} - - -.controls-player-container { - width: 663px; - height: 30px; - border-radius: 15px; - - background: var(--background-color); - backdrop-filter: blur(20px); - cursor: pointer; - @include flex-center; - justify-content: space-between; - position: fixed; - bottom: 40px; - left: 50%; - transform: translate(-50%, 0); - color: var(--accent-color); - z-index: 100; - isolation: isolate; - font-weight: 700; - padding: 8px; - - .controls-left, - .controls-right { - display: flex; - align-items: center; - gap: 12px; - font-size: var(--font-size-small); - - .label { - text-transform: capitalize; - font-size: var(--font-size-small); - } - - .walkMode-wrapper { - display: flex; - align-items: center; - gap: 4px; - - - .input-toggle-container { - padding: 0; - gap: 4px; - - .label { - - font-size: var(--font-size-small); - } - } - } - - .btn-wrapper { - background: var(--background-color); - backdrop-filter: blur(8px); - display: flex; - align-items: center; - justify-content: space-between; - gap: 5px; - border-radius: 15px; - padding: 2px 8px; - color: var(--highlight-text-color); - cursor: pointer; - font-size: var(--font-size-small); - - .icon { - width: 16px; - height: 16px; - @include flex-center; - } - } - } - - -} - @keyframes pulse { 0% { opacity: 0; @@ -286,4 +212,4 @@ width: fit-content; opacity: 1; } -} \ No newline at end of file +} diff --git a/app/src/styles/pages/realTimeViz.scss b/app/src/styles/pages/realTimeViz.scss index ffe34f2..498938a 100644 --- a/app/src/styles/pages/realTimeViz.scss +++ b/app/src/styles/pages/realTimeViz.scss @@ -22,7 +22,6 @@ .floating { // width: calc(var(--realTimeViz-container-width) * 0.2px); - // transform: scale(min(1, calc(var(--realTimeViz-container-width) / 1000))); min-width: 230px; @@ -63,7 +62,6 @@ left: 50%; gap: 6px; border-radius: #{$border-radius-medium}; - overflow: auto; max-width: calc(100% - 500px); z-index: 3; @@ -71,7 +69,7 @@ pointer-events: all; transition: all 0.3s linear; - &.bottom { + &.bottom{ bottom: var(--bottomWidth); } @@ -122,7 +120,10 @@ } .zone-container.visualization-playing { - bottom: 70px; + bottom: 74px; + &.bottom{ + bottom: var(--bottomWidth); + } } .zone-wrapper.bottom { diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts new file mode 100644 index 0000000..adfdc78 --- /dev/null +++ b/app/src/types/builderTypes.d.ts @@ -0,0 +1,31 @@ +interface Asset { + modelUuid: string; + modelName: string; + assetId: string; + position: [number, number, number]; + rotation: [number, number, number]; + isLocked: boolean; + isCollidable: boolean; + isVisible: boolean; + opacity: number; + animations?: string[]; + animationState?: { + current: string; + playing: boolean; + }; + eventData?: { + type: string; + point?: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + } + points?: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + }[]; + } +}; + +type Assets = Asset[]; \ No newline at end of file diff --git a/app/src/utils/shortcutkeys/handleShortcutKeys.ts b/app/src/utils/shortcutkeys/handleShortcutKeys.ts index a446b2a..ab736bd 100644 --- a/app/src/utils/shortcutkeys/handleShortcutKeys.ts +++ b/app/src/utils/shortcutkeys/handleShortcutKeys.ts @@ -12,25 +12,25 @@ import { useToggleView, useToolMode, } from "../../store/builder/store"; -import { usePlayButtonStore } from "../../store/usePlayButtonStore"; +import useCameraModeStore, { usePlayButtonStore } from "../../store/usePlayButtonStore"; import { detectModifierKeys } from "./detectModifierKeys"; import { useSelectedZoneStore } from "../../store/visualization/useZoneStore"; const KeyPressListener: React.FC = () => { - const { activeModule, setActiveModule } = useModuleStore(); - const { setActiveSubTool } = useActiveSubTool(); - const { toggleUILeft, toggleUIRight, setToggleUI } = useToggleStore(); - const { setToggleThreeD } = useThreeDStore(); - const { setToolMode } = useToolMode(); - const { setIsPlaying } = usePlayButtonStore(); - const { toggleView, setToggleView } = useToggleView(); - const { setDeleteTool } = useDeleteTool(); - const { setAddAction } = useAddAction(); - const { setSelectedWallItem } = useSelectedWallItem(); - const { setActiveTool } = useActiveTool(); - const { clearSelectedZone } = useSelectedZoneStore(); - const { showShortcuts, setShowShortcuts } = useShortcutStore(); - const { setIsVersionSaved } = useSaveVersion(); + const { activeModule, setActiveModule } = useModuleStore(); + const { setActiveSubTool } = useActiveSubTool(); + const { toggleUILeft, toggleUIRight, setToggleUI } = useToggleStore(); + const { setToggleThreeD } = useThreeDStore(); + const { setToolMode } = useToolMode(); + const { isPlaying, setIsPlaying } = usePlayButtonStore(); + const { toggleView, setToggleView } = useToggleView(); + const { setDeleteTool } = useDeleteTool(); + const { setAddAction } = useAddAction(); + const { setSelectedWallItem } = useSelectedWallItem(); + const { setActiveTool } = useActiveTool(); + const { clearSelectedZone } = useSelectedZoneStore(); + const { showShortcuts, setShowShortcuts } = useShortcutStore(); + const { setWalkMode } = useCameraModeStore(); const isTextInput = (element: Element | null): boolean => element instanceof HTMLInputElement || @@ -67,8 +67,8 @@ const KeyPressListener: React.FC = () => { } }; - const handleBuilderShortcuts = (key: string) => { - if (activeModule !== "builder") return; + const handleBuilderShortcuts = (key: string) => { + if (activeModule !== "builder" || isPlaying) return; if (key === "TAB") { const toggleTo2D = toggleView; @@ -172,14 +172,14 @@ const KeyPressListener: React.FC = () => { setIsPlaying(true); } - if (keyCombination === "ESCAPE") { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setIsPlaying(false); - clearSelectedZone(); - setShowShortcuts(false); - setIsVersionSaved(false); - } + if (keyCombination === "ESCAPE") { + setWalkMode(false); + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setIsPlaying(false); + clearSelectedZone(); + setShowShortcuts(false); + } if (keyCombination === "Ctrl+Shift+?") { setShowShortcuts(!showShortcuts); @@ -195,11 +195,11 @@ const KeyPressListener: React.FC = () => { } }; - useEffect(() => { - window.addEventListener("keydown", handleKeyPress); - return () => window.removeEventListener("keydown", handleKeyPress); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeModule, toggleUIRight, toggleUILeft, toggleView, showShortcuts]); + useEffect(() => { + window.addEventListener("keydown", handleKeyPress); + return () => window.removeEventListener("keydown", handleKeyPress); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeModule, toggleUIRight, toggleUILeft, toggleView, showShortcuts, isPlaying]); return null; };