Refactor Outline Component and Styles

- Removed the Outline component from MainScene and SideBarLeft.
- Deleted outline.css and integrated styles into a new _assetOutline.scss file.
- Updated Outline component to use new styles and improved structure.
- Added functionality for collapsing all groups in the Outline.
- Enhanced drag-and-drop functionality for asset management.
- Improved accessibility and usability of the Outline component.
This commit is contained in:
2025-10-06 17:17:39 +05:30
parent 72f2b0f4bc
commit 9e40670d69
9 changed files with 2067 additions and 2083 deletions

2
app/package-lock.json generated
View File

@@ -29,6 +29,7 @@
"@use-gesture/react": "^10.3.1", "@use-gesture/react": "^10.3.1",
"chart.js": "^4.4.8", "chart.js": "^4.4.8",
"chartjs-plugin-annotation": "^3.1.0", "chartjs-plugin-annotation": "^3.1.0",
"clsx": "^2.1.1",
"dxf-parser": "^1.1.2", "dxf-parser": "^1.1.2",
"glob": "^11.0.0", "glob": "^11.0.0",
"gsap": "^3.12.5", "gsap": "^3.12.5",
@@ -8760,6 +8761,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }

View File

@@ -24,6 +24,7 @@
"@use-gesture/react": "^10.3.1", "@use-gesture/react": "^10.3.1",
"chart.js": "^4.4.8", "chart.js": "^4.4.8",
"chartjs-plugin-annotation": "^3.1.0", "chartjs-plugin-annotation": "^3.1.0",
"clsx": "^2.1.1",
"dxf-parser": "^1.1.2", "dxf-parser": "^1.1.2",
"glob": "^11.0.0", "glob": "^11.0.0",
"gsap": "^3.12.5", "gsap": "^3.12.5",

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,6 @@ import { recentlyViewedApi } from "../../../services/dashboard/recentlyViewedApi
import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
import { getVersionHistoryApi } from "../../../services/factoryBuilder/versionControl/getVersionHistoryApi"; import { getVersionHistoryApi } from "../../../services/factoryBuilder/versionControl/getVersionHistoryApi";
import { getUserData } from "../../../functions/getUserData"; import { getUserData } from "../../../functions/getUserData";
import { Outline } from "../../../modules/builder/testUi/outline";
function MainScene() { function MainScene() {
const { setMainState, clearComparisonState } = useSimulationState(); const { setMainState, clearComparisonState } = useSimulationState();
@@ -186,7 +185,6 @@ function MainScene() {
<> <>
{!selectedUser && ( {!selectedUser && (
<> <>
<Outline />
<KeyPressListener /> <KeyPressListener />
{loadingProgress > 0 && <LoadingPage progress={loadingProgress} />} {loadingProgress > 0 && <LoadingPage progress={loadingProgress} />}
{!isPlaying && ( {!isPlaying && (

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import ToggleHeader from "../../ui/inputs/ToggleHeader"; import ToggleHeader from "../../ui/inputs/ToggleHeader";
import Outline from "./Outline"; // import Outline from "./Outline";
import Header from "./Header"; import Header from "./Header";
import { useToggleStore } from "../../../store/ui/useUIToggleStore"; import { useToggleStore } from "../../../store/ui/useUIToggleStore";
import Assets from "./assetList/Assets"; import Assets from "./assetList/Assets";
@@ -9,6 +9,7 @@ import Widgets from "./visualization/widgets/Widgets";
import Templates from "../../../modules/visualization/template/Templates"; import Templates from "../../../modules/visualization/template/Templates";
import Search from "../../ui/inputs/Search"; import Search from "../../ui/inputs/Search";
import { useIsComparing } from "../../../store/builder/store"; import { useIsComparing } from "../../../store/builder/store";
import { Outline } from "../../../modules/builder/testUi/outline";
const SideBarLeft: React.FC = () => { const SideBarLeft: React.FC = () => {
const [activeOption, setActiveOption] = useState("Widgets"); const [activeOption, setActiveOption] = useState("Widgets");

View File

@@ -1,422 +0,0 @@
/* Hierarchy Overlay Styles */
.outline-overlay {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif;
}
.outline-card {
width: 320px;
background: rgba(15, 15, 25, 0.98);
backdrop-filter: blur(30px);
border-radius: 16px;
border: 1px solid hsl(262 83% 58% / 0.3);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 60px hsl(262 83% 68% / 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 0 0 1px hsl(262 83% 58% / 0.1);
overflow: hidden;
animation: slideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
}
.outline-card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(135deg, hsl(262 83% 68% / 0.05), transparent 50%);
pointer-events: none;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
/* Header */
.outline-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: linear-gradient(135deg, hsl(262 83% 58% / 0.15), hsl(262 83% 58% / 0.05));
border-bottom: 1px solid hsl(262 83% 58% / 0.3);
position: relative;
}
.outline-header::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, hsl(262 83% 68% / 0.5) 50%, transparent);
}
.header-title {
display: flex;
align-items: center;
gap: 10px;
color: #e0e0ff;
}
.header-title svg {
color: hsl(262 83% 68%);
filter: drop-shadow(0 0 4px hsl(262 83% 68% / 0.5));
}
.header-title h2 {
margin: 0;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.3px;
}
.close-button {
background: none;
border: none;
color: #9ca3af;
cursor: pointer;
padding: 4px;
border-radius: 6px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
background: hsl(262 83% 58% / 0.2);
color: #e0e0ff;
transform: rotate(90deg);
box-shadow: 0 0 8px hsl(262 83% 68% / 0.3);
}
/* Toolbar */
.outline-toolbar {
display: flex;
gap: 4px;
padding: 12px;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid hsl(262 83% 58% / 0.1);
}
.toolbar-button {
background: hsl(262 83% 58% / 0.1);
border: 1px solid hsl(262 83% 58% / 0.2);
color: hsl(262 83% 68%);
cursor: pointer;
padding: 6px 10px;
border-radius: 6px;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
}
.toolbar-button:hover {
background: hsl(262 83% 58% / 0.2);
border-color: hsl(262 83% 58% / 0.4);
transform: translateY(-2px);
box-shadow: 0 4px 12px hsl(262 83% 58% / 0.3), 0 0 16px hsl(262 83% 68% / 0.2);
}
.toolbar-button:active {
transform: translateY(0);
}
/* Content */
.outline-content {
max-height: 500px;
overflow-y: auto;
padding: 8px 0;
background: rgba(0, 0, 0, 0.2);
}
.outline-content::-webkit-scrollbar {
width: 6px;
}
.outline-content::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
.outline-content::-webkit-scrollbar-thumb {
background: hsl(262 83% 58% / 0.4);
border-radius: 3px;
box-shadow: 0 0 4px hsl(262 83% 68% / 0.2);
}
.outline-content::-webkit-scrollbar-thumb:hover {
background: hsl(262 83% 58% / 0.6);
box-shadow: 0 0 8px hsl(262 83% 68% / 0.4);
}
/* Tree Node */
.tree-node {
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.tree-node.drop-target-highlight {
background: hsl(262 83% 58% / 0.12);
border-radius: 10px;
box-shadow: inset 0 0 0 2px hsl(262 83% 58% / 0.4), 0 0 20px hsl(262 83% 68% / 0.3), 0 4px 12px hsl(262 83% 58% / 0.2);
}
.tree-node-content {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 12px;
margin: 0px 8px;
border-radius: 8px;
cursor: grab;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
background: transparent;
}
.tree-node-content:hover {
background: hsl(262 83% 58% / 0.15);
}
.tree-node-content.selected {
background: hsl(262 83% 58% / 0.3);
}
.tree-node-content.dragging {
opacity: 0.5;
background: hsl(262 83% 58% / 0.25);
cursor: grabbing;
transform: scale(0.98);
box-shadow: 0 4px 12px hsl(262 83% 58% / 0.3);
}
.tree-node-content.locked {
opacity: 0.6;
cursor: not-allowed;
}
.tree-node-content.hidden {
opacity: 0.4;
}
.expand-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: #9ca3af;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
width: 20px;
height: 20px;
border-radius: 4px;
}
.expand-button:hover {
background: hsl(262 83% 58% / 0.2);
color: hsl(262 83% 68%);
box-shadow: 0 0 8px hsl(262 83% 68% / 0.2);
}
.node-icon {
display: flex;
align-items: center;
justify-content: center;
color: hsl(262 83% 68%);
flex-shrink: 0;
}
.node-name {
flex: 1;
color: #e5e7eb;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
}
.node-controls {
display: flex;
}
.control-button {
background: none;
border: none;
color: #6b7280;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.control-button:hover {
background: hsl(262 83% 58% / 0.2);
color: hsl(262 83% 68%);
box-shadow: 0 0 6px hsl(262 83% 68% / 0.3);
}
.tree-children {
position: relative;
}
.tree-children::before {
content: "";
position: absolute;
left: 24px;
top: 0;
bottom: 0;
width: 1px;
background: linear-gradient(to bottom, hsl(262 83% 58% / 0.3), hsl(262 83% 58% / 0.1));
box-shadow: 0 0 4px hsl(262 83% 68% / 0.2);
}
/* Footer */
.outline-footer {
padding: 12px 16px;
background: rgba(0, 0, 0, 0.3);
border-top: 1px solid hsl(262 83% 58% / 0.1);
}
.footer-stats {
color: #9ca3af;
font-size: 12px;
font-weight: 500;
}
/* Toggle Button */
.outline-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: linear-gradient(135deg, hsl(262 83% 58% / 0.95), hsl(262 83% 58% / 0.85));
backdrop-filter: blur(12px);
border: 1px solid hsl(262 83% 58% / 0.4);
border-radius: 12px;
padding: 12px;
cursor: pointer;
color: white;
box-shadow: 0 10px 30px hsl(262 83% 58% / 0.4), 0 0 25px hsl(262 83% 68% / 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.outline-toggle:hover {
transform: scale(1.08) rotate(2deg);
box-shadow: 0 15px 45px hsl(262 83% 58% / 0.5), 0 0 40px hsl(262 83% 68% / 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.outline-toggle:active {
transform: scale(0.95);
}
/* Enhanced Glow Effect */
@keyframes cardGlow {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 0.8;
}
}
.outline-card:hover {
border-color: hsl(262 83% 58% / 0.5);
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.7), 0 0 80px hsl(262 83% 68% / 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 0 0 1px hsl(262 83% 58% / 0.2);
}
/* Responsive */
@media (max-width: 768px) {
.outline-overlay {
top: 10px;
right: 10px;
}
.outline-card {
width: 280px;
}
.outline-content {
max-height: 400px;
}
}
/* Root Drop Target Highlight */
.outline-content.root-drop-target {
background: hsl(262 83% 58% / 0.12);
box-shadow: inset 0 0 0 2px hsl(262 83% 58% / 0.4), inset 0 0 20px hsl(262 83% 68% / 0.2);
border-radius: 8px;
}
.outline-content.root-drop-target::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: hsl(262 83% 68%);
font-size: 14px;
font-weight: 600;
pointer-events: none;
text-shadow: 0 0 8px hsl(262 83% 68% / 0.5);
z-index: 10;
background: rgba(15, 15, 25, 0.8);
padding: 8px 16px;
border-radius: 8px;
border: 1px solid hsl(262 83% 58% / 0.4);
}
/* Multi-selection styling */
.tree-node-content.multi-selected {
background: hsl(262 83% 58% / 0.25);
}
.tree-node-content.multi-selected::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(to bottom, hsl(262 83% 68% / 0.8), hsl(262 83% 68% / 0.4));
box-shadow: 0 0 8px hsl(262 83% 68% / 0.4);
}
.tree-node-content.multi-selected:hover {
background: hsl(262 83% 58% / 0.35);
}
/* Selection count indicator (optional - add to footer) */
.footer-stats.multi-selection {
color: hsl(262 83% 68%);
font-weight: 600;
}

View File

@@ -1,14 +1,23 @@
import { useState, useRef, DragEvent, useCallback } from "react"; import { useState, useRef, DragEvent, useCallback } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { EyeIcon, LockIcon, FolderIcon, ChevronIcon, CubeIcon, AddIcon, DeleteIcon, KebebIcon } from "../../../components/icons/ExportCommonIcons"; import {
EyeIcon,
LockIcon,
FolderIcon,
ChevronIcon,
CubeIcon,
AddIcon,
KebebIcon,
CollapseAllIcon,
} from "../../../components/icons/ExportCommonIcons";
import RenameInput from "../../../components/ui/inputs/RenameInput"; import RenameInput from "../../../components/ui/inputs/RenameInput";
import { useSceneContext } from "../../scene/sceneContext"; import { useSceneContext } from "../../scene/sceneContext";
import { useSocketStore } from "../../../store/socket/useSocketStore"; import { useSocketStore } from "../../../store/socket/useSocketStore";
import useAssetResponseHandler from "../../collaboration/responseHandler/useAssetResponseHandler"; import useAssetResponseHandler from "../../collaboration/responseHandler/useAssetResponseHandler";
import "./outline.css";
import { getUserData } from "../../../functions/getUserData"; import { getUserData } from "../../../functions/getUserData";
import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
import clsx from "clsx";
interface DragState { interface DragState {
draggedItem: AssetGroupChild | null; draggedItem: AssetGroupChild | null;
@@ -51,7 +60,8 @@ const TreeNode = ({
const isLocked = item.isLocked; const isLocked = item.isLocked;
const isExpanded = isGroupNode ? item.isExpanded : false; const isExpanded = isGroupNode ? item.isExpanded : false;
const isSelected = !isGroupNode ? hasSelectedAsset(item.modelUuid) : 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 // Determine the parent group of this item
const getParentGroup = useCallback( const getParentGroup = useCallback(
@@ -76,7 +86,7 @@ const TreeNode = ({
// Highlight if this is the target group or belongs to the target group // Highlight if this is the target group or belongs to the target group
return thisGroupUuid === dragState.targetGroupUuid; return thisGroupUuid === dragState.targetGroupUuid;
}, [dragState, isGroupNode, item, getParentGroup]); }, [dragState, isGroupNode, item]);
const handleNodeDragStart = (e: DragEvent) => { const handleNodeDragStart = (e: DragEvent) => {
const parentGroupUuid = getParentGroup(item); const parentGroupUuid = getParentGroup(item);
@@ -112,9 +122,13 @@ const TreeNode = ({
return ( return (
<div className={`tree-node ${shouldShowHighlight ? "drop-target-highlight" : ""}`}> <div className={`tree-node ${shouldShowHighlight ? "drop-target-highlight" : ""}`}>
<div <div
className={`tree-node-content ${isLocked ? "locked" : ""} ${!isVisible ? "hidden" : ""} ${dragState.draggedItem === item ? "dragging" : ""} ${isSelected ? "selected" : ""} ${ className={clsx("tree-node-content", {
isMultiSelected ? "multi-selected" : "" locked: isLocked,
}`} hidden: !isVisible,
dragging: dragState.draggedItem === item,
selected: isSelected,
"multi-selected": isMultiSelected,
})}
style={{ paddingLeft: `${level * 25 + 8}px` }} style={{ paddingLeft: `${level * 25 + 8}px` }}
draggable={!isLocked} draggable={!isLocked}
onDragStart={handleNodeDragStart} onDragStart={handleNodeDragStart}
@@ -124,12 +138,17 @@ const TreeNode = ({
onClick={handleNodeClick} onClick={handleNodeClick}
> >
{isGroupNode && ( {isGroupNode && (
<button className="expand-button" onClick={() => onToggleExpand(item.groupUuid, !isExpanded)}> <button
className="expand-button"
onClick={() => onToggleExpand(item.groupUuid, !isExpanded)}
>
<ChevronIcon isOpen={isExpanded} /> <ChevronIcon isOpen={isExpanded} />
</button> </button>
)} )}
<div className="node-icon">{isGroupNode ? <FolderIcon isOpen={isExpanded} /> : <CubeIcon />}</div> <div className="node-icon">
{isGroupNode ? <FolderIcon isOpen={isExpanded} /> : <CubeIcon />}
</div>
<RenameInput value={itemName} onRename={() => {}} canEdit={true} /> <RenameInput value={itemName} onRename={() => {}} canEdit={true} />
@@ -166,7 +185,14 @@ const TreeNode = ({
</div> </div>
{isGroupNode && isExpanded && item.children && ( {isGroupNode && isExpanded && item.children && (
<div className="tree-children"> <div
className="tree-children"
style={
{
"--left": level,
} as React.CSSProperties
}
>
{item.children.map((child) => ( {item.children.map((child) => (
<TreeNode <TreeNode
key={isGroup(child) ? child.groupUuid : child.modelUuid} key={isGroup(child) ? child.groupUuid : child.modelUuid}
@@ -200,8 +226,24 @@ export const Outline = () => {
}); });
const [_, forceUpdate] = useState({}); const [_, forceUpdate] = useState({});
const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext(); const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext();
const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, toggleSelectedAsset, selectedAssets } = assetStore(); const {
const { groupHierarchy, isGroup, getGroupsContainingAsset, getFlatGroupChildren, setGroupExpanded, addChildToGroup, removeChildFromGroup, getGroupsContainingGroup } = assetGroupStore(); addSelectedAsset,
clearSelectedAssets,
getAssetById,
peekToggleVisibility,
toggleSelectedAsset,
selectedAssets,
} = assetStore();
const {
groupHierarchy,
isGroup,
getGroupsContainingAsset,
getFlatGroupChildren,
setGroupExpanded,
addChildToGroup,
removeChildFromGroup,
getGroupsContainingGroup,
} = assetGroupStore();
const { projectId } = useParams(); const { projectId } = useParams();
const { push3D } = undoRedo3DStore(); const { push3D } = undoRedo3DStore();
const { builderSocket } = useSocketStore(); const { builderSocket } = useSocketStore();
@@ -245,7 +287,11 @@ export const Outline = () => {
}) })
.then((data) => { .then((data) => {
if (!data.message || !data.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; return;
} }
if (data.message === "Model updated successfully" && data.data) { if (data.message === "Model updated successfully" && data.data) {
@@ -264,14 +310,22 @@ export const Outline = () => {
}; };
updateAssetInScene(model, () => { updateAssetInScene(model, () => {
echo.info(`${asset.isVisible ? "Hid" : "Unhid"} asset: ${model.modelName}`); echo.info(
`${asset.isVisible ? "Hid" : "Unhid"} asset: ${model.modelName}`
);
}); });
} else { } else {
echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); echo.error(
`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${
asset.modelName
}`
);
} }
}) })
.catch(() => { .catch(() => {
echo.error(`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`); echo.error(
`Error ${asset.isVisible ? "hiding" : "unhiding"} asset: ${asset.modelName}`
);
}); });
} else { } else {
const data = { const data = {
@@ -311,7 +365,7 @@ export const Outline = () => {
e.dataTransfer.effectAllowed = "move"; e.dataTransfer.effectAllowed = "move";
forceUpdate({}); forceUpdate({});
}, },
[isGroup] []
); );
const handleDragOver = useCallback( const handleDragOver = useCallback(
@@ -382,7 +436,10 @@ export const Outline = () => {
} }
// Update target group // 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.targetGroupUuid = targetGroupUuid;
dragStateRef.current.isRootTarget = false; dragStateRef.current.isRootTarget = false;
forceUpdate({}); forceUpdate({});
@@ -449,9 +506,15 @@ export const Outline = () => {
console.log("Dropped:", draggedItem, "into group:", targetGroupUuid); console.log("Dropped:", draggedItem, "into group:", targetGroupUuid);
if (isGroup(draggedItem)) { if (isGroup(draggedItem)) {
addChildToGroup(targetGroupUuid, { type: "Group", childrenUuid: draggedItem.groupUuid }); addChildToGroup(targetGroupUuid, {
type: "Group",
childrenUuid: draggedItem.groupUuid,
});
} else { } else {
addChildToGroup(targetGroupUuid, { type: "Asset", childrenUuid: draggedItem.modelUuid }); addChildToGroup(targetGroupUuid, {
type: "Asset",
childrenUuid: draggedItem.modelUuid,
});
} }
} }
@@ -490,7 +553,9 @@ export const Outline = () => {
// Update last selected reference // Update last selected reference
const flattened = getFlattenedHierarchy(); 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 }; lastSelectedRef.current = { item, index };
} }
} }
@@ -504,7 +569,9 @@ export const Outline = () => {
// Update last selected reference // Update last selected reference
const flattened = getFlattenedHierarchy(); 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 }; lastSelectedRef.current = { item, index };
} }
} }
@@ -512,7 +579,9 @@ export const Outline = () => {
// Shift+Click - range selection // Shift+Click - range selection
else if (isShiftClick) { else if (isShiftClick) {
const flattened = getFlattenedHierarchy(); 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; if (clickedIndex === -1) return;
@@ -558,7 +627,7 @@ export const Outline = () => {
}); });
} }
}, },
[scene.current, isGroup, getFlattenedHierarchy, clearSelectedAssets, addSelectedAsset, toggleSelectedAsset] [scene, isGroup, clearSelectedAssets, addSelectedAsset, getFlattenedHierarchy, toggleSelectedAsset]
); );
const handleOptionClick = useCallback( const handleOptionClick = useCallback(
@@ -620,78 +689,79 @@ export const Outline = () => {
} }
} }
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps
[selectedVersion, builderSocket, projectId, userId, organization] [selectedVersion, builderSocket, projectId, userId, organization]
); );
if (!isOpen) {
return (
<button className="outline-toggle" onClick={() => setIsOpen(true)}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M3 6h14 M3 10h14 M3 14h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
);
}
return ( return (
<div className="outline-overlay" onDragEnd={handleDragEnd}> <>
<div className="outline-card"> <div className="outline-overlay" onDragEnd={handleDragEnd}>
<div className="outline-header"> <div className="outline-card">
<div className="header-title"> <div className="outline-header">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"> <div className="header-title">
<path d="M3 3h6v6H3V3z M11 3h6v6h-6V3z M3 11h6v6H3v-6z M11 11h6v6h-6v-6z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> <p>Scene Hierarchy</p>
</svg> </div>
<h2>Scene Hierarchy</h2> <div className="outline-toolbar">
<button className="toolbar-button" title="Add Group">
<AddIcon />
</button>
{/* <button className="toolbar-button" title="Delete">
<DeleteIcon />
</button> */}
<button
className="toolbar-button"
title="Expand All"
onClick={() => {
const { assetGroups, setGroupExpanded } =
assetGroupStore.getState();
assetGroups.forEach((group) =>
setGroupExpanded(group.groupUuid, true)
);
}}
>
<CollapseAllIcon />
</button>
<button className="close-button" onClick={() => setIsOpen(!isOpen)}>
<ChevronIcon isOpen />
</button>
</div>
</div> </div>
<button className="close-button" onClick={() => setIsOpen(false)}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M5 5l10 10 M15 5L5 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
</div>
<div className="outline-toolbar"> {isOpen && (
<button className="toolbar-button" title="Add Group"> <div
<AddIcon /> className={`outline-content ${
</button> dragStateRef.current.isRootTarget ? "root-drop-target" : ""
<button className="toolbar-button" title="Delete"> }`}
<DeleteIcon /> onDragOver={handleRootDragOver}
</button>
<button
className="toolbar-button"
title="Expand All"
onClick={() => {
const { assetGroups, setGroupExpanded } = assetGroupStore.getState();
assetGroups.forEach((group) => setGroupExpanded(group.groupUuid, true));
}}
>
<ChevronIcon isOpen />
</button>
</div>
<div className={`outline-content ${dragStateRef.current.isRootTarget ? "root-drop-target" : ""}`} onDragOver={handleRootDragOver} onDrop={handleDrop}>
{groupHierarchy.map((item) => (
<TreeNode
key={isGroup(item) ? item.groupUuid : item.modelUuid}
item={item}
dragState={dragStateRef.current}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
onClick={handleClick} >
onToggleExpand={handleToggleExpand} {groupHierarchy.map((item) => (
onOptionClick={handleOptionClick} <TreeNode
/> key={isGroup(item) ? item.groupUuid : item.modelUuid}
))} item={item}
</div> dragState={dragStateRef.current}
onDragStart={handleDragStart}
<div className="outline-footer"> onDragOver={handleDragOver}
<span className={`footer-stats ${selectedAssets.length > 1 ? "multi-selection" : ""}`}> onDragLeave={handleDragLeave}
{selectedAssets.length > 1 ? `${selectedAssets.length} items selected` : `${groupHierarchy.length} root items`} onDrop={handleDrop}
</span> onClick={handleClick}
onToggleExpand={handleToggleExpand}
onOptionClick={handleOptionClick}
/>
))}
</div>
)}
</div> </div>
</div> </div>
</div> <div className="outline-footer">
<span
className={`footer-stats ${selectedAssets.length > 1 ? "multi-selection" : ""}`}
>
{selectedAssets.length > 1
? `${selectedAssets.length} items selected`
: `${groupHierarchy.length} root items`}
</span>
</div>
</>
); );
}; };

View File

@@ -0,0 +1,344 @@
@use "../abstracts/variables" as *;
@use "../abstracts/mixins" as *;
.outline-overlay {
padding: 0 4px;
}
.outline-card {
border-radius: $border-radius-extra-large;
overflow: hidden;
position: relative;
background: var(--background-color);
border: 1px solid var(--border-color);
box-shadow: var(--box-shadow-medium);
}
// Header
.outline-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 16px;
padding-right: 12px;
position: relative;
.header-title {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-color);
p {
margin: 0;
font-weight: 600;
}
}
.close-button {
@include flex-center;
background: none;
border: none;
color: var(--icon-default-color);
cursor: pointer;
height: 18px;
width: 18px;
min-width: 18px;
border-radius: $border-radius-small;
transition: all 0.2s;
&:hover {
background: var(--background-color-solid);
transform: rotate(-90deg);
}
}
}
// Toolbar
.outline-toolbar {
display: flex;
gap: 2px;
padding: 2px;
.toolbar-button {
@include flex-center;
cursor: pointer;
height: 18px;
width: 18px;
min-width: 18px;
border-radius: $border-radius-small;
font-size: 13px;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
background: var(--background-color-solid);
}
&:active {
transform: translateY(0);
}
}
}
// Content
.outline-content {
max-height: 52vh;
overflow-y: auto;
border-radius: $border-radius-medium;
position: relative;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: var(--background-color-secondary);
}
&::-webkit-scrollbar-thumb {
background: var(--accent-color);
border-radius: 3px;
box-shadow: 0 0 4px var(--accent-color);
&:hover {
background: var(--accent-color);
box-shadow: 0 0 8px var(--accent-color);
}
}
&.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);
border-radius: $border-radius-medium;
&::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--accent-color);
font-size: 14px;
font-weight: 600;
pointer-events: none;
text-shadow: 0 0 8px var(--accent-color);
z-index: 10;
background: var(--background-color-drop-down);
padding: 8px 16px;
border-radius: $border-radius-medium;
border: 1px solid var(--border-color-accent);
}
}
}
// Tree Node
.tree-node {
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.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);
}
.tree-node-content {
@include flex-center;
gap: 6px;
padding: 6px 12px;
border-radius: $border-radius-medium;
cursor: grab;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
background: transparent;
position: relative;
&:hover {
background: var(--background-color-accent);
}
&.selected {
background: var(--background-color-selected);
}
&.dragging {
opacity: 0.5;
background: var(--background-color-selected);
cursor: grabbing;
transform: scale(0.98);
box-shadow: 0 4px 12px var(--highlight-accent-color);
}
&.locked {
opacity: 0.6;
cursor: not-allowed;
}
&.hidden {
opacity: 0.4;
}
&.multi-selected {
background: var(--background-color-selected);
&::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--highlight-secondary-color);
box-shadow: 0 0 8px var(--highlight-secondary-color);
}
&:hover {
background: var(--background-color-accent);
}
}
}
.expand-button {
@include flex-center;
width: 20px;
height: 20px;
border-radius: $border-radius-small;
background: none;
border: none;
cursor: pointer;
color: var(--icon-default-color);
transition: all 0.2s;
&:hover {
background: var(--background-color-selected);
color: var(--accent-color);
box-shadow: 0 0 8px var(--highlight-accent-color);
}
}
.node-icon {
@include flex-center;
color: var(--accent-color);
flex-shrink: 0;
}
.node-name {
flex: 1;
color: var(--text-color);
font-size: var(--font-size-small);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
}
.node-controls {
display: flex;
.control-button {
@include flex-center;
background: none;
border: none;
color: var(--icon-default-color);
cursor: pointer;
padding: 4px;
border-radius: $border-radius-small;
transition: all 0.2s;
&:hover {
background: var(--background-color-selected);
color: var(--accent-color);
box-shadow: 0 0 6px var(--highlight-accent-color);
}
}
}
}
// Tree children lines
.tree-children {
position: relative;
--left: 1;
&::before {
content: "";
position: absolute;
left: calc(12px + (var(--left) * 26px));
top: 0;
bottom: 0;
width: 1px;
background: linear-gradient(
to bottom,
var(--accent-color),
var(--background-color-selected)
);
box-shadow: 0 0 4px var(--highlight-accent-color);
}
}
// Footer
.outline-footer {
position: fixed;
padding: 8px 16px;
width: 270px;
background: var(--background-color-solid);
bottom: -36px;
border-radius: 16px;
.footer-stats {
color: var(--text-color);
font-size: 12px;
font-weight: 500;
&.multi-selection {
color: var(--accent-color);
font-weight: 600;
}
}
}
// Toggle Button
.outline-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: $z-index-tools;
background: var(--background-color-button);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color-accent);
border-radius: $border-radius-extra-large;
padding: 12px;
cursor: pointer;
color: var(--text-button-color);
box-shadow: var(--box-shadow-medium);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
animation: fadeIn 0.3s;
&:hover {
transform: scale(1.08) rotate(2deg);
box-shadow: var(--box-shadow-heavy);
}
&:active {
transform: scale(0.95);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes cardGlow {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 0.8;
}
}

View File

@@ -39,6 +39,7 @@
@use "layout/compareLayout"; @use "layout/compareLayout";
@use "layout/resourceManagement.scss"; @use "layout/resourceManagement.scss";
@use "layout/previewModel"; @use "layout/previewModel";
@use "layout/assetOutline";
// pages // pages
@use "pages/dashboard"; @use "pages/dashboard";