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 a6924faf..04952346 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx @@ -49,8 +49,10 @@ function SitePhaseView() { const selectedBuildingId = useViewer((state) => state.selection.buildingId); const setSelection = useViewer((state) => state.setSelection); - const buildings = rootNodeIds - .map((id) => nodes[id]) + // Get site node and its building children + const siteNode = rootNodeIds[0] ? nodes[rootNodeIds[0]] : null; + const buildings = (siteNode?.type === 'site' ? siteNode.children : []) + .map((child) => typeof child === 'string' ? nodes[child] : child) .filter((node): node is BuildingNode => node?.type === "building"); if (buildings.length === 0) { @@ -92,8 +94,10 @@ function BuildingSelector() { const selectedBuildingId = useViewer((state) => state.selection.buildingId); const setSelection = useViewer((state) => state.setSelection); - const buildings = rootNodeIds - .map((id) => nodes[id]) + // Get site node and its building children + const siteNode = rootNodeIds[0] ? nodes[rootNodeIds[0]] : null; + const buildings = (siteNode?.type === 'site' ? siteNode.children : []) + .map((child) => typeof child === 'string' ? nodes[child] : child) .filter((node): node is BuildingNode => node?.type === "building"); const selectedBuilding = selectedBuildingId diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts index 080da1ce..60966b9b 100644 --- a/packages/core/src/events/bus.ts +++ b/packages/core/src/events/bus.ts @@ -1,6 +1,6 @@ import type { ThreeEvent } from '@react-three/fiber' import mitt from 'mitt' -import type { BuildingNode, CeilingNode, ItemNode, LevelNode, RoofNode, SlabNode, WallNode, ZoneNode } from '../schema' +import type { BuildingNode, CeilingNode, ItemNode, LevelNode, RoofNode, SiteNode, SlabNode, WallNode, ZoneNode } from '../schema' import type { AnyNode } from '../schema/types' // Base event interfaces @@ -20,6 +20,7 @@ export interface NodeEvent { export type WallEvent = NodeEvent export type ItemEvent = NodeEvent +export type SiteEvent = NodeEvent export type BuildingEvent = NodeEvent export type LevelEvent = NodeEvent export type ZoneEvent = NodeEvent @@ -64,6 +65,7 @@ type CameraControlEvents = { type EditorEvents = GridEvents & NodeEvents<'wall', WallEvent> & NodeEvents<'item', ItemEvent> & + NodeEvents<'site', SiteEvent> & NodeEvents<'building', BuildingEvent> & NodeEvents<'level', LevelEvent> & NodeEvents<'zone', ZoneEvent> & diff --git a/packages/core/src/hooks/scene-registry/scene-registry.ts b/packages/core/src/hooks/scene-registry/scene-registry.ts index 3f13c69d..0e0b7a49 100644 --- a/packages/core/src/hooks/scene-registry/scene-registry.ts +++ b/packages/core/src/hooks/scene-registry/scene-registry.ts @@ -8,6 +8,7 @@ export const sceneRegistry = { // Categorized lookups: Type -> Set of IDs // Using a Set is faster for adding/deleting than an Array byType: { + site: new Set(), building: new Set(), ceiling: new Set(), level: new Set(), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dbad2144..d86a0ffd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,6 +8,7 @@ export type { ItemEvent, LevelEvent, NodeEvent, + SiteEvent, SlabEvent, WallEvent, ZoneEvent, diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts index 64ad6f7c..3708f460 100644 --- a/packages/core/src/store/use-scene.ts +++ b/packages/core/src/store/use-scene.ts @@ -6,6 +6,7 @@ import { create, type StoreApi, type UseBoundStore } from 'zustand' import { persist } from 'zustand/middleware' import { BuildingNode } from '../schema' import { LevelNode } from '../schema/nodes/level' +import { SiteNode } from '../schema/nodes/site' import type { AnyNode, AnyNodeId } from '../schema/types' import { isObject } from '../utils/types' import * as nodeActions from './actions/node-actions' @@ -87,25 +88,29 @@ const useScene: UseSceneStore = create()( return // Scene already loaded } - const building = BuildingNode.parse({ - children: [], - }) - + // Create hierarchy: Site → Building → Level const level0 = LevelNode.parse({ level: 0, children: [], }) - building.children.push(level0.id) + const building = BuildingNode.parse({ + children: [level0.id], + }) + + const site = SiteNode.parse({ + children: [building], + }) // Define all nodes flat const nodes: Record = { + [site.id]: site, [building.id]: building, [level0.id]: level0, } - // Root nodes are the levels - const rootNodeIds = [building.id] + // Site is the root + const rootNodeIds = [site.id] set({ nodes, rootNodeIds }) }, @@ -155,13 +160,44 @@ const useScene: UseSceneStore = create()( onRehydrateStorage: (state) => { console.log('hydrating...') - // optional return (state, error) => { if (error) { console.log('an error happened during hydration', error) - } else { - console.log('hydration finished') + return } + + if (!state) { + console.log('hydration finished - no state') + return + } + + // Migration: Wrap old scenes (where root is not a SiteNode) in a SiteNode + const rootId = state.rootNodeIds?.[0] + const rootNode = rootId ? state.nodes[rootId] : null + + if (rootNode && rootNode.type !== 'site') { + console.log('Migrating old scene: wrapping in SiteNode') + + // Collect existing root nodes (should be BuildingNode or ItemNode) + const existingRoots = (state.rootNodeIds || []) + .map(id => state.nodes[id]) + .filter(node => node?.type === 'building' || node?.type === 'item') + + // Create a new SiteNode with existing roots as children + const site = SiteNode.parse({ + children: existingRoots, + }) + + // Add site to nodes + state.nodes[site.id] = site + + // Update root to be the site + state.rootNodeIds = [site.id] + + console.log('Migration complete: scene now has SiteNode as root') + } + + console.log('hydration finished') } }, }, diff --git a/packages/viewer/src/components/renderers/node-renderer.tsx b/packages/viewer/src/components/renderers/node-renderer.tsx index 8224bcc8..b3aacaea 100644 --- a/packages/viewer/src/components/renderers/node-renderer.tsx +++ b/packages/viewer/src/components/renderers/node-renderer.tsx @@ -8,6 +8,7 @@ import { ItemRenderer } from './item/item-renderer' import { LevelRenderer } from './level/level-renderer' import { RoofRenderer } from './roof/roof-renderer' import { ScanRenderer } from './scan/scan-renderer' +import { SiteRenderer } from './site/site-renderer' import { SlabRenderer } from './slab/slab-renderer' import { WallRenderer } from './wall/wall-renderer' import { ZoneRenderer } from './zone/zone-renderer' @@ -19,6 +20,7 @@ export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => { return ( <> + {node.type === 'site' && } {node.type === 'building' && } {node.type === 'ceiling' && } {node.type === 'level' && } diff --git a/packages/viewer/src/components/renderers/site/site-renderer.tsx b/packages/viewer/src/components/renderers/site/site-renderer.tsx new file mode 100644 index 00000000..8d4f7387 --- /dev/null +++ b/packages/viewer/src/components/renderers/site/site-renderer.tsx @@ -0,0 +1,20 @@ +import { type SiteNode, useRegistry } from '@pascal-app/core' +import { useRef } from 'react' +import type { Group } from 'three' +import { useNodeEvents } from '../../../hooks/use-node-events' +import { NodeRenderer } from '../node-renderer' + +export const SiteRenderer = ({ node }: { node: SiteNode }) => { + const ref = useRef(null!) + + useRegistry(node.id, node.type, ref) + const handlers = useNodeEvents(node, 'site') + + return ( + + {node.children.map((child) => ( + + ))} + + ) +} diff --git a/packages/viewer/src/hooks/use-node-events.ts b/packages/viewer/src/hooks/use-node-events.ts index 64042113..c350edb7 100644 --- a/packages/viewer/src/hooks/use-node-events.ts +++ b/packages/viewer/src/hooks/use-node-events.ts @@ -11,6 +11,8 @@ import { type LevelNode, type RoofEvent, type RoofNode, + type SiteEvent, + type SiteNode, type SlabEvent, type SlabNode, type WallEvent, @@ -21,6 +23,7 @@ import { import type { ThreeEvent } from '@react-three/fiber' type NodeConfig = { + site: { node: SiteNode; event: SiteEvent } item: { node: ItemNode; event: ItemEvent } wall: { node: WallNode; event: WallEvent } building: { node: BuildingNode; event: BuildingEvent }