event systems
This commit is contained in:
@@ -1,17 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import { useScene } from "@pascal-app/core";
|
||||
import {
|
||||
emitter,
|
||||
ItemNode,
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
WallNode,
|
||||
} from "@pascal-app/core";
|
||||
import { Viewer } from "@pascal-app/viewer";
|
||||
import { Stats } from "@react-three/drei";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Mesh } from "three";
|
||||
import { MeshBasicNodeMaterial } from "three/webgpu";
|
||||
|
||||
export default function Editor() {
|
||||
return (
|
||||
<div className="w-full h-full bg-pink-50">
|
||||
<LevelModeSwitcher />
|
||||
<Viewer />
|
||||
<Viewer>
|
||||
<Selector />
|
||||
<Stats />
|
||||
</Viewer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Selector = () => {
|
||||
const selectedItemId = useRef<ItemNode["id"]>(null);
|
||||
const itemSelectedAt = useRef<number>(0);
|
||||
useEffect(() => {
|
||||
emitter.on("item:click", (event) => {
|
||||
if (Date.now() - itemSelectedAt.current < 50) {
|
||||
return;
|
||||
}
|
||||
itemSelectedAt.current = Date.now();
|
||||
if (selectedItemId.current === event.node.id) {
|
||||
selectedItemId.current = null;
|
||||
console.log("Deselected item:", event.node.id);
|
||||
return;
|
||||
}
|
||||
selectedItemId.current = event.node.id;
|
||||
const itemMesh = sceneRegistry.nodes.get(event.node.id);
|
||||
if (!itemMesh) return;
|
||||
itemMesh.traverse((child) => {
|
||||
if ((child as Mesh).isMesh) {
|
||||
(child as Mesh).material = new MeshBasicNodeMaterial({
|
||||
color: "yellow",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Selected item:", event.node.id);
|
||||
});
|
||||
|
||||
emitter.on("wall:move", (event) => {
|
||||
const wallNode = event.node as WallNode;
|
||||
|
||||
if (wallNode.children.length === 0) return;
|
||||
const itemId = wallNode.children[0];
|
||||
if (!itemId) return;
|
||||
if (selectedItemId.current !== itemId) return;
|
||||
const itemNode = useScene.getState().nodes[itemId];
|
||||
const itemMesh = sceneRegistry.nodes.get(itemId);
|
||||
if (!itemNode || !itemMesh) return;
|
||||
|
||||
itemMesh.position.set(
|
||||
event.position[0],
|
||||
event.position[1],
|
||||
event.position[2]
|
||||
);
|
||||
useScene.getState().dirtyNodes.add(wallNode.id);
|
||||
console.log(
|
||||
"Wall move event:",
|
||||
wallNode.id,
|
||||
"Point position:",
|
||||
event.position
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const LevelModeSwitcher = () => {
|
||||
const setLevelMode = useScene((state) => state.setLevelMode);
|
||||
const levelMode = useScene((state) => state.levelMode);
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"dedent": "^1.7.1",
|
||||
"mitt": "^3.0.1",
|
||||
"nanoid": "^5.1.6",
|
||||
"zod": "^4.3.5",
|
||||
"zustand": "^5.0.10",
|
||||
@@ -752,6 +753,8 @@
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dedent": "^1.7.1",
|
||||
"mitt": "^3.0.1",
|
||||
"nanoid": "^5.1.6",
|
||||
"zod": "^4.3.5",
|
||||
"zustand": "^5.0.10"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import mitt from "mitt";
|
||||
import { ItemNode, WallNode } from "../schema";
|
||||
import { AnyNode } from "../schema/types";
|
||||
export interface GridEvent {
|
||||
position: [number, number];
|
||||
}
|
||||
|
||||
export interface NodeEvent {
|
||||
node: AnyNode;
|
||||
position: [number, number, number]; // [x, y, z] world coordinates
|
||||
normal?: [number, number, number]; // [x, y, z] normal vector
|
||||
stopPropagation: () => void;
|
||||
}
|
||||
|
||||
export interface WallEvent extends NodeEvent {
|
||||
node: WallNode;
|
||||
}
|
||||
|
||||
export interface ItemEvent extends NodeEvent {
|
||||
node: ItemNode;
|
||||
}
|
||||
|
||||
type EditorEvents = {
|
||||
"grid:click": GridEvent;
|
||||
"grid:rightclick": GridEvent;
|
||||
"grid:move": GridEvent;
|
||||
"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;
|
||||
};
|
||||
export const emitter = mitt<EditorEvents>();
|
||||
@@ -2,11 +2,17 @@
|
||||
export { default as useScene } from "./store/useScene";
|
||||
|
||||
// Hooks
|
||||
export { useRegistry } from "./hooks/scene-registry/scene-registry";
|
||||
export {
|
||||
sceneRegistry,
|
||||
useRegistry,
|
||||
} from "./hooks/scene-registry/scene-registry";
|
||||
|
||||
// Systems
|
||||
export { LevelSystem } from "./systems/level/level-system";
|
||||
export { WallSystem } from "./systems/wall/wall-system";
|
||||
|
||||
// Events
|
||||
export { emitter } from "./events/bus";
|
||||
|
||||
// Schema
|
||||
export * from "./schema";
|
||||
|
||||
@@ -43,9 +43,9 @@ function updateWallGeometry(wallId: string) {
|
||||
if (!mesh) return;
|
||||
|
||||
const childrenIds = node.children || [];
|
||||
const childrenNodes = childrenIds.map(
|
||||
(childId) => useScene.getState().nodes[childId]
|
||||
);
|
||||
const childrenNodes = childrenIds
|
||||
.map((childId) => useScene.getState().nodes[childId])
|
||||
.filter((n): n is AnyNode => n !== undefined);
|
||||
|
||||
// Perform the Extrusion with Holes logic we discussed
|
||||
const newGeo = generateExtrudedWall(node, childrenNodes);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ItemNode, useRegistry } from "@pascal-app/core";
|
||||
import { emitter, ItemNode, useRegistry } from "@pascal-app/core";
|
||||
import { Clone } from "@react-three/drei/core/Clone";
|
||||
import { useGLTF } from "@react-three/drei/core/Gltf";
|
||||
import { useRef } from "react";
|
||||
import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Group } from "three";
|
||||
|
||||
export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
||||
@@ -14,12 +15,127 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
||||
nodes.cutout.visible = false;
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
|
||||
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 (
|
||||
<Clone
|
||||
ref={ref}
|
||||
object={scene}
|
||||
position={node.position}
|
||||
rotation={node.rotation}
|
||||
onPointerDown={onPointerDown}
|
||||
onClick={onClick}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerEnter={onPointerEnter}
|
||||
onPointerLeave={onPointerLeave}
|
||||
onPointerMove={onPointerMove}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRegistry, WallNode } from "@pascal-app/core";
|
||||
import { useRef } from "react";
|
||||
import { emitter, useRegistry, WallNode } from "@pascal-app/core";
|
||||
import { ThreeEvent } from "@react-three/fiber";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Mesh } from "three";
|
||||
import { NodeRenderer } from "../node-renderer";
|
||||
|
||||
@@ -8,8 +9,127 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
|
||||
|
||||
useRegistry(node.id, "wall", ref);
|
||||
|
||||
// Event handlers
|
||||
|
||||
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 (
|
||||
<mesh ref={ref} castShadow receiveShadow>
|
||||
<mesh
|
||||
ref={ref}
|
||||
castShadow
|
||||
receiveShadow
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerEnter={onPointerEnter}
|
||||
onPointerLeave={onPointerLeave}
|
||||
onPointerMove={onPointerMove}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* WallSystem will replace this geometry in the next frame */}
|
||||
<boxGeometry args={[0, 0, 0]} />
|
||||
<meshStandardMaterial color="lightgray" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Environment, OrbitControls } from "@react-three/drei";
|
||||
import { Bvh, Environment, OrbitControls } from "@react-three/drei";
|
||||
import { Canvas, ThreeToJSXElements } from "@react-three/fiber";
|
||||
|
||||
import { LevelSystem, WallSystem } from "@pascal-app/core";
|
||||
@@ -14,9 +14,11 @@ declare module "@react-three/fiber" {
|
||||
|
||||
extend(THREE as any);
|
||||
|
||||
interface ViewerProps {}
|
||||
interface ViewerProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Viewer: React.FC<ViewerProps> = () => {
|
||||
const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
||||
return (
|
||||
<Canvas
|
||||
className={"bg-[#303035]"}
|
||||
@@ -30,11 +32,15 @@ const Viewer: React.FC<ViewerProps> = () => {
|
||||
>
|
||||
<OrbitControls />
|
||||
<Environment preset="sunset" />
|
||||
<Bvh>
|
||||
<SceneRenderer />
|
||||
</Bvh>
|
||||
|
||||
{/* Default Systems */}
|
||||
<LevelSystem />
|
||||
<WallSystem />
|
||||
|
||||
{children}
|
||||
</Canvas>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user