event systems

This commit is contained in:
wass08
2026-01-15 11:24:41 +09:00
parent 0c07c482a5
commit c0626662a8
9 changed files with 382 additions and 15 deletions
+72 -2
View File
@@ -1,17 +1,87 @@
"use client"; "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 { 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() { export default function Editor() {
return ( return (
<div className="w-full h-full bg-pink-50"> <div className="w-full h-full bg-pink-50">
<LevelModeSwitcher /> <LevelModeSwitcher />
<Viewer /> <Viewer>
<Selector />
<Stats />
</Viewer>
</div> </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 LevelModeSwitcher = () => {
const setLevelMode = useScene((state) => state.setLevelMode); const setLevelMode = useScene((state) => state.setLevelMode);
const levelMode = useScene((state) => state.levelMode); const levelMode = useScene((state) => state.levelMode);
+3
View File
@@ -42,6 +42,7 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"dedent": "^1.7.1", "dedent": "^1.7.1",
"mitt": "^3.0.1",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"zod": "^4.3.5", "zod": "^4.3.5",
"zustand": "^5.0.10", "zustand": "^5.0.10",
@@ -752,6 +753,8 @@
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "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=="], "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=="], "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
+1
View File
@@ -27,6 +27,7 @@
}, },
"dependencies": { "dependencies": {
"dedent": "^1.7.1", "dedent": "^1.7.1",
"mitt": "^3.0.1",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"zod": "^4.3.5", "zod": "^4.3.5",
"zustand": "^5.0.10" "zustand": "^5.0.10"
+45
View File
@@ -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>();
+7 -1
View File
@@ -2,11 +2,17 @@
export { default as useScene } from "./store/useScene"; export { default as useScene } from "./store/useScene";
// Hooks // Hooks
export { useRegistry } from "./hooks/scene-registry/scene-registry"; export {
sceneRegistry,
useRegistry,
} from "./hooks/scene-registry/scene-registry";
// Systems // Systems
export { LevelSystem } from "./systems/level/level-system"; export { LevelSystem } from "./systems/level/level-system";
export { WallSystem } from "./systems/wall/wall-system"; export { WallSystem } from "./systems/wall/wall-system";
// Events
export { emitter } from "./events/bus";
// Schema // Schema
export * from "./schema"; export * from "./schema";
@@ -43,9 +43,9 @@ function updateWallGeometry(wallId: string) {
if (!mesh) return; if (!mesh) return;
const childrenIds = node.children || []; const childrenIds = node.children || [];
const childrenNodes = childrenIds.map( const childrenNodes = childrenIds
(childId) => useScene.getState().nodes[childId] .map((childId) => useScene.getState().nodes[childId])
); .filter((n): n is AnyNode => n !== undefined);
// Perform the Extrusion with Holes logic we discussed // Perform the Extrusion with Holes logic we discussed
const newGeo = generateExtrudedWall(node, childrenNodes); 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 { Clone } from "@react-three/drei/core/Clone";
import { useGLTF } from "@react-three/drei/core/Gltf"; 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"; import { Group } from "three";
export const ItemRenderer = ({ node }: { node: ItemNode }) => { export const ItemRenderer = ({ node }: { node: ItemNode }) => {
@@ -14,12 +15,127 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => {
nodes.cutout.visible = false; 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 ( return (
<Clone <Clone
ref={ref} ref={ref}
object={scene} object={scene}
position={node.position} position={node.position}
rotation={node.rotation} 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 { emitter, useRegistry, WallNode } from "@pascal-app/core";
import { useRef } from "react"; import { ThreeEvent } from "@react-three/fiber";
import { useCallback, useRef } from "react";
import { Mesh } from "three"; import { Mesh } from "three";
import { NodeRenderer } from "../node-renderer"; import { NodeRenderer } from "../node-renderer";
@@ -8,8 +9,127 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
useRegistry(node.id, "wall", ref); 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 ( 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 */} {/* WallSystem will replace this geometry in the next frame */}
<boxGeometry args={[0, 0, 0]} /> <boxGeometry args={[0, 0, 0]} />
<meshStandardMaterial color="lightgray" /> <meshStandardMaterial color="lightgray" />
@@ -1,6 +1,6 @@
"use client"; "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 { Canvas, ThreeToJSXElements } from "@react-three/fiber";
import { LevelSystem, WallSystem } from "@pascal-app/core"; import { LevelSystem, WallSystem } from "@pascal-app/core";
@@ -14,9 +14,11 @@ declare module "@react-three/fiber" {
extend(THREE as any); extend(THREE as any);
interface ViewerProps {} interface ViewerProps {
children?: React.ReactNode;
}
const Viewer: React.FC<ViewerProps> = () => { const Viewer: React.FC<ViewerProps> = ({ children }) => {
return ( return (
<Canvas <Canvas
className={"bg-[#303035]"} className={"bg-[#303035]"}
@@ -30,11 +32,15 @@ const Viewer: React.FC<ViewerProps> = () => {
> >
<OrbitControls /> <OrbitControls />
<Environment preset="sunset" /> <Environment preset="sunset" />
<SceneRenderer /> <Bvh>
<SceneRenderer />
</Bvh>
{/* Default Systems */} {/* Default Systems */}
<LevelSystem /> <LevelSystem />
<WallSystem /> <WallSystem />
{children}
</Canvas> </Canvas>
); );
}; };