default seleciton manager
This commit is contained in:
@@ -37,7 +37,7 @@ export default function Editor() {
|
|||||||
<SidebarProvider className="fixed z-10">
|
<SidebarProvider className="fixed z-10">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
<Viewer>
|
<Viewer selectionManager="custom">
|
||||||
<SelectionManager />
|
<SelectionManager />
|
||||||
<ExportManager />
|
<ExportManager />
|
||||||
{/* Editor only system to toggle zone visibility */}
|
{/* Editor only system to toggle zone visibility */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ThreeEvent } from '@react-three/fiber'
|
import type { ThreeEvent } from '@react-three/fiber'
|
||||||
import mitt from 'mitt'
|
import mitt from 'mitt'
|
||||||
import type { BuildingNode, CeilingNode, ItemNode, RoofNode, SlabNode, WallNode, ZoneNode } from '../schema'
|
import type { BuildingNode, CeilingNode, ItemNode, LevelNode, RoofNode, SlabNode, WallNode, ZoneNode } from '../schema'
|
||||||
import type { AnyNode } from '../schema/types'
|
import type { AnyNode } from '../schema/types'
|
||||||
|
|
||||||
// Base event interfaces
|
// Base event interfaces
|
||||||
@@ -21,6 +21,7 @@ export interface NodeEvent<T extends AnyNode = AnyNode> {
|
|||||||
export type WallEvent = NodeEvent<WallNode>
|
export type WallEvent = NodeEvent<WallNode>
|
||||||
export type ItemEvent = NodeEvent<ItemNode>
|
export type ItemEvent = NodeEvent<ItemNode>
|
||||||
export type BuildingEvent = NodeEvent<BuildingNode>
|
export type BuildingEvent = NodeEvent<BuildingNode>
|
||||||
|
export type LevelEvent = NodeEvent<LevelNode>
|
||||||
export type ZoneEvent = NodeEvent<ZoneNode>
|
export type ZoneEvent = NodeEvent<ZoneNode>
|
||||||
export type SlabEvent = NodeEvent<SlabNode>
|
export type SlabEvent = NodeEvent<SlabNode>
|
||||||
export type CeilingEvent = NodeEvent<CeilingNode>
|
export type CeilingEvent = NodeEvent<CeilingNode>
|
||||||
@@ -61,6 +62,7 @@ type EditorEvents = GridEvents &
|
|||||||
NodeEvents<'wall', WallEvent> &
|
NodeEvents<'wall', WallEvent> &
|
||||||
NodeEvents<'item', ItemEvent> &
|
NodeEvents<'item', ItemEvent> &
|
||||||
NodeEvents<'building', BuildingEvent> &
|
NodeEvents<'building', BuildingEvent> &
|
||||||
|
NodeEvents<'level', LevelEvent> &
|
||||||
NodeEvents<'zone', ZoneEvent> &
|
NodeEvents<'zone', ZoneEvent> &
|
||||||
NodeEvents<'slab', SlabEvent> &
|
NodeEvents<'slab', SlabEvent> &
|
||||||
NodeEvents<'ceiling', CeilingEvent> &
|
NodeEvents<'ceiling', CeilingEvent> &
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type {
|
|||||||
EventSuffix,
|
EventSuffix,
|
||||||
GridEvent,
|
GridEvent,
|
||||||
ItemEvent,
|
ItemEvent,
|
||||||
|
LevelEvent,
|
||||||
NodeEvent,
|
NodeEvent,
|
||||||
SlabEvent,
|
SlabEvent,
|
||||||
WallEvent,
|
WallEvent,
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { type LevelNode, useRegistry } from '@pascal-app/core'
|
import { type LevelNode, useRegistry } from '@pascal-app/core'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import type { Group } from 'three'
|
import type { Group } from 'three'
|
||||||
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
import { NodeRenderer } from '../node-renderer'
|
import { NodeRenderer } from '../node-renderer'
|
||||||
|
|
||||||
export const LevelRenderer = ({ node }: { node: LevelNode }) => {
|
export const LevelRenderer = ({ node }: { node: LevelNode }) => {
|
||||||
const ref = useRef<Group>(null!)
|
const ref = useRef<Group>(null!)
|
||||||
|
|
||||||
useRegistry(node.id, node.type, ref)
|
useRegistry(node.id, node.type, ref)
|
||||||
|
const handlers = useNodeEvents(node, 'level')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={ref}>
|
<group ref={ref} {...handlers}>
|
||||||
{/* <mesh receiveShadow>
|
|
||||||
<boxGeometry args={[10, 0.1, 10]} />
|
|
||||||
<meshStandardMaterial color="orange" />
|
|
||||||
</mesh> */}
|
|
||||||
{node.children.map((childId) => (
|
{node.children.map((childId) => (
|
||||||
<NodeRenderer key={childId} nodeId={childId} />
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { LevelSystem } from '../../systems/level/level-system'
|
|||||||
import { ScanSystem } from '../../systems/scan/scan-system'
|
import { ScanSystem } from '../../systems/scan/scan-system'
|
||||||
import { SceneRenderer } from '../renderers/scene-renderer'
|
import { SceneRenderer } from '../renderers/scene-renderer'
|
||||||
import PostProcessing from './post-processing'
|
import PostProcessing from './post-processing'
|
||||||
|
import { SelectionManager } from './selection-manager'
|
||||||
import { ViewerCamera } from './viewer-camera'
|
import { ViewerCamera } from './viewer-camera'
|
||||||
|
|
||||||
declare module '@react-three/fiber' {
|
declare module '@react-three/fiber' {
|
||||||
@@ -19,9 +20,10 @@ extend(THREE as any)
|
|||||||
|
|
||||||
interface ViewerProps {
|
interface ViewerProps {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
|
selectionManager?: 'default' | 'custom'
|
||||||
}
|
}
|
||||||
|
|
||||||
const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
const Viewer: React.FC<ViewerProps> = ({ children, selectionManager = 'default' }) => {
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
className={'bg-[#303035]'}
|
className={'bg-[#303035]'}
|
||||||
@@ -54,6 +56,7 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
|||||||
<WallSystem />
|
<WallSystem />
|
||||||
<PostProcessing />
|
<PostProcessing />
|
||||||
|
|
||||||
|
{selectionManager === 'default' && <SelectionManager />}
|
||||||
{children}
|
{children}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type BuildingNode,
|
||||||
|
type LevelNode,
|
||||||
|
type NodeEvent,
|
||||||
|
emitter,
|
||||||
|
sceneRegistry,
|
||||||
|
} from '@pascal-app/core'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import useViewer from '../../store/use-viewer'
|
||||||
|
|
||||||
|
export const SelectionManager = () => {
|
||||||
|
const selection = useViewer((s) => s.selection)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { buildingId, levelId, zoneId } = selection
|
||||||
|
|
||||||
|
// Determine current selection depth and what can be clicked
|
||||||
|
// 0: no building → can click buildings
|
||||||
|
// 1: building selected → can click levels
|
||||||
|
// 2: level selected → can click/hover zones
|
||||||
|
// 3: zone selected → can hover items/walls (no click needed)
|
||||||
|
|
||||||
|
const onBuildingClick = (event: NodeEvent<BuildingNode>) => {
|
||||||
|
if (buildingId) return // Already have a building selected
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.getState().setSelection({ buildingId: event.node.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBuildingEnter = (event: NodeEvent<BuildingNode>) => {
|
||||||
|
if (buildingId) return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: event.node.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBuildingLeave = (event: NodeEvent<BuildingNode>) => {
|
||||||
|
if (buildingId) return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLevelClick = (event: NodeEvent<LevelNode>) => {
|
||||||
|
if (!buildingId || levelId) return // Need building, but no level yet
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.getState().setSelection({ levelId: event.node.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLevelEnter = (event: NodeEvent<LevelNode>) => {
|
||||||
|
if (!buildingId || levelId) return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: event.node.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLevelLeave = (event: NodeEvent<LevelNode>) => {
|
||||||
|
if (!buildingId || levelId) return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onZoneClick = (event: NodeEvent) => {
|
||||||
|
if (!levelId || zoneId) return // Need level, but no zone yet
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.getState().setSelection({ zoneId: event.node.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onZoneEnter = (event: NodeEvent) => {
|
||||||
|
if (!levelId) return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: event.node.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onZoneLeave = (event: NodeEvent) => {
|
||||||
|
if (!levelId) return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover for items/walls when zone is selected
|
||||||
|
const onItemEnter = (event: NodeEvent) => {
|
||||||
|
if (!zoneId) return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: event.node.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onItemLeave = (event: NodeEvent) => {
|
||||||
|
if (!zoneId) return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid click deselects in reverse order
|
||||||
|
const onGridClick = () => {
|
||||||
|
const { buildingId, levelId, zoneId, selectedIds } = useViewer.getState().selection
|
||||||
|
|
||||||
|
if (selectedIds.length > 0) {
|
||||||
|
useViewer.getState().setSelection({ selectedIds: [] })
|
||||||
|
} else if (zoneId) {
|
||||||
|
useViewer.getState().setSelection({ zoneId: null })
|
||||||
|
} else if (levelId) {
|
||||||
|
useViewer.getState().setSelection({ levelId: null })
|
||||||
|
} else if (buildingId) {
|
||||||
|
useViewer.getState().setSelection({ buildingId: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to events
|
||||||
|
emitter.on('building:click', onBuildingClick)
|
||||||
|
emitter.on('building:enter', onBuildingEnter)
|
||||||
|
emitter.on('building:leave', onBuildingLeave)
|
||||||
|
|
||||||
|
emitter.on('level:click', onLevelClick)
|
||||||
|
emitter.on('level:enter', onLevelEnter)
|
||||||
|
emitter.on('level:leave', onLevelLeave)
|
||||||
|
|
||||||
|
emitter.on('zone:click', onZoneClick)
|
||||||
|
emitter.on('zone:enter', onZoneEnter)
|
||||||
|
emitter.on('zone:leave', onZoneLeave)
|
||||||
|
|
||||||
|
emitter.on('item:enter', onItemEnter)
|
||||||
|
emitter.on('item:leave', onItemLeave)
|
||||||
|
emitter.on('wall:enter', onItemEnter)
|
||||||
|
emitter.on('wall:leave', onItemLeave)
|
||||||
|
|
||||||
|
emitter.on('grid:click', onGridClick)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
emitter.off('building:click', onBuildingClick)
|
||||||
|
emitter.off('building:enter', onBuildingEnter)
|
||||||
|
emitter.off('building:leave', onBuildingLeave)
|
||||||
|
|
||||||
|
emitter.off('level:click', onLevelClick)
|
||||||
|
emitter.off('level:enter', onLevelEnter)
|
||||||
|
emitter.off('level:leave', onLevelLeave)
|
||||||
|
|
||||||
|
emitter.off('zone:click', onZoneClick)
|
||||||
|
emitter.off('zone:enter', onZoneEnter)
|
||||||
|
emitter.off('zone:leave', onZoneLeave)
|
||||||
|
|
||||||
|
emitter.off('item:enter', onItemEnter)
|
||||||
|
emitter.off('item:leave', onItemLeave)
|
||||||
|
emitter.off('wall:enter', onItemEnter)
|
||||||
|
emitter.off('wall:leave', onItemLeave)
|
||||||
|
|
||||||
|
emitter.off('grid:click', onGridClick)
|
||||||
|
}
|
||||||
|
}, [selection])
|
||||||
|
|
||||||
|
return <OutlinerSync />
|
||||||
|
}
|
||||||
|
|
||||||
|
const OutlinerSync = () => {
|
||||||
|
const selection = useViewer((s) => s.selection)
|
||||||
|
const hoveredId = useViewer((s) => s.hoveredId)
|
||||||
|
const outliner = useViewer((s) => s.outliner)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let idsToHighlight: string[] = []
|
||||||
|
|
||||||
|
// Highlight based on the "deepest" selection
|
||||||
|
if (selection.selectedIds.length > 0) {
|
||||||
|
idsToHighlight = selection.selectedIds
|
||||||
|
} else if (selection.zoneId) {
|
||||||
|
idsToHighlight = [selection.zoneId]
|
||||||
|
} else if (selection.levelId) {
|
||||||
|
idsToHighlight = [selection.levelId]
|
||||||
|
} else if (selection.buildingId) {
|
||||||
|
idsToHighlight = [selection.buildingId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync with the imperative outliner arrays
|
||||||
|
outliner.selectedObjects.length = 0
|
||||||
|
for (const id of idsToHighlight) {
|
||||||
|
const obj = sceneRegistry.nodes.get(id)
|
||||||
|
if (obj) outliner.selectedObjects.push(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
outliner.hoveredObjects.length = 0
|
||||||
|
if (hoveredId) {
|
||||||
|
const obj = sceneRegistry.nodes.get(hoveredId)
|
||||||
|
if (obj) outliner.hoveredObjects.push(obj)
|
||||||
|
}
|
||||||
|
}, [selection, hoveredId, outliner])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
emitter,
|
emitter,
|
||||||
type ItemEvent,
|
type ItemEvent,
|
||||||
type ItemNode,
|
type ItemNode,
|
||||||
|
type LevelEvent,
|
||||||
|
type LevelNode,
|
||||||
type RoofEvent,
|
type RoofEvent,
|
||||||
type RoofNode,
|
type RoofNode,
|
||||||
type SlabEvent,
|
type SlabEvent,
|
||||||
@@ -22,6 +24,7 @@ type NodeConfig = {
|
|||||||
item: { node: ItemNode; event: ItemEvent }
|
item: { node: ItemNode; event: ItemEvent }
|
||||||
wall: { node: WallNode; event: WallEvent }
|
wall: { node: WallNode; event: WallEvent }
|
||||||
building: { node: BuildingNode; event: BuildingEvent }
|
building: { node: BuildingNode; event: BuildingEvent }
|
||||||
|
level: { node: LevelNode; event: LevelEvent }
|
||||||
zone: { node: ZoneNode; event: ZoneEvent }
|
zone: { node: ZoneNode; event: ZoneEvent }
|
||||||
slab: { node: SlabNode; event: SlabEvent }
|
slab: { node: SlabNode; event: SlabEvent }
|
||||||
ceiling: { node: CeilingNode; event: CeilingEvent }
|
ceiling: { node: CeilingNode; event: CeilingEvent }
|
||||||
|
|||||||
Reference in New Issue
Block a user