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/components/icons/AssetTypeIcons.tsx b/app/src/components/icons/AssetTypeIcons.tsx new file mode 100644 index 0000000..2f32358 --- /dev/null +++ b/app/src/components/icons/AssetTypeIcons.tsx @@ -0,0 +1,353 @@ +export const ForkLiftIcon = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const ConveyorIcon = () => { + return ( + + + + + + + + + + + + + + + ); +}; + +export const RoboticArmIcon = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +export const MachineIcon = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +type TypeBasedAssetIconsProps = { + assetType: string; +}; + +export function TypeBasedAssetIcons({ assetType }: TypeBasedAssetIconsProps) { + console.log("assetType: ", assetType); + return ( +
+ {assetType === "machine" && } + {assetType === "vehicle" && } + {assetType === "transfer" && } + {assetType === "roboticArm" && } +
+ ); +} 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 { setDroppedDecal } = useDroppedDecal(); const [searchValue, setSearchValue] = useState(""); const [selectedCategory, setSelectedCategory] = useState(null); const [categoryAssets, setCategoryAssets] = useState([]); @@ -146,7 +151,12 @@ const Assets: React.FC = () => { echo.error("failed to fetch assets"); setisLoading(false); } + + if (asset === "Decals") { + fetchCategoryDecals("Safety"); + } }; + const fetchCategoryDecals = async (asset: any) => { setisLoading(true); // setSelectedCategory(asset); @@ -185,7 +195,10 @@ const Assets: React.FC = () => {
-

Results for {searchValue}

+

+ Results for{" "} + '{searchValue}' +

{selectedCategory == "Decals" ? ( @@ -291,7 +304,7 @@ const Assets: React.FC = () => { if (selectedCategory) { return (
-

+

{selectedCategory}

@@ -327,7 +343,6 @@ const Assets: React.FC = () => {
)} - {selectedCategory !== "Decals" && !selectedSubCategory ? (
{categoryAssets?.map((asset: any, index: number) => ( @@ -386,15 +401,11 @@ const Assets: React.FC = () => { alt={asset.decalName} className="asset-image" onPointerDown={() => { - setSelectedItem({ - name: asset.decalName, - id: asset.id, - type: - asset.type === "undefined" - ? undefined - : asset.type, + setDroppedDecal({ category: asset.category, - // subType: asset.subType, + decalName: asset.decalName, + decalImage: asset.decalImage, + decalId: asset.id, }); }} /> @@ -423,7 +434,7 @@ const Assets: React.FC = () => { return (
-

Categories

+

Categories

{Array.from( new Set(categoryList.map((asset) => asset.category)) diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx index 0041aaf..5e283c9 100644 --- a/app/src/components/layout/sidebarRight/SideBarRight.tsx +++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx @@ -1,29 +1,13 @@ 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 GlobalProperties from "./properties/GlobalProperties"; import AssetProperties from "./properties/AssetProperties"; @@ -35,319 +19,281 @@ 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"; type DisplayComponent = - | "versionHistory" - | "globalProperties" - | "aisleProperties" - | "wallProperties" - | "floorProperties" - | "assetProperties" - | "selectedWallProperties" - | "selectedFloorProperties" - | "zoneProperties" - | "simulations" - | "mechanics" - | "analysis" - | "visualization" - | "selectedDecalProperties" - | "resourceManagement" - | "none"; + | "versionHistory" + | "globalProperties" + | "aisleProperties" + | "wallProperties" + | "floorProperties" + | "assetProperties" + | "selectedWallProperties" + | "selectedFloorProperties" + | "selectedDecalProperties" + | "selectedAisleProperties" + | "zoneProperties" + | "simulations" + | "mechanics" + | "analysis" + | "visualization" + | "resourceManagement" + | "none"; const SideBarRight: React.FC = () => { - const { activeModule } = useModuleStore(); - const { toggleUIRight } = useToggleStore(); - const { toolMode } = useToolMode(); - const { subModule, setSubModule } = useSubModuleStore(); - const { selectedFloorItem } = useSelectedFloorItem(); - const { selectedWall, selectedFloor, selectedAisle } = useBuilderStore(); - const { selectedEventData } = useSelectedEventData(); - const { selectedEventSphere } = useSelectedEventSphere(); - const { viewVersionHistory, setVersionHistoryVisible } = - useVersionHistoryVisibleStore(); - const { isVersionSaved } = useSaveVersion(); - const { selectedSubCategory } = useDecalStore(); + const { selectedDecal } = useBuilderStore(); + const { activeModule } = useModuleStore(); + const { toggleUIRight } = useToggleStore(); + const { toolMode } = useToolMode(); + const { subModule, setSubModule } = useSubModuleStore(); + const { selectedFloorItem } = useSelectedFloorItem(); + const { selectedWall, selectedFloor, selectedAisle } = useBuilderStore(); + const { selectedEventData } = useSelectedEventData(); + const { selectedEventSphere } = useSelectedEventSphere(); + const { viewVersionHistory, setVersionHistoryVisible } = useVersionHistoryVisibleStore(); + const { isVersionSaved } = useSaveVersion(); - 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 === "visualization") { - setDisplayComponent("visualization"); - return; - } - - if (!isVersionSaved && activeModule === "simulation") { - if (subModule === "simulations") { - setDisplayComponent("simulations"); - return; - } - if (subModule === "mechanics") { - setDisplayComponent("mechanics"); - return; - } - if (subModule === "analysis") { - setDisplayComponent("analysis"); - return; - } - if (subModule === "resourceManagement") { - setDisplayComponent("resourceManagement"); - return; - } - } - - if (activeModule === "simulation" || activeModule === "builder") { - if (subModule === "resourceManagement") { - setDisplayComponent("resourceManagement"); - 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; + useEffect(() => { + if (activeModule !== "mechanics" && selectedEventData && selectedEventSphere) { + setSubModule("mechanics"); + } else if (!selectedEventData && !selectedEventSphere) { + if (activeModule === "simulation") { + setSubModule("simulations"); + } } - if (toolMode === "Wall") { - setDisplayComponent("wallProperties"); - return; + if (activeModule !== "simulation") { + setSubModule("properties"); } - if (toolMode === "Floor") { - setDisplayComponent("floorProperties"); - return; + }, [activeModule, selectedEventData, selectedEventSphere, setSubModule]); + + useEffect(() => { + if (activeModule === "visualization") { + setDisplayComponent("visualization"); + return; } - setDisplayComponent("globalProperties"); - return; - } - } - if ( - subModule === "zoneProperties" && - (activeModule === "builder" || activeModule === "simulation") - ) { - setDisplayComponent("zoneProperties"); - return; - } + if (!isVersionSaved && activeModule === "simulation") { + if (subModule === "simulations") { + setDisplayComponent("simulations"); + return; + } + if (subModule === "mechanics") { + setDisplayComponent("mechanics"); + return; + } + if (subModule === "analysis") { + setDisplayComponent("analysis"); + return; + } + if (subModule === "resourceManagement") { + setDisplayComponent("resourceManagement"); + return; + } + } - setDisplayComponent("none"); - }, [ - viewVersionHistory, - activeModule, - subModule, - isVersionSaved, - selectedFloorItem, - selectedWall, - selectedFloor, - selectedAisle, - toolMode, - selectedSubCategory, - ]); + if (activeModule === "simulation" || activeModule === "builder") { + if (subModule === "resourceManagement") { + setDisplayComponent("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 "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 ; - default: - return null; - } - }; + 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; + } + } - return ( -
-
- {toggleUIRight && ( - <> - {(!isVersionSaved || activeModule !== "simulation") && ( -
- {activeModule !== "simulation" && ( + if (subModule === "zoneProperties" && (activeModule === "builder" || activeModule === "simulation")) { + setDisplayComponent("zoneProperties"); + return; + } + + setDisplayComponent("none"); + }, [viewVersionHistory, activeModule, subModule, isVersionSaved, selectedFloorItem, selectedWall, selectedFloor, selectedAisle, toolMode, selectedDecal]); + + 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 ; + default: + return null; + } + }; + + 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..e605363 --- /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/assetManagement/AssetManagement.tsx b/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx index faa6015..718915c 100644 --- a/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx +++ b/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx @@ -1,18 +1,19 @@ import { useEffect, useState } from 'react' // import NavigateCatagory from '../../NavigateCatagory' -import { EyeIcon, ForkLiftIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons'; +import { EyeIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons'; import assetImage 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 { 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 { getProductById } = productStore(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { setResourceManagementId } = useResourceManagementId(); @@ -30,6 +31,7 @@ const AssetManagement = () => { grouped[asset.modelName] = { id: asset.modelUuid, name: asset.modelName, + type: asset.type, model: asset.modelCode || "N/A", status: asset.status || "Online", usageRate: asset.usageRate || 15, @@ -46,6 +48,7 @@ const AssetManagement = () => { setAssets(Object.values(grouped)); } + // eslint-disable-next-line }, [selectedProduct]); function handleRenameAsset(newName: string) { @@ -130,7 +133,7 @@ const AssetManagement = () => { : -
+
}
@@ -210,16 +213,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/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..35b54ce --- /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: 1, + } + + 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: 1, + } + + 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 2f5f77c..ca1b406 100644 --- a/app/src/modules/builder/asset/models/model/model.tsx +++ b/app/src/modules/builder/asset/models/model/model.tsx @@ -88,6 +88,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) => { @@ -103,6 +107,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere clone.animations = cachedModel.animations || []; setGltfScene(clone); calculateBoundingBox(clone); + logModelStatus(assetId, 'cache-loaded'); return; } @@ -118,6 +123,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) => { @@ -140,6 +146,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/hooks/useCameraShortcuts.ts b/app/src/modules/scene/camera/shortcutsControls/cameraShortcutsControls.tsx similarity index 50% rename from app/src/hooks/useCameraShortcuts.ts rename to app/src/modules/scene/camera/shortcutsControls/cameraShortcutsControls.tsx index e4b897a..a073e40 100644 --- a/app/src/hooks/useCameraShortcuts.ts +++ b/app/src/modules/scene/camera/shortcutsControls/cameraShortcutsControls.tsx @@ -3,20 +3,24 @@ import { useThree } from "@react-three/fiber"; import * as THREE from "three"; import type { CameraControls } from "@react-three/drei"; -export const useCameraShortcuts = (controlsRef: React.RefObject) => { - const { camera } = useThree(); +const CameraShortcutsControls = () => { + const { camera, controls } = useThree(); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (!controlsRef.current) return; + if (!controls) return; - // get current distance from camera to target + const cc = controls as CameraControls; + + // get current target const target = new THREE.Vector3(); - controlsRef.current.getTarget(target); + 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(); + switch (e.key) { case "1": // Front pos = new THREE.Vector3(0, 0, distance).add(target); @@ -27,13 +31,24 @@ export const useCameraShortcuts = (controlsRef: React.RefObject) 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); + 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) { - controlsRef.current.setLookAt( + cc.setLookAt( pos.x, pos.y, pos.z, // camera position target.x, target.y, target.z, // keep same target true // smooth transition @@ -43,5 +58,9 @@ export const useCameraShortcuts = (controlsRef: React.RefObject) window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [controlsRef, camera]); + }, [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/moveControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx index e0940c9..248ec9d 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.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 { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; @@ -12,6 +12,7 @@ import { useProductContext } from "../../../../simulation/products/productContex import { getUserData } from "../../../../../functions/getUserData"; import { useSceneContext } from "../../../sceneContext"; import { useVersionContext } from "../../../../builder/version/versionContext"; +import useModuleStore from "../../../../../store/useModuleStore"; // import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; @@ -20,6 +21,8 @@ function MoveControls3D({ boundingBoxRef }: any) { const plane = useMemo(() => 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 && ( ((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 }), @@ -663,6 +679,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/layout/sidebar.scss b/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/pages/dashboard.scss b/app/src/styles/pages/dashboard.scss index 5a628a4..01024d0 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; diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts index 3487fc6..7d5eefb 100644 --- a/app/src/types/builderTypes.d.ts +++ b/app/src/types/builderTypes.d.ts @@ -88,6 +88,7 @@ interface Decal { decalType: WallDecal | FloorDecal; decalPosition: [number, number, number]; decalRotation: number; + decalOpacity: number; decalScale: number; } @@ -150,7 +151,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/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);