From fb3cd3e34e9d5d8e94986e13e10b04d88f1fa2d9 Mon Sep 17 00:00:00 2001 From: wass08 Date: Mon, 26 Jan 2026 07:34:53 +0900 Subject: [PATCH] zones as nodes --- packages/core/src/events/bus.ts | 90 ++++----- packages/core/src/index.ts | 3 +- packages/core/src/schema/index.ts | 6 +- packages/core/src/schema/nodes/level.ts | 4 +- packages/core/src/schema/nodes/zone.ts | 31 +++ packages/core/src/schema/types.ts | 2 + packages/core/src/schema/zone.ts | 35 ---- .../core/src/store/actions/zone-actions.ts | 60 ------ packages/core/src/store/use-scene.ts | 31 +-- .../renderers/level/level-renderer.tsx | 29 +-- .../components/renderers/node-renderer.tsx | 2 + .../renderers/zone/zone-renderer.tsx | 186 +++++++----------- packages/viewer/src/hooks/use-node-events.ts | 62 +++--- 13 files changed, 192 insertions(+), 349 deletions(-) create mode 100644 packages/core/src/schema/nodes/zone.ts delete mode 100644 packages/core/src/schema/zone.ts delete mode 100644 packages/core/src/store/actions/zone-actions.ts diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts index 88bbc7a2..3a221bec 100644 --- a/packages/core/src/events/bus.ts +++ b/packages/core/src/events/bus.ts @@ -1,74 +1,64 @@ -import type { ThreeEvent } from "@react-three/fiber"; -import mitt from "mitt"; -import type { BuildingNode, ItemNode, WallNode } from "../schema"; -import type { AnyNode } from "../schema/types"; -import type { Zone } from "../schema/zone"; +import type { ThreeEvent } from '@react-three/fiber' +import mitt from 'mitt' +import type { BuildingNode, ItemNode, WallNode, ZoneNode } from '../schema' +import type { AnyNode } from '../schema/types' // Base event interfaces export interface GridEvent { - position: [number, number, number]; - nativeEvent: ThreeEvent; + position: [number, number, number] + nativeEvent: ThreeEvent } export interface NodeEvent { - node: T; - position: [number, number, number]; - localPosition: [number, number, number]; - normal?: [number, number, number]; - stopPropagation: () => void; - nativeEvent: ThreeEvent; + node: T + position: [number, number, number] + localPosition: [number, number, number] + normal?: [number, number, number] + stopPropagation: () => void + nativeEvent: ThreeEvent } -export interface ZoneEvent { - zone: Zone; - position: [number, number, number]; - stopPropagation: () => void; - nativeEvent: ThreeEvent; -} - -export type WallEvent = NodeEvent; -export type ItemEvent = NodeEvent; -export type BuildingEvent = NodeEvent; +export type WallEvent = NodeEvent +export type ItemEvent = NodeEvent +export type BuildingEvent = NodeEvent +export type ZoneEvent = NodeEvent // Event suffixes - exported for use in hooks export const eventSuffixes = [ - "click", - "move", - "enter", - "leave", - "pointerdown", - "pointerup", - "context-menu", - "double-click", -] as const; + 'click', + 'move', + 'enter', + 'leave', + 'pointerdown', + 'pointerup', + 'context-menu', + 'double-click', +] as const -export type EventSuffix = (typeof eventSuffixes)[number]; +export type EventSuffix = (typeof eventSuffixes)[number] type NodeEvents = { - [K in `${T}:${EventSuffix}`]: E; -}; + [K in `${T}:${EventSuffix}`]: E +} type GridEvents = { - [K in `grid:${EventSuffix}`]: GridEvent; -}; + [K in `grid:${EventSuffix}`]: GridEvent +} export interface CameraControlEvent { - nodeId: AnyNode["id"]; + nodeId: AnyNode['id'] } type CameraControlEvents = { - "camera-controls:view": CameraControlEvent; - "camera-controls:capture": CameraControlEvent; -}; -type ZoneEvents = { - [K in `zone:${EventSuffix}`]: ZoneEvent; -}; + 'camera-controls:view': CameraControlEvent + 'camera-controls:capture': CameraControlEvent +} type EditorEvents = GridEvents & - NodeEvents<"wall", WallEvent> & - NodeEvents<"item", ItemEvent> & - NodeEvents<"building", BuildingEvent> & - ZoneEvents & - CameraControlEvents; + NodeEvents<'wall', WallEvent> & + NodeEvents<'item', ItemEvent> & + NodeEvents<'building', BuildingEvent> & + NodeEvents<'zone', ZoneEvent> & + CameraControlEvents -export const emitter = mitt(); +export const emitter = mitt() diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b340bd8a..12648883 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,7 @@ // Store export type { + BuildingEvent, CameraControlEvent, EventSuffix, GridEvent, @@ -27,4 +28,4 @@ export { default as useScene } from './store/use-scene' // Systems export { WallSystem } from './systems/wall/wall-system' -export { isObject } from './utils/types' \ No newline at end of file +export { isObject } from './utils/types' diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts index af2f1edc..8596ba0a 100644 --- a/packages/core/src/schema/index.ts +++ b/packages/core/src/schema/index.ts @@ -9,10 +9,8 @@ export { LevelNode } from './nodes/level' // Nodes export { SiteNode } from './nodes/site' export { WallNode } from './nodes/wall' +// Zones +export type { ZoneNode, ZonePolygon } from './nodes/zone' export type { AnyNodeId, AnyNodeType } from './types' // Union types export { AnyNode } from './types' - -// Zones -export type { Zone, ZonePolygon } from './zone' -export { ZoneSchema } from './zone' diff --git a/packages/core/src/schema/nodes/level.ts b/packages/core/src/schema/nodes/level.ts index 1e641669..77685b7e 100644 --- a/packages/core/src/schema/nodes/level.ts +++ b/packages/core/src/schema/nodes/level.ts @@ -2,11 +2,13 @@ import dedent from 'dedent' import { z } from 'zod' import { BaseNode, nodeType, objectId } from '../base' import { WallNode } from './wall' +import { ZoneNode } from './zone' + export const LevelNode = BaseNode.extend({ id: objectId('level'), type: nodeType('level'), - children: z.array(WallNode.shape.id).default([]), + children: z.array(z.union([WallNode.shape.id, ZoneNode.shape.id])).default([]), // Specific props level: z.number().default(0), }).describe( diff --git a/packages/core/src/schema/nodes/zone.ts b/packages/core/src/schema/nodes/zone.ts new file mode 100644 index 00000000..cd9fdfd9 --- /dev/null +++ b/packages/core/src/schema/nodes/zone.ts @@ -0,0 +1,31 @@ +import dedent from 'dedent' +import { z } from 'zod' +import { BaseNode, nodeType, objectId } from '../base' + +// Polygon boundary for zone area - array of [x, z] coordinates +export const ZonePolygon = z.array(z.tuple([z.number(), z.number()])) + +export const ZoneNode = BaseNode.extend({ + id: objectId('zone'), + type: nodeType('zone'), + name: z.string(), + // Polygon boundary - array of [x, z] coordinates defining the zone + polygon: ZonePolygon, + // Visual styling + color: z.string().default('#3b82f6'), // Default blue + metadata: z.json().optional().default({}), +}).describe( + dedent` + Zone schema - a polygon zone attached to a level + - object: "zone" + - id: zone id + - levelId: level this zone is attached to + - name: zone name + - polygon: array of [x, z] points defining the zone boundary + - color: hex color for visual styling + - metadata: zone metadata (optional) + `, +) + +export type ZoneNode = z.infer +export type ZonePolygon = z.infer diff --git a/packages/core/src/schema/types.ts b/packages/core/src/schema/types.ts index 1d8b80b1..cca1b644 100644 --- a/packages/core/src/schema/types.ts +++ b/packages/core/src/schema/types.ts @@ -4,6 +4,7 @@ import { ItemNode } from './nodes/item' import { LevelNode } from './nodes/level' import { SiteNode } from './nodes/site' import { WallNode } from './nodes/wall' +import { ZoneNode } from './nodes/zone' export const AnyNode = z.discriminatedUnion('type', [ SiteNode, @@ -11,6 +12,7 @@ export const AnyNode = z.discriminatedUnion('type', [ LevelNode, WallNode, ItemNode, + ZoneNode, ]) export type AnyNode = z.infer diff --git a/packages/core/src/schema/zone.ts b/packages/core/src/schema/zone.ts deleted file mode 100644 index f79b6b34..00000000 --- a/packages/core/src/schema/zone.ts +++ /dev/null @@ -1,35 +0,0 @@ -import dedent from 'dedent' -import { z } from 'zod' -import { objectId } from './base' -import { LevelNode } from './nodes/level' - -// Polygon boundary for zone area - array of [x, z] coordinates -export const ZonePolygon = z.array(z.tuple([z.number(), z.number()])) - -export const ZoneSchema = z - .object({ - id: objectId('zone'), - object: z.literal('zone').default('zone'), - levelId: LevelNode.shape.id, // Required - must be attached to a level - name: z.string(), - // Polygon boundary - array of [x, z] coordinates defining the zone - polygon: ZonePolygon, - // Visual styling - color: z.string().default('#3b82f6'), // Default blue - metadata: z.json().optional().default({}), - }) - .describe( - dedent` - Zone schema - a polygon zone attached to a level - - object: "zone" - - id: zone id - - levelId: level this zone is attached to - - name: zone name - - polygon: array of [x, z] points defining the zone boundary - - color: hex color for visual styling - - metadata: zone metadata (optional) - `, - ) - -export type Zone = z.infer -export type ZonePolygon = z.infer diff --git a/packages/core/src/store/actions/zone-actions.ts b/packages/core/src/store/actions/zone-actions.ts deleted file mode 100644 index 9ad10b9c..00000000 --- a/packages/core/src/store/actions/zone-actions.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Zone } from '../../schema' -import type { SceneState } from '../use-scene' - -export const createZonesAction = ( - set: (fn: (state: SceneState) => Partial) => void, - _get: () => SceneState, - zones: Zone[], -) => { - set((state) => { - const nextZones = { ...state.zones } - const nextZoneIds = [...state.zoneIds] - - for (const zone of zones) { - nextZones[zone.id] = zone - - if (!nextZoneIds.includes(zone.id)) { - nextZoneIds.push(zone.id) - } - } - - return { zones: nextZones, zoneIds: nextZoneIds } - }) -} - -export const updateZonesAction = ( - set: (fn: (state: SceneState) => Partial) => void, - _get: () => SceneState, - updates: { id: Zone['id']; data: Partial }[], -) => { - set((state) => { - const nextZones = { ...state.zones } - - for (const { id, data } of updates) { - const currentZone = nextZones[id] - if (!currentZone) continue - - nextZones[id] = { ...currentZone, ...data } - } - - return { zones: nextZones } - }) -} - -export const deleteZonesAction = ( - set: (fn: (state: SceneState) => Partial) => void, - _get: () => SceneState, - ids: Zone['id'][], -) => { - set((state) => { - const nextZones = { ...state.zones } - let nextZoneIds = [...state.zoneIds] - - for (const id of ids) { - delete nextZones[id] - nextZoneIds = nextZoneIds.filter((zid) => zid !== id) - } - - return { zones: nextZones, zoneIds: nextZoneIds } - }) -} diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts index 3f01a087..d467523c 100644 --- a/packages/core/src/store/use-scene.ts +++ b/packages/core/src/store/use-scene.ts @@ -3,21 +3,18 @@ import { temporal } from 'zundo' import { create } from 'zustand' import { persist } from 'zustand/middleware' -import { BuildingNode, type Zone } from '../schema' +import { BuildingNode } from '../schema' import { LevelNode } from '../schema/nodes/level' import type { AnyNode, AnyNodeId } from '../schema/types' import { isObject } from '../utils/types' import * as nodeActions from './actions/node-actions' -import * as zoneActions from './actions/zone-actions' export type SceneState = { // 1. The Data: A flat dictionary of all nodes nodes: Record - zones: Record // 2. The Root: Which nodes are at the top level? rootNodeIds: AnyNodeId[] - zoneIds: Zone['id'][] // 3. The "Dirty" Set: For the Wall/Physics systems dirtyNodes: Set @@ -37,14 +34,6 @@ export type SceneState = { deleteNode: (id: AnyNodeId) => void deleteNodes: (ids: AnyNodeId[]) => void - - // Zone actions - createZone: (zone: Zone) => void - createZones: (zones: Zone[]) => void - updateZone: (id: Zone['id'], data: Partial) => void - updateZones: (updates: { id: Zone['id']; data: Partial }[]) => void - deleteZone: (id: Zone['id']) => void - deleteZones: (ids: Zone['id'][]) => void } // type PartializedStoreState = Pick; @@ -55,11 +44,9 @@ const useScene = create()( (set, get) => ({ // 1. Flat dictionary of all nodes nodes: {}, - zones: {}, // 2. Root node IDs rootNodeIds: [], - zoneIds: [], // 3. Dirty set dirtyNodes: new Set(), @@ -68,8 +55,6 @@ const useScene = create()( set({ nodes: {}, rootNodeIds: [], - zones: {}, - zoneIds: [], dirtyNodes: new Set(), }) get().loadScene() // Default scene @@ -127,17 +112,6 @@ const useScene = create()( deleteNodes: (ids) => nodeActions.deleteNodesAction(set, get, ids), deleteNode: (id) => nodeActions.deleteNodesAction(set, get, [id]), - - // --- ZONES --- - - createZones: (zones) => zoneActions.createZonesAction(set, get, zones), - createZone: (zone) => zoneActions.createZonesAction(set, get, [zone]), - - updateZones: (updates) => zoneActions.updateZonesAction(set, get, updates), - updateZone: (id, data) => zoneActions.updateZonesAction(set, get, [{ id, data }]), - - deleteZones: (ids) => zoneActions.deleteZonesAction(set, get, ids), - deleteZone: (id) => zoneActions.deleteZonesAction(set, get, [id]), }), { partialize: (state) => { @@ -159,11 +133,8 @@ const useScene = create()( }), ), rootNodeIds: state.rootNodeIds, - zones: state.zones, - zoneIds: state.zoneIds, }), onRehydrateStorage: (state) => { - console.log('hydrating...') // optional diff --git a/packages/viewer/src/components/renderers/level/level-renderer.tsx b/packages/viewer/src/components/renderers/level/level-renderer.tsx index 4aa43643..0ee20ed9 100644 --- a/packages/viewer/src/components/renderers/level/level-renderer.tsx +++ b/packages/viewer/src/components/renderers/level/level-renderer.tsx @@ -1,14 +1,12 @@ -import { type LevelNode, useRegistry, useScene } from "@pascal-app/core"; -import { useRef } from "react"; -import type { Group } from "three"; -import { useShallow } from "zustand/shallow"; -import { NodeRenderer } from "../node-renderer"; -import { ZoneRenderer } from "../zone/zone-renderer"; +import { type LevelNode, useRegistry } from '@pascal-app/core' +import { useRef } from 'react' +import type { Group } from 'three' +import { NodeRenderer } from '../node-renderer' export const LevelRenderer = ({ node }: { node: LevelNode }) => { - const ref = useRef(null!); + const ref = useRef(null!) - useRegistry(node.id, node.type, ref); + useRegistry(node.id, node.type, ref) return ( @@ -19,17 +17,6 @@ export const LevelRenderer = ({ node }: { node: LevelNode }) => { {node.children.map((childId) => ( ))} - - ); -}; - -const LevelZones = ({ levelId }: { levelId: LevelNode["id"] }) => { - const zoneIds = useScene( - useShallow((s) => - s.zoneIds.filter((id) => s.zones[id]?.levelId === levelId), - ), - ); - - return zoneIds.map((zoneId) => ); -}; + ) +} diff --git a/packages/viewer/src/components/renderers/node-renderer.tsx b/packages/viewer/src/components/renderers/node-renderer.tsx index 21e1d8c9..54fdec91 100644 --- a/packages/viewer/src/components/renderers/node-renderer.tsx +++ b/packages/viewer/src/components/renderers/node-renderer.tsx @@ -5,6 +5,7 @@ import { BuildingRenderer } from './building/building-renderer' import { ItemRenderer } from './item/item-renderer' import { LevelRenderer } from './level/level-renderer' import { WallRenderer } from './wall/wall-renderer' +import { ZoneRenderer } from './zone/zone-renderer' export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => { const node = useScene((state) => state.nodes[nodeId]) @@ -17,6 +18,7 @@ export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => { {node.type === 'level' && } {node.type === 'item' && } {node.type === 'wall' && } + {node.type === 'zone' && } ) } diff --git a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx index 4e76dab7..8959acea 100644 --- a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx +++ b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx @@ -1,33 +1,25 @@ -import { emitter, useRegistry, useScene, type Zone } from "@pascal-app/core"; +import { useRegistry, type ZoneNode } from '@pascal-app/core' +import { useMemo, useRef } from 'react' +import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three' +import { color, float, uv } from 'three/tsl' +import { MeshBasicNodeMaterial } from 'three/webgpu' +import { useNodeEvents } from '../../../hooks/use-node-events' -import type { ThreeEvent } from "@react-three/fiber"; -import { useCallback, useMemo, useRef } from "react"; -import { - BufferGeometry, - Color, - DoubleSide, - Float32BufferAttribute, - type Group, - Shape, -} from "three"; -import { color, float, uv } from "three/tsl"; -import { MeshBasicNodeMaterial } from "three/webgpu"; - -const Y_OFFSET = 0.01; -const WALL_HEIGHT = 2.3; +const Y_OFFSET = 0.01 +const WALL_HEIGHT = 2.3 /** * Creates a gradient material for zone walls using TSL * Gradient goes from zone color at bottom to transparent at top */ const createWallGradientMaterial = (zoneColor: string) => { - const baseColor = color(new Color(zoneColor)); + const baseColor = color(new Color(zoneColor)) // Use UV y coordinate for vertical gradient (0 at bottom, 1 at top) - const gradientT = uv().y; + const gradientT = uv().y // Fade opacity from 0.6 at bottom to 0 at top - const opacity = float(0.6).mul(float(1).sub(gradientT)); + const opacity = float(0.6).mul(float(1).sub(gradientT)) return new MeshBasicNodeMaterial({ transparent: true, @@ -35,14 +27,14 @@ const createWallGradientMaterial = (zoneColor: string) => { opacityNode: opacity, side: DoubleSide, depthWrite: false, - }); -}; + }) +} /** * Creates a floor material for zones using TSL */ const createFloorMaterial = (zoneColor: string) => { - const baseColor = color(new Color(zoneColor)); + const baseColor = color(new Color(zoneColor)) return new MeshBasicNodeMaterial({ transparent: true, @@ -50,150 +42,114 @@ const createFloorMaterial = (zoneColor: string) => { opacityNode: float(0.15), side: DoubleSide, depthWrite: false, - }); -}; + }) +} /** * Creates wall geometry for zone borders * Each wall segment is a vertical quad from one polygon point to the next */ -const createWallGeometry = ( - polygon: Array<[number, number]>, -): BufferGeometry => { - const geometry = new BufferGeometry(); +const createWallGeometry = (polygon: Array<[number, number]>): BufferGeometry => { + const geometry = new BufferGeometry() - if (polygon.length < 2) return geometry; + if (polygon.length < 2) return geometry - const positions: number[] = []; - const uvs: number[] = []; - const indices: number[] = []; + const positions: number[] = [] + const uvs: number[] = [] + const indices: number[] = [] // Create a wall segment for each edge of the polygon for (let i = 0; i < polygon.length; i++) { - const current = polygon[i]!; - const next = polygon[(i + 1) % polygon.length]!; + const current = polygon[i]! + const next = polygon[(i + 1) % polygon.length]! - const baseIndex = i * 4; + const baseIndex = i * 4 // Four vertices per wall segment (two triangles forming a quad) // Bottom-left - positions.push(current[0]!, Y_OFFSET, current[1]!); - uvs.push(0, 0); + positions.push(current[0]!, Y_OFFSET, current[1]!) + uvs.push(0, 0) // Bottom-right - positions.push(next[0]!, Y_OFFSET, next[1]!); - uvs.push(1, 0); + positions.push(next[0]!, Y_OFFSET, next[1]!) + uvs.push(1, 0) // Top-right - positions.push(next[0]!, Y_OFFSET + WALL_HEIGHT, next[1]!); - uvs.push(1, 1); + positions.push(next[0]!, Y_OFFSET + WALL_HEIGHT, next[1]!) + uvs.push(1, 1) // Top-left - positions.push(current[0]!, Y_OFFSET + WALL_HEIGHT, current[1]!); - uvs.push(0, 1); + positions.push(current[0]!, Y_OFFSET + WALL_HEIGHT, current[1]!) + uvs.push(0, 1) // Two triangles for the quad - indices.push( - baseIndex, - baseIndex + 1, - baseIndex + 2, - baseIndex, - baseIndex + 2, - baseIndex + 3, - ); + indices.push(baseIndex, baseIndex + 1, baseIndex + 2, baseIndex, baseIndex + 2, baseIndex + 3) } - geometry.setAttribute("position", new Float32BufferAttribute(positions, 3)); - geometry.setAttribute("uv", new Float32BufferAttribute(uvs, 2)); - geometry.setIndex(indices); - geometry.computeVertexNormals(); + geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)) + geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2)) + geometry.setIndex(indices) + geometry.computeVertexNormals() - return geometry; -}; + return geometry +} -export const ZoneRenderer = ({ zoneId }: { zoneId: Zone["id"] }) => { - const ref = useRef(null!); - const zone = useScene((state) => state.zones[zoneId]); - useRegistry(zoneId, "zone", ref); +export const ZoneRenderer = ({ node }: { node: ZoneNode }) => { + const ref = useRef(null!) + + useRegistry(node.id, 'zone', ref) // Create floor shape from polygon const floorShape = useMemo(() => { - if (!zone?.polygon || zone.polygon.length < 3) return null; - - const shape = new Shape(); - const firstPt = zone.polygon[0]!; + if (!node?.polygon || node.polygon.length < 3) return null + const shape = new Shape() + const firstPt = node.polygon[0]! // Shape is in X-Y plane, we rotate it to X-Z plane // Negate Y (which becomes Z) to get correct orientation - shape.moveTo(firstPt[0]!, -firstPt[1]!); + shape.moveTo(firstPt[0]!, -firstPt[1]!) - for (let i = 1; i < zone.polygon.length; i++) { - const pt = zone.polygon[i]!; - shape.lineTo(pt[0]!, -pt[1]!); + for (let i = 1; i < node.polygon.length; i++) { + const pt = node.polygon[i]! + shape.lineTo(pt[0]!, -pt[1]!) } - shape.closePath(); + shape.closePath() - return shape; - }, [zone?.polygon]); + return shape + }, [node?.polygon]) // Create wall geometry from polygon const wallGeometry = useMemo(() => { - if (!zone?.polygon || zone.polygon.length < 2) return null; - return createWallGeometry(zone.polygon); - }, [zone?.polygon]); + if (!node?.polygon || node.polygon.length < 2) return null + return createWallGeometry(node.polygon) + }, [node?.polygon]) // Create materials const floorMaterial = useMemo(() => { - if (!zone?.color) return null; - return createFloorMaterial(zone.color); - }, [zone?.color]); + if (!node?.color) return null + return createFloorMaterial(node.color) + }, [node?.color]) const wallMaterial = useMemo(() => { - if (!zone?.color) return null; - return createWallGradientMaterial(zone.color); - }, [zone?.color]); + if (!node?.color) return null + return createWallGradientMaterial(node.color) + }, [node?.color]) - if ( - !zone || - !floorShape || - !wallGeometry || - !floorMaterial || - !wallMaterial - ) { - return null; + const handlers = useNodeEvents(node, 'zone') + + if (!node || !floorShape || !wallGeometry || !floorMaterial || !wallMaterial) { + return null } - const emitZoneEvent = useCallback( - (suffix: string, e: ThreeEvent) => { - const eventKey = `zone:${suffix}` as `zone:${typeof suffix}`; - emitter.emit(eventKey, { - zone, - position: [e.point.x, e.point.y, e.point.z], - stopPropagation: () => e.stopPropagation(), - nativeEvent: e, - }); - }, - [zone] - ); - return ( - emitZoneEvent("click", e)} - onPointerEnter={(e) => emitZoneEvent("enter", e)} - onPointerLeave={(e) => emitZoneEvent("leave", e)} - > + {/* Floor fill */} - + {/* Wall borders with gradient */} - ); -}; + ) +} diff --git a/packages/viewer/src/hooks/use-node-events.ts b/packages/viewer/src/hooks/use-node-events.ts index 7b1acf24..18d2b561 100644 --- a/packages/viewer/src/hooks/use-node-events.ts +++ b/packages/viewer/src/hooks/use-node-events.ts @@ -1,4 +1,5 @@ import { + type BuildingEvent, type BuildingNode, type EventSuffix, emitter, @@ -6,56 +7,53 @@ import { type ItemNode, type WallEvent, type WallNode, -} from "@pascal-app/core"; -import type { ThreeEvent } from "@react-three/fiber"; -import type { BuildingEvent } from "../../../core/src/events/bus"; + type ZoneEvent, + type ZoneNode, +} from '@pascal-app/core' +import type { ThreeEvent } from '@react-three/fiber' type NodeConfig = { - item: { node: ItemNode; event: ItemEvent }; - wall: { node: WallNode; event: WallEvent }; - building: { node: BuildingNode; event: BuildingEvent }; -}; + item: { node: ItemNode; event: ItemEvent } + wall: { node: WallNode; event: WallEvent } + building: { node: BuildingNode; event: BuildingEvent } + zone: { node: ZoneNode; event: ZoneEvent } +} -type NodeType = keyof NodeConfig; +type NodeType = keyof NodeConfig -export function useNodeEvents( - node: NodeConfig[T]["node"], - type: T, -) { +export function useNodeEvents(node: NodeConfig[T]['node'], type: T) { const emit = (suffix: EventSuffix, e: ThreeEvent) => { - const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`; - const localPoint = e.object.worldToLocal(e.point.clone()); + const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}` + const localPoint = e.object.worldToLocal(e.point.clone()) const payload = { node, position: [e.point.x, e.point.y, e.point.z], localPosition: [localPoint.x, localPoint.y, localPoint.z], - normal: e.face - ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] - : undefined, + normal: e.face ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] : undefined, stopPropagation: () => e.stopPropagation(), nativeEvent: e, - } as NodeConfig[T]["event"]; + } as NodeConfig[T]['event'] - emitter.emit(eventKey, payload); - }; + emitter.emit(eventKey, payload) + } return { onPointerDown: (e: ThreeEvent) => { - if (e.button !== 0) return; - emit("pointerdown", e); + if (e.button !== 0) return + emit('pointerdown', e) }, onPointerUp: (e: ThreeEvent) => { - if (e.button !== 0) return; - emit("pointerup", e); + if (e.button !== 0) return + emit('pointerup', e) }, onClick: (e: ThreeEvent) => { - if (e.button !== 0) return; - emit("click", e); + if (e.button !== 0) return + emit('click', e) }, - onPointerEnter: (e: ThreeEvent) => emit("enter", e), - onPointerLeave: (e: ThreeEvent) => emit("leave", e), - onPointerMove: (e: ThreeEvent) => emit("move", e), - onDoubleClick: (e: ThreeEvent) => emit("double-click", e), - onContextMenu: (e: ThreeEvent) => emit("context-menu", e), - }; + onPointerEnter: (e: ThreeEvent) => emit('enter', e), + onPointerLeave: (e: ThreeEvent) => emit('leave', e), + onPointerMove: (e: ThreeEvent) => emit('move', e), + onDoubleClick: (e: ThreeEvent) => emit('double-click', e), + onContextMenu: (e: ThreeEvent) => emit('context-menu', e), + } }