From 21ff522f67f31d4a1479b55d13d0e441a5bf336c Mon Sep 17 00:00:00 2001 From: Nalvazhuthi Date: Mon, 12 May 2025 18:01:45 +0530 Subject: [PATCH 01/24] Develop Ui for Version history --- app/src/components/footer/shortcutHelper.tsx | 7 + .../components/icons/ExportCommonIcons.tsx | 19 +++ .../layout/sidebarRight/SideBarRight.tsx | 41 ++++- .../versionHisory/VersionHistory.tsx | 116 +++++++++++++ app/src/components/ui/ModuleToggle.tsx | 6 + app/src/components/ui/menu/menu.tsx | 11 +- app/src/store/store.ts | 12 ++ app/src/styles/layout/sidebar.scss | 155 +++++++++++++++++- 8 files changed, 354 insertions(+), 13 deletions(-) create mode 100644 app/src/components/footer/shortcutHelper.tsx create mode 100644 app/src/components/layout/sidebarRight/versionHisory/VersionHistory.tsx diff --git a/app/src/components/footer/shortcutHelper.tsx b/app/src/components/footer/shortcutHelper.tsx new file mode 100644 index 0000000..af6dacd --- /dev/null +++ b/app/src/components/footer/shortcutHelper.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const ShortcutHelper = () => { + return
; +}; + +export default ShortcutHelper; diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index 9b66849..83505e9 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -947,3 +947,22 @@ export const ErrorIcon = () => { ); }; + +export const LocationIcon = () => { + return ( + + + + ); +}; diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx index 0cff6eb..953aa74 100644 --- a/app/src/components/layout/sidebarRight/SideBarRight.tsx +++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx @@ -13,7 +13,9 @@ import useToggleStore from "../../../store/useUIToggleStore"; import Visualization from "./visualization/Visualization"; import Analysis from "./analysis/Analysis"; import Simulations from "./simulation/Simulations"; -import { useSelectedFloorItem } from "../../../store/store"; +import useVersionHistoryStore, { + useSelectedFloorItem, +} from "../../../store/store"; import { useSelectedEventData, useSelectedEventSphere, @@ -22,6 +24,7 @@ import GlobalProperties from "./properties/GlobalProperties"; import AsstePropertiies from "./properties/AssetProperties"; import ZoneProperties from "./properties/ZoneProperties"; import EventProperties from "./properties/eventProperties/EventProperties"; +import VersionHistory from "./versionHisory/VersionHistory"; const SideBarRight: React.FC = () => { const { activeModule } = useModuleStore(); @@ -30,6 +33,7 @@ const SideBarRight: React.FC = () => { const { selectedFloorItem } = useSelectedFloorItem(); const { selectedEventData } = useSelectedEventData(); const { selectedEventSphere } = useSelectedEventSphere(); + const { viewVersionHistory, setVersionHistory } = useVersionHistoryStore(); // Reset activeList whenever activeModule changes useEffect(() => { @@ -64,7 +68,10 @@ const SideBarRight: React.FC = () => { className={`sidebar-action-list ${ subModule === "properties" ? "active" : "" }`} - onClick={() => setSubModule("properties")} + onClick={() => { + setSubModule("properties"); + setVersionHistory(false); + }} >
properties
@@ -76,7 +83,10 @@ const SideBarRight: React.FC = () => { className={`sidebar-action-list ${ subModule === "simulations" ? "active" : "" }`} - onClick={() => setSubModule("simulations")} + onClick={() => { + setSubModule("simulations"); + setVersionHistory(false); + }} >
simulations
@@ -85,7 +95,10 @@ const SideBarRight: React.FC = () => { className={`sidebar-action-list ${ subModule === "mechanics" ? "active" : "" }`} - onClick={() => setSubModule("mechanics")} + onClick={() => { + setSubModule("mechanics"); + setVersionHistory(false); + }} >
mechanics
@@ -94,7 +107,10 @@ const SideBarRight: React.FC = () => { className={`sidebar-action-list ${ subModule === "analysis" ? "active" : "" }`} - onClick={() => setSubModule("analysis")} + onClick={() => { + setSubModule("analysis"); + setVersionHistory(false); + }} >
analysis
@@ -103,8 +119,18 @@ const SideBarRight: React.FC = () => { )} )} + + {toggleUI && viewVersionHistory && ( +
+
+ +
+
+ )} + {/* process builder */} {toggleUI && + !viewVersionHistory && subModule === "properties" && activeModule !== "visualization" && !selectedFloorItem && ( @@ -115,6 +141,7 @@ const SideBarRight: React.FC = () => { )} {toggleUI && + !viewVersionHistory && subModule === "properties" && activeModule !== "visualization" && selectedFloorItem && ( @@ -124,7 +151,9 @@ const SideBarRight: React.FC = () => { )} + {toggleUI && + !viewVersionHistory && subModule === "zoneProperties" && (activeModule === "builder" || activeModule === "simulation") && (
@@ -134,7 +163,7 @@ const SideBarRight: React.FC = () => {
)} {/* simulation */} - {toggleUI && activeModule === "simulation" && ( + {toggleUI && !viewVersionHistory && activeModule === "simulation" && ( <> {subModule === "simulations" && (
diff --git a/app/src/components/layout/sidebarRight/versionHisory/VersionHistory.tsx b/app/src/components/layout/sidebarRight/versionHisory/VersionHistory.tsx new file mode 100644 index 0000000..9e49b48 --- /dev/null +++ b/app/src/components/layout/sidebarRight/versionHisory/VersionHistory.tsx @@ -0,0 +1,116 @@ +import React, { useState } from "react"; +import { + AddIcon, + CloseIcon, + KebabIcon, + LocationIcon, +} from "../../../icons/ExportCommonIcons"; + +const VersionHistory = () => { + // Start with only v1.0 + const initialVersions = [ + { + versionName: "v1.0", + timestamp: "April 09, 2025", + savedBy: "Nanni", + }, + ]; + + const [versions, setVersions] = useState(initialVersions); + const [selectedVersion, setSelectedVersion] = useState(initialVersions[0]); + const userName = localStorage.getItem("userName") ?? "Anonymous"; + + // Function to simulate adding a new version + const addNewVersion = () => { + const newVersionNumber = versions.length + 1; + const newVersion = { + versionName: `v${newVersionNumber}.0`, + timestamp: new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "2-digit", + }), + savedBy: userName, // Simulate user name + }; + + const updated = [newVersion, ...versions]; + setVersions(updated); + setSelectedVersion(newVersion); + }; + + // Handle user selecting a version + const handleSelectVersion = (version: any) => { + setSelectedVersion(version); + const reordered = [version, ...versions.filter((v) => v !== version)]; + setVersions(reordered); + }; + + return ( +
+ {/* Header */} +
+
Version History
+
+ +
+ +
+
+ +
+
+
+ + {/* Shortcut Info */} +
+
i
+
+ Press Ctrl + Alt + S to add to version history while editing +
+
+ + {/* Current Version Display */} +
+
+ +
+
+
+ Current Version ({selectedVersion.versionName}) +
+
+ {versions.length} Saved History +
+
+
+ + {/* Versions List */} +
+ {versions.map((version, index) => ( + + ))} +
+
+ ); +}; + +export default VersionHistory; diff --git a/app/src/components/ui/ModuleToggle.tsx b/app/src/components/ui/ModuleToggle.tsx index a1583e1..63a11ee 100644 --- a/app/src/components/ui/ModuleToggle.tsx +++ b/app/src/components/ui/ModuleToggle.tsx @@ -7,10 +7,12 @@ import { VisualizationIcon, } from "../icons/ExportModuleIcons"; import useToggleStore from "../../store/useUIToggleStore"; +import useVersionHistoryStore from "../../store/store"; const ModuleToggle: React.FC = () => { const { activeModule, setActiveModule } = useModuleStore(); const { setToggleUI } = useToggleStore(); + const { setVersionHistory } = useVersionHistoryStore(); return (
@@ -18,6 +20,7 @@ const ModuleToggle: React.FC = () => { className={`module-list ${activeModule === "builder" ? "active" : ""}`} onClick={() => { setActiveModule("builder"); + setVersionHistory(false); setToggleUI( localStorage.getItem("navBarUi") ? localStorage.getItem("navBarUi") === "true" @@ -36,6 +39,7 @@ const ModuleToggle: React.FC = () => { }`} onClick={() => { setActiveModule("simulation"); + setVersionHistory(false); setToggleUI( localStorage.getItem("navBarUi") ? localStorage.getItem("navBarUi") === "true" @@ -54,6 +58,7 @@ const ModuleToggle: React.FC = () => { }`} onClick={() => { setActiveModule("visualization"); + setVersionHistory(false); setToggleUI( localStorage.getItem("navBarUi") ? localStorage.getItem("navBarUi") === "true" @@ -70,6 +75,7 @@ const ModuleToggle: React.FC = () => { className={`module-list ${activeModule === "market" ? "active" : ""}`} onClick={() => { setActiveModule("market"); + setVersionHistory(false); setToggleUI(false); }} > diff --git a/app/src/components/ui/menu/menu.tsx b/app/src/components/ui/menu/menu.tsx index 39508b6..c5f24a9 100644 --- a/app/src/components/ui/menu/menu.tsx +++ b/app/src/components/ui/menu/menu.tsx @@ -1,6 +1,8 @@ import React, { useState } from "react"; import { ArrowIcon } from "../../icons/ExportCommonIcons"; import { toggleTheme } from "../../../utils/theme"; +import useVersionHistoryStore from "../../../store/store"; +import { useSubModuleStore } from "../../../store/useModuleStore"; interface MenuBarProps { setOpenMenu: (isOpen: boolean) => void; // Function to update menu state @@ -10,6 +12,9 @@ const MenuBar: React.FC = ({ setOpenMenu }) => { const [activeMenu, setActiveMenu] = useState(null); const [activeSubMenu, setActiveSubMenu] = useState(null); + const { viewVersionHistory, setVersionHistory } = useVersionHistoryStore(); + const { subModule, setSubModule } = useSubModuleStore(); + // State to track selection for all menu items const [selectedItems, setSelectedItems] = useState>( {} @@ -23,7 +28,7 @@ const MenuBar: React.FC = ({ setOpenMenu }) => { })); }; - function handleThemeChange(){ + function handleThemeChange() { toggleTheme(); window.location.reload(); } @@ -373,6 +378,10 @@ const MenuBar: React.FC = ({ setOpenMenu }) => { setActiveMenu(null); setActiveSubMenu(null); }} + onClick={() => { + setVersionHistory(true); + setSubModule("properties"); + }} >
Version history
diff --git a/app/src/store/store.ts b/app/src/store/store.ts index 61d84d5..35ccdd5 100644 --- a/app/src/store/store.ts +++ b/app/src/store/store.ts @@ -435,3 +435,15 @@ export const useZoneAssetId = create((set) => ({ zoneAssetId: null, setZoneAssetId: (asset) => set({ zoneAssetId: asset }), })); + +interface VersionHistoryState { + viewVersionHistory: boolean; + setVersionHistory: (value: boolean) => void; +} + +const useVersionHistoryStore = create((set) => ({ + viewVersionHistory: false, + setVersionHistory: (value) => set({ viewVersionHistory: value }), +})); + +export default useVersionHistoryStore; diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss index 4d321d4..efa77a3 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/sidebar.scss @@ -456,6 +456,144 @@ position: relative; width: 304px; + .version-history-container { + max-height: calc(62vh - 12px); + display: flex; + flex-direction: column; + padding: 0 8px; + gap: 10px; + + .version-history-header { + display: flex; + justify-content: space-between; + align-items: center; + + .version-history-icons { + display: flex; + align-items: center; + gap: 6px; + + .kebab-icon { + transform: rotate(90deg); + } + } + } + + .version-history-shortcut-info { + display: flex; + gap: 6px; + border: 1px solid var(--border-color); + + background: var(--background-color); + padding: 10px 8px; + border-radius: 13px; + + .info-icon { + width: 12px; + height: 12px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + border: 1px solid var(--border-color); + padding: 4px; + font-size: 10px; + } + + .shortcut-text { + color: var(--text-disabled); + } + } + + .version-history-location { + display: flex; + align-items: center; + gap: 6px; + + .location-details { + display: flex; + flex-direction: column; + gap: 4px; + + .saved-history-count { + font-size: var(--font-size-tiny) + } + } + } + + .saved-versions-list { + padding-top: 16px; + display: flex; + flex-direction: column; + gap: 20px; + + .saved-version { + display: flex; + align-items: center; + gap: 12px; + + .version-name { + + background: var(--background-color); + border: 1px solid var(--border-color); + color: var(--text-color); + border-radius: 13px; + padding: 4px 8px; + position: relative; + /* Ensure position relative for ::after */ + } + + &:not(:first-child) .version-name::after { + content: ""; + position: absolute; + top: -35px; + left: 50%; + transform: translateX(-50%); + width: 1px; + height: 32px; + background-color: var(--text-disabled); + } + + .version-details { + display: flex; + flex-direction: column; + gap: 6px; + + .saved-by { + display: flex; + align-items: center; + gap: 6px; + + .user-profile { + + background: var(--background-color-accent); + color: var(--text-button-color); + width: 20px; + height: 20px; + border-radius: 50%; + + display: flex; + justify-content: center; + align-items: center; + text-transform: uppercase; + } + + .user-name { + text-transform: capitalize; + + } + } + + .timestamp { + text-align: start; + } + } + } + } + + + } + .no-event-selected { color: #666; padding: 16px; @@ -513,6 +651,7 @@ max-height: 60vh; .sidebar-right-content-container { + .dataSideBar { .inputs-wrapper { display: flex; @@ -995,6 +1134,7 @@ margin: 6px 0; padding-left: 16px; position: relative; + &::after { content: "↶"; rotate: -90deg; @@ -1009,6 +1149,7 @@ top: 0; left: 4px; } + &:last-child { &::after { display: none; @@ -1462,11 +1603,9 @@ width: 100%; height: 100%; font-size: var(--font-size-regular); - background: linear-gradient( - 0deg, - rgba(37, 24, 51, 0) 0%, - rgba(52, 41, 61, 0.5) 100% - ); + background: linear-gradient(0deg, + rgba(37, 24, 51, 0) 0%, + rgba(52, 41, 61, 0.5) 100%); pointer-events: none; backdrop-filter: blur(8px); opacity: 0; @@ -1519,6 +1658,7 @@ .sidebar-right-wrapper.open { height: fit-content; animation: openSidebar 0.2s linear; + .sidebar-right-container, .sidebar-left-container { opacity: 0; @@ -1530,6 +1670,7 @@ from { opacity: 0; } + to { opacity: 1; } @@ -1539,6 +1680,7 @@ from { height: 60%; } + to { height: 52px; } @@ -1548,7 +1690,8 @@ from { height: 52px; } + to { height: 60%; } -} +} \ No newline at end of file -- 2.49.1 From f16c57a65fe702cedfa2316bcbaf9bc8733e859f Mon Sep 17 00:00:00 2001 From: Poovizhi99 Date: Tue, 13 May 2025 09:43:03 +0530 Subject: [PATCH 02/24] changed the draw mode ui visibility as per figma --- .../builder/groups/calculateAreaGroup.tsx | 130 ++++++++++++------ app/src/modules/builder/groups/zoneGroup.tsx | 1 - 2 files changed, 85 insertions(+), 46 deletions(-) diff --git a/app/src/modules/builder/groups/calculateAreaGroup.tsx b/app/src/modules/builder/groups/calculateAreaGroup.tsx index e50ae4b..4db1389 100644 --- a/app/src/modules/builder/groups/calculateAreaGroup.tsx +++ b/app/src/modules/builder/groups/calculateAreaGroup.tsx @@ -4,62 +4,102 @@ import { computeArea } from '../functions/computeArea'; import { Html } from '@react-three/drei'; import * as CONSTANTS from "../../../types/world/worldConstants"; import * as turf from '@turf/turf'; +import * as THREE from "three" const CalculateAreaGroup = () => { - const { roomsState } = useRoomsState(); - const { toggleView } = useToggleView(); + const { roomsState } = useRoomsState(); + const { toggleView } = useToggleView(); + const savedTheme: string | null = localStorage.getItem('theme'); - return ( - - {roomsState.length > 0 && - roomsState.flat().map((room: any, index: number) => { - if (!toggleView) return null; - const coordinates = room.coordinates; + return ( + + + {roomsState.length > 0 && + roomsState.flat().map((room: any, index: number) => { + const coordinates = room.coordinates; + if (!coordinates || coordinates.length < 3) return null; - if (!coordinates || coordinates.length < 3) return null; + const yPos = (room.layer || 0) * CONSTANTS.zoneConfig.height; + const coords2D = coordinates.map((p: any) => new THREE.Vector2(p.position.x, p.position.z)); + // console.log('coords2D: ', coords2D); - let coords2D = coordinates.map((p: any) => [p.position.x, p.position.z]); + if (!coords2D[0].equals(coords2D[coords2D.length - 1])) { + coords2D.push(coords2D[0]); + } - const first = coords2D[0]; - const last = coords2D[coords2D.length - 1]; - if (first[0] !== last[0] || first[1] !== last[1]) { - coords2D.push(first); - } + const shape = new THREE.Shape(coords2D); + const extrudeSettings = { + depth: 0.01, + bevelEnabled: false, + }; - const polygon = turf.polygon([coords2D]); - const center2D = turf.center(polygon).geometry.coordinates; + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + geometry.rotateX(Math.PI / 2); - const sumY = coordinates.reduce((sum: number, p: any) => sum + p.position.y, 0); - const avgY = sumY / coordinates.length; + const material = new THREE.MeshBasicMaterial({ + color: savedTheme === "dark" ? "#d2baff" : '#6f42c1', + side: THREE.DoubleSide, + transparent: true, + opacity: 0.4, + depthWrite: false, + }); - const area = computeArea(room, "rooms"); - const formattedArea = `${area.toFixed(2)} m²`; + return ( + + + + ); + })} + + {roomsState.length > 0 && + roomsState.flat().map((room: any, index: number) => { + if (!toggleView) return null; + const coordinates = room.coordinates; - const htmlPosition: [number, number, number] = [ - center2D[0], - avgY + CONSTANTS.zoneConfig.height, - center2D[1], - ]; + if (!coordinates || coordinates.length < 3) return null; - return ( - -
- Room ({formattedArea}) -
- - ); - })} -
- ); + let coords2D = coordinates.map((p: any) => [p.position.x, p.position.z]); + + const first = coords2D[0]; + const last = coords2D[coords2D.length - 1]; + if (first[0] !== last[0] || first[1] !== last[1]) { + coords2D.push(first); + } + + const polygon = turf.polygon([coords2D]); + const center2D = turf.center(polygon).geometry.coordinates; + + const sumY = coordinates.reduce((sum: number, p: any) => sum + p.position.y, 0); + const avgY = sumY / coordinates.length; + + const area = computeArea(room, "rooms"); + const formattedArea = `${area.toFixed(2)} m²`; + + const htmlPosition: [number, number, number] = [ + center2D[0], + avgY + CONSTANTS.zoneConfig.height, + center2D[1], + ]; + + return ( + +
+ Room ({formattedArea}) +
+ + ); + })} +
+ ); } export default CalculateAreaGroup; diff --git a/app/src/modules/builder/groups/zoneGroup.tsx b/app/src/modules/builder/groups/zoneGroup.tsx index e6c0f1a..ff3a4a3 100644 --- a/app/src/modules/builder/groups/zoneGroup.tsx +++ b/app/src/modules/builder/groups/zoneGroup.tsx @@ -376,7 +376,6 @@ const ZoneGroup: React.FC = () => { setZonePoints(updatedZonePoints); addZoneToBackend(newZone); - setStartPoint(null); setEndPoint(null); } -- 2.49.1 From ecab03c5f0df3ec9b2dd523c74a2fa21e2e9fb19 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Tue, 13 May 2025 12:58:04 +0530 Subject: [PATCH 03/24] Refactor MoveControls and TransformControls for improved asset manipulation - Cleaned up MoveControls component by removing unused variables and optimizing event handling. - Enhanced asset movement logic with better key event detection and state management. - Removed deprecated transform mode state from store. - Updated worldTypes to remove unnecessary positionY property. - Introduced TransformControls component for handling object transformations (translate/rotate) with proper state management and backend updates. - Implemented event handling for mouse actions and keyboard shortcuts to toggle transformation modes. --- .../components/layout/sidebarLeft/Assets.tsx | 18 +- app/src/components/ui/Tools.tsx | 17 - app/src/modules/builder/builder.tsx | 449 ++++++------ app/src/modules/builder/csg/csg.tsx | 78 +-- .../geomentries/floors/addFloorToScene.ts | 1 - .../builder/geomentries/walls/addWallItems.ts | 2 +- .../builder/groups/floorItemsGroup.tsx | 19 +- .../modules/builder/groups/wallItemsGroup.tsx | 502 +++++++------- .../builder/groups/wallsAndWallItems.tsx | 139 ++-- app/src/modules/builder/groups/wallsMesh.tsx | 100 +-- app/src/modules/scene/controls/controls.tsx | 3 + .../distanceFindingControls.tsx | 486 ++++++------- .../selectionControls/moveControls.tsx | 653 +++++++++--------- .../transformControls/transformControls.tsx | 206 ++++++ app/src/store/store.ts | 5 - app/src/types/world/worldTypes.d.ts | 1 - 16 files changed, 1401 insertions(+), 1278 deletions(-) create mode 100644 app/src/modules/scene/controls/transformControls/transformControls.tsx diff --git a/app/src/components/layout/sidebarLeft/Assets.tsx b/app/src/components/layout/sidebarLeft/Assets.tsx index b863500..d0aaef2 100644 --- a/app/src/components/layout/sidebarLeft/Assets.tsx +++ b/app/src/components/layout/sidebarLeft/Assets.tsx @@ -234,14 +234,16 @@ const Assets: React.FC = () => { alt={asset.filename} className="asset-image" onPointerDown={() => { - setSelectedItem({ - name: asset.filename, - id: asset.AssetID, - type: - asset.type === "undefined" - ? undefined - : asset.type, - }); + if (asset.category !== 'Feneration') { + setSelectedItem({ + name: asset.filename, + id: asset.AssetID, + type: + asset.type === "undefined" + ? undefined + : asset.type, + }); + } }} />
diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index 2187fdc..40fec27 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -29,7 +29,6 @@ import { useSocketStore, useToggleView, useToolMode, - useTransformMode, useActiveSubTool, } from "../../store/store"; import useToggleStore from "../../store/useUIToggleStore"; @@ -61,7 +60,6 @@ const Tools: React.FC = () => { const { setAddAction } = useAddAction(); const { setSelectedWallItem } = useSelectedWallItem(); - const { setTransformMode } = useTransformMode(); const { setDeletePointOrLine } = useDeletePointOrLine(); const { setToolMode } = useToolMode(); const { activeTool, setActiveTool } = useActiveTool(); @@ -126,7 +124,6 @@ const Tools: React.FC = () => { setToolMode(null); setDeleteTool(false); setAddAction(null); - setTransformMode(null); setDeletePointOrLine(false); setRefTextUpdate((prevUpdate) => prevUpdate - 1); @@ -134,20 +131,6 @@ const Tools: React.FC = () => { case "cursor": if (toggleView) { setToolMode('move'); - } else { - setTransformMode("translate"); - } - break; - - case "Rotate": - if (!toggleView) { - setTransformMode("rotate"); - } - break; - - case "Scale": - if (!toggleView) { - setTransformMode("scale"); } break; diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index dfae129..f43cf3d 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -54,57 +54,57 @@ import NavMesh from "../simulation/vehicle/navMesh/navMesh"; import CalculateAreaGroup from "./groups/calculateAreaGroup"; export default function Builder() { - const state = useThree(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements. - const csg = useRef(); // Reference for CSG object, used for 3D modeling. - const CSGGroup = useRef() as Types.RefMesh; // Reference to a group of CSG objects. - const scene = useRef() as Types.RefScene; // Reference to the scene. - const camera = useRef() as Types.RefCamera; // Reference to the camera object. - const controls = useRef(); // Reference to the controls object. - const raycaster = useRef() as Types.RefRaycaster; // Reference for raycaster used for detecting objects being pointed at in the scene. - const dragPointControls = useRef() as Types.RefDragControl; // Reference for drag point controls, an array for drag control. + const state = useThree(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements. + const csg = useRef(); // Reference for CSG object, used for 3D modeling. + const CSGGroup = useRef() as Types.RefMesh; // Reference to a group of CSG objects. + const scene = useRef() as Types.RefScene; // Reference to the scene. + const camera = useRef() as Types.RefCamera; // Reference to the camera object. + const controls = useRef(); // Reference to the controls object. + const raycaster = useRef() as Types.RefRaycaster; // Reference for raycaster used for detecting objects being pointed at in the scene. + const dragPointControls = useRef() as Types.RefDragControl; // Reference for drag point controls, an array for drag control. - // Assigning the scene and camera from the Three.js state to the references. + // Assigning the scene and camera from the Three.js state to the references. - scene.current = state.scene; - camera.current = state.camera; - controls.current = state.controls; - raycaster.current = state.raycaster; + scene.current = state.scene; + camera.current = state.camera; + controls.current = state.controls; + raycaster.current = state.raycaster; - const plane = useRef(null); // Reference for a plane object for raycaster reference. - const grid = useRef() as any; // Reference for a grid object for raycaster reference. - const snappedPoint = useRef() as Types.RefVector3; // Reference for storing a snapped point at the (end = isSnapped) and (start = ispreSnapped) of the line. - const isSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (end). - const anglesnappedPoint = useRef() as Types.RefVector3; // Reference for storing an angle-snapped point when the line is in 90 degree etc... - const isAngleSnapped = useRef(false) as Types.RefBoolean; // Boolean to indicate if angle snapping is active. - const isSnappedUUID = useRef() as Types.RefString; // UUID reference to identify the snapped point. - const ispreSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (start). - const tempLoader = useRef() as Types.RefMesh; // Reference for a temporary loader for the floor items. - const isTempLoader = useRef() as Types.RefBoolean; // Reference to check if a temporary loader is active. - const Tube = useRef() as Types.RefTubeGeometry; // Reference for tubes used for reference line creation and updation. - const line = useRef([]) as Types.RefLine; // Reference for line which stores the current line that is being drawn. - const lines = useRef([]) as Types.RefLines; // Reference for lines which stores all the lines that are ever drawn. - const onlyFloorline = useRef([]); // Reference for floor lines which does not have walls or roof and have only floor used to store the current line that is being drawn. - const onlyFloorlines = useRef([]); // Reference for all the floor lines that are ever drawn. - const ReferenceLineMesh = useRef() as Types.RefMesh; // Reference for storing the mesh of the reference line for moving it during draw. - const LineCreated = useRef(false) as Types.RefBoolean; // Boolean to track whether the reference line is created or not. - const referencePole = useRef() as Types.RefMesh; // Reference for a pole that is used as the reference for the user to show where it is placed. - const itemsGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the floor items (Gltf). - const floorGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the roofs and the floors. - const AttachedObject = useRef() as Types.RefMesh; // Reference for an object that is attached using dbl click for transform controls rotation. - const floorPlanGroup = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines group and the points group. - const floorPlanGroupLine = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines that are drawn. - const floorPlanGroupPoint = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the points that are created. - const floorGroupAisle = useRef() as Types.RefGroup; - const zoneGroup = useRef() as Types.RefGroup; - const currentLayerPoint = useRef([]) as Types.RefMeshArray; // Reference for points that re in the current layer used to update the points in drag controls. - const hoveredDeletablePoint = useRef() as Types.RefMesh; // Reference for the currently hovered point that can be deleted. - const hoveredDeletableLine = useRef() as Types.RefMesh; // Reference for the currently hovered line that can be deleted. - const hoveredDeletableFloorItem = useRef() as Types.RefMesh; // Reference for the currently hovered floor item that can be deleted. - const hoveredDeletableWallItem = useRef() as Types.RefMesh; // Reference for the currently hovered wall item that can be deleted. - const hoveredDeletablePillar = useRef() as Types.RefMesh; // Reference for the currently hovered pillar that can be deleted. - const currentWallItem = useRef() as Types.RefMesh; // Reference for the currently selected wall item that can be scaled, dragged etc... + const plane = useRef(null); // Reference for a plane object for raycaster reference. + const grid = useRef() as any; // Reference for a grid object for raycaster reference. + const snappedPoint = useRef() as Types.RefVector3; // Reference for storing a snapped point at the (end = isSnapped) and (start = ispreSnapped) of the line. + const isSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (end). + const anglesnappedPoint = useRef() as Types.RefVector3; // Reference for storing an angle-snapped point when the line is in 90 degree etc... + const isAngleSnapped = useRef(false) as Types.RefBoolean; // Boolean to indicate if angle snapping is active. + const isSnappedUUID = useRef() as Types.RefString; // UUID reference to identify the snapped point. + const ispreSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (start). + const tempLoader = useRef() as Types.RefMesh; // Reference for a temporary loader for the floor items. + const isTempLoader = useRef() as Types.RefBoolean; // Reference to check if a temporary loader is active. + const Tube = useRef() as Types.RefTubeGeometry; // Reference for tubes used for reference line creation and updation. + const line = useRef([]) as Types.RefLine; // Reference for line which stores the current line that is being drawn. + const lines = useRef([]) as Types.RefLines; // Reference for lines which stores all the lines that are ever drawn. + const onlyFloorline = useRef([]); // Reference for floor lines which does not have walls or roof and have only floor used to store the current line that is being drawn. + const onlyFloorlines = useRef([]); // Reference for all the floor lines that are ever drawn. + const ReferenceLineMesh = useRef() as Types.RefMesh; // Reference for storing the mesh of the reference line for moving it during draw. + const LineCreated = useRef(false) as Types.RefBoolean; // Boolean to track whether the reference line is created or not. + const referencePole = useRef() as Types.RefMesh; // Reference for a pole that is used as the reference for the user to show where it is placed. + const itemsGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the floor items (Gltf). + const floorGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the roofs and the floors. + const AttachedObject = useRef() as Types.RefMesh; // Reference for an object that is attached using dbl click for transform controls rotation. + const floorPlanGroup = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines group and the points group. + const floorPlanGroupLine = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines that are drawn. + const floorPlanGroupPoint = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the points that are created. + const floorGroupAisle = useRef() as Types.RefGroup; + const zoneGroup = useRef() as Types.RefGroup; + const currentLayerPoint = useRef([]) as Types.RefMeshArray; // Reference for points that re in the current layer used to update the points in drag controls. + const hoveredDeletablePoint = useRef() as Types.RefMesh; // Reference for the currently hovered point that can be deleted. + const hoveredDeletableLine = useRef() as Types.RefMesh; // Reference for the currently hovered line that can be deleted. + const hoveredDeletableFloorItem = useRef() as Types.RefMesh; // Reference for the currently hovered floor item that can be deleted. + const hoveredDeletableWallItem = useRef() as Types.RefMesh; // Reference for the currently hovered wall item that can be deleted. + const hoveredDeletablePillar = useRef() as Types.RefMesh; // Reference for the currently hovered pillar that can be deleted. + const currentWallItem = useRef() as Types.RefMesh; // Reference for the currently selected wall item that can be scaled, dragged etc... - const cursorPosition = new THREE.Vector3(); // 3D vector for storing the cursor position. + const cursorPosition = new THREE.Vector3(); // 3D vector for storing the cursor position. const [selectedItemsIndex, setSelectedItemsIndex] = useState(null); // State for tracking the index of the selected item. const { activeLayer } = useActiveLayer(); // State that changes based on which layer the user chooses in Layers.jsx. @@ -120,42 +120,39 @@ export default function Builder() { const { setWalls } = useWalls(); const { refTextupdate, setRefTextUpdate } = useRefTextUpdate(); - // const loader = new GLTFLoader(); - // const dracoLoader = new DRACOLoader(); + // 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); + // dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); + // loader.setDRACOLoader(dracoLoader); - ////////// Assest Configuration Values ////////// + ////////// Assest Configuration Values ////////// - const AssetConfigurations: Types.AssetConfigurations = { - arch: { - modelUrl: arch, - scale: [0.75, 0.75, 0.75], - csgscale: [2, 4, 0.5], - csgposition: [0, 2, 0], - positionY: () => 0, - type: "Fixed-Move", - }, - door: { - modelUrl: door, - scale: [0.75, 0.75, 0.75], - csgscale: [2, 4, 0.5], - csgposition: [0, 2, 0], - positionY: () => 0, - type: "Fixed-Move", - }, - window: { - modelUrl: Window, - scale: [0.75, 0.75, 0.75], - csgscale: [5, 3, 0.5], - csgposition: [0, 1.5, 0], - positionY: (intersectionPoint) => intersectionPoint.point.y, - type: "Free-Move", - }, - }; + const AssetConfigurations: Types.AssetConfigurations = { + arch: { + modelUrl: arch, + scale: [0.75, 0.75, 0.75], + csgscale: [2, 4, 0.5], + csgposition: [0, 2, 0], + type: "Fixed-Move", + }, + door: { + modelUrl: door, + scale: [0.75, 0.75, 0.75], + csgscale: [2, 4, 0.5], + csgposition: [0, 2, 0], + type: "Fixed-Move", + }, + window: { + modelUrl: Window, + scale: [0.75, 0.75, 0.75], + csgscale: [5, 3, 0.5], + csgposition: [0, 1.5, 0], + type: "Free-Move", + }, + }; - ////////// All Toggle's ////////// + ////////// All Toggle's ////////// useEffect(() => { setRefTextUpdate((prevUpdate: number) => prevUpdate - 1); @@ -177,167 +174,167 @@ export default function Builder() { } }, [toggleView]); - useEffect(() => { - THREE.Cache.clear(); - THREE.Cache.enabled = true; - }, []); + useEffect(() => { + THREE.Cache.clear(); + THREE.Cache.enabled = true; + }, []); - useEffect(() => { - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; + useEffect(() => { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - async function fetchVisibility() { - const visibility = await findEnvironment( - organization, - localStorage.getItem("userId")! - ); - if (visibility) { - setRoofVisibility(visibility.roofVisibility); - setWallVisibility(visibility.wallVisibility); - setShadows(visibility.shadowVisibility); - setRenderDistance(visibility.renderDistance); - setLimitDistance(visibility.limitDistance); - } - } - fetchVisibility(); - }, []); + async function fetchVisibility() { + const visibility = await findEnvironment( + organization, + localStorage.getItem("userId")! + ); + if (visibility) { + setRoofVisibility(visibility.roofVisibility); + setWallVisibility(visibility.wallVisibility); + setShadows(visibility.shadowVisibility); + setRenderDistance(visibility.renderDistance); + setLimitDistance(visibility.limitDistance); + } + } + fetchVisibility(); + }, []); - ////////// UseFrame is Here ////////// + ////////// UseFrame is Here ////////// - useFrame(() => { - if (toolMode) { - Draw( - state, - plane, - cursorPosition, - floorPlanGroupPoint, - floorPlanGroupLine, - snappedPoint, - isSnapped, - isSnappedUUID, - line, - lines, - ispreSnapped, - floorPlanGroup, - ReferenceLineMesh, - LineCreated, - setRefTextUpdate, - Tube, - anglesnappedPoint, - isAngleSnapped, - toolMode - ); - } - }); + useFrame(() => { + if (toolMode) { + Draw( + state, + plane, + cursorPosition, + floorPlanGroupPoint, + floorPlanGroupLine, + snappedPoint, + isSnapped, + isSnappedUUID, + line, + lines, + ispreSnapped, + floorPlanGroup, + ReferenceLineMesh, + LineCreated, + setRefTextUpdate, + Tube, + anglesnappedPoint, + isAngleSnapped, + toolMode + ); + } + }); - ////////// Return ////////// + ////////// Return ////////// - return ( - <> - + return ( + <> + - + - + - + - + - + - + - + - + - + - - - - - ); + + + + + ); } diff --git a/app/src/modules/builder/csg/csg.tsx b/app/src/modules/builder/csg/csg.tsx index 7e49598..eab8edc 100644 --- a/app/src/modules/builder/csg/csg.tsx +++ b/app/src/modules/builder/csg/csg.tsx @@ -4,51 +4,51 @@ import { useDeleteTool } from "../../../store/store"; import { useRef } from "react"; export interface CsgProps { - position: THREE.Vector3 | [number, number, number]; - scale: THREE.Vector3 | [number, number, number]; - model: THREE.Object3D; - hoveredDeletableWallItem: { current: THREE.Mesh | null }; + position: THREE.Vector3 | [number, number, number]; + scale: THREE.Vector3 | [number, number, number]; + model: THREE.Object3D; + hoveredDeletableWallItem: { current: THREE.Mesh | null }; } export const Csg: React.FC = (props) => { - const { deleteTool } = useDeleteTool(); - const modelRef = useRef(); - const originalMaterials = useRef>(new Map()); + const { deleteTool } = useDeleteTool(); + const modelRef = useRef(); + const originalMaterials = useRef>(new Map()); - const handleHover = (hovered: boolean, object: THREE.Mesh | null) => { - if (modelRef.current && deleteTool) { - modelRef.current.traverse((child) => { - if (child instanceof THREE.Mesh) { - if (!originalMaterials.current.has(child)) { - originalMaterials.current.set(child, child.material); - } - child.material = child.material.clone(); - child.material.color.set(hovered && deleteTool ? 0xff0000 : (originalMaterials.current.get(child) as any).color); + const handleHover = (hovered: boolean, object: THREE.Mesh | null) => { + if (modelRef.current && deleteTool) { + modelRef.current.traverse((child) => { + if (child instanceof THREE.Mesh) { + if (!originalMaterials.current.has(child)) { + originalMaterials.current.set(child, child.material); + } + child.material = child.material.clone(); + child.material.color.set(hovered && deleteTool ? 0xff0000 : (originalMaterials.current.get(child) as any).color); + } + }); } - }); - } - props.hoveredDeletableWallItem.current = hovered ? object : null; - }; + props.hoveredDeletableWallItem.current = hovered ? object : null; + }; - return ( - - + return ( - + + + + + + { + e.stopPropagation(); + handleHover(true, e.object.parent); + }} + onPointerOut={(e: any) => { + e.stopPropagation(); + handleHover(false, null); + }} + /> - - { - e.stopPropagation(); - handleHover(true, e.object.parent); - }} - onPointerOut={(e: any) => { - e.stopPropagation(); - handleHover(false, null); - }} - /> - - ); + ); }; diff --git a/app/src/modules/builder/geomentries/floors/addFloorToScene.ts b/app/src/modules/builder/geomentries/floors/addFloorToScene.ts index 7ee2ceb..71f2d24 100644 --- a/app/src/modules/builder/geomentries/floors/addFloorToScene.ts +++ b/app/src/modules/builder/geomentries/floors/addFloorToScene.ts @@ -26,7 +26,6 @@ export default function addFloorToScene( if (materialCache.has(materialKey)) { material = materialCache.get(materialKey) as THREE.Material; - // } else { } else { const floorTexture = textureLoader.load(savedTheme === "dark" ? texturePathDark : texturePath); // const floorTexture = textureLoader.load(texturePath); diff --git a/app/src/modules/builder/geomentries/walls/addWallItems.ts b/app/src/modules/builder/geomentries/walls/addWallItems.ts index fd9eb48..8b472ec 100644 --- a/app/src/modules/builder/geomentries/walls/addWallItems.ts +++ b/app/src/modules/builder/geomentries/walls/addWallItems.ts @@ -35,7 +35,7 @@ async function AddWallItems( }); const config = AssetConfigurations[selected]; - let positionY = typeof config.positionY === 'function' ? config.positionY(intersectionPoint) : config.positionY; + let positionY = config.type === 'Fixed-Move' ? 0 : intersectionPoint.point.y; if (positionY === 0) { positionY = Math.floor(intersectionPoint.point.y / CONSTANTS.wallConfig.height) * CONSTANTS.wallConfig.height; } diff --git a/app/src/modules/builder/groups/floorItemsGroup.tsx b/app/src/modules/builder/groups/floorItemsGroup.tsx index 206d00d..e58530b 100644 --- a/app/src/modules/builder/groups/floorItemsGroup.tsx +++ b/app/src/modules/builder/groups/floorItemsGroup.tsx @@ -11,7 +11,6 @@ import { useSelectedItem, useSocketStore, useToggleView, - useTransformMode, } from "../../../store/store"; import { useEffect } from "react"; import * as THREE from "three"; @@ -59,7 +58,6 @@ const FloorItemsGroup = ({ const { camMode } = useCamMode(); const { deleteTool } = useDeleteTool(); const { setDeletableFloorItem } = useDeletableFloorItem(); - const { transformMode } = useTransformMode(); const { setSelectedFloorItem } = useSelectedFloorItem(); const { activeTool } = useActiveTool(); const { selectedItem, setSelectedItem } = useSelectedItem(); @@ -257,9 +255,8 @@ const FloorItemsGroup = ({ socket ); } - const Mode = transformMode; - if (Mode !== null || activeTool === "cursor") { + if (activeTool === "cursor") { if (!itemsGroup.current) return; let intersects = raycaster.intersectObjects( itemsGroup.current.children, @@ -296,9 +293,8 @@ const FloorItemsGroup = ({ isLeftMouseDown = false; if (drag) return; - const Mode = transformMode; - if (Mode !== null || activeTool === "cursor") { + if (activeTool === "cursor") { if (!itemsGroup.current) return; let intersects = raycaster.intersectObjects( itemsGroup.current.children, @@ -412,16 +408,7 @@ const FloorItemsGroup = ({ canvasElement.removeEventListener("drop", onDrop); canvasElement.removeEventListener("dragover", onDragOver); }; - }, [ - deleteTool, - transformMode, - controls, - selectedItem, - state.camera, - state.pointer, - activeTool, - activeModule, - ]); + }, [deleteTool, controls, selectedItem, state.camera, state.pointer, activeTool, activeModule,]); useFrame(() => { if (controls) diff --git a/app/src/modules/builder/groups/wallItemsGroup.tsx b/app/src/modules/builder/groups/wallItemsGroup.tsx index 76ba502..faae06d 100644 --- a/app/src/modules/builder/groups/wallItemsGroup.tsx +++ b/app/src/modules/builder/groups/wallItemsGroup.tsx @@ -1,12 +1,12 @@ import { useEffect } from "react"; import { - useDeleteTool, - useDeletePointOrLine, - useObjectPosition, - useObjectRotation, - useSelectedWallItem, - useSocketStore, - useWallItems, + useDeleteTool, + useDeletePointOrLine, + useObjectPosition, + useObjectRotation, + useSelectedWallItem, + useSocketStore, + useWallItems, } from "../../../store/store"; import { Csg } from "../csg/csg"; import * as Types from "../../../types/world/worldTypes"; @@ -20,276 +20,276 @@ import AddWallItems from "../geomentries/walls/addWallItems"; import useModuleStore from "../../../store/useModuleStore"; const WallItemsGroup = ({ - currentWallItem, - AssetConfigurations, - hoveredDeletableWallItem, - selectedItemsIndex, - setSelectedItemsIndex, - CSGGroup, + currentWallItem, + AssetConfigurations, + hoveredDeletableWallItem, + selectedItemsIndex, + setSelectedItemsIndex, + CSGGroup, }: any) => { - const state = useThree(); - const { socket } = useSocketStore(); - const { pointer, camera, raycaster } = state; - const { deleteTool } = useDeleteTool(); - const { wallItems, setWallItems } = useWallItems(); - const { setObjectPosition } = useObjectPosition(); - const { setObjectRotation } = useObjectRotation(); - const { deletePointOrLine } = useDeletePointOrLine(); - const { setSelectedWallItem } = useSelectedWallItem(); - const { activeModule } = useModuleStore(); + const state = useThree(); + const { socket } = useSocketStore(); + const { pointer, camera, raycaster } = state; + const { deleteTool } = useDeleteTool(); + const { wallItems, setWallItems } = useWallItems(); + const { setObjectPosition } = useObjectPosition(); + const { setObjectRotation } = useObjectRotation(); + const { deletePointOrLine } = useDeletePointOrLine(); + const { setSelectedWallItem } = useSelectedWallItem(); + const { activeModule } = useModuleStore(); - useEffect(() => { - // Load Wall Items from the backend - loadInitialWallItems(setWallItems, AssetConfigurations); - }, []); + useEffect(() => { + // Load Wall Items from the backend + loadInitialWallItems(setWallItems, AssetConfigurations); + }, []); - ////////// Update the Scale value changes in thewallItems State ////////// + ////////// Update the Scale value changes in thewallItems State ////////// - ////////// Update the Position value changes in the selected item ////////// + ////////// Update the Position value changes in the selected item ////////// - ////////// Update the Rotation value changes in the selected item ////////// + ////////// Update the Rotation value changes in the selected item ////////// - useEffect(() => { - const canvasElement = state.gl.domElement; - function handlePointerMove(e: any) { - if ( - selectedItemsIndex !== null && - !deletePointOrLine && - e.buttons === 1 - ) { - const Raycaster = state.raycaster; - const intersects = Raycaster.intersectObjects( - CSGGroup.current?.children[0].children!, - true - ); - const Object = intersects.find((child) => - child.object.name.includes("WallRaycastReference") - ); + useEffect(() => { + const canvasElement = state.gl.domElement; + function handlePointerMove(e: any) { + if ( + selectedItemsIndex !== null && + !deletePointOrLine && + e.buttons === 1 + ) { + const Raycaster = state.raycaster; + const intersects = Raycaster.intersectObjects( + CSGGroup.current?.children[0].children!, + true + ); + const Object = intersects.find((child) => + child.object.name.includes("WallRaycastReference") + ); - if (Object) { - (state.controls as any)!.enabled = false; - setWallItems((prevItems: any) => { - const updatedItems = [...prevItems]; - let position: [number, number, number] = [0, 0, 0]; + if (Object) { + (state.controls as any)!.enabled = false; + setWallItems((prevItems: any) => { + const updatedItems = [...prevItems]; + let position: [number, number, number] = [0, 0, 0]; - if (updatedItems[selectedItemsIndex].type === "Fixed-Move") { - position = [ - Object!.point.x, - Math.floor(Object!.point.y / CONSTANTS.wallConfig.height) * - CONSTANTS.wallConfig.height, - Object!.point.z, - ]; - } else if (updatedItems[selectedItemsIndex].type === "Free-Move") { - position = [Object!.point.x, Object!.point.y, Object!.point.z]; + if (updatedItems[selectedItemsIndex].type === "Fixed-Move") { + position = [ + Object!.point.x, + Math.floor(Object!.point.y / CONSTANTS.wallConfig.height) * + CONSTANTS.wallConfig.height, + Object!.point.z, + ]; + } else if (updatedItems[selectedItemsIndex].type === "Free-Move") { + position = [Object!.point.x, Object!.point.y, Object!.point.z]; + } + + requestAnimationFrame(() => { + setObjectPosition(new THREE.Vector3(...position)); + setObjectRotation({ + x: THREE.MathUtils.radToDeg(Object!.object.rotation.x), + y: THREE.MathUtils.radToDeg(Object!.object.rotation.y), + z: THREE.MathUtils.radToDeg(Object!.object.rotation.z), + }); + }); + + updatedItems[selectedItemsIndex] = { + ...updatedItems[selectedItemsIndex], + position: position, + quaternion: + Object!.object.quaternion.clone() as Types.QuaternionType, + }; + + return updatedItems; + }); + } } - - requestAnimationFrame(() => { - setObjectPosition(new THREE.Vector3(...position)); - setObjectRotation({ - x: THREE.MathUtils.radToDeg(Object!.object.rotation.x), - y: THREE.MathUtils.radToDeg(Object!.object.rotation.y), - z: THREE.MathUtils.radToDeg(Object!.object.rotation.z), - }); - }); - - updatedItems[selectedItemsIndex] = { - ...updatedItems[selectedItemsIndex], - position: position, - quaternion: - Object!.object.quaternion.clone() as Types.QuaternionType, - }; - - return updatedItems; - }); } - } - } - async function handlePointerUp() { - const Raycaster = state.raycaster; - const intersects = Raycaster.intersectObjects( - CSGGroup.current?.children[0].children!, - true - ); - const Object = intersects.find((child) => - child.object.name.includes("WallRaycastReference") - ); - if (Object) { - if (selectedItemsIndex !== null) { - let currentItem: any = null; - setWallItems((prevItems: any) => { - const updatedItems = [...prevItems]; - const WallItemsForStorage = updatedItems.map((item) => { - const { model, ...rest } = item; - return { - ...rest, - modelUuid: model?.uuid, - }; - }); - - currentItem = updatedItems[selectedItemsIndex]; - localStorage.setItem( - "WallItems", - JSON.stringify(WallItemsForStorage) + async function handlePointerUp() { + const Raycaster = state.raycaster; + const intersects = Raycaster.intersectObjects( + CSGGroup.current?.children[0].children!, + true ); - return updatedItems; - }); + const Object = intersects.find((child) => + child.object.name.includes("WallRaycastReference") + ); + if (Object) { + if (selectedItemsIndex !== null) { + let currentItem: any = null; + setWallItems((prevItems: any) => { + const updatedItems = [...prevItems]; + const WallItemsForStorage = updatedItems.map((item) => { + const { model, ...rest } = item; + return { + ...rest, + modelUuid: model?.uuid, + }; + }); - setTimeout(async () => { - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; + currentItem = updatedItems[selectedItemsIndex]; + localStorage.setItem( + "WallItems", + JSON.stringify(WallItemsForStorage) + ); + return updatedItems; + }); - //REST + setTimeout(async () => { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - // await setWallItem( - // organization, - // currentItem?.model?.uuid, - // currentItem.modelName, - // currentItem.type!, - // currentItem.csgposition!, - // currentItem.csgscale!, - // currentItem.position, - // currentItem.quaternion, - // currentItem.scale!, - // ) + //REST - //SOCKET + // await setWallItem( + // organization, + // currentItem?.model?.uuid, + // currentItem.modelName, + // currentItem.type!, + // currentItem.csgposition!, + // currentItem.csgscale!, + // currentItem.position, + // currentItem.quaternion, + // currentItem.scale!, + // ) - const data = { - organization: organization, - modelUuid: currentItem.model?.uuid!, - modelName: currentItem.modelName!, - type: currentItem.type!, - csgposition: currentItem.csgposition!, - csgscale: currentItem.csgscale!, - position: currentItem.position!, - quaternion: currentItem.quaternion, - scale: currentItem.scale!, - socketId: socket.id, - }; + //SOCKET - socket.emit("v1:wallItems:set", data); - }, 0); - (state.controls as any)!.enabled = true; + const data = { + organization: organization, + modelUuid: currentItem.model?.uuid!, + modelName: currentItem.modelName!, + type: currentItem.type!, + csgposition: currentItem.csgposition!, + csgscale: currentItem.csgscale!, + position: currentItem.position!, + quaternion: currentItem.quaternion, + scale: currentItem.scale!, + socketId: socket.id, + }; + + socket.emit("v1:wallItems:set", data); + }, 0); + (state.controls as any)!.enabled = true; + } + } } - } - } - canvasElement.addEventListener("pointermove", handlePointerMove); - canvasElement.addEventListener("pointerup", handlePointerUp); + canvasElement.addEventListener("pointermove", handlePointerMove); + canvasElement.addEventListener("pointerup", handlePointerUp); - return () => { - canvasElement.removeEventListener("pointermove", handlePointerMove); - canvasElement.removeEventListener("pointerup", handlePointerUp); - }; - }, [selectedItemsIndex]); + return () => { + canvasElement.removeEventListener("pointermove", handlePointerMove); + canvasElement.removeEventListener("pointerup", handlePointerUp); + }; + }, [selectedItemsIndex]); - useEffect(() => { - const canvasElement = state.gl.domElement; - let drag = false; - let isLeftMouseDown = false; + useEffect(() => { + const canvasElement = state.gl.domElement; + let drag = false; + let isLeftMouseDown = false; - const onMouseDown = (evt: any) => { - if (evt.button === 0) { - isLeftMouseDown = true; - drag = false; - } - }; + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + } + }; - const onMouseUp = (evt: any) => { - if (evt.button === 0) { - isLeftMouseDown = false; - if (!drag && deleteTool && activeModule === "builder") { - DeleteWallItems( - hoveredDeletableWallItem, - setWallItems, - wallItems, - socket - ); + const onMouseUp = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = false; + if (!drag && deleteTool && activeModule === "builder") { + DeleteWallItems( + hoveredDeletableWallItem, + setWallItems, + wallItems, + socket + ); + } + } + }; + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + }; + + const onDrop = (event: any) => { + if (!event.dataTransfer?.files[0]) return; + + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; + raycaster.setFromCamera(pointer, camera); + + if (AssetConfigurations[event.dataTransfer.files[0].name.split(".")[0]]) { + const selected = event.dataTransfer.files[0].name.split(".")[0]; + + if (AssetConfigurations[selected]?.type) { + AddWallItems( + selected, + raycaster, + CSGGroup, + AssetConfigurations, + setWallItems, + socket + ); + } + event.preventDefault(); + } + }; + + const onDragOver = (event: any) => { + event.preventDefault(); + }; + + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("drop", onDrop); + canvasElement.addEventListener("dragover", onDragOver); + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("drop", onDrop); + canvasElement.removeEventListener("dragover", onDragOver); + }; + }, [deleteTool, wallItems]); + + useEffect(() => { + if (deleteTool && activeModule === "builder") { + handleMeshMissed( + currentWallItem, + setSelectedWallItem, + setSelectedItemsIndex + ); + setSelectedWallItem(null); + setSelectedItemsIndex(null); } - } - }; + }, [deleteTool]); - const onMouseMove = () => { - if (isLeftMouseDown) { - drag = true; - } - }; - - const onDrop = (event: any) => { - if (!event.dataTransfer?.files[0]) return; - - pointer.x = (event.clientX / window.innerWidth) * 2 - 1; - pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; - raycaster.setFromCamera(pointer, camera); - - if (AssetConfigurations[event.dataTransfer.files[0].name.split(".")[0]]) { - const selected = event.dataTransfer.files[0].name.split(".")[0]; - - if (AssetConfigurations[selected]?.type) { - AddWallItems( - selected, - raycaster, - CSGGroup, - AssetConfigurations, - setWallItems, - socket - ); - } - event.preventDefault(); - } - }; - - const onDragOver = (event: any) => { - event.preventDefault(); - }; - - canvasElement.addEventListener("mousedown", onMouseDown); - canvasElement.addEventListener("mouseup", onMouseUp); - canvasElement.addEventListener("mousemove", onMouseMove); - canvasElement.addEventListener("drop", onDrop); - canvasElement.addEventListener("dragover", onDragOver); - - return () => { - canvasElement.removeEventListener("mousedown", onMouseDown); - canvasElement.removeEventListener("mouseup", onMouseUp); - canvasElement.removeEventListener("mousemove", onMouseMove); - canvasElement.removeEventListener("drop", onDrop); - canvasElement.removeEventListener("dragover", onDragOver); - }; - }, [deleteTool, wallItems]); - - useEffect(() => { - if (deleteTool && activeModule === "builder") { - handleMeshMissed( - currentWallItem, - setSelectedWallItem, - setSelectedItemsIndex - ); - setSelectedWallItem(null); - setSelectedItemsIndex(null); - } - }, [deleteTool]); - - return ( - <> - {wallItems.map((item: Types.WallItem, index: number) => ( - - - - ))} - - ); + return ( + <> + {wallItems.map((item: Types.WallItem, index: number) => ( + + + + ))} + + ); }; export default WallItemsGroup; diff --git a/app/src/modules/builder/groups/wallsAndWallItems.tsx b/app/src/modules/builder/groups/wallsAndWallItems.tsx index 455896e..19d5833 100644 --- a/app/src/modules/builder/groups/wallsAndWallItems.tsx +++ b/app/src/modules/builder/groups/wallsAndWallItems.tsx @@ -1,11 +1,10 @@ import { Geometry } from "@react-three/csg"; import { - useDeleteTool, - useSelectedWallItem, - useToggleView, - useTransformMode, - useWallItems, - useWalls, + useDeleteTool, + useSelectedWallItem, + useToggleView, + useWallItems, + useWalls, } from "../../../store/store"; import handleMeshDown from "../eventFunctions/handleMeshDown"; import handleMeshMissed from "../eventFunctions/handleMeshMissed"; @@ -14,79 +13,65 @@ import WallItemsGroup from "./wallItemsGroup"; import { useEffect } from "react"; const WallsAndWallItems = ({ - CSGGroup, - AssetConfigurations, - setSelectedItemsIndex, - selectedItemsIndex, - currentWallItem, - csg, - lines, - hoveredDeletableWallItem, + CSGGroup, + AssetConfigurations, + setSelectedItemsIndex, + selectedItemsIndex, + currentWallItem, + csg, + lines, + hoveredDeletableWallItem, }: any) => { - const { walls } = useWalls(); - const { wallItems } = useWallItems(); - const { toggleView } = useToggleView(); - const { deleteTool } = useDeleteTool(); - const { transformMode } = useTransformMode(); - const { setSelectedWallItem } = useSelectedWallItem(); + const { walls } = useWalls(); + const { wallItems } = useWallItems(); + const { toggleView } = useToggleView(); + const { deleteTool } = useDeleteTool(); + const { setSelectedWallItem } = useSelectedWallItem(); - useEffect(() => { - if (transformMode === null) { - if (!deleteTool) { - handleMeshMissed( - currentWallItem, - setSelectedWallItem, - setSelectedItemsIndex - ); - setSelectedWallItem(null); - setSelectedItemsIndex(null); - } - } - }, [transformMode]); - return ( - { - if (!deleteTool && transformMode !== null) { - handleMeshDown( - event, - currentWallItem, - setSelectedWallItem, - setSelectedItemsIndex, - wallItems, - toggleView - ); - } - }} - onPointerMissed={() => { - if (!deleteTool) { - handleMeshMissed( - currentWallItem, - setSelectedWallItem, - setSelectedItemsIndex - ); - setSelectedWallItem(null); - setSelectedItemsIndex(null); - } - }} - > - - - - - - ); + return ( + { + if (!deleteTool) { + handleMeshDown( + event, + currentWallItem, + setSelectedWallItem, + setSelectedItemsIndex, + wallItems, + toggleView + ); + } + }} + onPointerMissed={() => { + if (!deleteTool) { + handleMeshMissed( + currentWallItem, + setSelectedWallItem, + setSelectedItemsIndex + ); + setSelectedWallItem(null); + setSelectedItemsIndex(null); + } + }} + > + + + + + + ); }; export default WallsAndWallItems; diff --git a/app/src/modules/builder/groups/wallsMesh.tsx b/app/src/modules/builder/groups/wallsMesh.tsx index 4f71680..9abf59d 100644 --- a/app/src/modules/builder/groups/wallsMesh.tsx +++ b/app/src/modules/builder/groups/wallsMesh.tsx @@ -15,63 +15,63 @@ import texturePath from "../../../assets/textures/floor/wall-tex.png"; const materialCache = new Map(); const WallsMeshComponent = ({ lines }: any) => { - const { walls, setWalls } = useWalls(); - const { updateScene, setUpdateScene } = useUpdateScene(); + const { walls, setWalls } = useWalls(); + const { updateScene, setUpdateScene } = useUpdateScene(); - useEffect(() => { - if (updateScene) { - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; + useEffect(() => { + if (updateScene) { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - getLines(organization).then((data) => { - const Lines: Types.Lines = objectLinesToArray(data); - localStorage.setItem("Lines", JSON.stringify(Lines)); + getLines(organization).then((data) => { + const Lines: Types.Lines = objectLinesToArray(data); + localStorage.setItem("Lines", JSON.stringify(Lines)); - if (Lines) { - loadWalls(lines, setWalls); + if (Lines) { + loadWalls(lines, setWalls); + } + }); + setUpdateScene(false); } - }); - setUpdateScene(false); - } - }, [updateScene]); + }, [updateScene]); - const textureLoader = new THREE.TextureLoader(); - const wallTexture = textureLoader.load(texturePath); + const textureLoader = new THREE.TextureLoader(); + const wallTexture = textureLoader.load(texturePath); - wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping; - wallTexture.repeat.set(0.1, 0.1); - wallTexture.colorSpace = THREE.SRGBColorSpace; + wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping; + wallTexture.repeat.set(0.1, 0.1); + wallTexture.colorSpace = THREE.SRGBColorSpace; - return ( - <> - {walls.map((wall: Types.Wall, index: number) => ( - - - - - - - - - ))} - - ); + return ( + <> + {walls.map((wall: Types.Wall, index: number) => ( + + + + + + + + + ))} + + ); }; const WallsMesh = React.memo(WallsMeshComponent); diff --git a/app/src/modules/scene/controls/controls.tsx b/app/src/modules/scene/controls/controls.tsx index 8bdd3aa..1420d89 100644 --- a/app/src/modules/scene/controls/controls.tsx +++ b/app/src/modules/scene/controls/controls.tsx @@ -10,6 +10,7 @@ import updateCamPosition from "../camera/updateCameraPosition"; import CamMode from "../camera/camMode"; import SwitchView from "../camera/switchView"; import SelectionControls from "./selectionControls/selectionControls"; +import TransformControl from "./transformControls/transformControls"; export default function Controls() { const controlsRef = useRef(null); @@ -138,6 +139,8 @@ export default function Controls() { + + ); } \ No newline at end of file diff --git a/app/src/modules/scene/controls/selectionControls/distanceFindingControls.tsx b/app/src/modules/scene/controls/selectionControls/distanceFindingControls.tsx index 25e6c96..1cfe244 100644 --- a/app/src/modules/scene/controls/selectionControls/distanceFindingControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/distanceFindingControls.tsx @@ -1,270 +1,270 @@ import React, { useRef } from "react"; import { - Vector3, - Raycaster, - BufferGeometry, - LineBasicMaterial, - Line, - Mesh, - Group, + Vector3, + Raycaster, + BufferGeometry, + LineBasicMaterial, + Line, + Mesh, + Group, } from "three"; import { useThree, useFrame } from "@react-three/fiber"; import { Html } from "@react-three/drei"; interface DistanceFindingControlsProps { - boundingBoxRef: React.RefObject; - object: number; + boundingBoxRef: React.RefObject; + object: number; } const DistanceFindingControls = ({ - boundingBoxRef, - object, + boundingBoxRef, + object, }: DistanceFindingControlsProps) => { - const { camera, scene } = useThree(); + const { camera, scene } = useThree(); - // Refs for measurement lines - const line1 = useRef(null); - const line2 = useRef(null); - const line3 = useRef(null); - const line4 = useRef(null); - const line5 = useRef(null); + // Refs for measurement lines + const line1 = useRef(null); + const line2 = useRef(null); + const line3 = useRef(null); + const line4 = useRef(null); + const line5 = useRef(null); - // Refs for measurement text labels - const textPosX = useRef(null); - const textNegX = useRef(null); - const textPosZ = useRef(null); - const textNegZ = useRef(null); - const textPosY = useRef(null); + // Refs for measurement text labels + const textPosX = useRef(null); + const textNegX = useRef(null); + const textPosZ = useRef(null); + const textNegZ = useRef(null); + const textPosY = useRef(null); - // Store line geometries to avoid recreation - const lineGeometries = useRef({ - posX: new BufferGeometry(), - negX: new BufferGeometry(), - posZ: new BufferGeometry(), - negZ: new BufferGeometry(), - posY: new BufferGeometry(), - }); + // Store line geometries to avoid recreation + const lineGeometries = useRef({ + posX: new BufferGeometry(), + negX: new BufferGeometry(), + posZ: new BufferGeometry(), + negZ: new BufferGeometry(), + posY: new BufferGeometry(), + }); - useFrame(() => { - if (!boundingBoxRef?.current) return; + useFrame(() => { + if (!boundingBoxRef?.current) return; - boundingBoxRef.current.geometry.computeBoundingBox(); - const bbox = boundingBoxRef.current.geometry.boundingBox; + boundingBoxRef.current.geometry.computeBoundingBox(); + const bbox = boundingBoxRef.current.geometry.boundingBox; - if (!bbox) return; + if (!bbox) return; - const size = { - x: bbox.max.x - bbox.min.x, - y: bbox.max.y - bbox.min.y, - z: bbox.max.z - bbox.min.z, + const size = { + x: bbox.max.x - bbox.min.x, + y: bbox.max.y - bbox.min.y, + z: bbox.max.z - bbox.min.z, + }; + + const vec = boundingBoxRef.current?.getWorldPosition(new Vector3()).clone(); + + if (!vec) return; + updateLine({ + line: line1.current, + geometry: lineGeometries.current.posX, + direction: new Vector3(1, 0, 0), // Positive X + angle: "pos", + mesh: textPosX, + vec, + size, + }); + updateLine({ + line: line2.current, + geometry: lineGeometries.current.negX, + direction: new Vector3(-1, 0, 0), // Negative X + angle: "neg", + mesh: textNegX, + vec, + size, + }); + updateLine({ + line: line3.current, + geometry: lineGeometries.current.posZ, + direction: new Vector3(0, 0, 1), // Positive Z + angle: "pos", + mesh: textPosZ, + vec, + size, + }); + updateLine({ + line: line4.current, + geometry: lineGeometries.current.negZ, + direction: new Vector3(0, 0, -1), // Negative Z + angle: "neg", + mesh: textNegZ, + vec, + size, + }); + updateLine({ + line: line5.current, + geometry: lineGeometries.current.posY, + direction: new Vector3(0, -1, 0), // Down (Y) + angle: "posY", + mesh: textPosY, + vec, + size, + }); + }); + + const updateLine = ({ + line, + geometry, + direction, + angle, + mesh, + vec, + size, + }: { + line: Line | null; + geometry: BufferGeometry; + direction: Vector3; + angle: string; + mesh: React.RefObject; + vec: Vector3; + size: { x: number; y: number; z: number }; + }) => { + if (!line) return; + + const points = []; + + if (angle === "pos") { + points[0] = new Vector3(vec.x, vec.y, vec.z).add( + new Vector3((direction.x * size.x) / 2, 0, (direction.z * size.z) / 2) + ); + } else if (angle === "neg") { + points[0] = new Vector3(vec.x, vec.y, vec.z).sub( + new Vector3((-direction.x * size.x) / 2, 0, (-direction.z * size.z) / 2) + ); + } else if (angle === "posY") { + points[0] = new Vector3(vec.x, vec.y, vec.z).sub( + new Vector3(0, size.y / 2, 0) + ); + } + + const ray = new Raycaster(); + if (camera) ray.camera = camera; + ray.set(new Vector3(vec.x, vec.y, vec.z), direction); + ray.params.Line.threshold = 0.1; + + // Find intersection points + const wallsGroup = scene.children.find((val) => + val?.name.includes("Walls") + ); + const intersects = wallsGroup + ? ray.intersectObjects([wallsGroup], true) + : []; + + // Find intersection point + if (intersects[0]) { + for (const intersect of intersects) { + if (intersect.object.name.includes("Wall")) { + points[1] = + angle !== "posY" ? intersect.point : new Vector3(vec.x, 0, vec.z); // Floor + break; + } + } + } + + // Update line geometry + if (points[1]) { + geometry.dispose(); + geometry.setFromPoints([points[0], points[1]]); + line.geometry = geometry; + + // Update measurement text + if (mesh?.current) { + geometry.computeBoundingSphere(); + const center = geometry.boundingSphere?.center; + if (center) { + mesh.current.position.copy(center); + } + const label = document.getElementById(mesh.current.name); + if (label) { + label.innerText = `${points[0].distanceTo(points[1]).toFixed(2)}m`; + } + } + } else { + // No intersection found - clear the line + geometry.dispose(); + geometry.setFromPoints([new Vector3(), new Vector3()]); + line.geometry = geometry; + const label = document.getElementById(mesh?.current?.name ?? ""); + if (label) label.innerText = ""; + } }; - const vec = boundingBoxRef.current?.getWorldPosition(new Vector3()).clone(); + const Material = new LineBasicMaterial({ color: "#d2baff" }); - if (!vec) return; - updateLine({ - line: line1.current, - geometry: lineGeometries.current.posX, - direction: new Vector3(1, 0, 0), // Positive X - angle: "pos", - mesh: textPosX, - vec, - size, - }); - updateLine({ - line: line2.current, - geometry: lineGeometries.current.negX, - direction: new Vector3(-1, 0, 0), // Negative X - angle: "neg", - mesh: textNegX, - vec, - size, - }); - updateLine({ - line: line3.current, - geometry: lineGeometries.current.posZ, - direction: new Vector3(0, 0, 1), // Positive Z - angle: "pos", - mesh: textPosZ, - vec, - size, - }); - updateLine({ - line: line4.current, - geometry: lineGeometries.current.negZ, - direction: new Vector3(0, 0, -1), // Negative Z - angle: "neg", - mesh: textNegZ, - vec, - size, - }); - updateLine({ - line: line5.current, - geometry: lineGeometries.current.posY, - direction: new Vector3(0, -1, 0), // Down (Y) - angle: "posY", - mesh: textPosY, - vec, - size, - }); - }); - - const updateLine = ({ - line, - geometry, - direction, - angle, - mesh, - vec, - size, - }: { - line: Line | null; - geometry: BufferGeometry; - direction: Vector3; - angle: string; - mesh: React.RefObject; - vec: Vector3; - size: { x: number; y: number; z: number }; - }) => { - if (!line) return; - - const points = []; - - if (angle === "pos") { - points[0] = new Vector3(vec.x, vec.y, vec.z).add( - new Vector3((direction.x * size.x) / 2, 0, (direction.z * size.z) / 2) - ); - } else if (angle === "neg") { - points[0] = new Vector3(vec.x, vec.y, vec.z).sub( - new Vector3((-direction.x * size.x) / 2, 0, (-direction.z * size.z) / 2) - ); - } else if (angle === "posY") { - points[0] = new Vector3(vec.x, vec.y, vec.z).sub( - new Vector3(0, size.y / 2, 0) - ); - } - - const ray = new Raycaster(); - if (camera) ray.camera = camera; - ray.set(new Vector3(vec.x, vec.y, vec.z), direction); - ray.params.Line.threshold = 0.1; - - // Find intersection points - const wallsGroup = scene.children.find((val) => - val?.name.includes("Walls") - ); - const intersects = wallsGroup - ? ray.intersectObjects([wallsGroup], true) - : []; - - // Find intersection point - if (intersects[0]) { - for (const intersect of intersects) { - if (intersect.object.name.includes("Wall")) { - points[1] = - angle !== "posY" ? intersect.point : new Vector3(vec.x, 0, vec.z); // Floor - break; - } - } - } - - // Update line geometry - if (points[1]) { - geometry.dispose(); - geometry.setFromPoints([points[0], points[1]]); - line.geometry = geometry; - - // Update measurement text - if (mesh?.current) { - geometry.computeBoundingSphere(); - const center = geometry.boundingSphere?.center; - if (center) { - mesh.current.position.copy(center); - } - const label = document.getElementById(mesh.current.name); - if (label) { - label.innerText = `${points[0].distanceTo(points[1]).toFixed(2)}m`; - } - } - } else { - // No intersection found - clear the line - geometry.dispose(); - geometry.setFromPoints([new Vector3(), new Vector3()]); - line.geometry = geometry; - const label = document.getElementById(mesh?.current?.name ?? ""); - if (label) label.innerText = ""; - } - }; - - const Material = new LineBasicMaterial({ color: "#d2baff" }); - - return ( - <> - {/* Measurement text labels */} - {boundingBoxRef.current && object > 0 && ( + return ( <> - - -
- -
- - -
- -
- - -
- -
- - -
- -
+ {/* Measurement text labels */} + {boundingBoxRef.current && object > 0 && ( + <> + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
- {/* Measurement lines */} - - - - + {/* Measurement lines */} + + + + + + )} - )} - - ); + ); }; export default DistanceFindingControls; diff --git a/app/src/modules/scene/controls/selectionControls/moveControls.tsx b/app/src/modules/scene/controls/selectionControls/moveControls.tsx index 498056a..5398fe6 100644 --- a/app/src/modules/scene/controls/selectionControls/moveControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/moveControls.tsx @@ -2,10 +2,10 @@ import * as THREE from "three"; import { useEffect, useMemo, useRef, useState } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import { - useFloorItems, - useSelectedAssets, - useSocketStore, - useToggleView, + useFloorItems, + useSelectedAssets, + useSocketStore, + useToggleView, } from "../../../../store/store"; // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; import { toast } from "react-toastify"; @@ -19,370 +19,337 @@ import { snapControls } from "../../../../utils/handleSnap"; import DistanceFindingControls from "./distanceFindingControls"; function MoveControls({ - movedObjects, - setMovedObjects, - itemsGroupRef, - copiedObjects, - setCopiedObjects, - pastedObjects, - setpastedObjects, - duplicatedObjects, - setDuplicatedObjects, - selectionGroup, - rotatedObjects, - setRotatedObjects, - boundingBoxRef, -}: any) { - const { camera, controls, gl, scene, pointer, raycaster } = useThree(); - const plane = useMemo( - () => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), - [] - ); - - const { toggleView } = useToggleView(); - const { selectedAssets, setSelectedAssets } = useSelectedAssets(); - const { selectedProduct } = useSelectedProduct(); - const { floorItems, setFloorItems } = useFloorItems(); - const { socket } = useSocketStore(); - const itemsData = useRef([]); - const [keyEvent, setKeyEvent] = useState< - "Ctrl" | "Shift" | "Ctrl+Shift" | "" - >(""); - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; - - const updateBackend = ( - productName: string, - productId: string, - organization: string, - eventData: EventsSchema - ) => { - upsertProductOrEventApi({ - productName: productName, - productId: productId, - organization: organization, - eventDatas: eventData, - }); - }; - - useEffect(() => { - if (!camera || !scene || toggleView || !itemsGroupRef.current) return; - - const canvasElement = gl.domElement; - canvasElement.tabIndex = 0; - - let isMoving = false; - - const onPointerDown = () => { - isMoving = false; - }; - - const onPointerMove = () => { - isMoving = true; - }; - const onKeyUp = (event: KeyboardEvent) => { - // When any modifier is released, reset snap - const isModifierKey = event.key === "Control" || event.key === "Shift"; - - if (isModifierKey) { - setKeyEvent(""); - } - }; - - const onPointerUp = (event: PointerEvent) => { - if (!isMoving && movedObjects.length > 0 && event.button === 0) { - event.preventDefault(); - placeMovedAssets(); - } - if (!isMoving && movedObjects.length > 0 && event.button === 2) { - event.preventDefault(); - - clearSelection(); - movedObjects.forEach((asset: any) => { - if (itemsGroupRef.current) { - itemsGroupRef.current.attach(asset); - } - }); - - setFloorItems([...floorItems, ...itemsData.current]); - - setMovedObjects([]); - itemsData.current = []; - } - setKeyEvent(""); - }; - - const onKeyDown = (event: KeyboardEvent) => { - const keyCombination = detectModifierKeys(event); - - if ( - pastedObjects.length > 0 || - duplicatedObjects.length > 0 || - rotatedObjects.length > 0 - ) - return; - - if ( - keyCombination === "Ctrl" || - keyCombination === "Ctrl+Shift" || - keyCombination === "Shift" - ) { - // update state here - setKeyEvent(keyCombination); - } else { - setKeyEvent(""); - } - - if (keyCombination === "G") { - if (selectedAssets.length > 0) { - moveAssets(); - itemsData.current = floorItems.filter((item: { modelUuid: string }) => - selectedAssets.some((asset: any) => asset.uuid === item.modelUuid) - ); - } - } - - if (keyCombination === "ESCAPE") { - event.preventDefault(); - - clearSelection(); - movedObjects.forEach((asset: any) => { - if (itemsGroupRef.current) { - itemsGroupRef.current.attach(asset); - } - }); - - setFloorItems([...floorItems, ...itemsData.current]); - - setMovedObjects([]); - itemsData.current = []; - } - }; - - if (!toggleView) { - canvasElement.addEventListener("pointerdown", onPointerDown); - canvasElement.addEventListener("pointermove", onPointerMove); - canvasElement.addEventListener("pointerup", onPointerUp); - canvasElement.addEventListener("keydown", onKeyDown); - canvasElement?.addEventListener("keyup", onKeyUp); - } - - return () => { - canvasElement.removeEventListener("pointerdown", onPointerDown); - canvasElement.removeEventListener("pointermove", onPointerMove); - canvasElement.removeEventListener("pointerup", onPointerUp); - canvasElement.removeEventListener("keydown", onKeyDown); - canvasElement?.removeEventListener("keyup", onKeyUp); - }; - }, [ - camera, - controls, - scene, - toggleView, - selectedAssets, - socket, - floorItems, - pastedObjects, - duplicatedObjects, movedObjects, + setMovedObjects, + itemsGroupRef, + pastedObjects, + setpastedObjects, + duplicatedObjects, + setDuplicatedObjects, + selectionGroup, rotatedObjects, - keyEvent, - ]); + setRotatedObjects, + boundingBoxRef, +}: any) { + const { camera, controls, gl, scene, pointer, raycaster } = useThree(); + const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); - let moveSpeed = keyEvent === "Ctrl" || "Ctrl+Shift" ? 1 : 0.25; + const { toggleView } = useToggleView(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const { selectedProduct } = useSelectedProduct(); + const { floorItems, setFloorItems } = useFloorItems(); + const { socket } = useSocketStore(); + const itemsData = useRef([]); + const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - useFrame(() => { - if (movedObjects.length > 0) { - const intersectionPoint = new THREE.Vector3(); - raycaster.setFromCamera(pointer, camera); - const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData, + }); + }; - if (point) { - let targetX = point.x; - let targetZ = point.z; - if (keyEvent === "Ctrl") { - targetX = snapControls(targetX, "Ctrl"); - targetZ = snapControls(targetZ, "Ctrl"); - } - // else if (keyEvent === "Ctrl+Shift") { - // targetX = snapControls(targetX, "Ctrl+Shift"); - // targetZ = snapControls(targetZ, "Ctrl+Shift"); - // } else if (keyEvent === "Shift") { - // targetX = snapControls(targetX, "Shift"); - // targetZ = snapControls(targetZ, "Shift"); - // } else { - // } + useEffect(() => { + if (!camera || !scene || toggleView || !itemsGroupRef.current) return; - const position = new THREE.Vector3(); + const canvasElement = gl.domElement; + canvasElement.tabIndex = 0; - if (boundingBoxRef.current) { - boundingBoxRef.current.getWorldPosition(position); - selectionGroup.current.position.lerp( - new THREE.Vector3( - targetX - (position.x - selectionGroup.current.position.x), - selectionGroup.current.position.y, - targetZ - (position.z - selectionGroup.current.position.z) - ), - moveSpeed - ); - } else { - const box = new THREE.Box3(); - movedObjects.forEach((obj: THREE.Object3D) => - box.expandByObject(obj) - ); - const center = new THREE.Vector3(); - box.getCenter(center); + let isMoving = false; - selectionGroup.current.position.lerp( - new THREE.Vector3( - targetX - (center.x - selectionGroup.current.position.x), - selectionGroup.current.position.y, - targetZ - (center.z - selectionGroup.current.position.z) - ), - moveSpeed - ); - } - } - } - }); - - const moveAssets = () => { - const updatedItems = floorItems.filter( - (item: { modelUuid: string }) => - !selectedAssets.some((asset: any) => asset.uuid === item.modelUuid) - ); - setFloorItems(updatedItems); - setMovedObjects(selectedAssets); - selectedAssets.forEach((asset: any) => { - selectionGroup.current.attach(asset); - }); - }; - - const placeMovedAssets = () => { - if (movedObjects.length === 0) return; - - movedObjects.forEach(async (obj: THREE.Object3D) => { - const worldPosition = new THREE.Vector3(); - obj.getWorldPosition(worldPosition); - - selectionGroup.current.remove(obj); - obj.position.copy(worldPosition); - - if (itemsGroupRef.current) { - const newFloorItem: Types.FloorItemType = { - modelUuid: obj.uuid, - modelName: obj.userData.name, - modelfileID: obj.userData.modelId, - position: [worldPosition.x, worldPosition.y, worldPosition.z], - rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, - isLocked: false, - isVisible: true, + const onPointerDown = () => { + isMoving = false; }; - if (obj.userData.eventData) { - const eventData = useEventsStore - .getState() - .getEventByModelUuid(obj.userData.modelUuid); - const productData = useProductStore - .getState() - .getEventByModelUuid( - useSelectedProduct.getState().selectedProduct.productId, - obj.userData.modelUuid - ); + const onPointerMove = () => { + isMoving = true; + }; - if (eventData) { - useEventsStore.getState().updateEvent(obj.userData.modelUuid, { - position: [worldPosition.x, worldPosition.y, worldPosition.z], - rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], - }); - } - if (productData) { - const event = useProductStore - .getState() - .updateEvent( - useSelectedProduct.getState().selectedProduct.productId, - obj.userData.modelUuid, - { - position: [worldPosition.x, worldPosition.y, worldPosition.z], - rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], - } - ); + const onKeyUp = (event: KeyboardEvent) => { + const isModifierKey = event.key === "Control" || event.key === "Shift"; - if (event) { - updateBackend( - selectedProduct.productName, - selectedProduct.productId, - organization, - event - ); + if (isModifierKey) { + setKeyEvent(""); + } + }; + + const onPointerUp = (event: PointerEvent) => { + if (!isMoving && movedObjects.length > 0 && event.button === 0) { + event.preventDefault(); + placeMovedAssets(); + } + if (!isMoving && movedObjects.length > 0 && event.button === 2) { + event.preventDefault(); + + clearSelection(); + movedObjects.forEach((asset: any) => { + if (itemsGroupRef.current) { + itemsGroupRef.current.attach(asset); + } + }); + + setFloorItems([...floorItems, ...itemsData.current]); + + setMovedObjects([]); + itemsData.current = []; + } + setKeyEvent(""); + }; + + const onKeyDown = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + + if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || rotatedObjects.length > 0) + return; + + if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } else { + setKeyEvent(""); } - newFloorItem.eventData = eventData; - } - } + if (keyCombination === "G") { + if (selectedAssets.length > 0) { + moveAssets(); + itemsData.current = floorItems.filter((item: { modelUuid: string }) => + selectedAssets.some((asset: any) => asset.uuid === item.modelUuid) + ); + } + } - setFloorItems((prevItems: Types.FloorItems) => { - const updatedItems = [...(prevItems || []), newFloorItem]; - localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); - return updatedItems; - }); + if (keyCombination === "ESCAPE") { + event.preventDefault(); - //REST + clearSelection(); + movedObjects.forEach((asset: any) => { + if (itemsGroupRef.current) { + itemsGroupRef.current.attach(asset); + } + }); - // await setFloorItemApi( - // organization, - // obj.uuid, - // obj.userData.name, - // [worldPosition.x, worldPosition.y, worldPosition.z], - // { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z }, - // obj.userData.modelId, - // false, - // true, - // ); + setFloorItems([...floorItems, ...itemsData.current]); - //SOCKET - - const data = { - organization, - modelUuid: newFloorItem.modelUuid, - modelName: newFloorItem.modelName, - modelfileID: newFloorItem.modelfileID, - position: newFloorItem.position, - rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, - isLocked: false, - isVisible: true, - socketId: socket.id, + setMovedObjects([]); + itemsData.current = []; + } }; - socket.emit("v2:model-asset:add", data); + if (!toggleView) { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + canvasElement.addEventListener("keydown", onKeyDown); + canvasElement?.addEventListener("keyup", onKeyUp); + } - itemsGroupRef.current.add(obj); - } + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("keydown", onKeyDown); + canvasElement?.removeEventListener("keyup", onKeyUp); + }; + }, [camera, controls, scene, toggleView, selectedAssets, socket, floorItems, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent,]); + + let moveSpeed = keyEvent === "Ctrl" || "Ctrl+Shift" ? 1 : 0.25; + + useFrame(() => { + if (movedObjects.length > 0) { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (point) { + let targetX = point.x; + let targetZ = point.z; + + if (keyEvent === "Ctrl") { + targetX = snapControls(targetX, "Ctrl"); + targetZ = snapControls(targetZ, "Ctrl"); + } + + // else if (keyEvent === "Ctrl+Shift") { + // targetX = snapControls(targetX, "Ctrl+Shift"); + // targetZ = snapControls(targetZ, "Ctrl+Shift"); + // } else if (keyEvent === "Shift") { + // targetX = snapControls(targetX, "Shift"); + // targetZ = snapControls(targetZ, "Shift"); + // } else { + // } + + const position = new THREE.Vector3(); + + if (boundingBoxRef.current) { + boundingBoxRef.current.getWorldPosition(position); + selectionGroup.current.position.lerp( + new THREE.Vector3( + targetX - (position.x - selectionGroup.current.position.x), + selectionGroup.current.position.y, + targetZ - (position.z - selectionGroup.current.position.z) + ), + moveSpeed + ); + } else { + const box = new THREE.Box3(); + movedObjects.forEach((obj: THREE.Object3D) => + box.expandByObject(obj) + ); + const center = new THREE.Vector3(); + box.getCenter(center); + + selectionGroup.current.position.lerp( + new THREE.Vector3( + targetX - (center.x - selectionGroup.current.position.x), + selectionGroup.current.position.y, + targetZ - (center.z - selectionGroup.current.position.z) + ), + moveSpeed + ); + } + } + } }); - toast.success("Object moved!"); - itemsData.current = []; - clearSelection(); - }; + const moveAssets = () => { + const updatedItems = floorItems.filter( + (item: { modelUuid: string }) => + !selectedAssets.some((asset: any) => asset.uuid === item.modelUuid) + ); + setFloorItems(updatedItems); + setMovedObjects(selectedAssets); + selectedAssets.forEach((asset: any) => { + selectionGroup.current.attach(asset); + }); + }; - const clearSelection = () => { - selectionGroup.current.children = []; - selectionGroup.current.position.set(0, 0, 0); - selectionGroup.current.rotation.set(0, 0, 0); - setpastedObjects([]); - setDuplicatedObjects([]); - setMovedObjects([]); - setRotatedObjects([]); - setSelectedAssets([]); - setKeyEvent(""); - }; + const placeMovedAssets = () => { + if (movedObjects.length === 0) return; - return ( - - ); + movedObjects.forEach(async (obj: THREE.Object3D) => { + const worldPosition = new THREE.Vector3(); + obj.getWorldPosition(worldPosition); + + selectionGroup.current.remove(obj); + obj.position.copy(worldPosition); + + if (itemsGroupRef.current) { + const newFloorItem: Types.FloorItemType = { + modelUuid: obj.uuid, + modelName: obj.userData.name, + modelfileID: obj.userData.modelId, + position: [worldPosition.x, worldPosition.y, worldPosition.z], + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + isLocked: false, + isVisible: true, + }; + + if (obj.userData.eventData) { + const eventData = useEventsStore.getState().getEventByModelUuid(obj.userData.modelUuid); + const productData = useProductStore.getState().getEventByModelUuid(selectedProduct.productId, obj.userData.modelUuid); + + if (eventData) { + useEventsStore.getState().updateEvent(obj.userData.modelUuid, { + position: [worldPosition.x, worldPosition.y, worldPosition.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }); + } + + if (productData) { + const event = useProductStore + .getState() + .updateEvent( + selectedProduct.productId, + obj.userData.modelUuid, + { + position: [worldPosition.x, worldPosition.y, worldPosition.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + } + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } + + newFloorItem.eventData = eventData; + } + } + + setFloorItems((prevItems: Types.FloorItems) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + //REST + + // await setFloorItemApi( + // organization, + // obj.uuid, + // obj.userData.name, + // [worldPosition.x, worldPosition.y, worldPosition.z], + // { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z }, + // obj.userData.modelId, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization, + modelUuid: newFloorItem.modelUuid, + modelName: newFloorItem.modelName, + modelfileID: newFloorItem.modelfileID, + position: newFloorItem.position, + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id, + }; + + socket.emit("v2:model-asset:add", data); + + itemsGroupRef.current.add(obj); + } + }); + toast.success("Object moved!"); + + itemsData.current = []; + clearSelection(); + }; + + const clearSelection = () => { + selectionGroup.current.children = []; + selectionGroup.current.position.set(0, 0, 0); + selectionGroup.current.rotation.set(0, 0, 0); + setpastedObjects([]); + setDuplicatedObjects([]); + setMovedObjects([]); + setRotatedObjects([]); + setSelectedAssets([]); + setKeyEvent(""); + }; + + return ( + + ); } export default MoveControls; diff --git a/app/src/modules/scene/controls/transformControls/transformControls.tsx b/app/src/modules/scene/controls/transformControls/transformControls.tsx new file mode 100644 index 0000000..5311f4f --- /dev/null +++ b/app/src/modules/scene/controls/transformControls/transformControls.tsx @@ -0,0 +1,206 @@ +import { TransformControls } from "@react-three/drei"; +import * as THREE from "three"; +import { useSelectedFloorItem, useObjectPosition, useObjectRotation, useFloorItems, useActiveTool, useSocketStore } from "../../../../store/store"; +import { useThree } from "@react-three/fiber"; + +import * as Types from '../../../../types/world/worldTypes'; +import { useEffect, useState } from "react"; +import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; +import { useEventsStore } from "../../../../store/simulation/useEventsStore"; +import { useProductStore } from "../../../../store/simulation/useProductStore"; +import { useSelectedProduct } from "../../../../store/simulation/useSimulationStore"; +import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertProductOrEventApi"; +import { setFloorItemApi } from "../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi"; + +export default function TransformControl() { + const state = useThree(); + const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null); + const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem(); + const { setObjectPosition } = useObjectPosition(); + const { setObjectRotation } = useObjectRotation(); + const { setFloorItems } = useFloorItems(); + const { activeTool } = useActiveTool(); + const { socket } = useSocketStore(); + const { selectedProduct } = useSelectedProduct(); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData, + }); + }; + + function handleObjectChange() { + if (selectedFloorItem) { + setObjectPosition(selectedFloorItem.position); + setObjectRotation({ + x: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.x), + y: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y), + z: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.z), + }); + } + } + + function handleMouseUp() { + if (selectedFloorItem) { + setObjectPosition(selectedFloorItem.position); + setObjectRotation({ + x: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.x), + y: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y), + z: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.z), + }); + } + setFloorItems((prevItems: Types.FloorItems) => { + if (!prevItems) { + return + } + let updatedItem: any = null; + const updatedItems = prevItems.map((item) => { + if (item.modelUuid === selectedFloorItem?.uuid) { + updatedItem = { + ...item, + position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z] as [number, number, number], + rotation: { x: selectedFloorItem.rotation.x, y: selectedFloorItem.rotation.y, z: selectedFloorItem.rotation.z }, + }; + return updatedItem; + } + return item; + }); + if (updatedItem && selectedFloorItem) { + if (updatedItem.eventData) { + const eventData = useEventsStore.getState().getEventByModelUuid(updatedItem.modelUuid); + const productData = useProductStore.getState().getEventByModelUuid(selectedProduct.productId, updatedItem.modelUuid); + + if (eventData) { + useEventsStore.getState().updateEvent(updatedItem.modelUuid, { + position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z], + rotation: [selectedFloorItem.rotation.x, selectedFloorItem.rotation.y, selectedFloorItem.rotation.z], + }); + } + + if (productData) { + const event = useProductStore + .getState() + .updateEvent( + selectedProduct.productId, + updatedItem.modelUuid, + { + position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z], + rotation: [selectedFloorItem.rotation.x, selectedFloorItem.rotation.y, selectedFloorItem.rotation.z], + } + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } + + updatedItem.eventData = eventData; + } + } + + setFloorItems((prevItems: Types.FloorItems) => { + const updatedItems = [...(prevItems || []), updatedItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + // REST + + // setFloorItemApi( + // organization, + // updatedItem.modelUuid, + // updatedItem.modelName, + // updatedItem.modelfileid, + // [selectedFloorItem.position.x, 0, selectedFloorItem.position.z,], + // { "x": selectedFloorItem.rotation.x, "y": selectedFloorItem.rotation.y, "z": selectedFloorItem.rotation.z }, + // false, + // true, + // ); + + // SOCKET + + const data = { + organization: organization, + modelUuid: updatedItem.modelUuid, + modelName: updatedItem.modelName, + modelfileID: updatedItem.modelfileID, + position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z], + rotation: { "x": selectedFloorItem.rotation.x, "y": selectedFloorItem.rotation.y, "z": selectedFloorItem.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id + } + + socket.emit("v2:model-asset:add", data); + } + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + } + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const keyCombination = detectModifierKeys(e); + if (!selectedFloorItem) return; + if (keyCombination === "G") { + setTransformMode((prev) => (prev === "translate" ? null : "translate")); + } + if (keyCombination === "R") { + setTransformMode((prev) => (prev === "rotate" ? null : "rotate")); + } + }; + + if (selectedFloorItem) { + window.addEventListener("keydown", handleKeyDown); + } else { + setTransformMode(null); + } + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [selectedFloorItem]); + + useEffect(() => { + if (activeTool === "delete") { + if (state.controls) { + const target = (state.controls as any).getTarget(new THREE.Vector3()); + (state.controls as any).setTarget(target.x, 0, target.z, true); + } + setSelectedFloorItem(null); + setObjectPosition({ x: undefined, y: undefined, z: undefined }); + setObjectRotation({ x: undefined, y: undefined, z: undefined }); + } + }, [activeTool]); + + return ( + <> + {(selectedFloorItem && transformMode) && + + } + + ); +} diff --git a/app/src/store/store.ts b/app/src/store/store.ts index 61d84d5..a86d289 100644 --- a/app/src/store/store.ts +++ b/app/src/store/store.ts @@ -144,11 +144,6 @@ export const useMovePoint = create((set: any) => ({ setMovePoint: (x: any) => set(() => ({ movePoint: x })), })); -export const useTransformMode = create((set: any) => ({ - transformMode: null, - setTransformMode: (x: any) => set(() => ({ transformMode: x })), -})); - export const useDeletePointOrLine = create((set: any) => ({ deletePointOrLine: false, setDeletePointOrLine: (x: any) => set(() => ({ deletePointOrLine: x })), diff --git a/app/src/types/world/worldTypes.d.ts b/app/src/types/world/worldTypes.d.ts index c148038..3d6115a 100644 --- a/app/src/types/world/worldTypes.d.ts +++ b/app/src/types/world/worldTypes.d.ts @@ -225,7 +225,6 @@ interface AssetConfiguration { scale?: [number, number, number]; csgscale?: [number, number, number]; csgposition?: [number, number, number]; - positionY?: (intersectionPoint: { point: THREE.Vector3 }) => number; type?: "Fixed-Move" | "Free-Move"; } -- 2.49.1 From 35fc19042795fd793c295273704cd7c29bc8c552 Mon Sep 17 00:00:00 2001 From: Poovizhi99 Date: Tue, 13 May 2025 13:05:13 +0530 Subject: [PATCH 04/24] working with activeTime incremented --- .../instances/animator/vehicleAnimator.tsx | 90 +++++++++- .../instances/instance/vehicleInstance.tsx | 157 +++++++++++++++++- 2 files changed, 243 insertions(+), 4 deletions(-) diff --git a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx index a3af481..9d51a8d 100644 --- a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx +++ b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx @@ -64,7 +64,91 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai } }, [isReset, isPlaying]) - useFrame((_, delta) => { + // useFrame((_, delta) => { + // const object = scene.getObjectByProperty('uuid', agvUuid); + // if (!object || currentPath.length < 2) return; + // if (isPaused) return; + + // let totalDistance = 0; + // const distances = []; + // let accumulatedDistance = 0; + // let index = 0; + // const rotationSpeed = 1; + + // for (let i = 0; i < currentPath.length - 1; i++) { + // const start = new THREE.Vector3(...currentPath[i]); + // const end = new THREE.Vector3(...currentPath[i + 1]); + // const segmentDistance = start.distanceTo(end); + // distances.push(segmentDistance); + // totalDistance += segmentDistance; + // } + + // while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) { + // accumulatedDistance += distances[index]; + // index++; + // } + + // if (index < distances.length) { + // const start = new THREE.Vector3(...currentPath[index]); + // const end = new THREE.Vector3(...currentPath[index + 1]); + // const segmentDistance = distances[index]; + + // const currentDirection = new THREE.Vector3().subVectors(end, start).normalize(); + // const targetAngle = Math.atan2(currentDirection.x, currentDirection.z); + + // const currentAngle = object.rotation.y; + + // let angleDifference = targetAngle - currentAngle; + // if (angleDifference > Math.PI) angleDifference -= 2 * Math.PI; + // if (angleDifference < -Math.PI) angleDifference += 2 * Math.PI; + + // const maxRotationStep = (rotationSpeed * speed * agvDetail.speed) * delta; + // object.rotation.y += Math.sign(angleDifference) * Math.min(Math.abs(angleDifference), maxRotationStep); + // const isAligned = Math.abs(angleDifference) < 0.01; + + // if (isAligned) { + // progressRef.current += delta * (speed * agvDetail.speed); + // const t = (progressRef.current - accumulatedDistance) / segmentDistance; + // const position = start.clone().lerp(end, t); + // object.position.copy(position); + // } + // } + + // if (progressRef.current >= totalDistance) { + // if (restRotation && objectRotation) { + // const targetEuler = new THREE.Euler( + // objectRotation.x, + // objectRotation.y - (agvDetail.point.action.steeringAngle), + // objectRotation.z + // ); + // const targetQuaternion = new THREE.Quaternion().setFromEuler(targetEuler); + // object.quaternion.slerp(targetQuaternion, delta * (rotationSpeed * speed * agvDetail.speed)); + // if (object.quaternion.angleTo(targetQuaternion) < 0.01) { + // object.quaternion.copy(targetQuaternion); + // object.rotation.copy(targetEuler); + // setRestingRotation(false); + // } + // return; + // } + // } + // if (progressRef.current >= totalDistance) { + // setRestingRotation(true); + // progressRef.current = 0; + // movingForward.current = !movingForward.current; + // setCurrentPath([]); + // handleCallBack(); + // if (currentPhase === 'pickup-drop') { + // requestAnimationFrame(startUnloadingProcess); + // } + // } + // }); + const lastTimeRef = useRef(performance.now()); + + useFrame(() => { + const now = performance.now(); + const delta = (now - lastTimeRef.current) / 1000; + lastTimeRef.current = now; + const object = scene.getObjectByProperty('uuid', agvUuid); if (!object || currentPath.length < 2) return; if (isPaused) return; @@ -95,7 +179,6 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai const currentDirection = new THREE.Vector3().subVectors(end, start).normalize(); const targetAngle = Math.atan2(currentDirection.x, currentDirection.z); - const currentAngle = object.rotation.y; let angleDifference = targetAngle - currentAngle; @@ -118,7 +201,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai if (restRotation && objectRotation) { const targetEuler = new THREE.Euler( objectRotation.x, - objectRotation.y - (agvDetail.point.action.steeringAngle), + objectRotation.y - agvDetail.point.action.steeringAngle, objectRotation.z ); const targetQuaternion = new THREE.Quaternion().setFromEuler(targetEuler); @@ -131,6 +214,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai return; } } + if (progressRef.current >= totalDistance) { setRestingRotation(true); progressRef.current = 0; diff --git a/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx b/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx index f4e7e61..3b6568d 100644 --- a/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx +++ b/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx @@ -20,16 +20,20 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) const { triggerPointActions } = useTriggerHandler(); const { getActionByUuid, getEventByModelUuid, getTriggerByUuid } = useProductStore(); const { selectedProduct } = useSelectedProduct(); - const { vehicles, setVehicleActive, setVehicleState, setVehiclePicking, clearCurrentMaterials, setVehicleLoad, decrementVehicleLoad, removeLastMaterial } = useVehicleStore(); + const { vehicles, setVehicleActive, setVehicleState, setVehiclePicking, clearCurrentMaterials, setVehicleLoad, decrementVehicleLoad, removeLastMaterial, incrementIdleTime, incrementActiveTime } = useVehicleStore(); + const [currentPhase, setCurrentPhase] = useState('stationed'); const [path, setPath] = useState<[number, number, number][]>([]); const pauseTimeRef = useRef(null); + const idleTimeRef = useRef(0); + const activeTimeRef = useRef(0); const isPausedRef = useRef(false); let startTime: number; let fixedInterval: number; const { speed } = useAnimationPlaySpeed(); const { isPaused } = usePauseButtonStore(); + useEffect(() => { isPausedRef.current = isPaused; }, [isPaused]); @@ -117,6 +121,157 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [vehicles, currentPhase, path, isPlaying]); + const previousTimeRef = useRef(null); // Tracks the last frame time + const isActiveRef = useRef(agvDetail.isActive); // Tracks the previous isActive state + const animationFrameIdRef = useRef(null); // Tracks the animation frame ID + + // // Animate function (moved outside useEffect) + // function animate(currentTime: number) { + // if (previousTimeRef.current === null) { + // // Initialize previousTime on the first frame + // previousTimeRef.current = currentTime; + // } + + // const deltaTime = (currentTime - previousTimeRef.current) / 1000; // Time difference in seconds + // previousTimeRef.current = currentTime; + + // if (idleTimeRef.current == 0) { + // activeTimeRef.current += deltaTime * speed; // Scale active time by speed + // const roundedActiveTime = Math.round(activeTimeRef.current); // Round to nearest integer + // console.log('Active Time:', roundedActiveTime, 'seconds'); + // incrementActiveTime(agvDetail.modelUuid, roundedActiveTime); + // incrementIdleTime(agvDetail.modelUuid, 0); + // } else if (activeTimeRef.current = 0) { + // idleTimeRef.current += deltaTime * speed; // Scale idle time by speed + // const roundedIdleTime = Math.round(idleTimeRef.current); // Round to nearest integer + // console.log('Idle Time:', roundedIdleTime, 'seconds'); + // incrementIdleTime(agvDetail.modelUuid, roundedIdleTime); + // incrementActiveTime(agvDetail.modelUuid, 0); + // } + + // // Request the next animation frame + // animationFrameIdRef.current = requestAnimationFrame(animate); + // } + + // useEffect(() => { + + // // Reset timers when transitioning between states + // if (!agvDetail.isActive) { + // activeTimeRef.current = 0; + // } else { + // idleTimeRef.current = 0; + // } + + // if (animationFrameIdRef.current === null) { + // animationFrameIdRef.current = requestAnimationFrame(animate); + // } + // console.log("veh", vehicles); + // // Cleanup function to stop the animation loop + // return () => { + // if (animationFrameIdRef.current !== null) { + // cancelAnimationFrame(animationFrameIdRef.current); + // animationFrameIdRef.current = null; // Reset the animation frame ID + // } + // }; + // }, [agvDetail.isActive]); + // Animate function (moved outside useEffect) + function animate(currentTime: number) { + if (previousTimeRef.current === null) { + // Initialize previousTime on the first frame + previousTimeRef.current = currentTime; + } + + const deltaTime = (currentTime - previousTimeRef.current) / 1000; // Time difference in seconds + previousTimeRef.current = currentTime; + + if (agvDetail.isActive) { + // AGV is active: Increment active time + activeTimeRef.current += deltaTime * speed; // Scale active time by speed + const roundedActiveTime = Math.round(activeTimeRef.current); // Round to nearest integer + + } else { + + idleTimeRef.current += deltaTime * speed; // Scale idle time by speed + const roundedIdleTime = Math.round(idleTimeRef.current); // Round to nearest integer + + } + + // Request the next animation frame + animationFrameIdRef.current = requestAnimationFrame(animate); + } + + useEffect(() => { + // Start or stop the animation based on the isActive state + + if (!agvDetail.isActive) { + // Transitioning to idle: Reset idle time + const roundedIdleTime = Math.round(idleTimeRef.current); // Get the final rounded idle time + console.log('Final Idle Time:', roundedIdleTime, 'seconds'); + incrementIdleTime(agvDetail.modelUuid, roundedIdleTime); + activeTimeRef.current = 0; + } else { + // Transitioning to active: Finalize idle time and pass it to incrementIdleTime + const roundedActiveTime = Math.round(activeTimeRef.current); // Round to nearest integer + console.log('Active Time:', roundedActiveTime, 'seconds'); + incrementActiveTime(agvDetail.modelUuid, roundedActiveTime); + idleTimeRef.current = 0; // Reset idle time + } + + + + // Start the animation loop if not already running + if (animationFrameIdRef.current === null) { + animationFrameIdRef.current = requestAnimationFrame(animate); + } + + // Cleanup function to stop the animation loop + return () => { + if (animationFrameIdRef.current !== null) { + cancelAnimationFrame(animationFrameIdRef.current); + animationFrameIdRef.current = null; // Reset the animation frame ID + } + }; + }, [agvDetail.isActive]); + // let position = 0; + // let previousTime: number; + + // function animate(currentTime: number) { + // previousTime = performance.now(); + + // const deltaTime = (currentTime - previousTime) / 1000; + // previousTime = currentTime; + + // position += speed * deltaTime; + // console.log('deltaTime: ', deltaTime); + // console.log('position: ', position); + // if (idleTimeRef.current === 0) { + // activeTimeRef.current = position; + // console.log('position: active', position); + // console.log(' activeTimeRef.current: ', activeTimeRef.current); + // incrementActiveTime(agvDetail.modelUuid, position) + // incrementIdleTime(agvDetail.modelUuid, idleTimeRef.current) + // } + // else if (activeTimeRef.current === 0) { + // idleTimeRef.current = position; + // console.log('position:idle ', position); + // console.log(' idleTimeRef.current: ', idleTimeRef.current); + // incrementActiveTime(agvDetail.modelUuid, activeTimeRef.current) + // incrementIdleTime(agvDetail.modelUuid, position) + // } + // } + + // useEffect(() => { + // if (agvDetail.isActive) { + // idleTimeRef.current = 0; + // activeTimeRef.current = performance.now(); + // requestAnimationFrame(animate); + // } else { + // activeTimeRef.current = 0; + // idleTimeRef.current = performance.now(); + // requestAnimationFrame(animate); + // } + // console.log('vehicles: ', vehicles); + // }, [agvDetail.isActive]); function handleCallBack() { if (currentPhase === 'stationed-pickup') { -- 2.49.1 From 48fc770b51d6d1d7226a60181f4550e3101b1f1b Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Tue, 13 May 2025 13:40:51 +0530 Subject: [PATCH 05/24] Refactor AssetProperties and TransformControl to improve object position and rotation handling; streamline socket store structure for better maintainability --- .../properties/AssetProperties.tsx | 31 +- .../transformControls/transformControls.tsx | 6 + app/src/store/store.ts | 432 +++++++++--------- 3 files changed, 237 insertions(+), 232 deletions(-) diff --git a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx index b81465d..25428d1 100644 --- a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx @@ -1,11 +1,10 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import InputToggle from "../../../ui/inputs/InputToggle"; import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; import { RemoveIcon } from "../../../icons/ExportCommonIcons"; import PositionInput from "../customInput/PositionInputs"; import RotationInput from "../customInput/RotationInput"; -import { useSelectedFloorItem } from "../../../../store/store"; -import * as THREE from "three"; +import { useSelectedFloorItem, useObjectPosition, useObjectRotation } from "../../../../store/store"; interface UserData { id: number; // Unique identifier for the user data @@ -17,6 +16,8 @@ const AssetProperties: React.FC = () => { const [userData, setUserData] = useState([]); // State to track user data const [nextId, setNextId] = useState(1); // Unique ID for new entries const { selectedFloorItem } = useSelectedFloorItem(); + const { objectPosition } = useObjectPosition(); + const { objectRotation } = useObjectRotation(); // Function to handle adding new user data const handleAddUserData = () => { const newUserData: UserData = { @@ -49,17 +50,19 @@ const AssetProperties: React.FC = () => { {/* Name */}
{selectedFloorItem.userData.name}
- {}} - value1={selectedFloorItem.position.x.toFixed(5)} - value2={selectedFloorItem.position.z.toFixed(5)} - /> - {}} - value={parseFloat( - THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y).toFixed(5) - )} - /> + {objectPosition.x && objectPosition.z && + { }} + value1={parseFloat(objectPosition.x.toFixed(5))} + value2={parseFloat(objectPosition.z.toFixed(5))} + /> + } + {objectRotation.y && + { }} + value={parseFloat(objectRotation.y.toFixed(5))} + /> + }
diff --git a/app/src/modules/scene/controls/transformControls/transformControls.tsx b/app/src/modules/scene/controls/transformControls/transformControls.tsx index 5311f4f..632c012 100644 --- a/app/src/modules/scene/controls/transformControls/transformControls.tsx +++ b/app/src/modules/scene/controls/transformControls/transformControls.tsx @@ -167,6 +167,12 @@ export default function TransformControl() { if (selectedFloorItem) { window.addEventListener("keydown", handleKeyDown); + setObjectPosition(selectedFloorItem.position); + setObjectRotation({ + x: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.x), + y: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y), + z: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.z), + }); } else { setTransformMode(null); } diff --git a/app/src/store/store.ts b/app/src/store/store.ts index a86d289..4218a4e 100644 --- a/app/src/store/store.ts +++ b/app/src/store/store.ts @@ -4,429 +4,425 @@ import { create } from "zustand"; import { io } from "socket.io-client"; export const useSocketStore = create((set: any, get: any) => ({ - socket: null, - initializeSocket: (email: string, organization: string) => { - const existingSocket = get().socket; - if (existingSocket) { - return; - } + socket: null, + initializeSocket: (email: string, organization: string) => { + const existingSocket = get().socket; + if (existingSocket) { + return; + } - const socket = io( - `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Builder`, - { - reconnection: true, - auth: { email, organization }, - } - ); + const socket = io( + `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Builder`, + { + reconnection: true, + auth: { email, organization }, + } + ); - const visualizationSocket = io( - `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Visualization`, - { - reconnection: true, - auth: { email, organization }, - } - ); + const visualizationSocket = io( + `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Visualization`, + { + reconnection: true, + auth: { email, organization }, + } + ); - set({ socket, visualizationSocket }); - }, - disconnectSocket: () => { - set((state: any) => { - state.socket?.disconnect(); - state.visualizationSocket?.disconnect(); - return { socket: null }; - }); - }, + set({ socket, visualizationSocket }); + }, + disconnectSocket: () => { + set((state: any) => { + state.socket?.disconnect(); + state.visualizationSocket?.disconnect(); + return { socket: null }; + }); + }, })); export const useLoadingProgress = create<{ - loadingProgress: number; - setLoadingProgress: (x: number) => void; + loadingProgress: number; + setLoadingProgress: (x: number) => void; }>((set) => ({ - loadingProgress: 1, - setLoadingProgress: (x: number) => set({ loadingProgress: x }), + loadingProgress: 1, + setLoadingProgress: (x: number) => set({ loadingProgress: x }), })); export const useOrganization = create((set: any) => ({ - organization: "", - setOrganization: (x: any) => set(() => ({ organization: x })), + organization: "", + setOrganization: (x: any) => set(() => ({ organization: x })), })); export const useToggleView = create((set: any) => ({ - toggleView: false, - setToggleView: (x: any) => set(() => ({ toggleView: x })), + toggleView: false, + setToggleView: (x: any) => set(() => ({ toggleView: x })), })); export const useUpdateScene = create((set: any) => ({ - updateScene: false, - setUpdateScene: (x: any) => set(() => ({ updateScene: x })), + updateScene: false, + setUpdateScene: (x: any) => set(() => ({ updateScene: x })), })); export const useWalls = create((set: any) => ({ - walls: [], - setWalls: (x: any) => set(() => ({ walls: x })), + walls: [], + setWalls: (x: any) => set(() => ({ walls: x })), })); export const useRoomsState = create((set: any) => ({ - roomsState: [], - setRoomsState: (x: any) => set(() => ({ roomsState: x })), + roomsState: [], + setRoomsState: (x: any) => set(() => ({ roomsState: x })), })); export const useZones = create((set: any) => ({ - zones: [], - setZones: (callback: any) => - set((state: any) => ({ - zones: typeof callback === "function" ? callback(state.zones) : callback, - })), + zones: [], + setZones: (callback: any) => + set((state: any) => ({ + zones: typeof callback === "function" ? callback(state.zones) : callback, + })), })); interface ZonePointsState { - zonePoints: THREE.Vector3[]; - setZonePoints: (points: THREE.Vector3[]) => void; + zonePoints: THREE.Vector3[]; + setZonePoints: (points: THREE.Vector3[]) => void; } export const useZonePoints = create((set) => ({ - zonePoints: [], - setZonePoints: (points) => set({ zonePoints: points }), + zonePoints: [], + setZonePoints: (points) => set({ zonePoints: points }), })); export const useSelectedItem = create((set: any) => ({ - selectedItem: { name: "", id: "", type: undefined }, - setSelectedItem: (x: any) => set(() => ({ selectedItem: x })), + selectedItem: { name: "", id: "", type: undefined }, + setSelectedItem: (x: any) => set(() => ({ selectedItem: x })), })); export const useNavMesh = create((set: any) => ({ - navMesh: null, - setNavMesh: (x: any) => set({ navMesh: x }), + navMesh: null, + setNavMesh: (x: any) => set({ navMesh: x }), })); export const useSelectedAssets = create((set: any) => ({ - selectedAssets: [], - setSelectedAssets: (x: any) => set(() => ({ selectedAssets: x })), + selectedAssets: [], + setSelectedAssets: (x: any) => set(() => ({ selectedAssets: x })), })); export const useLayers = create((set: any) => ({ - Layers: 1, - setLayers: (x: any) => set(() => ({ Layers: x })), + Layers: 1, + setLayers: (x: any) => set(() => ({ Layers: x })), })); export const useCamPosition = create((set: any) => ({ - camPosition: { x: undefined, y: undefined, z: undefined }, - setCamPosition: (newCamPosition: any) => set({ camPosition: newCamPosition }), + camPosition: { x: undefined, y: undefined, z: undefined }, + setCamPosition: (newCamPosition: any) => set({ camPosition: newCamPosition }), })); export const useMenuVisible = create((set: any) => ({ - menuVisible: false, - setMenuVisible: (x: any) => set(() => ({ menuVisible: x })), + menuVisible: false, + setMenuVisible: (x: any) => set(() => ({ menuVisible: x })), })); export const useDeleteTool = create((set: any) => ({ - deleteTool: false, - setDeleteTool: (x: any) => set(() => ({ deleteTool: x })), + deleteTool: false, + setDeleteTool: (x: any) => set(() => ({ deleteTool: x })), })); export const useToolMode = create((set: any) => ({ - toolMode: null, - setToolMode: (x: any) => set(() => ({ toolMode: x })), + toolMode: null, + setToolMode: (x: any) => set(() => ({ toolMode: x })), })); export const useNewLines = create((set: any) => ({ - newLines: [], - setNewLines: (x: any) => set(() => ({ newLines: x })), + newLines: [], + setNewLines: (x: any) => set(() => ({ newLines: x })), })); export const useDeletedLines = create((set: any) => ({ - deletedLines: [], - setDeletedLines: (x: any) => set(() => ({ deletedLines: x })), + deletedLines: [], + setDeletedLines: (x: any) => set(() => ({ deletedLines: x })), })); export const useMovePoint = create((set: any) => ({ - movePoint: false, - setMovePoint: (x: any) => set(() => ({ movePoint: x })), + movePoint: false, + setMovePoint: (x: any) => set(() => ({ movePoint: x })), })); export const useDeletePointOrLine = create((set: any) => ({ - deletePointOrLine: false, - setDeletePointOrLine: (x: any) => set(() => ({ deletePointOrLine: x })), + deletePointOrLine: false, + setDeletePointOrLine: (x: any) => set(() => ({ deletePointOrLine: x })), })); export const useFloorItems = create((set: any) => ({ - floorItems: null, - setFloorItems: (callback: any) => - set((state: any) => ({ - floorItems: - typeof callback === "function" ? callback(state.floorItems) : callback, - })), + floorItems: null, + setFloorItems: (callback: any) => + set((state: any) => ({ + floorItems: + typeof callback === "function" ? callback(state.floorItems) : callback, + })), })); export const useWallItems = create((set: any) => ({ - wallItems: [], - setWallItems: (callback: any) => - set((state: any) => ({ - wallItems: - typeof callback === "function" ? callback(state.wallItems) : callback, - })), + wallItems: [], + setWallItems: (callback: any) => + set((state: any) => ({ + wallItems: + typeof callback === "function" ? callback(state.wallItems) : callback, + })), })); export const useSelectedWallItem = create((set: any) => ({ - selectedWallItem: null, - setSelectedWallItem: (x: any) => set(() => ({ selectedWallItem: x })), + selectedWallItem: null, + setSelectedWallItem: (x: any) => set(() => ({ selectedWallItem: x })), })); export const useSelectedFloorItem = create((set: any) => ({ - selectedFloorItem: null, - setSelectedFloorItem: (x: any) => set(() => ({ selectedFloorItem: x })), + selectedFloorItem: null, + setSelectedFloorItem: (x: any) => set(() => ({ selectedFloorItem: x })), })); export const useDeletableFloorItem = create((set: any) => ({ - deletableFloorItem: null, - setDeletableFloorItem: (x: any) => set(() => ({ deletableFloorItem: x })), + deletableFloorItem: null, + setDeletableFloorItem: (x: any) => set(() => ({ deletableFloorItem: x })), })); export const useSetScale = create((set: any) => ({ - scale: null, - setScale: (x: any) => set(() => ({ scale: x })), + scale: null, + setScale: (x: any) => set(() => ({ scale: x })), })); export const useRoofVisibility = create((set: any) => ({ - roofVisibility: false, - setRoofVisibility: (x: any) => set(() => ({ roofVisibility: x })), + roofVisibility: false, + setRoofVisibility: (x: any) => set(() => ({ roofVisibility: x })), })); export const useWallVisibility = create((set: any) => ({ - wallVisibility: false, - setWallVisibility: (x: any) => set(() => ({ wallVisibility: x })), + wallVisibility: false, + setWallVisibility: (x: any) => set(() => ({ wallVisibility: x })), })); export const useShadows = create((set: any) => ({ - shadows: false, - setShadows: (x: any) => set(() => ({ shadows: x })), + shadows: false, + setShadows: (x: any) => set(() => ({ shadows: x })), })); export const useSunPosition = create((set: any) => ({ - sunPosition: { x: undefined, y: undefined, z: undefined }, - setSunPosition: (newSuntPosition: any) => - set({ sunPosition: newSuntPosition }), + sunPosition: { x: undefined, y: undefined, z: undefined }, + setSunPosition: (newSuntPosition: any) => + set({ sunPosition: newSuntPosition }), })); export const useRemoveLayer = create((set: any) => ({ - removeLayer: false, - setRemoveLayer: (x: any) => set(() => ({ removeLayer: x })), + removeLayer: false, + setRemoveLayer: (x: any) => set(() => ({ removeLayer: x })), })); export const useRemovedLayer = create((set: any) => ({ - removedLayer: null, - setRemovedLayer: (x: any) => set(() => ({ removedLayer: x })), + removedLayer: null, + setRemovedLayer: (x: any) => set(() => ({ removedLayer: x })), })); export const useActiveLayer = create((set: any) => ({ - activeLayer: 1, - setActiveLayer: (x: any) => set({ activeLayer: x }), + activeLayer: 1, + setActiveLayer: (x: any) => set({ activeLayer: x }), })); interface RefTextUpdateState { - refTextupdate: number; - setRefTextUpdate: ( - callback: (currentValue: number) => number | number - ) => void; + refTextupdate: number; + setRefTextUpdate: ( + callback: (currentValue: number) => number | number + ) => void; } export const useRefTextUpdate = create((set) => ({ - refTextupdate: -1000, - setRefTextUpdate: (callback) => - set((state) => ({ - refTextupdate: - typeof callback === "function" - ? callback(state.refTextupdate) - : callback, - })), + refTextupdate: -1000, + setRefTextUpdate: (callback) => + set((state) => ({ + refTextupdate: + typeof callback === "function" + ? callback(state.refTextupdate) + : callback, + })), })); export const useResetCamera = create((set: any) => ({ - resetCamera: false, - setResetCamera: (x: any) => set({ resetCamera: x }), + resetCamera: false, + setResetCamera: (x: any) => set({ resetCamera: x }), })); export const useAddAction = create((set: any) => ({ - addAction: null, - setAddAction: (x: any) => set({ addAction: x }), + addAction: null, + setAddAction: (x: any) => set({ addAction: x }), })); export const useActiveTool = create((set: any) => ({ - activeTool: "cursor", - setActiveTool: (x: any) => set({ activeTool: x }), + activeTool: "cursor", + setActiveTool: (x: any) => set({ activeTool: x }), })); export const useActiveSubTool = create((set: any) => ({ - activeSubTool: "cursor", - setActiveSubTool: (x: any) => set({ activeSubTool: x }), + activeSubTool: "cursor", + setActiveSubTool: (x: any) => set({ activeSubTool: x }), })); export const use2DUndoRedo = create((set: any) => ({ - is2DUndoRedo: null, - set2DUndoRedo: (x: any) => set({ is2DUndoRedo: x }), + is2DUndoRedo: null, + set2DUndoRedo: (x: any) => set({ is2DUndoRedo: x }), })); export const useElevation = create((set: any) => ({ - elevation: 45, - setElevation: (x: any) => set({ elevation: x }), + elevation: 45, + setElevation: (x: any) => set({ elevation: x }), })); export const useAzimuth = create((set: any) => ({ - azimuth: -160, - setAzimuth: (x: any) => set({ azimuth: x }), + azimuth: -160, + setAzimuth: (x: any) => set({ azimuth: x }), })); export const useRenderDistance = create((set: any) => ({ - renderDistance: 40, - setRenderDistance: (x: any) => set({ renderDistance: x }), + renderDistance: 40, + setRenderDistance: (x: any) => set({ renderDistance: x }), })); export const useCamMode = create((set: any) => ({ - camMode: "ThirdPerson", - setCamMode: (x: any) => set({ camMode: x }), + camMode: "ThirdPerson", + setCamMode: (x: any) => set({ camMode: x }), })); export const useUserName = create((set: any) => ({ - userName: "", - setUserName: (x: any) => set({ userName: x }), + userName: "", + setUserName: (x: any) => set({ userName: x }), })); export const useObjectPosition = create((set: any) => ({ - objectPosition: { x: undefined, y: undefined, z: undefined }, - setObjectPosition: (newObjectPosition: any) => - set({ objectPosition: newObjectPosition }), -})); - -export const useObjectScale = create((set: any) => ({ - objectScale: { x: undefined, y: undefined, z: undefined }, - setObjectScale: (newObjectScale: any) => set({ objectScale: newObjectScale }), + objectPosition: { x: undefined, y: undefined, z: undefined }, + setObjectPosition: (newObjectPosition: any) => + set({ objectPosition: newObjectPosition }), })); export const useObjectRotation = create((set: any) => ({ - objectRotation: { x: undefined, y: undefined, z: undefined }, - setObjectRotation: (newObjectRotation: any) => - set({ objectRotation: newObjectRotation }), + objectRotation: { x: undefined, y: undefined, z: undefined }, + setObjectRotation: (newObjectRotation: any) => + set({ objectRotation: newObjectRotation }), })); export const useDrieTemp = create((set: any) => ({ - drieTemp: undefined, - setDrieTemp: (x: any) => set({ drieTemp: x }), + drieTemp: undefined, + setDrieTemp: (x: any) => set({ drieTemp: x }), })); export const useActiveUsers = create((set: any) => ({ - activeUsers: [], - setActiveUsers: (callback: (prev: any[]) => any[] | any[]) => - set((state: { activeUsers: any[] }) => ({ - activeUsers: - typeof callback === "function" ? callback(state.activeUsers) : callback, - })), + activeUsers: [], + setActiveUsers: (callback: (prev: any[]) => any[] | any[]) => + set((state: { activeUsers: any[] }) => ({ + activeUsers: + typeof callback === "function" ? callback(state.activeUsers) : callback, + })), })); export const useDrieUIValue = create((set: any) => ({ - drieUIValue: { touch: null, temperature: null, humidity: null }, + drieUIValue: { touch: null, temperature: null, humidity: null }, - setDrieUIValue: (x: any) => - set((state: any) => ({ drieUIValue: { ...state.drieUIValue, ...x } })), + setDrieUIValue: (x: any) => + set((state: any) => ({ drieUIValue: { ...state.drieUIValue, ...x } })), - setTouch: (value: any) => - set((state: any) => ({ - drieUIValue: { ...state.drieUIValue, touch: value }, - })), - setTemperature: (value: any) => - set((state: any) => ({ - drieUIValue: { ...state.drieUIValue, temperature: value }, - })), - setHumidity: (value: any) => - set((state: any) => ({ - drieUIValue: { ...state.drieUIValue, humidity: value }, - })), + setTouch: (value: any) => + set((state: any) => ({ + drieUIValue: { ...state.drieUIValue, touch: value }, + })), + setTemperature: (value: any) => + set((state: any) => ({ + drieUIValue: { ...state.drieUIValue, temperature: value }, + })), + setHumidity: (value: any) => + set((state: any) => ({ + drieUIValue: { ...state.drieUIValue, humidity: value }, + })), })); export const useStartSimulation = create((set: any) => ({ - startSimulation: false, - setStartSimulation: (x: any) => set({ startSimulation: x }), + startSimulation: false, + setStartSimulation: (x: any) => set({ startSimulation: x }), })); export const useEyeDropMode = create((set: any) => ({ - eyeDropMode: false, - setEyeDropMode: (x: any) => set({ eyeDropMode: x }), + eyeDropMode: false, + setEyeDropMode: (x: any) => set({ eyeDropMode: x }), })); export const useEditingPoint = create((set: any) => ({ - editingPoint: false, - setEditingPoint: (x: any) => set({ editingPoint: x }), + editingPoint: false, + setEditingPoint: (x: any) => set({ editingPoint: x }), })); export const usezoneTarget = create((set: any) => ({ - zoneTarget: [], - setZoneTarget: (x: any) => set({ zoneTarget: x }), + zoneTarget: [], + setZoneTarget: (x: any) => set({ zoneTarget: x }), })); export const usezonePosition = create((set: any) => ({ - zonePosition: [], - setZonePosition: (x: any) => set({ zonePosition: x }), + zonePosition: [], + setZonePosition: (x: any) => set({ zonePosition: x }), })); interface EditPositionState { - Edit: boolean; - setEdit: (value: boolean) => void; + Edit: boolean; + setEdit: (value: boolean) => void; } export const useEditPosition = create((set) => ({ - Edit: false, - setEdit: (value) => set({ Edit: value }), + Edit: false, + setEdit: (value) => set({ Edit: value }), })); export const useAsset3dWidget = create((set: any) => ({ - widgetSelect: "", - setWidgetSelect: (x: any) => set({ widgetSelect: x }), + widgetSelect: "", + setWidgetSelect: (x: any) => set({ widgetSelect: x }), })); export const useWidgetSubOption = create((set: any) => ({ - widgetSubOption: "2D", - setWidgetSubOption: (x: any) => set({ widgetSubOption: x }), + widgetSubOption: "2D", + setWidgetSubOption: (x: any) => set({ widgetSubOption: x }), })); export const useLimitDistance = create((set: any) => ({ - limitDistance: true, - setLimitDistance: (x: any) => set({ limitDistance: x }), + limitDistance: true, + setLimitDistance: (x: any) => set({ limitDistance: x }), })); export const useTileDistance = create((set: any) => ({ - gridValue: { size: 300, divisions: 75 }, - planeValue: { height: 300, width: 300 }, + gridValue: { size: 300, divisions: 75 }, + planeValue: { height: 300, width: 300 }, - setGridValue: (value: any) => - set((state: any) => ({ - gridValue: { ...state.gridValue, ...value }, - })), + setGridValue: (value: any) => + set((state: any) => ({ + gridValue: { ...state.gridValue, ...value }, + })), - setPlaneValue: (value: any) => - set((state: any) => ({ - planeValue: { ...state.planeValue, ...value }, - })), + setPlaneValue: (value: any) => + set((state: any) => ({ + planeValue: { ...state.planeValue, ...value }, + })), })); export const usePlayAgv = create((set, get) => ({ - PlayAgv: [], - setPlayAgv: (updateFn: (prev: any[]) => any[]) => - set({ PlayAgv: updateFn(get().PlayAgv) }), + PlayAgv: [], + setPlayAgv: (updateFn: (prev: any[]) => any[]) => + set({ PlayAgv: updateFn(get().PlayAgv) }), })); + // Define the Asset type type Asset = { - id: string; - name: string; - position?: [number, number, number]; // Optional: 3D position - rotation?: { x: number; y: number; z: number }; // Optional: Euler rotation + id: string; + name: string; + position?: [number, number, number]; // Optional: 3D position + rotation?: { x: number; y: number; z: number }; // Optional: Euler rotation }; // Zustand store type type ZoneAssetState = { - zoneAssetId: Asset | null; - setZoneAssetId: (asset: Asset | null) => void; + zoneAssetId: Asset | null; + setZoneAssetId: (asset: Asset | null) => void; }; // Zustand store export const useZoneAssetId = create((set) => ({ - zoneAssetId: null, - setZoneAssetId: (asset) => set({ zoneAssetId: asset }), + zoneAssetId: null, + setZoneAssetId: (asset) => set({ zoneAssetId: asset }), })); -- 2.49.1 From d9b5f1e2d26a1d5813e6a6cf4a72f5a49a123ebf Mon Sep 17 00:00:00 2001 From: Nalvazhuthi Date: Tue, 13 May 2025 16:50:50 +0530 Subject: [PATCH 06/24] Developed Ui for shortcuts preview --- app/src/components/footer/Footer.tsx | 115 ++- app/src/components/footer/shortcutHelper.tsx | 312 ++++++- .../icons/RealTimeVisulationIcons.tsx | 3 + app/src/components/icons/ShortcutIcons.tsx | 827 ++++++++++++++++++ .../versionHisory/VersionHistory.tsx | 41 +- app/src/components/ui/Tools.tsx | 67 +- app/src/components/ui/list/List.tsx | 10 +- app/src/components/ui/menu/menu.tsx | 8 +- .../floating/DroppedFloatingWidgets.tsx | 1 + .../widgets/floating/cards/SimpleCard.tsx | 4 +- .../floating/cards/TotalCardComponent.tsx | 2 + app/src/store/store.ts | 14 + app/src/styles/components/footer/footer.scss | 390 ++++++--- app/src/styles/components/lists.scss | 11 +- .../components/marketPlace/marketPlace.scss | 18 +- app/src/styles/components/tools.scss | 34 +- app/src/styles/layout/sidebar.scss | 35 +- app/src/styles/pages/realTimeViz.scss | 4 +- 18 files changed, 1658 insertions(+), 238 deletions(-) create mode 100644 app/src/components/icons/ShortcutIcons.tsx diff --git a/app/src/components/footer/Footer.tsx b/app/src/components/footer/Footer.tsx index 39b1df0..ae38c3a 100644 --- a/app/src/components/footer/Footer.tsx +++ b/app/src/components/footer/Footer.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { HelpIcon } from "../icons/DashboardIcon"; import { useLogger } from "../ui/log/LoggerContext"; import { GetLogIcon } from "./getLogIcons"; @@ -7,59 +7,94 @@ import { CurserMiddleIcon, CurserRightIcon, } from "../icons/LogIcons"; +import ShortcutHelper from "./shortcutHelper"; +import { useShortcutStore } from "../../store/store"; +import { usePlayButtonStore } from "../../store/usePlayButtonStore"; const Footer: React.FC = () => { const { logs, setIsLogListVisible } = useLogger(); const lastLog = logs.length > 0 ? logs[logs.length - 1] : null; + const { showShortcuts, setShowShortcuts } = useShortcutStore(); + const { isPlaying } = usePlayButtonStore(); + + // Listen for Ctrl + Shift + ? + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ( + e.ctrlKey && + e.shiftKey && + (e.key === "?" || e.key === "/") // for some keyboards ? and / share the same key + ) { + e.preventDefault(); + setShowShortcuts(!showShortcuts); // toggle visibility directly + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [showShortcuts, setShowShortcuts]); + return ( -
-
-
-
- +
+
+
+
+
+ +
+
Selection
+
+
+
+ +
+
Rotate/Zoom
+
+
+
+ +
+
Pan/Context Menu
-
Selection
-
-
- + +
+
+
+
+
-
Rotate/Zoom
-
-
-
- +
+ V 0.01 +
+ +
-
Pan/Context Menu
-
-
-
-
- + {!isPlaying && ( +
+
-
- V 0.01 -
- -
-
-
+ )}
); }; diff --git a/app/src/components/footer/shortcutHelper.tsx b/app/src/components/footer/shortcutHelper.tsx index af6dacd..96ff8ad 100644 --- a/app/src/components/footer/shortcutHelper.tsx +++ b/app/src/components/footer/shortcutHelper.tsx @@ -1,7 +1,317 @@ import React from "react"; +import { + UndoIcon, + RedoIcon, + ESCIcon, + HelpIcon, + FindIcon, + InfoIcon, + CurserIcon, + DeleteIcon, + FreeHandIcon, + MeasurementToolIcon, + WallToolIcon, + ZoneToolIcon, + AisleToolIcon, + FloorToolIcon, + MoveIcon, + RotateIcon, + ToogleViewIcon, + UIVisiblityIcon, + FirstPersonViewIcon, + BuilderIcon, + SimulationIcon, + VisualizationIcon, + MarketplaceIcon, + CopyIcon, + PasteIcon, + DublicateIcon, + DuplicateInstanceIcon, + PlayIcon, + BrowserIcon, +} from "../icons/ShortcutIcons"; + +interface ShortcutItem { + keys: string[]; + name?: string; + description: string; + icon: any; +} + +interface ShortcutGroup { + category: string; + items: ShortcutItem[]; +} + const ShortcutHelper = () => { - return
; + const shortcuts: ShortcutGroup[] = [ + { + category: "Essential", + items: [ + { + keys: ["CTRL", "+", "Z"], + name: "Undo", + description: "Undo Last action", + icon: , + }, + { + keys: ["CTRL", "+", "Y"], + name: "Redo", + description: "Redo Last action", + icon: , + }, + { + keys: ["ESC"], + name: "Escape", + description: "Reset to Cursor & Stop Playback", + icon: , + }, + { + keys: ["CTRL", "+", "H"], + name: "Help", + description: "Open Help", + icon: , + }, + { + keys: ["CTRL", "+", "F"], + name: "Find", + description: "Find / Search Functionality", + icon: , + }, + { + keys: ["CTRL", "+", "?"], + name: "Info", + description: "Show Shortcut Info", + icon: , + }, + ], + }, + { + category: "Tools", + items: [ + { + keys: ["V"], + name: "Cursor Tool", + description: "Activate Cursor tool", + icon: , + }, + { + keys: ["X"], + name: "Delete Tool", + description: "Activate Delete tool", + icon: , + }, + { + keys: ["H"], + name: "Freehand Tool", + description: "Activate Free-Hand tool", + icon: , + }, + { + keys: ["M"], + name: "Measurement Tool", + description: "Activate Measurement tool", + icon: , + }, + { + keys: ["Q", "OR", "6"], + name: "Wall Tool", + description: "Select Wall floor tool (2D)", + icon: , + }, + { + keys: ["E", "OR", "8"], + name: "Zone Tool", + description: "Select Draw zone tool (2D)", + icon: , + }, + { + keys: ["R", "OR", "7"], + name: "Aisle Tool", + description: "Select Aisle floor tool (2D)", + icon: , + }, + { + keys: ["T", "OR", "9"], + name: "Floor Tool", + description: "Select Draw floor tool (2D)", + icon: , + }, + { + keys: ["G"], + name: "Move Asset", + description: "Move Selected Asset", + icon: , + }, + { + keys: ["R"], + name: "Rotate Asset", + description: "Rotate Selected Asset", + icon: , + }, + ], + }, + { + category: "View & Navigation", + items: [ + { + keys: ["TAB"], + name: "Toggle View", + description: "Toggle between 2D & 3D views (Builder)", + icon: , + }, + { + keys: ["CTRL", "+", "."], + name: "Toggle UI", + description: "Toggle UI Visibility", + icon: , + }, + { + keys: ["/"], + name: "First Person View", + description: "Switch to First-person View", + icon: , + }, + ], + }, + { + category: "Module Switching", + items: [ + { + keys: ["1"], + name: "Builder", + description: "Switch to Builder module", + icon: , + }, + { + keys: ["2"], + name: "Simulation", + description: "Switch to Simulation module", + icon: , + }, + { + keys: ["3"], + name: "Visualization", + description: "Switch to Visualization module", + icon: , + }, + { + keys: ["4"], + name: "Marketplace", + description: "Switch to Marketplace module", + icon: , + }, + ], + }, + { + category: "Selection", + items: [ + { + keys: ["CTRL", "+", "C"], + name: "Copy", + description: "Copy an Asset", + icon: , + }, + { + keys: ["CTRL", "+", "V"], + name: "Paste", + description: "Paste an Asset", + icon: , + }, + { + keys: ["CTRL", "+", "D"], + name: "Duplicate", + description: "Duplicate an Asset", + icon: , + }, + { + keys: ["ALT", "+", "D"], + name: "Duplicate (Instance)", + description: "Duplicate an Instanced Asset", + icon: , + }, + ], + }, + { + category: "Simulation", + items: [ + { + keys: ["CTRL", "+", "P"], + name: "Play", + description: "Play Simulation", + icon: , + }, + ], + }, + { + 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 || []; + + return ( +
+
+
+ {shortcuts.map((group) => ( + + ))} +
Keyboard
+
+
+ +
+ {activeShortcuts.map((item) => ( +
+
+
{item.icon}
+
+
{item.name}
+
{item.description}
+
+
+
+ {item.keys.map((key, i) => ( + + {key} + + ))} +
+
+ ))} +
+
+ ); }; export default ShortcutHelper; diff --git a/app/src/components/icons/RealTimeVisulationIcons.tsx b/app/src/components/icons/RealTimeVisulationIcons.tsx index 84d8cca..c38560d 100644 --- a/app/src/components/icons/RealTimeVisulationIcons.tsx +++ b/app/src/components/icons/RealTimeVisulationIcons.tsx @@ -166,3 +166,6 @@ export function StockIncreseIcon() { ); } + + + diff --git a/app/src/components/icons/ShortcutIcons.tsx b/app/src/components/icons/ShortcutIcons.tsx new file mode 100644 index 0000000..5a7c06f --- /dev/null +++ b/app/src/components/icons/ShortcutIcons.tsx @@ -0,0 +1,827 @@ +export function UndoIcon() { + return ( + + + + ); +} + +export function RedoIcon() { + return ( + + + + ); +} + +export function ESCIcon() { + return ( + + + + + + ); +} + +export function HelpIcon() { + return ( + + + + ); +} + +export function FindIcon() { + return ( + + + + ); +} + +export function InfoIcon() { + return ( + + + + + + ); +} + +export function CurserIcon() { + return ( + + + + ); +} + +export function DeleteIcon() { + return ( + + + + ); +} + +export function FreeHandIcon() { + return ( + + + + ); +} + +export function MeasurementToolIcon() { + return ( + + + + + + ); +} + +export function WallToolIcon() { + return ( + + + + + + + + + + + + + + + + + + ); +} + +export function ZoneToolIcon() { + return ( + + + + + + + + + + + + + + + + + + ); +} + +export function AisleToolIcon() { + return ( + + + + + + + + + ); +} + +export function FloorToolIcon() { + return ( + + + + + + + + + + ); +} + +export function MoveIcon() { + return ( + + + + ); +} + +export function RotateIcon() { + return ( + + + + + + + ); +} + +export function ToogleViewIcon() { + return ( + + + + ); +} + +export function UIVisiblityIcon() { + return ( + + + + + ); +} + +export function FirstPersonViewIcon() { + return ( + + + + + + + + + ); +} + +export function BuilderIcon() { + return ( + + + + ); +} + +export function SimulationIcon() { + return ( + + + + + + + + + + ); +} + +export function VisualizationIcon() { + return ( + + + + + + ); +} + +export function MarketplaceIcon() { + return ( + + + + + + + ); +} + +export function CopyIcon() { + return ( + + + + ); +} + +export function PasteIcon() { + return ( + + + + ); +} + +export function DublicateIcon() { + return ( + + + + ); +} + +export function DuplicateInstanceIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} + +export function BrowserIcon() { + return ( + + + + ); +} diff --git a/app/src/components/layout/sidebarRight/versionHisory/VersionHistory.tsx b/app/src/components/layout/sidebarRight/versionHisory/VersionHistory.tsx index 9e49b48..a3cb286 100644 --- a/app/src/components/layout/sidebarRight/versionHisory/VersionHistory.tsx +++ b/app/src/components/layout/sidebarRight/versionHisory/VersionHistory.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; import { AddIcon, + ArrowIcon, CloseIcon, KebabIcon, LocationIcon, } from "../../../icons/ExportCommonIcons"; +import RenameInput from "../../../ui/inputs/RenameInput"; const VersionHistory = () => { - // Start with only v1.0 const initialVersions = [ { versionName: "v1.0", @@ -20,7 +21,6 @@ const VersionHistory = () => { const [selectedVersion, setSelectedVersion] = useState(initialVersions[0]); const userName = localStorage.getItem("userName") ?? "Anonymous"; - // Function to simulate adding a new version const addNewVersion = () => { const newVersionNumber = versions.length + 1; const newVersion = { @@ -30,7 +30,7 @@ const VersionHistory = () => { month: "long", day: "2-digit", }), - savedBy: userName, // Simulate user name + savedBy: userName, }; const updated = [newVersion, ...versions]; @@ -38,24 +38,25 @@ const VersionHistory = () => { setSelectedVersion(newVersion); }; - // Handle user selecting a version const handleSelectVersion = (version: any) => { setSelectedVersion(version); const reordered = [version, ...versions.filter((v) => v !== version)]; setVersions(reordered); }; + const handleTimestampChange = (newTimestamp: string, index: number) => { + const updatedVersions = [...versions]; + updatedVersions[index].timestamp = newTimestamp; + setVersions(updatedVersions); + }; + return (
{/* Header */}
Version History
-
@@ -95,16 +96,26 @@ const VersionHistory = () => { {versions.map((version, index) => ( ))} diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index 2187fdc..c5457c4 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -31,6 +31,7 @@ import { useToolMode, useTransformMode, useActiveSubTool, + useShortcutStore, } from "../../store/store"; import useToggleStore from "../../store/useUIToggleStore"; import { @@ -133,7 +134,7 @@ const Tools: React.FC = () => { switch (activeTool) { case "cursor": if (toggleView) { - setToolMode('move'); + setToolMode("move"); } else { setTransformMode("translate"); } @@ -200,16 +201,19 @@ const Tools: React.FC = () => { setActiveTool(activeTool); }, [activeTool, toggleView]); + const { showShortcuts } = useShortcutStore(); + return ( <> {!isPlaying ? ( -
+
{activeSubTool == "cursor" && (
{ setActiveTool("cursor"); }} @@ -220,8 +224,9 @@ const Tools: React.FC = () => { )} {activeSubTool == "free-hand" && (
{ setActiveTool("free-hand"); }} @@ -232,8 +237,9 @@ const Tools: React.FC = () => { )} {activeSubTool == "delete" && (
{ setActiveTool("delete"); }} @@ -306,8 +312,9 @@ const Tools: React.FC = () => {
{ setActiveTool("draw-wall"); }} @@ -316,8 +323,9 @@ const Tools: React.FC = () => {
{ setActiveTool("draw-zone"); }} @@ -326,8 +334,9 @@ const Tools: React.FC = () => {
{ setActiveTool("draw-aisle"); }} @@ -336,8 +345,9 @@ const Tools: React.FC = () => {
{ setActiveTool("draw-floor"); }} @@ -353,8 +363,9 @@ const Tools: React.FC = () => {
{ setActiveTool("measure"); }} @@ -370,8 +381,9 @@ const Tools: React.FC = () => {
{ setActiveTool("pen"); }} @@ -408,8 +420,9 @@ const Tools: React.FC = () => {
{ setActiveTool("comment"); }} @@ -419,8 +432,9 @@ const Tools: React.FC = () => {
{toggleThreeD && (
{ setIsPlaying(!isPlaying); }} @@ -434,8 +448,9 @@ const Tools: React.FC = () => { <>
toggle view (tab)
diff --git a/app/src/components/ui/list/List.tsx b/app/src/components/ui/list/List.tsx index 80ecf8a..04ffbb1 100644 --- a/app/src/components/ui/list/List.tsx +++ b/app/src/components/ui/list/List.tsx @@ -158,13 +158,13 @@ const List: React.FC = ({ items = [], remove }) => {
    {items?.map((item) => ( -
  • +
  • handleSelectZone(item.id)} + >
    - + ); + + const renderSubMenu = (submenu: MenuItem[], parentLabel: string) => ( +
    + {submenu.map((item) => ( + + ))} +
    + ); + + return ( +
    setOpenMenu(false)}>
    - {/* File Menu */} -
    setActiveMenu("File")} - onMouseLeave={() => { - setActiveMenu(null); - setActiveSubMenu(null); - }} - > -
    - File - - - -
    - - {/* File Dropdown */} - {activeMenu === "File" && ( -
    - {/* New File */} -
    toggleSelection("New File")} - > -
    - New File -
    - Ctrl + N -
    -
    -
    - - {/* Open Local File */} -
    toggleSelection("Open Local File")} - > -
    - Open Local File -
    - Ctrl + O -
    -
    -
    - - {/* Save Version */} -
    toggleSelection("Save Version")} - > -
    - Save Version -
    -
    -
    - - {/* Make a Copy */} -
    toggleSelection("Make a Copy")} - > -
    - Make a Copy -
    -
    - - {/* Share */} -
    toggleSelection("Share")} - > -
    - Share -
    -
    - - {/* Rename */} -
    toggleSelection("Rename")} - > -
    - Rename -
    -
    -
    - - {/* Import */} -
    toggleSelection("Import")} - > -
    - Import -
    -
    - - {/* Close File */} -
    toggleSelection("Close File")} - > -
    - Close File -
    -
    + {Object.entries(menus).map(([menu, items]) => ( +
    - {/* Edit Menu */} -
    setActiveMenu("Edit")} - onMouseLeave={() => { - setActiveMenu(null); - setActiveSubMenu(null); - }} - > -
    - Edit - - - -
    - - {/* Edit Dropdown */} - {activeMenu === "Edit" && ( -
    - {/* Undo */} -
    toggleSelection("Undo")} - > -
    - Undo -
    - Ctrl + Z -
    -
    -
    - - {/* Redo */} -
    toggleSelection("Redo")} - > -
    - Redo -
    - Ctrl + Shift + Z -
    -
    -
    -
    - - {/* Undo History */} -
    toggleSelection("Undo History")} - > -
    - Undo History -
    -
    - - {/* Redo History */} -
    toggleSelection("Redo History")} - > -
    - Redo History -
    -
    -
    - - {/* Find */} -
    toggleSelection("Find")} - > -
    - Find -
    - Ctrl + F -
    -
    -
    - - {/* Delete */} -
    toggleSelection("Delete")} - > -
    - Delete -
    -
    - - {/* Select by... */} -
    toggleSelection("Select by...")} - > -
    - Select by... -
    -
    - - {/* Keymap */} -
    toggleSelection("Keymap")} - > -
    - Keymap -
    -
    -
    - )} -
    - - {/* View Menu */} -
    setActiveMenu("View")} - onMouseLeave={() => { - setActiveMenu(null); - setActiveSubMenu(null); - }} - > -
    - View - - - -
    - - {/* View Dropdown */} - {activeMenu === "View" && ( -
    - {/* Grid */} -
    toggleSelection("Grid")} - > -
    - Grid -
    -
    - - {/* Gizmo */} -
    setActiveSubMenu("View-Gizmo")} - onMouseLeave={() => setActiveSubMenu(null)} - > -
    - Gizmo - - - -
    -
    - - {/* Gizmo Submenu */} - {activeSubMenu === "View-Gizmo" && ( -
    - {/* Visibility */} -
    toggleSelection("Visibility")} + {activeMenu === menu && ( +
    + {items.map((item) => + item.submenu ? ( +
    -
    - - {/* Cube view */} -
    toggleSelection("Cube view")} - > - Cube view -
    - - {/* Sphere view */} -
    toggleSelection("Sphere view")} - > - Sphere view -
    -
    +
    + {item.label} + + + +
    + {activeSubMenu === item.label && + renderSubMenu(item.submenu, item.label)} + + ) : ( + renderMenuItem(item) + ) )}
    + )} + + ))} - {/* Zoom */} -
    toggleSelection("Zoom")} - > -
    - Zoom -
    -
    - - {/* Full Screen */} -
    toggleSelection("Full Screen")} - > -
    - Full Screen -
    - F11 -
    -
    -
    -
    - )} -
    - - {/* Version History Menu */} -
    setActiveMenu("Version history")} onMouseLeave={() => { @@ -392,173 +205,27 @@ const MenuBar: React.FC = ({ setOpenMenu }) => { }} >
    Version history
    -
    + - {/* Export As Menu */} -
    setActiveMenu("Export as...")} + onMouseEnter={() => setActiveMenu("Theme")} onMouseLeave={() => { setActiveMenu(null); setActiveSubMenu(null); }} - > -
    Export as...
    -
    - -
    setActiveMenu("theme")} - onMouseLeave={() => { - setActiveMenu(null); - setActiveSubMenu(null); - }} - onClick={() => { - handleThemeChange(); - }} + onClick={handleThemeChange} >
    Theme
    {savedTheme}
    -
    + - {/* Apps Menu */} - {/*
    setActiveMenu("Apps")} - onMouseLeave={() => { - setActiveMenu(null); - setActiveSubMenu(null); - }} - > -
    - Apps - - - -
    - - {activeMenu === "Apps" && ( -
    -
    toggleSelection("New App")} - > -
    - - - New App - -
    -
    -
    - -
    toggleSelection("Work-flow Monitor")} - > -
    - - - Work-flow Monitor - -
    -
    - -
    toggleSelection("Temperature Visualizer")} - > -
    - - - Temperature Visualizer - -
    -
    - -
    toggleSelection("View all")} - > -
    - - - View all - -
    -
    -
    - )} -
    */} - - {/* Help Menu */} -
    setActiveMenu("Help")} - onMouseLeave={() => { - setActiveMenu(null); - setActiveSubMenu(null); - }} - > -
    - Help - - - -
    - - {/* Help Dropdown */} - {activeMenu === "Help" && ( -
    - {/* Shortcuts */} -
    toggleSelection("Shortcuts")} - > -
    - Shortcuts -
    - Ctrl + Shift + ? -
    -
    -
    - - {/* Manual */} -
    toggleSelection("Manual")} - > -
    - Manual -
    -
    - - {/* Video Tutorials */} -
    toggleSelection("Video Tutorials")} - > -
    - Video Tutorials -
    -
    - - {/* Report a bug */} -
    toggleSelection("Report a bug")} - > -
    - Report a bug -
    -
    -
    - )} -
    -
    + {/* Log out */} +
    +
    ); diff --git a/app/src/styles/components/menu/menu.scss b/app/src/styles/components/menu/menu.scss index d25173c..4a27f4f 100644 --- a/app/src/styles/components/menu/menu.scss +++ b/app/src/styles/components/menu/menu.scss @@ -102,6 +102,7 @@ padding: 4px; .menu-item-container { position: relative; + width: 100%; .menu-item { padding: 4px 8px 4px 12px; border-radius: #{$border-radius-medium}; -- 2.49.1 From 2c6708117339f6a508ef076a4126437584b8b6aa Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Tue, 13 May 2025 17:21:52 +0530 Subject: [PATCH 10/24] Refactor success notifications to use echo instead of toast; update selection controls and duplication logic - Changed success notifications from `toast.success` to `echo.success` in copy, duplication, move, rotate, and selection controls. - Updated selection logic in `PostProcessing` to use `flattenChildren` for better performance. - Enhanced `Simulator` component to handle selected product state and execution order more effectively. - Modified `useSelectedItem` state to include `category` and `subCategory` properties for better item classification. - Adjusted `WallItem` interface to standardize type values and added `modelfileID` for improved asset management. --- .../components/layout/sidebarLeft/Assets.tsx | 20 +- .../IntialLoad/loadInitialFloorItems.ts | 5 +- .../IntialLoad/loadInitialWallItems.ts | 118 +- app/src/modules/builder/builder.tsx | 28 - app/src/modules/builder/csg/csg.tsx | 11 +- .../builder/eventFunctions/handleMeshDown.ts | 39 +- .../geomentries/assets/addAssetModel.ts | 4 +- .../geomentries/assets/deleteFloorItems.ts | 2 +- .../builder/geomentries/layers/deleteLayer.ts | 2 +- .../builder/geomentries/lines/deleteLine.ts | 2 +- .../geomentries/pillars/deletePillar.ts | 2 +- .../builder/geomentries/points/deletePoint.ts | 2 +- .../builder/geomentries/walls/addWallItems.ts | 204 +- .../geomentries/walls/deleteWallItems.ts | 9 +- .../builder/groups/floorItemsGroup.tsx | 2 +- .../modules/builder/groups/wallItemsGroup.tsx | 52 +- .../builder/groups/wallsAndWallItems.tsx | 3 - app/src/modules/builder/groups/wallsMesh.tsx | 2 +- .../socket/socketResponses.dev.tsx | 1911 +++++++++-------- .../selectionControls/copyPasteControls.tsx | 2 +- .../selectionControls/duplicationControls.tsx | 2 +- .../selectionControls/moveControls.tsx | 2 +- .../selectionControls/rotateControls.tsx | 2 +- .../selectionControls/selectionControls.tsx | 2 +- .../scene/postProcessing/postProcessing.tsx | 4 +- .../simulation/simulator/simulator.tsx | 13 +- app/src/store/store.ts | 2 +- app/src/types/world/worldTypes.d.ts | 17 +- 28 files changed, 1257 insertions(+), 1207 deletions(-) diff --git a/app/src/components/layout/sidebarLeft/Assets.tsx b/app/src/components/layout/sidebarLeft/Assets.tsx index dd2c2a2..e693684 100644 --- a/app/src/components/layout/sidebarLeft/Assets.tsx +++ b/app/src/components/layout/sidebarLeft/Assets.tsx @@ -140,16 +140,14 @@ const Assets: React.FC = () => { alt={asset.filename} className="asset-image" onPointerDown={() => { - if (asset.category !== 'Feneration') { - setSelectedItem({ - name: asset.filename, - id: asset.AssetID, - type: - asset.type === "undefined" - ? undefined - : asset.type, - }); - } + setSelectedItem({ + name: asset.filename, + id: asset.AssetID, + type: + asset.type === "undefined" + ? undefined + : asset.type + }); }} /> @@ -206,6 +204,8 @@ const Assets: React.FC = () => { asset.type === "undefined" ? undefined : asset.type, + category: asset.category, + subCategory: asset.subCategory }); }} /> diff --git a/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts b/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts index e625116..0d96c8d 100644 --- a/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts +++ b/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts @@ -71,7 +71,6 @@ async function loadInitialFloorItems( // Check Three.js Cache const cachedModel = THREE.Cache.get(item.modelfileID!); if (cachedModel) { - // processLoadedModel(cachedModel.scene.clone(), item, itemsGroup, setFloorItems, addEvent); modelsLoaded++; checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, resolve); @@ -81,7 +80,6 @@ async function loadInitialFloorItems( // Check IndexedDB const indexedDBModel = await retrieveGLTF(item.modelfileID!); if (indexedDBModel) { - // const blobUrl = URL.createObjectURL(indexedDBModel); loader.load(blobUrl, (gltf) => { URL.revokeObjectURL(blobUrl); @@ -102,7 +100,6 @@ async function loadInitialFloorItems( } // Fetch from Backend - // const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${item.modelfileID!}`; loader.load(modelUrl, async (gltf) => { const modelBlob = await fetch(modelUrl).then((res) => res.blob()); @@ -338,7 +335,7 @@ function checkLoadingCompletion( resolve: () => void ) { if (modelsLoaded === modelsToLoad) { - toast.success("Models Loaded!"); + echo.success("Models Loaded!"); dracoLoader.dispose(); } resolve(); diff --git a/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts b/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts index f9799a8..2258f3b 100644 --- a/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts +++ b/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts @@ -1,54 +1,102 @@ -import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; - +import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import * as THREE from 'three'; import * as Types from "../../../types/world/worldTypes"; import { getWallItems } from '../../../services/factoryBuilder/assest/wallAsset/getWallItemsApi'; - -////////// Load the Wall Items's intially of there is any ////////// +import { retrieveGLTF, storeGLTF } from '../../../utils/indexDB/idbUtils'; async function loadInitialWallItems( setWallItems: Types.setWallItemSetState, - AssetConfigurations: Types.AssetConfigurations ): Promise { + try { + const email = localStorage.getItem('email'); + if (!email) { + throw new Error('No email found in localStorage'); + } - const email = localStorage.getItem('email') - const organization = (email!.split("@")[1]).split(".")[0]; + const organization = email.split("@")[1].split(".")[0]; + const items = await getWallItems(organization); - const items = await getWallItems(organization); + let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; - localStorage.setItem("WallItems", JSON.stringify(items)); - if (items.length > 0) { - const storedWallItems: Types.wallItems = items; + if (!items || items.length === 0) { + localStorage.removeItem("WallItems"); + return; + } - const loadedWallItems = await Promise.all(storedWallItems.map(async (item) => { - const loader = new GLTFLoader(); + localStorage.setItem("WallItems", JSON.stringify(items)); + + 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 loadedWallItems = await Promise.all(items.map(async (item: Types.WallItem) => { + // Check THREE.js cache first + const cachedModel = THREE.Cache.get(item.modelName!); + if (cachedModel) { + return processModel(cachedModel, item); + } + + // Check IndexedDB cache + const cachedModelBlob = await retrieveGLTF(item.modelfileID!); + if (cachedModelBlob) { + const blobUrl = URL.createObjectURL(cachedModelBlob); + return new Promise((resolve) => { + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.add(item.modelName!, gltf); + resolve(processModel(gltf, item)); + }); + }); + } + + // Load from original URL if not cached + const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${item.modelfileID!}`; return new Promise((resolve) => { - loader.load(AssetConfigurations[item.modelName!].modelUrl, (gltf) => { - const model = gltf.scene; - model.uuid = item.modelUuid!; - - model.children[0].children.forEach((child: any) => { - if (child.name !== "CSG_REF") { - child.castShadow = true; - child.receiveShadow = true; - } + loader.load(modelUrl, async (gltf) => { + try { + // Cache the model + const modelBlob = await fetch(modelUrl).then((res) => res.blob()); + await storeGLTF(item.modelName!, modelBlob); + THREE.Cache.add(item.modelName!, gltf); + resolve(processModel(gltf, item)); + } catch (error) { + console.error('Failed to cache model:', error); + resolve(processModel(gltf, item)); + } }); - - resolve({ - type: item.type, - model: model, - modelName: item.modelName, - scale: item.scale, - csgscale: item.csgscale, - csgposition: item.csgposition, - position: item.position, - quaternion: item.quaternion, - }); - }); }); })); setWallItems(loadedWallItems); + } catch (error) { + console.error('Failed to load wall items:', error); } } -export default loadInitialWallItems; +function processModel(gltf: GLTF, item: Types.WallItem): Types.WallItem { + const model = gltf.scene.clone(); + model.uuid = item.modelUuid!; + + model.children[0]?.children?.forEach((child: THREE.Object3D) => { + if (child.name !== "CSG_REF") { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + return { + type: item.type, + model: model, + modelName: item.modelName, + modelfileID: item.modelfileID, + scale: item.scale, + csgscale: item.csgscale, + csgposition: item.csgposition, + position: item.position, + quaternion: item.quaternion, + }; +} + +export default loadInitialWallItems; \ No newline at end of file diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index f43cf3d..993bc8f 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -126,32 +126,6 @@ export default function Builder() { // dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); // loader.setDRACOLoader(dracoLoader); - ////////// Assest Configuration Values ////////// - - const AssetConfigurations: Types.AssetConfigurations = { - arch: { - modelUrl: arch, - scale: [0.75, 0.75, 0.75], - csgscale: [2, 4, 0.5], - csgposition: [0, 2, 0], - type: "Fixed-Move", - }, - door: { - modelUrl: door, - scale: [0.75, 0.75, 0.75], - csgscale: [2, 4, 0.5], - csgposition: [0, 2, 0], - type: "Fixed-Move", - }, - window: { - modelUrl: Window, - scale: [0.75, 0.75, 0.75], - csgscale: [5, 3, 0.5], - csgposition: [0, 1.5, 0], - type: "Free-Move", - }, - }; - ////////// All Toggle's ////////// useEffect(() => { @@ -247,7 +221,6 @@ export default function Builder() { floorGroupAisle={floorGroupAisle} scene={scene} onlyFloorlines={onlyFloorlines} - AssetConfigurations={AssetConfigurations} itemsGroup={itemsGroup} isTempLoader={isTempLoader} tempLoader={tempLoader} @@ -260,7 +233,6 @@ export default function Builder() { = (props) => { } }); } - props.hoveredDeletableWallItem.current = hovered ? object : null; + let currentObject = object; + while (currentObject) { + if (currentObject.name === "Scene") { + break; + } + currentObject = currentObject.parent as THREE.Mesh; + } + if (currentObject) { + props.hoveredDeletableWallItem.current = hovered ? currentObject : null; + } }; return ( diff --git a/app/src/modules/builder/eventFunctions/handleMeshDown.ts b/app/src/modules/builder/eventFunctions/handleMeshDown.ts index 1f00038..9fb7839 100644 --- a/app/src/modules/builder/eventFunctions/handleMeshDown.ts +++ b/app/src/modules/builder/eventFunctions/handleMeshDown.ts @@ -35,29 +35,26 @@ function handleMeshDown( } if (event.intersections.length > 0) { - const clickedIndex = wallItems.findIndex((item) => item.model === event.intersections[0]?.object?.parent?.parent); - if (clickedIndex !== -1) { - setSelectedItemsIndex(clickedIndex); - const wallItemModel = wallItems[clickedIndex]?.model; - if (wallItemModel && wallItemModel.parent && wallItemModel.parent.parent) { - currentWallItem.current = (wallItemModel.parent.parent.children[0]?.children[1]?.children[0] as Types.Mesh) || null; - setSelectedWallItem(wallItemModel.parent); - // currentWallItem.current?.children.forEach((child) => { - // if ((child as THREE.Mesh).isMesh && child.name !== "CSG_REF") { - // const material = (child as THREE.Mesh).material; - // if (Array.isArray(material)) { - // material.forEach(mat => { - // if (mat instanceof THREE.MeshStandardMaterial) { - // mat.emissive = new THREE.Color("green"); - // } - // }); - // } else if (material instanceof THREE.MeshStandardMaterial) { - // material.emissive = new THREE.Color("green"); - // } - // } - // }); + if (event.object) { + const wallItemModel = event.object; + let currentObject = wallItemModel as THREE.Object3D; + while (currentObject) { + if (currentObject.name === "Scene") { + break; + } + currentObject = currentObject.parent as THREE.Object3D; + } + if (!currentObject) return; + const clickedIndex = wallItems.findIndex((item) => item.model?.uuid === currentObject.uuid); + if (clickedIndex !== -1) { + setSelectedItemsIndex(clickedIndex); + const wallItemModel = wallItems[clickedIndex]?.model; + if (wallItemModel) { + setSelectedWallItem(wallItemModel); + } } } + } } } diff --git a/app/src/modules/builder/geomentries/assets/addAssetModel.ts b/app/src/modules/builder/geomentries/assets/addAssetModel.ts index 6023422..692664e 100644 --- a/app/src/modules/builder/geomentries/assets/addAssetModel.ts +++ b/app/src/modules/builder/geomentries/assets/addAssetModel.ts @@ -397,7 +397,7 @@ async function handleModelLoad( socket.emit("v2:model-asset:add", completeData); gsap.to(model.position, { y: newFloorItem.position[1], duration: 1.5, ease: "power2.out" }); - gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: "power2.out", onComplete: () => { toast.success("Model Added!"); } }); + gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: "power2.out", onComplete: () => { echo.success("Model Added!"); } }); } else { const data = { @@ -421,7 +421,7 @@ async function handleModelLoad( socket.emit("v2:model-asset:add", data); gsap.to(model.position, { y: newFloorItem.position[1], duration: 1.5, ease: "power2.out" }); - gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: "power2.out", onComplete: () => { toast.success("Model Added!"); } }); + gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: "power2.out", onComplete: () => { echo.success("Model Added!"); } }); } } diff --git a/app/src/modules/builder/geomentries/assets/deleteFloorItems.ts b/app/src/modules/builder/geomentries/assets/deleteFloorItems.ts index f13ebee..5b234b4 100644 --- a/app/src/modules/builder/geomentries/assets/deleteFloorItems.ts +++ b/app/src/modules/builder/geomentries/assets/deleteFloorItems.ts @@ -80,7 +80,7 @@ async function DeleteFloorItems( } setFloorItems(updatedItems); - toast.success("Model Removed!"); + echo.success("Model Removed!"); } } } diff --git a/app/src/modules/builder/geomentries/layers/deleteLayer.ts b/app/src/modules/builder/geomentries/layers/deleteLayer.ts index 41afa6e..4fe2a28 100644 --- a/app/src/modules/builder/geomentries/layers/deleteLayer.ts +++ b/app/src/modules/builder/geomentries/layers/deleteLayer.ts @@ -83,7 +83,7 @@ async function DeleteLayer( floorGroup.current?.remove(meshToRemove); } - toast.success("Layer Removed!"); + echo.success("Layer Removed!"); setRemovedLayer(null); } export default DeleteLayer; diff --git a/app/src/modules/builder/geomentries/lines/deleteLine.ts b/app/src/modules/builder/geomentries/lines/deleteLine.ts index 14a5e27..42b32b3 100644 --- a/app/src/modules/builder/geomentries/lines/deleteLine.ts +++ b/app/src/modules/builder/geomentries/lines/deleteLine.ts @@ -82,7 +82,7 @@ function deleteLine( } }); - toast.success("Line Removed!"); + echo.success("Line Removed!"); } export default deleteLine; diff --git a/app/src/modules/builder/geomentries/pillars/deletePillar.ts b/app/src/modules/builder/geomentries/pillars/deletePillar.ts index b735c81..39e0b28 100644 --- a/app/src/modules/builder/geomentries/pillars/deletePillar.ts +++ b/app/src/modules/builder/geomentries/pillars/deletePillar.ts @@ -13,7 +13,7 @@ function DeletePillar( (hoveredDeletablePillar.current.material).dispose(); (hoveredDeletablePillar.current.geometry).dispose(); floorGroup.current.remove(hoveredDeletablePillar.current); - toast.success("Pillar Removed!"); + echo.success("Pillar Removed!"); hoveredDeletablePillar.current = undefined; } } diff --git a/app/src/modules/builder/geomentries/points/deletePoint.ts b/app/src/modules/builder/geomentries/points/deletePoint.ts index 937e57e..827818f 100644 --- a/app/src/modules/builder/geomentries/points/deletePoint.ts +++ b/app/src/modules/builder/geomentries/points/deletePoint.ts @@ -51,7 +51,7 @@ function deletePoint( RemoveConnectedLines(DeletedPointUUID, floorPlanGroupLine, floorPlanGroupPoint, setDeletedLines, lines); - toast.success("Point Removed!"); + echo.success("Point Removed!"); } export default deletePoint; diff --git a/app/src/modules/builder/geomentries/walls/addWallItems.ts b/app/src/modules/builder/geomentries/walls/addWallItems.ts index 8b472ec..415fd35 100644 --- a/app/src/modules/builder/geomentries/walls/addWallItems.ts +++ b/app/src/modules/builder/geomentries/walls/addWallItems.ts @@ -1,108 +1,138 @@ -import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import { toast } from 'react-toastify'; - import * as THREE from 'three'; import * as Types from "../../../../types/world/worldTypes"; import * as CONSTANTS from '../../../../types/world/worldConstants'; -// import { setWallItem } from '../../../../services/factoryBuilder/assest/wallAsset/setWallItemApi'; import { Socket } from 'socket.io-client'; +import { retrieveGLTF, storeGLTF } from '../../../../utils/indexDB/idbUtils'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; async function AddWallItems( - selected: Types.String, + selected: any, raycaster: THREE.Raycaster, CSGGroup: Types.RefMesh, - AssetConfigurations: Types.AssetConfigurations, setWallItems: Types.setWallItemSetState, socket: Socket ): Promise { - - ////////// Load Wall GLtf's and set the positions, rotation, type etc. in state and store in localstorage ////////// - let intersects = raycaster?.intersectObject(CSGGroup.current!, true); const wallRaycastIntersection = intersects?.find((child) => child.object.name.includes("WallRaycastReference")); + let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; - if (wallRaycastIntersection) { - const intersectionPoint = wallRaycastIntersection; - const loader = new GLTFLoader(); - loader.load(AssetConfigurations[selected].modelUrl, async (gltf) => { - const model = gltf.scene; - model.userData = { wall: intersectionPoint.object.parent }; - model.children[0].children.forEach((child) => { - if (child.name !== "CSG_REF") { - child.castShadow = true; - child.receiveShadow = true; - } + if (!wallRaycastIntersection) return; + + const intersectionPoint = wallRaycastIntersection; + 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); + + // Check THREE.js cache first + const cachedModel = THREE.Cache.get(selected.id); + if (cachedModel) { + handleModelLoad(cachedModel); + return; + } + + // Check IndexedDB cache + const cachedModelBlob = await retrieveGLTF(selected.id); + if (cachedModelBlob) { + const blobUrl = URL.createObjectURL(cachedModelBlob); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(selected.id, gltf); + handleModelLoad(gltf); + }); + return; + } + + // Load from backend if not in any cache + loader.load(`${url_Backend_dwinzo}/api/v2/AssetFile/${selected.id}`, async (gltf) => { + try { + const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v2/AssetFile/${selected.id}`).then((res) => res.blob()); + await storeGLTF(selected.id, modelBlob); + THREE.Cache.add(selected.id, gltf); + await handleModelLoad(gltf); + } catch (error) { + console.error('Failed to cache model:', error); + handleModelLoad(gltf); + } + }); + + async function handleModelLoad(gltf: GLTF) { + const model = gltf.scene.clone(); + model.userData = { wall: intersectionPoint.object.parent }; + + model.children[0].children.forEach((child) => { + if (child.name !== "CSG_REF") { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const boundingBox = new THREE.Box3().setFromObject(model); + const size = new THREE.Vector3(); + boundingBox.getSize(size); + + const csgscale = [size.x, size.y, size.z] as [number, number, number]; + + const center = new THREE.Vector3(); + boundingBox.getCenter(center); + const csgposition = [center.x, center.y, center.z] as [number, number, number]; + + let positionY = selected.subCategory === 'fixed-move' ? 0 : intersectionPoint.point.y; + if (positionY === 0) { + positionY = Math.floor(intersectionPoint.point.y / CONSTANTS.wallConfig.height) * CONSTANTS.wallConfig.height; + } + + const newWallItem = { + type: selected.subCategory, + model: model, + modelName: selected.name, + modelfileID: selected.id, + scale: [1, 1, 1] as [number, number, number], + csgscale: csgscale, + csgposition: csgposition, + position: [intersectionPoint.point.x, positionY, intersectionPoint.point.z] as [number, number, number], + quaternion: intersectionPoint.object.quaternion.clone() as Types.QuaternionType + }; + + const email = localStorage.getItem('email'); + const organization = email ? (email.split("@")[1]).split(".")[0] : 'default'; + + const data = { + organization: organization, + modelUuid: model.uuid, + modelName: newWallItem.modelName, + modelfileID: selected.id, + type: selected.subCategory, + csgposition: newWallItem.csgposition, + csgscale: newWallItem.csgscale, + position: newWallItem.position, + quaternion: newWallItem.quaternion, + scale: newWallItem.scale, + socketId: socket.id + }; + + socket.emit('v1:wallItems:set', data); + + setWallItems((prevItems) => { + const updatedItems = [...prevItems, newWallItem]; + + const WallItemsForStorage = updatedItems.map(item => { + const { model, ...rest } = item; + return { + ...rest, + modelUuid: model?.uuid, + }; }); - const config = AssetConfigurations[selected]; - let positionY = config.type === 'Fixed-Move' ? 0 : intersectionPoint.point.y; - if (positionY === 0) { - positionY = Math.floor(intersectionPoint.point.y / CONSTANTS.wallConfig.height) * CONSTANTS.wallConfig.height; - } - - const newWallItem = { - type: config.type, - model: model, - modelName: selected, - scale: config.scale, - csgscale: config.csgscale, - csgposition: config.csgposition, - position: [intersectionPoint.point.x, positionY, intersectionPoint.point.z] as [number, number, number], - quaternion: intersectionPoint.object.quaternion.clone() as Types.QuaternionType - }; - - const email = localStorage.getItem('email') - const organization = (email!.split("@")[1]).split(".")[0]; - - //REST - - // await setWallItem( - // organization, - // model.uuid, - // newWallItem.modelName, - // newWallItem.type!, - // newWallItem.csgposition!, - // newWallItem.csgscale!, - // newWallItem.position, - // newWallItem.quaternion, - // newWallItem.scale!, - // ) - - //SOCKET - - const data = { - organization: organization, - modelUuid: model.uuid, - modelName: newWallItem.modelName, - type: newWallItem.type!, - csgposition: newWallItem.csgposition!, - csgscale: newWallItem.csgscale!, - position: newWallItem.position, - quaternion: newWallItem.quaternion, - scale: newWallItem.scale!, - socketId: socket.id - } - - socket.emit('v1:wallItems:set', data); - - setWallItems((prevItems) => { - const updatedItems = [...prevItems, newWallItem]; - - const WallItemsForStorage = updatedItems.map(item => { - const { model, ...rest } = item; - return { - ...rest, - modelUuid: model?.uuid, - }; - }); - - localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); - toast.success("Model Added!"); - - return updatedItems; - }); + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + echo.success("Model Added!"); + return updatedItems; }); } } -export default AddWallItems; +export default AddWallItems; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/walls/deleteWallItems.ts b/app/src/modules/builder/geomentries/walls/deleteWallItems.ts index b5d40f4..80bc75f 100644 --- a/app/src/modules/builder/geomentries/walls/deleteWallItems.ts +++ b/app/src/modules/builder/geomentries/walls/deleteWallItems.ts @@ -1,5 +1,3 @@ -import { toast } from 'react-toastify'; - import * as Types from "../../../../types/world/worldTypes"; // import { deleteWallItem } from '../../../../services/factoryBuilder/assest/wallAsset/deleteWallItemApi'; import { Socket } from 'socket.io-client'; @@ -13,11 +11,11 @@ function DeleteWallItems( ////////// Deleting the hovered Wall GLTF from thewallItems and also update it in the localstorage ////////// - if (hoveredDeletableWallItem.current && hoveredDeletableWallItem.current.parent) { + if (hoveredDeletableWallItem.current && hoveredDeletableWallItem.current) { setWallItems([]); let WallItemsRef = wallItems; - const removedItem = WallItemsRef.find((item) => item.model?.uuid === hoveredDeletableWallItem.current?.parent?.uuid); - const Items = WallItemsRef.filter((item) => item.model?.uuid !== hoveredDeletableWallItem.current?.parent?.uuid); + const removedItem = WallItemsRef.find((item) => item.model?.uuid === hoveredDeletableWallItem.current?.uuid); + const Items = WallItemsRef.filter((item) => item.model?.uuid !== hoveredDeletableWallItem.current?.uuid); setTimeout(async () => { WallItemsRef = Items; @@ -50,7 +48,6 @@ function DeleteWallItems( }); localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); - toast.success("Model Removed!"); hoveredDeletableWallItem.current = null; }, 50); } diff --git a/app/src/modules/builder/groups/floorItemsGroup.tsx b/app/src/modules/builder/groups/floorItemsGroup.tsx index e58530b..8bd52f9 100644 --- a/app/src/modules/builder/groups/floorItemsGroup.tsx +++ b/app/src/modules/builder/groups/floorItemsGroup.tsx @@ -362,7 +362,7 @@ const FloorItemsGroup = ({ const onDrop = (event: any) => { if (!event.dataTransfer?.files[0]) return; - if (selectedItem.id !== "" && event.dataTransfer?.files[0]) { + if (selectedItem.id !== "" && event.dataTransfer?.files[0] && selectedItem.category !== 'Fenestration') { addAssetModel( raycaster, state.camera, diff --git a/app/src/modules/builder/groups/wallItemsGroup.tsx b/app/src/modules/builder/groups/wallItemsGroup.tsx index faae06d..7bdb9e7 100644 --- a/app/src/modules/builder/groups/wallItemsGroup.tsx +++ b/app/src/modules/builder/groups/wallItemsGroup.tsx @@ -7,6 +7,7 @@ import { useSelectedWallItem, useSocketStore, useWallItems, + useSelectedItem, } from "../../../store/store"; import { Csg } from "../csg/csg"; import * as Types from "../../../types/world/worldTypes"; @@ -21,7 +22,6 @@ import useModuleStore from "../../../store/useModuleStore"; const WallItemsGroup = ({ currentWallItem, - AssetConfigurations, hoveredDeletableWallItem, selectedItemsIndex, setSelectedItemsIndex, @@ -37,34 +37,22 @@ const WallItemsGroup = ({ const { deletePointOrLine } = useDeletePointOrLine(); const { setSelectedWallItem } = useSelectedWallItem(); const { activeModule } = useModuleStore(); + const { selectedItem, setSelectedItem } = useSelectedItem(); useEffect(() => { // Load Wall Items from the backend - loadInitialWallItems(setWallItems, AssetConfigurations); + loadInitialWallItems(setWallItems); }, []); - ////////// Update the Scale value changes in thewallItems State ////////// - ////////// Update the Position value changes in the selected item ////////// - ////////// Update the Rotation value changes in the selected item ////////// - useEffect(() => { const canvasElement = state.gl.domElement; function handlePointerMove(e: any) { - if ( - selectedItemsIndex !== null && - !deletePointOrLine && - e.buttons === 1 - ) { + if (selectedItemsIndex !== null && !deletePointOrLine && e.buttons === 1) { const Raycaster = state.raycaster; - const intersects = Raycaster.intersectObjects( - CSGGroup.current?.children[0].children!, - true - ); - const Object = intersects.find((child) => - child.object.name.includes("WallRaycastReference") - ); + const intersects = Raycaster.intersectObjects(CSGGroup.current?.children[0].children!, true); + const Object = intersects.find((child) => child.object.name.includes("WallRaycastReference")); if (Object) { (state.controls as any)!.enabled = false; @@ -72,14 +60,14 @@ const WallItemsGroup = ({ const updatedItems = [...prevItems]; let position: [number, number, number] = [0, 0, 0]; - if (updatedItems[selectedItemsIndex].type === "Fixed-Move") { + if (updatedItems[selectedItemsIndex].type === "fixed-move") { position = [ Object!.point.x, Math.floor(Object!.point.y / CONSTANTS.wallConfig.height) * CONSTANTS.wallConfig.height, Object!.point.z, ]; - } else if (updatedItems[selectedItemsIndex].type === "Free-Move") { + } else if (updatedItems[selectedItemsIndex].type === "free-move") { position = [Object!.point.x, Object!.point.y, Object!.point.z]; } @@ -95,8 +83,7 @@ const WallItemsGroup = ({ updatedItems[selectedItemsIndex] = { ...updatedItems[selectedItemsIndex], position: position, - quaternion: - Object!.object.quaternion.clone() as Types.QuaternionType, + quaternion: Object!.object.quaternion.clone() as Types.QuaternionType, }; return updatedItems; @@ -128,10 +115,7 @@ const WallItemsGroup = ({ }); currentItem = updatedItems[selectedItemsIndex]; - localStorage.setItem( - "WallItems", - JSON.stringify(WallItemsForStorage) - ); + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); return updatedItems; }); @@ -145,6 +129,7 @@ const WallItemsGroup = ({ // organization, // currentItem?.model?.uuid, // currentItem.modelName, + // currentItem.modelfileID, // currentItem.type!, // currentItem.csgposition!, // currentItem.csgscale!, @@ -158,6 +143,7 @@ const WallItemsGroup = ({ const data = { organization: organization, modelUuid: currentItem.model?.uuid!, + modelfileID: currentItem.modelfileID, modelName: currentItem.modelName!, type: currentItem.type!, csgposition: currentItem.csgposition!, @@ -217,21 +203,19 @@ const WallItemsGroup = ({ }; const onDrop = (event: any) => { - if (!event.dataTransfer?.files[0]) return; + if (selectedItem.category !== 'Fenestration') return; pointer.x = (event.clientX / window.innerWidth) * 2 - 1; pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; + raycaster.setFromCamera(pointer, camera); - if (AssetConfigurations[event.dataTransfer.files[0].name.split(".")[0]]) { - const selected = event.dataTransfer.files[0].name.split(".")[0]; - - if (AssetConfigurations[selected]?.type) { + if (selectedItem.id) { + if (selectedItem.subCategory) { AddWallItems( - selected, + selectedItem, raycaster, CSGGroup, - AssetConfigurations, setWallItems, socket ); @@ -257,7 +241,7 @@ const WallItemsGroup = ({ canvasElement.removeEventListener("drop", onDrop); canvasElement.removeEventListener("dragover", onDragOver); }; - }, [deleteTool, wallItems]); + }, [deleteTool, wallItems, selectedItem, camera]); useEffect(() => { if (deleteTool && activeModule === "builder") { diff --git a/app/src/modules/builder/groups/wallsAndWallItems.tsx b/app/src/modules/builder/groups/wallsAndWallItems.tsx index 19d5833..320a3bf 100644 --- a/app/src/modules/builder/groups/wallsAndWallItems.tsx +++ b/app/src/modules/builder/groups/wallsAndWallItems.tsx @@ -10,11 +10,9 @@ import handleMeshDown from "../eventFunctions/handleMeshDown"; import handleMeshMissed from "../eventFunctions/handleMeshMissed"; import WallsMesh from "./wallsMesh"; import WallItemsGroup from "./wallItemsGroup"; -import { useEffect } from "react"; const WallsAndWallItems = ({ CSGGroup, - AssetConfigurations, setSelectedItemsIndex, selectedItemsIndex, currentWallItem, @@ -63,7 +61,6 @@ const WallsAndWallItems = ({ { - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; + useEffect(() => { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - if (!socket) return; + if (!socket) return; - socket.on("cameraCreateResponse", (data: any) => { - // console.log('data: ', data); - }); + socket.on("cameraCreateResponse", (data: any) => { + // console.log('data: ', data); + }); - socket.on("userConnectRespones", (data: any) => { - // console.log('data: ', data); - }); + socket.on("userConnectRespones", (data: any) => { + // console.log('data: ', data); + }); - socket.on("userDisConnectRespones", (data: any) => { - // console.log('data: ', data); - }); + socket.on("userDisConnectRespones", (data: any) => { + // console.log('data: ', data); + }); - socket.on("cameraUpdateResponse", (data: any) => { - // console.log('data: ', data); - }); + socket.on("cameraUpdateResponse", (data: any) => { + // console.log('data: ', data); + }); - socket.on("EnvironmentUpdateResponse", (data: any) => { - // console.log('data: ', data); - }); + socket.on("EnvironmentUpdateResponse", (data: any) => { + // console.log('data: ', data); + }); - socket.on("model-asset:response:updates", async (data: any) => { - // console.log('data: ', data); - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "Model created successfully") { - 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); - let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; - - try { - isTempLoader.current = true; - const cachedModel = THREE.Cache.get(data.data.modelName); - let url; - if (cachedModel) { - // console.log(`Getting ${data.data.modelName} from cache`); - const model = cachedModel.scene.clone(); - model.uuid = data.data.modelUuid; - model.userData = { - name: data.data.modelName, - modelId: data.data.modelfileID, - modelUuid: data.data.modelUuid, - }; - model.position.set( - ...(data.data.position as [number, number, number]) - ); - model.rotation.set( - data.data.rotation.x, - data.data.rotation.y, - data.data.rotation.z - ); - model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); - - model.traverse((child: any) => { - if (child.isMesh) { - // Clone the material to ensure changes are independent - // child.material = child.material.clone(); - - child.castShadow = true; - child.receiveShadow = true; - } - }); - - itemsGroup.current.add(model); - - if (tempLoader.current) { - tempLoader.current.material.dispose(); - tempLoader.current.geometry.dispose(); - itemsGroup.current.remove(tempLoader.current); - tempLoader.current = undefined; + socket.on("model-asset:response:updates", async (data: any) => { + // console.log('data: ', data); + if (socket.id === data.socketId) { + return; } - - const newFloorItem: Types.FloorItemType = { - modelUuid: data.data.modelUuid, - modelName: data.data.modelName, - modelfileID: data.data.modelfileID, - position: [...(data.data.position as [number, number, number])], - rotation: { - x: model.rotation.x, - y: model.rotation.y, - z: model.rotation.z, - }, - isLocked: data.data.isLocked, - isVisible: data.data.isVisible, - }; - - setFloorItems((prevItems: any) => { - const updatedItems = [...(prevItems || []), newFloorItem]; - localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); - return updatedItems; - }); - - gsap.to(model.position, { - y: data.data.position[1], - duration: 1.5, - ease: "power2.out", - }); - gsap.to(model.scale, { - x: 1, - y: 1, - z: 1, - duration: 1.5, - ease: "power2.out", - onComplete: () => { - toast.success("Model Added!"); - }, - }); - } else { - const indexedDBModel = await retrieveGLTF(data.data.modelName); - if (indexedDBModel) { - // console.log(`Getting ${data.data.modelName} from IndexedDB`); - url = URL.createObjectURL(indexedDBModel); - } else { - // console.log(`Getting ${data.data.modelName} from Backend`); - url = `${url_Backend_dwinzo}/api/v2/AssetFile/${data.data.modelfileID}`; - const modelBlob = await fetch(url).then((res) => res.blob()); - await storeGLTF(data.data.modelfileID, modelBlob); + if (organization !== data.organization) { + return; } - } + if (data.message === "Model created successfully") { + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); - if (url) { - loadModel(url); - } - } catch (error) { - echo.error("Failed to update responce"); - console.error("Error fetching asset model:", error); - } - - function loadModel(url: string) { - loader.load( - url, - (gltf) => { - URL.revokeObjectURL(url); - THREE.Cache.remove(url); - const model = gltf.scene; - model.uuid = data.data.modelUuid; - model.userData = { - name: data.data.modelName, - modelId: data.data.modelfileID, - modelUuid: data.data.modelUuid, - }; - model.position.set( - ...(data.data.position as [number, number, number]) - ); - model.rotation.set( - data.data.rotation.x, - data.data.rotation.y, - data.data.rotation.z - ); - model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); - - model.traverse((child: any) => { - if (child.isMesh) { - // Clone the material to ensure changes are independent - // child.material = child.material.clone(); - - child.castShadow = true; - child.receiveShadow = true; - } - }); - - itemsGroup.current.add(model); - - if (tempLoader.current) { - tempLoader.current.material.dispose(); - tempLoader.current.geometry.dispose(); - itemsGroup.current.remove(tempLoader.current); - tempLoader.current = undefined; - } - - const newFloorItem: Types.FloorItemType = { - modelUuid: data.data.modelUuid, - modelName: data.data.modelName, - modelfileID: data.data.modelfileID, - position: [...(data.data.position as [number, number, number])], - rotation: { - x: model.rotation.x, - y: model.rotation.y, - z: model.rotation.z, - }, - isLocked: data.data.isLocked, - isVisible: data.data.isVisible, - }; - - setFloorItems((prevItems: any) => { - const updatedItems = [...(prevItems || []), newFloorItem]; - localStorage.setItem( - "FloorItems", - JSON.stringify(updatedItems) + dracoLoader.setDecoderPath( + "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/" ); - return updatedItems; - }); + loader.setDRACOLoader(dracoLoader); + let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; - gsap.to(model.position, { - y: data.data.position[1], - duration: 1.5, - ease: "power2.out", - }); - gsap.to(model.scale, { - x: 1, - y: 1, - z: 1, - duration: 1.5, - ease: "power2.out", - onComplete: () => { - toast.success("Model Added!"); - }, - }); + try { + isTempLoader.current = true; + const cachedModel = THREE.Cache.get(data.data.modelName); + let url; + if (cachedModel) { + // console.log(`Getting ${data.data.modelName} from cache`); + const model = cachedModel.scene.clone(); + model.uuid = data.data.modelUuid; + model.userData = { + name: data.data.modelName, + modelId: data.data.modelfileID, + modelUuid: data.data.modelUuid, + }; + model.position.set( + ...(data.data.position as [number, number, number]) + ); + model.rotation.set( + data.data.rotation.x, + data.data.rotation.y, + data.data.rotation.z + ); + model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); - THREE.Cache.add(data.data.modelName, gltf); - }, - () => { - TempLoader( - new THREE.Vector3(...data.data.position), - isTempLoader, - tempLoader, - itemsGroup - ); + model.traverse((child: any) => { + if (child.isMesh) { + // Clone the material to ensure changes are independent + // child.material = child.material.clone(); + + child.castShadow = true; + child.receiveShadow = true; + } + }); + + itemsGroup.current.add(model); + + if (tempLoader.current) { + tempLoader.current.material.dispose(); + tempLoader.current.geometry.dispose(); + itemsGroup.current.remove(tempLoader.current); + tempLoader.current = undefined; + } + + const newFloorItem: Types.FloorItemType = { + modelUuid: data.data.modelUuid, + modelName: data.data.modelName, + modelfileID: data.data.modelfileID, + position: [...(data.data.position as [number, number, number])], + rotation: { + x: model.rotation.x, + y: model.rotation.y, + z: model.rotation.z, + }, + isLocked: data.data.isLocked, + isVisible: data.data.isVisible, + }; + + setFloorItems((prevItems: any) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + gsap.to(model.position, { + y: data.data.position[1], + duration: 1.5, + ease: "power2.out", + }); + gsap.to(model.scale, { + x: 1, + y: 1, + z: 1, + duration: 1.5, + ease: "power2.out", + onComplete: () => { + echo.success("Model Added!"); + }, + }); + } else { + const indexedDBModel = await retrieveGLTF(data.data.modelName); + if (indexedDBModel) { + // console.log(`Getting ${data.data.modelName} from IndexedDB`); + url = URL.createObjectURL(indexedDBModel); + } else { + // console.log(`Getting ${data.data.modelName} from Backend`); + url = `${url_Backend_dwinzo}/api/v2/AssetFile/${data.data.modelfileID}`; + const modelBlob = await fetch(url).then((res) => res.blob()); + await storeGLTF(data.data.modelfileID, modelBlob); + } + } + + if (url) { + loadModel(url); + } + } catch (error) { + echo.error("Failed to update responce"); + console.error("Error fetching asset model:", error); + } + + function loadModel(url: string) { + loader.load( + url, + (gltf) => { + URL.revokeObjectURL(url); + THREE.Cache.remove(url); + const model = gltf.scene; + model.uuid = data.data.modelUuid; + model.userData = { + name: data.data.modelName, + modelId: data.data.modelfileID, + modelUuid: data.data.modelUuid, + }; + model.position.set( + ...(data.data.position as [number, number, number]) + ); + model.rotation.set( + data.data.rotation.x, + data.data.rotation.y, + data.data.rotation.z + ); + model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); + + model.traverse((child: any) => { + if (child.isMesh) { + // Clone the material to ensure changes are independent + // child.material = child.material.clone(); + + child.castShadow = true; + child.receiveShadow = true; + } + }); + + itemsGroup.current.add(model); + + if (tempLoader.current) { + tempLoader.current.material.dispose(); + tempLoader.current.geometry.dispose(); + itemsGroup.current.remove(tempLoader.current); + tempLoader.current = undefined; + } + + const newFloorItem: Types.FloorItemType = { + modelUuid: data.data.modelUuid, + modelName: data.data.modelName, + modelfileID: data.data.modelfileID, + position: [...(data.data.position as [number, number, number])], + rotation: { + x: model.rotation.x, + y: model.rotation.y, + z: model.rotation.z, + }, + isLocked: data.data.isLocked, + isVisible: data.data.isVisible, + }; + + setFloorItems((prevItems: any) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem( + "FloorItems", + JSON.stringify(updatedItems) + ); + return updatedItems; + }); + + gsap.to(model.position, { + y: data.data.position[1], + duration: 1.5, + ease: "power2.out", + }); + gsap.to(model.scale, { + x: 1, + y: 1, + z: 1, + duration: 1.5, + ease: "power2.out", + onComplete: () => { + echo.success("Model Added!"); + }, + }); + + THREE.Cache.add(data.data.modelName, gltf); + }, + () => { + TempLoader( + new THREE.Vector3(...data.data.position), + isTempLoader, + tempLoader, + itemsGroup + ); + } + ); + } + } else if (data.message === "Model updated successfully") { + itemsGroup.current?.children.forEach((item: THREE.Group) => { + if (item.uuid === data.data.modelUuid) { + item.position.set( + ...(data.data.position as [number, number, number]) + ); + item.rotation.set( + data.data.rotation.x, + data.data.rotation.y, + data.data.rotation.z + ); + } + }); + + setFloorItems((prevItems: Types.FloorItems) => { + if (!prevItems) { + return; + } + let updatedItem: any = null; + const updatedItems = prevItems.map((item) => { + if (item.modelUuid === data.data.modelUuid) { + updatedItem = { + ...item, + position: [...data.data.position] as [number, number, number], + rotation: { + x: data.data.rotation.x, + y: data.data.rotation.y, + z: data.data.rotation.z, + }, + }; + return updatedItem; + } + return item; + }); + return updatedItems; + }); } - ); - } - } else if (data.message === "Model updated successfully") { - itemsGroup.current?.children.forEach((item: THREE.Group) => { - if (item.uuid === data.data.modelUuid) { - item.position.set( - ...(data.data.position as [number, number, number]) - ); - item.rotation.set( - data.data.rotation.x, - data.data.rotation.y, - data.data.rotation.z - ); - } }); - setFloorItems((prevItems: Types.FloorItems) => { - if (!prevItems) { - return; - } - let updatedItem: any = null; - const updatedItems = prevItems.map((item) => { - if (item.modelUuid === data.data.modelUuid) { - updatedItem = { - ...item, - position: [...data.data.position] as [number, number, number], - rotation: { - x: data.data.rotation.x, - y: data.data.rotation.y, - z: data.data.rotation.z, - }, - }; - return updatedItem; + socket.on("model-asset:response:updates", (data: any) => { + if (socket.id === data.socketId) { + return; } - return item; - }); - return updatedItems; - }); - } - }); - - socket.on("model-asset:response:updates", (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "Model deleted successfully") { - const deletedUUID = data.data.modelUuid; - let items = JSON.parse(localStorage.getItem("FloorItems")!); - - const updatedItems = items.filter( - (item: { modelUuid: string }) => item.modelUuid !== deletedUUID - ); - - const storedItems = JSON.parse( - localStorage.getItem("FloorItems") || "[]" - ); - const updatedStoredItems = storedItems.filter( - (item: { modelUuid: string }) => item.modelUuid !== deletedUUID - ); - localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); - - itemsGroup.current.children.forEach((item: any) => { - if (item.uuid === deletedUUID) { - itemsGroup.current.remove(item); - } - }); - setFloorItems(updatedItems); - toast.success("Model Removed!"); - } - }); - - socket.on("Line:response:update", (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "line updated") { - const DraggedUUID = data.data.uuid; - const DraggedPosition = new THREE.Vector3( - data.data.position.x, - data.data.position.y, - data.data.position.z - ); - - const point = floorPlanGroupPoint.current.getObjectByProperty( - "uuid", - DraggedUUID - ); - point.position.set( - DraggedPosition.x, - DraggedPosition.y, - DraggedPosition.z - ); - const affectedLines = updateLinesPositions( - { uuid: DraggedUUID, position: DraggedPosition }, - lines - ); - - updateLines(floorPlanGroupLine, affectedLines); - updateDistanceText(scene, floorPlanGroupLine, affectedLines); - updateFloorLines(onlyFloorlines, { - uuid: DraggedUUID, - position: DraggedPosition, - }); - - loadWalls(lines, setWalls); - setUpdateScene(true); - } - }); - - socket.on("Line:response:delete", (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "line deleted") { - const line = objectLineToArray(data.data); - const linePoints = line; - const connectedpoints = [linePoints[0][1], linePoints[1][1]]; - - onlyFloorlines.current = onlyFloorlines.current - .map((floorline: any) => - floorline.filter( - (line: any) => - line[0][1] !== connectedpoints[0] && - line[1][1] !== connectedpoints[1] - ) - ) - .filter((floorline: any) => floorline.length > 0); - - const removedLine = lines.current.find( - (item: any) => - (item[0][1] === linePoints[0][1] && - item[1][1] === linePoints[1][1]) || - (item[0][1] === linePoints[1][1] && item[1][1] === linePoints[0][1]) - ); - lines.current = lines.current.filter( - (item: any) => item !== removedLine - ); - - floorPlanGroupLine.current.children.forEach((line: any) => { - const linePoints = line.userData.linePoints as [ - number, - string, - number - ][]; - const uuid1 = linePoints[0][1]; - const uuid2 = linePoints[1][1]; - - if ( - (uuid1 === connectedpoints[0] && uuid2 === connectedpoints[1]) || - (uuid1 === connectedpoints[1] && uuid2 === connectedpoints[0]) - ) { - line.material.dispose(); - line.geometry.dispose(); - floorPlanGroupLine.current.remove(line); - setDeletedLines([line.userData.linePoints]); - } - }); - - connectedpoints.forEach((pointUUID) => { - let isConnected = false; - floorPlanGroupLine.current.children.forEach((line: any) => { - const linePoints = line.userData.linePoints; - const uuid1 = linePoints[0][1]; - const uuid2 = linePoints[1][1]; - if (uuid1 === pointUUID || uuid2 === pointUUID) { - isConnected = true; + if (organization !== data.organization) { + return; } - }); + if (data.message === "Model deleted successfully") { + const deletedUUID = data.data.modelUuid; + let items = JSON.parse(localStorage.getItem("FloorItems")!); - if (!isConnected) { - floorPlanGroupPoint.current.children.forEach((point: any) => { - if (point.uuid === pointUUID) { + const updatedItems = items.filter( + (item: { modelUuid: string }) => item.modelUuid !== deletedUUID + ); + + const storedItems = JSON.parse( + localStorage.getItem("FloorItems") || "[]" + ); + const updatedStoredItems = storedItems.filter( + (item: { modelUuid: string }) => item.modelUuid !== deletedUUID + ); + localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); + + itemsGroup.current.children.forEach((item: any) => { + if (item.uuid === deletedUUID) { + itemsGroup.current.remove(item); + } + }); + setFloorItems(updatedItems); + echo.success("Model Removed!"); + } + }); + + socket.on("Line:response:update", (data: any) => { + if (socket.id === data.socketId) { + return; + } + if (organization !== data.organization) { + return; + } + if (data.message === "line updated") { + const DraggedUUID = data.data.uuid; + const DraggedPosition = new THREE.Vector3( + data.data.position.x, + data.data.position.y, + data.data.position.z + ); + + const point = floorPlanGroupPoint.current.getObjectByProperty( + "uuid", + DraggedUUID + ); + point.position.set( + DraggedPosition.x, + DraggedPosition.y, + DraggedPosition.z + ); + const affectedLines = updateLinesPositions( + { uuid: DraggedUUID, position: DraggedPosition }, + lines + ); + + updateLines(floorPlanGroupLine, affectedLines); + updateDistanceText(scene, floorPlanGroupLine, affectedLines); + updateFloorLines(onlyFloorlines, { + uuid: DraggedUUID, + position: DraggedPosition, + }); + + loadWalls(lines, setWalls); + setUpdateScene(true); + } + }); + + socket.on("Line:response:delete", (data: any) => { + if (socket.id === data.socketId) { + return; + } + if (organization !== data.organization) { + return; + } + if (data.message === "line deleted") { + const line = objectLineToArray(data.data); + const linePoints = line; + const connectedpoints = [linePoints[0][1], linePoints[1][1]]; + + onlyFloorlines.current = onlyFloorlines.current + .map((floorline: any) => + floorline.filter( + (line: any) => + line[0][1] !== connectedpoints[0] && + line[1][1] !== connectedpoints[1] + ) + ) + .filter((floorline: any) => floorline.length > 0); + + const removedLine = lines.current.find( + (item: any) => + (item[0][1] === linePoints[0][1] && + item[1][1] === linePoints[1][1]) || + (item[0][1] === linePoints[1][1] && item[1][1] === linePoints[0][1]) + ); + lines.current = lines.current.filter( + (item: any) => item !== removedLine + ); + + floorPlanGroupLine.current.children.forEach((line: any) => { + const linePoints = line.userData.linePoints as [ + number, + string, + number + ][]; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + + if ( + (uuid1 === connectedpoints[0] && uuid2 === connectedpoints[1]) || + (uuid1 === connectedpoints[1] && uuid2 === connectedpoints[0]) + ) { + line.material.dispose(); + line.geometry.dispose(); + floorPlanGroupLine.current.remove(line); + setDeletedLines([line.userData.linePoints]); + } + }); + + connectedpoints.forEach((pointUUID) => { + let isConnected = false; + floorPlanGroupLine.current.children.forEach((line: any) => { + const linePoints = line.userData.linePoints; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + if (uuid1 === pointUUID || uuid2 === pointUUID) { + isConnected = true; + } + }); + + if (!isConnected) { + floorPlanGroupPoint.current.children.forEach((point: any) => { + if (point.uuid === pointUUID) { + point.material.dispose(); + point.geometry.dispose(); + floorPlanGroupPoint.current.remove(point); + } + }); + } + }); + + loadWalls(lines, setWalls); + setUpdateScene(true); + + echo.success("Line Removed!"); + } + }); + + socket.on("Line:response:delete:point", (data: any) => { + if (socket.id === data.socketId) { + return; + } + if (organization !== data.organization) { + return; + } + if (data.message === "point deleted") { + const point = floorPlanGroupPoint.current?.getObjectByProperty( + "uuid", + data.data + ); point.material.dispose(); point.geometry.dispose(); floorPlanGroupPoint.current.remove(point); - } - }); - } - }); - loadWalls(lines, setWalls); - setUpdateScene(true); + onlyFloorlines.current = onlyFloorlines.current + .map((floorline: any) => + floorline.filter( + (line: any) => + line[0][1] !== data.data && line[1][1] !== data.data + ) + ) + .filter((floorline: any) => floorline.length > 0); - toast.success("Line Removed!"); - } - }); - - socket.on("Line:response:delete:point", (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "point deleted") { - const point = floorPlanGroupPoint.current?.getObjectByProperty( - "uuid", - data.data - ); - point.material.dispose(); - point.geometry.dispose(); - floorPlanGroupPoint.current.remove(point); - - onlyFloorlines.current = onlyFloorlines.current - .map((floorline: any) => - floorline.filter( - (line: any) => - line[0][1] !== data.data && line[1][1] !== data.data - ) - ) - .filter((floorline: any) => floorline.length > 0); - - RemoveConnectedLines( - data.data, - floorPlanGroupLine, - floorPlanGroupPoint, - setDeletedLines, - lines - ); - - loadWalls(lines, setWalls); - setUpdateScene(true); - - toast.success("Point Removed!"); - } - }); - - socket.on("Line:response:delete:layer", async (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "layer deleted") { - setActiveLayer(1); - const removedLayer = data.data; - const removedLines: Types.Lines = lines.current.filter( - (line: any) => line[0][2] === removedLayer - ); - - ////////// Remove Points and lines from the removed layer ////////// - - removedLines.forEach(async (line) => { - line.forEach(async (removedPoint) => { - const removableLines: THREE.Mesh[] = []; - const connectedpoints: string[] = []; - - floorPlanGroupLine.current.children.forEach((line: any) => { - const linePoints = line.userData.linePoints as [ - number, - string, - number - ][]; - const uuid1 = linePoints[0][1]; - const uuid2 = linePoints[1][1]; - - if (uuid1 === removedPoint[1] || uuid2 === removedPoint[1]) { - connectedpoints.push(uuid1 === removedPoint[1] ? uuid2 : uuid1); - removableLines.push(line as THREE.Mesh); - } - }); - - if (removableLines.length > 0) { - removableLines.forEach((line: any) => { - lines.current = lines.current.filter( - (item: any) => - JSON.stringify(item) !== - JSON.stringify(line.userData.linePoints) + RemoveConnectedLines( + data.data, + floorPlanGroupLine, + floorPlanGroupPoint, + setDeletedLines, + lines ); - line.material.dispose(); - line.geometry.dispose(); - floorPlanGroupLine.current.remove(line); - }); + + loadWalls(lines, setWalls); + setUpdateScene(true); + + echo.success("Point Removed!"); } + }); - const point = floorPlanGroupPoint.current.getObjectByProperty( - "uuid", - removedPoint[1] - ); - if (point) { - point.material.dispose(); - point.geometry.dispose(); - floorPlanGroupPoint.current.remove(point); + socket.on("Line:response:delete:layer", async (data: any) => { + if (socket.id === data.socketId) { + return; + } + if (organization !== data.organization) { + return; + } + if (data.message === "layer deleted") { + setActiveLayer(1); + const removedLayer = data.data; + const removedLines: Types.Lines = lines.current.filter( + (line: any) => line[0][2] === removedLayer + ); + + ////////// Remove Points and lines from the removed layer ////////// + + removedLines.forEach(async (line) => { + line.forEach(async (removedPoint) => { + const removableLines: THREE.Mesh[] = []; + const connectedpoints: string[] = []; + + floorPlanGroupLine.current.children.forEach((line: any) => { + const linePoints = line.userData.linePoints as [ + number, + string, + number + ][]; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + + if (uuid1 === removedPoint[1] || uuid2 === removedPoint[1]) { + connectedpoints.push(uuid1 === removedPoint[1] ? uuid2 : uuid1); + removableLines.push(line as THREE.Mesh); + } + }); + + if (removableLines.length > 0) { + removableLines.forEach((line: any) => { + lines.current = lines.current.filter( + (item: any) => + JSON.stringify(item) !== + JSON.stringify(line.userData.linePoints) + ); + line.material.dispose(); + line.geometry.dispose(); + floorPlanGroupLine.current.remove(line); + }); + } + + const point = floorPlanGroupPoint.current.getObjectByProperty( + "uuid", + removedPoint[1] + ); + if (point) { + point.material.dispose(); + point.geometry.dispose(); + floorPlanGroupPoint.current.remove(point); + } + }); + }); + + ////////// Update the remaining lines layer values in the userData and in lines.current ////////// + + let remaining = lines.current.filter( + (line: any) => line[0][2] !== removedLayer + ); + let updatedLines: Types.Lines = []; + remaining.forEach((line: any) => { + let newLines = JSON.parse(JSON.stringify(line)); + if (newLines[0][2] > removedLayer) { + newLines[0][2] -= 1; + newLines[1][2] -= 1; + } + + const matchingLine = floorPlanGroupLine.current.children.find( + (l: any) => + l.userData.linePoints[0][1] === line[0][1] && + l.userData.linePoints[1][1] === line[1][1] + ); + if (matchingLine) { + const updatedUserData = JSON.parse( + JSON.stringify(matchingLine.userData) + ); + updatedUserData.linePoints[0][2] = newLines[0][2]; + updatedUserData.linePoints[1][2] = newLines[1][2]; + matchingLine.userData = updatedUserData; + } + updatedLines.push(newLines); + }); + + lines.current = updatedLines; + localStorage.setItem("Lines", JSON.stringify(lines.current)); + + ////////// Also remove OnlyFloorLines and update it in localstorage ////////// + + onlyFloorlines.current = onlyFloorlines.current.filter((floor: any) => { + return floor[0][0][2] !== removedLayer; + }); + const meshToRemove = floorGroup.current?.children.find( + (mesh: any) => mesh.name === `Only_Floor_Line_${removedLayer}` + ); + if (meshToRemove) { + meshToRemove.geometry.dispose(); + meshToRemove.material.dispose(); + floorGroup.current?.remove(meshToRemove); + } + + const zonesData = await getZonesApi(organization); + const highestLayer = Math.max( + 1, + lines.current.reduce( + (maxLayer: number, segment: any) => + Math.max(maxLayer, segment.layer || 0), + 0 + ), + zonesData.data.reduce( + (maxLayer: number, zone: any) => + Math.max(maxLayer, zone.layer || 0), + 0 + ) + ); + + setLayers(highestLayer); + + loadWalls(lines, setWalls); + setUpdateScene(true); + + echo.success("Layer Removed!"); + } + }); + }, [socket]); + + useEffect(() => { + if (!socket) return; + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; + let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + + socket.on("wallItemsDeleteResponse", (data: any) => { + if (socket.id === data.socketId) { + return; + } + if (organization !== data.organization) { + return; + } + if (data.message === "wallitem deleted") { + const deletedUUID = data.data.modelUuid; + let WallItemsRef = wallItems; + const Items = WallItemsRef.filter((item: any) => item.model?.uuid !== deletedUUID); + + setWallItems([]); + setTimeout(async () => { + WallItemsRef = Items; + setWallItems(WallItemsRef); + const WallItemsForStorage = WallItemsRef.map((item: any) => { + const { model, ...rest } = item; + return { + ...rest, + modelUuid: model?.uuid, + }; + }); + + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + echo.success("Model Removed!"); + }, 50); } - }); }); - ////////// Update the remaining lines layer values in the userData and in lines.current ////////// + socket.on("wallItemsUpdateResponse", (data: any) => { + if (socket.id === data.socketId) { + return; + } + if (organization !== data.organization) { + return; + } + if (data.message === "wallIitem created") { + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); - let remaining = lines.current.filter( - (line: any) => line[0][2] !== removedLayer - ); - let updatedLines: Types.Lines = []; - remaining.forEach((line: any) => { - let newLines = JSON.parse(JSON.stringify(line)); - if (newLines[0][2] > removedLayer) { - newLines[0][2] -= 1; - newLines[1][2] -= 1; - } + dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); + loader.setDRACOLoader(dracoLoader); - const matchingLine = floorPlanGroupLine.current.children.find( - (l: any) => - l.userData.linePoints[0][1] === line[0][1] && - l.userData.linePoints[1][1] === line[1][1] - ); - if (matchingLine) { - const updatedUserData = JSON.parse( - JSON.stringify(matchingLine.userData) - ); - updatedUserData.linePoints[0][2] = newLines[0][2]; - updatedUserData.linePoints[1][2] = newLines[1][2]; - matchingLine.userData = updatedUserData; - } - updatedLines.push(newLines); + // Check THREE.js cache first + const cachedModel = THREE.Cache.get(data.data.modelfileID); + if (cachedModel) { + handleModelLoad(cachedModel); + return; + } + + // Check IndexedDB cache + retrieveGLTF(data.data.modelfileID).then((cachedModelBlob) => { + if (cachedModelBlob) { + const blobUrl = URL.createObjectURL(cachedModelBlob); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(data.data.modelfileID, gltf); + handleModelLoad(gltf); + }); + return; + } + }) + + // Load from backend if not in any cache + loader.load(`${url_Backend_dwinzo}/api/v2/AssetFile/${data.data.modelfileID}`, async (gltf) => { + try { + const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v2/AssetFile/${data.data.modelfileID}`).then((res) => res.blob()); + await storeGLTF(data.data.modelfileID, modelBlob); + THREE.Cache.add(data.data.modelfileID, gltf); + await handleModelLoad(gltf); + } catch (error) { + console.error('Failed to cache model:', error); + handleModelLoad(gltf); + } + }); + + async function handleModelLoad(gltf: GLTF) { + const model = gltf.scene.clone(); + model.uuid = data.data.modelUuid; + + model.children[0].children.forEach((child) => { + if (child.name !== "CSG_REF") { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const newWallItem = { + type: data.data.type, + model: model, + modelName: data.data.modelName, + modelfileID: data.data.modelfileID, + scale: data.data.scale, + csgscale: data.data.csgscale, + csgposition: data.data.csgposition, + position: data.data.position, + quaternion: data.data.quaternion, + }; + + setWallItems((prevItems: Types.wallItems) => { + const updatedItems = [...prevItems, newWallItem]; + + const WallItemsForStorage = updatedItems.map(item => { + const { model, ...rest } = item; + return { + ...rest, + modelUuid: model?.uuid, + }; + }); + + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + echo.success("Model Added!"); + return updatedItems; + }); + } + + } else if (data.message === "wallIitem updated") { + const updatedUUID = data.data.modelUuid; + + setWallItems((prevItems: any) => { + const updatedItems = prevItems.map((item: any) => { + if (item.model.uuid === updatedUUID) { + return { + ...item, + position: data.data.position, + quaternion: data.data.quaternion, + scale: data.data.scale, + csgscale: data.data.csgscale, + csgposition: data.data.csgposition, + }; + } + return item; + }); + + const WallItemsForStorage = updatedItems.map((item: any) => { + const { model, ...rest } = item; + return { + ...rest, + modelUuid: model?.uuid, + }; + }); + + localStorage.setItem( + "WallItems", + JSON.stringify(WallItemsForStorage) + ); + echo.success("Model Updated!"); + + return updatedItems; + }); + } }); - lines.current = updatedLines; - localStorage.setItem("Lines", JSON.stringify(lines.current)); + return () => { + socket.off("wallItemsDeleteResponse"); + socket.off("wallItemsUpdateResponse"); + }; + }, [wallItems]); - ////////// Also remove OnlyFloorLines and update it in localstorage ////////// - - onlyFloorlines.current = onlyFloorlines.current.filter((floor: any) => { - return floor[0][0][2] !== removedLayer; - }); - const meshToRemove = floorGroup.current?.children.find( - (mesh: any) => mesh.name === `Only_Floor_Line_${removedLayer}` - ); - if (meshToRemove) { - meshToRemove.geometry.dispose(); - meshToRemove.material.dispose(); - floorGroup.current?.remove(meshToRemove); + function getPointColor(lineType: string | undefined): string { + switch (lineType) { + case CONSTANTS.lineConfig.wallName: + return CONSTANTS.pointConfig.wallOuterColor; + case CONSTANTS.lineConfig.floorName: + return CONSTANTS.pointConfig.floorOuterColor; + case CONSTANTS.lineConfig.aisleName: + return CONSTANTS.pointConfig.aisleOuterColor; + default: + return CONSTANTS.pointConfig.defaultOuterColor; } + } - const zonesData = await getZonesApi(organization); - const highestLayer = Math.max( - 1, - lines.current.reduce( - (maxLayer: number, segment: any) => - Math.max(maxLayer, segment.layer || 0), - 0 - ), - zonesData.data.reduce( - (maxLayer: number, zone: any) => - Math.max(maxLayer, zone.layer || 0), - 0 - ) - ); + function getLineColor(lineType: string | undefined): string { + switch (lineType) { + case CONSTANTS.lineConfig.wallName: + return CONSTANTS.lineConfig.wallColor; + case CONSTANTS.lineConfig.floorName: + return CONSTANTS.lineConfig.floorColor; + case CONSTANTS.lineConfig.aisleName: + return CONSTANTS.lineConfig.aisleColor; + default: + return CONSTANTS.lineConfig.defaultColor; + } + } - setLayers(highestLayer); + useEffect(() => { + if (!socket) return; + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - loadWalls(lines, setWalls); - setUpdateScene(true); - - toast.success("Layer Removed!"); - } - }); - }, [socket]); - - useEffect(() => { - if (!socket) return; - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; - - socket.on("wallItemsDeleteResponse", (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "wallitem deleted") { - const deletedUUID = data.data.modelUuid; - let WallItemsRef = wallItems; - const Items = WallItemsRef.filter( - (item: any) => item.model?.uuid !== deletedUUID - ); - - setWallItems([]); - setTimeout(async () => { - WallItemsRef = Items; - setWallItems(WallItemsRef); - const WallItemsForStorage = WallItemsRef.map((item: any) => { - const { model, ...rest } = item; - return { - ...rest, - modelUuid: model?.uuid, - }; - }); - - localStorage.setItem( - "WallItems", - JSON.stringify(WallItemsForStorage) - ); - toast.success("Model Removed!"); - }, 50); - } - }); - - socket.on("wallItemsUpdateResponse", (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "wallIitem created") { - const loader = new GLTFLoader(); - loader.load( - AssetConfigurations[data.data.modelName].modelUrl, - async (gltf) => { - const model = gltf.scene; - model.uuid = data.data.modelUuid; - model.children[0].children.forEach((child) => { - if (child.name !== "CSG_REF") { - child.castShadow = true; - child.receiveShadow = true; - } - }); - - const newWallItem = { - type: data.data.type, - model: model, - modelName: data.data.modelName, - scale: data.data.scale, - csgscale: data.data.csgscale, - csgposition: data.data.csgposition, - position: data.data.position, - quaternion: data.data.quaternion, - }; - - setWallItems((prevItems: any) => { - const updatedItems = [...prevItems, newWallItem]; - - const WallItemsForStorage = updatedItems.map((item) => { - const { model, ...rest } = item; - return { - ...rest, - modelUuid: model?.uuid, - }; - }); - - localStorage.setItem( - "WallItems", - JSON.stringify(WallItemsForStorage) - ); - toast.success("Model Added!"); - - return updatedItems; - }); - } - ); - } else if (data.message === "wallIitem updated") { - const updatedUUID = data.data.modelUuid; - - setWallItems((prevItems: any) => { - const updatedItems = prevItems.map((item: any) => { - if (item.model.uuid === updatedUUID) { - return { - ...item, - position: data.data.position, - quaternion: data.data.quaternion, - scale: data.data.scale, - csgscale: data.data.csgscale, - csgposition: data.data.csgposition, - }; + socket.on("Line:response:create", async (data: any) => { + if (socket.id === data.socketId) { + return; } - return item; - }); + if (organization !== data.organization) { + return; + } + if (data.message === "line create") { + const line: Types.Line = objectLineToArray(data.data); + const type = line[0][3]; + const pointColour = getPointColor(type); + const lineColour = getLineColor(type); + setNewLines([line]); - const WallItemsForStorage = updatedItems.map((item: any) => { - const { model, ...rest } = item; - return { - ...rest, - modelUuid: model?.uuid, - }; - }); - - localStorage.setItem( - "WallItems", - JSON.stringify(WallItemsForStorage) - ); - toast.success("Model Updated!"); - - return updatedItems; - }); - } - }); - - return () => { - socket.off("wallItemsDeleteResponse"); - socket.off("wallItemsUpdateResponse"); - }; - }, [wallItems]); - - function getPointColor(lineType: string | undefined): string { - switch (lineType) { - case CONSTANTS.lineConfig.wallName: - return CONSTANTS.pointConfig.wallOuterColor; - case CONSTANTS.lineConfig.floorName: - return CONSTANTS.pointConfig.floorOuterColor; - case CONSTANTS.lineConfig.aisleName: - return CONSTANTS.pointConfig.aisleOuterColor; - default: - return CONSTANTS.pointConfig.defaultOuterColor; - } - } - - function getLineColor(lineType: string | undefined): string { - switch (lineType) { - case CONSTANTS.lineConfig.wallName: - return CONSTANTS.lineConfig.wallColor; - case CONSTANTS.lineConfig.floorName: - return CONSTANTS.lineConfig.floorColor; - case CONSTANTS.lineConfig.aisleName: - return CONSTANTS.lineConfig.aisleColor; - default: - return CONSTANTS.lineConfig.defaultColor; - } - } - - useEffect(() => { - if (!socket) return; - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; - - socket.on("Line:response:create", async (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "line create") { - const line: Types.Line = objectLineToArray(data.data); - const type = line[0][3]; - const pointColour = getPointColor(type); - const lineColour = getLineColor(type); - setNewLines([line]); - - line.forEach((line) => { - const existingPoint = - floorPlanGroupPoint.current?.getObjectByProperty("uuid", line[1]); - if (existingPoint) { - return; - } - const geometry = new THREE.BoxGeometry( - ...CONSTANTS.pointConfig.boxScale - ); - const material = new THREE.ShaderMaterial({ - uniforms: { - uColor: { value: new THREE.Color(pointColour) }, // Blue color for the border - uInnerColor: { - value: new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor), - }, // White color for the inner square - }, - vertexShader: ` + line.forEach((line) => { + const existingPoint = + floorPlanGroupPoint.current?.getObjectByProperty("uuid", line[1]); + if (existingPoint) { + return; + } + const geometry = new THREE.BoxGeometry( + ...CONSTANTS.pointConfig.boxScale + ); + const material = new THREE.ShaderMaterial({ + uniforms: { + uColor: { value: new THREE.Color(pointColour) }, // Blue color for the border + uInnerColor: { + value: new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor), + }, // White color for the inner square + }, + vertexShader: ` varying vec2 vUv; void main() { @@ -870,7 +899,7 @@ export default function SocketResponses({ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, - fragmentShader: ` + fragmentShader: ` varying vec2 vUv; uniform vec3 uColor; uniform vec3 uInnerColor; @@ -886,162 +915,162 @@ export default function SocketResponses({ } } `, - }); - const point = new THREE.Mesh(geometry, material); - point.name = "point"; - point.uuid = line[1]; - point.userData = { type: type, color: pointColour }; - point.position.set(line[0].x, line[0].y, line[0].z); - currentLayerPoint.current.push(point); + }); + const point = new THREE.Mesh(geometry, material); + point.name = "point"; + point.uuid = line[1]; + point.userData = { type: type, color: pointColour }; + point.position.set(line[0].x, line[0].y, line[0].z); + currentLayerPoint.current.push(point); - floorPlanGroupPoint.current?.add(point); + floorPlanGroupPoint.current?.add(point); + }); + if (dragPointControls.current) { + dragPointControls.current!.objects = currentLayerPoint.current; + } + addLineToScene( + new THREE.Vector3(line[0][0].x, line[0][0].y, line[0][0].z), + new THREE.Vector3(line[1][0].x, line[1][0].y, line[1][0].z), + lineColour, + line, + floorPlanGroupLine + ); + lines.current.push(line); + + const zonesData = await getZonesApi(organization); + const highestLayer = Math.max( + 1, + lines.current.reduce( + (maxLayer: number, segment: any) => + Math.max(maxLayer, segment.layer || 0), + 0 + ), + zonesData.data.reduce( + (maxLayer: number, zone: any) => + Math.max(maxLayer, zone.layer || 0), + 0 + ) + ); + + setLayers(highestLayer); + + Layer2DVisibility( + activeLayer, + floorPlanGroup, + floorPlanGroupLine, + floorPlanGroupPoint, + currentLayerPoint, + dragPointControls + ); + + loadWalls(lines, setWalls); + setUpdateScene(true); + } }); - if (dragPointControls.current) { - dragPointControls.current!.objects = currentLayerPoint.current; - } - addLineToScene( - new THREE.Vector3(line[0][0].x, line[0][0].y, line[0][0].z), - new THREE.Vector3(line[1][0].x, line[1][0].y, line[1][0].z), - lineColour, - line, - floorPlanGroupLine - ); - lines.current.push(line); - const zonesData = await getZonesApi(organization); - const highestLayer = Math.max( - 1, - lines.current.reduce( - (maxLayer: number, segment: any) => - Math.max(maxLayer, segment.layer || 0), - 0 - ), - zonesData.data.reduce( - (maxLayer: number, zone: any) => - Math.max(maxLayer, zone.layer || 0), - 0 - ) - ); + return () => { + socket.off("Line:response:create"); + }; + }, [socket, activeLayer]); - setLayers(highestLayer); + useEffect(() => { + if (!socket) return; + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - Layer2DVisibility( - activeLayer, - floorPlanGroup, - floorPlanGroupLine, - floorPlanGroupPoint, - currentLayerPoint, - dragPointControls - ); + socket.on("zone:response:updates", (data: any) => { + if (socket.id === data.socketId) { + return; + } + if (organization !== data.organization) { + return; + } - loadWalls(lines, setWalls); - setUpdateScene(true); - } - }); + if (data.message === "zone created") { + const pointsArray: [number, number, number][] = data.data.points; + const vector3Array = pointsArray.map( + ([x, y, z]) => new THREE.Vector3(x, y, z) + ); + const newZones = [...zones, data.data]; + setZones(newZones); + const updatedZonePoints = [...zonePoints, ...vector3Array]; + setZonePoints(updatedZonePoints); - return () => { - socket.off("Line:response:create"); - }; - }, [socket, activeLayer]); + const highestLayer = Math.max( + 1, + lines.current.reduce( + (maxLayer: number, segment: any) => + Math.max(maxLayer, segment.layer || 0), + 0 + ), + newZones.reduce( + (maxLayer: number, zone: any) => + Math.max(maxLayer, zone.layer || 0), + 0 + ) + ); - useEffect(() => { - if (!socket) return; - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; + setLayers(highestLayer); + setUpdateScene(true); + } - socket.on("zone:response:updates", (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } + if (data.message === "zone updated") { + const updatedZones = zones.map((zone: any) => + zone.zoneId === data.data.zoneId ? data.data : zone + ); + setZones(updatedZones); + setUpdateScene(true); + } + }); - if (data.message === "zone created") { - const pointsArray: [number, number, number][] = data.data.points; - const vector3Array = pointsArray.map( - ([x, y, z]) => new THREE.Vector3(x, y, z) - ); - const newZones = [...zones, data.data]; - setZones(newZones); - const updatedZonePoints = [...zonePoints, ...vector3Array]; - setZonePoints(updatedZonePoints); + socket.on("zone:response:delete", (data: any) => { + if (socket.id === data.socketId) { + return; + } + if (organization !== data.organization) { + return; + } + if (data.message === "zone deleted") { + const updatedZones = zones.filter( + (zone: any) => zone.zoneId !== data.data.zoneId + ); + setZones(updatedZones); - const highestLayer = Math.max( - 1, - lines.current.reduce( - (maxLayer: number, segment: any) => - Math.max(maxLayer, segment.layer || 0), - 0 - ), - newZones.reduce( - (maxLayer: number, zone: any) => - Math.max(maxLayer, zone.layer || 0), - 0 - ) - ); + const zoneIndex = zones.findIndex( + (zone: any) => zone.zoneId === data.data.zoneId + ); + if (zoneIndex !== -1) { + const updatedzonePoints = zonePoints.filter( + (_: any, index: any) => + index < zoneIndex * 4 || index >= zoneIndex * 4 + 4 + ); + setZonePoints(updatedzonePoints); + } - setLayers(highestLayer); - setUpdateScene(true); - } + const highestLayer = Math.max( + 1, + lines.current.reduce( + (maxLayer: number, segment: any) => + Math.max(maxLayer, segment.layer || 0), + 0 + ), + updatedZones.reduce( + (maxLayer: number, zone: any) => + Math.max(maxLayer, zone.layer || 0), + 0 + ) + ); - if (data.message === "zone updated") { - const updatedZones = zones.map((zone: any) => - zone.zoneId === data.data.zoneId ? data.data : zone - ); - setZones(updatedZones); - setUpdateScene(true); - } - }); + setLayers(highestLayer); + setUpdateScene(true); + } + }); - socket.on("zone:response:delete", (data: any) => { - if (socket.id === data.socketId) { - return; - } - if (organization !== data.organization) { - return; - } - if (data.message === "zone deleted") { - const updatedZones = zones.filter( - (zone: any) => zone.zoneId !== data.data.zoneId - ); - setZones(updatedZones); + return () => { + socket.off("zone:response:updates"); + socket.off("zone:response:delete"); + }; + }, [socket, zones, zonePoints]); - const zoneIndex = zones.findIndex( - (zone: any) => zone.zoneId === data.data.zoneId - ); - if (zoneIndex !== -1) { - const updatedzonePoints = zonePoints.filter( - (_: any, index: any) => - index < zoneIndex * 4 || index >= zoneIndex * 4 + 4 - ); - setZonePoints(updatedzonePoints); - } - - const highestLayer = Math.max( - 1, - lines.current.reduce( - (maxLayer: number, segment: any) => - Math.max(maxLayer, segment.layer || 0), - 0 - ), - updatedZones.reduce( - (maxLayer: number, zone: any) => - Math.max(maxLayer, zone.layer || 0), - 0 - ) - ); - - setLayers(highestLayer); - setUpdateScene(true); - } - }); - - return () => { - socket.off("zone:response:updates"); - socket.off("zone:response:delete"); - }; - }, [socket, zones, zonePoints]); - - return <>; + return <>; } diff --git a/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx b/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx index 82f21ab..4e2dac3 100644 --- a/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx @@ -415,7 +415,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas } }); - toast.success("Object added!"); + echo.success("Object added!"); clearSelection(); }; diff --git a/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx b/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx index 3e951a3..83764a3 100644 --- a/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx @@ -392,7 +392,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb } }); - toast.success("Object duplicated!"); + echo.success("Object duplicated!"); clearSelection(); } diff --git a/app/src/modules/scene/controls/selectionControls/moveControls.tsx b/app/src/modules/scene/controls/selectionControls/moveControls.tsx index 5398fe6..8029d66 100644 --- a/app/src/modules/scene/controls/selectionControls/moveControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/moveControls.tsx @@ -326,7 +326,7 @@ function MoveControls({ itemsGroupRef.current.add(obj); } }); - toast.success("Object moved!"); + echo.success("Object moved!"); itemsData.current = []; clearSelection(); diff --git a/app/src/modules/scene/controls/selectionControls/rotateControls.tsx b/app/src/modules/scene/controls/selectionControls/rotateControls.tsx index 31ba8c9..057db06 100644 --- a/app/src/modules/scene/controls/selectionControls/rotateControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/rotateControls.tsx @@ -265,7 +265,7 @@ function RotateControls({ rotatedObjects, setRotatedObjects, movedObjects, setMo itemsGroupRef.current.add(obj); } }); - toast.success("Object rotated!"); + echo.success("Object rotated!"); itemsData.current = []; clearSelection(); diff --git a/app/src/modules/scene/controls/selectionControls/selectionControls.tsx b/app/src/modules/scene/controls/selectionControls/selectionControls.tsx index 5041434..67775a9 100644 --- a/app/src/modules/scene/controls/selectionControls/selectionControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/selectionControls.tsx @@ -251,7 +251,7 @@ const SelectionControls: React.FC = () => { const updatedItems = floorItems.filter((item: { modelUuid: string }) => !selectedUUIDs.includes(item.modelUuid)); setFloorItems(updatedItems); } - toast.success("Selected models removed!"); + echo.success("Selected models removed!"); clearSelection(); }; diff --git a/app/src/modules/scene/postProcessing/postProcessing.tsx b/app/src/modules/scene/postProcessing/postProcessing.tsx index dd5607a..b22c7bf 100644 --- a/app/src/modules/scene/postProcessing/postProcessing.tsx +++ b/app/src/modules/scene/postProcessing/postProcessing.tsx @@ -57,9 +57,7 @@ export default function PostProcessing() { )} {selectedWallItem && ( child.name !== "CSG_REF" - )} + selection={flattenChildren(selectedWallItem.children)} selectionLayer={10} width={3000} blendFunction={BlendFunction.ALPHA} diff --git a/app/src/modules/simulation/simulator/simulator.tsx b/app/src/modules/simulation/simulator/simulator.tsx index d6ff2fa..aa0b9f6 100644 --- a/app/src/modules/simulation/simulator/simulator.tsx +++ b/app/src/modules/simulation/simulator/simulator.tsx @@ -3,22 +3,27 @@ import { useProductStore } from '../../../store/simulation/useProductStore'; import { useActionHandler } from '../actions/useActionHandler'; import { usePlayButtonStore, useResetButtonStore } from '../../../store/usePlayButtonStore'; import { determineExecutionOrder } from './functions/determineExecutionOrder'; +import { useSelectedProduct } from '../../../store/simulation/useSimulationStore'; function Simulator() { - const { products } = useProductStore(); + const { products, getProductById } = useProductStore(); const { handleAction } = useActionHandler(); + const { selectedProduct } = useSelectedProduct(); const { isPlaying } = usePlayButtonStore(); const { isReset } = useResetButtonStore(); useEffect(() => { - if (!isPlaying || isReset) return; + if (!isPlaying || isReset || !selectedProduct.productId) return; - const executionOrder = determineExecutionOrder(products); + const product = getProductById(selectedProduct.productId); + if (!product) return; + + const executionOrder = determineExecutionOrder([product]); executionOrder.forEach(point => { const action = 'actions' in point ? point.actions[0] : point.action; handleAction(action); }); - }, [products, isPlaying, isReset]); + }, [products, isPlaying, isReset, selectedProduct]); return ( diff --git a/app/src/store/store.ts b/app/src/store/store.ts index 4218a4e..4270735 100644 --- a/app/src/store/store.ts +++ b/app/src/store/store.ts @@ -90,7 +90,7 @@ export const useZonePoints = create((set) => ({ })); export const useSelectedItem = create((set: any) => ({ - selectedItem: { name: "", id: "", type: undefined }, + selectedItem: { name: "", id: "", type: undefined, category: '', subCatergory: '' }, setSelectedItem: (x: any) => set(() => ({ selectedItem: x })), })); diff --git a/app/src/types/world/worldTypes.d.ts b/app/src/types/world/worldTypes.d.ts index 3d6115a..a84eb5e 100644 --- a/app/src/types/world/worldTypes.d.ts +++ b/app/src/types/world/worldTypes.d.ts @@ -217,27 +217,14 @@ export type FloorItems = Array; // Dispatch type for setting floor item state in React export type setFloorItemSetState = React.Dispatch>; -/** Asset Configuration for Loading and Positioning **/ - -// Configuration for assets, allowing model URLs, scaling, positioning, and types -interface AssetConfiguration { - modelUrl: string; - scale?: [number, number, number]; - csgscale?: [number, number, number]; - csgposition?: [number, number, number]; - type?: "Fixed-Move" | "Free-Move"; -} - -// Collection of asset configurations, keyed by unique identifiers -export type AssetConfigurations = { [key: string]: AssetConfiguration; }; - /** Wall Item Configuration **/ // Configuration for wall items, including model, scale, position, and rotation interface WallItem { - type: "Fixed-Move" | "Free-Move" | undefined; + type: "fixed-move" | "free-move" | undefined; model?: THREE.Group; modelUuid?: string; + modelfileID: string; modelName?: string; scale?: [number, number, number]; csgscale?: [number, number, number]; -- 2.49.1 From be23b30e9157dbdde1da9b4151121ca6bdbc3853 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Tue, 13 May 2025 17:23:12 +0530 Subject: [PATCH 11/24] Refactor notification system to replace toast with echo for consistency; update relevant controls for improved user feedback --- .../scene/controls/selectionControls/copyPasteControls.tsx | 3 +-- .../scene/controls/selectionControls/duplicationControls.tsx | 1 - .../modules/scene/controls/selectionControls/moveControls.tsx | 1 - .../scene/controls/selectionControls/rotateControls.tsx | 1 - .../scene/controls/selectionControls/selectionControls.tsx | 3 +-- 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx b/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx index 4e2dac3..b69339c 100644 --- a/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx @@ -2,7 +2,6 @@ import * as THREE from "three"; import { useEffect, useMemo } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; -import { toast } from "react-toastify"; // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; import * as Types from "../../../../types/world/worldTypes"; import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; @@ -95,7 +94,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas return clone; }); setCopiedObjects(newClones); - toast.info("Objects copied!"); + echo.info("Objects copied!"); } }; diff --git a/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx b/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx index 83764a3..a2fa163 100644 --- a/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx @@ -2,7 +2,6 @@ import * as THREE from "three"; import { useEffect, useMemo } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; -import { toast } from "react-toastify"; // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; import * as Types from "../../../../types/world/worldTypes"; import { setFloorItemApi } from "../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi"; diff --git a/app/src/modules/scene/controls/selectionControls/moveControls.tsx b/app/src/modules/scene/controls/selectionControls/moveControls.tsx index 8029d66..3d861b3 100644 --- a/app/src/modules/scene/controls/selectionControls/moveControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/moveControls.tsx @@ -8,7 +8,6 @@ import { useToggleView, } from "../../../../store/store"; // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; -import { toast } from "react-toastify"; import * as Types from "../../../../types/world/worldTypes"; import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; import { useEventsStore } from "../../../../store/simulation/useEventsStore"; diff --git a/app/src/modules/scene/controls/selectionControls/rotateControls.tsx b/app/src/modules/scene/controls/selectionControls/rotateControls.tsx index 057db06..a05b8fb 100644 --- a/app/src/modules/scene/controls/selectionControls/rotateControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/rotateControls.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useRef } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; -import { toast } from "react-toastify"; import * as Types from "../../../../types/world/worldTypes"; import { useEventsStore } from "../../../../store/simulation/useEventsStore"; import { useProductStore } from "../../../../store/simulation/useProductStore"; diff --git a/app/src/modules/scene/controls/selectionControls/selectionControls.tsx b/app/src/modules/scene/controls/selectionControls/selectionControls.tsx index 67775a9..d9ce7a4 100644 --- a/app/src/modules/scene/controls/selectionControls/selectionControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/selectionControls.tsx @@ -5,7 +5,6 @@ import { SelectionHelper } from "./selectionHelper"; import { useFrame, useThree } from "@react-three/fiber"; import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView, } from "../../../../store/store"; import BoundingBox from "./boundingBoxHelper"; -import { toast } from "react-toastify"; // import { deleteFloorItem } from '../../../../services/factoryBuilder/assest/floorAsset/deleteFloorItemApi'; import * as Types from "../../../../types/world/worldTypes"; @@ -51,7 +50,7 @@ const SelectionControls: React.FC = () => { const helper = new SelectionHelper(gl); if (!itemsGroup) { - toast.warn("itemsGroup not found in the scene."); + echo.warn("itemsGroup not found in the scene."); return; } -- 2.49.1 From e16092b20448634e3e4620061e1fd37a4ee77a4c Mon Sep 17 00:00:00 2001 From: Vishnu Date: Tue, 13 May 2025 17:46:30 +0530 Subject: [PATCH 12/24] Refactor code structure for improved readability and maintainability --- .../assets/gltf-glb/layouts/floorplane_1.glb | Bin 0 -> 161880 bytes .../assets/gltf-glb/layouts/floorplane_2.glb | Bin 0 -> 226304 bytes app/src/components/footer/Footer.tsx | 20 +--- app/src/components/footer/shortcutHelper.tsx | 15 ++- app/src/components/icons/ShortcutIcons.tsx | 106 +++++++++--------- .../utils/shortcutkeys/handleShortcutKeys.ts | 11 +- 6 files changed, 77 insertions(+), 75 deletions(-) create mode 100644 app/src/assets/gltf-glb/layouts/floorplane_1.glb create mode 100644 app/src/assets/gltf-glb/layouts/floorplane_2.glb diff --git a/app/src/assets/gltf-glb/layouts/floorplane_1.glb b/app/src/assets/gltf-glb/layouts/floorplane_1.glb new file mode 100644 index 0000000000000000000000000000000000000000..88e4c6c84568f262ffc589562e5ca1feb371cf5a GIT binary patch literal 161880 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