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:
2
app/package-lock.json
generated
2
app/package-lock.json
generated
@@ -29,6 +29,7 @@
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dxf-parser": "^1.1.2",
|
||||
"glob": "^11.0.0",
|
||||
"gsap": "^3.12.5",
|
||||
@@ -8760,6 +8761,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dxf-parser": "^1.1.2",
|
||||
"glob": "^11.0.0",
|
||||
"gsap": "^3.12.5",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,6 @@ import { recentlyViewedApi } from "../../../services/dashboard/recentlyViewedApi
|
||||
import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
|
||||
import { getVersionHistoryApi } from "../../../services/factoryBuilder/versionControl/getVersionHistoryApi";
|
||||
import { getUserData } from "../../../functions/getUserData";
|
||||
import { Outline } from "../../../modules/builder/testUi/outline";
|
||||
|
||||
function MainScene() {
|
||||
const { setMainState, clearComparisonState } = useSimulationState();
|
||||
@@ -186,7 +185,6 @@ function MainScene() {
|
||||
<>
|
||||
{!selectedUser && (
|
||||
<>
|
||||
<Outline />
|
||||
<KeyPressListener />
|
||||
{loadingProgress > 0 && <LoadingPage progress={loadingProgress} />}
|
||||
{!isPlaying && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ToggleHeader from "../../ui/inputs/ToggleHeader";
|
||||
import Outline from "./Outline";
|
||||
// import Outline from "./Outline";
|
||||
import Header from "./Header";
|
||||
import { useToggleStore } from "../../../store/ui/useUIToggleStore";
|
||||
import Assets from "./assetList/Assets";
|
||||
@@ -9,6 +9,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";
|
||||
|
||||
const SideBarLeft: React.FC = () => {
|
||||
const [activeOption, setActiveOption] = useState("Widgets");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
import { useState, useRef, DragEvent, useCallback } from "react";
|
||||
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 { useSceneContext } from "../../scene/sceneContext";
|
||||
import { useSocketStore } from "../../../store/socket/useSocketStore";
|
||||
import useAssetResponseHandler from "../../collaboration/responseHandler/useAssetResponseHandler";
|
||||
import "./outline.css";
|
||||
|
||||
import { getUserData } from "../../../functions/getUserData";
|
||||
import { setAssetsApi } from "../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface DragState {
|
||||
draggedItem: AssetGroupChild | null;
|
||||
@@ -51,7 +60,8 @@ 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(
|
||||
@@ -76,7 +86,7 @@ const TreeNode = ({
|
||||
|
||||
// Highlight if this is the target group or belongs to the target group
|
||||
return thisGroupUuid === dragState.targetGroupUuid;
|
||||
}, [dragState, isGroupNode, item, getParentGroup]);
|
||||
}, [dragState, isGroupNode, item]);
|
||||
|
||||
const handleNodeDragStart = (e: DragEvent) => {
|
||||
const parentGroupUuid = getParentGroup(item);
|
||||
@@ -112,9 +122,13 @@ const TreeNode = ({
|
||||
return (
|
||||
<div className={`tree-node ${shouldShowHighlight ? "drop-target-highlight" : ""}`}>
|
||||
<div
|
||||
className={`tree-node-content ${isLocked ? "locked" : ""} ${!isVisible ? "hidden" : ""} ${dragState.draggedItem === item ? "dragging" : ""} ${isSelected ? "selected" : ""} ${
|
||||
isMultiSelected ? "multi-selected" : ""
|
||||
}`}
|
||||
className={clsx("tree-node-content", {
|
||||
locked: isLocked,
|
||||
hidden: !isVisible,
|
||||
dragging: dragState.draggedItem === item,
|
||||
selected: isSelected,
|
||||
"multi-selected": isMultiSelected,
|
||||
})}
|
||||
style={{ paddingLeft: `${level * 25 + 8}px` }}
|
||||
draggable={!isLocked}
|
||||
onDragStart={handleNodeDragStart}
|
||||
@@ -124,12 +138,17 @@ const TreeNode = ({
|
||||
onClick={handleNodeClick}
|
||||
>
|
||||
{isGroupNode && (
|
||||
<button className="expand-button" onClick={() => onToggleExpand(item.groupUuid, !isExpanded)}>
|
||||
<button
|
||||
className="expand-button"
|
||||
onClick={() => onToggleExpand(item.groupUuid, !isExpanded)}
|
||||
>
|
||||
<ChevronIcon isOpen={isExpanded} />
|
||||
</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} />
|
||||
|
||||
@@ -166,7 +185,14 @@ const TreeNode = ({
|
||||
</div>
|
||||
|
||||
{isGroupNode && isExpanded && item.children && (
|
||||
<div className="tree-children">
|
||||
<div
|
||||
className="tree-children"
|
||||
style={
|
||||
{
|
||||
"--left": level,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{item.children.map((child) => (
|
||||
<TreeNode
|
||||
key={isGroup(child) ? child.groupUuid : child.modelUuid}
|
||||
@@ -200,8 +226,24 @@ export const Outline = () => {
|
||||
});
|
||||
const [_, forceUpdate] = useState({});
|
||||
const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext();
|
||||
const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, toggleSelectedAsset, selectedAssets } = assetStore();
|
||||
const { groupHierarchy, isGroup, getGroupsContainingAsset, getFlatGroupChildren, setGroupExpanded, addChildToGroup, removeChildFromGroup, getGroupsContainingGroup } = assetGroupStore();
|
||||
const {
|
||||
addSelectedAsset,
|
||||
clearSelectedAssets,
|
||||
getAssetById,
|
||||
peekToggleVisibility,
|
||||
toggleSelectedAsset,
|
||||
selectedAssets,
|
||||
} = assetStore();
|
||||
const {
|
||||
groupHierarchy,
|
||||
isGroup,
|
||||
getGroupsContainingAsset,
|
||||
getFlatGroupChildren,
|
||||
setGroupExpanded,
|
||||
addChildToGroup,
|
||||
removeChildFromGroup,
|
||||
getGroupsContainingGroup,
|
||||
} = assetGroupStore();
|
||||
const { projectId } = useParams();
|
||||
const { push3D } = undoRedo3DStore();
|
||||
const { builderSocket } = useSocketStore();
|
||||
@@ -245,7 +287,11 @@ 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) {
|
||||
@@ -264,14 +310,22 @@ 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 = {
|
||||
@@ -311,7 +365,7 @@ export const Outline = () => {
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
forceUpdate({});
|
||||
},
|
||||
[isGroup]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
@@ -382,7 +436,10 @@ 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({});
|
||||
@@ -449,9 +506,15 @@ export const Outline = () => {
|
||||
console.log("Dropped:", draggedItem, "into group:", targetGroupUuid);
|
||||
|
||||
if (isGroup(draggedItem)) {
|
||||
addChildToGroup(targetGroupUuid, { type: "Group", childrenUuid: draggedItem.groupUuid });
|
||||
addChildToGroup(targetGroupUuid, {
|
||||
type: "Group",
|
||||
childrenUuid: draggedItem.groupUuid,
|
||||
});
|
||||
} 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
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -504,7 +569,9 @@ 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 };
|
||||
}
|
||||
}
|
||||
@@ -512,7 +579,9 @@ 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;
|
||||
|
||||
@@ -558,7 +627,7 @@ export const Outline = () => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[scene.current, isGroup, getFlattenedHierarchy, clearSelectedAssets, addSelectedAsset, toggleSelectedAsset]
|
||||
[scene, isGroup, clearSelectedAssets, addSelectedAsset, getFlattenedHierarchy, toggleSelectedAsset]
|
||||
);
|
||||
|
||||
const handleOptionClick = useCallback(
|
||||
@@ -620,78 +689,79 @@ export const Outline = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[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 (
|
||||
<div className="outline-overlay" onDragEnd={handleDragEnd}>
|
||||
<div className="outline-card">
|
||||
<div className="outline-header">
|
||||
<div className="header-title">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M3 3h6v6H3V3z M11 3h6v6h-6V3z M3 11h6v6H3v-6z M11 11h6v6h-6v-6z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
<h2>Scene Hierarchy</h2>
|
||||
<>
|
||||
<div className="outline-overlay" onDragEnd={handleDragEnd}>
|
||||
<div className="outline-card">
|
||||
<div className="outline-header">
|
||||
<div className="header-title">
|
||||
<p>Scene Hierarchy</p>
|
||||
</div>
|
||||
<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>
|
||||
<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">
|
||||
<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));
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`outline-content ${
|
||||
dragStateRef.current.isRootTarget ? "root-drop-target" : ""
|
||||
}`}
|
||||
onDragOver={handleRootDragOver}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
onOptionClick={handleOptionClick}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
>
|
||||
{groupHierarchy.map((item) => (
|
||||
<TreeNode
|
||||
key={isGroup(item) ? item.groupUuid : item.modelUuid}
|
||||
item={item}
|
||||
dragState={dragStateRef.current}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
onOptionClick={handleOptionClick}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
344
app/src/styles/layout/_assetOutline.scss
Normal file
344
app/src/styles/layout/_assetOutline.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@
|
||||
@use "layout/compareLayout";
|
||||
@use "layout/resourceManagement.scss";
|
||||
@use "layout/previewModel";
|
||||
@use "layout/assetOutline";
|
||||
|
||||
// pages
|
||||
@use "pages/dashboard";
|
||||
|
||||
Reference in New Issue
Block a user