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
+1
View File
@@ -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"
+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";
// 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" />
<SceneRenderer />
<Bvh>
<SceneRenderer />
</Bvh>
{/* Default Systems */}
<LevelSystem />
<WallSystem />
{children}
</Canvas>
);
};