diff --git a/app/src/app.tsx b/app/src/app.tsx index 9685c13..bbd5984 100644 --- a/app/src/app.tsx +++ b/app/src/app.tsx @@ -7,6 +7,7 @@ import UserAuth from "./pages/UserAuth"; import "./styles/main.scss"; import { LoggerProvider } from "./components/ui/log/LoggerContext"; import ForgotPassword from "./pages/ForgotPassword"; +import PageNotFound from "./pages/PageNotFound"; const App: React.FC = () => { @@ -23,6 +24,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> diff --git a/app/src/assets/image/404/404.svg b/app/src/assets/image/404/404.svg new file mode 100644 index 0000000..d17802e --- /dev/null +++ b/app/src/assets/image/404/404.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/assets/image/404/404_bk.png b/app/src/assets/image/404/404_bk.png new file mode 100644 index 0000000..27bfd56 Binary files /dev/null and b/app/src/assets/image/404/404_bk.png differ diff --git a/app/src/assets/image/categories/worker.png b/app/src/assets/image/categories/worker.png index e2287e7..bdd26c1 100644 Binary files a/app/src/assets/image/categories/worker.png and b/app/src/assets/image/categories/worker.png differ diff --git a/app/src/assets/image/fallback/fallback decal 1.png b/app/src/assets/image/fallback/fallback decal 1.png new file mode 100644 index 0000000..b47f408 Binary files /dev/null and b/app/src/assets/image/fallback/fallback decal 1.png differ diff --git a/app/src/assets/image/fallback/fallback decal.png b/app/src/assets/image/fallback/fallback decal.png new file mode 100644 index 0000000..b30e9fc Binary files /dev/null and b/app/src/assets/image/fallback/fallback decal.png differ diff --git a/app/src/assets/image/localAssets/arch.png b/app/src/assets/image/localAssets/arch.png deleted file mode 100644 index bf14278..0000000 Binary files a/app/src/assets/image/localAssets/arch.png and /dev/null differ diff --git a/app/src/assets/image/localAssets/window.png b/app/src/assets/image/localAssets/window.png deleted file mode 100644 index c6dbd54..0000000 Binary files a/app/src/assets/image/localAssets/window.png and /dev/null differ diff --git a/app/src/assets/image/sampleDecal.png b/app/src/assets/image/sampleDecal.png deleted file mode 100644 index ed8c9fd..0000000 Binary files a/app/src/assets/image/sampleDecal.png and /dev/null differ diff --git a/app/src/components/Dashboard/DashboardCard.tsx b/app/src/components/Dashboard/DashboardCard.tsx index 8ac05b6..7dfb283 100644 --- a/app/src/components/Dashboard/DashboardCard.tsx +++ b/app/src/components/Dashboard/DashboardCard.tsx @@ -1,4 +1,5 @@ -import React, { useState, useRef, useEffect, act } from "react"; +import React, { useState, useRef } from "react"; +import { createPortal } from "react-dom"; import img from "../../assets/image/image.png"; import { useNavigate } from "react-router-dom"; import { getUserData } from "../../functions/getUserData"; @@ -7,11 +8,11 @@ import { useProjectName, useSocketStore, } from "../../store/builder/store"; -import { viewProject } from "../../services/dashboard/viewProject"; import OuterClick from "../../utils/outerClick"; import { KebabIcon } from "../icons/ExportCommonIcons"; import { getAllProjects } from "../../services/dashboard/getAllProjects"; -import { updateProject } from "../../services/dashboard/updateProject"; +// import { viewProject } from "../../services/dashboard/viewProject"; +// import { updateProject } from "../../services/dashboard/updateProject"; interface DashBoardCardProps { projectName: string; @@ -69,9 +70,9 @@ const DashboardCard: React.FC = ({ const kebabRef = useRef(null); const navigateToProject = async (e: any) => { - if (active && active == "trash") return; + if (active && active === "trash") return; try { - const viewProjects = await viewProject(organization, projectId, userId); + // const viewProjects = await viewProject(organization, projectId, userId); setLoadingProgress(1); setProjectName(projectName); @@ -96,13 +97,13 @@ const DashboardCard: React.FC = ({ case "open in new tab": try { if (active === "shared" && createdBy) { - const newTab = await viewProject( - organization, - projectId, - createdBy?._id - ); + // const newTab = await viewProject( + // organization, + // projectId, + // createdBy?._id + // ); } else { - const newTab = await viewProject(organization, projectId, userId); + // const newTab = await viewProject(organization, projectId, userId); setProjectName(projectName); setIsKebabOpen(false); @@ -226,7 +227,7 @@ const DashboardCard: React.FC = ({ className="dashboard-card-container" onClick={navigateToProject} title={projectName} - onMouseLeave={() => setIsKebabOpen(false)} + // onMouseLeave={() => setIsKebabOpen(false)} >
@@ -267,7 +268,7 @@ const DashboardCard: React.FC = ({ )} {createdAt && (
- {active && active == "trash" ? `Trashed by you` : `Edited `}{" "} + {active && active === "trash" ? `Trashed by you` : `Edited `}{" "} {getRelativeTime(createdAt)}
)} @@ -292,22 +293,37 @@ const DashboardCard: React.FC = ({
- {isKebabOpen && ( -
- {getOptions().map((option) => ( - - ))} -
- )} + {isKebabOpen && + createPortal( +
+ {getOptions().map((option) => ( + + ))} +
, + document.body + )} ); }; diff --git a/app/src/components/Dashboard/SidePannel.tsx b/app/src/components/Dashboard/SidePannel.tsx index db7d203..2c0ad64 100644 --- a/app/src/components/Dashboard/SidePannel.tsx +++ b/app/src/components/Dashboard/SidePannel.tsx @@ -64,7 +64,7 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { if (projectSocket) { const handleResponse = (data: any) => { if (data.message === "Project created successfully") { - setLoadingProgress(1) + setLoadingProgress(1); navigate(`/projects/${data.data.projectId}`); } projectSocket.off("v1-project:response:add", handleResponse); // Clean up @@ -141,7 +141,10 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { activeTab === "Tutorials" ? "option-list active" : "option-list" } title="coming soon" - onClick={() => setActiveTab("Tutorials")} + onClick={() => { + // setActiveTab("Tutorials"); + console.warn("Tutorials comming soon"); + }} > Tutorials @@ -153,14 +156,17 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { : "option-list" } title="coming soon" - onClick={() => setActiveTab("Documentation")} + onClick={() => { + // setActiveTab("Documentation"); + console.warn("Documentation comming soon"); + }} > Documentation -
-
+
+
Settings
@@ -175,7 +181,7 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { Log out
-
+
Help & Feedback
diff --git a/app/src/components/footer/Footer.tsx b/app/src/components/footer/Footer.tsx index 94ee0b9..39273df 100644 --- a/app/src/components/footer/Footer.tsx +++ b/app/src/components/footer/Footer.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from "react"; -import { HelpIcon } from "../icons/DashboardIcon"; +import React, { useEffect, useState } from "react"; +import { HelpIcon, WifiIcon } from "../icons/DashboardIcon"; import { useLogger } from "../ui/log/LoggerContext"; import { GetLogIcon } from "./getLogIcons"; import { @@ -31,6 +31,27 @@ const Footer: React.FC = () => { const { Leftnote, Middlenote, Rightnote } = useMouseNoteStore(); + const [isOnline, setIsOnline] = useState(navigator.onLine); + + useEffect(() => { + const handleOnline = () => { + echo.success('You are back Online'); + setIsOnline(true); + }; + const handleOffline = () => { + echo.warn('Changes made now might not be saved'); + echo.error('You are now Offline.'); + setIsOnline(false); + }; + + window.addEventListener("online", handleOnline); + window.addEventListener("offline", handleOffline); + + return () => { + window.removeEventListener("online", handleOnline); + window.removeEventListener("offline", handleOffline); + }; + }, []); const mouseButtons = [ { icon: , @@ -98,6 +119,16 @@ const Footer: React.FC = () => {
+
+
+ +
+
{isOnline ? "Online" : "Offline"}
+
diff --git a/app/src/components/icons/AssetTypeIcons.tsx b/app/src/components/icons/AssetTypeIcons.tsx new file mode 100644 index 0000000..aa6a051 --- /dev/null +++ b/app/src/components/icons/AssetTypeIcons.tsx @@ -0,0 +1,370 @@ +export const ForkLiftIcon = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const ConveyorIcon = () => { + return ( + + + + + + + + + + + + + + + ); +}; + +export const RoboticArmIcon = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +export const MachineIcon = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const CraneIcon = () => { + return ( + + + + ); +}; + +type TypeBasedAssetIconsProps = { + assetType: string; +}; + +export function TypeBasedAssetIcons({ assetType }: TypeBasedAssetIconsProps) { + return ( +
+ {assetType === "StaticMachine" && } + {assetType === "Vehicle" && } + {assetType === "Conveyor" && } + {assetType === "Crane" && } + {assetType === "ArmBot" && } +
+ ); +} diff --git a/app/src/components/icons/DashboardIcon.tsx b/app/src/components/icons/DashboardIcon.tsx index 1aa94b5..3eb5301 100644 --- a/app/src/components/icons/DashboardIcon.tsx +++ b/app/src/components/icons/DashboardIcon.tsx @@ -253,3 +253,20 @@ export function LogoutIcon() { ); } + +export function WifiIcon() { + return ( + + + + ); +} diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index 72f9ca0..eaf1cf4 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -1697,93 +1697,6 @@ export const TargetIcon = () => { ); }; -export const ForkLiftIcon = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; export const RightHalfFillCircleIcon = () => { return ( { - const { setSelectedItem } = useSelectedItem(); - const [searchValue, setSearchValue] = useState(""); - const [selectedCategory, setSelectedCategory] = useState(null); - const [categoryAssets, setCategoryAssets] = useState([]); - const [filtereredAssets, setFiltereredAssets] = useState([]); - const [categoryList, setCategoryList] = useState([]); - const [isLoading, setisLoading] = useState(false); // Loading state for assets - - const handleSearchChange = (value: string) => { - const searchTerm = value.toLowerCase(); - setSearchValue(value); - if (searchTerm.trim() === "" && !selectedCategory) { - setCategoryAssets([]); - return; - } - const filteredModels = filtereredAssets?.filter((model) => { - if (!model?.tags || !model?.filename || !model?.category) return false; - if (searchTerm.startsWith(":") && searchTerm.length > 1) { - const tagSearchTerm = searchTerm.slice(1); - return model.tags.toLowerCase().includes(tagSearchTerm); - } else if (selectedCategory) { - return ( - model.category - .toLowerCase() - .includes(selectedCategory.toLowerCase()) && - model.filename.toLowerCase().includes(searchTerm) - ); - } else { - return model.filename.toLowerCase().includes(searchTerm); - } - }); - - setCategoryAssets(filteredModels); - }; - - useEffect(() => { - const filteredAssets = async () => { - try { - const filt = await fetchAssets(); - setFiltereredAssets(filt); - } catch { - echo.error("Filter asset not found"); - } - }; - filteredAssets(); - }, [categoryAssets]); - - useEffect(() => { - setCategoryList([ - { category: "Fenestration", categoryImage: feneration }, - { category: "Decals", categoryImage: decal }, - { category: "Vehicles", categoryImage: vehicle }, - { category: "Workstation", categoryImage: workStation }, - { category: "Machines", categoryImage: machines }, - { category: "Workers", categoryImage: worker }, - { category: "Storage", categoryImage: storage }, - { category: "Safety", categoryImage: safety }, - { category: "Office", categoryImage: office }, - ]); - }, []); - - const fetchCategoryAssets = async (asset: any) => { - setisLoading(true); - setSelectedCategory(asset); - try { - const res = await getCategoryAsset(asset); - setCategoryAssets(res); - setFiltereredAssets(res); - setisLoading(false); // End loading - // eslint-disable-next-line - } catch (error) { - echo.error("failed to fetch assets"); - setisLoading(false); - } - }; - - const activeSubcategories = [ - { name: "Safety", icon: }, - { name: "Navigation", icon: }, - { name: "Branding", icon: }, - { name: "Informational", icon: }, - ]; - - const { selectedSubCategory, setSelectedSubCategory } = useDecalStore(); - return ( -
- -
-
- {(() => { - if (isLoading) { - return ; // Show skeleton when loading - } - if (searchValue) { - return ( -
-
-
-

Results for {searchValue}

-
-
- {categoryAssets?.map((asset: any, index: number) => ( -
- {asset.filename} { - setSelectedItem({ - name: asset.filename, - id: asset.AssetID, - type: - asset.type === "undefined" - ? undefined - : asset.type, - }); - }} - /> - -
- {asset.filename - .split("_") - .map( - (word: any) => - word.charAt(0).toUpperCase() + word.slice(1) - ) - .join(" ")} -
-
- ))} -
-
-
- ); - } - - if (selectedCategory) { - return ( -
-

- {selectedCategory} - -

- - {selectedCategory === "Decals" && ( - <> -
- {activeSubcategories.map((cat, index) => ( -
setSelectedSubCategory(cat.name)} - > -
{cat.icon}
-
{cat.name}
-
- ))} -
- - )} -
- {categoryAssets?.map((asset: any, index: number) => ( -
- {asset.filename} { - setSelectedItem({ - name: asset.filename, - id: asset.AssetID, - type: - asset.type === "undefined" - ? undefined - : asset.type, - category: asset.category, - subType: asset.subType, - }); - }} - /> -
- {asset.filename - .split("_") - .map( - (word: any) => - word.charAt(0).toUpperCase() + word.slice(1) - ) - .join(" ")} -
-
- ))} - {categoryAssets.length === 0 && ( -
- 🚧 The asset shelf is empty. We're working on filling it - up! -
- )} -
-
- ); - } - - return ( -
-

Categories

-
- {Array.from( - new Set(categoryList.map((asset) => asset.category)) - ).map((category, index) => { - const categoryInfo = categoryList.find( - (asset) => asset.category === category - ); - return ( -
fetchCategoryAssets(category)} - > - {category} -
{category}
-
- ); - })} -
-
- ); - })()} -
-
-
- ); -}; - -export default Assets; diff --git a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx index fdd4f87..ad26627 100644 --- a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx +++ b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx @@ -3,7 +3,7 @@ import ToggleHeader from "../../ui/inputs/ToggleHeader"; import Outline from "./Outline"; import Header from "./Header"; import { useToggleStore } from "../../../store/useUIToggleStore"; -import Assets from "./Assets"; +import Assets from "./assetList/Assets"; import useModuleStore from "../../../store/useModuleStore"; import Widgets from "./visualization/widgets/Widgets"; import Templates from "../../../modules/visualization/template/Templates"; diff --git a/app/src/components/layout/sidebarLeft/assetList/Assets.tsx b/app/src/components/layout/sidebarLeft/assetList/Assets.tsx new file mode 100644 index 0000000..5294be5 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/Assets.tsx @@ -0,0 +1,177 @@ +import React, { useState, useEffect, useMemo, useCallback } from "react"; +import { useDecalStore } from "../../../../store/builder/store"; +import { getFilteredAssets } from "./assetsHelpers/filteredAssetsHelper"; +import { fetchCategoryDecals } from "./assetsHelpers/fetchDecalsHelper"; +import { + fetchAllAssets, + fetchCategoryAssets, +} from "./assetsHelpers/fetchAssetsHelper"; +import Search from "../../../ui/inputs/Search"; +import SkeletonUI from "../../../templates/SkeletonUI"; +import { RenderAsset } from "./assetsHelpers/renderAssetHelper"; +import { + ACTIVE_DECAL_SUBCATEGORIES, + CATEGORY_LIST, +} from "./assetsHelpers/constants"; +import { ArrowIcon } from "../../../icons/ExportCommonIcons"; + +const Assets: React.FC = () => { + const { selectedSubCategory, setSelectedSubCategory } = useDecalStore(); + const [searchValue, setSearchValue] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(null); + const [assets, setAssets] = useState([]); + const [globalResults, setGlobalResults] = useState<(AssetProp | DecalProp)[]>( + [] + ); + const [isLoading, setIsLoading] = useState(false); + + const filteredAssets = useMemo( + () => + getFilteredAssets({ + assets, + searchValue, + selectedCategory, + selectedSubCategory, + }), + [assets, searchValue, selectedCategory, selectedSubCategory] + ); + + const handleFetchCategory = useCallback( + async (category: string) => { + setIsLoading(true); + setSelectedCategory(category); + if (category === "Decals") { + const res = await fetchCategoryDecals("Safety"); + setAssets(res); + setSelectedSubCategory("Safety"); + } else { + const res = await fetchCategoryAssets(category); + setAssets(res); + } + setIsLoading(false); + }, + [setSelectedSubCategory] + ); + + const fetchGlobalSearch = useCallback(async (term: string) => { + setIsLoading(true); + const allAssets = await fetchAllAssets(); + const lowerTerm = term.toLowerCase(); + const matches = allAssets.filter( + (a) => + a.filename.toLowerCase().includes(lowerTerm) || + a.tags?.toLowerCase().includes(lowerTerm) || + a.category?.toLowerCase().includes(lowerTerm) + ); + setGlobalResults(matches); + setIsLoading(false); + }, []); + + useEffect(() => { + if (!selectedCategory && searchValue?.trim()) + fetchGlobalSearch(searchValue); + else setGlobalResults([]); + }, [searchValue, selectedCategory, fetchGlobalSearch]); + + return ( +
+ +
+
+ {isLoading ? ( + + ) : searchValue || selectedCategory ? ( +
+ {selectedCategory ? ( + <> +

+ {selectedCategory} + +

+ {selectedCategory === "Decals" && ( +
+ {ACTIVE_DECAL_SUBCATEGORIES.map((cat) => ( +
{ + setIsLoading(true); + const res = await fetchCategoryDecals(cat.name); + setAssets(res); + setSelectedSubCategory(cat.name); + setIsLoading(false); + }} + > +
{cat.icon}
+
{cat.name}
+
+ ))} +
+ )} +
+ {filteredAssets.map((a, i) => ( + + ))} + {filteredAssets.length === 0 && ( +
🚧 No assets found
+ )} +
+ + ) : ( + <> +

Global Search Results

+
+ {globalResults.map((a, i) => ( + + ))} + {globalResults.length === 0 && ( +
🔎 No matches found
+ )} +
+ + )} +
+ ) : ( +
+

Categories

+
+ {CATEGORY_LIST.map((cat) => ( +
handleFetchCategory(cat.category)} + > + {cat.category} +
{cat.category}
+
+ ))} +
+
+ )} +
+
+
+ ); +}; + +export default Assets; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/constants.tsx b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/constants.tsx new file mode 100644 index 0000000..fe06ecc --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/constants.tsx @@ -0,0 +1,34 @@ +import vehicle from "../../../../../assets/image/categories/vehicles.png"; +import workStation from "../../../../../assets/image/categories/workStation.png"; +import machines from "../../../../../assets/image/categories/machines.png"; +import worker from "../../../../../assets/image/categories/worker.png"; +import storage from "../../../../../assets/image/categories/storage.png"; +import office from "../../../../../assets/image/categories/office.png"; +import safety from "../../../../../assets/image/categories/safety.png"; +import feneration from "../../../../../assets/image/categories/feneration.png"; +import decal from "../../../../../assets/image/categories/decal.png"; +import { + AlertIcon, + DecalInfoIcon, + HangTagIcon, + NavigationIcon, +} from "../../../../icons/ExportCommonIcons"; + +export const CATEGORY_LIST: CategoryListProp[] = [ + { category: "Fenestration", categoryImage: feneration }, + { category: "Decals", categoryImage: decal }, + { category: "Vehicles", categoryImage: vehicle }, + { category: "Workstation", categoryImage: workStation }, + { category: "Machines", categoryImage: machines }, + { category: "Workers", categoryImage: worker }, + { category: "Storage", categoryImage: storage }, + { category: "Safety", categoryImage: safety }, + { category: "Office", categoryImage: office }, +]; + +export const ACTIVE_DECAL_SUBCATEGORIES = [ + { name: "Safety", icon: }, + { name: "Navigation", icon: }, + { name: "Branding", icon: }, + { name: "Informational", icon: }, +]; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchAssetsHelper.ts b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchAssetsHelper.ts new file mode 100644 index 0000000..9fb678e --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchAssetsHelper.ts @@ -0,0 +1,22 @@ +import { getCategoryAsset } from "../../../../../services/factoryBuilder/asset/assets/getCategoryAsset"; +import { fetchAssets } from "../../../../../services/marketplace/fetchAssets"; + +export const fetchCategoryAssets = async (category: string): Promise => { + if (category === "Decals") return []; // handled separately + try { + const res = await getCategoryAsset(category); + return res; + } catch (err) { + console.error("Failed to fetch category assets", err); + return []; + } +}; + +export const fetchAllAssets = async (): Promise => { + try { + return await fetchAssets(); + } catch (err) { + console.error("Failed to fetch all assets", err); + return []; + } +}; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchDecalsHelper.ts b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchDecalsHelper.ts new file mode 100644 index 0000000..b0f1be4 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchDecalsHelper.ts @@ -0,0 +1,11 @@ +import { getCategoryDecals } from "../../../../../services/factoryBuilder/asset/decals/getCategoryDecals"; + +export const fetchCategoryDecals = async (subcategory: string): Promise => { + try { + const res = await getCategoryDecals(subcategory); + return res; + } catch (err) { + console.error("Failed to fetch decals", err); + return []; + } +}; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/filteredAssetsHelper.ts b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/filteredAssetsHelper.ts new file mode 100644 index 0000000..38d2b39 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/filteredAssetsHelper.ts @@ -0,0 +1,34 @@ +interface FilterProps { + assets: AssetProp[] | DecalProp[]; + searchValue: string | null; + selectedCategory: string | null; + selectedSubCategory: string | null; +} + +export const getFilteredAssets = ({ + assets, + searchValue, + selectedCategory, + selectedSubCategory, +}: FilterProps) => { + const term = searchValue?.trim().toLowerCase(); + if (!term) return assets; + + if (selectedCategory === "Decals" || selectedSubCategory) { + return (assets as DecalProp[]).filter((a) => + a.decalName?.toLowerCase().includes(term) + ); + } + + return (assets as AssetProp[]).filter((a) => { + const tags = a.tags?.toLowerCase() ?? ""; + const filename = a.filename?.toLowerCase() ?? ""; + const category = a.category?.toLowerCase() ?? ""; + + if (term.startsWith(":")) return tags.includes(term.slice(1)); + if (selectedCategory) + return category.includes(selectedCategory.toLowerCase()) && filename.includes(term); + + return filename.includes(term); + }); +}; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/renderAssetHelper.tsx b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/renderAssetHelper.tsx new file mode 100644 index 0000000..2c27f79 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/renderAssetHelper.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { useDroppedDecal, useSelectedItem } from "../../../../../store/builder/store"; + +export const RenderAsset: React.FC<{ asset: AssetProp | DecalProp; index: number }> = ({ asset, index }) => { + const { setSelectedItem } = useSelectedItem(); + const { setDroppedDecal } = useDroppedDecal(); + + if ("decalName" in asset) { + return ( +
+ {asset.decalName} + setDroppedDecal({ + category: asset.category, + decalName: asset.decalName, + decalImage: asset.decalImage, + decalId: asset.id, + }) + } + /> +
+ {asset.decalName + .split("_") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" ")} +
+
+ ); + } + + return ( +
+ {asset.filename} + setSelectedItem({ + name: asset.filename, + id: asset.AssetID, + type: asset.type === "undefined" ? undefined : asset.type, + category: asset.category, + subType: asset.subType, + }) + } + /> +
+ {asset.filename + .split("_") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" ")} +
+
+ ); +}; diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx index 9caafc2..03bfad0 100644 --- a/app/src/components/layout/sidebarRight/SideBarRight.tsx +++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx @@ -1,30 +1,15 @@ import React, { useEffect, useState } from "react"; import Header from "./Header"; -import useModuleStore, { - useSubModuleStore, -} from "../../../store/useModuleStore"; -import { - AnalysisIcon, - FilePackageIcon, - MechanicsIcon, - PropertiesIcon, - SimulationIcon, -} from "../../icons/SimulationIcons"; +import useModuleStore, { useSubModuleStore } from "../../../store/useModuleStore"; +import { AnalysisIcon, FilePackageIcon, MechanicsIcon, PropertiesIcon, SimulationIcon, } from "../../icons/SimulationIcons"; import { useToggleStore } from "../../../store/useUIToggleStore"; import Visualization from "./visualization/Visualization"; import Analysis from "./analysis/Analysis"; import Simulations from "./simulation/Simulations"; -import useVersionHistoryVisibleStore, { - useDecalStore, - useSaveVersion, - useSelectedFloorItem, - useToolMode, -} from "../../../store/builder/store"; -import { - useSelectedEventData, - useSelectedEventSphere, -} from "../../../store/simulation/useSimulationStore"; +import useVersionHistoryVisibleStore, { useSaveVersion, useSelectedFloorItem, useToolMode } from "../../../store/builder/store"; +import { useSelectedEventData, useSelectedEventSphere, } from "../../../store/simulation/useSimulationStore"; import { useBuilderStore } from "../../../store/builder/useBuilderStore"; +import { useSceneContext } from "../../../modules/scene/sceneContext"; import GlobalProperties from "./properties/GlobalProperties"; import AssetProperties from "./properties/AssetProperties"; import ZoneProperties from "./properties/ZoneProperties"; @@ -35,10 +20,10 @@ import WallProperties from "./properties/WallProperties"; import FloorProperties from "./properties/FloorProperties"; import SelectedWallProperties from "./properties/SelectedWallProperties"; import SelectedFloorProperties from "./properties/SelectedFloorProperties"; +import SelectedDecalProperties from "./properties/SelectedDecalProperties"; +import SelectedAisleProperties from "./properties/SelectedAisleProperties"; import ResourceManagement from "./resourceManagement/ResourceManagement"; -import DecalProperties from "./properties/DecalProperties"; import ColliderProperties from "../../../modules/scene/physics/ui/ColliderProperties"; -import { useSceneContext } from "../../../modules/scene/sceneContext"; type DisplayComponent = | "versionHistory" @@ -49,17 +34,19 @@ type DisplayComponent = | "assetProperties" | "selectedWallProperties" | "selectedFloorProperties" + | "selectedDecalProperties" + | "selectedAisleProperties" | "zoneProperties" | "simulations" | "mechanics" | "analysis" | "visualization" + | "resourceManagement" | "colliderProperties" - | "selectedDecalProperties" - | "resourceManagement" | "none"; const SideBarRight: React.FC = () => { + const { selectedDecal } = useBuilderStore(); const { activeModule } = useModuleStore(); const { toggleUIRight } = useToggleStore(); const { toolMode } = useToolMode(); @@ -70,40 +57,34 @@ const SideBarRight: React.FC = () => { const { selectedEventSphere } = useSelectedEventSphere(); const { viewVersionHistory, setVersionHistoryVisible } = useVersionHistoryVisibleStore(); const { isVersionSaved } = useSaveVersion(); - const { selectedSubCategory } = useDecalStore(); const { colliderStore } = useSceneContext(); const { selectedCollider } = colliderStore(); - const [displayComponent, setDisplayComponent] = - useState("none"); + const [displayComponent, setDisplayComponent] = useState("none"); - useEffect(() => { - if (activeModule !== "simulation") setSubModule("properties"); - if (activeModule === "simulation") setSubModule("simulations"); - }, [activeModule, setSubModule]); + useEffect(() => { + if (activeModule !== "simulation") setSubModule("properties"); + if (activeModule === "simulation") setSubModule("simulations"); + }, [activeModule, setSubModule]); - useEffect(() => { - if ( - activeModule !== "mechanics" && - selectedEventData && - selectedEventSphere - ) { - setSubModule("mechanics"); - } else if (!selectedEventData && !selectedEventSphere) { - if (activeModule === "simulation") { - setSubModule("simulations"); - } - } - if (activeModule !== "simulation") { - setSubModule("properties"); - } - }, [activeModule, selectedEventData, selectedEventSphere, setSubModule]); + useEffect(() => { + if (activeModule !== "mechanics" && selectedEventData && selectedEventSphere) { + setSubModule("mechanics"); + } else if (!selectedEventData && !selectedEventSphere) { + if (activeModule === "simulation") { + setSubModule("simulations"); + } + } + if (activeModule !== "simulation") { + setSubModule("properties"); + } + }, [activeModule, selectedEventData, selectedEventSphere, setSubModule]); - useEffect(() => { - if (activeModule === "visualization") { - setDisplayComponent("visualization"); - return; - } + useEffect(() => { + if (activeModule === "visualization") { + setDisplayComponent("visualization"); + return; + } if (!isVersionSaved && activeModule === "simulation") { if (subModule === "simulations") { @@ -117,247 +98,216 @@ const SideBarRight: React.FC = () => { if (subModule === "analysis") { setDisplayComponent("analysis"); return; - }if (subModule === "resourceManagement") { - setDisplayComponent("resourceManagement"); - return; - } - } + } + if (subModule === "resourceManagement") { + setDisplayComponent("resourceManagement"); + return; + } + } - if (activeModule === "simulation" || activeModule === "builder") { - if (subModule === "resourceManagement") { - setDisplayComponent("resourceManagement"); - return; - } - }if (!isVersionSaved && activeModule === "builder") { + if (activeModule === "simulation" || activeModule === "builder") { + if (subModule === "resourceManagement") { + setDisplayComponent("resourceManagement"); + return; + } + } + + if (!isVersionSaved && activeModule === "builder") { if (selectedCollider) { setDisplayComponent("colliderProperties"); return; } } - if (subModule === "properties" && activeModule !== "visualization") { - if (selectedFloorItem) { - setDisplayComponent("assetProperties"); - return; - } - if ( - !selectedFloorItem && - !selectedFloor && - !selectedAisle && - selectedWall - ) { - setDisplayComponent("selectedWallProperties"); - return; - } - if ( - !selectedFloorItem && - !selectedWall && - !selectedAisle && - selectedFloor - ) { - setDisplayComponent("selectedFloorProperties"); - return; - } - if (viewVersionHistory) { - setDisplayComponent("versionHistory"); - return; - } - if (selectedSubCategory) { - setDisplayComponent("selectedDecalProperties"); - return; - } - if ( - !selectedFloorItem && - !selectedFloor && - !selectedWall && - !selectedSubCategory - ) { - if (toolMode === "Aisle") { - setDisplayComponent("aisleProperties"); - return; + if (subModule === "properties" && activeModule !== "visualization") { + if (selectedFloorItem) { + setDisplayComponent("assetProperties"); + return; + } + if (!selectedFloorItem && !selectedFloor && !selectedAisle && !selectedDecal && selectedWall) { + setDisplayComponent("selectedWallProperties"); + return; + } + if (!selectedFloorItem && !selectedWall && !selectedAisle && !selectedDecal && selectedFloor) { + setDisplayComponent("selectedFloorProperties"); + return; + } + if (viewVersionHistory && !selectedFloorItem && !selectedWall && !selectedAisle && !selectedFloor && !selectedDecal) { + setDisplayComponent("versionHistory"); + return; + } + if (!selectedFloorItem && !selectedFloor && !selectedAisle && !selectedWall && selectedDecal) { + setDisplayComponent("selectedDecalProperties"); + return; + } + if (!selectedFloorItem && !selectedFloor && !selectedWall && !selectedDecal && selectedAisle) { + setDisplayComponent("selectedAisleProperties"); + return; + } + if (!selectedFloorItem && !selectedFloor && !selectedWall && !selectedDecal && !selectedAisle) { + if (toolMode === "Aisle") { + setDisplayComponent("aisleProperties"); + return; + } + if (toolMode === "Wall") { + setDisplayComponent("wallProperties"); + return; + } + if (toolMode === "Floor") { + setDisplayComponent("floorProperties"); + return; + } + setDisplayComponent("globalProperties"); + return; + } } - if (toolMode === "Wall") { - setDisplayComponent("wallProperties"); - return; + + if (subModule === "zoneProperties" && (activeModule === "builder" || activeModule === "simulation")) { + setDisplayComponent("zoneProperties"); + return; } - if (toolMode === "Floor") { - setDisplayComponent("floorProperties"); - return; - } - setDisplayComponent("globalProperties"); - return; - } - } - if ( - subModule === "zoneProperties" && - (activeModule === "builder" || activeModule === "simulation") - ) { - setDisplayComponent("zoneProperties"); - return; - } + setDisplayComponent("none"); + }, [viewVersionHistory, activeModule, subModule, isVersionSaved, selectedFloorItem, selectedWall, selectedFloor, selectedAisle, toolMode, selectedDecal]); - setDisplayComponent("none"); - }, [ - viewVersionHistory, - activeModule, - subModule, - isVersionSaved, - selectedFloorItem, - selectedWall, - selectedFloor, - selectedAisle, - toolMode, - selectedSubCategory,selectedCollider - ]); - - const renderComponent = () => { - switch (displayComponent) { - case "versionHistory": - return ; - case "globalProperties": - return ; - case "aisleProperties": - return ; - case "wallProperties": - return ; - case "floorProperties": - return ; - case "assetProperties": - return ; - case "selectedWallProperties": - return ; - case "selectedFloorProperties": - return ; - case "zoneProperties": - return ; - case "simulations": - return ; - case "mechanics": - return ; - case "analysis": - return ; - case "visualization": - return ; - case "selectedDecalProperties": - return ; - case "resourceManagement": - return ; + const renderComponent = () => { + switch (displayComponent) { + case "versionHistory": + return ; + case "globalProperties": + return ; + case "aisleProperties": + return ; + case "wallProperties": + return ; + case "floorProperties": + return ; + case "assetProperties": + return ; + case "zoneProperties": + return ; + case "selectedWallProperties": + return ; + case "selectedFloorProperties": + return ; + case "selectedDecalProperties": + return ; + case "selectedAisleProperties": + return ; + case "simulations": + return ; + case "mechanics": + return ; + case "analysis": + return ; + case "visualization": + return ; + case "resourceManagement": + return ; case "colliderProperties": return ; - default: - return null; - } - }; + default: + return null; + } + }; - return ( -
-
- {toggleUIRight && ( - <> - {(!isVersionSaved || activeModule !== "simulation") && ( -
- {activeModule !== "simulation" && ( + return ( +
+
+ {toggleUIRight && ( <> - + {(!isVersionSaved || activeModule !== "simulation") && ( +
+ {activeModule !== "simulation" && ( + <> + + + )} + + {activeModule === "simulation" && ( + <> + + + + + )} + + {(activeModule === "builder" || + activeModule === "simulation") && ( + + )} +
+ )} + + {displayComponent !== "none" && ( +
+
+ {renderComponent()} + {/* */} +
+
+ )} - )} - - {activeModule === "simulation" && ( - <> - - - - - )} - - {(activeModule === "builder" || - activeModule === "simulation") && ( - - )} -
- )} - - {displayComponent !== "none" && ( -
-
- {renderComponent()} - {/* */} -
-
- )} - - )} -
- ); + )} +
+ ); }; export default SideBarRight; diff --git a/app/src/components/layout/sidebarRight/customInput/RotationInput.tsx b/app/src/components/layout/sidebarRight/customInput/RotationInput.tsx index 962e967..1265d81 100644 --- a/app/src/components/layout/sidebarRight/customInput/RotationInput.tsx +++ b/app/src/components/layout/sidebarRight/customInput/RotationInput.tsx @@ -1,30 +1,46 @@ import React from "react"; interface RotationInputProps { - onChange: (value: string) => void; // Callback for value change - placeholder?: string; // Optional placeholder - type?: string; // Input type (e.g., text, number, email) + heading?: string; + label?: string; + onChange: (value: string) => void; + placeholder?: string; + type?: string; value?: number; + disabled?: boolean; + min?: number; + max?: number; + step?: number; } const RotationInput: React.FC = ({ + label = "Rotate :", + heading = "Rotation", onChange, - placeholder = "Enter value", // Default placeholder - type = "number", // Default type - value = "number", + placeholder = "Enter value", + type = "number", + value, + disabled = false, + min, + max, + step, }) => { return (
-
Rotation
+
{heading}
-
Rotate :
+
{label}
onChange(e.target.value)} placeholder={placeholder} value={value} + disabled={disabled} + min={min} + max={max} + step={step} />
diff --git a/app/src/components/layout/sidebarRight/properties/AisleProperties.tsx b/app/src/components/layout/sidebarRight/properties/AisleProperties.tsx index c343805..ca2317d 100644 --- a/app/src/components/layout/sidebarRight/properties/AisleProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/AisleProperties.tsx @@ -21,24 +21,25 @@ interface TextureItem { texture: string; } +export const aisleTextureList: TextureItem[] = [ + { color: "yellow", id: "#FBE50E", brief: "pedestrian walkways", texture: "" }, + { color: "gray", id: "#6F6F7A", brief: "basic", texture: "" }, + { color: "green", id: "#43C06D", brief: "pedestrian walkways", texture: "" }, + { color: "orange", id: "#FF711B", brief: "material flow", texture: "" }, + { color: "blue", id: "#488EF6", brief: "vehicle paths", texture: "" }, + { color: "purple", id: "#AF52DE", brief: "material flow", texture: "" }, + { color: "red", id: "#FF3B30", brief: "safety zone", texture: "" }, + { color: "bright green", id: "#66FF00", brief: "safety zone", texture: "" }, + { color: "yellow-black", id: "yellow-black", brief: "utility aisles", texture: "" }, + { color: "white-black", id: "white-black", brief: "utility aisles", texture: "" }, +]; + const AisleProperties: React.FC = () => { const [collapsePresets, setCollapsePresets] = useState(false); const [collapseTexture, setCollapseTexture] = useState(true); const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, isFlipped, setAisleType, setAisleColor, setAisleWidth, setDashLength, setGapLength, setDotRadius, setAisleLength, setIsFlipped } = useBuilderStore(); - const aisleTextureList: TextureItem[] = [ - { color: "yellow", id: "yellow", brief: "pedestrian walkways", texture: "" }, - { color: "gray", id: "gray", brief: "basic", texture: "" }, - { color: "green", id: "green", brief: "pedestrian walkways", texture: "" }, - { color: "orange", id: "orange", brief: "material flow", texture: "" }, - { color: "blue", id: "blue", brief: "vehicle paths", texture: "" }, - { color: "purple", id: "purple", brief: "material flow", texture: "" }, - { color: "red", id: "red", brief: "safety zone", texture: "" }, - { color: "bright green", id: "#66FF00", brief: "safety zone", texture: "" }, - { color: "yellow-black", id: "yellow-black", brief: "utility aisles", texture: "" }, - { color: "white-black", id: "white-black", brief: "utility aisles", texture: "" }, - ]; const aisleTypes: { name: string; @@ -285,7 +286,12 @@ const AisleProperties: React.FC = () => { onClick={() => setAisleColor(val.id)} aria-pressed={aisleColor === val.id} > -
{val.texture}
+
+ {val.texture} +
{val.color}
{`( ${val.brief} )`}
diff --git a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx index a62abda..3a36e6c 100644 --- a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx @@ -32,9 +32,9 @@ const AssetProperties: React.FC = () => { setUserData([]); }; - const handleUserDataChange = (id: number, newValue: string) => {}; + const handleUserDataChange = (id: number, newValue: string) => { }; - const handleRemoveUserData = (id: number) => {}; + const handleRemoveUserData = (id: number) => { }; const handleAnimationClick = (animation: string) => { if (selectedFloorItem) { @@ -57,14 +57,16 @@ const AssetProperties: React.FC = () => {
{objectPosition && ( {}} + disabled={true} + onChange={() => { }} value1={parseFloat(objectPosition.x.toFixed(5))} value2={parseFloat(objectPosition.z.toFixed(5))} /> )} {objectRotation && ( {}} + disabled={true} + onChange={() => { }} value={parseFloat(objectRotation.y.toFixed(5))} /> )} @@ -107,7 +109,7 @@ const AssetProperties: React.FC = () => { if (asset.modelUuid !== selectedFloorItem.uuid || !asset.animations) return ( i === 0 && ( -
+
Looks like there are no preset animations yet. Stay tuned for future additions!
diff --git a/app/src/components/layout/sidebarRight/properties/DecalProperties.tsx b/app/src/components/layout/sidebarRight/properties/DecalProperties.tsx deleted file mode 100644 index 4163ea5..0000000 --- a/app/src/components/layout/sidebarRight/properties/DecalProperties.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - LayeringBottomIcon, - LayeringTopIcon, -} from "../../../icons/ExportCommonIcons"; -import InputRange from "../../../ui/inputs/InputRange"; -import RotationInput from "../customInput/RotationInput"; -import Vector3Input from "../customInput/Vector3Input"; - -const DecalProperties = () => { - return ( -
-
Decal Propertis
-
- {}} - value={10} - /> - console.log(value)} - header="Scale" - value={[0, 0, 0] as [number, number, number]} - /> -
- -
- console.log(value)} - key={"6"} - /> - -
-
Layering
- -
- - -
-
-
-
- ); -}; - -export default DecalProperties; diff --git a/app/src/components/layout/sidebarRight/properties/SelectedAisleProperties.tsx b/app/src/components/layout/sidebarRight/properties/SelectedAisleProperties.tsx new file mode 100644 index 0000000..3dc2540 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/SelectedAisleProperties.tsx @@ -0,0 +1,529 @@ +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; +import { ArrowIcon } from "../../../icons/ExportCommonIcons"; + +// image imports +import Arc from "../../../../assets/image/aisleTypes/Arc.png"; +import Arrow from "../../../../assets/image/aisleTypes/Arrow.png"; +import Arrows from "../../../../assets/image/aisleTypes/Arrows.png"; +import Circle from "../../../../assets/image/aisleTypes/Circle.png"; +import Dashed from "../../../../assets/image/aisleTypes/Dashed.png"; +import Directional from "../../../../assets/image/aisleTypes/Directional.png"; +import Dotted from "../../../../assets/image/aisleTypes/Dotted.png"; +import Solid from "../../../../assets/image/aisleTypes/Solid.png"; +import InputToggle from "../../../ui/inputs/InputToggle"; +import { useBuilderStore } from "../../../../store/builder/useBuilderStore"; +import { useSceneContext } from "../../../../modules/scene/sceneContext"; +import { useVersionContext } from "../../../../modules/builder/version/versionContext"; +import { useSocketStore } from "../../../../store/builder/store"; +import { getUserData } from "../../../../functions/getUserData"; +import { aisleTextureList } from "./AisleProperties"; + +const SelectedAisleProperties: React.FC = () => { + const [collapsePresets, setCollapsePresets] = useState(false); + const [collapseTexture, setCollapseTexture] = useState(true); + const { aisleStore } = useSceneContext(); + const { getAisleById, updateAisle, setDashedAisleProperties, setDottedAisleProperties, setArrowsAisleProperties, setArcAisleWidth, setColor } = aisleStore(); + const { selectedVersionStore } = useVersionContext(); + const { selectedVersion } = selectedVersionStore(); + const { socket } = useSocketStore(); + const { userId, organization } = getUserData(); + const { projectId } = useParams(); + const [selectedAisleData, setSelectedAisleData] = useState(); + + const { selectedAisle, setSelectedAisle } = useBuilderStore(); + + useEffect(() => { + const aisleData = getAisleById(selectedAisle?.aisleMesh?.uuid || ""); + setSelectedAisleData(aisleData); + }, [selectedAisle, getAisleById]); + + if (!selectedAisleData) return null; + + const updateBackend = (updatedAisle: Aisle) => { + if (updatedAisle && projectId) { + + // API + + // upsertAisleApi(updatedAisle.aisleUuid, updatedAisle.points, updatedAisle.type, projectId, selectedVersion?.versionId || ''); + + // SOCKET + + const data = { + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization, + aisleUuid: updatedAisle.aisleUuid, + points: updatedAisle.points, + type: updatedAisle.type + } + + socket.emit('v1:model-aisle:add', data); + } + } + + const aisleTypes: { + name: string; + type: AisleTypes; + id: string; + thumbnail: string; + }[] = [ + { name: "Solid", type: "solid-aisle", id: "1", thumbnail: Solid }, + { name: "Dotted", type: "dotted-aisle", id: "2", thumbnail: Dotted }, + { name: "Dashed", type: "dashed-aisle", id: "3", thumbnail: Dashed }, + { name: "Arrow", type: "arrow-aisle", id: "4", thumbnail: Arrow }, + { name: "Continuous Arrows", type: "arrows-aisle", id: "5", thumbnail: Arrows }, + { name: "Directional", type: "junction-aisle", id: "6", thumbnail: Directional }, + { name: "Arc", type: "arc-aisle", id: "7", thumbnail: Arc }, + { name: "Circle", type: "circle-aisle", id: "8", thumbnail: Circle }, + ]; + + const createAisleTypeObject = (newType: AisleTypes, currentType: AisleType): AisleType => { + switch (newType) { + case 'solid-aisle': + return { + aisleType: 'solid-aisle', + aisleColor: currentType.aisleColor, + aisleWidth: 'aisleWidth' in currentType ? currentType.aisleWidth : 0.1 + } as SolidAisle; + + case 'dashed-aisle': + return { + aisleType: 'dashed-aisle', + aisleColor: currentType.aisleColor, + aisleWidth: 'aisleWidth' in currentType ? currentType.aisleWidth : 0.1, + dashLength: 'dashLength' in currentType ? (currentType as DashedAisle).dashLength : 0.5, + gapLength: 'gapLength' in currentType ? (currentType as DashedAisle).gapLength : 0.3 + } as DashedAisle; + + case 'dotted-aisle': + return { + aisleType: 'dotted-aisle', + aisleColor: currentType.aisleColor, + dotRadius: 'dotRadius' in currentType ? (currentType as DottedAisle).dotRadius : 0.1, + gapLength: 'gapLength' in currentType ? (currentType as DottedAisle).gapLength : 0.3 + } as DottedAisle; + + case 'arrow-aisle': + return { + aisleType: 'arrow-aisle', + aisleColor: currentType.aisleColor, + aisleWidth: 'aisleWidth' in currentType ? currentType.aisleWidth : 0.1 + } as ArrowAisle; + + case 'arrows-aisle': + return { + aisleType: 'arrows-aisle', + aisleColor: currentType.aisleColor, + aisleWidth: 'aisleWidth' in currentType ? currentType.aisleWidth : 0.1, + aisleLength: 'aisleLength' in currentType ? (currentType as ArrowsAisle).aisleLength : 0.6, + gapLength: 'gapLength' in currentType ? (currentType as ArrowsAisle).gapLength : 0.3 + } as ArrowsAisle; + + case 'arc-aisle': + return { + aisleType: 'arc-aisle', + aisleColor: currentType.aisleColor, + aisleWidth: 'aisleWidth' in currentType ? currentType.aisleWidth : 0.1, + isFlipped: 'isFlipped' in currentType ? (currentType as ArcAisle).isFlipped : false + } as ArcAisle; + + case 'circle-aisle': + return { + aisleType: 'circle-aisle', + aisleColor: currentType.aisleColor, + aisleWidth: 'aisleWidth' in currentType ? currentType.aisleWidth : 0.1 + } as CircleAisle; + + case 'junction-aisle': + return { + aisleType: 'junction-aisle', + aisleColor: currentType.aisleColor, + aisleWidth: 'aisleWidth' in currentType ? currentType.aisleWidth : 0.1, + isFlipped: 'isFlipped' in currentType ? (currentType as JunctionAisle).isFlipped : false + } as JunctionAisle; + + default: + return { + aisleType: 'solid-aisle', + aisleColor: currentType.aisleColor, + aisleWidth: 0.1 + } as SolidAisle; + } + }; + + const handleAisleTypeChange = (newType: AisleTypes) => { + if (!selectedAisle?.aisleData) return; + const newAisleType = createAisleTypeObject(newType, selectedAisleData.type); + + const updatedAisle = updateAisle(selectedAisleData.aisleUuid, { + type: newAisleType + }); + + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: null }); + + setSelectedAisleData({ + ...selectedAisleData, + type: newAisleType + }); + + if (updatedAisle) { + updateBackend(updatedAisle); + } + }; + + const handleColorChange = (value: AisleColors) => { + const updatedAisle = setColor(selectedAisleData.aisleUuid, value); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...selectedAisleData.type, + aisleColor: value + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + }; + + const handleAisleWidthChange = (value: string) => { + const width = parseFloat(value); + if (!isNaN(width) && selectedAisleData.type.aisleType !== 'dotted-aisle') { + const updatedAisle = updateAisle(selectedAisleData.aisleUuid, { + type: { + ...selectedAisleData.type, + aisleWidth: width + } + }); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...selectedAisleData.type, + aisleWidth: width + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + } + }; + + const handleDashLengthChange = (value: string) => { + const length = parseFloat(value); + if (!isNaN(length) && selectedAisleData.type.aisleType === 'dashed-aisle') { + const updatedAisle = setDashedAisleProperties(selectedAisleData.aisleUuid, { + dashLength: length + }); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...(selectedAisleData.type as DashedAisle), + dashLength: length + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + } + }; + + const handleGapLengthChange = (value: string) => { + const length = parseFloat(value); + if (!isNaN(length) && (selectedAisleData.type.aisleType === 'dashed-aisle' || selectedAisleData.type.aisleType === 'dotted-aisle' || selectedAisleData.type.aisleType === 'arrows-aisle')) { + if (selectedAisleData.type.aisleType === 'dashed-aisle') { + const updatedAisle = setDashedAisleProperties(selectedAisleData.aisleUuid, { + gapLength: length + }); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...(selectedAisleData.type as DashedAisle), + gapLength: length + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + } else if (selectedAisleData.type.aisleType === 'dotted-aisle') { + const updatedAisle = setDottedAisleProperties(selectedAisleData.aisleUuid, { + gapLength: length + }); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...(selectedAisleData.type as DottedAisle), + gapLength: length + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + } else if (selectedAisleData.type.aisleType === 'arrows-aisle') { + const updatedAisle = setArrowsAisleProperties(selectedAisleData.aisleUuid, { + gapLength: length + }); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...(selectedAisleData.type as ArrowsAisle), + gapLength: length + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + } + } + }; + + const handleDotRadiusChange = (value: string) => { + const radius = parseFloat(value); + if (!isNaN(radius) && selectedAisleData.type.aisleType === 'dotted-aisle') { + const updatedAisle = setDottedAisleProperties(selectedAisleData.aisleUuid, { + dotRadius: radius + }); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...(selectedAisleData.type as DottedAisle), + dotRadius: radius + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + } + }; + + const handleAisleLengthChange = (value: string) => { + const length = parseFloat(value); + if (!isNaN(length) && selectedAisleData.type.aisleType === 'arrows-aisle') { + const updatedAisle = setArrowsAisleProperties(selectedAisleData.aisleUuid, { + aisleLength: length + }); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...(selectedAisleData.type as ArrowsAisle), + aisleLength: length + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + } + }; + + const handleIsFlippedChange = () => { + if (selectedAisleData.type.aisleType === 'arc-aisle' || selectedAisleData.type.aisleType === 'junction-aisle') { + const currentType = selectedAisleData.type as ArcAisle | JunctionAisle; + const currentFlipped = currentType.isFlipped || false; + const updatedAisle = setArcAisleWidth(selectedAisleData.aisleUuid, { + isFlipped: !currentFlipped + }); + + setSelectedAisleData({ + ...selectedAisleData, + type: { + ...currentType, + isFlipped: !currentFlipped + } + }) + + if (updatedAisle) { + updateBackend(updatedAisle); + } + } + }; + + const renderAdvancedProperties = () => { + switch (selectedAisleData.type.aisleType) { + case 'dashed-aisle': + const dashedType = selectedAisleData.type as DashedAisle; + return ( + <> + + + + ); + case 'dotted-aisle': + const dottedType = selectedAisleData.type as DottedAisle; + return ( + <> + + + + ); + case 'arrows-aisle': + const arrowsType = selectedAisleData.type as ArrowsAisle; + return ( + <> + + + + ); + case 'junction-aisle': + case 'arc-aisle': + const flippedType = selectedAisleData.type as ArcAisle | JunctionAisle; + return ( + + ); + default: + return null; + } + }; + + return ( +
+
Properties
+ + {/* Basic Properties */} +
+ {selectedAisleData.type.aisleType !== 'dotted-aisle' && + + } + {renderAdvancedProperties()} +
+ + {/* Presets */} +
+ + {!collapsePresets && ( +
+ {aisleTypes.map((val) => ( +
+ +
+ ))} +
+ )} +
+ + {/* Texture */} +
+ + + {collapseTexture && ( +
+ {aisleTextureList.map((val) => ( + + ))} +
+ )} +
+
+ ); +}; + +export default SelectedAisleProperties; \ No newline at end of file diff --git a/app/src/components/layout/sidebarRight/properties/SelectedDecalProperties.tsx b/app/src/components/layout/sidebarRight/properties/SelectedDecalProperties.tsx new file mode 100644 index 0000000..3a67865 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/SelectedDecalProperties.tsx @@ -0,0 +1,188 @@ +import { useParams } from "react-router-dom"; +import { useVersionContext } from "../../../../modules/builder/version/versionContext"; +import { useSceneContext } from "../../../../modules/scene/sceneContext"; +import { useBuilderStore } from "../../../../store/builder/useBuilderStore"; +import { LayeringBottomIcon, LayeringTopIcon } from "../../../icons/ExportCommonIcons"; +import { useSocketStore } from "../../../../store/builder/store"; +import InputRange from "../../../ui/inputs/InputRange"; + +import { getUserData } from "../../../../functions/getUserData"; +// import { upsertWallApi } from "../../../../services/factoryBuilder/wall/upsertWallApi"; +// import { upsertFloorApi } from "../../../../services/factoryBuilder/floor/upsertFloorApi"; + +const SelectedDecalProperties = () => { + const { selectedDecal, setSelectedDecal } = useBuilderStore(); + const { wallStore, floorStore } = useSceneContext(); + const { updateDecal: updateDecalInWall } = wallStore(); + const { updateDecal: updateDecalInFloor } = floorStore(); + const { userId, organization } = getUserData(); + const { selectedVersionStore } = useVersionContext(); + const { selectedVersion } = selectedVersionStore(); + const { projectId } = useParams(); + const { socket } = useSocketStore(); + + const updateBackend = (updatedData: Wall | Floor) => { + if ('wallUuid' in updatedData) { + if (projectId && updatedData) { + // API + + // upsertWallApi(projectId, selectedVersion?.versionId || '', updatedData); + + // SOCKET + + const data = { + wallData: updatedData, + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization + } + + socket.emit('v1:model-Wall:add', data); + } + } else if ('floorUuid' in updatedData) { + if (projectId && updatedData) { + // API + + // upsertFloorApi(projectId, selectedVersion?.versionId || '', updatedData); + + // SOCKET + + const data = { + floorData: updatedData, + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization + } + + socket.emit('v1:model-Floor:add', data); + } + } + } + + const handleRotationChange = (value: number) => { + if (!selectedDecal) return; + const updatedDecal = { ...selectedDecal.decalData, decalRotation: value }; + setSelectedDecal({ ...selectedDecal, decalData: updatedDecal }); + + if ('wallUuid' in selectedDecal.decalData.decalType) { + const updatedWall = updateDecalInWall(updatedDecal.decalUuid, updatedDecal); + if (updatedWall) updateBackend(updatedWall); + } else if ('floorUuid' in selectedDecal.decalData.decalType) { + const updatedFloor = updateDecalInFloor(updatedDecal.decalUuid, updatedDecal); + if (updatedFloor) updateBackend(updatedFloor); + } + } + + const handleScaleChange = (value: number) => { + if (!selectedDecal) return; + const updatedDecal = { ...selectedDecal.decalData, decalScale: value }; + setSelectedDecal({ ...selectedDecal, decalData: updatedDecal }); + + if ('wallUuid' in selectedDecal.decalData.decalType) { + const updatedWall = updateDecalInWall(updatedDecal.decalUuid, updatedDecal); + if (updatedWall) updateBackend(updatedWall); + } else if ('floorUuid' in selectedDecal.decalData.decalType) { + const updatedFloor = updateDecalInFloor(updatedDecal.decalUuid, updatedDecal); + if (updatedFloor) updateBackend(updatedFloor); + } + } + + const handleOpacityChange = (value: number) => { + if (!selectedDecal) return; + const updatedDecal = { ...selectedDecal.decalData, decalOpacity: value }; + setSelectedDecal({ ...selectedDecal, decalData: updatedDecal }); + + if ('wallUuid' in selectedDecal.decalData.decalType) { + const updatedWall = updateDecalInWall(updatedDecal.decalUuid, updatedDecal); + if (updatedWall) updateBackend(updatedWall); + } else if ('floorUuid' in selectedDecal.decalData.decalType) { + const updatedFloor = updateDecalInFloor(updatedDecal.decalUuid, updatedDecal); + if (updatedFloor) updateBackend(updatedFloor); + } + } + + const handleLayerChange = (direction: "up" | "down") => { + if (!selectedDecal) return; + + const position: [number, number, number] = [...(selectedDecal.decalData.decalPosition || [0, 0, 0]),]; + + if (direction === "up") { + position[2] = Math.abs(position[2]); + } else { + position[2] = -Math.abs(position[2]); + } + + const updatedDecal: Decal = { ...selectedDecal.decalData, decalPosition: position, }; + + setSelectedDecal({ ...selectedDecal, decalData: updatedDecal }); + + if ("wallUuid" in selectedDecal.decalData.decalType) { + const updatedWall = updateDecalInWall(updatedDecal.decalUuid, updatedDecal); + if (updatedWall) updateBackend(updatedWall); + } else if ("floorUuid" in selectedDecal.decalData.decalType) { + const updatedFloor = updateDecalInFloor(updatedDecal.decalUuid, updatedDecal); + if (updatedFloor) updateBackend(updatedFloor); + } + }; + + if (!selectedDecal) return null; + + return ( +
+
Decal Properties
+
+ handleRotationChange(value)} + /> + + handleScaleChange(value)} + /> +
+ +
+ handleOpacityChange(value)} + /> + +
+
Layering
+ +
+ + +
+
+
+
+ ); +}; + +export default SelectedDecalProperties; diff --git a/app/src/components/layout/sidebarRight/resourceManagement/hrm/Hrm.tsx b/app/src/components/layout/sidebarRight/resourceManagement/hrm/Hrm.tsx index 99828ae..ce17d60 100644 --- a/app/src/components/layout/sidebarRight/resourceManagement/hrm/Hrm.tsx +++ b/app/src/components/layout/sidebarRight/resourceManagement/hrm/Hrm.tsx @@ -1,199 +1,225 @@ -import { useEffect, useState } from 'react' -import { ClockThreeIcon, LocationPinIcon, TargetIcon } from '../../../../icons/ExportCommonIcons' -import { useSceneContext } from '../../../../../modules/scene/sceneContext'; -import { useProductContext } from '../../../../../modules/simulation/products/productContext'; -import RenameInput from '../../../../ui/inputs/RenameInput'; -import { useResourceManagementId } from '../../../../../store/builder/store'; -import { set } from 'immer/dist/internal'; +import { useEffect, useState } from "react"; +import { + ClockThreeIcon, + LocationPinIcon, + TargetIcon, +} from "../../../../icons/ExportCommonIcons"; +import { useSceneContext } from "../../../../../modules/scene/sceneContext"; +import RenameInput from "../../../../ui/inputs/RenameInput"; +import { useResourceManagementId } from "../../../../../store/builder/store"; +import { getAssetThumbnail } from "../../../../../services/factoryBuilder/asset/assets/getAssetThumbnail"; // import NavigateCatagory from '../NavigateCatagory' const Hrm = () => { - const [selectedCard, setSelectedCard] = useState(0); - const [workers, setWorkers] = useState([]); - - const { productStore } = useSceneContext(); - const { products, getProductById } = productStore(); - const { selectedProductStore } = useProductContext(); - const { selectedProduct } = selectedProductStore(); - const { setResourceManagementId } = useResourceManagementId(); - - useEffect(() => { - if (selectedProduct) { - const productDetails = getProductById(selectedProduct.productUuid); - const workerDetails = productDetails?.eventDatas || []; - - const formattedWorkers = workerDetails - .filter((worker: any) => worker.type === "human") - .map((worker: any, index: number) => ({ - employee: { - image: "", - name: worker.modelName, - modelId: worker.modelUuid, - employee_id: `HR-${204 + index}`, - status: "Active", - }, - task: { - status: "Ongoing", - title: worker.taskTitle || "No Task Assigned", - location: { - floor: worker.floor || 0, - zone: worker.zone || "N/A" - }, - planned_time_hours: worker.plannedTime || 0, - time_spent_hours: worker.timeSpent || 0, - total_tasks: worker.totalTasks || 0, - completed_tasks: worker.completedTasks || 0 - }, - actions: [ - "Assign Task", - "Reassign Task", - "Pause", - "Emergency Stop" - ], - location: `Floor ${worker.floor || "-"} . Zone ${worker.zone || "-"}` - })); - - setWorkers(formattedWorkers); - } - }, [selectedProduct, getProductById]); - - useEffect(() => { - // - }, [workers]); - - - - - // const employee_details = [ - // { - // "employee": { - // image: "", - // "name": "John Doe", - // "employee_id": "HR-204", - // "status": "Active", - - // }, - // "task": { - // "status": "Ongoing", - // "title": "Inspecting Machine X", - // "location": { - // "floor": 4, - // "zone": "B" - // }, - // "planned_time_hours": 6, - // "time_spent_hours": 2, - // "total_tasks": 12, - // "completed_tasks": 3 - // }, - // "actions": [ - // "Assign Task", - // "Reassign Task", - // "Pause", - // "Emergency Stop" - // ], - // "location": "Floor 4 . Zone B" - // }, - // { - // "employee": { - // image: "", - // "name": "Alice Smith", - // "employee_id": "HR-205", - // "status": "Active", - - // }, - // "task": { - // "status": "Ongoing", - // "title": "Calibrating Sensor Y", - // "location": { - // "floor": 2, - // "zone": "A" - // }, - // "planned_time_hours": 4, - // "time_spent_hours": 1.5, - // "total_tasks": 10, - // "completed_tasks": 2 - // }, - // "actions": [ - // "Assign Task", - // "Reassign Task", - // "Pause", - // "Emergency Stop" - // ], - // "location": "Floor 4 . Zone B" - // }, - // { - // "employee": { - // image: "", - // "name": "Michael Lee", - // "employee_id": "HR-206", - // "status": "Active", - - // }, - // "task": { - // "status": "Ongoing", - // "title": "Testing Conveyor Belt Z", - // "location": { - // "floor": 5, - // "zone": "C" - // }, - // "planned_time_hours": 5, - // "time_spent_hours": 3, - // "total_tasks": 8, - // "completed_tasks": 5 - // }, - // "actions": [ - // "Assign Task", - // "Reassign Task", - // "Pause", - // "Emergency Stop" - // ], - // "location": "Floor 4 . Zone B" - // }, - // ] - function handleRenameWorker(newName: string) { - // + const [selectedCard, setSelectedCard] = useState(0); + const [workers, setWorkers] = useState([]); + const { setResourceManagementId } = useResourceManagementId(); + const { assetStore } = useSceneContext(); + const { assets: allAssets } = assetStore(); + async function getAsset(assetId: string) { + let thumbnail = await getAssetThumbnail(assetId); + if (thumbnail.thumbnail) { + let assetImage = thumbnail.thumbnail; + return assetImage; } - function handleHumanClick(employee: any) { - if (employee.modelId) { - setResourceManagementId(employee.modelId); - } + } + + useEffect(() => { + if (allAssets.length > 0) { + const fetchWorkers = async () => { + const humans = allAssets.filter( + (worker: any) => worker.eventData.type === "Human" + ); + + const formattedWorkers = await Promise.all( + humans.map(async (worker: any, index: number) => { + const assetImage = await getAsset(worker.assetId); + + return { + employee: { + image: assetImage, + name: worker.modelName, + modelId: worker.modelUuid, + employee_id: `HR-${204 + index}`, + status: "Active", + }, + task: { + status: "Ongoing", + title: worker.taskTitle ?? "No Task Assigned", + location: { + floor: worker.floor ?? 0, + zone: worker.zone ?? "N/A", + }, + planned_time_hours: worker.plannedTime ?? 0, + time_spent_hours: worker.timeSpent ?? 0, + total_tasks: worker.totalTasks ?? 0, + completed_tasks: worker.completedTasks ?? 0, + }, + actions: [ + "Assign Task", + "Reassign Task", + "Pause", + "Emergency Stop", + ], + location: `Floor ${worker.floor || "-"} . Zone ${ + worker.zone || "-" + }`, + }; + }) + ); + + setWorkers(formattedWorkers); + }; + + fetchWorkers(); } + }, [allAssets]); + // const employee_details = [ + // { + // "employee": { + // image: "", + // "name": "John Doe", + // "employee_id": "HR-204", + // "status": "Active", - return ( - <> - {/* + {/* */} -
- {workers.map((employee, index) => ( -
setSelectedCard(index)} - key={index} - > -
-
-
- -
-
-
- {/*
{employee.employee.name}
*/} - -
{employee.employee.employee_id}
-
-
+
+ {workers.map((employee, index) => ( +
setSelectedCard(index)} + key={index} + > +
+
+
+ +
+
+
+ {/*
{employee.employee.name}
*/} + +
+ {employee.employee.employee_id} +
+
+
-
{ handleHumanClick(employee.employee) }}>View in Scene
-
+
{ + handleHumanClick(employee.employee); + }} + > + View in Scene +
+
-
- {/*
+
+ {/*
@@ -214,17 +240,20 @@ const Hrm = () => {
*/} -
-
+
+
+
+ + + + Planned time: +
-
- - Planned time: -
- - {employee.task.planned_time_hours} hr -
- {/*
+ + {employee.task.planned_time_hours} hr + +
+ {/*
@@ -242,39 +271,41 @@ const Hrm = () => { {employee.task.time_spent_hours} hr
*/} -
+
+
+ + + + Cost per hr: +
-
- - Cost per hr: -
+ + {employee.task.completed_tasks} + +
+
- {employee.task.completed_tasks} -
-
- -
-
-
- -
-
Location:
-
-
{employee.location}
-
-
- {/* - */} - - -
-
- -
- ))} +
+
+
+ +
+
Location:
+
+
{employee.location}
+
+ {/*
+ + + + +
*/}
- - ) -} +
+ ))} +
+ + ); +}; -export default Hrm +export default Hrm; diff --git a/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx b/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx index faa6015..eddec0b 100644 --- a/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx +++ b/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx @@ -1,62 +1,70 @@ import { useEffect, useState } from 'react' // import NavigateCatagory from '../../NavigateCatagory' -import { EyeIcon, ForkLiftIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons'; -import assetImage from "../../../../../../assets/image/asset-image.png" +import { EyeIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons'; +import assetImageFallback from "../../../../../../assets/image/asset-image.png" import { useSceneContext } from '../../../../../../modules/scene/sceneContext'; -import { useProductContext } from '../../../../../../modules/simulation/products/productContext'; import RenameInput from '../../../../../ui/inputs/RenameInput'; import { useResourceManagementId } from '../../../../../../store/builder/store'; +import { getAssetThumbnail } from '../../../../../../services/factoryBuilder/asset/assets/getAssetThumbnail'; +import { TypeBasedAssetIcons } from '../../../../../icons/AssetTypeIcons'; + const AssetManagement = () => { // const [selectedCategory, setSelectedCategory] = useState("All Assets"); const [expandedAssetId, setExpandedAssetId] = useState(null); const [assets, setAssets] = useState([]); - - const { productStore } = useSceneContext(); - const { products, getProductById } = productStore(); - const { selectedProductStore } = useProductContext(); - const { selectedProduct } = selectedProductStore(); const { setResourceManagementId } = useResourceManagementId(); + const { assetStore } = useSceneContext(); + const { assets: allAssets } = assetStore(); - + async function getAsset(assetId: string) { + let thumbnail = await getAssetThumbnail(assetId) + if (thumbnail.thumbnail) { + let assetImage = thumbnail.thumbnail ?? assetImageFallback; + return assetImage; + } + } useEffect(() => { - if (selectedProduct) { - const productDetails = getProductById(selectedProduct.productUuid); - const productAssets = productDetails?.eventDatas || []; - const grouped: Record = {}; - productAssets.forEach((asset: any) => { - if (asset.type === "storageUnit" || asset.type === "human") return; - if (!grouped[asset.modelName]) { - grouped[asset.modelName] = { - id: asset.modelUuid, - name: asset.modelName, - model: asset.modelCode || "N/A", - status: asset.status || "Online", - usageRate: asset.usageRate || 15, - level: asset.level || "Level 1", - image: assetImage, - description: asset.description || "No description", - cost: asset.cost || 0, - count: 1, - }; - } else { - grouped[asset.modelName].count += 1; - } - }); + if (allAssets.length > 0) { + const fetchAssets = async () => { + const grouped: Record = {}; - setAssets(Object.values(grouped)); + // Use Promise.all to handle all async operations + await Promise.all(allAssets.map(async (asset: any) => { + + if (asset.eventData.type === "Storage" || asset.eventData.type === "Human") return; + + const assetImage = await getAsset(asset.assetId); + + if (!grouped[asset.assetId]) { + // + grouped[asset.assetId] = { + id: asset.modelUuid, + assetId: asset.assetId, + name: asset.modelName, + type: asset.eventData.type, + model: asset.modelCode ?? "N/A", + status: asset.status ?? "Online", + usageRate: asset.usageRate ?? 15, + level: asset.level ?? "Level 1", + image: assetImage, + description: asset.description ?? "No description", + cost: asset.cost ?? 0, + count: 1, + }; + } else { + grouped[asset.assetId].count += 1; + } + })); + + setAssets(Object.values(grouped)); + } + fetchAssets(); } - }, [selectedProduct]); + }, [allAssets]); function handleRenameAsset(newName: string) { - // - // if (expandedAssetId) { - // setAssets(prevAssets => - // prevAssets.map(asset => - // asset.id === expandedAssetId ? { ...asset, name: newName } : asset - // ) - // ); - // } + } useEffect(() => { @@ -65,8 +73,6 @@ const AssetManagement = () => { }, [assets]); function handleAssetClick(id: string) { - - setResourceManagementId(id); } @@ -123,14 +129,13 @@ const AssetManagement = () => {
- {expandedAssetId === asset.id ? <>
setExpandedAssetId(null)}>▾
: -
+
}
@@ -170,9 +175,9 @@ const AssetManagement = () => {
-
+
handleAssetClick(asset.id)}> -
handleAssetClick(asset.id)}>View in Scene
+
View in Scene
@@ -210,16 +215,10 @@ const AssetManagement = () => {
{expandedAssetId === asset.id ? "View Less" : "View More"}
-
- -
- )} -
- ))}
diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index d5d49e4..4d29ad0 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -146,6 +146,12 @@ const Tools: React.FC = () => { case "draw-floor": is2D && setToolMode("Floor"); break; + case "move": + if (!is2D) setToolMode("Move-Asset"); + break; + case "rotate": + if (!is2D) setToolMode("Rotate-Asset"); + break; case "measure": setToolMode("MeasurementScale"); break; @@ -368,7 +374,7 @@ const Tools: React.FC = () => { )}
- {activeModule !== "visualization" && ( + {toggleThreeD && activeModule !== "visualization" && ( <>
diff --git a/app/src/components/ui/features/RenameTooltip.tsx b/app/src/components/ui/features/RenameTooltip.tsx index 87de122..d55c5d7 100644 --- a/app/src/components/ui/features/RenameTooltip.tsx +++ b/app/src/components/ui/features/RenameTooltip.tsx @@ -4,7 +4,6 @@ import { useLeftData, useTopData, } from "../../../store/visualization/useZone3DWidgetStore"; -import { useRenameModeStore } from "../../../store/builder/store"; type RenameTooltipProps = { name: string; @@ -20,6 +19,7 @@ const RenameTooltip: React.FC = ({ name, onSubmit }) => { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(value.trim()); + echo.info(`Selected Object has been renamed to ${value.trim()}`); setTop(0); setLeft(0); }; diff --git a/app/src/components/ui/inputs/RenameInput.tsx b/app/src/components/ui/inputs/RenameInput.tsx index 637dec3..31cc311 100644 --- a/app/src/components/ui/inputs/RenameInput.tsx +++ b/app/src/components/ui/inputs/RenameInput.tsx @@ -12,7 +12,12 @@ interface RenameInputProps { canEdit?: boolean; } -const RenameInput: React.FC = ({ value, onRename, checkDuplicate, canEdit = true }) => { +const RenameInput: React.FC = ({ + value, + onRename, + checkDuplicate, + canEdit = true, +}) => { const [isEditing, setIsEditing] = useState(false); const [text, setText] = useState(value); const [isDuplicate, setIsDuplicate] = useState(false); @@ -36,10 +41,10 @@ const RenameInput: React.FC = ({ value, onRename, checkDuplica }; const handleBlur = () => { - - if (isDuplicate) return + if (isDuplicate) return; setIsEditing(false); if (onRename && !isDuplicate) { + echo.info(`Selected Object has been renamed to ${text}`) onRename(text); } }; @@ -52,6 +57,7 @@ const RenameInput: React.FC = ({ value, onRename, checkDuplica if (e.key === "Enter" && !isDuplicate) { setIsEditing(false); if (onRename) { + echo.info(`Selected Object has been renamed to ${text}`) onRename(text); } } @@ -80,4 +86,4 @@ const RenameInput: React.FC = ({ value, onRename, checkDuplica ); }; -export default RenameInput +export default RenameInput; diff --git a/app/src/components/ui/inputs/Search.tsx b/app/src/components/ui/inputs/Search.tsx index ff9c6ef..73db02a 100644 --- a/app/src/components/ui/inputs/Search.tsx +++ b/app/src/components/ui/inputs/Search.tsx @@ -1,8 +1,8 @@ -import React, { ChangeEvent, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import { CloseIcon, SearchIcon } from "../../icons/ExportCommonIcons"; interface SearchProps { - value?: string; // The current value of the search input + value?: string | null; // The current value of the search input placeholder?: string; // Placeholder text for the input onChange: (value: string) => void; // Callback function to handle input changes } @@ -22,7 +22,15 @@ const Search: React.FC = ({ onChange(newValue); // Call the onChange prop with the new value }; + useEffect(() => { + if (value === null) { + setInputValue(""); + handleBlur(); + } + }, [value]); + const handleClear = () => { + echo.warn("Search field cleared."); setInputValue(""); onChange(""); // Clear the input value }; @@ -48,7 +56,7 @@ const Search: React.FC = ({ ) => { - const { camera } = useThree(); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (!controlsRef.current) return; - - // get current distance from camera to target - const target = new THREE.Vector3(); - controlsRef.current.getTarget(target); - - const distance = camera.position.distanceTo(target); - let pos: THREE.Vector3 | null = null; - - switch (e.key) { - case "1": // Front - pos = new THREE.Vector3(0, 0, distance).add(target); - break; - case "3": // Right - pos = new THREE.Vector3(distance, 0, 0).add(target); - break; - case "7": // Top - pos = new THREE.Vector3(0, distance, 0).add(target); - break; - case "9": // Back - pos = new THREE.Vector3(0, 0, -distance).add(target); - break; - } - - if (pos) { - controlsRef.current.setLookAt( - pos.x, pos.y, pos.z, // camera position - target.x, target.y, target.z, // keep same target - true // smooth transition - ); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [controlsRef, camera]); -}; diff --git a/app/src/modules/builder/Decal/decal.tsx b/app/src/modules/builder/Decal/decal.tsx new file mode 100644 index 0000000..d14e150 --- /dev/null +++ b/app/src/modules/builder/Decal/decal.tsx @@ -0,0 +1,14 @@ +import DecalCreator from './decalCreator/decalCreator' + +function Decal() { + + return ( + <> + + + + + ) +} + +export default Decal \ No newline at end of file diff --git a/app/src/modules/builder/Decal/decalCreator/decalCreator.tsx b/app/src/modules/builder/Decal/decalCreator/decalCreator.tsx new file mode 100644 index 0000000..44e46e3 --- /dev/null +++ b/app/src/modules/builder/Decal/decalCreator/decalCreator.tsx @@ -0,0 +1,152 @@ +import { MathUtils } from 'three'; +import { useEffect } from 'react'; +import { useThree } from '@react-three/fiber'; +import { useParams } from 'react-router-dom'; +import { useDroppedDecal, useSocketStore } from '../../../../store/builder/store'; +import useModuleStore from '../../../../store/useModuleStore'; +import { useSceneContext } from '../../../scene/sceneContext'; +import { useVersionContext } from '../../version/versionContext'; + +import { getUserData } from '../../../../functions/getUserData'; + +// import { upsertWallApi } from '../../../../services/factoryBuilder/wall/upsertWallApi'; +// import { upsertFloorApi } from '../../../../services/factoryBuilder/floor/upsertFloorApi'; + +function DecalCreator() { + const { wallStore, floorStore } = useSceneContext(); + const { addDecal: addDecalOnWall, getWallById } = wallStore(); + const { addDecal: addDecalOnFloor, getFloorById } = floorStore(); + const { droppedDecal } = useDroppedDecal(); + const { activeModule } = useModuleStore(); + const { userId, organization } = getUserData(); + const { selectedVersionStore } = useVersionContext(); + const { selectedVersion } = selectedVersionStore(); + const { projectId } = useParams(); + const { socket } = useSocketStore(); + const { controls, gl, pointer, camera, raycaster, scene } = useThree(); + + useEffect(() => { + const canvasElement = gl.domElement; + + const onDrop = (event: DragEvent) => { + if (droppedDecal) { + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(scene.children, true); + const wallIntersect = intersects.find(i => i.object.userData && i.object.userData.wallUuid); + const floorIntersect = intersects.find(i => i.object.userData && i.object.userData.floorUuid); + console.log('wallIntersect: ', wallIntersect); + + if (wallIntersect) { + const wall = getWallById(wallIntersect.object.userData.wallUuid); + if (!wall) return; + + const point = wallIntersect.object.worldToLocal(wallIntersect.point.clone()); + + const decal: Decal = { + decalUuid: MathUtils.generateUUID(), + decalName: droppedDecal.decalName, + decalId: droppedDecal.decalId, + decalType: { + type: 'Wall', + wallUuid: wallIntersect.object.userData.wallUuid, + }, + decalPosition: [point.x, point.y, (wall.wallThickness / 2 + 0.001) * (wallIntersect.normal?.z || 1)], + decalRotation: 0, + decalOpacity: 1, + decalScale: 0.5, + } + + addDecalOnWall(wallIntersect.object.userData.wallUuid, decal); + + setTimeout(() => { + const updatedWall = getWallById(wallIntersect.object.userData.wallUuid); + if (updatedWall) { + if (projectId && updatedWall) { + // API + + // upsertWallApi(projectId, selectedVersion?.versionId || '', updatedWall); + + // SOCKET + + const data = { + wallData: updatedWall, + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization + } + + socket.emit('v1:model-Wall:add', data); + } + } + }, 0) + } else if (floorIntersect) { + const floor = getFloorById(floorIntersect.object.userData.floorUuid); + if (!floor) return; + + const point = floorIntersect.object.worldToLocal(floorIntersect.point.clone()); + + const decal: Decal = { + decalUuid: MathUtils.generateUUID(), + decalName: droppedDecal.decalName, + decalId: droppedDecal.decalId, + decalType: { + type: 'Floor', + floorUuid: floorIntersect.object.userData.floorUuid, + }, + decalPosition: [point.x, point.y, -0.001], + decalRotation: 0, + decalOpacity: 1, + decalScale: 0.5, + } + + addDecalOnFloor(floorIntersect.object.userData.floorUuid, decal); + + setTimeout(() => { + const updatedFloor = getFloorById(floorIntersect.object.userData.floorUuid); + if (projectId && updatedFloor) { + // API + + // upsertFloorApi(projectId, selectedVersion?.versionId || '', updatedFloor); + + // SOCKET + + const data = { + floorData: updatedFloor, + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization + } + + socket.emit('v1:model-Floor:add', data); + } + }, 0) + } + } + }; + + const onDragOver = (event: any) => { + event.preventDefault(); + }; + + if (activeModule === "builder") { + canvasElement.addEventListener("drop", onDrop); + canvasElement.addEventListener("dragover", onDragOver); + } + + return () => { + canvasElement.removeEventListener("drop", onDrop); + canvasElement.removeEventListener("dragover", onDragOver); + }; + }, [droppedDecal, camera, activeModule, controls]); + + return ( + <> + + ) +} + +export default DecalCreator \ No newline at end of file diff --git a/app/src/modules/builder/Decal/decalInstance.tsx b/app/src/modules/builder/Decal/decalInstance.tsx deleted file mode 100644 index 1b68f3d..0000000 --- a/app/src/modules/builder/Decal/decalInstance.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as THREE from 'three'; -import { Decal } from '@react-three/drei' -import { useLoader } from '@react-three/fiber'; -import { useToggleView } from '../../../store/builder/store'; -import { useBuilderStore } from '../../../store/builder/useBuilderStore'; - -import defaultMaterial from '../../../assets/textures/floor/wall-tex.png'; -import useModuleStore from '../../../store/useModuleStore'; - -function DecalInstance({ visible = true, decal, zPosition = decal.decalPosition[2] }: { visible?: boolean, decal: Decal, zPosition?: number }) { - const { setSelectedWall, setSelectedFloor, selectedDecal, setSelectedDecal } = useBuilderStore(); - const { togglView } = useToggleView(); - const { activeModule } = useModuleStore(); - const material = useLoader(THREE.TextureLoader, defaultMaterial); - - return ( - { - if (visible && !togglView && activeModule === 'builder') { - if (e.object.userData.decalUuid) { - e.stopPropagation(); - setSelectedDecal(e.object); - setSelectedWall(null); - setSelectedFloor(null); - } - } - }} - onPointerMissed={() => { - if (selectedDecal && selectedDecal.userData.decalUuid === decal.decalUuid) { - setSelectedDecal(null); - } - }} - > - - - ) -} - -export default DecalInstance \ No newline at end of file diff --git a/app/src/modules/builder/Decal/decalInstance/decalInstance.tsx b/app/src/modules/builder/Decal/decalInstance/decalInstance.tsx new file mode 100644 index 0000000..f31dddd --- /dev/null +++ b/app/src/modules/builder/Decal/decalInstance/decalInstance.tsx @@ -0,0 +1,172 @@ +import * as THREE from 'three'; +import { Decal } from '@react-three/drei' +import { useToggleView, useToolMode } from '../../../../store/builder/store'; +import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; +import { retrieveImage, storeImage } from '../../../../utils/indexDB/idbUtils'; + +import defaultMaterial from '../../../../assets/image/fallback/fallback decal 1.png'; +import useModuleStore from '../../../../store/useModuleStore'; +import { useEffect, useRef, useState } from 'react'; + +import { useDecalEventHandlers } from '../eventHandler/useDecalEventHandlers'; + +// import { upsertWallApi } from '../../../../services/factoryBuilder/wall/upsertWallApi'; +// import { upsertFloorApi } from '../../../../services/factoryBuilder/floor/upsertFloorApi'; + +function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalPosition[2] }: { parent: Wall | Floor; visible?: boolean, decal: Decal, zPosition?: number }) { + const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + const { selectedDecal, deletableDecal, setSelectedDecal, setDeletableDecal } = useBuilderStore(); + const { toolMode } = useToolMode(); + const { toggleView } = useToggleView(); + const { activeModule } = useModuleStore(); + const decalRef = useRef(null); + + useEffect(() => { + if (selectedDecal?.decalData.decalUuid === decal.decalUuid && !selectedDecal.decalMesh) { + setSelectedDecal({ decalData: selectedDecal.decalData, decalMesh: decalRef.current }); + } + }, [selectedDecal]) + + const { handlePointerMissed, handlePointerLeave, handleClick, handlePointerDown, handlePointerEnter } = useDecalEventHandlers({ parent, decal, visible }); + + const [texture, setTexture] = useState(null); + + const logDecalStatus = (decalId: string, status: string) => { + // console.log(decalId, status); + } + + const loadDefaultTexture = () => { + const textureLoader = new THREE.TextureLoader(); + textureLoader.load( + defaultMaterial, + (fallbackTex) => { + fallbackTex.name = "default-decal"; + setTexture(fallbackTex); + logDecalStatus(decal.decalId, 'default-loaded'); + }, + undefined, + (error) => { + console.error("Error loading default decal texture:", error); + } + ); + }; + + const loadDecalTexture = async (decalId: string) => { + + try { + const cachedTexture = THREE.Cache.get(decalId); + if (cachedTexture) { + setTexture(cachedTexture); + logDecalStatus(decalId, 'cache-loaded'); + return; + } + + const indexedDBTexture = await retrieveImage(decalId); + if (indexedDBTexture) { + const blobUrl = URL.createObjectURL(indexedDBTexture); + const textureLoader = new THREE.TextureLoader(); + textureLoader.load( + blobUrl, + (tex) => { + URL.revokeObjectURL(blobUrl); + tex.name = decalId; + THREE.Cache.add(decalId, tex); + setTexture(tex); + logDecalStatus(decalId, 'indexedDB-loaded'); + }, + undefined, + (error) => { + console.error(`Error loading texture from IndexedDB:`, error); + URL.revokeObjectURL(blobUrl); + loadFromBackend(decalId); + } + ); + return; + } + + loadFromBackend(decalId); + } catch (error) { + console.error("Error loading decal texture:", error); + loadDefaultTexture(); + } + }; + + const loadFromBackend = (decalId: string) => { + + const textureUrl = `${url_Backend_dwinzo}/api/v1/DecalImage/${decalId}`; + const textureLoader = new THREE.TextureLoader(); + + textureLoader.load( + textureUrl, + async (tex) => { + tex.name = decalId; + THREE.Cache.add(decalId, tex); + setTexture(tex); + logDecalStatus(decalId, 'backend-loaded'); + + try { + const response = await fetch(textureUrl); + const blob = await response.blob(); + await storeImage(decalId, blob); + } catch (error) { + console.error("Error storing texture in IndexedDB:", error); + } + }, + undefined, + (error) => { + echo.error(`Error loading texture from backend: ${decal.decalName}`); + loadDefaultTexture(); + } + ); + }; + + useEffect(() => { + if (decal.decalId) { + loadDecalTexture(decal.decalId); + } else { + loadDefaultTexture(); + } + }, [decal.decalId]); + + useEffect(() => { + if (!toggleView && activeModule === 'builder') { + if (toolMode !== 'cursor') { + if (selectedDecal) setSelectedDecal(null); + } + if (toolMode !== '3D-Delete') { + if (deletableDecal) setDeletableDecal(null); + } + } else { + if (selectedDecal) setSelectedDecal(null); + if (deletableDecal) setDeletableDecal(null); + } + }, [toggleView, toolMode, activeModule, selectedDecal, deletableDecal]); + + return ( + { if (e.button === 0) handlePointerDown(e) }} + onClick={(e) => { handleClick(e) }} + onPointerEnter={(e) => { handlePointerEnter(e) }} + onPointerLeave={(e) => { handlePointerLeave(e) }} + onPointerMissed={() => handlePointerMissed()} + > + + + ) +} + +export default DecalInstance \ No newline at end of file diff --git a/app/src/modules/builder/Decal/eventHandler/useDecalEventHandlers.ts b/app/src/modules/builder/Decal/eventHandler/useDecalEventHandlers.ts new file mode 100644 index 0000000..c0c5161 --- /dev/null +++ b/app/src/modules/builder/Decal/eventHandler/useDecalEventHandlers.ts @@ -0,0 +1,301 @@ +import * as THREE from 'three'; +import { CameraControls } from '@react-three/drei'; +import { ThreeEvent, useFrame, useThree } from '@react-three/fiber'; +import { useEffect, useRef } from 'react'; +import { useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store'; +import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; +import useModuleStore from '../../../../store/useModuleStore'; +import { getUserData } from '../../../../functions/getUserData'; +import { useVersionContext } from '../../version/versionContext'; +import { useParams } from 'react-router-dom'; +import { useSceneContext } from '../../../scene/sceneContext'; + +// import { upsertWallApi } from '../../../../services/factoryBuilder/wall/upsertWallApi'; +// import { upsertFloorApi } from '../../../../services/factoryBuilder/floor/upsertFloorApi'; + +export function useDecalEventHandlers({ + parent, + decal, + visible, +}: { + parent: Wall | Floor; + decal: Decal; + visible: boolean; +}) { + const { wallStore, floorStore } = useSceneContext(); + const { removeDecal: removeDecalInWall, updateDecalPosition: updateDecalPositionInWall, getWallById, addDecal: addDecalToWall } = wallStore(); + const { removeDecal: removeDecalInFloor, updateDecalPosition: updateDecalPositionInFloor, getFloorById, addDecal: addDecalToFloor } = floorStore(); + const { setSelectedWall, setSelectedFloor, setSelectedDecal, setDeletableDecal, deletableDecal, selectedDecal, setDecalDragState, decalDragState } = useBuilderStore(); + const { toolMode } = useToolMode(); + const { toggleView } = useToggleView(); + const { activeModule } = useModuleStore(); + const { userId, organization } = getUserData(); + const { selectedVersionStore } = useVersionContext(); + const { selectedVersion } = selectedVersionStore(); + const { projectId } = useParams(); + const { socket } = useSocketStore(); + const { raycaster, pointer, camera, scene, gl, controls } = useThree(); + + useFrame(() => { + if (activeModule !== 'builder' || toggleView || !decalDragState.isDragging || !selectedDecal || selectedDecal.decalData.decalUuid !== decal.decalUuid) return; + + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(scene.children, true); + + const wallIntersect = intersects.find(i => i.object.userData?.wallUuid); + const floorIntersect = intersects.find(i => i.object.userData?.floorUuid); + + let offset = decalDragState.dragOffset || new THREE.Vector3(0, 0, 0); + + if (wallIntersect) { + const wallUuid = wallIntersect.object.userData.wallUuid; + const point = wallIntersect.object.worldToLocal(wallIntersect.point.clone()); + + if ("wallUuid" in parent && parent.wallUuid === wallUuid && decal.decalType.type === 'Wall') { + updateDecalPositionInWall(decal.decalUuid, [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]]); + } else if (decal.decalType.type === 'Wall' && wallUuid) { + deleteDecal(decal.decalUuid, parent); + + const addedDecal = addDecalToWall(wallUuid, { + ...decal, + decalPosition: [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]], + decalType: { type: 'Wall', wallUuid: wallUuid } + }); + + if (addedDecal) { + setSelectedDecal({ decalMesh: null, decalData: addedDecal }) + } + } else if (decal.decalType.type === 'Floor' && wallUuid) { + deleteDecal(decal.decalUuid, parent); + const wall = getWallById(wallUuid); + if (!wall) return; + + const addedDecal = addDecalToWall(wallUuid, { + ...decal, + decalPosition: [point.x + offset.x, point.y + offset.y, wall.wallThickness / 2 + 0.001], + decalType: { type: 'Wall', wallUuid: wallUuid } + }); + + if (addedDecal) { + setSelectedDecal({ decalMesh: null, decalData: addedDecal }) + } + } + } else if (floorIntersect) { + const floorUuid = floorIntersect.object.userData.floorUuid; + const point = floorIntersect.object.worldToLocal(floorIntersect.point.clone()); + + if ("floorUuid" in parent && parent.floorUuid === floorUuid && decal.decalType.type === 'Floor') { + updateDecalPositionInFloor(decal.decalUuid, [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]]); + } else if (decal.decalType.type === 'Floor' && floorUuid) { + deleteDecal(decal.decalUuid, parent); + + const addedDecal = addDecalToFloor(floorUuid, { + ...decal, + decalPosition: [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]], + decalType: { type: 'Floor', floorUuid: floorUuid } + }); + + if (addedDecal) { + setSelectedDecal({ decalMesh: null, decalData: addedDecal }) + } + } else if (decal.decalType.type === 'Wall' && floorUuid) { + deleteDecal(decal.decalUuid, parent); + const floor = getFloorById(floorUuid); + if (!floor) return; + + const addedDecal = addDecalToFloor(floorUuid, { + ...decal, + decalPosition: [point.x + offset.x, point.y + offset.y, -0.001], + decalType: { type: 'Floor', floorUuid: floorUuid } + }); + + if (addedDecal) { + setSelectedDecal({ decalMesh: null, decalData: addedDecal }) + } + } + } + }); + + const handlePointerUp = (e: PointerEvent) => { + if (controls) { + (controls as CameraControls).enabled = true; + } + if (decalDragState.isDragging) { + setDecalDragState(false, null, null); + + if ('wallUuid' in parent) { + setTimeout(() => { + const updatedWall = getWallById(parent.wallUuid); + if (updatedWall) { + if (projectId && updatedWall) { + // API + + // upsertWallApi(projectId, selectedVersion?.versionId || '', updatedWall); + + // SOCKET + + const data = { + wallData: updatedWall, + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization + } + + socket.emit('v1:model-Wall:add', data); + } + } + }, 0) + } else if ('floorUuid' in parent) { + setTimeout(() => { + const updatedFloor = parent; + + if (projectId && updatedFloor) { + // API + + // upsertFloorApi(projectId, selectedVersion?.versionId || '', updatedFloor); + + // SOCKET + + const data = { + floorData: updatedFloor, + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization + } + + socket.emit('v1:model-Floor:add', data); + } + }, 0) + } + } + }; + + const deleteDecal = (decalUuid: string, parent: Wall | Floor) => { + if ('wallUuid' in parent) { + const updatedWall = removeDecalInWall(decalUuid); + + if (projectId && updatedWall) { + // API + + // upsertWallApi(projectId, selectedVersion?.versionId || '', updatedWall); + + // SOCKET + + const data = { + wallData: updatedWall, + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization + } + + socket.emit('v1:model-Wall:add', data); + } + } else if ('floorUuid' in parent) { + const updatedFloor = removeDecalInFloor(decalUuid); + + if (projectId && updatedFloor) { + // API + + // upsertFloorApi(projectId, selectedVersion?.versionId || '', updatedFloor); + + // SOCKET + + const data = { + floorData: updatedFloor, + projectId: projectId, + versionId: selectedVersion?.versionId || '', + userId: userId, + organization: organization + } + + socket.emit('v1:model-Floor:add', data); + } + } + } + + const handlePointerDown = (e: ThreeEvent) => { + if (visible && !toggleView && activeModule === 'builder') { + if (e.object.userData.decalUuid && toolMode === 'cursor') { + e.stopPropagation(); + setDecalDragState(true, decal.decalUuid, null); + if (controls) { + (controls as CameraControls).enabled = false; + } + setSelectedDecal({ decalMesh: e.object, decalData: decal }); + setSelectedWall(null); + setSelectedFloor(null); + + const localIntersect = e.object.worldToLocal(e.point.clone()); + let dragOffset = new THREE.Vector3(decal.decalPosition[0] - localIntersect.x, decal.decalPosition[1] - localIntersect.y, 0); + setDecalDragState(true, decal.decalUuid, dragOffset); + } + } + }; + + const handleClick = (e: ThreeEvent) => { + if (visible && !toggleView && activeModule === 'builder') { + if (e.object.userData.decalUuid) { + e.stopPropagation(); + if (toolMode === 'cursor') { + setSelectedDecal({ decalMesh: e.object, decalData: decal }); + setSelectedWall(null); + setSelectedFloor(null); + } else if (toolMode === '3D-Delete') { + deleteDecal(e.object.userData.decalUuid, parent); + } + } + } + }; + + const handlePointerEnter = (e: ThreeEvent) => { + if (visible && !toggleView && activeModule === 'builder') { + if (e.object.userData.decalUuid) { + e.stopPropagation(); + if (toolMode === '3D-Delete') { + setDeletableDecal(e.object); + } + } + } + }; + + const handlePointerLeave = (e: ThreeEvent) => { + if (visible && !toggleView && activeModule === 'builder') { + if (e.object.userData.decalUuid) { + e.stopPropagation(); + if (toolMode === '3D-Delete' && deletableDecal && deletableDecal?.userData.decalUuid === e.object.userData.decalUuid) { + setDeletableDecal(null); + } + } + } + }; + + const handlePointerMissed = () => { + if (selectedDecal && selectedDecal.decalMesh && selectedDecal.decalMesh.userData.decalUuid === decal.decalUuid) { + setSelectedDecal(null); + } + }; + + useEffect(() => { + const canvasElement = gl.domElement; + + if (activeModule === 'builder' && !toggleView && selectedDecal && selectedDecal.decalData.decalUuid === decal.decalUuid) { + canvasElement.addEventListener('pointerup', handlePointerUp); + } + + return () => { + canvasElement.removeEventListener('pointerup', handlePointerUp); + }; + }, [gl, activeModule, toggleView, selectedDecal, camera, controls, visible, parent, decal, decalDragState]); + + return { + handlePointerDown, + handleClick, + handlePointerEnter, + handlePointerLeave, + handlePointerMissed, + deleteDecal + }; +} \ No newline at end of file diff --git a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arcAisle.tsx b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arcAisle.tsx index bc09b68..d791f8f 100644 --- a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arcAisle.tsx +++ b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arcAisle.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { Extrude } from '@react-three/drei'; import * as Constants from '../../../../../../types/world/worldConstants'; import { useToolMode } from '../../../../../../store/builder/store'; @@ -8,7 +8,13 @@ import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore function ArcAisle({ aisle }: { readonly aisle: Aisle }) { const aisleRef = useRef(null); const { toolMode } = useToolMode(); - const { setSelectedAisle, hoveredPoint } = useBuilderStore(); + const { setSelectedAisle, hoveredPoint, selectedAisle } = useBuilderStore(); + + useEffect(() => { + if (selectedAisle?.aisleData.aisleUuid === aisle.aisleUuid && !selectedAisle.aisleMesh) { + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: aisleRef.current }); + } + }, [selectedAisle]) const arc = useMemo(() => { if (aisle.points.length < 2 || aisle.type.aisleType !== 'arc-aisle') return null; @@ -63,8 +69,8 @@ function ArcAisle({ aisle }: { readonly aisle: Aisle }) { }, [aisle]); const handleClick = () => { - if (toolMode === 'move' && !hoveredPoint) { - setSelectedAisle(aisleRef.current); + if ((toolMode === 'move' || toolMode === 'cursor') && !hoveredPoint) { + setSelectedAisle({ aisleMesh: aisleRef.current, aisleData: aisle }); } } diff --git a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arrowAisle.tsx b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arrowAisle.tsx index 49bf666..115edc6 100644 --- a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arrowAisle.tsx +++ b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arrowAisle.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { Extrude } from '@react-three/drei'; import * as Constants from '../../../../../../types/world/worldConstants'; import { useToolMode } from '../../../../../../store/builder/store'; @@ -8,7 +8,13 @@ import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore function ArrowAisle({ aisle }: { readonly aisle: Aisle }) { const aisleRef = useRef(null); const { toolMode } = useToolMode(); - const { setSelectedAisle, hoveredPoint } = useBuilderStore(); + const { setSelectedAisle, hoveredPoint, selectedAisle } = useBuilderStore(); + + useEffect(() => { + if (selectedAisle?.aisleData.aisleUuid === aisle.aisleUuid && !selectedAisle.aisleMesh) { + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: aisleRef.current }); + } + }, [selectedAisle]) const arrow = useMemo(() => { if (aisle.points.length < 2 || aisle.type.aisleType !== 'arrow-aisle') return null; @@ -50,8 +56,8 @@ function ArrowAisle({ aisle }: { readonly aisle: Aisle }) { }, [aisle]); const handleClick = () => { - if (toolMode === 'move' && !hoveredPoint) { - setSelectedAisle(aisleRef.current); + if ((toolMode === 'move' || toolMode === 'cursor') && !hoveredPoint) { + setSelectedAisle({ aisleMesh: aisleRef.current, aisleData: aisle }); } } diff --git a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arrowsAisle.tsx b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arrowsAisle.tsx index 6908cf3..3700d55 100644 --- a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arrowsAisle.tsx +++ b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/arrowsAisle.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { Instances, Instance } from '@react-three/drei'; import * as Constants from '../../../../../../types/world/worldConstants'; import { useToolMode } from '../../../../../../store/builder/store'; @@ -8,7 +8,13 @@ import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore function ArrowsAisle({ aisle }: { readonly aisle: Aisle }) { const aisleRef = useRef(null); const { toolMode } = useToolMode(); - const { setSelectedAisle, hoveredPoint } = useBuilderStore(); + const { setSelectedAisle, hoveredPoint, selectedAisle } = useBuilderStore(); + + useEffect(() => { + if (selectedAisle?.aisleData.aisleUuid === aisle.aisleUuid && !selectedAisle.aisleMesh) { + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: aisleRef.current }); + } + }, [selectedAisle]) const { arrowGeometry, arrowInstances } = useMemo(() => { const result = { @@ -68,8 +74,8 @@ function ArrowsAisle({ aisle }: { readonly aisle: Aisle }) { }, [aisle]); const handleClick = () => { - if (toolMode === 'move' && !hoveredPoint) { - setSelectedAisle(aisleRef.current); + if ((toolMode === 'move' || toolMode === 'cursor') && !hoveredPoint) { + setSelectedAisle({ aisleMesh: aisleRef.current, aisleData: aisle }); } }; diff --git a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/circleAisle.tsx b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/circleAisle.tsx index af852e8..d9bc2d2 100644 --- a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/circleAisle.tsx +++ b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/circleAisle.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { Extrude } from '@react-three/drei'; import * as Constants from '../../../../../../types/world/worldConstants'; import { useToolMode } from '../../../../../../store/builder/store'; @@ -8,7 +8,13 @@ import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore function CircleAisle({ aisle }: { readonly aisle: Aisle }) { const aisleRef = useRef(null); const { toolMode } = useToolMode(); - const { setSelectedAisle, hoveredPoint } = useBuilderStore(); + const { setSelectedAisle, hoveredPoint, selectedAisle } = useBuilderStore(); + + useEffect(() => { + if (selectedAisle?.aisleData.aisleUuid === aisle.aisleUuid && !selectedAisle.aisleMesh) { + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: aisleRef.current }); + } + }, [selectedAisle]) const circle = useMemo(() => { if (aisle.points.length < 2 || aisle.type.aisleType !== 'circle-aisle') return null; @@ -38,8 +44,8 @@ function CircleAisle({ aisle }: { readonly aisle: Aisle }) { }, [aisle]); const handleClick = () => { - if (toolMode === 'move' && !hoveredPoint) { - setSelectedAisle(aisleRef.current); + if ((toolMode === 'move' || toolMode === 'cursor') && !hoveredPoint) { + setSelectedAisle({ aisleMesh: aisleRef.current, aisleData: aisle }); } } diff --git a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/dashedAisle.tsx b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/dashedAisle.tsx index 72fb3c7..b00c87b 100644 --- a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/dashedAisle.tsx +++ b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/dashedAisle.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { Instances, Instance } from '@react-three/drei'; import * as Constants from '../../../../../../types/world/worldConstants'; import { useToolMode } from '../../../../../../store/builder/store'; @@ -8,7 +8,13 @@ import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore function DashedAisle({ aisle }: { readonly aisle: Aisle }) { const aisleRef = useRef(null); const { toolMode } = useToolMode(); - const { setSelectedAisle, hoveredPoint } = useBuilderStore(); + const { setSelectedAisle, hoveredPoint, selectedAisle } = useBuilderStore(); + + useEffect(() => { + if (selectedAisle?.aisleData.aisleUuid === aisle.aisleUuid && !selectedAisle.aisleMesh) { + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: aisleRef.current }); + } + }, [selectedAisle]) const dashInstances = useMemo(() => { if (aisle.points.length < 2 || aisle.type.aisleType !== 'dashed-aisle') return []; @@ -43,8 +49,8 @@ function DashedAisle({ aisle }: { readonly aisle: Aisle }) { }, [aisle]); const handleClick = () => { - if (toolMode === 'move' && !hoveredPoint) { - setSelectedAisle(aisleRef.current); + if ((toolMode === 'move' || toolMode === 'cursor') && !hoveredPoint) { + setSelectedAisle({ aisleMesh: aisleRef.current, aisleData: aisle }); } }; diff --git a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/dottedAisle.tsx b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/dottedAisle.tsx index e2b264a..09557aa 100644 --- a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/dottedAisle.tsx +++ b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/dottedAisle.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { Instance, Instances } from '@react-three/drei'; import * as Constants from '../../../../../../types/world/worldConstants'; import { useToolMode } from '../../../../../../store/builder/store'; @@ -8,7 +8,13 @@ import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore function DottedAisle({ aisle }: { readonly aisle: Aisle }) { const aisleRef = useRef(null); const { toolMode } = useToolMode(); - const { setSelectedAisle, hoveredPoint } = useBuilderStore(); + const { setSelectedAisle, hoveredPoint, selectedAisle } = useBuilderStore(); + + useEffect(() => { + if (selectedAisle?.aisleData.aisleUuid === aisle.aisleUuid && !selectedAisle.aisleMesh) { + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: aisleRef.current }); + } + }, [selectedAisle]) const dotPositions = useMemo(() => { if (aisle.points.length < 2 || aisle.type.aisleType !== 'dotted-aisle') return []; @@ -27,8 +33,8 @@ function DottedAisle({ aisle }: { readonly aisle: Aisle }) { }, [aisle]); const handleClick = () => { - if (toolMode === 'move' && !hoveredPoint) { - setSelectedAisle(aisleRef.current); + if ((toolMode === 'move' || toolMode === 'cursor') && !hoveredPoint) { + setSelectedAisle({ aisleMesh: aisleRef.current, aisleData: aisle }); } } diff --git a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/junctionAisle.tsx b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/junctionAisle.tsx index a3f8bdf..8d45763 100644 --- a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/junctionAisle.tsx +++ b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/junctionAisle.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { Extrude } from '@react-three/drei'; import * as Constants from '../../../../../../types/world/worldConstants'; import { useToolMode } from '../../../../../../store/builder/store'; @@ -8,7 +8,13 @@ import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore function JunctionAisle({ aisle }: { readonly aisle: Aisle }) { const aisleRef = useRef(null); const { toolMode } = useToolMode(); - const { setSelectedAisle, hoveredPoint } = useBuilderStore(); + const { setSelectedAisle, hoveredPoint, selectedAisle } = useBuilderStore(); + + useEffect(() => { + if (selectedAisle?.aisleData.aisleUuid === aisle.aisleUuid && !selectedAisle.aisleMesh) { + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: aisleRef.current }); + } + }, [selectedAisle]) const arrows = useMemo(() => { if (aisle.points.length < 2 || aisle.type.aisleType !== 'junction-aisle') return null; @@ -85,8 +91,8 @@ function JunctionAisle({ aisle }: { readonly aisle: Aisle }) { }, [aisle]); const handleClick = () => { - if (toolMode === 'move' && !hoveredPoint) { - setSelectedAisle(aisleRef.current); + if ((toolMode === 'move' || toolMode === 'cursor') && !hoveredPoint) { + setSelectedAisle({ aisleMesh: aisleRef.current, aisleData: aisle }); } } diff --git a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/solidAisle.tsx b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/solidAisle.tsx index e9321b0..e43f717 100644 --- a/app/src/modules/builder/aisle/Instances/instance/aisleTypes/solidAisle.tsx +++ b/app/src/modules/builder/aisle/Instances/instance/aisleTypes/solidAisle.tsx @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { Extrude } from '@react-three/drei'; import * as Constants from '../../../../../../types/world/worldConstants'; import { useToolMode } from '../../../../../../store/builder/store'; @@ -8,7 +8,14 @@ import { useBuilderStore } from '../../../../../../store/builder/useBuilderStore function SolidAisle({ aisle }: { readonly aisle: Aisle }) { const aisleRef = useRef(null); const { toolMode } = useToolMode(); - const { setSelectedAisle, hoveredPoint } = useBuilderStore(); + const { setSelectedAisle, hoveredPoint, selectedAisle } = useBuilderStore(); + + useEffect(() => { + if (selectedAisle?.aisleData.aisleUuid === aisle.aisleUuid && !selectedAisle.aisleMesh) { + setSelectedAisle({ aisleData: selectedAisle.aisleData, aisleMesh: aisleRef.current }); + } + }, [selectedAisle]) + const shape = useMemo(() => { if (aisle.points.length < 2 || aisle.type.aisleType !== 'solid-aisle') return null; @@ -35,8 +42,8 @@ function SolidAisle({ aisle }: { readonly aisle: Aisle }) { }, [aisle]); const handleClick = () => { - if (toolMode === 'move' && !hoveredPoint) { - setSelectedAisle(aisleRef.current); + if ((toolMode === 'move' || toolMode === 'cursor') && !hoveredPoint) { + setSelectedAisle({ aisleMesh: aisleRef.current, aisleData: aisle }); } } diff --git a/app/src/modules/builder/asset/assetsGroup.tsx b/app/src/modules/builder/asset/assetsGroup.tsx index 9d36efc..0205a4c 100644 --- a/app/src/modules/builder/asset/assetsGroup.tsx +++ b/app/src/modules/builder/asset/assetsGroup.tsx @@ -363,6 +363,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) { setLeft(relativeX); } }; + const onMouseUp = (evt: any) => { setIsRenameMode(false); } diff --git a/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts b/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts index 360c513..85b5b4b 100644 --- a/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts +++ b/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts @@ -34,7 +34,7 @@ export function useModelEventHandlers({ const { socket } = useSocketStore(); const { eventStore, productStore, assetStore, undoRedo3DStore } = useSceneContext(); const { push3D } = undoRedo3DStore(); - const { getAssetById, removeAsset } = assetStore(); + const { removeAsset } = assetStore(); const { zoneAssetId, setZoneAssetId } = useZoneAssetId(); const { resourceManagementId, setResourceManagementId } = useResourceManagementId(); const { removeEvent, getEventByModelUuid } = eventStore(); @@ -77,11 +77,12 @@ export function useModelEventHandlers({ } }, [zoneAssetId]) + useEffect(() => { if (!resourceManagementId) return if (resourceManagementId === asset.modelUuid) { - - + + handleDblClick(asset); } diff --git a/app/src/modules/builder/asset/models/model/model.tsx b/app/src/modules/builder/asset/models/model/model.tsx index ecce40e..a7502db 100644 --- a/app/src/modules/builder/asset/models/model/model.tsx +++ b/app/src/modules/builder/asset/models/model/model.tsx @@ -89,6 +89,10 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere } }, [gltfScene]); + const logModelStatus = (modelId: string, status: string) => { + // console.log(modelId, status); + } + useEffect(() => { // Calculate Bounding Box const calculateBoundingBox = (scene: THREE.Object3D) => { @@ -104,6 +108,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere clone.animations = cachedModel.animations || []; setGltfScene(clone); calculateBoundingBox(clone); + logModelStatus(assetId, 'cache-loaded'); return; } @@ -119,6 +124,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere THREE.Cache.add(assetId, gltf); setGltfScene(gltf.scene.clone()); calculateBoundingBox(gltf.scene); + logModelStatus(assetId, 'indexedDB-loaded'); }, undefined, (error) => { @@ -141,6 +147,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere THREE.Cache.add(assetId, gltf); setGltfScene(gltf.scene.clone()); calculateBoundingBox(gltf.scene); + logModelStatus(assetId, 'backend-loaded'); }) .catch((error) => { console.error( diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index ef310fc..d7b8d34 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -33,13 +33,14 @@ import AssetsGroup from "./asset/assetsGroup"; import DxfFile from "./dfx/LoadBlueprint"; import AislesGroup from "./aisle/aislesGroup"; import WallGroup from "./wall/wallGroup"; +import WallAssetGroup from "./wallAsset/wallAssetGroup"; import FloorGroup from "./floor/floorGroup"; import ZoneGroup from "./zone/zoneGroup"; +import Decal from "./Decal/decal"; import { useParams } from "react-router-dom"; import { useBuilderStore } from "../../store/builder/useBuilderStore"; import { getUserData } from "../../functions/getUserData"; -import WallAssetGroup from "./wallAsset/wallAssetGroup"; export default function Builder() { const state = useThree(); @@ -106,6 +107,8 @@ export default function Builder() { + + diff --git a/app/src/modules/builder/floor/Instances/Instance/floorInstance.tsx b/app/src/modules/builder/floor/Instances/Instance/floorInstance.tsx index aa60691..4ea5b43 100644 --- a/app/src/modules/builder/floor/Instances/Instance/floorInstance.tsx +++ b/app/src/modules/builder/floor/Instances/Instance/floorInstance.tsx @@ -1,12 +1,13 @@ import { useMemo } from "react"; -import { Shape, Vector2, DoubleSide, TextureLoader, RepeatWrapping, SRGBColorSpace, NoColorSpace, } from "three"; +import { Shape, Vector2, DoubleSide, TextureLoader, RepeatWrapping, SRGBColorSpace, NoColorSpace, ExtrudeGeometry, Vector3, Euler, } from "three"; import { useLoader } from "@react-three/fiber"; -import { Extrude } from "@react-three/drei"; import useModuleStore from "../../../../../store/useModuleStore"; import { useBuilderStore } from "../../../../../store/builder/useBuilderStore"; import { useToggleView } from "../../../../../store/builder/store"; import * as Constants from "../../../../../types/world/worldConstants"; +import DecalInstance from "../../../Decal/decalInstance/decalInstance"; + import texturePath from "../../../../../assets/textures/floor/white.png"; import texturePathDark from "../../../../../assets/textures/floor/black.png"; import material1 from "../../../../../assets/textures/floor/factory wall texture.jpg"; @@ -28,7 +29,7 @@ import material4MetalicMap from "../../../../../assets/textures/floor/tex3/metal import material4NormalMap from "../../../../../assets/textures/floor/tex3/metal_plate_nor_gl_1k.png"; function FloorInstance({ floor }: { floor: Floor }) { - const { togglView } = useToggleView(); + const { toggleView } = useToggleView(); const { activeModule } = useModuleStore(); const { selectedFloor, setSelectedFloor, setSelectedDecal } = useBuilderStore(); const savedTheme = localStorage.getItem("theme"); @@ -67,20 +68,26 @@ function FloorInstance({ floor }: { floor: Floor }) { }, }; - const shape = useMemo(() => { - const shape = new Shape(); + const shapeData = useMemo(() => { const points = floor.points.map((p) => new Vector2(p.position[0], p.position[2])); if (points.length < 3) return null; - shape.moveTo(points[0].x, points[0].y); - for (let i = 1; i < points.length; i++) { - shape.lineTo(points[i].x, points[i].y); + + const centroidX = points.reduce((sum, p) => sum + p.x, 0) / points.length; + const centroidY = points.reduce((sum, p) => sum + p.y, 0) / points.length; + + const relativePoints = points.map((p) => new Vector2(p.x - centroidX, p.y - centroidY)); + + const shape = new Shape(); + shape.moveTo(relativePoints[0].x, relativePoints[0].y); + for (let i = 1; i < relativePoints.length; i++) { + shape.lineTo(relativePoints[i].x, relativePoints[i].y); } - return shape; + + return { shape, center: [centroidX, centroidY] }; }, [floor]); const textureScale = Constants.floorConfig.textureScale; - // Helper function to handle texture maps and filter out null values function getMaterialMaps(material: any, defaultMap: any) { const materialMap = material.map || defaultMap; const normalMap = material.normalMap || null; @@ -90,26 +97,18 @@ function FloorInstance({ floor }: { floor: Floor }) { return [materialMap, normalMap, roughnessMap, metalnessMap].filter((texture): texture is string => texture !== null); } - // Default material map const defaultMaterialMap = materials["Default Material"].map; - // Get top and side material maps const topMaterial = materials[floor.topMaterial]; const sideMaterial = materials[floor.sideMaterial]; - // Get the filtered lists for top and side textures const topTexturesList = getMaterialMaps(topMaterial, defaultMaterialMap); const sideTexturesList = getMaterialMaps(sideMaterial, defaultMaterialMap); - // Use loader to load top and side textures const [topTexture, topNormalTexture, topRoughnessTexture, topMetalicTexture] = useLoader(TextureLoader, topTexturesList); const [sideTexture, sideNormalTexture, sideRoughnessTexture, sideMetalicTexture] = useLoader(TextureLoader, sideTexturesList); - // Early exit if materials are missing - if (!materials[floor.topMaterial] || !materials[floor.sideMaterial]) return null; - - // Combine and pair textures with their corresponding material const textureMaterialMap = [ { textures: [ @@ -131,7 +130,6 @@ function FloorInstance({ floor }: { floor: Floor }) { }, ]; - // Apply texture settings textureMaterialMap.forEach(({ textures, materialKey }) => { const tileScale = materials[materialKey]?.textureTileScale ?? [ textureScale, @@ -143,23 +141,39 @@ function FloorInstance({ floor }: { floor: Floor }) { tex.wrapS = tex.wrapT = RepeatWrapping; tex.repeat.set(tileScale[0], tileScale[1]); tex.anisotropy = 16; - // First texture is always the color map (use SRGB), others should be linear tex.colorSpace = idx < 1 ? SRGBColorSpace : NoColorSpace; }); }); - if (!shape) return null; + const geometry = useMemo(() => { + if (!shapeData) return null; + return new ExtrudeGeometry(shapeData.shape, { + depth: !floor.isBeveled ? floor.floorDepth : floor.floorDepth - 0.1, + bevelEnabled: floor.isBeveled, + bevelSegments: floor.bevelStrength, + bevelOffset: -0.1, + bevelSize: 0.1, + bevelThickness: 0.1, + }); + }, [shapeData, floor]); + + if (!geometry) return null; return ( { - if (!togglView && activeModule === "builder") { + if (!toggleView && activeModule === "builder") { if (e.object.userData.floorUuid) { e.stopPropagation(); setSelectedFloor(e.object); @@ -173,41 +187,32 @@ function FloorInstance({ floor }: { floor: Floor }) { } }} > - - - - + + + + {floor.decals.map((decal) => ( + + ))} ); } diff --git a/app/src/modules/builder/floor/Instances/floorInstances.tsx b/app/src/modules/builder/floor/Instances/floorInstances.tsx index a578800..2af5787 100644 --- a/app/src/modules/builder/floor/Instances/floorInstances.tsx +++ b/app/src/modules/builder/floor/Instances/floorInstances.tsx @@ -2,21 +2,36 @@ import React, { useEffect, useMemo } from 'react'; import { Vector3 } from 'three'; import { Html } from '@react-three/drei'; import { useSceneContext } from '../../../scene/sceneContext'; -import { useToggleView } from '../../../../store/builder/store'; +import { useToggleView, useToolMode } from '../../../../store/builder/store'; import Line from '../../line/line'; import Point from '../../point/point'; import FloorInstance from './Instance/floorInstance'; import Floor2DInstance from './Instance/floor2DInstance'; +import useModuleStore from '../../../../store/useModuleStore'; +import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; function FloorInstances() { const { floorStore } = useSceneContext(); const { floors } = floorStore(); + const { setSelectedFloor, selectedFloor } = useBuilderStore(); + const { toolMode } = useToolMode(); const { toggleView } = useToggleView(); + const { activeModule } = useModuleStore(); useEffect(() => { // console.log('floors: ', floors); }, [floors]) + useEffect(() => { + if (!toggleView && activeModule === 'builder') { + if (toolMode !== 'cursor') { + if (selectedFloor) setSelectedFloor(null); + } + } else { + if (selectedFloor) setSelectedFloor(null); + } + }, [toggleView, toolMode, activeModule, selectedFloor]); + const allPoints = useMemo(() => { const points: Point[] = []; const seenUuids = new Set(); diff --git a/app/src/modules/builder/floor/floorGroup.tsx b/app/src/modules/builder/floor/floorGroup.tsx index 47ea3b5..d782e1a 100644 --- a/app/src/modules/builder/floor/floorGroup.tsx +++ b/app/src/modules/builder/floor/floorGroup.tsx @@ -10,7 +10,7 @@ import FloorInstances from './Instances/floorInstances'; import { getFloorsApi } from '../../../services/factoryBuilder/floor/getFloorsApi'; function FloorGroup() { - const { togglView } = useToggleView(); + const { toggleView } = useToggleView(); const { setSelectedFloor, setSelectedDecal } = useBuilderStore(); const { activeModule } = useModuleStore(); const { activeTool } = useActiveTool(); @@ -21,11 +21,11 @@ function FloorGroup() { const { projectId } = useParams(); useEffect(() => { - if (togglView || activeModule !== 'builder') { + if (toggleView || activeModule !== 'builder') { setSelectedFloor(null); setSelectedDecal(null); } - }, [togglView, activeModule, activeTool]) + }, [toggleView, activeModule, activeTool]) useEffect(() => { if (projectId && selectedVersion) { diff --git a/app/src/modules/builder/wall/Instances/instance/wall.tsx b/app/src/modules/builder/wall/Instances/instance/wall.tsx index 9d68d73..67d5b8a 100644 --- a/app/src/modules/builder/wall/Instances/instance/wall.tsx +++ b/app/src/modules/builder/wall/Instances/instance/wall.tsx @@ -10,7 +10,7 @@ import { useToggleView, useWallVisibility } from '../../../../../store/builder/s import { useBuilderStore } from '../../../../../store/builder/useBuilderStore'; import * as Constants from '../../../../../types/world/worldConstants'; -import DecalInstance from '../../../Decal/decalInstance'; +import DecalInstance from '../../../Decal/decalInstance/decalInstance'; import defaultMaterial from '../../../../../assets/textures/floor/wall-tex.png'; import material1 from '../../../../../assets/textures/floor/factory wall texture.jpg'; @@ -21,7 +21,7 @@ function Wall({ wall }: { readonly wall: Wall }) { const { wallAssets, getWallAssetsByWall, setVisibility } = wallAssetStore(); const assets = getWallAssetsByWall(wall.wallUuid); const { selectedWall, setSelectedWall, setSelectedDecal } = useBuilderStore(); - const { togglView } = useToggleView(); + const { toggleView } = useToggleView(); const { activeModule } = useModuleStore(); const { camera } = useThree(); const { wallVisibility } = useWallVisibility(); @@ -118,10 +118,11 @@ function Wall({ wall }: { readonly wall: Wall }) { > {(assets.length > 0 || (walls[0].wallUuid === wall.wallUuid && wallAssets.length > 0)) ? { - if (visible && !togglView && activeModule === 'builder') { + if (visible && !toggleView && activeModule === 'builder') { if (e.object.userData.wallUuid) { e.stopPropagation(); setSelectedWall(e.object); @@ -171,7 +172,7 @@ function Wall({ wall }: { readonly wall: Wall }) { {wall.decals.map((decal) => ( - + ))} diff --git a/app/src/modules/builder/wall/Instances/wallInstances.tsx b/app/src/modules/builder/wall/Instances/wallInstances.tsx index 254705a..a67e3a3 100644 --- a/app/src/modules/builder/wall/Instances/wallInstances.tsx +++ b/app/src/modules/builder/wall/Instances/wallInstances.tsx @@ -2,8 +2,9 @@ import React, { useEffect, useMemo } from 'react'; import { DoubleSide, RepeatWrapping, Shape, SRGBColorSpace, TextureLoader, Vector2, Vector3 } from 'three'; import { Html, Extrude } from '@react-three/drei'; import { useLoader } from '@react-three/fiber'; +import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; import { useSceneContext } from '../../../scene/sceneContext'; -import { useToggleView } from '../../../../store/builder/store'; +import { useToggleView, useToolMode } from '../../../../store/builder/store'; import { useWallClassification } from './instance/helpers/useWallClassification'; import Line from '../../line/line'; import Point from '../../point/point'; @@ -12,17 +13,31 @@ import * as Constants from '../../../../types/world/worldConstants'; import texturePath from "../../../../assets/textures/floor/white.png"; import texturePathDark from "../../../../assets/textures/floor/black.png"; +import useModuleStore from '../../../../store/useModuleStore'; function WallInstances() { const { wallStore } = useSceneContext(); + const { setSelectedWall, selectedWall } = useBuilderStore(); + const { toolMode } = useToolMode(); + const { toggleView } = useToggleView(); + const { activeModule } = useModuleStore(); const { walls } = wallStore(); const { rooms } = useWallClassification(walls); - const { toggleView } = useToggleView(); useEffect(() => { // console.log('walls: ', walls); }, [walls]) + useEffect(() => { + if (!toggleView && activeModule === 'builder') { + if (toolMode !== 'cursor') { + if (selectedWall) setSelectedWall(null); + } + } else { + if (selectedWall) setSelectedWall(null); + } + }, [toggleView, toolMode, activeModule, selectedWall]); + const allPoints = useMemo(() => { const points: Point[] = []; const seenUuids = new Set(); diff --git a/app/src/modules/builder/wall/wallGroup.tsx b/app/src/modules/builder/wall/wallGroup.tsx index 084b969..3561751 100644 --- a/app/src/modules/builder/wall/wallGroup.tsx +++ b/app/src/modules/builder/wall/wallGroup.tsx @@ -11,7 +11,7 @@ import WallInstances from './Instances/wallInstances'; import { getWallsApi } from '../../../services/factoryBuilder/wall/getWallsApi'; function WallGroup() { - const { togglView } = useToggleView(); + const { toggleView } = useToggleView(); const { setSelectedWall, setSelectedDecal } = useBuilderStore(); const { activeModule } = useModuleStore(); const { activeTool } = useActiveTool(); @@ -22,11 +22,11 @@ function WallGroup() { const { projectId } = useParams(); useEffect(() => { - if (togglView || activeModule !== 'builder') { + if (toggleView || activeModule !== 'builder') { setSelectedWall(null); setSelectedDecal(null); } - }, [togglView, activeModule, activeTool]) + }, [toggleView, activeModule, activeTool]) useEffect(() => { if (projectId && selectedVersion) { diff --git a/app/src/modules/builder/wallAsset/Instances/Instance/functions/calculateAssetTransformationOnWall.ts b/app/src/modules/builder/wallAsset/Instances/Instance/functions/calculateAssetTransformationOnWall.ts index aa881c8..89142d5 100644 --- a/app/src/modules/builder/wallAsset/Instances/Instance/functions/calculateAssetTransformationOnWall.ts +++ b/app/src/modules/builder/wallAsset/Instances/Instance/functions/calculateAssetTransformationOnWall.ts @@ -18,10 +18,10 @@ const calculateAssetTransformationOnWall = ( const projection = initialWallNormalized.clone().multiplyScalar(dotProduct); const perpendicular = new THREE.Vector3().subVectors(assetVector, projection); - const distanceFromWall = perpendicular.length(); + const distanceInWall = perpendicular.length(); const crossProduct = new THREE.Vector3().crossVectors(initialWallNormalized, perpendicular).y; - const signedDistance = distanceFromWall * (crossProduct >= 0 ? 1 : -1); + const signedDistance = distanceInWall * (crossProduct >= 0 ? 1 : -1); const percentage = Math.max(0, Math.min(1, dotProduct / initialWallLength)); diff --git a/app/src/modules/builder/wallAsset/Instances/Instance/wallAssetInstance.tsx b/app/src/modules/builder/wallAsset/Instances/Instance/wallAssetInstance.tsx index 76b3ad7..3919f65 100644 --- a/app/src/modules/builder/wallAsset/Instances/Instance/wallAssetInstance.tsx +++ b/app/src/modules/builder/wallAsset/Instances/Instance/wallAssetInstance.tsx @@ -23,7 +23,7 @@ function WallAssetInstance({ wallAsset }: { wallAsset: WallAsset }) { const { raycaster, pointer, camera, scene, controls, gl } = useThree(); const { wallStore, wallAssetStore } = useSceneContext(); const { walls, getWallById } = wallStore(); - const { updateWallAsset, removeWallAsset } = wallAssetStore(); + const { updateWallAsset, removeWallAsset, getWallAssetById } = wallAssetStore(); const { toggleView } = useToggleView(); const { activeTool } = useActiveTool(); const { activeModule } = useModuleStore(); @@ -116,33 +116,14 @@ function WallAssetInstance({ wallAsset }: { wallAsset: WallAsset }) { const canvasElement = gl.domElement; const onPointerUp = (e: PointerEvent) => { - draggingRef.current = false; - if (controls) { - (controls as any).enabled = true; - } + if (draggingRef.current) { + draggingRef.current = false; + if (controls) { + (controls as any).enabled = true; + } - if (selectedWallAsset) { - pointer.x = (e.clientX / window.innerWidth) * 2 - 1; - pointer.y = -(e.clientY / window.innerHeight) * 2 + 1; - - raycaster.setFromCamera(pointer, camera); - const intersects = raycaster.intersectObjects(scene.children, true); - const intersect = intersects.find((i: any) => i.object.name.includes('WallReference')); - - if (intersect && intersect.object.userData.wallUuid && selectedWallAsset.userData.modelUuid === wallAsset.modelUuid) { - const newPoint = closestPointOnLineSegment( - new THREE.Vector3(intersect.point.x, 0, intersect.point.z), - new THREE.Vector3(...intersect.object.userData.points[0].position), - new THREE.Vector3(...intersect.object.userData.points[1].position) - ); - - const wallRotation = intersect.object.rotation.clone(); - - const updatedWallAsset = updateWallAsset(wallAsset.modelUuid, { - wallUuid: intersect.object.userData.wallUuid, - position: [newPoint.x, wallAsset.wallAssetType === 'fixedMove' ? 0 : intersect.point.y, newPoint.z], - rotation: [wallRotation.x, wallRotation.y, wallRotation.z], - }); + if (selectedWallAsset) { + const updatedWallAsset = getWallAssetById(wallAsset.modelUuid); if (projectId && updatedWallAsset) { diff --git a/app/src/modules/builder/wallAsset/wallAssetCreator.tsx b/app/src/modules/builder/wallAsset/wallAssetCreator.tsx index fb95236..13438c2 100644 --- a/app/src/modules/builder/wallAsset/wallAssetCreator.tsx +++ b/app/src/modules/builder/wallAsset/wallAssetCreator.tsx @@ -14,7 +14,7 @@ import closestPointOnLineSegment from '../line/helpers/getClosestPointOnLineSegm function WallAssetCreator() { const { socket } = useSocketStore(); const { pointer, camera, raycaster, scene, gl } = useThree(); - const { togglView } = useToggleView(); + const { toggleView } = useToggleView(); const { activeModule } = useModuleStore(); const { wallAssetStore } = useSceneContext(); const { addWallAsset } = wallAssetStore(); @@ -84,7 +84,7 @@ function WallAssetCreator() { } }; - if (!togglView && activeModule === 'builder') { + if (!toggleView && activeModule === 'builder') { canvasElement.addEventListener('drop', onDrop); } @@ -92,7 +92,7 @@ function WallAssetCreator() { canvasElement.removeEventListener('drop', onDrop); }; - }, [gl, camera, togglView, activeModule, socket, selectedItem, setSelectedItem]); + }, [gl, camera, toggleView, activeModule, socket, selectedItem, setSelectedItem]); return ( <> diff --git a/app/src/modules/builder/wallAsset/wallAssetGroup.tsx b/app/src/modules/builder/wallAsset/wallAssetGroup.tsx index bae9f87..95e2e90 100644 --- a/app/src/modules/builder/wallAsset/wallAssetGroup.tsx +++ b/app/src/modules/builder/wallAsset/wallAssetGroup.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useActiveTool, useToggleView } from '../../../store/builder/store'; +import { useToggleView, useToolMode } from '../../../store/builder/store'; import { useBuilderStore } from '../../../store/builder/useBuilderStore'; import { useVersionContext } from '../version/versionContext'; import { useSceneContext } from '../../scene/sceneContext'; @@ -10,10 +10,10 @@ import WallAssetInstances from './Instances/wallAssetInstances' import { getWallAssetsApi } from '../../../services/factoryBuilder/asset/wallAsset/getWallAssetsApi'; function WallAssetGroup() { - const { togglView } = useToggleView(); - const { setSelectedFloorAsset, setDeletableWallAsset } = useBuilderStore(); + const { toggleView } = useToggleView(); + const { setSelectedWallAsset, setDeletableWallAsset } = useBuilderStore(); const { activeModule } = useModuleStore(); - const { activeTool } = useActiveTool(); + const { toolMode } = useToolMode(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { wallAssetStore } = useSceneContext(); @@ -21,11 +21,11 @@ function WallAssetGroup() { const { projectId } = useParams(); useEffect(() => { - if (togglView || activeModule !== 'builder') { - setSelectedFloorAsset(null); + if (toggleView || activeModule !== 'builder' || toolMode !== 'cursor') { + setSelectedWallAsset(null); } setDeletableWallAsset(null); - }, [togglView, activeModule, activeTool]) + }, [toggleView, activeModule, toolMode]) useEffect(() => { if (projectId && selectedVersion) { diff --git a/app/src/modules/builder/zone/zoneGroup.tsx b/app/src/modules/builder/zone/zoneGroup.tsx index c44bf09..08d3e9c 100644 --- a/app/src/modules/builder/zone/zoneGroup.tsx +++ b/app/src/modules/builder/zone/zoneGroup.tsx @@ -11,7 +11,7 @@ import ZoneInstances from './Instances/zoneInstances'; import { getZonesApi } from '../../../services/factoryBuilder/zone/getZonesApi'; function ZoneGroup() { - const { togglView } = useToggleView(); + const { toggleView } = useToggleView(); const { setSelectedZone } = useBuilderStore(); const { activeModule } = useModuleStore(); const { activeTool } = useActiveTool(); @@ -22,10 +22,10 @@ function ZoneGroup() { const { projectId } = useParams(); useEffect(() => { - if (togglView || activeModule !== 'builder') { + if (toggleView || activeModule !== 'builder') { setSelectedZone(null); } - }, [togglView, activeModule, activeTool]) + }, [toggleView, activeModule, activeTool]) useEffect(() => { if (projectId && selectedVersion) { diff --git a/app/src/modules/scene/camera/camMode.tsx b/app/src/modules/scene/camera/camMode.tsx index 77e2f31..60d0798 100644 --- a/app/src/modules/scene/camera/camMode.tsx +++ b/app/src/modules/scene/camera/camMode.tsx @@ -1,12 +1,13 @@ import { useFrame, useThree } from "@react-three/fiber"; import React, { useEffect, useState } from "react"; +import { useKeyboardControls } from "@react-three/drei"; import * as CONSTANTS from "../../../types/world/worldConstants"; import { useCamMode, useToggleView } from "../../../store/builder/store"; -import { useKeyboardControls } from "@react-three/drei"; -import switchToThirdPerson from "./switchToThirdPerson"; -import switchToFirstPerson from "./switchToFirstPerson"; + +import switchToThirdPerson from "./functions/switchToThirdPerson"; +import switchToFirstPerson from "./functions/switchToFirstPerson"; import { detectModifierKeys } from "../../../utils/shortcutkeys/detectModifierKeys"; -import { firstPersonCamera } from "./firstPersonCamera"; +import { firstPersonCamera } from "./functions/firstPersonCamera"; const CamMode: React.FC = () => { const { camMode, setCamMode } = useCamMode(); diff --git a/app/src/modules/scene/camera/firstPersonCamera.ts b/app/src/modules/scene/camera/functions/firstPersonCamera.ts similarity index 92% rename from app/src/modules/scene/camera/firstPersonCamera.ts rename to app/src/modules/scene/camera/functions/firstPersonCamera.ts index 85aacb3..27620f3 100644 --- a/app/src/modules/scene/camera/firstPersonCamera.ts +++ b/app/src/modules/scene/camera/functions/firstPersonCamera.ts @@ -1,39 +1,39 @@ -import * as CONSTANTS from "../../../types/world/worldConstants"; - -interface FirstPersonCameraProps { - setIsTransitioning?: (value: boolean) => void; - state: any; -} - -interface FirstPersonCameraParams extends FirstPersonCameraProps { - camMode: string; - setCamMode: (mode: string) => void; - switchToFirstPerson: (controls: any, camera: any) => Promise; - switchToThirdPerson: (controls: any, camera: any) => Promise; -} - -export async function firstPersonCamera({ - setIsTransitioning, - state, - camMode, - setCamMode, - switchToFirstPerson, - switchToThirdPerson -}: FirstPersonCameraParams): Promise { - setIsTransitioning && setIsTransitioning(true); - - state.controls.mouseButtons.left = CONSTANTS.controlsTransition.leftMouse; - state.controls.mouseButtons.right = CONSTANTS.controlsTransition.rightMouse; - state.controls.mouseButtons.wheel = CONSTANTS.controlsTransition.wheelMouse; - state.controls.mouseButtons.middle = CONSTANTS.controlsTransition.middleMouse; - - if (camMode === "ThirdPerson") { - setCamMode("FirstPerson"); - await switchToFirstPerson(state.controls, state.camera); - } else if (camMode === "FirstPerson") { - setCamMode("ThirdPerson"); - await switchToThirdPerson(state.controls, state.camera); - } - - setIsTransitioning && setIsTransitioning(false); -} +import * as CONSTANTS from "../../../../types/world/worldConstants"; + +interface FirstPersonCameraProps { + setIsTransitioning?: (value: boolean) => void; + state: any; +} + +interface FirstPersonCameraParams extends FirstPersonCameraProps { + camMode: string; + setCamMode: (mode: string) => void; + switchToFirstPerson: (controls: any, camera: any) => Promise; + switchToThirdPerson: (controls: any, camera: any) => Promise; +} + +export async function firstPersonCamera({ + setIsTransitioning, + state, + camMode, + setCamMode, + switchToFirstPerson, + switchToThirdPerson +}: FirstPersonCameraParams): Promise { + setIsTransitioning && setIsTransitioning(true); + + state.controls.mouseButtons.left = CONSTANTS.controlsTransition.leftMouse; + state.controls.mouseButtons.right = CONSTANTS.controlsTransition.rightMouse; + state.controls.mouseButtons.wheel = CONSTANTS.controlsTransition.wheelMouse; + state.controls.mouseButtons.middle = CONSTANTS.controlsTransition.middleMouse; + + if (camMode === "ThirdPerson") { + setCamMode("FirstPerson"); + await switchToFirstPerson(state.controls, state.camera); + } else if (camMode === "FirstPerson") { + setCamMode("ThirdPerson"); + await switchToThirdPerson(state.controls, state.camera); + } + + setIsTransitioning && setIsTransitioning(false); +} diff --git a/app/src/modules/scene/camera/switchToFirstPerson.ts b/app/src/modules/scene/camera/functions/switchToFirstPerson.ts similarity index 91% rename from app/src/modules/scene/camera/switchToFirstPerson.ts rename to app/src/modules/scene/camera/functions/switchToFirstPerson.ts index a5371c4..a8a35de 100644 --- a/app/src/modules/scene/camera/switchToFirstPerson.ts +++ b/app/src/modules/scene/camera/functions/switchToFirstPerson.ts @@ -1,25 +1,25 @@ -import * as THREE from 'three'; -import * as CONSTANTS from '../../../types/world/worldConstants'; - -export default async function switchToFirstPerson( - controls: any, - camera: any -) { - if (!controls) return; - - const cameraDirection = new THREE.Vector3(); - camera.getWorldDirection(cameraDirection); - cameraDirection.normalize(); - - await controls.setPosition(camera.position.x, 2, camera.position.z, true); - controls.setTarget(camera.position.x, 2, camera.position.z, true); - controls.mouseButtons.left = CONSTANTS.firstPersonControls.leftMouse; - controls.lockPointer(); - - controls.azimuthRotateSpeed = CONSTANTS.firstPersonControls.azimuthRotateSpeed; - controls.polarRotateSpeed = CONSTANTS.firstPersonControls.polarRotateSpeed; - controls.truckSpeed = CONSTANTS.firstPersonControls.truckSpeed; - controls.minDistance = CONSTANTS.firstPersonControls.minDistance; - controls.maxDistance = CONSTANTS.firstPersonControls.maxDistance; - controls.maxPolarAngle = CONSTANTS.firstPersonControls.maxPolarAngle; +import * as THREE from 'three'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +export default async function switchToFirstPerson( + controls: any, + camera: any +) { + if (!controls) return; + + const cameraDirection = new THREE.Vector3(); + camera.getWorldDirection(cameraDirection); + cameraDirection.normalize(); + + await controls.setPosition(camera.position.x, 2, camera.position.z, true); + controls.setTarget(camera.position.x, 2, camera.position.z, true); + controls.mouseButtons.left = CONSTANTS.firstPersonControls.leftMouse; + controls.lockPointer(); + + controls.azimuthRotateSpeed = CONSTANTS.firstPersonControls.azimuthRotateSpeed; + controls.polarRotateSpeed = CONSTANTS.firstPersonControls.polarRotateSpeed; + controls.truckSpeed = CONSTANTS.firstPersonControls.truckSpeed; + controls.minDistance = CONSTANTS.firstPersonControls.minDistance; + controls.maxDistance = CONSTANTS.firstPersonControls.maxDistance; + controls.maxPolarAngle = CONSTANTS.firstPersonControls.maxPolarAngle; } \ No newline at end of file diff --git a/app/src/modules/scene/camera/switchToThirdPerson.ts b/app/src/modules/scene/camera/functions/switchToThirdPerson.ts similarity index 93% rename from app/src/modules/scene/camera/switchToThirdPerson.ts rename to app/src/modules/scene/camera/functions/switchToThirdPerson.ts index 1e59749..b66adc5 100644 --- a/app/src/modules/scene/camera/switchToThirdPerson.ts +++ b/app/src/modules/scene/camera/functions/switchToThirdPerson.ts @@ -1,29 +1,29 @@ -import * as THREE from 'three'; -import * as CONSTANTS from '../../../types/world/worldConstants'; - -export default async function switchToThirdPerson( - controls: any, - camera: any -) { - if (!controls) return; - controls.mouseButtons.left = CONSTANTS.thirdPersonControls.leftMouse; - controls.mouseButtons.right = CONSTANTS.thirdPersonControls.rightMouse; - controls.mouseButtons.middle = CONSTANTS.thirdPersonControls.middleMouse; - controls.mouseButtons.wheel = CONSTANTS.thirdPersonControls.wheelMouse; - controls.unlockPointer(); - - const cameraDirection = new THREE.Vector3(); - camera.getWorldDirection(cameraDirection); - const targetOffset = cameraDirection.multiplyScalar(CONSTANTS.thirdPersonControls.targetOffset); - const targetPosition = new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z).add(targetOffset); - - controls.setPosition(camera.position.x, CONSTANTS.thirdPersonControls.cameraHeight, camera.position.z, true); - controls.setTarget(targetPosition.x, 0, targetPosition.z, true); - - controls.azimuthRotateSpeed = CONSTANTS.thirdPersonControls.azimuthRotateSpeed; - controls.polarRotateSpeed = CONSTANTS.thirdPersonControls.polarRotateSpeed; - controls.truckSpeed = CONSTANTS.thirdPersonControls.truckSpeed; - controls.minDistance = CONSTANTS.threeDimension.minDistance; - controls.maxDistance = CONSTANTS.thirdPersonControls.maxDistance; - controls.maxPolarAngle = CONSTANTS.thirdPersonControls.maxPolarAngle; +import * as THREE from 'three'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +export default async function switchToThirdPerson( + controls: any, + camera: any +) { + if (!controls) return; + controls.mouseButtons.left = CONSTANTS.thirdPersonControls.leftMouse; + controls.mouseButtons.right = CONSTANTS.thirdPersonControls.rightMouse; + controls.mouseButtons.middle = CONSTANTS.thirdPersonControls.middleMouse; + controls.mouseButtons.wheel = CONSTANTS.thirdPersonControls.wheelMouse; + controls.unlockPointer(); + + const cameraDirection = new THREE.Vector3(); + camera.getWorldDirection(cameraDirection); + const targetOffset = cameraDirection.multiplyScalar(CONSTANTS.thirdPersonControls.targetOffset); + const targetPosition = new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z).add(targetOffset); + + controls.setPosition(camera.position.x, CONSTANTS.thirdPersonControls.cameraHeight, camera.position.z, true); + controls.setTarget(targetPosition.x, 0, targetPosition.z, true); + + controls.azimuthRotateSpeed = CONSTANTS.thirdPersonControls.azimuthRotateSpeed; + controls.polarRotateSpeed = CONSTANTS.thirdPersonControls.polarRotateSpeed; + controls.truckSpeed = CONSTANTS.thirdPersonControls.truckSpeed; + controls.minDistance = CONSTANTS.threeDimension.minDistance; + controls.maxDistance = CONSTANTS.thirdPersonControls.maxDistance; + controls.maxPolarAngle = CONSTANTS.thirdPersonControls.maxPolarAngle; } \ No newline at end of file diff --git a/app/src/modules/scene/camera/updateCameraPosition.ts b/app/src/modules/scene/camera/functions/updateCameraPosition.ts similarity index 88% rename from app/src/modules/scene/camera/updateCameraPosition.ts rename to app/src/modules/scene/camera/functions/updateCameraPosition.ts index 26e22ed..fb8c956 100644 --- a/app/src/modules/scene/camera/updateCameraPosition.ts +++ b/app/src/modules/scene/camera/functions/updateCameraPosition.ts @@ -1,26 +1,26 @@ -import { Socket } from "socket.io-client"; -import * as THREE from "three"; -import { getUserData } from "../../../functions/getUserData"; - -export default function updateCamPosition( - controls: any, - socket: Socket, - position: THREE.Vector3, - rotation: THREE.Euler, - projectId?: string -) { - const { userId, organization } = getUserData(); - if (!controls.current) return; - const target = controls.current.getTarget(new THREE.Vector3()); - - const camData = { - organization, - userId: userId, - position: position, - target: new THREE.Vector3(target.x, 0, target.z), - rotation: new THREE.Vector3(rotation.x, rotation.y, rotation.z), - socketId: socket.id, - projectId, - }; - socket.emit("v1:Camera:set", camData); -} +import { Socket } from "socket.io-client"; +import * as THREE from "three"; +import { getUserData } from "../../../../functions/getUserData"; + +export default function updateCamPosition( + controls: any, + socket: Socket, + position: THREE.Vector3, + rotation: THREE.Euler, + projectId?: string +) { + const { userId, organization } = getUserData(); + if (!controls.current) return; + const target = controls.current.getTarget(new THREE.Vector3()); + + const camData = { + organization, + userId: userId, + position: position, + target: new THREE.Vector3(target.x, 0, target.z), + rotation: new THREE.Vector3(rotation.x, rotation.y, rotation.z), + socketId: socket.id, + projectId, + }; + socket.emit("v1:Camera:set", camData); +} diff --git a/app/src/modules/scene/camera/shortcutsControls/cameraShortcutsControls.tsx b/app/src/modules/scene/camera/shortcutsControls/cameraShortcutsControls.tsx new file mode 100644 index 0000000..573584c --- /dev/null +++ b/app/src/modules/scene/camera/shortcutsControls/cameraShortcutsControls.tsx @@ -0,0 +1,73 @@ +import { useEffect } from "react"; +import { useThree } from "@react-three/fiber"; +import * as THREE from "three"; +import type { CameraControls } from "@react-three/drei"; + +const CameraShortcutsControls = () => { + const { camera, controls } = useThree(); + + const isTextInput = (element: Element | null): boolean => + element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement || + element?.getAttribute("contenteditable") === "true"; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!controls) return; + + const cc = controls as CameraControls; + + // get current target + const target = new THREE.Vector3(); + cc.getTarget(target); + + const distance = camera.position.distanceTo(target); + let pos: THREE.Vector3 | null = null; + + const dir = new THREE.Vector3().subVectors(camera.position, target).normalize(); + + if (isTextInput(document.activeElement)) return; + + switch (e.key) { + case "1": // Front + pos = new THREE.Vector3(0, 0, distance).add(target); + break; + case "3": // Right + pos = new THREE.Vector3(distance, 0, 0).add(target); + break; + case "7": // Top + pos = new THREE.Vector3(0, distance, 0).add(target); + break; + case "9": { + // Opposite view logic + if (Math.abs(dir.z) > Math.abs(dir.x) && Math.abs(dir.z) > Math.abs(dir.y)) { + // Currently looking Front/Back → flip Z + pos = new THREE.Vector3(0, 0, -Math.sign(dir.z) * distance).add(target); + } else if (Math.abs(dir.x) > Math.abs(dir.z) && Math.abs(dir.x) > Math.abs(dir.y)) { + // Currently looking Right/Left → flip X + pos = new THREE.Vector3(-Math.sign(dir.x) * distance, 0, 0).add(target); + } else { + // Currently looking Top/Bottom → stay Top + pos = new THREE.Vector3(0, distance, 0).add(target); + } + break; + } + } + + if (pos) { + cc.setLookAt( + pos.x, pos.y, pos.z, // camera position + target.x, target.y, target.z, // keep same target + true // smooth transition + ); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [controls, camera]); + + return null; +}; + +export default CameraShortcutsControls; diff --git a/app/src/modules/scene/controls/contextControls/contextControls.tsx b/app/src/modules/scene/controls/contextControls/contextControls.tsx index 142f8d1..46801c1 100644 --- a/app/src/modules/scene/controls/contextControls/contextControls.tsx +++ b/app/src/modules/scene/controls/contextControls/contextControls.tsx @@ -1,41 +1,14 @@ import { useEffect, useRef, useState } from "react"; import { useThree } from "@react-three/fiber"; import { CameraControls, Html, ScreenSpace } from "@react-three/drei"; -import { - useContextActionStore, - useRenameModeStore, - useSelectedAssets, -} from "../../../../store/builder/store"; +import { useContextActionStore, useRenameModeStore, useSelectedAssets, useToggleView, useToolMode, } from "../../../../store/builder/store"; import ContextMenu from "../../../../components/ui/menu/contextMenu"; +import useModuleStore from "../../../../store/useModuleStore"; function ContextControls() { - const { gl, controls } = useThree(); - const [canRender, setCanRender] = useState(false); - const [visibility, setVisibility] = useState({ - rename: true, - focus: true, - flipX: true, - flipZ: true, - move: true, - rotate: true, - duplicate: true, - copy: true, - paste: true, - modifier: false, - group: false, - array: false, - delete: true, - }); - const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); - const { selectedAssets } = useSelectedAssets(); - const { setContextAction } = useContextActionStore(); - const { setIsRenameMode } = useRenameModeStore(); - const rightDrag = useRef(false); - const isRightMouseDown = useRef(false); - - useEffect(() => { - if (selectedAssets.length === 1) { - setVisibility({ + const { gl, controls } = useThree(); + const [canRender, setCanRender] = useState(false); + const [visibility, setVisibility] = useState({ rename: true, focus: true, flipX: true, @@ -49,195 +22,219 @@ function ContextControls() { group: false, array: false, delete: true, - }); - } else if (selectedAssets.length > 1) { - setVisibility({ - rename: false, - focus: true, - flipX: true, - flipZ: true, - move: true, - rotate: true, - duplicate: true, - copy: true, - paste: true, - modifier: false, - group: true, - array: false, - delete: true, - }); - } else { - setVisibility({ - rename: false, - focus: false, - flipX: false, - flipZ: false, - move: false, - rotate: false, - duplicate: false, - copy: false, - paste: false, - modifier: false, - group: false, - array: false, - delete: false, - }); - } - }, [selectedAssets]); + }); + const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); + const { toggleView } = useToggleView(); + const { activeModule } = useModuleStore(); + const { toolMode } = useToolMode(); + const { selectedAssets } = useSelectedAssets(); + const { setContextAction } = useContextActionStore(); + const { setIsRenameMode } = useRenameModeStore(); + const rightDrag = useRef(false); + const isRightMouseDown = useRef(false); - useEffect(() => { - const canvasElement = gl.domElement; - - const onPointerDown = (evt: any) => { - if (evt.button === 2) { - isRightMouseDown.current = true; - rightDrag.current = false; - } - }; - - const onPointerMove = () => { - if (isRightMouseDown.current) { - rightDrag.current = true; - } - }; - - const onPointerUp = (evt: any) => { - if (evt.button === 2) { - isRightMouseDown.current = false; - } - }; - - const handleContextClick = (event: MouseEvent) => { - event.preventDefault(); - if (rightDrag.current) return; - if (selectedAssets.length > 0) { - setMenuPosition({ - x: event.clientX - gl.domElement.width / 2, - y: event.clientY - gl.domElement.height / 2, - }); - setCanRender(true); - if (controls) { - (controls as CameraControls).enabled = false; + useEffect(() => { + if (selectedAssets.length === 1) { + setVisibility({ + rename: true, + focus: true, + flipX: true, + flipZ: true, + move: true, + rotate: true, + duplicate: true, + copy: true, + paste: true, + modifier: false, + group: false, + array: false, + delete: true, + }); + } else if (selectedAssets.length > 1) { + setVisibility({ + rename: false, + focus: true, + flipX: true, + flipZ: true, + move: true, + rotate: true, + duplicate: true, + copy: true, + paste: true, + modifier: false, + group: true, + array: false, + delete: true, + }); + } else { + setVisibility({ + rename: false, + focus: false, + flipX: false, + flipZ: false, + move: false, + rotate: false, + duplicate: false, + copy: false, + paste: false, + modifier: false, + group: false, + array: false, + delete: false, + }); } - } else { + }, [selectedAssets]); + + useEffect(() => { + const canvasElement = gl.domElement; + + const onPointerDown = (evt: any) => { + if (evt.button === 2) { + isRightMouseDown.current = true; + rightDrag.current = false; + } + }; + + const onPointerMove = () => { + if (isRightMouseDown.current) { + rightDrag.current = true; + } + }; + + const onPointerUp = (evt: any) => { + if (evt.button === 2) { + isRightMouseDown.current = false; + } + }; + + const handleContextClick = (event: MouseEvent) => { + event.preventDefault(); + if (rightDrag.current) return; + if (selectedAssets.length > 0) { + setMenuPosition({ x: event.clientX - gl.domElement.width / 2, y: event.clientY - gl.domElement.height / 2, }); + setCanRender(true); + if (controls) { + (controls as CameraControls).enabled = false; + } + } else { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + } + }; + + if (selectedAssets.length > 0 && !toggleView && activeModule === "builder" && toolMode === 'cursor') { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + canvasElement.addEventListener("contextmenu", handleContextClick); + } else { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setMenuPosition({ x: 0, y: 0 }); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("contextmenu", handleContextClick); + }; + }, [controls, gl, selectedAssets, toggleView, activeModule, toolMode]); + + const handleAssetRename = () => { setCanRender(false); if (controls) { - (controls as CameraControls).enabled = true; + (controls as CameraControls).enabled = true; } - } + setContextAction("renameAsset"); + setIsRenameMode(true); + }; + const handleAssetFocus = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("focusAsset"); + }; + const handleAssetMove = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("moveAsset"); + }; + const handleAssetRotate = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("rotateAsset"); + }; + const handleAssetCopy = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("copyAsset"); + }; + const handleAssetPaste = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("pasteAsset"); + }; + const handleAssetDelete = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("deleteAsset"); + }; + const handleAssetDuplicate = () => { + setCanRender(false); + if (controls) { + (controls as CameraControls).enabled = true; + } + setContextAction("duplicateAsset"); }; - if (selectedAssets.length > 0) { - canvasElement.addEventListener("pointerdown", onPointerDown); - canvasElement.addEventListener("pointermove", onPointerMove); - canvasElement.addEventListener("pointerup", onPointerUp); - canvasElement.addEventListener("contextmenu", handleContextClick); - } else { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setMenuPosition({ x: 0, y: 0 }); - } - - return () => { - canvasElement.removeEventListener("pointerdown", onPointerDown); - canvasElement.removeEventListener("pointermove", onPointerMove); - canvasElement.removeEventListener("pointerup", onPointerUp); - canvasElement.removeEventListener("contextmenu", handleContextClick); - }; - }, [controls, gl, selectedAssets]); - - const handleAssetRename = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("renameAsset"); - setIsRenameMode(true); - }; - const handleAssetFocus = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("focusAsset"); - }; - const handleAssetMove = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("moveAsset"); - }; - const handleAssetRotate = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("rotateAsset"); - }; - const handleAssetCopy = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("copyAsset"); - }; - const handleAssetPaste = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("pasteAsset"); - }; - const handleAssetDelete = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("deleteAsset"); - }; - const handleAssetDuplicate = () => { - setCanRender(false); - if (controls) { - (controls as CameraControls).enabled = true; - } - setContextAction("duplicateAsset"); - }; - - return ( - <> - {canRender && ( - - - handleAssetRename()} - onFocus={() => handleAssetFocus()} - onFlipX={() => console.log("Flip to X")} - onFlipZ={() => console.log("Flip to Z")} - onMove={() => handleAssetMove()} - onRotate={() => handleAssetRotate()} - onDuplicate={() => handleAssetDuplicate()} - onCopy={() => handleAssetCopy()} - onPaste={() => handleAssetPaste()} - onGroup={() => console.log("Group")} - onArray={() => console.log("Array")} - onDelete={() => handleAssetDelete()} - /> - - - )} - - ); + return ( + <> + {canRender && ( + + + handleAssetRename()} + onFocus={() => handleAssetFocus()} + onFlipX={() => console.log("Flip to X")} + onFlipZ={() => console.log("Flip to Z")} + onMove={() => handleAssetMove()} + onRotate={() => handleAssetRotate()} + onDuplicate={() => handleAssetDuplicate()} + onCopy={() => handleAssetCopy()} + onPaste={() => handleAssetPaste()} + onGroup={() => console.log("Group")} + onArray={() => console.log("Array")} + onDelete={() => handleAssetDelete()} + /> + + + )} + + ); } export default ContextControls; diff --git a/app/src/modules/scene/controls/controls.tsx b/app/src/modules/scene/controls/controls.tsx index d63f0ac..a3c9638 100644 --- a/app/src/modules/scene/controls/controls.tsx +++ b/app/src/modules/scene/controls/controls.tsx @@ -3,22 +3,22 @@ import { useRef, useEffect } from "react"; import { useThree } from "@react-three/fiber"; import * as THREE from "three"; import * as CONSTANTS from '../../../types/world/worldConstants'; - import { useSocketStore, useToggleView, useResetCamera } from "../../../store/builder/store"; -import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi"; -import updateCamPosition from "../camera/updateCameraPosition"; + import CamMode from "../camera/camMode"; import SwitchView from "../camera/switchView"; -import SelectionControls3D from "./selectionControls/selection3D/selectionControls3D"; -import TransformControl from "./transformControls/transformControls"; -import { useParams } from "react-router-dom"; -import { getUserData } from "../../../functions/getUserData"; - import ContextControls from "./contextControls/contextControls"; +import TransformControl from "./transformControls/transformControls"; import SelectionControls2D from "./selectionControls/selection2D/selectionControls2D"; +import SelectionControls3D from "./selectionControls/selection3D/selectionControls3D"; import UndoRedo2DControls from "./undoRedoControls/undoRedo2D/undoRedo2DControls"; import UndoRedo3DControls from "./undoRedoControls/undoRedo3D/undoRedo3DControls"; -import { useCameraShortcuts } from "../../../hooks/useCameraShortcuts"; +import CameraShortcutsControls from "../camera/shortcutsControls/cameraShortcutsControls"; + +import { useParams } from "react-router-dom"; +import { getUserData } from "../../../functions/getUserData"; +import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi"; +import updateCamPosition from "../camera/functions/updateCameraPosition"; export default function Controls() { const controlsRef = useRef(null); @@ -117,7 +117,6 @@ export default function Controls() { stopInterval(); }; }, [toggleView, state, socket]); - useCameraShortcuts(controlsRef); return ( <> @@ -140,6 +139,8 @@ export default function Controls() { + + diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/boundingBoxHelper3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/boundingBoxHelper3D.tsx index dde668e..3345a2d 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/boundingBoxHelper3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/boundingBoxHelper3D.tsx @@ -91,9 +91,9 @@ const BoundingBox = ({ boundingBoxRef, isPerAsset = true }: BoundingBoxProps) => > new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); const { toggleView } = useToggleView(); + const { toolMode } = useToolMode(); + const { activeModule } = useModuleStore(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); @@ -162,7 +165,7 @@ function MoveControls3D({ boundingBoxRef }: any) { } }; - if (!toggleView) { + if (!toggleView && toolMode === 'cursor') { canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); @@ -178,7 +181,14 @@ function MoveControls3D({ boundingBoxRef }: any) { canvasElement?.removeEventListener("keyup", onKeyUp); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [camera, controls, scene, toggleView, selectedAssets, socket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent, initialStates]); + }, [camera, controls, scene, toggleView, toolMode, selectedAssets, socket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent, initialStates]); + + useEffect(() => { + if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) { + resetToInitialPositions(); + setMovedObjects([]); + } + }, [activeModule, toolMode, toggleView]); const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => { const pointPosition = new THREE.Vector3().copy(point.position); diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx index 5af7a3e..4f2319f 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx @@ -1,7 +1,7 @@ import * as THREE from "three"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useFrame, useThree } from "@react-three/fiber"; -import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store"; +import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView, useToolMode } from "../../../../../store/builder/store"; import * as Types from "../../../../../types/world/worldTypes"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; import { useParams } from "react-router-dom"; @@ -11,6 +11,7 @@ import { useSceneContext } from "../../../sceneContext"; import { useVersionContext } from "../../../../builder/version/versionContext"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { handleAssetRotationSnap } from "./functions/handleAssetRotationSnap"; +import useModuleStore from "../../../../../store/useModuleStore"; // import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; @@ -18,6 +19,8 @@ function RotateControls3D() { const { camera, gl, scene, pointer, raycaster } = useThree(); const { toggleView } = useToggleView(); + const { toolMode } = useToolMode(); + const { activeModule } = useModuleStore(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); @@ -34,6 +37,7 @@ function RotateControls3D() { const [initialRotations, setInitialRotations] = useState>({}); const [initialPositions, setInitialPositions] = useState>({}); const [isRotating, setIsRotating] = useState(false); + const [isIndividualRotating, setIsIndividualRotating] = useState(false); const prevPointerPosition = useRef(null); const rotationCenter = useRef(null); const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, }); @@ -117,6 +121,12 @@ function RotateControls3D() { } } + if (event.key.toLowerCase() === 'i') { + if (rotatedObjects.length > 0) { + setIsIndividualRotating(!isIndividualRotating); + } + } + if (event.key.toLowerCase() === "escape") { event.preventDefault(); resetToInitialRotations(); @@ -137,7 +147,7 @@ function RotateControls3D() { prevPointerPosition.current = currentPointer.clone(); }; - if (!toggleView) { + if (!toggleView && toolMode === 'cursor') { canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); @@ -152,7 +162,14 @@ function RotateControls3D() { canvasElement.removeEventListener("keydown", onKeyDown); canvasElement?.removeEventListener("keyup", onKeyUp); }; - }, [camera, scene, toggleView, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects, keyEvent, initialPositions, initialRotations]); + }, [camera, scene, toggleView, toolMode, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects, keyEvent, initialPositions, initialRotations, isIndividualRotating]); + + useEffect(() => { + if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) { + resetToInitialRotations(); + setRotatedObjects([]); + } + }, [activeModule, toolMode, toggleView]); const resetToInitialRotations = useCallback(() => { setTimeout(() => { @@ -204,10 +221,12 @@ function RotateControls3D() { wasShiftHeldRef }); - const relativePosition = new THREE.Vector3().subVectors(obj.position, center); - const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta); - relativePosition.applyMatrix4(rotationMatrix); - obj.position.copy(center).add(relativePosition); + if (!isIndividualRotating) { + const relativePosition = new THREE.Vector3().subVectors(obj.position, center); + const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta); + relativePosition.applyMatrix4(rotationMatrix); + obj.position.copy(center).add(relativePosition); + } const rotationQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), angleDelta); obj.quaternion.multiply(rotationQuat); @@ -384,8 +403,9 @@ function RotateControls3D() { } setIsRotating(false); + setIsIndividualRotating(false); clearSelection(); - }, [rotatedObjects, eventStore, productStore, selectedProduct, updateBackend, projectId, updateAsset, organization, socket, selectedVersion, userId]); + }, [rotatedObjects, eventStore, productStore, selectedProduct, updateBackend, projectId, updateAsset, organization, socket, selectedVersion, userId, initialPositions, initialRotations]); const clearSelection = () => { setPastedObjects([]); @@ -393,6 +413,7 @@ function RotateControls3D() { setMovedObjects([]); setRotatedObjects([]); setSelectedAssets([]); + setIsIndividualRotating(false); }; return null; diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx index ed458c2..512b80f 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx @@ -16,22 +16,23 @@ import DuplicationControls3D from "./duplicationControls3D"; import CopyPasteControls3D from "./copyPasteControls3D"; import MoveControls3D from "./moveControls3D"; import RotateControls3D from "./rotateControls3D"; +import TransformControls3D from "./transformControls3D"; // import { deleteFloorItem } from '../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorItemApi'; const SelectionControls3D: React.FC = () => { const { camera, controls, gl, scene, raycaster, pointer } = useThree(); const { toggleView } = useToggleView(); + const { activeModule } = useModuleStore(); + const { toolMode } = useToolMode(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const boundingBoxRef = useRef(); - const { activeModule } = useModuleStore(); const { socket } = useSocketStore(); const { contextAction, setContextAction } = useContextActionStore() const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext(); const { push3D } = undoRedo3DStore(); const { removeAsset, getAssetById, movedObjects, rotatedObjects, copiedObjects, pastedObjects, duplicatedObjects, setPastedObjects, setDuplicatedObjects } = assetStore(); const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]); - const { toolMode } = useToolMode(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { selectedProductStore } = useProductContext(); @@ -202,7 +203,7 @@ const SelectionControls3D: React.FC = () => { rightClickMoved.current = false; }; - if (!toggleView && activeModule === "builder" && toolMode === 'cursor') { + if (!toggleView && activeModule === "builder" && (toolMode === 'cursor' || toolMode === 'Move-Asset' || toolMode === 'Rotate-Asset')) { helper.enabled = true; canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); @@ -227,7 +228,7 @@ const SelectionControls3D: React.FC = () => { }, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, rotatedObjects, activeModule, toolMode]); useEffect(() => { - if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) { + if (activeModule !== "builder" || (toolMode !== 'cursor' && toolMode !== 'Move-Asset' && toolMode !== 'Rotate-Asset') || toggleView) { clearSelection(); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -381,6 +382,8 @@ const SelectionControls3D: React.FC = () => { + + ); }; diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/transformControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/transformControls3D.tsx new file mode 100644 index 0000000..3f3162b --- /dev/null +++ b/app/src/modules/scene/controls/selectionControls/selection3D/transformControls3D.tsx @@ -0,0 +1,380 @@ +import * as THREE from 'three'; +import { useRef, useEffect, useState, useCallback } from 'react'; +import { useThree } from '@react-three/fiber'; +import { TransformControls } from '@react-three/drei'; +import { useSelectedAssets, useSocketStore, useToolMode } from '../../../../../store/builder/store'; +import { useProductContext } from '../../../../simulation/products/productContext'; +import { useVersionContext } from '../../../../builder/version/versionContext'; +import { useSceneContext } from '../../../sceneContext'; +import { useParams } from 'react-router-dom'; + +import { getUserData } from '../../../../../functions/getUserData'; +import { upsertProductOrEventApi } from '../../../../../services/simulation/products/UpsertProductOrEventApi'; +import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys'; +// import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; + +function TransformControls3D() { + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const { toolMode } = useToolMode(); + const { camera, scene, gl } = useThree(); + const transformControlsRef = useRef(null); + const [visible, setVisible] = useState(false); + const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); + const { socket } = useSocketStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct } = selectedProductStore(); + const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext(); + const { push3D, subscribeUndoRedo } = undoRedo3DStore(); + const { updateAsset, getAssetById } = assetStore(); + const { userId, organization } = getUserData(); + const { selectedVersionStore } = useVersionContext(); + const { selectedVersion } = selectedVersionStore(); + const { projectId } = useParams(); + const pivotPointRef = useRef(new THREE.Vector3()); + const initialPositionsRef = useRef>(new Map()); + const initialRotationsRef = useRef>(new Map()); + const initialPivotPositionRef = useRef(new THREE.Vector3()); + const initialQuaternionsRef = useRef>(new Map()); + const tempObjectRef = useRef(new THREE.Object3D()); + + const updateBackend = useCallback(( + productName: string, + productUuid: string, + projectId: string, + eventData: any + ) => { + upsertProductOrEventApi({ + productName: productName, + productUuid: productUuid, + projectId: projectId, + eventDatas: eventData, + versionId: selectedVersion?.versionId || '', + }); + }, [selectedVersion]); + + const recalcGizmo = useCallback(() => { + if (selectedAssets.length === 0) return; + + const bbox = new THREE.Box3(); + selectedAssets.forEach(obj => bbox.expandByObject(obj)); + bbox.getCenter(pivotPointRef.current); + initialPivotPositionRef.current.copy(pivotPointRef.current); + + tempObjectRef.current.position.copy(pivotPointRef.current); + tempObjectRef.current.rotation.set(0, 0, 0); + tempObjectRef.current.scale.set(1, 1, 1); + + initialPositionsRef.current.clear(); + initialRotationsRef.current.clear(); + initialQuaternionsRef.current.clear(); + + selectedAssets.forEach(obj => { + initialPositionsRef.current.set(obj.uuid, obj.position.clone()); + initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); + initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); + }); + }, [selectedAssets]); + + useEffect(() => { + const unsubscribe = subscribeUndoRedo(() => { + setTimeout(() => { + recalcGizmo(); + }, 10); + }); + return unsubscribe; + }, [subscribeUndoRedo, recalcGizmo]); + + const handleTransformationComplete = useCallback(() => { + if (selectedAssets.length === 0) return; + + const updatedAssets = [...selectedAssets]; + setSelectedAssets(updatedAssets); + + selectedAssets.forEach(obj => { + const asset = getAssetById(obj.uuid); + if (!asset) return; + + updateAsset(asset.modelUuid, { + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }); + + if (asset.eventData) { + const eventData = eventStore.getState().getEventByModelUuid(asset.modelUuid); + const productData = productStore.getState().getEventByModelUuid( + selectedProduct.productUuid, + asset.modelUuid + ); + + if (eventData) { + eventStore.getState().updateEvent(asset.modelUuid, { + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }); + } + + if (productData) { + const event = productStore + .getState() + .updateEvent( + selectedProduct.productUuid, + asset.modelUuid, + { + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + } + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + } + } + } + + //REST + + // setAssetsApi( + // organization, + // asset.modelUuid, + // asset.modelName, + // [obj.position.x, 0, obj.position.z], + // { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + // asset.assetId, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization, + modelUuid: asset.modelUuid, + modelName: asset.modelName, + assetId: asset.assetId, + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id, + versionId: selectedVersion?.versionId || '', + userId, + projectId + }; + + socket.emit("v1:model-asset:add", data); + + }); + }, [selectedAssets, setSelectedAssets, getAssetById, updateAsset, eventStore, productStore, selectedProduct, updateBackend, projectId, organization, socket, selectedVersion, userId, push3D]); + + useEffect(() => { + const temp = tempObjectRef.current; + scene.add(temp); + return () => { + scene.remove(temp); + }; + }, [scene]); + + useEffect(() => { + if (selectedAssets.length === 0) { + setVisible(false); + return; + } + + const bbox = new THREE.Box3(); + selectedAssets.forEach(obj => bbox.expandByObject(obj)); + bbox.getCenter(pivotPointRef.current); + initialPivotPositionRef.current.copy(pivotPointRef.current); + + tempObjectRef.current.position.copy(pivotPointRef.current); + tempObjectRef.current.rotation.set(0, 0, 0); + tempObjectRef.current.scale.set(1, 1, 1); + + initialPositionsRef.current.clear(); + initialRotationsRef.current.clear(); + initialQuaternionsRef.current.clear(); + selectedAssets.forEach(obj => { + initialPositionsRef.current.set(obj.uuid, obj.position.clone()); + initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); + initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); + }); + + setVisible(true); + }, [selectedAssets]); + + useEffect(() => { + const controls = transformControlsRef.current; + if (!controls || !visible) return; + + controls.attach(tempObjectRef.current); + controls.setMode(toolMode === 'Move-Asset' ? 'translate' : 'rotate'); + + const onObjectChange = () => { + const temp = tempObjectRef.current; + if (!temp) return; + + selectedAssets.forEach(obj => { + const initialPos = initialPositionsRef.current.get(obj.uuid); + const initialQuat = initialQuaternionsRef.current.get(obj.uuid); + if (!initialPos || !initialQuat) return; + + if (controls.mode === 'translate') { + const delta = new THREE.Vector3().copy(temp.position).sub(initialPivotPositionRef.current); + obj.position.copy(initialPos).add(delta); + } else if (controls.mode === 'rotate') { + const deltaQuat = temp.quaternion.clone(); + const relPos = initialPos.clone().sub(initialPivotPositionRef.current); + relPos.applyQuaternion(deltaQuat); + obj.position.copy(initialPivotPositionRef.current).add(relPos); + obj.quaternion.copy(deltaQuat).multiply(initialQuat); + } + }); + }; + + const onMouseUp = () => { + const assetsToUpdate: AssetData[] = []; + const undoActions: UndoRedo3DAction[] = []; + + selectedAssets.forEach((obj) => { + const asset = getAssetById(obj.uuid); + if (!asset) return; + + const initialPos = initialPositionsRef.current.get(obj.uuid); + const initialRot = initialRotationsRef.current.get(obj.uuid); + if (!initialPos || !initialRot) return; + + const assetData: Asset = { + ...asset, + position: [initialPos.x, initialPos.y, initialPos.z], + rotation: [initialRot.x, initialRot.y, initialRot.z], + }; + + const newData: Asset = { + ...asset, + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }; + + assetsToUpdate.push({ + type: "Asset", + assetData, + newData, + timeStap: new Date().toISOString(), + }); + + updateAsset(asset.modelUuid, { + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + }); + }); + + if (assetsToUpdate.length > 0) { + if (assetsToUpdate.length === 1) { + undoActions.push({ + module: "builder", + actionType: "Asset-Update", + asset: assetsToUpdate[0], + }); + } else { + undoActions.push({ + module: "builder", + actionType: "Assets-Update", + assets: assetsToUpdate, + }); + } + + push3D({ + type: "Scene", + actions: undoActions, + }); + } + + selectedAssets.forEach(obj => { + initialPositionsRef.current.set(obj.uuid, obj.position.clone()); + initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); + initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); + }); + + if (selectedAssets.length > 0) { + const bbox = new THREE.Box3(); + selectedAssets.forEach(obj => bbox.expandByObject(obj)); + bbox.getCenter(pivotPointRef.current); + initialPivotPositionRef.current.copy(pivotPointRef.current); + + tempObjectRef.current.position.copy(pivotPointRef.current); + tempObjectRef.current.rotation.set(0, 0, 0); + } + + recalcGizmo(); + handleTransformationComplete(); + }; + + controls.addEventListener('objectChange', onObjectChange); + controls.addEventListener('mouseUp', onMouseUp); + + return () => { + controls.removeEventListener('objectChange', onObjectChange); + controls.removeEventListener('mouseUp', onMouseUp); + controls.detach(); + }; + }, [selectedAssets, toolMode, visible, handleTransformationComplete]); + + useEffect(() => { + const canvasElement = gl.domElement; + + const handleKeyDown = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + + if (keyCombination !== keyEvent) { + if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } else { + setKeyEvent(""); + } + } + }; + + const onKeyUp = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + + if (keyCombination === "") { + setKeyEvent(""); + } else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } + } + + if (visible) { + canvasElement.addEventListener('keydown', handleKeyDown); + canvasElement.addEventListener('keyup', onKeyUp); + } + + return () => { + canvasElement.removeEventListener('keydown', handleKeyDown); + canvasElement.removeEventListener('keyup', onKeyUp); + } + }, [gl, visible, keyEvent]); + + if (!visible || (toolMode !== 'Move-Asset' && toolMode !== 'Rotate-Asset')) { + return null; + } + + return ( + + ); +} + +export default TransformControls3D; \ No newline at end of file diff --git a/app/src/modules/scene/controls/transformControls/transformControls.tsx b/app/src/modules/scene/controls/transformControls/transformControls.tsx index 415be9b..8428a63 100644 --- a/app/src/modules/scene/controls/transformControls/transformControls.tsx +++ b/app/src/modules/scene/controls/transformControls/transformControls.tsx @@ -1,17 +1,17 @@ -import { TransformControls } from "@react-three/drei"; import * as THREE from "three"; -import { useSelectedFloorItem, useObjectPosition, useObjectRotation, useActiveTool, useSocketStore } from "../../../../store/builder/store"; -import { useThree } from "@react-three/fiber"; - -import { useEffect, useRef, useState } from "react"; -import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; -import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi"; -// import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import { useParams } from "react-router-dom"; +import { useThree } from "@react-three/fiber"; +import { TransformControls } from "@react-three/drei"; +import { useEffect, useRef, useState } from "react"; import { useProductContext } from "../../../simulation/products/productContext"; import { getUserData } from "../../../../functions/getUserData"; import { useSceneContext } from "../../sceneContext"; import { useVersionContext } from "../../../builder/version/versionContext"; +import { useSelectedFloorItem, useObjectPosition, useObjectRotation, useActiveTool, useSocketStore } from "../../../../store/builder/store"; + +import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; +import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi"; +// import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; export default function TransformControl() { const state = useThree(); diff --git a/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx b/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx index 3ca5633..9331320 100644 --- a/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx +++ b/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx @@ -20,7 +20,7 @@ function UndoRedo3DControls() { const { selectedVersion } = selectedVersionStore(); useEffect(() => { - // console.log(undoStack, redoStack); + console.log(undoStack, redoStack); }, [undoStack, redoStack]); useEffect(() => { diff --git a/app/src/modules/scene/postProcessing/postProcessing.tsx b/app/src/modules/scene/postProcessing/postProcessing.tsx index 8aacd96..e6d656d 100644 --- a/app/src/modules/scene/postProcessing/postProcessing.tsx +++ b/app/src/modules/scene/postProcessing/postProcessing.tsx @@ -1,25 +1,19 @@ -import { DepthOfField, Bloom, EffectComposer, N8AO, Outline } from "@react-three/postprocessing"; -import { useThree } from "@react-three/fiber"; -import { BlendFunction } from "postprocessing"; -import { - useDeletableFloorItem, - useSelectedWallItem, - useSelectedFloorItem, -} from "../../../store/builder/store"; -import * as CONSTANTS from "../../../types/world/worldConstants"; -import { useDeletableEventSphere, useSelectedEventSphere, useSelectedPoints } from "../../../store/simulation/useSimulationStore"; import { useEffect } from "react"; +import { BlendFunction } from "postprocessing"; +import { DepthOfField, Bloom, EffectComposer, N8AO, Outline } from "@react-three/postprocessing"; +import { useDeletableFloorItem, useSelectedWallItem, useSelectedFloorItem, } from "../../../store/builder/store"; +import { useDeletableEventSphere, useSelectedEventSphere, useSelectedPoints } from "../../../store/simulation/useSimulationStore"; import { useBuilderStore } from "../../../store/builder/useBuilderStore"; +import * as CONSTANTS from "../../../types/world/worldConstants"; export default function PostProcessing() { - const { scene } = useThree(); const { selectedPoints } = useSelectedPoints(); const { deletableFloorItem } = useDeletableFloorItem(); const { selectedWallItem } = useSelectedWallItem(); const { selectedFloorItem } = useSelectedFloorItem(); const { selectedEventSphere } = useSelectedEventSphere(); const { deletableEventSphere } = useDeletableEventSphere(); - const { selectedAisle, selectedWall, selectedDecal, selectedFloor, selectedWallAsset, deletableWallAsset } = useBuilderStore(); + const { selectedAisle, selectedWall, selectedDecal, selectedFloor, selectedWallAsset, deletableWallAsset, deletableDecal } = useBuilderStore(); function flattenChildren(children: any[]) { const allChildren: any[] = []; @@ -68,6 +62,10 @@ export default function PostProcessing() { // console.log('selectedPoints: ', selectedPoints); }, [selectedPoints]) + useEffect(() => { + // console.log('deletableDecal: ', deletableDecal); + }, [deletableDecal]) + return ( )} + {deletableDecal && ( + + )} {deletableFloorItem && ( - {vehicles.map((vehicle: VehicleStatus) => ( - - - {viewSceneLabels && } - - ))} - - ); + return ( + <> + {vehicles.map((vehicle: VehicleStatus) => ( + + + {viewSceneLabels && } + + ))} + + ); } export default VehicleInstances; diff --git a/app/src/pages/PageNotFound.tsx b/app/src/pages/PageNotFound.tsx new file mode 100644 index 0000000..3de9ee8 --- /dev/null +++ b/app/src/pages/PageNotFound.tsx @@ -0,0 +1,57 @@ +import { useLocation, useNavigate } from "react-router-dom"; +import text404 from "../assets/image/404/404.svg"; +import hero from "../assets/image/404/404_bk.png"; + +const PageNotFound = () => { + const savedTheme = localStorage.getItem("theme"); + const isLogedIn = localStorage.getItem("userId"); + const navigate = useNavigate(); + const { hash } = useLocation(); + + function getErrorContext() { + const contexts = hash.split("#"); + const context = contexts[1]; + const info = contexts.length > 1 ? contexts[2] : ""; + switch (context) { + case "project_not_found": + return `Project Not found - The project ${ + info !== "" && `with ID (${info})` + } was not found.`; + default: + return "Page not Found - looks like we have hit a roadblock"; + } + } + + return ( +
+
+
+ +
+
+ +
+
{getErrorContext()}
+ +
+
+ ); +}; + +export default PageNotFound; diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index 2ba745c..7fa6e1d 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -43,6 +43,7 @@ const Project: React.FC = () => { useEffect(() => { if (!email || !userId) { console.error("User data not found in localStorage"); + navigate("/page-not-found"); return; } @@ -61,6 +62,7 @@ const Project: React.FC = () => { await viewProject(organization, matchedProject._id, userId); } else { console.warn("Project not found with given ID:", projectId); + navigate(`/not_found#project_not_found#${projectId}`); } } catch (error) { console.error("Error fetching projects:", error); @@ -73,6 +75,7 @@ const Project: React.FC = () => { useEffect(() => { if (!projectId) return; + getVersionHistoryApi(projectId) .then((data) => { const versions: VersionHistory = []; @@ -99,6 +102,7 @@ const Project: React.FC = () => { if (email) { const token = localStorage.getItem("token"); const refreshToken = localStorage.getItem("refreshToken"); + echo.warn('Validating token'); if (token) { useSocketStore .getState() @@ -108,7 +112,7 @@ const Project: React.FC = () => { setOrganization(organization); setUserName(userName); } - echo.success("Log in successful"); + echo.success("Project initialized and loaded successfully"); } else { navigate("/"); } diff --git a/app/src/pages/UserAuth.tsx b/app/src/pages/UserAuth.tsx index 06352a0..4db2358 100644 --- a/app/src/pages/UserAuth.tsx +++ b/app/src/pages/UserAuth.tsx @@ -36,8 +36,6 @@ const UserAuth: React.FC = () => { initializeFingerprint(); }, []); - const { userId, organization } = getUserData(); - const handleLogin = async (e: FormEvent) => { e.preventDefault(); const organization = email.split("@")[1].split(".")[0]; diff --git a/app/src/services/factoryBuilder/asset/assets/getAssetThumbnail.ts b/app/src/services/factoryBuilder/asset/assets/getAssetThumbnail.ts new file mode 100644 index 0000000..77469a0 --- /dev/null +++ b/app/src/services/factoryBuilder/asset/assets/getAssetThumbnail.ts @@ -0,0 +1,33 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + +export const getAssetThumbnail = async (assetId: String) => { + + try { + const response = await fetch( + `${url_Backend_dwinzo}/api/v2/getAssetThumbnail/${assetId}`, + { + method: "GET", + headers: { + Authorization: "Bearer ", + "Content-Type": "application/json", + token: localStorage.getItem("token") || "", + refresh_token: localStorage.getItem("refreshToken") || "", + }, + } + ); + const newAccessToken = response.headers.get("x-access-token"); + if (newAccessToken) { + localStorage.setItem("token", newAccessToken); + } + + if (!response.ok) { + throw new Error("Failed to fetch assets"); + } + + // + return await response.json(); + } catch (error: any) { + echo.error("Failed to get asset image"); + + } +}; diff --git a/app/src/services/factoryBuilder/asset/decals/getCategoryDecals.ts b/app/src/services/factoryBuilder/asset/decals/getCategoryDecals.ts new file mode 100644 index 0000000..9a4f2ba --- /dev/null +++ b/app/src/services/factoryBuilder/asset/decals/getCategoryDecals.ts @@ -0,0 +1,20 @@ +let BackEnd_url = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; +export const getCategoryDecals = async (category: any) => { + try { + const response = await fetch( + `${BackEnd_url}/api/v1/categoryDecalDatas/${category}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + + const result = await response.json(); + return result; + } catch (error: any) { + echo.error("Failed to get category asset"); + console.log(error.message); + } +}; \ No newline at end of file diff --git a/app/src/services/simulation/products/renameProductApi.ts b/app/src/services/simulation/products/renameProductApi.ts index d64f0da..d43997e 100644 --- a/app/src/services/simulation/products/renameProductApi.ts +++ b/app/src/services/simulation/products/renameProductApi.ts @@ -25,6 +25,7 @@ export const renameProductApi = async (body: { if (!response.ok) { console.error("Failed to rename product"); + echo.error("Failed to rename product"); } const result = await response.json(); @@ -32,11 +33,11 @@ export const renameProductApi = async (body: { return result; } catch (error) { echo.error("Failed to rename product Api"); - if (error instanceof Error) { console.log(error.message); } else { console.log("An unknown error occurred"); + echo.log("An unknown error occurred"); } } }; diff --git a/app/src/store/builder/store.ts b/app/src/store/builder/store.ts index 84501dc..82ebe5e 100644 --- a/app/src/store/builder/store.ts +++ b/app/src/store/builder/store.ts @@ -128,6 +128,7 @@ export const useSocketStore = create((set: any, get: any) => ({ // }); // }, // })); + export const useLoadingProgress = create<{ loadingProgress: number; setLoadingProgress: (x: number) => void; @@ -162,6 +163,21 @@ export const useSelectedItem = create((set: any) => ({ setSelectedItem: (x: any) => set(() => ({ selectedItem: x })), })); +type DroppedDecalType = { + category: string; + decalName: string; + decalImage: string; + decalId: string; +}; + +export const useDroppedDecal = create<{ + droppedDecal: DroppedDecalType | null; + setDroppedDecal: (x: DroppedDecalType | null) => void; +}>((set) => ({ + droppedDecal: null, + setDroppedDecal: (x) => set({ droppedDecal: x }), +})); + export const useNavMesh = create((set: any) => ({ navMesh: null, setNavMesh: (x: any) => set({ navMesh: x }), @@ -709,6 +725,6 @@ interface DecalStore { // Create the Zustand store with types export const useDecalStore = create((set) => ({ - selectedSubCategory: null, + selectedSubCategory: 'Safety', setSelectedSubCategory: (subCategory: string | null) => set({ selectedSubCategory: subCategory }), })); diff --git a/app/src/store/builder/useAisleStore.ts b/app/src/store/builder/useAisleStore.ts index 4e9e6ad..1591756 100644 --- a/app/src/store/builder/useAisleStore.ts +++ b/app/src/store/builder/useAisleStore.ts @@ -3,43 +3,42 @@ import { immer } from "zustand/middleware/immer"; interface AisleStore { aisles: Aisles; - setAisles: (aisles: Aisles) => void; - addAisle: (aisle: Aisle) => void; - updateAisle: (uuid: string, updated: Partial) => void; - removeAisle: (uuid: string) => void; + setAisles: (aisles: Aisles) => Aisles; + addAisle: (aisle: Aisle) => Aisle; + updateAisle: (uuid: string, updated: Partial) => Aisle | undefined; + removeAisle: (uuid: string) => Aisle | undefined; removePoint: (uuid: string) => Aisles; - clearAisles: () => void; + clearAisles: () => Aisles; setPosition: ( pointUuid: string, position: [number, number, number] ) => Aisle | undefined; - setLayer: (pointUuid: string, layer: number) => void; - setColor: (aisleUuid: string, color: AisleColors) => void; + setLayer: (pointUuid: string, layer: number) => Aisle | undefined; + setColor: (aisleUuid: string, color: AisleColors) => Aisle | undefined; - // Type-specific setters - setSolidAisleWidth: (aisleUuid: string, width: number) => void; + setSolidAisleWidth: (aisleUuid: string, width: number) => Aisle | undefined; setDashedAisleProperties: ( aisleUuid: string, props: { aisleWidth?: number; dashLength?: number; gapLength?: number } - ) => void; + ) => Aisle | undefined; setDottedAisleProperties: ( aisleUuid: string, props: { dotRadius?: number; gapLength?: number } - ) => void; - setArrowAisleWidth: (aisleUuid: string, width: number) => void; + ) => Aisle | undefined; + setArrowAisleWidth: (aisleUuid: string, width: number) => Aisle | undefined; setArrowsAisleProperties: ( aisleUuid: string, props: { aisleWidth?: number; aisleLength?: number; gapLength?: number } - ) => void; + ) => Aisle | undefined; setArcAisleWidth: ( aisleUuid: string, props: { aisleWidth?: number; isFlipped: boolean } - ) => void; - setCircleAisleWidth: (aisleUuid: string, width: number) => void; + ) => Aisle | undefined; + setCircleAisleWidth: (aisleUuid: string, width: number) => Aisle | undefined; setJunctionAisleProperties: ( aisleUuid: string, props: { aisleWidth?: number; isFlipped: boolean } - ) => void; + ) => Aisle | undefined; getAisleById: (uuid: string) => Aisle | undefined; getAislesByPointId: (uuid: string) => Aisle[] | []; @@ -53,28 +52,43 @@ export const createAisleStore = () => { immer((set, get) => ({ aisles: [], - setAisles: (aisles) => + setAisles: (aisles) => { set((state) => { state.aisles = aisles; - }), + }); + return aisles; + }, - addAisle: (aisle) => + addAisle: (aisle) => { set((state) => { state.aisles.push(aisle); - }), + }); + return aisle; + }, - updateAisle: (uuid, updated) => + updateAisle: (uuid, updated) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === uuid); if (aisle) { Object.assign(aisle, updated); + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - removeAisle: (uuid) => + removeAisle: (uuid) => { + let removedAisle: Aisle | undefined; set((state) => { - state.aisles = state.aisles.filter((a) => a.aisleUuid !== uuid); - }), + const index = state.aisles.findIndex((a) => a.aisleUuid === uuid); + if (index !== -1) { + removedAisle = JSON.parse(JSON.stringify(state.aisles[index])); + state.aisles.splice(index, 1); + } + }); + return removedAisle; + }, removePoint: (uuid) => { const removedAisles: Aisle[] = []; @@ -94,9 +108,11 @@ export const createAisleStore = () => { }, clearAisles: () => { + const clearedAisles = get().aisles; set((state) => { state.aisles = []; - }) + }); + return clearedAisles; }, setPosition: (pointUuid, position) => { @@ -113,34 +129,46 @@ export const createAisleStore = () => { return updatedAisle; }, - setLayer: (pointUuid, layer) => + setLayer: (pointUuid, layer) => { + let updatedAisle: Aisle | undefined; set((state) => { for (const aisle of state.aisles) { const point = aisle.points.find((p) => p.pointUuid === pointUuid); if (point) { point.layer = layer; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } } - }), + }); + return updatedAisle; + }, - setColor: (aisleUuid, color) => + setColor: (aisleUuid, color) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle) { aisle.type.aisleColor = color; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - // Type-specific property setters - setSolidAisleWidth: (aisleUuid, width) => + setSolidAisleWidth: (aisleUuid, width) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle && aisle.type.aisleType === "solid-aisle") { aisle.type.aisleWidth = width; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - setDashedAisleProperties: (aisleUuid, props) => + setDashedAisleProperties: (aisleUuid, props) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle && aisle.type.aisleType === "dashed-aisle") { @@ -150,10 +178,14 @@ export const createAisleStore = () => { aisle.type.dashLength = props.dashLength; if (props.gapLength !== undefined) aisle.type.gapLength = props.gapLength; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - setDottedAisleProperties: (aisleUuid, props) => + setDottedAisleProperties: (aisleUuid, props) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle && aisle.type.aisleType === "dotted-aisle") { @@ -161,18 +193,26 @@ export const createAisleStore = () => { aisle.type.dotRadius = props.dotRadius; if (props.gapLength !== undefined) aisle.type.gapLength = props.gapLength; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - setArrowAisleWidth: (aisleUuid, width) => + setArrowAisleWidth: (aisleUuid, width) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle && aisle.type.aisleType === "arrow-aisle") { aisle.type.aisleWidth = width; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - setArrowsAisleProperties: (aisleUuid, props) => + setArrowsAisleProperties: (aisleUuid, props) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle && aisle.type.aisleType === "arrows-aisle") { @@ -182,10 +222,14 @@ export const createAisleStore = () => { aisle.type.aisleLength = props.aisleLength; if (props.gapLength !== undefined) aisle.type.gapLength = props.gapLength; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - setArcAisleWidth: (aisleUuid, props) => + setArcAisleWidth: (aisleUuid, props) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle && aisle.type.aisleType === "arc-aisle") { @@ -193,18 +237,26 @@ export const createAisleStore = () => { aisle.type.aisleWidth = props.aisleWidth; if (props.isFlipped !== undefined) aisle.type.isFlipped = props.isFlipped; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - setCircleAisleWidth: (aisleUuid, width) => + setCircleAisleWidth: (aisleUuid, width) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle && aisle.type.aisleType === "circle-aisle") { aisle.type.aisleWidth = width; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, - setJunctionAisleProperties: (aisleUuid, props) => + setJunctionAisleProperties: (aisleUuid, props) => { + let updatedAisle: Aisle | undefined; set((state) => { const aisle = state.aisles.find((a) => a.aisleUuid === aisleUuid); if (aisle && aisle.type.aisleType === "junction-aisle") { @@ -212,8 +264,11 @@ export const createAisleStore = () => { aisle.type.aisleWidth = props.aisleWidth; if (props.isFlipped !== undefined) aisle.type.isFlipped = props.isFlipped; + updatedAisle = JSON.parse(JSON.stringify(aisle)); } - }), + }); + return updatedAisle; + }, getAisleById: (uuid) => { return get().aisles.find((a) => a.aisleUuid === uuid); diff --git a/app/src/store/builder/useBuilderStore.ts b/app/src/store/builder/useBuilderStore.ts index 5be4e3b..1606c40 100644 --- a/app/src/store/builder/useBuilderStore.ts +++ b/app/src/store/builder/useBuilderStore.ts @@ -1,4 +1,4 @@ -import { Object3D } from 'three'; +import { Object3D, Vector3 } from 'three'; import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; @@ -38,10 +38,16 @@ interface BuilderState { zoneColor: string; // Decal Settings - selectedDecal: Object3D | null; + selectedDecal: { decalMesh: Object3D | null, decalData: Decal } | null; + deletableDecal: Object3D | null; + decalDragState: { + isDragging: boolean, + draggingDecalUuid: string | null, + dragOffset: Vector3 | null, + }, // Aisle General - selectedAisle: Object3D | null; + selectedAisle: {aisleMesh: Object3D | null, aisleData: Aisle} | null; aisleType: AisleTypes; aisleWidth: number; aisleColor: AisleColors; @@ -86,10 +92,12 @@ interface BuilderState { setZoneColor: (color: string) => void; // Setters - Decal - setSelectedDecal: (decal: Object3D | null) => void; + setSelectedDecal: (decal: { decalMesh: Object3D | null, decalData: Decal } | null) => void; + setDeletableDecal: (decal: Object3D | null) => void; + setDecalDragState: (isDragging: boolean, draggingDecalUuid: string | null, dragOffset: Vector3 | null) => void; // Setters - Aisle General - setSelectedAisle: (aisle: Object3D | null) => void; + setSelectedAisle: (aisle: {aisleMesh: Object3D | null, aisleData: Aisle} | null) => void; setAisleType: (type: AisleTypes) => void; setAisleWidth: (width: number) => void; setAisleColor: (color: AisleColors) => void; @@ -140,11 +148,17 @@ export const useBuilderStore = create()( zoneColor: 'blue', selectedDecal: null, + deletableDecal: null, + decalDragState: { + isDragging: false, + draggingDecalUuid: null, + dragOffset: null, + }, selectedAisle: null, aisleType: 'solid-aisle', aisleWidth: 0.1, - aisleColor: 'yellow', + aisleColor: '#FBE50E', dashLength: 0.5, gapLength: 0.3, @@ -287,15 +301,31 @@ export const useBuilderStore = create()( // === Setters: Decal === - setSelectedDecal: (decal: Object3D | null) => { + setSelectedDecal: (decal: { decalMesh: Object3D | null, decalData: Decal } | null) => { set((state) => { state.selectedDecal = decal; }) }, + setDeletableDecal: (decal: Object3D | null) => { + set((state) => { + state.deletableDecal = decal; + }) + }, + + setDecalDragState: (isDragging: boolean, draggingDecalUuid: string | null, dragOffset: Vector3 | null) => { + set((state) => { + state.decalDragState = { + isDragging, + draggingDecalUuid, + dragOffset, + } + }) + }, + // === Setters: Aisle General === - setSelectedAisle: (aisle: Object3D | null) => { + setSelectedAisle: (aisle: {aisleMesh: Object3D | null, aisleData: Aisle} | null) => { set((state) => { state.selectedAisle = aisle; }); diff --git a/app/src/store/builder/useFloorStore.ts b/app/src/store/builder/useFloorStore.ts index a045884..e875c78 100644 --- a/app/src/store/builder/useFloorStore.ts +++ b/app/src/store/builder/useFloorStore.ts @@ -19,9 +19,9 @@ interface FloorStore { setBevelStrength: (uuid: string, strength: number) => void; setDepth: (uuid: string, depth: number) => void; setMaterial: (uuid: string, sideMaterial: string, topMaterial: string) => void; - addDecal: (floors: string, decal: Decal) => void; - updateDecal: (decalUuid: string, decal: Decal) => void; - removeDecal: (decalUuid: string) => void; + addDecal: (floors: string, decal: Decal) => Decal | undefined; + updateDecal: (decalUuid: string, decal: Partial) => Floor | undefined; + removeDecal: (decalUuid: string) => Floor | undefined; updateDecalPosition: (decalUuid: string, position: [number, number, number]) => void; updateDecalRotation: (decalUuid: string, rotation: number) => void; updateDecalScale: (decalUuid: string, scale: number) => void; @@ -196,28 +196,46 @@ export const createFloorStore = () => { } }), - addDecal: (floorUuid, decal) => set(state => { - const floor = state.floors.find(f => f.floorUuid === floorUuid); - if (floor) { - floor.decals.push(decal); - } - }), - - updateDecal: (decalUuid, updatedDecal) => set(state => { - for (const floor of state.floors) { - const index = floor.decals.findIndex(d => d.decalUuid === decalUuid); - if (index !== -1) { - floor.decals[index] = updatedDecal; - break; + addDecal: (floorUuid, decal) => { + let addedDecal: Decal | undefined; + set(state => { + const floor = state.floors.find(f => f.floorUuid === floorUuid); + if (floor) { + floor.decals.push(decal); + addedDecal = JSON.parse(JSON.stringify(decal)); } - } - }), + }) + return addedDecal; + }, - removeDecal: (decalUuid) => set(state => { - for (const floor of state.floors) { - floor.decals = floor.decals.filter(d => d.decalUuid !== decalUuid); - } - }), + updateDecal: (decalUuid, updatedDecal) => { + let affectedFloor: Floor | undefined; + set(state => { + for (const floor of state.floors) { + const index = floor.decals.findIndex(d => d.decalUuid === decalUuid); + if (index !== -1) { + Object.assign(floor.decals[index], updatedDecal); + affectedFloor = JSON.parse(JSON.stringify(floor)); + break; + } + } + }); + return affectedFloor; + }, + + removeDecal: (decalUuid) => { + let affectedFloor: Floor | undefined; + set(state => { + for (const floor of state.floors) { + const hasDecal = floor.decals.some(d => d.decalUuid === decalUuid); + if (hasDecal) { + floor.decals = floor.decals.filter(d => d.decalUuid !== decalUuid); + affectedFloor = JSON.parse(JSON.stringify(floor)); + } + } + }); + return affectedFloor; + }, updateDecalPosition: (decalUuid, position) => set(state => { for (const floor of state.floors) { diff --git a/app/src/store/builder/useUndoRedo3DStore.ts b/app/src/store/builder/useUndoRedo3DStore.ts index e8c3ce3..d3e2b7b 100644 --- a/app/src/store/builder/useUndoRedo3DStore.ts +++ b/app/src/store/builder/useUndoRedo3DStore.ts @@ -13,6 +13,9 @@ type UndoRedo3DStore = { peekUndo3D: () => UndoRedo3DTypes | undefined; peekRedo3D: () => UndoRedo3DTypes | undefined; + + subscribeUndoRedo: (cb: (action: { type: "undo" | "redo"; entry: UndoRedo3DTypes }) => void) => () => void; + _listeners: ((action: { type: "undo" | "redo"; entry: UndoRedo3DTypes }) => void)[]; }; export const createUndoRedo3DStore = () => { @@ -20,15 +23,14 @@ export const createUndoRedo3DStore = () => { immer((set, get) => ({ undoStack: [], redoStack: [], + _listeners: [], push3D: (entry) => { set((state) => { state.undoStack.push(entry); - if (state.undoStack.length > undoRedoConfig.undoRedoCount) { state.undoStack.shift(); } - state.redoStack = []; }); }, @@ -41,6 +43,9 @@ export const createUndoRedo3DStore = () => { state.redoStack.unshift(lastAction); } }); + if (lastAction) { + get()._listeners.forEach((cb) => cb({ type: "undo", entry: lastAction! })); + } return lastAction; }, @@ -52,6 +57,9 @@ export const createUndoRedo3DStore = () => { state.undoStack.push(redoAction); } }); + if (redoAction) { + get()._listeners.forEach((cb) => cb({ type: "redo", entry: redoAction! })); + } return redoAction; }, @@ -71,8 +79,19 @@ export const createUndoRedo3DStore = () => { const stack = get().redoStack; return stack.length > 0 ? stack[0] : undefined; }, + + subscribeUndoRedo: (cb) => { + set((state) => { + state._listeners.push(cb); + }); + return () => { + set((state) => { + state._listeners = state._listeners.filter((l) => l !== cb); + }); + }; + }, })) ); -} +}; export type UndoRedo3DStoreType = ReturnType; \ No newline at end of file diff --git a/app/src/store/builder/useWallStore.ts b/app/src/store/builder/useWallStore.ts index db40269..705e611 100644 --- a/app/src/store/builder/useWallStore.ts +++ b/app/src/store/builder/useWallStore.ts @@ -9,10 +9,10 @@ interface WallStore { removeWall: (uuid: string) => void; clearWalls: () => void; removeWallByPoints: (Points: [Point, Point]) => Wall | undefined; - addDecal: (wallUuid: string, decal: Decal) => void; - updateDecal: (decalUuid: string, decal: Decal) => void; - removeDecal: (decalUuid: string) => void; - updateDecalPosition: (decalUuid: string, position: [number, number, number]) => void; + addDecal: (wallUuid: string, decal: Decal) => Decal | undefined; + updateDecal: (decalUuid: string, decal: Partial) => Wall | undefined; + removeDecal: (decalUuid: string) => Wall | undefined; + updateDecalPosition: (decalUuid: string, position: [number, number, number]) => Wall | undefined; updateDecalRotation: (decalUuid: string, rotation: number) => void; updateDecalScale: (decalUuid: string, scale: number) => void; @@ -83,37 +83,60 @@ export const createWallStore = () => { return removedWall; }, - addDecal: (wallUuid, decal) => set((state) => { - const wallToUpdate = state.walls.find(w => w.wallUuid === wallUuid); - if (wallToUpdate) { - wallToUpdate.decals.push(decal); - } - }), - - updateDecal: (decalUuid, decal) => set((state) => { - for (const wall of state.walls) { - const decalToUpdate = wall.decals.find(d => d.decalUuid === decalUuid); - if (decalToUpdate) { - Object.assign(decalToUpdate, decal); + addDecal: (wallUuid, decal) => { + let addedDecal: Decal | undefined; + set((state) => { + const wallToUpdate = state.walls.find(w => w.wallUuid === wallUuid); + if (wallToUpdate) { + wallToUpdate.decals.push(decal); + addedDecal = JSON.parse(JSON.stringify(decal)); } - } - }), + }) + return addedDecal; + }, - removeDecal: (decalUuid) => set((state) => { - for (const wall of state.walls) { - wall.decals = wall.decals.filter(d => d.decalUuid !== decalUuid); - } - }), - - updateDecalPosition: (decalUuid, position) => set((state) => { - for (const wall of state.walls) { - const decal = wall.decals.find(d => d.decalUuid === decalUuid); - if (decal) { - decal.decalPosition = position; - break; + updateDecal: (decalUuid, decal) => { + let affectedWall: Wall | undefined; + set((state) => { + for (const wall of state.walls) { + const decalToUpdate = wall.decals.find(d => d.decalUuid === decalUuid); + if (decalToUpdate) { + Object.assign(decalToUpdate, decal); + affectedWall = JSON.parse(JSON.stringify(wall)); + } } - } - }), + }); + return affectedWall; + }, + + removeDecal: (decalUuid) => { + let affectedWall: Wall | undefined; + set((state) => { + for (const wall of state.walls) { + const hasDecal = wall.decals.some(d => d.decalUuid === decalUuid); + if (hasDecal) { + wall.decals = wall.decals.filter(d => d.decalUuid !== decalUuid); + affectedWall = JSON.parse(JSON.stringify(wall)); + } + } + }); + return affectedWall; + }, + + updateDecalPosition: (decalUuid, position) => { + let affectedWall: Wall | undefined; + set((state) => { + for (const wall of state.walls) { + const decal = wall.decals.find(d => d.decalUuid === decalUuid); + if (decal) { + decal.decalPosition = position; + affectedWall = JSON.parse(JSON.stringify(wall)); + break; + } + } + }) + return affectedWall; + }, updateDecalRotation: (decalUuid, rotation) => set((state) => { for (const wall of state.walls) { diff --git a/app/src/styles/abstracts/functions.scss b/app/src/styles/abstracts/_functions.scss similarity index 100% rename from app/src/styles/abstracts/functions.scss rename to app/src/styles/abstracts/_functions.scss diff --git a/app/src/styles/abstracts/mixins.scss b/app/src/styles/abstracts/_mixins.scss similarity index 100% rename from app/src/styles/abstracts/mixins.scss rename to app/src/styles/abstracts/_mixins.scss diff --git a/app/src/styles/abstracts/variables.scss b/app/src/styles/abstracts/_variables.scss similarity index 100% rename from app/src/styles/abstracts/variables.scss rename to app/src/styles/abstracts/_variables.scss diff --git a/app/src/styles/base/base.scss b/app/src/styles/base/_base.scss similarity index 100% rename from app/src/styles/base/base.scss rename to app/src/styles/base/_base.scss diff --git a/app/src/styles/base/global.scss b/app/src/styles/base/_global.scss similarity index 100% rename from app/src/styles/base/global.scss rename to app/src/styles/base/_global.scss diff --git a/app/src/styles/base/reset.scss b/app/src/styles/base/_reset.scss similarity index 100% rename from app/src/styles/base/reset.scss rename to app/src/styles/base/_reset.scss diff --git a/app/src/styles/base/typography.scss b/app/src/styles/base/_typography.scss similarity index 100% rename from app/src/styles/base/typography.scss rename to app/src/styles/base/_typography.scss diff --git a/app/src/styles/components/button.scss b/app/src/styles/components/_button.scss similarity index 100% rename from app/src/styles/components/button.scss rename to app/src/styles/components/_button.scss diff --git a/app/src/styles/components/confirmationPopUp.scss b/app/src/styles/components/_confirmationPopUp.scss similarity index 100% rename from app/src/styles/components/confirmationPopUp.scss rename to app/src/styles/components/_confirmationPopUp.scss diff --git a/app/src/styles/components/form.scss b/app/src/styles/components/_form.scss similarity index 100% rename from app/src/styles/components/form.scss rename to app/src/styles/components/_form.scss diff --git a/app/src/styles/components/input.scss b/app/src/styles/components/_input.scss similarity index 100% rename from app/src/styles/components/input.scss rename to app/src/styles/components/_input.scss diff --git a/app/src/styles/components/lists.scss b/app/src/styles/components/_lists.scss similarity index 100% rename from app/src/styles/components/lists.scss rename to app/src/styles/components/_lists.scss diff --git a/app/src/styles/components/moduleToggle.scss b/app/src/styles/components/_moduleToggle.scss similarity index 100% rename from app/src/styles/components/moduleToggle.scss rename to app/src/styles/components/_moduleToggle.scss diff --git a/app/src/styles/components/templates.scss b/app/src/styles/components/_templates.scss similarity index 100% rename from app/src/styles/components/templates.scss rename to app/src/styles/components/_templates.scss diff --git a/app/src/styles/components/tools.scss b/app/src/styles/components/_tools.scss similarity index 100% rename from app/src/styles/components/tools.scss rename to app/src/styles/components/_tools.scss diff --git a/app/src/styles/components/footer/footer.scss b/app/src/styles/components/footer/_footer.scss similarity index 93% rename from app/src/styles/components/footer/footer.scss rename to app/src/styles/components/footer/_footer.scss index 152a39f..0b490ee 100644 --- a/app/src/styles/components/footer/footer.scss +++ b/app/src/styles/components/footer/_footer.scss @@ -63,7 +63,7 @@ } .bg-dummy.right-bottom { - right: 68px; + right: 84px; bottom: 0; width: 20%; height: 100%; @@ -79,7 +79,8 @@ } .logs-detail, - .version { + .version, + .wifi-connection { @include flex-center; border-radius: #{$border-radius-extra-large}; padding: 3px 6px; @@ -107,7 +108,28 @@ } } - .version { + .wifi-connection { + .tooltip { + transform: translateX(-16px); + &::after { + left: 76%; + } + } + &:hover { + .tooltip { + opacity: 1; + } + } + &.connected svg path { + fill: #1ec018; + } + &.disconnected svg path { + fill: #e44405; + } + } + + .version, + .wifi-connection { background: var(--background-color); font-size: var(--font-size-tiny); @@ -179,7 +201,7 @@ background: var(--background-color); border-radius: #{$border-radius-medium}; outline: 1px solid var(--border-color); - &:hover{ + &:hover { background: var(--background-color-solid); } } @@ -288,7 +310,7 @@ font-family: monospace; font-size: var(--font-size-tiny); color: var(--icon-default-color-active); - &:last-child{ + &:last-child { background: var(--background-color-button); } } diff --git a/app/src/styles/components/logs/logs.scss b/app/src/styles/components/logs/_logs.scss similarity index 100% rename from app/src/styles/components/logs/logs.scss rename to app/src/styles/components/logs/_logs.scss diff --git a/app/src/styles/components/marketPlace/marketPlace.scss b/app/src/styles/components/marketPlace/_marketPlace.scss similarity index 100% rename from app/src/styles/components/marketPlace/marketPlace.scss rename to app/src/styles/components/marketPlace/_marketPlace.scss diff --git a/app/src/styles/components/menu/menu.scss b/app/src/styles/components/menu/_menu.scss similarity index 100% rename from app/src/styles/components/menu/menu.scss rename to app/src/styles/components/menu/_menu.scss diff --git a/app/src/styles/components/simulation/analysis.scss b/app/src/styles/components/simulation/_analysis.scss similarity index 100% rename from app/src/styles/components/simulation/analysis.scss rename to app/src/styles/components/simulation/_analysis.scss diff --git a/app/src/styles/components/simulation/simulation.scss b/app/src/styles/components/simulation/_simulation.scss similarity index 100% rename from app/src/styles/components/simulation/simulation.scss rename to app/src/styles/components/simulation/_simulation.scss diff --git a/app/src/styles/components/visualization/floating/common.scss b/app/src/styles/components/visualization/floating/_common.scss similarity index 100% rename from app/src/styles/components/visualization/floating/common.scss rename to app/src/styles/components/visualization/floating/_common.scss diff --git a/app/src/styles/components/visualization/floating/energyConsumed.scss b/app/src/styles/components/visualization/floating/_energyConsumed.scss similarity index 100% rename from app/src/styles/components/visualization/floating/energyConsumed.scss rename to app/src/styles/components/visualization/floating/_energyConsumed.scss diff --git a/app/src/styles/components/visualization/ui/styledWidgets.scss b/app/src/styles/components/visualization/ui/_styledWidgets.scss similarity index 100% rename from app/src/styles/components/visualization/ui/styledWidgets.scss rename to app/src/styles/components/visualization/ui/_styledWidgets.scss diff --git a/app/src/styles/layout/compareLayout.scss b/app/src/styles/layout/_compareLayout.scss similarity index 100% rename from app/src/styles/layout/compareLayout.scss rename to app/src/styles/layout/_compareLayout.scss diff --git a/app/src/styles/layout/compareLayoutPopUp.scss b/app/src/styles/layout/_compareLayoutPopUp.scss similarity index 100% rename from app/src/styles/layout/compareLayoutPopUp.scss rename to app/src/styles/layout/_compareLayoutPopUp.scss diff --git a/app/src/styles/layout/loading.scss b/app/src/styles/layout/_loading.scss similarity index 100% rename from app/src/styles/layout/loading.scss rename to app/src/styles/layout/_loading.scss diff --git a/app/src/styles/layout/popup.scss b/app/src/styles/layout/_popup.scss similarity index 100% rename from app/src/styles/layout/popup.scss rename to app/src/styles/layout/_popup.scss diff --git a/app/src/styles/layout/resourceManagement.scss b/app/src/styles/layout/_resourceManagement.scss similarity index 97% rename from app/src/styles/layout/resourceManagement.scss rename to app/src/styles/layout/_resourceManagement.scss index 131220f..48d768c 100644 --- a/app/src/styles/layout/resourceManagement.scss +++ b/app/src/styles/layout/_resourceManagement.scss @@ -114,29 +114,34 @@ .user-image-wrapper { width: 28px; height: 28px; - border-radius: 50%; - background-color: #fff; position: relative; + .user-image { + height: 100%; + width: 100%; + border-radius: 50%; + background-color: #fff; + overflow: hidden; + // transform: translate(-26px, -12px); + } .status { border-radius: 50%; width: 6px; height: 6px; - outline: 1px solid #2f2c32; - + outline: 2px solid var(--background-color-solid); position: absolute; bottom: 0; right: 0; &.Active { - background-color: #44e5c6; + background-color: #1d9419; } } } .details { max-width: 144px; - .input-value{ + .input-value { max-width: 120px; } .employee-id { diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/_sidebar.scss similarity index 92% rename from app/src/styles/layout/sidebar.scss rename to app/src/styles/layout/_sidebar.scss index aa8e84e..34f5b6d 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/_sidebar.scss @@ -1500,6 +1500,28 @@ border-radius: #{$border-radius-large}; margin-right: 4px; overflow: hidden; + &.yellow-black { + background-color: black; + background-size: 10px 10px; + background-image: repeating-linear-gradient( + 45deg, + #FBE50E 0, + #FBE50E 2px, + black 0, + black 50% + ); + } + &.white-black { + background-color: black; + background-size: 10px 10px; + background-image: repeating-linear-gradient( + 45deg, + white 0, + white 2px, + black 0, + black 50% + ); + } } .aisle-color { @@ -1984,17 +2006,51 @@ max-height: 50vh; overflow: auto; - h2, - .searched-content { + .header, + .searched-content, + .categories-header { color: var(--text-color); - font-family: $large; font-weight: $bold-weight; - padding: 8px; + padding: 4px 8px; @include flex-space-between; - - .back-button { - cursor: pointer; + position: sticky; + top: 0; + z-index: 4; + background: var(--background-color); + border-radius: 0 0 12px 12px; + backdrop-filter: blur(4px); + .search-for { + display: inline-block; + color: var(--accent-color); + max-width: 238px; + overflow: hidden; + text-overflow: ellipsis; } + .back-button { + padding: 4px 12px; + border-radius: 20px; + transition: background 0.2s; + display: flex; + align-items: center; + .back-arrow { + margin-right: 2px; + transform: translateX(0) translateY(-1px) scale(1.6) rotate(90deg); + transition: all 0.2s; + } + &:hover { + background: var(--background-color-solid); + .back-arrow { + transform: translateX(-2px) translateY(-1px) scale(1.6) + rotate(90deg); + } + } + } + } + + .categories-header, + .searched-content { + position: relative; + padding: 8px; } .categories-container { @@ -2132,6 +2188,7 @@ border: 1px solid #564b69; padding: 12px 10px; border-radius: 15px; + margin-bottom: 4px; .catogory-asset-filter-wrapper { display: flex; diff --git a/app/src/styles/layout/skeleton.scss b/app/src/styles/layout/_skeleton.scss similarity index 100% rename from app/src/styles/layout/skeleton.scss rename to app/src/styles/layout/_skeleton.scss diff --git a/app/src/styles/layout/toast.scss b/app/src/styles/layout/_toast.scss similarity index 100% rename from app/src/styles/layout/toast.scss rename to app/src/styles/layout/_toast.scss diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss index cc59a33..6da5352 100644 --- a/app/src/styles/main.scss +++ b/app/src/styles/main.scss @@ -45,6 +45,7 @@ @use "pages/realTimeViz"; @use "pages/userAuth"; @use "pages/forgotPassword"; +@use "pages/pageNotFound.scss"; // @use "./scene/scene"; diff --git a/app/src/styles/pages/dashboard.scss b/app/src/styles/pages/_dashboard.scss similarity index 88% rename from app/src/styles/pages/dashboard.scss rename to app/src/styles/pages/_dashboard.scss index 5a628a4..cfb2770 100644 --- a/app/src/styles/pages/dashboard.scss +++ b/app/src/styles/pages/_dashboard.scss @@ -144,7 +144,8 @@ } .cards-container { - height: 100%; + height: auto; + max-height: 100%; display: flex; flex-wrap: wrap; position: relative; @@ -238,38 +239,6 @@ } } - .kebab-options-wrapper { - position: absolute; - bottom: 40px; - right: 40px; - background: var(--background-color); - border: 1px solid var(--border-color); - border-radius: 8px; - z-index: 100; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - backdrop-filter: blur(20px); - flex-direction: column; - transform: translate(100%, 100%); - overflow: hidden; - display: none; - - .option { - padding: 8px 12px; - font-size: 14px; - text-align: left; - background: transparent; - border: none; - color: var(--text-color); - cursor: pointer; - transition: background 0.2s ease; - text-transform: capitalize; - - &:hover { - background-color: var(--background-color-selected); - } - } - } - &:hover { overflow: visible; @@ -336,4 +305,37 @@ font-family: #{$font-roboto}; cursor: pointer; } -} \ No newline at end of file +} + +.kebab-options-wrapper { + // position: absolute; + // bottom: 40px; + // right: 40px; + // z-index: 100; + background: var(--background-color); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(20px); + flex-direction: column; + // transform: translate(100%, 100%); + overflow: hidden; + display: flex; + flex-direction: column; + transform: translate(0%, 0%); + .option { + padding: 8px 12px; + font-size: 14px; + text-align: left; + background: transparent; + border: none; + color: var(--text-color); + cursor: pointer; + transition: background 0.2s ease; + text-transform: capitalize; + + &:hover { + background-color: var(--background-color-selected); + } + } +} diff --git a/app/src/styles/pages/forgotPassword.scss b/app/src/styles/pages/_forgotPassword.scss similarity index 100% rename from app/src/styles/pages/forgotPassword.scss rename to app/src/styles/pages/_forgotPassword.scss diff --git a/app/src/styles/pages/home.scss b/app/src/styles/pages/_home.scss similarity index 100% rename from app/src/styles/pages/home.scss rename to app/src/styles/pages/_home.scss diff --git a/app/src/styles/pages/_pageNotFound.scss b/app/src/styles/pages/_pageNotFound.scss new file mode 100644 index 0000000..f185f47 --- /dev/null +++ b/app/src/styles/pages/_pageNotFound.scss @@ -0,0 +1,56 @@ +@use "../abstracts/variables" as *; +@use "../abstracts/mixins" as *; + +.page-not-found-wrapper { + height: 100vh; + width: 100vw; + background: var(--background-color); + .page-not-found-container { + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 30px; + &::after { + content: ""; + position: absolute; + background: var(--faint-gradient-color); + height: 100vh; + width: 100vw; + top: 0; + left: 0; + z-index: -1; + } + .text-404 { + height: 10vh; + img { + height: 100%; + width: 100%; + } + } + .hero-container { + height: 50vh; + img { + height: 100%; + width: 100%; + } + } + .context { + color: var(--text-color); + font-size: 1rem; + } + .back-to-home { + background: var(--background-color-button); + color: var(--text-button-color); + padding: 8px 16px; + border-radius: 30px; + cursor: pointer; + transition: all 0.2s; + &:hover { + transform: translateY(-2px); + } + } + } +} diff --git a/app/src/styles/pages/realTimeViz.scss b/app/src/styles/pages/_realTimeViz.scss similarity index 100% rename from app/src/styles/pages/realTimeViz.scss rename to app/src/styles/pages/_realTimeViz.scss diff --git a/app/src/styles/pages/userAuth.scss b/app/src/styles/pages/_userAuth.scss similarity index 100% rename from app/src/styles/pages/userAuth.scss rename to app/src/styles/pages/_userAuth.scss diff --git a/app/src/styles/scene/comments.scss b/app/src/styles/scene/_comments.scss similarity index 100% rename from app/src/styles/scene/comments.scss rename to app/src/styles/scene/_comments.scss diff --git a/app/src/styles/scene/cursors.scss b/app/src/styles/scene/_cursors.scss similarity index 100% rename from app/src/styles/scene/cursors.scss rename to app/src/styles/scene/_cursors.scss diff --git a/app/src/styles/scene/scene.scss b/app/src/styles/scene/_scene.scss similarity index 100% rename from app/src/styles/scene/scene.scss rename to app/src/styles/scene/_scene.scss diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts index 28e05bf..b0148bc 100644 --- a/app/src/types/builderTypes.d.ts +++ b/app/src/types/builderTypes.d.ts @@ -104,6 +104,7 @@ interface Decal { decalType: WallDecal | FloorDecal; decalPosition: [number, number, number]; decalRotation: number; + decalOpacity: number; decalScale: number; } @@ -166,7 +167,7 @@ type Zones = Zone[]; type AisleTypes = 'solid-aisle' | 'dashed-aisle' | 'stripped-aisle' | 'dotted-aisle' | 'arrow-aisle' | 'arrows-aisle' | 'arc-aisle' | 'circle-aisle' | 'junction-aisle'; -type AisleColors = 'gray' | 'yellow' | 'green' | 'orange' | 'blue' | 'purple' | 'red' | '#66FF00' | 'yellow-black' | 'white-black' +type AisleColors = '#6F6F7A' | '#FBE50E' | '#43C06D' | '#FF711B' | '#488EF6' | '#AF52DE' | '#FF3B30' | '#66FF00' | 'yellow-black' | 'white-black' interface SolidAisle { aisleType: 'solid-aisle'; diff --git a/app/src/types/uiTypes.d.ts b/app/src/types/uiTypes.d.ts new file mode 100644 index 0000000..16cb72c --- /dev/null +++ b/app/src/types/uiTypes.d.ts @@ -0,0 +1,29 @@ +interface AssetProp { + filename: string; + thumbnail?: string; + category: string; + description?: string; + tags: string; + url?: string; + uploadDate?: number; + isArchieve?: boolean; + animated?: boolean; + price?: number; + CreatedBy?: string; + AssetID?: string; + type?: string; + subType?: string; +} + +interface DecalProp { + id: string; + decalName: string; + decalImage: string; + category: string; + type?: string; +} + +interface CategoryListProp { + categoryImage: string; + category: string; +} \ No newline at end of file diff --git a/app/src/utils/indexDB/idbUtils.ts b/app/src/utils/indexDB/idbUtils.ts index 24b1448..79910bb 100644 --- a/app/src/utils/indexDB/idbUtils.ts +++ b/app/src/utils/indexDB/idbUtils.ts @@ -1,15 +1,19 @@ -const DB_NAME = 'GLTFStorage'; -const STORE_NAME = 'models'; const DB_VERSION = 1; -export function initializeDB(): Promise { +const ASSET_DB_NAME = 'GLTFStorage'; +const ASSET_STORE_NAME = 'models'; + +const IMAGE_DB_NAME = 'ImageStorage'; +const IMAGE_STORE_NAME = 'images'; + +export function initializeAssetDB(): Promise { return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); + const request = indexedDB.open(ASSET_DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const db = request.result; - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME); + if (!db.objectStoreNames.contains(ASSET_STORE_NAME)) { + db.createObjectStore(ASSET_STORE_NAME); } }; @@ -19,11 +23,11 @@ export function initializeDB(): Promise { } export async function storeGLTF(key: string, file: Blob): Promise { - const db = await initializeDB(); + const db = await initializeAssetDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction(STORE_NAME, 'readwrite'); - const store = transaction.objectStore(STORE_NAME); + const transaction = db.transaction(ASSET_STORE_NAME, 'readwrite'); + const store = transaction.objectStore(ASSET_STORE_NAME); const request = store.put(file, key); request.onsuccess = () => resolve(); @@ -32,11 +36,53 @@ export async function storeGLTF(key: string, file: Blob): Promise { } export async function retrieveGLTF(key: string): Promise { - const db = await initializeDB(); + const db = await initializeAssetDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction(STORE_NAME, 'readonly'); - const store = transaction.objectStore(STORE_NAME); + const transaction = db.transaction(ASSET_STORE_NAME, 'readonly'); + const store = transaction.objectStore(ASSET_STORE_NAME); + const request = store.get(key); + + request.onsuccess = () => resolve(request.result as Blob | undefined); + request.onerror = () => reject(request.error); + }); +} + +export function initializeImageDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(IMAGE_DB_NAME, DB_VERSION); + + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(IMAGE_STORE_NAME)) { + db.createObjectStore(IMAGE_STORE_NAME); + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +export async function storeImage(key: string, file: Blob): Promise { + const db = await initializeImageDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(IMAGE_STORE_NAME, 'readwrite'); + const store = transaction.objectStore(IMAGE_STORE_NAME); + const request = store.put(file, key); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +export async function retrieveImage(key: string): Promise { + const db = await initializeImageDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(IMAGE_STORE_NAME, 'readonly'); + const store = transaction.objectStore(IMAGE_STORE_NAME); const request = store.get(key); request.onsuccess = () => resolve(request.result as Blob | undefined);