From 7b704cc3a9625060b32f55e421663d9f191bc19a Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Wed, 8 Oct 2025 10:37:30 +0530 Subject: [PATCH 1/2] integerated asset group create api --- .../layout/sidebarLeft/SideBarLeft.tsx | 9 +- .../ui/list/OutlineList/AssetOutline.tsx} | 174 ++++-------------- .../builder/wall/wallCreator/wallCreator.tsx | 4 +- .../controls/assetControls/groupControls.tsx | 38 ++-- .../group/assetGroup/createAssetGroupApi.ts | 39 ++-- .../group/assetGroup/getAssetGroupsApi.ts | 2 +- 6 files changed, 95 insertions(+), 171 deletions(-) rename app/src/{modules/builder/testUi/outline.tsx => components/ui/list/OutlineList/AssetOutline.tsx} (84%) diff --git a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx index 20dd25d..c54d9b2 100644 --- a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx +++ b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import ToggleHeader from "../../ui/inputs/ToggleHeader"; -// import Outline from "./Outline"; import Header from "./Header"; import { useToggleStore } from "../../../store/ui/useUIToggleStore"; import Assets from "./assetList/Assets"; @@ -9,7 +8,7 @@ import Widgets from "./visualization/widgets/Widgets"; import Templates from "../../../modules/visualization/template/Templates"; import Search from "../../ui/inputs/Search"; import { useIsComparing } from "../../../store/builder/store"; -import { Outline } from "../../../modules/builder/testUi/outline"; +import { AssetOutline } from "../../ui/list/OutlineList/AssetOutline"; const SideBarLeft: React.FC = () => { const [activeOption, setActiveOption] = useState("Widgets"); @@ -54,7 +53,7 @@ const SideBarLeft: React.FC = () => { return ( <> -
{activeOption === "Outline" ? : }
+
{activeOption === "Outline" ? : }
); } else { @@ -63,7 +62,7 @@ const SideBarLeft: React.FC = () => { {!isComparing && ( <> -
{activeOption === "Outline" ? : }
+
{activeOption === "Outline" ? : }
)} @@ -78,4 +77,4 @@ const SideBarLeft: React.FC = () => { export default SideBarLeft; -// sidebar-left-container opemn close sidebar-left-container smoothly +// sidebar-left-container open close sidebar-left-container smoothly diff --git a/app/src/modules/builder/testUi/outline.tsx b/app/src/components/ui/list/OutlineList/AssetOutline.tsx similarity index 84% rename from app/src/modules/builder/testUi/outline.tsx rename to app/src/components/ui/list/OutlineList/AssetOutline.tsx index d980df8..c99d2e9 100644 --- a/app/src/modules/builder/testUi/outline.tsx +++ b/app/src/components/ui/list/OutlineList/AssetOutline.tsx @@ -1,25 +1,15 @@ import { useState, useRef, DragEvent, useCallback } from "react"; import { useParams } from "react-router-dom"; -import { - EyeIcon, - LockIcon, - FolderIcon, - ChevronIcon, - CubeIcon, - AddIcon, - KebebIcon, - CollapseAllIcon, - FocusIcon, -} from "../../../components/icons/ExportCommonIcons"; -import RenameInput from "../../../components/ui/inputs/RenameInput"; -import { useSceneContext } from "../../scene/sceneContext"; -import { useSocketStore } from "../../../store/socket/useSocketStore"; -import useAssetResponseHandler from "../../collaboration/responseHandler/useAssetResponseHandler"; +import { EyeIcon, LockIcon, FolderIcon, ChevronIcon, CubeIcon, AddIcon, KebebIcon, CollapseAllIcon, FocusIcon } from "../../../icons/ExportCommonIcons"; +import RenameInput from "../../inputs/RenameInput"; +import { useSceneContext } from "../../../../modules/scene/sceneContext"; +import { useSocketStore } from "../../../../store/socket/useSocketStore"; +import useAssetResponseHandler from "../../../../modules/collaboration/responseHandler/useAssetResponseHandler"; -import { getUserData } from "../../../functions/getUserData"; -import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; +import { getUserData } from "../../../../functions/getUserData"; +import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import clsx from "clsx"; -import { useContextActionStore } from "../../../store/builder/store"; +import { useContextActionStore } from "../../../../store/builder/store"; interface DragState { draggedItem: AssetGroupChild | null; @@ -63,8 +53,7 @@ const TreeNode = ({ const isLocked = item.isLocked; const isExpanded = isGroupNode ? item.isExpanded : false; const isSelected = !isGroupNode ? hasSelectedAsset(item.modelUuid) : false; - const isMultiSelected = - !isGroupNode && selectedAssets.length > 1 && hasSelectedAsset(item.modelUuid); + const isMultiSelected = !isGroupNode && selectedAssets.length > 1 && hasSelectedAsset(item.modelUuid); // Determine the parent group of this item const getParentGroup = useCallback( @@ -123,10 +112,7 @@ const TreeNode = ({ const shouldShowHighlight = isDropTarget(); return ( -
+
{isGroupNode && ( - )} -
- {isGroupNode ? : } -
+
{isGroupNode ? : }
{}} canEdit={true} /> @@ -230,7 +211,7 @@ const TreeNode = ({ }; // Main Component -export const Outline = () => { +export const AssetOutline = () => { const [isOpen, setIsOpen] = useState(true); const { setContextAction } = useContextActionStore(); const lastSelectedRef = useRef<{ item: AssetGroupChild; index: number } | null>(null); @@ -242,25 +223,8 @@ export const Outline = () => { }); const [_, forceUpdate] = useState({}); const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext(); - const { - addSelectedAsset, - clearSelectedAssets, - getAssetById, - peekToggleVisibility, - peekToggleLock, - toggleSelectedAsset, - selectedAssets, - } = assetStore(); - const { - groupHierarchy, - isGroup, - getGroupsContainingAsset, - getFlatGroupChildren, - setGroupExpanded, - addChildToGroup, - removeChildFromGroup, - getGroupsContainingGroup, - } = assetGroupStore(); + const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, peekToggleLock, toggleSelectedAsset, selectedAssets } = assetStore(); + const { groupHierarchy, isGroup, getGroupsContainingAsset, getFlatGroupChildren, setGroupExpanded, addChildToGroup, removeChildFromGroup, getGroupsContainingGroup } = assetGroupStore(); const { projectId } = useParams(); const { push3D } = undoRedo3DStore(); const { builderSocket } = useSocketStore(); @@ -304,11 +268,7 @@ export const Outline = () => { }) .then((data) => { if (!data.message || !data.data) { - echo.error( - `Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${ - asset.modelName - }` - ); + echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); return; } if (data.message === "Model updated successfully" && data.data) { @@ -327,22 +287,14 @@ export const Outline = () => { }; updateAssetInScene(model, () => { - echo.info( - `${asset.isVisible ? "Hid" : "Unhid"} asset: ${model.modelName}` - ); + echo.info(`${asset.isVisible ? "Hid" : "Unhid"} asset: ${model.modelName}`); }); } else { - echo.error( - `Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${ - asset.modelName - }` - ); + echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); } }) .catch(() => { - echo.error( - `Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}` - ); + echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); }); } else { const data = { @@ -387,11 +339,7 @@ export const Outline = () => { }) .then((data) => { if (!data.message || !data.data) { - echo.error( - `Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${ - asset.modelName - }` - ); + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); return; } if (data.message === "Model updated successfully" && data.data) { @@ -410,26 +358,14 @@ export const Outline = () => { }; updateAssetInScene(model, () => { - echo.info( - `${asset.isVisible ? "Locked" : "Unlocked"} asset: ${ - model.modelName - }` - ); + echo.info(`${asset.isVisible ? "Locked" : "Unlocked"} asset: ${model.modelName}`); }); } else { - echo.error( - `Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${ - asset.modelName - }` - ); + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); } }) .catch(() => { - echo.error( - `Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${ - asset.modelName - }` - ); + echo.error(`Error ${asset.isVisible ? "locking" : "unlocking"} asset: ${asset.modelName}`); }); } else { const data = { @@ -461,16 +397,13 @@ export const Outline = () => { [setGroupExpanded] ); - const handleDragStart = useCallback( - (e: DragEvent, item: AssetGroupChild, parentGroupUuid: string | null) => { - dragStateRef.current.draggedItem = item; - dragStateRef.current.draggedItemParentGroupUuid = parentGroupUuid; + const handleDragStart = useCallback((e: DragEvent, item: AssetGroupChild, parentGroupUuid: string | null) => { + dragStateRef.current.draggedItem = item; + dragStateRef.current.draggedItemParentGroupUuid = parentGroupUuid; - e.dataTransfer.effectAllowed = "move"; - forceUpdate({}); - }, - [] - ); + e.dataTransfer.effectAllowed = "move"; + forceUpdate({}); + }, []); const handleDragOver = useCallback( (e: DragEvent, targetItem: AssetGroupChild) => { @@ -540,10 +473,7 @@ export const Outline = () => { } // Update target group - if ( - dragStateRef.current.targetGroupUuid !== targetGroupUuid || - dragStateRef.current.isRootTarget !== false - ) { + if (dragStateRef.current.targetGroupUuid !== targetGroupUuid || dragStateRef.current.isRootTarget !== false) { dragStateRef.current.targetGroupUuid = targetGroupUuid; dragStateRef.current.isRootTarget = false; forceUpdate({}); @@ -657,9 +587,7 @@ export const Outline = () => { // Update last selected reference const flattened = getFlattenedHierarchy(); - const index = flattened.findIndex( - (flatItem) => getItemId(flatItem) === getItemId(item) - ); + const index = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); lastSelectedRef.current = { item, index }; } } @@ -673,9 +601,7 @@ export const Outline = () => { // Update last selected reference const flattened = getFlattenedHierarchy(); - const index = flattened.findIndex( - (flatItem) => getItemId(flatItem) === getItemId(item) - ); + const index = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); lastSelectedRef.current = { item, index }; } } @@ -683,9 +609,7 @@ export const Outline = () => { // Shift+Click - range selection else if (isShiftClick) { const flattened = getFlattenedHierarchy(); - const clickedIndex = flattened.findIndex( - (flatItem) => getItemId(flatItem) === getItemId(item) - ); + const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); if (clickedIndex === -1) return; @@ -731,14 +655,7 @@ export const Outline = () => { }); } }, - [ - scene, - isGroup, - clearSelectedAssets, - addSelectedAsset, - getFlattenedHierarchy, - toggleSelectedAsset, - ] + [scene, isGroup, clearSelectedAssets, addSelectedAsset, getFlattenedHierarchy, toggleSelectedAsset] ); const handleOptionClick = useCallback( @@ -860,11 +777,8 @@ export const Outline = () => { className="toolbar-button" title="Expand All" onClick={() => { - const { assetGroups, setGroupExpanded } = - assetGroupStore.getState(); - assetGroups.forEach((group) => - setGroupExpanded(group.groupUuid, true) - ); + const { assetGroups, setGroupExpanded } = assetGroupStore.getState(); + assetGroups.forEach((group) => setGroupExpanded(group.groupUuid, true)); }} > @@ -876,13 +790,7 @@ export const Outline = () => {
{isOpen && ( -
+
{groupHierarchy.map((item) => ( {
- 1 ? "multi-selection" : ""}`} - > - {selectedAssets.length > 1 - ? `${selectedAssets.length} items selected` - : `${groupHierarchy.length} root items`} + 1 ? "multi-selection" : ""}`}> + {selectedAssets.length > 1 ? `${selectedAssets.length} items selected` : `${groupHierarchy.length} root items`}
diff --git a/app/src/modules/builder/wall/wallCreator/wallCreator.tsx b/app/src/modules/builder/wall/wallCreator/wallCreator.tsx index 709cc50..c7697ed 100644 --- a/app/src/modules/builder/wall/wallCreator/wallCreator.tsx +++ b/app/src/modules/builder/wall/wallCreator/wallCreator.tsx @@ -61,7 +61,7 @@ function WallCreator() { const addWallToBackend = (wall: Wall) => { if (projectId) { - if (!builderSocket?.connected) { + if (builderSocket?.connected) { // API upsertWallApi(projectId, selectedVersion?.versionId || "", wall) @@ -104,7 +104,7 @@ function WallCreator() { organization: organization, }; - builderSocket.emit("v1:model-Wall:add", data); + // builderSocket.emit("v1:model-Wall:add", data); setTempPoints([]); setIsCreating(false); diff --git a/app/src/modules/scene/controls/assetControls/groupControls.tsx b/app/src/modules/scene/controls/assetControls/groupControls.tsx index ba04658..1b92838 100644 --- a/app/src/modules/scene/controls/assetControls/groupControls.tsx +++ b/app/src/modules/scene/controls/assetControls/groupControls.tsx @@ -11,14 +11,15 @@ import useCallBackOnKey from "../../../../utils/hooks/useCallBackOnKey"; import generateUniqueAssetGroupName from "../../../builder/asset/functions/generateUniqueAssetGroupName"; import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import { getAssetGroupsApi } from "../../../../services/factoryBuilder/group/assetGroup/getAssetGroupsApi"; +import { createAssetGroupApi } from "../../../../services/factoryBuilder/group/assetGroup/createAssetGroupApi"; function GroupControls() { const { projectId } = useParams(); const { builderSocket } = useSocketStore(); const { assetStore, undoRedo3DStore, versionStore, assetGroupStore } = useSceneContext(); - const { assetGroups, addGroup, buildHierarchy } = assetGroupStore(); + const { assetGroups, addGroup, setGroups, buildHierarchy } = assetGroupStore(); const { push3D } = undoRedo3DStore(); - const { assets, selectedAssets, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects } = assetStore(); + const { assets, selectedAssets, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, getSelectedAssetUuids } = assetStore(); const { updateAssetInScene, removeAssetFromScene } = useAssetResponseHandler(); const { contextAction, setContextAction } = useContextActionStore(); const { selectedVersion } = versionStore(); @@ -29,6 +30,9 @@ function GroupControls() { getAssetGroupsApi(projectId, selectedVersion.versionId).then((data) => { console.log("data: ", data); + if (data && data.length > 0) { + setGroups(data); + } }); }, [projectId, selectedVersion]); @@ -43,18 +47,12 @@ function GroupControls() { setContextAction(null); groupSelection(); } - }, [contextAction]); + }, [contextAction, projectId, selectedVersion]); const groupSelection = () => { - const assetUuids: string[] = []; + const assetUuids: string[] = getSelectedAssetUuids(); - selectedAssets.forEach((selectedAsset) => { - if (selectedAsset.userData.modelUuid) { - assetUuids.push(selectedAsset.userData.modelUuid); - } - }); - - if (assetUuids.length > 0) { + if (assetUuids.length > 0 && projectId && selectedVersion) { const groupName = generateUniqueAssetGroupName({ baseName: "Group", existingGroups: assetGroups }); const assetGroup: AssetGroup = { @@ -67,7 +65,21 @@ function GroupControls() { return { type: "Asset", childrenUuid: assetUuid }; }), }; - addGroup(assetGroup); + console.log("assetGroup: ", assetGroup); + // addGroup(assetGroup); + + createAssetGroupApi({ + projectId, + versionId: selectedVersion.versionId, + groupUuid: assetGroup.groupUuid, + groupName: assetGroup.groupName, + isVisible: assetGroup.isVisible, + isExpanded: assetGroup.isExpanded, + isLocked: assetGroup.isLocked, + childrens: assetGroup.childrens, + }).then((data) => { + console.log("data: ", data); + }); } }; @@ -81,6 +93,8 @@ function GroupControls() { { dependencies: [pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, selectedAssets, selectedVersion, builderSocket, projectId, userId, organization], noRepeat: true } ); + useCallBackOnKey(() => {}, "Alt+G", { dependencies: [], noRepeat: true }); + return null; } diff --git a/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts b/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts index d4b49cd..c6072c0 100644 --- a/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts +++ b/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts @@ -1,37 +1,44 @@ const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; -interface Props { +export const createAssetGroupApi = async ({ + projectId, + versionId, + groupUuid, + groupName, + isVisible, + isExpanded, + isLocked, + childrens, +}: { projectId: string; versionId: string; groupUuid: string; groupName: string; - isVisible: string; - isExpanded: string; - isLocked: string; + isVisible: boolean; + isExpanded: boolean; + isLocked: boolean; childrens: { type: "Asset" | "Group"; childrenUuid: string; }[]; -} - -export const createAssetGroupApi = async (props: Props) => { +}) => { try { - const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetgroup/${props.projectId}/${props.versionId}`, { + const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetgroup/${projectId}/${versionId}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("token") || ""}`, token: localStorage.getItem("token") || "", refresh_token: localStorage.getItem("refreshToken") || "", - body: JSON.stringify({ - groupUuid: props.groupUuid, - groupName: props.groupName, - isVisible: props.isVisible, - isExpanded: props.isExpanded, - isLocked: props.isLocked, - childrens: props.childrens, - }), }, + body: JSON.stringify({ + groupUuid: groupUuid, + groupName: groupName, + isVisible: isVisible, + isExpanded: isExpanded, + isLocked: isLocked, + childrens: childrens, + }), }); const newAccessToken = response.headers.get("x-access-token"); diff --git a/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupsApi.ts b/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupsApi.ts index 7ad4a08..8fa9c8d 100644 --- a/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupsApi.ts +++ b/app/src/services/factoryBuilder/group/assetGroup/getAssetGroupsApi.ts @@ -2,7 +2,7 @@ let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_UR export const getAssetGroupsApi = async (projectId: string, versionId: string) => { try { - const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetGroups/${projectId}/${versionId}`, { + const response = await fetch(`${url_Backend_dwinzo}/api/V1/assetgroups/${projectId}/${versionId}`, { method: "GET", headers: { Authorization: "Bearer ", From 48faaac1fe0fd9a85a61e99d4bd0506dd595138c Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Thu, 9 Oct 2025 14:58:53 +0530 Subject: [PATCH 2/2] Enhance AssetOutline component with group selection and multi-selection features; refactor related styles and APIs for improved functionality --- .../ui/list/OutlineList/AssetOutline.tsx | 256 ++++++++++-------- .../builder/wall/wallCreator/wallCreator.tsx | 4 +- .../controls/assetControls/groupControls.tsx | 30 +- .../group/assetGroup/createAssetGroupApi.ts | 6 +- app/src/store/builder/useAssetGroupStore.ts | 51 ++-- app/src/styles/layout/_assetOutline.scss | 38 ++- app/src/types/builderTypes.d.ts | 2 +- app/src/utils/useOuterClick.ts | 66 +++++ 8 files changed, 291 insertions(+), 162 deletions(-) create mode 100644 app/src/utils/useOuterClick.ts diff --git a/app/src/components/ui/list/OutlineList/AssetOutline.tsx b/app/src/components/ui/list/OutlineList/AssetOutline.tsx index c99d2e9..0536262 100644 --- a/app/src/components/ui/list/OutlineList/AssetOutline.tsx +++ b/app/src/components/ui/list/OutlineList/AssetOutline.tsx @@ -1,15 +1,17 @@ -import { useState, useRef, DragEvent, useCallback } from "react"; +import { useState, useRef, DragEvent, useCallback, useMemo } from "react"; import { useParams } from "react-router-dom"; -import { EyeIcon, LockIcon, FolderIcon, ChevronIcon, CubeIcon, AddIcon, KebebIcon, CollapseAllIcon, FocusIcon } from "../../../icons/ExportCommonIcons"; +import { EyeIcon, LockIcon, FolderIcon, ChevronIcon, CubeIcon, AddIcon, KebebIcon, CollapseAllIcon, FocusIcon, DeleteIcon } from "../../../icons/ExportCommonIcons"; import RenameInput from "../../inputs/RenameInput"; +import clsx from "clsx"; import { useSceneContext } from "../../../../modules/scene/sceneContext"; import { useSocketStore } from "../../../../store/socket/useSocketStore"; import useAssetResponseHandler from "../../../../modules/collaboration/responseHandler/useAssetResponseHandler"; +import useZoomMesh from "../../../../modules/builder/hooks/useZoomMesh"; import { getUserData } from "../../../../functions/getUserData"; +import { useOuterClick } from "../../../../utils/useOuterClick"; + import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; -import clsx from "clsx"; -import { useContextActionStore } from "../../../../store/builder/store"; interface DragState { draggedItem: AssetGroupChild | null; @@ -18,7 +20,7 @@ interface DragState { isRootTarget: boolean; } -// Tree Node Component +// Enhanced Tree Node Component with Group Selection const TreeNode = ({ item, level = 0, @@ -44,7 +46,7 @@ const TreeNode = ({ }) => { const { assetGroupStore, assetStore } = useSceneContext(); const { hasSelectedAsset, selectedAssets } = assetStore(); - const { isGroup, getGroupsContainingAsset, getGroupsContainingGroup } = assetGroupStore(); + const { isGroup, hasSelectedGroup, selectedGroups, getParentGroup } = assetGroupStore(); const isGroupNode = isGroup(item); const itemId = isGroupNode ? item.groupUuid : item.modelUuid; @@ -52,31 +54,26 @@ const TreeNode = ({ const isVisible = item.isVisible; const isLocked = item.isLocked; const isExpanded = isGroupNode ? item.isExpanded : false; - const isSelected = !isGroupNode ? hasSelectedAsset(item.modelUuid) : false; - const isMultiSelected = !isGroupNode && selectedAssets.length > 1 && hasSelectedAsset(item.modelUuid); - // Determine the parent group of this item - const getParentGroup = useCallback( - (currentItem: AssetGroupChild): string | null => { - if (isGroup(currentItem)) { - const parents = getGroupsContainingGroup(currentItem.groupUuid); - return parents.length > 0 ? parents[0].groupUuid : null; - } else { - const parents = getGroupsContainingAsset(currentItem.modelUuid); - return parents.length > 0 ? parents[0].groupUuid : null; - } - }, - [getGroupsContainingAsset, getGroupsContainingGroup, isGroup] - ); + const isSelected = isGroupNode ? hasSelectedGroup(item.groupUuid) : hasSelectedAsset(item.modelUuid); + + const getMultiSelectionState = (item: AssetGroupChild) => { + const totalSelectedItems = selectedGroups.length + selectedAssets.length; + + if (totalSelectedItems <= 1) return false; + + if (isGroup(item)) { + return selectedGroups.includes(item.groupUuid); + } else { + return hasSelectedAsset(item.modelUuid); + } + }; + + const isMultiSelected = getMultiSelectionState(item); - // Check if this node should be highlighted as a drop target const isDropTarget = useCallback(() => { if (!dragState.draggedItem || !dragState.targetGroupUuid || !isGroupNode) return false; - - // Get the group UUID this item belongs to or is const thisGroupUuid = item.groupUuid; - - // Highlight if this is the target group or belongs to the target group return thisGroupUuid === dragState.targetGroupUuid; }, [dragState, isGroupNode, item]); @@ -112,9 +109,11 @@ const TreeNode = ({ const shouldShowHighlight = isDropTarget(); return ( -
+
{}} canEdit={true} />
- - - + )} + {isGroupNode && ( - )} @@ -181,14 +159,7 @@ const TreeNode = ({
{isGroupNode && isExpanded && item.children && ( -
+
{item.children.map((child) => ( { const [isOpen, setIsOpen] = useState(true); - const { setContextAction } = useContextActionStore(); const lastSelectedRef = useRef<{ item: AssetGroupChild; index: number } | null>(null); const dragStateRef = useRef({ draggedItem: null, @@ -223,14 +193,30 @@ export const AssetOutline = () => { }); const [_, forceUpdate] = useState({}); const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext(); - const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, peekToggleLock, toggleSelectedAsset, selectedAssets } = assetStore(); - const { groupHierarchy, isGroup, getGroupsContainingAsset, getFlatGroupChildren, setGroupExpanded, addChildToGroup, removeChildFromGroup, getGroupsContainingGroup } = assetGroupStore(); + const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, peekToggleLock, toggleSelectedAsset, selectedAssets, setSelectedAssets } = assetStore(); + const { + groupHierarchy, + isGroup, + getGroupsContainingAsset, + getFlatGroupChildren, + setGroupExpanded, + addChildToGroup, + removeChildFromGroup, + getGroupsContainingGroup, + selectedGroups, + addSelectedGroup, + removeSelectedGroup, + toggleSelectedGroup, + clearSelectedGroups, + hasSelectedGroup, + } = assetGroupStore(); const { projectId } = useParams(); const { push3D } = undoRedo3DStore(); const { builderSocket } = useSocketStore(); const { userId, organization } = getUserData(); const { selectedVersion } = versionStore(); const { updateAssetInScene } = useAssetResponseHandler(); + const { zoomMeshes } = useZoomMesh(); const getFlattenedHierarchy = useCallback((): AssetGroupChild[] => { const flattened: AssetGroupChild[] = []; @@ -568,7 +554,7 @@ export const AssetOutline = () => { (e: React.MouseEvent, item: AssetGroupChild) => { e.preventDefault(); const isShiftClick = e.shiftKey; - const isCtrlClick = e.ctrlKey; + const isCtrlClick = e.ctrlKey || e.metaKey; if (!scene.current) return; @@ -577,45 +563,55 @@ export const AssetOutline = () => { return isGroup(i) ? i.groupUuid : i.modelUuid; }; + const itemId = getItemId(item); + const flattened = getFlattenedHierarchy(); + const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === itemId); + // Single click (no modifiers) - select only this item if (!isShiftClick && !isCtrlClick) { - if (!isGroup(item)) { + if (isGroup(item)) { + clearSelectedAssets(); + clearSelectedGroups(); + addSelectedGroup(item.groupUuid); + lastSelectedRef.current = { item, index: clickedIndex }; + } else { + clearSelectedGroups(); + clearSelectedAssets(); const asset = scene.current.getObjectByProperty("uuid", item.modelUuid); if (asset) { - clearSelectedAssets(); addSelectedAsset(asset); - - // Update last selected reference - const flattened = getFlattenedHierarchy(); - const index = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); - lastSelectedRef.current = { item, index }; + lastSelectedRef.current = { item, index: clickedIndex }; } } } + // Ctrl+Click - toggle selection else if (isCtrlClick && !isShiftClick) { - if (!isGroup(item)) { + if (isGroup(item)) { + toggleSelectedGroup(item.groupUuid); + lastSelectedRef.current = { item, index: clickedIndex }; + } else { const asset = scene.current.getObjectByProperty("uuid", item.modelUuid); if (asset) { toggleSelectedAsset(asset); - - // Update last selected reference - const flattened = getFlattenedHierarchy(); - const index = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); - lastSelectedRef.current = { item, index }; + lastSelectedRef.current = { item, index: clickedIndex }; } } } // Shift+Click - range selection else if (isShiftClick) { - const flattened = getFlattenedHierarchy(); - const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item)); - if (clickedIndex === -1) return; // If no last selected item, treat as normal selection if (!lastSelectedRef.current) { - if (!isGroup(item)) { + if (isGroup(item)) { + clearSelectedAssets(); + clearSelectedGroups(); + addSelectedGroup(item.groupUuid); + lastSelectedRef.current = { item, index: clickedIndex }; + } else { + clearSelectedGroups(); + clearSelectedAssets(); const asset = scene.current.getObjectByProperty("uuid", item.modelUuid); if (asset) { clearSelectedAssets(); @@ -633,20 +629,29 @@ export const AssetOutline = () => { // Get all items in range const itemsInRange = flattened.slice(startIndex, endIndex + 1); - // Filter out groups, only select assets + const groupsInRange: AssetGroupHierarchyNode[] = []; const assetsInRange: Asset[] = []; + itemsInRange.forEach((rangeItem) => { - if (!isGroup(rangeItem)) { + if (isGroup(rangeItem)) { + groupsInRange.push(rangeItem); + } else { assetsInRange.push(rangeItem); } }); // If Ctrl+Shift, add to existing selection; otherwise replace if (!isCtrlClick) { + clearSelectedGroups(); clearSelectedAssets(); } - // Add all assets in range to selection + groupsInRange.forEach((group) => { + if (!hasSelectedGroup(group.groupUuid)) { + addSelectedGroup(group.groupUuid); + } + }); + assetsInRange.forEach((assetItem) => { const asset = scene.current!.getObjectByProperty("uuid", assetItem.modelUuid); if (asset) { @@ -655,11 +660,28 @@ export const AssetOutline = () => { }); } }, - [scene, isGroup, clearSelectedAssets, addSelectedAsset, getFlattenedHierarchy, toggleSelectedAsset] + [ + scene, + isGroup, + clearSelectedAssets, + addSelectedAsset, + getFlattenedHierarchy, + toggleSelectedAsset, + toggleSelectedGroup, + clearSelectedGroups, + addSelectedGroup, + removeSelectedGroup, + selectedGroups, + selectedAssets, + ] ); const handleOptionClick = useCallback( (option: string, item: AssetGroupChild) => { + const getItemId = (i: AssetGroupChild): string => { + return isGroup(i) ? i.groupUuid : i.modelUuid; + }; + if (option === "visibility") { if (isGroup(item)) { } else { @@ -747,7 +769,17 @@ export const AssetOutline = () => { } else if (option === "focus") { if (isGroup(item)) { } else { - setContextAction("focusAsset"); + clearSelectedGroups(); + clearSelectedAssets(); + const asset = scene.current?.getObjectByProperty("uuid", item.modelUuid); + if (asset) { + const itemId = getItemId(item); + const flattened = getFlattenedHierarchy(); + const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === itemId); + addSelectedAsset(asset); + lastSelectedRef.current = { item, index: clickedIndex }; + zoomMeshes([itemId]); + } } } else if (option === "kebab") { if (isGroup(item)) { @@ -758,6 +790,30 @@ export const AssetOutline = () => { [selectedVersion, builderSocket, projectId, userId, organization] ); + const handleAddGroup = useCallback(() => {}, [assetGroupStore, clearSelectedGroups, addSelectedGroup]); + + const handleExpandAll = useCallback(() => {}, [assetGroupStore, setGroupExpanded]); + + // Selection statistics + const selectionStats = useMemo(() => { + const totalSelectedAssets = selectedAssets.length; + const totalSelectedGroups = selectedGroups.length; + + if (totalSelectedGroups > 0 && totalSelectedAssets > 0) { + return `${totalSelectedGroups} group${totalSelectedGroups > 1 ? "s" : ""} and ${totalSelectedAssets} asset${totalSelectedAssets > 1 ? "s" : ""} selected`; + } else if (totalSelectedGroups > 0) { + return `${totalSelectedGroups} group${totalSelectedGroups > 1 ? "s" : ""} selected`; + } else if (totalSelectedAssets > 0) { + return `${totalSelectedAssets} asset${totalSelectedAssets > 1 ? "s" : ""} selected`; + } else { + return `${groupHierarchy.length} root items`; + } + }, [selectedAssets.length, selectedGroups.length, groupHierarchy.length]); + + useOuterClick(() => { + clearSelectedGroups(); + }, ["tree-node"]); + return ( <>
@@ -767,24 +823,14 @@ export const AssetOutline = () => {

Assets

- - {/* */} -
@@ -810,9 +856,7 @@ export const AssetOutline = () => {
- 1 ? "multi-selection" : ""}`}> - {selectedAssets.length > 1 ? `${selectedAssets.length} items selected` : `${groupHierarchy.length} root items`} - + 1 ? "multi-selection" : ""}`}>{selectionStats}
); diff --git a/app/src/modules/builder/wall/wallCreator/wallCreator.tsx b/app/src/modules/builder/wall/wallCreator/wallCreator.tsx index c7697ed..709cc50 100644 --- a/app/src/modules/builder/wall/wallCreator/wallCreator.tsx +++ b/app/src/modules/builder/wall/wallCreator/wallCreator.tsx @@ -61,7 +61,7 @@ function WallCreator() { const addWallToBackend = (wall: Wall) => { if (projectId) { - if (builderSocket?.connected) { + if (!builderSocket?.connected) { // API upsertWallApi(projectId, selectedVersion?.versionId || "", wall) @@ -104,7 +104,7 @@ function WallCreator() { organization: organization, }; - // builderSocket.emit("v1:model-Wall:add", data); + builderSocket.emit("v1:model-Wall:add", data); setTempPoints([]); setIsCreating(false); diff --git a/app/src/modules/scene/controls/assetControls/groupControls.tsx b/app/src/modules/scene/controls/assetControls/groupControls.tsx index 1b92838..b31f09a 100644 --- a/app/src/modules/scene/controls/assetControls/groupControls.tsx +++ b/app/src/modules/scene/controls/assetControls/groupControls.tsx @@ -29,7 +29,6 @@ function GroupControls() { if (!projectId || !selectedVersion) return; getAssetGroupsApi(projectId, selectedVersion.versionId).then((data) => { - console.log("data: ", data); if (data && data.length > 0) { setGroups(data); } @@ -61,25 +60,24 @@ function GroupControls() { isVisible: true, isLocked: false, isExpanded: true, - childrens: assetUuids.map((assetUuid) => { + children: assetUuids.map((assetUuid) => { return { type: "Asset", childrenUuid: assetUuid }; }), }; - console.log("assetGroup: ", assetGroup); - // addGroup(assetGroup); - createAssetGroupApi({ - projectId, - versionId: selectedVersion.versionId, - groupUuid: assetGroup.groupUuid, - groupName: assetGroup.groupName, - isVisible: assetGroup.isVisible, - isExpanded: assetGroup.isExpanded, - isLocked: assetGroup.isLocked, - childrens: assetGroup.childrens, - }).then((data) => { - console.log("data: ", data); - }); + // createAssetGroupApi({ + // projectId, + // versionId: selectedVersion.versionId, + // groupUuid: assetGroup.groupUuid, + // groupName: assetGroup.groupName, + // isVisible: assetGroup.isVisible, + // isExpanded: assetGroup.isExpanded, + // isLocked: assetGroup.isLocked, + // children: assetGroup.children, + // }).then((data) => { + // console.log("data: ", data); + // }); + addGroup(assetGroup); } }; diff --git a/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts b/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts index c6072c0..db3d6fe 100644 --- a/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts +++ b/app/src/services/factoryBuilder/group/assetGroup/createAssetGroupApi.ts @@ -8,7 +8,7 @@ export const createAssetGroupApi = async ({ isVisible, isExpanded, isLocked, - childrens, + children, }: { projectId: string; versionId: string; @@ -17,7 +17,7 @@ export const createAssetGroupApi = async ({ isVisible: boolean; isExpanded: boolean; isLocked: boolean; - childrens: { + children: { type: "Asset" | "Group"; childrenUuid: string; }[]; @@ -37,7 +37,7 @@ export const createAssetGroupApi = async ({ isVisible: isVisible, isExpanded: isExpanded, isLocked: isLocked, - childrens: childrens, + children: children, }), }); diff --git a/app/src/store/builder/useAssetGroupStore.ts b/app/src/store/builder/useAssetGroupStore.ts index 4e533e0..d0bd137 100644 --- a/app/src/store/builder/useAssetGroupStore.ts +++ b/app/src/store/builder/useAssetGroupStore.ts @@ -43,6 +43,7 @@ interface AssetGroupStore { getGroupById: (groupUuid: string) => AssetGroup | undefined; getGroupsContainingAsset: (assetUuid: string) => AssetGroup[]; getGroupsContainingGroup: (childGroupUuid: string) => AssetGroup[]; + getParentGroup: (item: AssetGroupChild) => string | null; hasGroup: (groupUuid: string) => boolean; isGroup: (item: AssetGroupChild) => item is AssetGroupHierarchyNode; isEmptyGroup: (groupUuid: string) => boolean; @@ -69,18 +70,18 @@ export const createAssetGroupStore = () => { } // Find all asset children in the new group - const assetChildren = group.childrens.filter((child) => child.type === "Asset"); + const assetChildren = group.children.filter((child) => child.type === "Asset"); const assetUuids = new Set(assetChildren.map((child) => child.childrenUuid)); // Remove these assets from existing groups and track updated groups const updatedGroups: AssetGroup[] = []; state.assetGroups.forEach((existingGroup) => { - const originalLength = existingGroup.childrens.length; - existingGroup.childrens = existingGroup.childrens.filter((child) => !(child.type === "Asset" && assetUuids.has(child.childrenUuid))); + const originalLength = existingGroup.children.length; + existingGroup.children = existingGroup.children.filter((child) => !(child.type === "Asset" && assetUuids.has(child.childrenUuid))); // If group was modified, add to updated groups - if (existingGroup.childrens.length !== originalLength) { + if (existingGroup.children.length !== originalLength) { updatedGroups.push({ ...existingGroup }); } }); @@ -98,7 +99,7 @@ export const createAssetGroupStore = () => { set((state) => { // First remove this group from any parent groups state.assetGroups.forEach((group) => { - group.childrens = group.childrens.filter((child) => !(child.type === "Group" && child.childrenUuid === groupUuid)); + group.children = group.children.filter((child) => !(child.type === "Group" && child.childrenUuid === groupUuid)); }); // Then remove the group itself @@ -179,17 +180,17 @@ export const createAssetGroupStore = () => { state.assetGroups.forEach((group) => { if (group.groupUuid === groupUuid) return; // skip target group - const originalLength = group.childrens.length; - group.childrens = group.childrens.filter((c) => c.childrenUuid !== child.childrenUuid); + const originalLength = group.children.length; + group.children = group.children.filter((c) => c.childrenUuid !== child.childrenUuid); - if (group.childrens.length !== originalLength) { + if (group.children.length !== originalLength) { updatedGroups.push({ ...group }); } }); // 2️⃣ Add the child to the target group (if not already present) - if (!targetGroup.childrens.some((c) => c.childrenUuid === child.childrenUuid)) { - targetGroup.childrens.push(child); + if (!targetGroup.children.some((c) => c.childrenUuid === child.childrenUuid)) { + targetGroup.children.push(child); updatedGroups.push({ ...targetGroup }); } @@ -206,7 +207,7 @@ export const createAssetGroupStore = () => { set((state) => { const group = state.assetGroups.find((g) => g.groupUuid === groupUuid); if (group) { - group.childrens = group.childrens.filter((child) => child.childrenUuid !== childUuid); + group.children = group.children.filter((child) => child.childrenUuid !== childUuid); state.groupHierarchy = get().buildHierarchy([], state.assetGroups); } }); @@ -214,7 +215,7 @@ export const createAssetGroupStore = () => { getGroupChildren: (groupUuid) => { const group = get().assetGroups.find((g) => g.groupUuid === groupUuid); - return group?.childrens || []; + return group?.children || []; }, // Group properties @@ -271,7 +272,7 @@ export const createAssetGroupStore = () => { const buildNode = (group: AssetGroup): AssetGroupHierarchyNode => { const children: AssetGroupChild[] = []; - group.childrens.forEach((child) => { + group.children.forEach((child) => { if (child.type === "Asset") { const asset = assetMap.get(child.childrenUuid); if (asset) { @@ -300,7 +301,7 @@ export const createAssetGroupStore = () => { // Find root groups (groups that are not children of any other group) const childGroupUuids = new Set(); groups.forEach((group) => { - group.childrens.forEach((child) => { + group.children.forEach((child) => { if (child.type === "Group") { childGroupUuids.add(child.childrenUuid); } @@ -345,7 +346,7 @@ export const createAssetGroupStore = () => { isVisible: node.isVisible, isLocked: node.isLocked, isExpanded: node.isExpanded, - childrens: node.children.map((child) => + children: node.children.map((child) => "modelUuid" in child ? { type: "Asset" as const, childrenUuid: child.modelUuid } : { type: "Group" as const, childrenUuid: child.groupUuid } ), }); @@ -413,13 +414,13 @@ export const createAssetGroupStore = () => { if (child.type === "Group") { const childGroup = get().assetGroups.find((g) => g.groupUuid === child.childrenUuid); if (childGroup) { - collectChildren(childGroup.childrens); + collectChildren(childGroup.children); } } }); }; - collectChildren(group.childrens); + collectChildren(group.children); return allChildren; }, @@ -429,11 +430,21 @@ export const createAssetGroupStore = () => { }, getGroupsContainingAsset: (assetUuid) => { - return get().assetGroups.filter((group) => group.childrens.some((child) => child.type === "Asset" && child.childrenUuid === assetUuid)); + return get().assetGroups.filter((group) => group.children.some((child) => child.type === "Asset" && child.childrenUuid === assetUuid)); }, getGroupsContainingGroup: (childGroupUuid) => { - return get().assetGroups.filter((group) => group.childrens.some((child) => child.type === "Group" && child.childrenUuid === childGroupUuid)); + return get().assetGroups.filter((group) => group.children.some((child) => child.type === "Group" && child.childrenUuid === childGroupUuid)); + }, + + getParentGroup: (item) => { + if (get().isGroup(item)) { + const parents = get().getGroupsContainingGroup(item.groupUuid); + return parents.length > 0 ? parents[0].groupUuid : null; + } else { + const parents = get().getGroupsContainingAsset(item.modelUuid); + return parents.length > 0 ? parents[0].groupUuid : null; + } }, hasGroup: (groupUuid) => { @@ -446,7 +457,7 @@ export const createAssetGroupStore = () => { isEmptyGroup: (groupUuid) => { const group = get().assetGroups.find((g) => g.groupUuid === groupUuid); - return !group || group.childrens.length === 0; + return !group || group.children.length === 0; }, })) ); diff --git a/app/src/styles/layout/_assetOutline.scss b/app/src/styles/layout/_assetOutline.scss index 6d86754..cd65fa7 100644 --- a/app/src/styles/layout/_assetOutline.scss +++ b/app/src/styles/layout/_assetOutline.scss @@ -85,6 +85,7 @@ overflow-y: auto; border-radius: $border-radius-medium; position: relative; + padding: 2px; &::-webkit-scrollbar { width: 6px; @@ -107,8 +108,7 @@ &.root-drop-target { background: var(--background-color-selected); - box-shadow: inset 0 0 0 2px var(--border-color-accent), - inset 0 0 20px var(--highlight-accent-color); + box-shadow: inset 0 0 0 2px var(--border-color-accent), inset 0 0 20px var(--highlight-accent-color); border-radius: $border-radius-medium; &::before { @@ -134,29 +134,42 @@ .tree-node { position: relative; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + outline: 2px solid transparent; &.drop-target-highlight { background: var(--background-color-selected); - border-radius: $border-radius-medium; - box-shadow: inset 0 0 0 2px var(--border-color-accent), - 0 0 20px var(--highlight-accent-color), 0 4px 12px var(--background-color-selected); + border-radius: $border-radius-extra-large; + outline: 2px solid var(--border-color-accent); + z-index: 1; + } + + &.group-selected { + background: var(--background-color-selected); + border-radius: $border-radius-extra-large; } .tree-node-content { @include flex-center; gap: 6px; - padding: 6px 12px; - border-radius: $border-radius-medium; + padding: 4px 12px; + border-radius: $border-radius-extra-large; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); background: transparent; position: relative; + outline: 1px solid transparent; + outline-offset: -1px; &:hover { - background: var(--background-color-accent); + // background: var(--background-color-secondary); + outline: 1px solid var(--accent-color); } &.selected { - background: var(--background-color-selected); + span { + color: var(--text-button-color); + } + background: var(--background-color-accent); + outline: 1px solid transparent; } &.dragging { @@ -264,11 +277,8 @@ top: 0; bottom: 0; width: 1px; - background: linear-gradient( - to bottom, - var(--accent-color), - var(--background-color-selected) - ); + background: linear-gradient(to bottom, var(--accent-color), var(--background-color-selected)); + z-index: 100; box-shadow: 0 0 4px var(--highlight-accent-color); } } diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts index 59d2ad3..ca6aedc 100644 --- a/app/src/types/builderTypes.d.ts +++ b/app/src/types/builderTypes.d.ts @@ -56,7 +56,7 @@ interface AssetGroup { isVisible: boolean; isLocked: boolean; isExpanded: boolean; - childrens: { + children: { type: "Asset" | "Group"; childrenUuid: string; }[]; diff --git a/app/src/utils/useOuterClick.ts b/app/src/utils/useOuterClick.ts new file mode 100644 index 0000000..d1456f1 --- /dev/null +++ b/app/src/utils/useOuterClick.ts @@ -0,0 +1,66 @@ +import { useEffect, useRef } from "react"; + +export function useOuterClick( + callback: () => void, + contextClassNames: string[], + enabled: boolean = true, + ignoreIfMoved: boolean = true, + clickType: "left" | "right" = "left", + dependencies: any[] = [] +) { + const callbackRef = useRef(callback); + const classNamesRef = useRef(contextClassNames); + const mouseDownTargetRef = useRef(null); + const movedRef = useRef(false); + + // Keep refs updated + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + classNamesRef.current = contextClassNames; + }, [contextClassNames]); + + useEffect(() => { + if (!enabled) return; + + const handleMouseDown = (event: MouseEvent) => { + mouseDownTargetRef.current = event.target; + movedRef.current = false; + }; + + const handleMouseMove = () => { + movedRef.current = true; + }; + + const handleMouseUp = (event: MouseEvent) => { + const downTarget = mouseDownTargetRef.current as HTMLElement | null; + const upTarget = event.target as HTMLElement; + if (!downTarget || !upTarget) return; + + // Check click type + const isCorrectClick = (clickType === "left" && event.button === 0) || (clickType === "right" && event.button === 2); + + if (!isCorrectClick) return; + if (ignoreIfMoved && movedRef.current) return; + + // Check if click is inside any ignored context + const isInside = classNamesRef.current.some((className) => downTarget.closest(`.${className}`) || upTarget.closest(`.${className}`)); + + if (!isInside) { + callbackRef.current(); + } + }; + + document.addEventListener("mousedown", handleMouseDown); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + return () => { + document.removeEventListener("mousedown", handleMouseDown); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [enabled, ignoreIfMoved, clickType, ...dependencies]); +}