diff --git a/app/package-lock.json b/app/package-lock.json
index 41110ce..b2ba539 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -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"
}
diff --git a/app/package.json b/app/package.json
index 9cee022..b370228 100644
--- a/app/package.json
+++ b/app/package.json
@@ -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",
diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx
index a8fe70b..5cd6b26 100644
--- a/app/src/components/icons/ExportCommonIcons.tsx
+++ b/app/src/components/icons/ExportCommonIcons.tsx
@@ -1,1761 +1,1750 @@
export function SearchIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function ArrowIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function FocusIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function LockIcon({ isLocked }: { isLocked: boolean }) {
- return isLocked ? (
-
- ) : (
-
- );
+ return isLocked ? (
+
+ ) : (
+
+ );
}
export function EyeIcon({ isClosed }: { isClosed: boolean }) {
- return isClosed ? (
-
- ) : (
-
- );
+ return isClosed ? (
+
+ ) : (
+
+ );
}
export function KebebIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function AddIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function CloseIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function SettingsIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function HelpIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function TrashIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function FilterIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function EyeDroperIcon({ isActive }: { isActive: boolean }) {
- return (
-
- );
+ return (
+
+ );
}
export function TickIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function UndoIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function RedoIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function ResizeHeightIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function RemoveIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function InfoIcon() {
- return (
-
- );
+ return (
+
+ );
}
export function AIIcon() {
- return (
-
- );
+ return (
+
+ );
}
export const KebabIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const DublicateIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const DeleteIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const HourlySimulationIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const DailyProductionIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const MonthlyROI = () => {
- return (
-
- );
+ return (
+
+ );
};
export const ExpandIcon = ({ isActive }: { isActive: boolean }) => {
- return isActive ? (
-
- ) : (
-
- );
+ return isActive ? (
+
+ ) : (
+
+ );
};
export const ExpandIcon2 = () => {
- return (
-
- );
+ return (
+
+ );
};
export const StartIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const EndIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const SpeedIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const LogListIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const LogTickIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const LogInfoIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const WarningIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const ErrorIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const LocationIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const SaveDiskIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const WalkIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const EyeCloseIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const SaveIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const FolderIcon = ({ isOpen }: { isOpen: boolean }) => (
-
+
);
export const CubeIcon = () => (
-
+
);
export const ChevronIcon = ({ isOpen }: { isOpen: boolean }) => {
- return isOpen ? (
-
- ) : (
-
- );
+ return isOpen ? (
+
+ ) : (
+
+ );
};
+export const CollapseAllIcon = () => {
+ return (
+
+ );
+};
export const SaveVersionIcon = () => {
- return (
-
- );
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
};
export const RenameVersionIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const FinishEditIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const PerformanceIcon = () => {
- return (
-
- );
+
+
+
+
+
+
+
+
+ );
};
export const GreenTickIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const SuccessIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const AlertIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const NavigationIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const HangTagIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const DecalInfoIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const LayeringBottomIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const LayeringTopIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const ValueUpdateIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const ListTaskIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const LocationPinIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const ClockThreeIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const SlectedTickIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const HourGlassIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const TargetIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const RightHalfFillCircleIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
export const PerformanceStatsIcon = () => {
- return (
-
- );
+ return (
+
+ );
};
diff --git a/app/src/components/layout/scenes/MainScene.tsx b/app/src/components/layout/scenes/MainScene.tsx
index 036b3ae..b25acf2 100644
--- a/app/src/components/layout/scenes/MainScene.tsx
+++ b/app/src/components/layout/scenes/MainScene.tsx
@@ -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 && (
<>
-
{loadingProgress > 0 && }
{!isPlaying && (
diff --git a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx
index fc84dc6..20dd25d 100644
--- a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx
+++ b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx
@@ -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");
diff --git a/app/src/modules/builder/testUi/outline.css b/app/src/modules/builder/testUi/outline.css
deleted file mode 100644
index 765ca66..0000000
--- a/app/src/modules/builder/testUi/outline.css
+++ /dev/null
@@ -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;
-}
diff --git a/app/src/modules/builder/testUi/outline.tsx b/app/src/modules/builder/testUi/outline.tsx
index eb8b0fc..0e65678 100644
--- a/app/src/modules/builder/testUi/outline.tsx
+++ b/app/src/modules/builder/testUi/outline.tsx
@@ -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 (
{isGroupNode && (
-
{isGroupNode && isExpanded && item.children && (
-
+
{item.children.map((child) => (
{
});
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 (
- setIsOpen(true)}>
-
-
- );
- }
-
return (
-
-
-
-
-
-
Scene Hierarchy
+ <>
+
+
+
+
+
+
+
+
+ {/*
+
+ */}
+
{
+ const { assetGroups, setGroupExpanded } =
+ assetGroupStore.getState();
+ assetGroups.forEach((group) =>
+ setGroupExpanded(group.groupUuid, true)
+ );
+ }}
+ >
+
+
+
setIsOpen(!isOpen)}>
+
+
+
-
setIsOpen(false)}>
-
-
-
-
-
-
-
-
-
-
-
{
- const { assetGroups, setGroupExpanded } = assetGroupStore.getState();
- assetGroups.forEach((group) => setGroupExpanded(group.groupUuid, true));
- }}
- >
-
-
-
-
-
- {groupHierarchy.map((item) => (
-
- ))}
-
-
-
- 1 ? "multi-selection" : ""}`}>
- {selectedAssets.length > 1 ? `${selectedAssets.length} items selected` : `${groupHierarchy.length} root items`}
-
+ >
+ {groupHierarchy.map((item) => (
+
+ ))}
+
+ )}
-
+
+ 1 ? "multi-selection" : ""}`}
+ >
+ {selectedAssets.length > 1
+ ? `${selectedAssets.length} items selected`
+ : `${groupHierarchy.length} root items`}
+
+
+ >
);
};
diff --git a/app/src/styles/layout/_assetOutline.scss b/app/src/styles/layout/_assetOutline.scss
new file mode 100644
index 0000000..0f777b7
--- /dev/null
+++ b/app/src/styles/layout/_assetOutline.scss
@@ -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;
+ }
+}
diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss
index ea3ab8e..19b4325 100644
--- a/app/src/styles/main.scss
+++ b/app/src/styles/main.scss
@@ -39,6 +39,7 @@
@use "layout/compareLayout";
@use "layout/resourceManagement.scss";
@use "layout/previewModel";
+@use "layout/assetOutline";
// pages
@use "pages/dashboard";