diff --git a/apps/editor/components/ui/sidebar/app-sidebar.tsx b/apps/editor/components/ui/sidebar/app-sidebar.tsx index 40b93b6e..7a12fe66 100644 --- a/apps/editor/components/ui/sidebar/app-sidebar.tsx +++ b/apps/editor/components/ui/sidebar/app-sidebar.tsx @@ -1,89 +1,24 @@ "use client"; -// import JsonView from '@uiw/react-json-view' -import { ChevronDown, ChevronRight, Plus, X } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useShallow } from "zustand/shallow"; +import { useState } from "react"; import { IconRail, type PanelId } from "./icon-rail"; -import { Button } from "@/components/ui/primitives/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/primitives/tooltip"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/primitives/dialog"; import { Sidebar, SidebarContent, SidebarHeader, } from "@/components/ui/primitives/sidebar"; import { cn } from "@/lib/utils"; -import useEditor from "@/store/use-editor"; -import { useViewer } from "@pascal-app/viewer"; import { SettingsPanel } from "./panels/settings-panel"; import { SitePanel } from "./panels/site-panel"; -import { ZonePanel } from "./panels/zone-panel"; export function AppSidebar() { - // const isHelpOpen = useEditor((state) => state.isHelpOpen); - // const setIsHelpOpen = useEditor((state) => state.setIsHelpOpen); - // const isJsonInspectorOpen = useEditor((state) => state.isJsonInspectorOpen); - // const setIsJsonInspectorOpen = useEditor( - // (state) => state.setIsJsonInspectorOpen, - // ); - // const selectedNodeIds = useEditor((state) => state.selectedNodeIds); - // const handleDeleteSelected = useEditor((state) => state.handleDeleteSelected); - // const serializeLayout = useEditor((state) => state.serializeLayout); - const activeTool = useEditor((state) => state.tool); - const currentLevelId = useViewer((state) => state.selection.levelId); - const setPhase = useEditor((state) => state.setPhase); - const setMode = useEditor((state) => state.setMode); - const setActiveTool = useEditor((state) => state.setTool); - - const [jsonCollapsed, setJsonCollapsed] = useState(1); - const [mounted, setMounted] = useState(false); const [activePanel, setActivePanel] = useState("site"); - // Wait for client-side hydration to complete before rendering store-dependent content - useEffect(() => { - setMounted(true); - }, []); - - // Handle backspace key to delete selected elements - // useEffect(() => { - // const handleKeyDown = (event: KeyboardEvent) => { - // if (event.key === "Backspace" && selectedNodeIds.length > 0) { - // event.preventDefault(); - // handleDeleteSelected(); - // } - // }; - - // document.addEventListener("keydown", handleKeyDown); - // return () => document.removeEventListener("keydown", handleKeyDown); - // }, [selectedNodeIds, handleDeleteSelected]); - - // Auto-switch to zones panel when zone tool is active - useEffect(() => { - if (activeTool === "zone") { - setActivePanel("zones"); - } - }, [activeTool]); - const renderPanelContent = () => { switch (activePanel) { case "site": return ; - case "zones": - return ; - // case "collections": - // return ; case "settings": return ; default: @@ -95,8 +30,6 @@ export function AppSidebar() { switch (activePanel) { case "site": return "Site"; - case "zones": - return "Zones"; case "collections": return "Collections"; case "settings": @@ -106,84 +39,6 @@ export function AppSidebar() { } }; - const handleAddZone = () => { - if (currentLevelId) { - setPhase("structure"); - setMode("build"); - setActiveTool("zone"); - } - }; - - // const handleAddCollection = () => { - // const existingNames = collections.map((c) => c.name); - // let counter = 1; - // let newName = `Collection ${counter}`; - // while (existingNames.includes(newName)) { - // counter++; - // newName = `Collection ${counter}`; - // } - // addCollection(newName); - // }; - - const renderHeaderAction = () => { - switch (activePanel) { - case "zones": - return ( - - - - - - {currentLevelId ? "Add new zone" : "Select a level first"} - - - ); - // case "collections": - // if (addToCollectionState.isActive) { - // return ( - // - // - // - // - // Cancel - // - // ); - // } - // return ( - // - // - // - // - // Add new collection - // - // ); - default: - return null; - } - }; - return (
@@ -194,7 +49,6 @@ export function AppSidebar() {

{getPanelTitle()}

- {renderHeaderAction()}
- - {/* Dialogs */} - {/* - - - - - Build Data Inspector -
- - -
-
- - View the raw JSON structure of your current build - -
-
- -
-
-
- - - - - House Builder Controls - - - Click on grid intersections to place walls using the building - tools. -
- Hold spacebar to enable camera controls (orbit, pan, - zoom). -
- Use control modes (Select/Delete/Building) to switch - between different interactions. -
- Create multiple levels and organize your 3D objects and - guides within each level. -
- Click on level names to select them and expand/collapse - with the chevron icons. -
- Upload PNG/JPEG reference images as guides within each - level. -
- Save your build as JSON file for later use or database - storage. -
- Load previously saved builds from JSON files. -
- Inspect data to view the raw JSON structure of your - current build. -
- Export your 3D model as GLB file. -
-
-
-
- */}
); } diff --git a/apps/editor/components/ui/sidebar/icon-rail.tsx b/apps/editor/components/ui/sidebar/icon-rail.tsx index ddc6b8ee..9d6f1c46 100644 --- a/apps/editor/components/ui/sidebar/icon-rail.tsx +++ b/apps/editor/components/ui/sidebar/icon-rail.tsx @@ -1,6 +1,6 @@ "use client"; -import { Building2, Layers, Map as MapIcon, Settings } from "lucide-react"; +import { Building2, Layers, Settings } from "lucide-react"; import { Tooltip, TooltipContent, @@ -8,7 +8,7 @@ import { } from "@/components/ui/primitives/tooltip"; import { cn } from "@/lib/utils"; -export type PanelId = "site" | "zones" | "collections" | "settings"; +export type PanelId = "site" | "collections" | "settings"; interface IconRailProps { activePanel: PanelId; @@ -18,7 +18,6 @@ interface IconRailProps { const panels: { id: PanelId; icon: typeof Building2; label: string }[] = [ { id: "site", icon: Building2, label: "Site" }, - { id: "zones", icon: MapIcon, label: "Zones" }, { id: "collections", icon: Layers, label: "Collections" }, { id: "settings", icon: Settings, label: "Settings" }, ]; diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx index 52f5ac3e..ecafb6a6 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx @@ -1,14 +1,428 @@ -import { useScene } from "@pascal-app/core"; +import { + type BuildingNode, + LevelNode, + useScene, + type ZoneNode, +} from "@pascal-app/core"; +import { useViewer } from "@pascal-app/viewer"; +import { + Building2, + ChevronDown, + Hexagon, + Layers, + Plus, + Trash2, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import useEditor from "@/store/use-editor"; import { TreeNode } from "./tree-node"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/primitives/popover"; -export function SitePanel() { +// Preset colors for zones +const PRESET_COLORS = [ + "#3b82f6", // blue + "#22c55e", // green + "#eab308", // yellow + "#f97316", // orange + "#ef4444", // red + "#a855f7", // purple + "#ec4899", // pink + "#06b6d4", // cyan +]; + +// ============================================================================ +// SITE PHASE VIEW - Simple building buttons +// ============================================================================ + +function SitePhaseView() { + const nodes = useScene((state) => state.nodes); const rootNodeIds = useScene((state) => state.rootNodeIds); + const selectedBuildingId = useViewer((state) => state.selection.buildingId); + const setSelection = useViewer((state) => state.setSelection); + + const buildings = rootNodeIds + .map((id) => nodes[id]) + .filter((node): node is BuildingNode => node?.type === "building"); + + if (buildings.length === 0) { + return ( +
+ No buildings yet +
+ ); + } return ( -
- {rootNodeIds.map((nodeId) => ( - +
+ {buildings.map((building) => ( + ))}
); } + +// ============================================================================ +// STRUCTURE/FURNISH PHASE VIEW - Building dropdown + Levels + Content +// ============================================================================ + +function BuildingSelector() { + const nodes = useScene((state) => state.nodes); + const rootNodeIds = useScene((state) => state.rootNodeIds); + const selectedBuildingId = useViewer((state) => state.selection.buildingId); + const setSelection = useViewer((state) => state.setSelection); + + const buildings = rootNodeIds + .map((id) => nodes[id]) + .filter((node): node is BuildingNode => node?.type === "building"); + + const selectedBuilding = selectedBuildingId + ? (nodes[selectedBuildingId] as BuildingNode) + : null; + + if (buildings.length === 0) return null; + + // If only one building, just show it as a header + if (buildings.length === 1) { + return ( +
+ + + {buildings[0]?.name || "Building"} + +
+ ); + } + + return ( + + + + + + {buildings.map((building) => ( + + ))} + + + ); +} + +function LevelsSection() { + const nodes = useScene((state) => state.nodes); + const createNode = useScene((state) => state.createNode); + const selectedBuildingId = useViewer((state) => state.selection.buildingId); + const selectedLevelId = useViewer((state) => state.selection.levelId); + const setSelection = useViewer((state) => state.setSelection); + + const building = selectedBuildingId + ? (nodes[selectedBuildingId] as BuildingNode) + : null; + + if (!building) return null; + + const levels = building.children + .map((id) => nodes[id]) + .filter((node): node is LevelNode => node?.type === "level"); + + const handleAddLevel = () => { + const newLevel = LevelNode.parse({ + level: levels.length, + children: [], + parentId: building.id, + }); + createNode(newLevel, building.id); + setSelection({ levelId: newLevel.id }); + }; + + return ( +
+ {/* Header */} +
+ + Levels + + +
+ + {/* Level buttons */} +
+ {levels.map((level) => ( + + ))} + {levels.length === 0 && ( +
+ No levels yet +
+ )} +
+
+ ); +} + +function LayerToggle() { + const structureLayer = useEditor((state) => state.structureLayer); + const setStructureLayer = useEditor((state) => state.setStructureLayer); + + return ( +
+ + +
+ ); +} + +function ZoneItem({ zone }: { zone: ZoneNode }) { + const deleteNode = useScene((state) => state.deleteNode); + const updateNode = useScene((state) => state.updateNode); + const selectedZoneId = useViewer((state) => state.selection.zoneId); + const setSelection = useViewer((state) => state.setSelection); + const setPhase = useEditor((state) => state.setPhase); + const setMode = useEditor((state) => state.setMode); + + const isSelected = selectedZoneId === zone.id; + + const handleClick = () => { + setSelection({ zoneId: zone.id }); + setPhase("structure"); + setMode("select"); + }; + + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + deleteNode(zone.id); + if (isSelected) { + setSelection({ zoneId: null }); + } + }; + + const handleColorChange = (color: string) => { + updateNode(zone.id, { color }); + }; + + return ( +
+ + +
+ + + + {zone.name} + +
+ ); +} + +function ContentSection() { + const nodes = useScene((state) => state.nodes); + const selectedLevelId = useViewer((state) => state.selection.levelId); + const structureLayer = useEditor((state) => state.structureLayer); + const setPhase = useEditor((state) => state.setPhase); + const setMode = useEditor((state) => state.setMode); + const setTool = useEditor((state) => state.setTool); + + const level = selectedLevelId ? (nodes[selectedLevelId] as LevelNode) : null; + + if (!level) { + return ( +
+ Select a level to view content +
+ ); + } + + if (structureLayer === "zones") { + // Show zones for this level + const levelZones = Object.values(nodes).filter( + (node): node is ZoneNode => + node.type === "zone" && node.parentId === selectedLevelId + ); + + const handleAddZone = () => { + setPhase("structure"); + setMode("build"); + setTool("zone"); + }; + + if (levelZones.length === 0) { + return ( +
+ No zones on this level.{" "} + +
+ ); + } + + return ( +
+ {levelZones.map((zone) => ( + + ))} +
+ ); + } + + // Show elements (walls, items, etc.) for this level + if (level.children.length === 0) { + return ( +
+ No elements on this level +
+ ); + } + + return ( +
+ {level.children.map((childId) => ( + + ))} +
+ ); +} + +function StructurePhaseView() { + return ( +
+ + + +
+ +
+
+ ); +} + +// ============================================================================ +// MAIN SITE PANEL +// ============================================================================ + +export function SitePanel() { + const phase = useEditor((state) => state.phase); + + if (phase === "site") { + return ; + } + + return ; +} diff --git a/apps/editor/store/use-editor.tsx b/apps/editor/store/use-editor.tsx index 75027e46..717f339c 100644 --- a/apps/editor/store/use-editor.tsx +++ b/apps/editor/store/use-editor.tsx @@ -38,6 +38,8 @@ export type CatalogCategory = | 'window' | 'door' +export type StructureLayer = 'zones' | 'elements' + // Combined tool type export type Tool = SiteTool | StructureTool | FurnishTool @@ -48,6 +50,8 @@ type EditorState = { setMode: (mode: Mode) => void tool: Tool | null setTool: (tool: Tool | null) => void + structureLayer: StructureLayer + setStructureLayer: (layer: StructureLayer) => void catalogCategory: CatalogCategory | null setCatalogCategory: (category: CatalogCategory | null) => void selectedItem: Asset | null @@ -104,6 +108,8 @@ const useEditor = create()((set, get) => ({ setMode: (mode) => set({ mode }), tool: null, setTool: (tool) => set({ tool }), + structureLayer: 'elements', + setStructureLayer: (layer) => set({ structureLayer: layer }), catalogCategory: null, setCatalogCategory: (category) => set({ catalogCategory: category }), selectedItem: null,