From c0626662a8282fb2cb8338550ddf9a73aeb55e9e Mon Sep 17 00:00:00 2001 From: wass08 Date: Thu, 15 Jan 2026 11:24:41 +0900 Subject: [PATCH] event systems --- apps/editor/components/editor.tsx | 74 +++++++++- bun.lock | 3 + packages/core/package.json | 1 + packages/core/src/events/bus.ts | 45 +++++++ packages/core/src/index.ts | 8 +- .../core/src/systems/wall/wall-system.tsx | 6 +- .../renderers/item/item-renderer.tsx | 120 ++++++++++++++++- .../renderers/wall/wall-renderer.tsx | 126 +++++++++++++++++- .../viewer/src/components/viewer/viewer.tsx | 14 +- 9 files changed, 382 insertions(+), 15 deletions(-) create mode 100644 packages/core/src/events/bus.ts diff --git a/apps/editor/components/editor.tsx b/apps/editor/components/editor.tsx index 17043745..ed72a2db 100644 --- a/apps/editor/components/editor.tsx +++ b/apps/editor/components/editor.tsx @@ -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 (
- + + + +
); } +const Selector = () => { + const selectedItemId = useRef(null); + const itemSelectedAt = useRef(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); diff --git a/bun.lock b/bun.lock index 18a2c378..2b419ad8 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/packages/core/package.json b/packages/core/package.json index 26ca1bbc..73b0a945 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts new file mode 100644 index 00000000..fb994417 --- /dev/null +++ b/packages/core/src/events/bus.ts @@ -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(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index afcf1ddf..fc5dde13 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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"; diff --git a/packages/core/src/systems/wall/wall-system.tsx b/packages/core/src/systems/wall/wall-system.tsx index 85601b69..e31cdfa5 100644 --- a/packages/core/src/systems/wall/wall-system.tsx +++ b/packages/core/src/systems/wall/wall-system.tsx @@ -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); diff --git a/packages/viewer/src/components/renderers/item/item-renderer.tsx b/packages/viewer/src/components/renderers/item/item-renderer.tsx index bdfdfccc..50e59a10 100644 --- a/packages/viewer/src/components/renderers/item/item-renderer.tsx +++ b/packages/viewer/src/components/renderers/item/item-renderer.tsx @@ -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) => { + // 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) => { + // 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) => { + // 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) => { + 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) => { + 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) => { + 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 ( ); }; diff --git a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx index e10ca3e7..6cf20fcc 100644 --- a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx +++ b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx @@ -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) => { + // 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) => { + // 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) => { + // 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) => { + 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) => { + 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) => { + 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 ( - + {/* WallSystem will replace this geometry in the next frame */} diff --git a/packages/viewer/src/components/viewer/viewer.tsx b/packages/viewer/src/components/viewer/viewer.tsx index b6cb73c5..df4dfb78 100644 --- a/packages/viewer/src/components/viewer/viewer.tsx +++ b/packages/viewer/src/components/viewer/viewer.tsx @@ -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 = () => { +const Viewer: React.FC = ({ children }) => { return ( = () => { > - + + + {/* Default Systems */} + + {children} ); };