default seleciton manager
This commit is contained in:
@@ -37,7 +37,7 @@ export default function Editor() {
|
||||
<SidebarProvider className="fixed z-10">
|
||||
<AppSidebar />
|
||||
</SidebarProvider>
|
||||
<Viewer>
|
||||
<Viewer selectionManager="custom">
|
||||
<SelectionManager />
|
||||
<ExportManager />
|
||||
{/* Editor only system to toggle zone visibility */}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ThreeEvent } from '@react-three/fiber'
|
||||
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'
|
||||
|
||||
// Base event interfaces
|
||||
@@ -21,6 +21,7 @@ export interface NodeEvent<T extends AnyNode = AnyNode> {
|
||||
export type WallEvent = NodeEvent<WallNode>
|
||||
export type ItemEvent = NodeEvent<ItemNode>
|
||||
export type BuildingEvent = NodeEvent<BuildingNode>
|
||||
export type LevelEvent = NodeEvent<LevelNode>
|
||||
export type ZoneEvent = NodeEvent<ZoneNode>
|
||||
export type SlabEvent = NodeEvent<SlabNode>
|
||||
export type CeilingEvent = NodeEvent<CeilingNode>
|
||||
@@ -61,6 +62,7 @@ type EditorEvents = GridEvents &
|
||||
NodeEvents<'wall', WallEvent> &
|
||||
NodeEvents<'item', ItemEvent> &
|
||||
NodeEvents<'building', BuildingEvent> &
|
||||
NodeEvents<'level', LevelEvent> &
|
||||
NodeEvents<'zone', ZoneEvent> &
|
||||
NodeEvents<'slab', SlabEvent> &
|
||||
NodeEvents<'ceiling', CeilingEvent> &
|
||||
|
||||
@@ -6,6 +6,7 @@ export type {
|
||||
EventSuffix,
|
||||
GridEvent,
|
||||
ItemEvent,
|
||||
LevelEvent,
|
||||
NodeEvent,
|
||||
SlabEvent,
|
||||
WallEvent,
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { type LevelNode, 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 LevelRenderer = ({ node }: { node: LevelNode }) => {
|
||||
const ref = useRef<Group>(null!)
|
||||
|
||||
useRegistry(node.id, node.type, ref)
|
||||
const handlers = useNodeEvents(node, 'level')
|
||||
|
||||
return (
|
||||
<group ref={ref}>
|
||||
{/* <mesh receiveShadow>
|
||||
<boxGeometry args={[10, 0.1, 10]} />
|
||||
<meshStandardMaterial color="orange" />
|
||||
</mesh> */}
|
||||
<group ref={ref} {...handlers}>
|
||||
{node.children.map((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 { SceneRenderer } from '../renderers/scene-renderer'
|
||||
import PostProcessing from './post-processing'
|
||||
import { SelectionManager } from './selection-manager'
|
||||
import { ViewerCamera } from './viewer-camera'
|
||||
|
||||
declare module '@react-three/fiber' {
|
||||
@@ -19,9 +20,10 @@ extend(THREE as any)
|
||||
|
||||
interface ViewerProps {
|
||||
children?: React.ReactNode
|
||||
selectionManager?: 'default' | 'custom'
|
||||
}
|
||||
|
||||
const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
||||
const Viewer: React.FC<ViewerProps> = ({ children, selectionManager = 'default' }) => {
|
||||
return (
|
||||
<Canvas
|
||||
className={'bg-[#303035]'}
|
||||
@@ -54,6 +56,7 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
||||
<WallSystem />
|
||||
<PostProcessing />
|
||||
|
||||
{selectionManager === 'default' && <SelectionManager />}
|
||||
{children}
|
||||
</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,
|
||||
type ItemEvent,
|
||||
type ItemNode,
|
||||
type LevelEvent,
|
||||
type LevelNode,
|
||||
type RoofEvent,
|
||||
type RoofNode,
|
||||
type SlabEvent,
|
||||
@@ -22,6 +24,7 @@ type NodeConfig = {
|
||||
item: { node: ItemNode; event: ItemEvent }
|
||||
wall: { node: WallNode; event: WallEvent }
|
||||
building: { node: BuildingNode; event: BuildingEvent }
|
||||
level: { node: LevelNode; event: LevelEvent }
|
||||
zone: { node: ZoneNode; event: ZoneEvent }
|
||||
slab: { node: SlabNode; event: SlabEvent }
|
||||
ceiling: { node: CeilingNode; event: CeilingEvent }
|
||||
|
||||
Reference in New Issue
Block a user