Pascal Editor Event Bus for Inter-Component Communication: Typed Events with Mitt
The Pascal Editor implements a centralized, strongly-typed event bus using the lightweight mitt library to decouple UI components, tools, and systems across the @pascal-app/core, viewer, and editor packages.
The pascalorg/editor repository relies on a single shared emitter instance to coordinate complex interactions between the Three.js-based viewer, property grids, and keyboard-controlled editing tools. This article examines how the Pascal Editor event bus for inter-component communication enables maintainable architecture through strongly-typed payloads, consistent event suffixes, and package-agnostic consumption patterns.
Core Architecture in packages/core/src/events/bus.ts
The foundation of the system resides in packages/core/src/events/bus.ts, which exports a configured mitt instance and comprehensive TypeScript definitions.
The Mitt-Based Emitter
The bus instantiates a single global emitter typed with a composite EditorEvents union:
export const emitter = mitt<EditorEvents>()
export const eventSuffixes = [...] as const
This design ensures that every package—@pascal-app/core, @pascal-app/viewer, and @pascal-app/editor—references the same event bus instance imported from the core package's public API.
Typed Event Payloads and Suffixes
The architecture enforces strict typing through the NodeEvent<T> interface, which describes event payloads containing the node reference, world/local positions, optional surface normals, and a stopPropagation helper. The system reuses event suffixes (click, move, enter, leave) across all node types, enabling a concise naming scheme such as wall:click or item:pointerdown. This approach generates a strongly-typed EditorEvents union that covers grid interactions, camera controls, tool actions, and preset operations.
Consuming Events Across Packages
Different subsystems consume the Pascal Editor event bus through specialized hooks that translate raw inputs into typed emissions.
Node Interactions via useNodeEvents
Located in packages/viewer/src/hooks/use-node-events.ts, this hook intercepts React Three Fiber (R3F) pointer events on mesh components and converts them into typed node events (e.g., wall:pointerdown, wall:click). It automatically wires callbacks like onPointerDown and onPointerEnter to the global emitter, keeping mesh components declarative and free of direct event handling logic.
Grid Raycasting with useGridEvents
The packages/editor/src/hooks/use-grid-events.ts module performs manual raycasting on the HTML5 canvas to detect interactions with the custom grid plane. It constructs GridEvent payloads and emits events such as grid:click and grid:pointermove, enabling tools to snap to cursor positions without relying on Three.js object intersection.
Keyboard Shortcuts and Tool Actions
In packages/editor/src/hooks/use-keyboard.ts, keyboard listeners emit high-level tool actions like tool:cancel when users press Esc, or tool:commit on Enter. This abstraction allows tool logic to remain keyboard-agnostic while reacting to semantic event names.
Practical Implementation Examples
Subscribing to Wall Click Events
Components subscribe to specific node events using the shared emitter and must cleanup listeners in effect hooks:
import { useEffect } from 'react'
import { emitter } from '@pascal-app/core'
export function WallClickListener() {
useEffect(() => {
const handler = (e: WallEvent) => {
console.log('Wall clicked:', e.node.id, e.position)
// Custom logic – e.g., open a property panel
}
emitter.on('wall:click', handler)
return () => emitter.off('wall:click', handler)
}, [])
return null
}
The WallEvent type is defined in packages/core/src/events/bus.ts at line 34, ensuring full IntelliSense support for the payload structure.
Emitting Custom Preset Events
Arbitrary systems can emit events to trigger cross-component workflows:
import { emitter } from '@pascal-app/core'
function generateThumbnail(presetId: string, nodeId: string) {
emitter.emit('preset:generate-thumbnail', { presetId, nodeId })
}
Corresponding listeners update UI state when asynchronous operations complete:
emitter.on('preset:thumbnail-updated', ({ presetId, thumbnailUrl }) => {
// Update preview image, cache, etc.
})
Declarative Event Binding with Hooks
The useNodeEvents hook abstracts event registration for Three.js meshes:
import { useNodeEvents } from '@pascal-app/viewer'
import { WallNode } from '@pascal-app/core'
type Props = { wall: WallNode }
export function WallMesh({ wall }: Props) {
const nodeEvents = useNodeEvents(wall, 'wall')
return (
<mesh
{...nodeEvents}
// ...geometry, material
/>
)
}
This pattern automatically spreads pointer event handlers onto the mesh, emitting typed bus events without imperative code.
Tracking Grid Movement in Tools
Tools requiring cursor position tracking subscribe to grid-specific events:
import { useEffect } from 'react'
import { emitter, type GridEvent } from '@pascal-app/core'
export function GridTracker() {
useEffect(() => {
const onMove = (e: GridEvent) => {
console.log('Cursor over grid at', e.position)
}
emitter.on('grid:move', onMove)
return () => emitter.off('grid:move', onMove)
}, [])
return null
}
Key Source Files Reference
The Pascal Editor event bus implementation spans several critical files:
packages/core/src/events/bus.ts– Central definition of themittinstance,EditorEventstypes,NodeEvent<T>interfaces, andeventSuffixesconstants.packages/core/src/index.ts– Public API surface that re-exportsemitterandeventSuffixesfor cross-package consumption.packages/viewer/src/hooks/use-node-events.ts– Hook translating R3F pointer events into typed node bus events.packages/editor/src/hooks/use-grid-events.ts– Canvas raycasting implementation emittinggrid:*events.packages/editor/src/hooks/use-keyboard.ts– Keyboard shortcut handler emitting high-level tool commands.packages/editor/src/lib/sfx-bus.ts– Demonstrates coexistence of multiple mitt instances by implementing a separate sound-effects bus.
Summary
- The Pascal Editor uses a single typed mitt instance exported from
packages/core/src/events/bus.tsto coordinate all inter-component communication. - Strongly-typed payloads via
NodeEvent<T>and compositeEditorEventsunions eliminate runtime type errors across the monorepo. - Consistent suffix patterns (
:click,:move,:enter) enable predictable event naming across node types like walls, items, and zones. - Specialized hooks (
useNodeEvents,useGridEvents,useKeyboard) abstract input sources while publishing to the centralized bus, maintaining loose coupling between the viewer, editor, and tool systems.
Frequently Asked Questions
What event library powers the Pascal Editor event bus?
The Pascal Editor uses mitt, a lightweight 200-byte event emitter. The source code in packages/core/src/events/bus.ts imports mitt and creates a single strongly-typed instance: export const emitter = mitt<EditorEvents>(). This tiny footprint keeps the core package size minimal while supporting typed event payloads.
How does the Pascal Editor maintain type safety across package boundaries?
Type safety is enforced through TypeScript's generic constraints. The EditorEvents interface in bus.ts defines all possible event keys and payload types. When packages like @pascal-app/viewer or @pascal-app/editor import the emitter, they receive the same typed instance, ensuring that emitter.on('wall:click') automatically infers the WallEvent payload structure without additional runtime checks.
Can multiple event buses coexist in the Pascal Editor architecture?
Yes. While the primary inter-component communication uses the shared emitter, the codebase demonstrates isolated buses for specific concerns. The packages/editor/src/lib/sfx-bus.ts file creates a separate mitt instance dedicated to sound effects, proving that multiple typed buses can operate independently without event leakage between subsystems.
How do grid events differ from node events in the implementation?
Grid events originate from manual raycasting logic in useGridEvents that queries the HTML5 canvas directly, emitting events like grid:click with custom GridEvent payloads containing world positions. Node events, handled by useNodeEvents, derive from React Three Fiber's pointer event system on Three.js meshes, automatically calculating intersections and surface normals before emitting typed events like wall:pointerdown or item:click.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →