events handlers
This commit is contained in:
@@ -1,45 +1,46 @@
|
|||||||
import mitt from "mitt";
|
import mitt from "mitt";
|
||||||
import { ItemNode, WallNode } from "../schema";
|
import { ItemNode, WallNode } from "../schema";
|
||||||
import { AnyNode } from "../schema/types";
|
import { AnyNode } from "../schema/types";
|
||||||
|
|
||||||
|
// Base event interfaces
|
||||||
export interface GridEvent {
|
export interface GridEvent {
|
||||||
position: [number, number];
|
position: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeEvent {
|
export interface NodeEvent<T extends AnyNode = AnyNode> {
|
||||||
node: AnyNode;
|
node: T;
|
||||||
position: [number, number, number]; // [x, y, z] world coordinates
|
position: [number, number, number];
|
||||||
normal?: [number, number, number]; // [x, y, z] normal vector
|
normal?: [number, number, number];
|
||||||
stopPropagation: () => void;
|
stopPropagation: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WallEvent extends NodeEvent {
|
export type WallEvent = NodeEvent<WallNode>;
|
||||||
node: WallNode;
|
export type ItemEvent = NodeEvent<ItemNode>;
|
||||||
}
|
|
||||||
|
|
||||||
export interface ItemEvent extends NodeEvent {
|
// Event suffixes - exported for use in hooks
|
||||||
node: ItemNode;
|
export const eventSuffixes = [
|
||||||
}
|
"click",
|
||||||
|
"move",
|
||||||
|
"enter",
|
||||||
|
"leave",
|
||||||
|
"pointerdown",
|
||||||
|
"pointerup",
|
||||||
|
"context-menu",
|
||||||
|
"double-click",
|
||||||
|
] as const;
|
||||||
|
|
||||||
type EditorEvents = {
|
export type EventSuffix = (typeof eventSuffixes)[number];
|
||||||
"grid:click": GridEvent;
|
|
||||||
"grid:rightclick": GridEvent;
|
type NodeEvents<T extends string, E> = {
|
||||||
"grid:move": GridEvent;
|
[K in `${T}:${EventSuffix}`]: E;
|
||||||
"grid:double-click": GridEvent;
|
|
||||||
"grid:enter": GridEvent;
|
|
||||||
"grid:leave": GridEvent;
|
|
||||||
"grid:pointerdown": GridEvent;
|
|
||||||
"grid:pointerup": GridEvent;
|
|
||||||
"wall:click": WallEvent;
|
|
||||||
"wall:move": WallEvent;
|
|
||||||
"wall:enter": WallEvent;
|
|
||||||
"wall:leave": WallEvent;
|
|
||||||
"wall:pointerdown": WallEvent;
|
|
||||||
"wall:pointerup": WallEvent;
|
|
||||||
"item:click": ItemEvent;
|
|
||||||
"item:move": ItemEvent;
|
|
||||||
"item:enter": ItemEvent;
|
|
||||||
"item:leave": ItemEvent;
|
|
||||||
"item:pointerdown": ItemEvent;
|
|
||||||
"item:pointerup": ItemEvent;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GridEvents = {
|
||||||
|
[K in `grid:${EventSuffix}`]: GridEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EditorEvents = GridEvents &
|
||||||
|
NodeEvents<"wall", WallEvent> &
|
||||||
|
NodeEvents<"item", ItemEvent>;
|
||||||
|
|
||||||
export const emitter = mitt<EditorEvents>();
|
export const emitter = mitt<EditorEvents>();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const sceneRegistry = {
|
|||||||
// Categorized lookups: Type -> Set of IDs
|
// Categorized lookups: Type -> Set of IDs
|
||||||
// Using a Set is faster for adding/deleting than an Array
|
// Using a Set is faster for adding/deleting than an Array
|
||||||
byType: {
|
byType: {
|
||||||
|
building: new Set<string>(),
|
||||||
level: new Set<string>(),
|
level: new Set<string>(),
|
||||||
wall: new Set<string>(),
|
wall: new Set<string>(),
|
||||||
item: new Set<string>(),
|
item: new Set<string>(),
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export { LevelSystem } from "../../viewer/src/systems/level/level-system";
|
|||||||
export { WallSystem } from "./systems/wall/wall-system";
|
export { WallSystem } from "./systems/wall/wall-system";
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
export { emitter } from "./events/bus";
|
export { emitter, eventSuffixes } from "./events/bus";
|
||||||
|
export type { ItemEvent, WallEvent, NodeEvent, GridEvent, EventSuffix } from "./events/bus";
|
||||||
|
|
||||||
// Schema
|
// Schema
|
||||||
export * from "./schema";
|
export * from "./schema";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { LevelNode } from "./level";
|
|||||||
export const BuildingNode = BaseNode.extend({
|
export const BuildingNode = BaseNode.extend({
|
||||||
id: objectId("building"),
|
id: objectId("building"),
|
||||||
type: nodeType("building"),
|
type: nodeType("building"),
|
||||||
children: z.array(LevelNode).default([LevelNode.parse({})]),
|
children: z.array(LevelNode.shape.id).default([]),
|
||||||
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||||
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||||
}).describe(
|
}).describe(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { ItemNode } from "../schema";
|
import { BuildingNode, ItemNode } from "../schema";
|
||||||
import { LevelNode } from "../schema/nodes/level";
|
import { LevelNode } from "../schema/nodes/level";
|
||||||
import { WallNode } from "../schema/nodes/wall";
|
import { WallNode } from "../schema/nodes/wall";
|
||||||
import { AnyNode, AnyNodeId } from "../schema/types";
|
import { AnyNode, AnyNodeId } from "../schema/types";
|
||||||
@@ -33,6 +33,12 @@ const useScene = create<SceneState>()((set, get) => ({
|
|||||||
dirtyNodes: new Set<AnyNodeId>(),
|
dirtyNodes: new Set<AnyNodeId>(),
|
||||||
|
|
||||||
loadScene: () => {
|
loadScene: () => {
|
||||||
|
|
||||||
|
const building = BuildingNode.parse({
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const level0 = LevelNode.parse({
|
const level0 = LevelNode.parse({
|
||||||
level: 0,
|
level: 0,
|
||||||
children: [],
|
children: [],
|
||||||
@@ -85,8 +91,11 @@ const useScene = create<SceneState>()((set, get) => ({
|
|||||||
|
|
||||||
level0.children.push(wall0.id, wall1.id, wall2.id, wall3.id);
|
level0.children.push(wall0.id, wall1.id, wall2.id, wall3.id);
|
||||||
|
|
||||||
|
building.children.push(level0.id, level1.id, level2.id);
|
||||||
|
|
||||||
// Define all nodes flat
|
// Define all nodes flat
|
||||||
const nodes: Record<AnyNodeId, AnyNode> = {
|
const nodes: Record<AnyNodeId, AnyNode> = {
|
||||||
|
[building.id]: building,
|
||||||
[level0.id]: level0,
|
[level0.id]: level0,
|
||||||
[level1.id]: level1,
|
[level1.id]: level1,
|
||||||
[level2.id]: level2,
|
[level2.id]: level2,
|
||||||
@@ -98,7 +107,7 @@ const useScene = create<SceneState>()((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Root nodes are the levels
|
// Root nodes are the levels
|
||||||
const rootNodeIds = [level0.id, level1.id, level2.id];
|
const rootNodeIds = [building.id];
|
||||||
|
|
||||||
get().dirtyNodes.add(wall0.id);
|
get().dirtyNodes.add(wall0.id);
|
||||||
get().dirtyNodes.add(wall1.id);
|
get().dirtyNodes.add(wall1.id);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { BuildingNode, LevelNode, useRegistry } from "@pascal-app/core";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { Group } from "three";
|
||||||
|
import { NodeRenderer } from "../node-renderer";
|
||||||
|
|
||||||
|
export const BuildingRenderer = ({ node }: { node: BuildingNode }) => {
|
||||||
|
const ref = useRef<Group>(null!);
|
||||||
|
|
||||||
|
useRegistry(node.id, node.type, ref);
|
||||||
|
return (
|
||||||
|
<group ref={ref}>
|
||||||
|
{/* <mesh receiveShadow>
|
||||||
|
<boxGeometry args={[10, 0.1, 10]} />
|
||||||
|
<meshStandardMaterial color="orange" />
|
||||||
|
</mesh> */}
|
||||||
|
{node.children.map((childId) => (
|
||||||
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { useGLTF } from "@react-three/drei/core/Gltf";
|
|||||||
import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events";
|
import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events";
|
||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { Group } from "three";
|
import { Group } from "three";
|
||||||
|
import { useNodeEvents } from "../../../hooks/use-node-events";
|
||||||
|
|
||||||
export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
||||||
const ref = useRef<Group>(null!);
|
const ref = useRef<Group>(null!);
|
||||||
@@ -15,114 +16,7 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
|||||||
nodes.cutout.visible = false;
|
nodes.cutout.visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event handlers
|
const handlers = useNodeEvents(node, "item");
|
||||||
|
|
||||||
const onPointerDown = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
// Only emit events for left-click (button 0)
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
const eventData = {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z] as [number, number, number],
|
|
||||||
normal: e.face
|
|
||||||
? ([e.face.normal.x, e.face.normal.y, e.face.normal.z] as [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number
|
|
||||||
])
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
};
|
|
||||||
emitter.emit("item:pointerdown", eventData);
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClick = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
// Only emit events for left-click (button 0)
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
const eventData = {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z] as [number, number, number],
|
|
||||||
normal: e.face
|
|
||||||
? ([e.face.normal.x, e.face.normal.y, e.face.normal.z] as [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number
|
|
||||||
])
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
};
|
|
||||||
emitter.emit("item:click", eventData);
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerUp = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
// Only emit events for left-click (button 0)
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
emitter.emit("item:pointerup", {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
|
||||||
normal: e.face
|
|
||||||
? ([e.face.normal.x, e.face.normal.y, e.face.normal.z] as [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number
|
|
||||||
])
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerEnter = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
emitter.emit("item:enter", {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
|
||||||
normal: e.face
|
|
||||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerLeave = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
emitter.emit("item:leave", {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
|
||||||
normal: e.face
|
|
||||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerMove = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
emitter.emit("item:move", {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
|
||||||
normal: e.face
|
|
||||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Clone
|
<Clone
|
||||||
@@ -130,12 +24,7 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
|||||||
object={scene}
|
object={scene}
|
||||||
position={node.position}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
onPointerDown={onPointerDown}
|
{...handlers}
|
||||||
onClick={onClick}
|
|
||||||
onPointerUp={onPointerUp}
|
|
||||||
onPointerEnter={onPointerEnter}
|
|
||||||
onPointerLeave={onPointerLeave}
|
|
||||||
onPointerMove={onPointerMove}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ export const LevelRenderer = ({ node }: { node: LevelNode }) => {
|
|||||||
useRegistry(node.id, node.type, ref);
|
useRegistry(node.id, node.type, ref);
|
||||||
return (
|
return (
|
||||||
<group ref={ref}>
|
<group ref={ref}>
|
||||||
<mesh receiveShadow>
|
{/* <mesh receiveShadow>
|
||||||
<boxGeometry args={[10, 0.1, 10]} />
|
<boxGeometry args={[10, 0.1, 10]} />
|
||||||
<meshStandardMaterial color="orange" />
|
<meshStandardMaterial color="orange" />
|
||||||
</mesh>
|
</mesh> */}
|
||||||
{node.children.map((childId) => (
|
{node.children.map((childId) => (
|
||||||
<NodeRenderer key={childId} nodeId={childId} />
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AnyNode, useScene } from "@pascal-app/core";
|
|||||||
import { ItemRenderer } from "./item/item-renderer";
|
import { ItemRenderer } from "./item/item-renderer";
|
||||||
import { LevelRenderer } from "./level/level-renderer";
|
import { LevelRenderer } from "./level/level-renderer";
|
||||||
import { WallRenderer } from "./wall/wall-renderer";
|
import { WallRenderer } from "./wall/wall-renderer";
|
||||||
|
import { BuildingRenderer } from "./building/building-renderer";
|
||||||
|
|
||||||
export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode["id"] }) => {
|
export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode["id"] }) => {
|
||||||
const node = useScene((state) => state.nodes[nodeId]);
|
const node = useScene((state) => state.nodes[nodeId]);
|
||||||
@@ -12,6 +13,7 @@ export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode["id"] }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{node.type === "building" && <BuildingRenderer node={node} />}
|
||||||
{node.type === "level" && <LevelRenderer node={node} />}
|
{node.type === "level" && <LevelRenderer node={node} />}
|
||||||
{node.type === "item" && <ItemRenderer node={node} />}
|
{node.type === "item" && <ItemRenderer node={node} />}
|
||||||
{node.type === "wall" && <WallRenderer node={node} />}
|
{node.type === "wall" && <WallRenderer node={node} />}
|
||||||
|
|||||||
@@ -3,132 +3,21 @@ import { ThreeEvent } from "@react-three/fiber";
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { Mesh } from "three";
|
import { Mesh } from "three";
|
||||||
import { NodeRenderer } from "../node-renderer";
|
import { NodeRenderer } from "../node-renderer";
|
||||||
|
import { useNodeEvents } from "../../../hooks/use-node-events";
|
||||||
|
|
||||||
export const WallRenderer = ({ node }: { node: WallNode }) => {
|
export const WallRenderer = ({ node }: { node: WallNode }) => {
|
||||||
const ref = useRef<Mesh>(null!);
|
const ref = useRef<Mesh>(null!);
|
||||||
|
|
||||||
useRegistry(node.id, "wall", ref);
|
useRegistry(node.id, "wall", ref);
|
||||||
|
|
||||||
// Event handlers
|
const handlers = useNodeEvents(node, "wall");
|
||||||
|
|
||||||
const onPointerDown = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
// Only emit events for left-click (button 0)
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
const eventData = {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z] as [number, number, number],
|
|
||||||
normal: e.face
|
|
||||||
? ([e.face.normal.x, e.face.normal.y, e.face.normal.z] as [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number
|
|
||||||
])
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
};
|
|
||||||
emitter.emit("wall:pointerdown", eventData);
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClick = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
// Only emit events for left-click (button 0)
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
const eventData = {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z] as [number, number, number],
|
|
||||||
normal: e.face
|
|
||||||
? ([e.face.normal.x, e.face.normal.y, e.face.normal.z] as [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number
|
|
||||||
])
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
};
|
|
||||||
emitter.emit("wall:click", eventData);
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerUp = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
// Only emit events for left-click (button 0)
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
emitter.emit("wall:pointerup", {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
|
||||||
normal: e.face
|
|
||||||
? ([e.face.normal.x, e.face.normal.y, e.face.normal.z] as [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number
|
|
||||||
])
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerEnter = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
emitter.emit("wall:enter", {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
|
||||||
normal: e.face
|
|
||||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerLeave = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
emitter.emit("wall:leave", {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
|
||||||
normal: e.face
|
|
||||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerMove = useCallback(
|
|
||||||
(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
emitter.emit("wall:move", {
|
|
||||||
node,
|
|
||||||
position: [e.point.x, e.point.y, e.point.z],
|
|
||||||
normal: e.face
|
|
||||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
|
||||||
: undefined,
|
|
||||||
stopPropagation: () => e.stopPropagation(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
ref={ref}
|
ref={ref}
|
||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
onPointerDown={onPointerDown}
|
{...handlers}
|
||||||
onPointerUp={onPointerUp}
|
|
||||||
onPointerEnter={onPointerEnter}
|
|
||||||
onPointerLeave={onPointerLeave}
|
|
||||||
onPointerMove={onPointerMove}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
>
|
||||||
{/* WallSystem will replace this geometry in the next frame */}
|
{/* WallSystem will replace this geometry in the next frame */}
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
<boxGeometry args={[0, 0, 0]} />
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
emitter,
|
||||||
|
EventSuffix,
|
||||||
|
ItemEvent,
|
||||||
|
ItemNode,
|
||||||
|
WallEvent,
|
||||||
|
WallNode,
|
||||||
|
} from "@pascal-app/core";
|
||||||
|
import { ThreeEvent } from "@react-three/fiber";
|
||||||
|
|
||||||
|
type NodeConfig = {
|
||||||
|
item: { node: ItemNode; event: ItemEvent };
|
||||||
|
wall: { node: WallNode; event: WallEvent };
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeType = keyof NodeConfig;
|
||||||
|
|
||||||
|
export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]["node"], type: T) {
|
||||||
|
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
|
||||||
|
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`;
|
||||||
|
const payload = {
|
||||||
|
node,
|
||||||
|
position: [e.point.x, e.point.y, e.point.z],
|
||||||
|
normal: e.face
|
||||||
|
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
||||||
|
: undefined,
|
||||||
|
stopPropagation: () => e.stopPropagation(),
|
||||||
|
} as NodeConfig[T]["event"];
|
||||||
|
|
||||||
|
emitter.emit(eventKey, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onPointerDown: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
emit("pointerdown", e);
|
||||||
|
},
|
||||||
|
onPointerUp: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
emit("pointerup", e);
|
||||||
|
},
|
||||||
|
onClick: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
emit("click", e);
|
||||||
|
},
|
||||||
|
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit("enter", e),
|
||||||
|
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit("leave", e),
|
||||||
|
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit("move", e),
|
||||||
|
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit("double-click", e),
|
||||||
|
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit("context-menu", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { LevelNode } from "@pascal-app/core";
|
import { BuildingNode, LevelNode } from "@pascal-app/core";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
type ViewerState = {
|
type ViewerState = {
|
||||||
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
||||||
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
||||||
|
|
||||||
|
currentBuildingId: BuildingNode["id"] | null;
|
||||||
|
setCurrentBuildingId: (id: BuildingNode["id"] | null) => void;
|
||||||
currentLevelId: LevelNode["id"] | null;
|
currentLevelId: LevelNode["id"] | null;
|
||||||
setCurrentLevelId: (id: LevelNode["id"] | null) => void;
|
setCurrentLevelId: (id: LevelNode["id"] | null) => void;
|
||||||
};
|
};
|
||||||
@@ -16,6 +18,8 @@ const useViewer = create<ViewerState>()((set, get) => ({
|
|||||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||||
currentLevelId: null,
|
currentLevelId: null,
|
||||||
setCurrentLevelId: (id) => set({ currentLevelId: id }),
|
setCurrentLevelId: (id) => set({ currentLevelId: id }),
|
||||||
|
currentBuildingId: null,
|
||||||
|
setCurrentBuildingId: (id) => set({ currentBuildingId: id }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useViewer;
|
export default useViewer;
|
||||||
|
|||||||
Reference in New Issue
Block a user