diff --git a/apps/editor/components/ui/sidebar/panels/site-panel.tsx b/apps/editor/components/ui/sidebar/panels/site-panel.tsx
deleted file mode 100644
index c9f86518..00000000
--- a/apps/editor/components/ui/sidebar/panels/site-panel.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useScene } from "@pascal-app/core";
-
-export function SitePanel() {
- const rootNodes = useScene((state) => state.rootNodeIds);
- return (
-
- {rootNodes.map((nodeId) => (
-
{nodeId}
- ))}
-
- );
-}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/building-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/building-tree-node.tsx
new file mode 100644
index 00000000..51d0c65e
--- /dev/null
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/building-tree-node.tsx
@@ -0,0 +1,62 @@
+import { BuildingNode, LevelNode, useScene } from "@pascal-app/core";
+import { Building2, Plus } from "lucide-react";
+import { useState } from "react";
+import { TreeNode, TreeNodeWrapper } from "./tree-node";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/primitives/tooltip";
+
+interface BuildingTreeNodeProps {
+ node: BuildingNode;
+ depth: number;
+}
+
+export function BuildingTreeNode({ node, depth }: BuildingTreeNodeProps) {
+ const [expanded, setExpanded] = useState(true);
+ const createNode = useScene((state) => state.createNode);
+
+ const handleClick = () => {
+ // Handle building selection
+ };
+
+ const handleAddLevel = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const newLevel = LevelNode.parse({
+ level: node.children.length,
+ children: [],
+ parentId: node.id,
+ });
+ createNode(newLevel, node.id);
+ };
+
+ return (
+
}
+ label={node.name || "Building"}
+ depth={depth}
+ hasChildren={node.children.length > 0}
+ expanded={expanded}
+ onToggle={() => setExpanded(!expanded)}
+ onClick={handleClick}
+ actions={
+
+
+
+
+ Add new level
+
+ }
+ >
+ {node.children.map((childId) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx
new file mode 100644
index 00000000..52f5ac3e
--- /dev/null
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx
@@ -0,0 +1,14 @@
+import { useScene } from "@pascal-app/core";
+import { TreeNode } from "./tree-node";
+
+export function SitePanel() {
+ const rootNodeIds = useScene((state) => state.rootNodeIds);
+
+ return (
+
+ {rootNodeIds.map((nodeId) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx
new file mode 100644
index 00000000..acc8877d
--- /dev/null
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx
@@ -0,0 +1,26 @@
+import { ItemNode } from "@pascal-app/core";
+import { Box } from "lucide-react";
+import { TreeNodeWrapper } from "./tree-node";
+
+interface ItemTreeNodeProps {
+ node: ItemNode;
+ depth: number;
+}
+
+export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
+ const handleClick = () => {
+ // Handle item selection
+ };
+
+ return (
+
}
+ label={node.name || node.asset.name || "Item"}
+ depth={depth}
+ hasChildren={false}
+ expanded={false}
+ onToggle={() => {}}
+ onClick={handleClick}
+ />
+ );
+}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
new file mode 100644
index 00000000..8666e42a
--- /dev/null
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
@@ -0,0 +1,39 @@
+import { LevelNode } from "@pascal-app/core";
+import { useViewer } from "@pascal-app/viewer";
+import { Layers } from "lucide-react";
+import { useState } from "react";
+import { TreeNode, TreeNodeWrapper } from "./tree-node";
+
+interface LevelTreeNodeProps {
+ node: LevelNode;
+ depth: number;
+}
+
+export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
+ const [expanded, setExpanded] = useState(true);
+ const currentLevelId = useViewer((state) => state.currentLevelId);
+ const setCurrentLevel = useViewer((state) => state.setCurrentLevelId);
+
+ const handleClick = () => {
+ setCurrentLevel(node.id);
+ };
+
+ const isSelected = currentLevelId === node.id;
+
+ return (
+
}
+ label={node.name || `Level ${node.level}`}
+ depth={depth}
+ hasChildren={node.children.length > 0}
+ expanded={expanded}
+ onToggle={() => setExpanded(!expanded)}
+ onClick={handleClick}
+ isSelected={isSelected}
+ >
+ {node.children.map((childId) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx
new file mode 100644
index 00000000..fe12921b
--- /dev/null
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx
@@ -0,0 +1,101 @@
+import { AnyNodeId, useScene } from "@pascal-app/core";
+import { ChevronDown, ChevronRight } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { BuildingTreeNode } from "./building-tree-node";
+import { LevelTreeNode } from "./level-tree-node";
+import { WallTreeNode } from "./wall-tree-node";
+import { ItemTreeNode } from "./item-tree-node";
+
+interface TreeNodeProps {
+ nodeId: AnyNodeId;
+ depth?: number;
+}
+
+export function TreeNode({ nodeId, depth = 0 }: TreeNodeProps) {
+ const node = useScene((state) => state.nodes[nodeId]);
+
+ if (!node) return null;
+
+ switch (node.type) {
+ case "building":
+ return
;
+ case "level":
+ return
;
+ case "wall":
+ return
;
+ case "item":
+ return
;
+ default:
+ return null;
+ }
+}
+
+interface TreeNodeWrapperProps {
+ icon: React.ReactNode;
+ label: string;
+ depth: number;
+ hasChildren: boolean;
+ expanded: boolean;
+ onToggle: () => void;
+ onClick: () => void;
+ actions?: React.ReactNode;
+ children?: React.ReactNode;
+ isSelected?: boolean;
+}
+
+export function TreeNodeWrapper({
+ icon,
+ label,
+ depth,
+ hasChildren,
+ expanded,
+ onToggle,
+ onClick,
+ actions,
+ children,
+ isSelected,
+}: TreeNodeWrapperProps) {
+ return (
+
+
+
+
+
+ {icon}
+
+ {label}
+
+ {actions && (
+
+ {actions}
+
+ )}
+
+ {expanded && children}
+
+ );
+}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx
new file mode 100644
index 00000000..3421dc4e
--- /dev/null
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx
@@ -0,0 +1,39 @@
+import { WallNode, useScene } from "@pascal-app/core";
+import { Square } from "lucide-react";
+import { useState } from "react";
+import { TreeNode, TreeNodeWrapper } from "./tree-node";
+
+interface WallTreeNodeProps {
+ node: WallNode;
+ depth: number;
+}
+
+export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
+ const [expanded, setExpanded] = useState(false);
+
+ const handleClick = () => {
+ // Handle wall selection
+ useScene.getState().markDirty(node.id);
+ };
+
+ const wallLength = Math.sqrt(
+ Math.pow(node.end[0] - node.start[0], 2) +
+ Math.pow(node.end[1] - node.start[1], 2)
+ ).toFixed(1);
+
+ return (
+
}
+ label={node.name || `Wall (${wallLength}m)`}
+ depth={depth}
+ hasChildren={node.children.length > 0}
+ expanded={expanded}
+ onToggle={() => setExpanded(!expanded)}
+ onClick={handleClick}
+ >
+ {node.children.map((childId) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/viewer/src/components/viewer/viewer.tsx b/packages/viewer/src/components/viewer/viewer.tsx
index 1b0f73aa..305827ba 100644
--- a/packages/viewer/src/components/viewer/viewer.tsx
+++ b/packages/viewer/src/components/viewer/viewer.tsx
@@ -32,7 +32,7 @@ const Viewer: React.FC
= ({ children }) => {
camera={{ position: [3, 3, 3], fov: 50 }}
>
-
+