From a8b1bb2dd409b158053ff5c9a58a9fb7d35e2473 Mon Sep 17 00:00:00 2001 From: Henry Allen <31718268+hmallen99@users.noreply.github.com> Date: Tue, 7 May 2024 13:42:28 -0400 Subject: [PATCH] Implement fireEvent and createEvent (#13) * Implement events * Add tests * Ensure export * Add wheel observable and more tests --- src/event.spec.ts | 118 ++++++++++++++++++++++++++++++++++++++++++++++ src/event.ts | 73 ++++++++++++++++++++++++++++ src/eventMap.ts | 66 ++++++++++++++++++++++++++ src/index.ts | 1 + 4 files changed, 258 insertions(+) create mode 100644 src/event.spec.ts create mode 100644 src/event.ts create mode 100644 src/eventMap.ts diff --git a/src/event.spec.ts b/src/event.spec.ts new file mode 100644 index 0000000..fa0252d --- /dev/null +++ b/src/event.spec.ts @@ -0,0 +1,118 @@ +import { + Engine, + Mesh, + MeshBuilder, + NullEngine, + Scene, + Vector2, +} from '@babylonjs/core'; +import { Event, createEvent, fireEvent } from './event'; +import { EventMap, eventMap } from './eventMap'; +import { AdvancedDynamicTexture, Button, Control, Grid } from '@babylonjs/gui'; + +describe('event', () => { + let scene: Scene, + engine: Engine, + texture: AdvancedDynamicTexture, + containerControl: Grid, + expectedControl: Control, + uiPlane: Mesh; + + beforeAll(() => { + engine = new NullEngine(); + }); + + beforeEach(() => { + scene = new Scene(engine); + + uiPlane = MeshBuilder.CreatePlane('container'); + texture = AdvancedDynamicTexture.CreateForMesh(uiPlane); + + containerControl = new Grid('container'); + containerControl.addColumnDefinition(1); + containerControl.addColumnDefinition(1); + texture.addControl(containerControl); + + expectedControl = new Button('button'); + containerControl.addControl(expectedControl, 0, 0); + }); + + afterEach(() => { + texture.dispose(); + uiPlane.material?.dispose(); + uiPlane.dispose(); + scene.dispose(); + }); + + afterAll(() => { + engine.dispose(); + }); + + it.each(Object.keys(eventMap))( + 'should create a default event for %s', + (key: keyof EventMap) => { + const eventFunc = createEvent[key]; + expect(eventFunc).toBeDefined(); + + const event = eventMap[key]; + expect(eventFunc(expectedControl)).toEqual({ + key, + observableName: event.observableName, + eventData: event.defaultInit, + }); + } + ); + + it.each(Object.keys(eventMap))( + 'should fire the %s event', + (key: keyof EventMap) => { + const spy = jest.fn(); + const { observableName } = eventMap[key]; + + expectedControl[observableName].add(spy as null); + + const fireFunc = fireEvent[key]; + expect(fireFunc).toBeDefined(); + + fireFunc(expectedControl); + + expect(spy).toHaveBeenCalledTimes(1); + } + ); + + it('should throw when fireEvent is called with a bad observable name', () => { + expect(() => { + fireEvent(expectedControl, { + key: 'bogusEvent', + observableName: 'notAnObservable', + eventData: {}, + } as unknown as Event<'pointerClick'>); + }).toThrow( + new Error( + `Unable to fire an event - event of type "bogusEvent" does not exist on "${expectedControl.getClassName()}"` + ) + ); + }); + + it('should allow passing custom data to fireEvent', () => { + const spy = jest.fn(); + expectedControl.onWheelObservable.add((eventData) => { + spy(eventData); + }); + + fireEvent.wheel(expectedControl, new Vector2(0.5, 0.5)); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(new Vector2(0.5, 0.5)); + }); + + it('should allow passing custom data to createEvent', () => { + const event = createEvent.wheel(expectedControl, new Vector2(0.5, 0.5)); + + expect(event).toEqual({ + key: 'wheel', + observableName: 'onWheelObservable', + eventData: new Vector2(0.5, 0.5), + }); + }); +}); diff --git a/src/event.ts b/src/event.ts new file mode 100644 index 0000000..1e9845c --- /dev/null +++ b/src/event.ts @@ -0,0 +1,73 @@ +import { Observable } from '@babylonjs/core'; +import { Control } from '@babylonjs/gui'; +import { EventMap, eventMap } from './eventMap'; + +export type Event = { + key: K; + observableName: EventMap[K]['observableName']; + eventData: EventMap[K]['defaultInit']; +}; + +function fireEventFn( + control: Control, + event: Event +) { + const eventObservable = control[event.observableName]; + + if (!(eventObservable instanceof Observable)) { + throw new Error( + `Unable to fire an event - event of type "${ + event.key + }" does not exist on "${control.getClassName()}"` + ); + } + + (eventObservable as Observable).notifyObservers( + event.eventData + ); +} + +function createEventFn( + eventName: K, + _control: Control, + init: EventMap[K]['defaultInit'] +): Event { + const event = eventMap[eventName]; + return { + key: eventName, + observableName: event.observableName, + eventData: init, + }; +} + +Object.keys(eventMap).forEach((key: keyof EventMap) => { + const { defaultInit } = eventMap[key]; + createEventFn[key] = (node: Control, init?: typeof defaultInit) => + createEventFn(key, node, init ?? defaultInit); + + fireEventFn[key] = (node: Control, init?: typeof defaultInit) => { + fireEventFn(node, createEventFn[key](node, init)); + }; +}); + +type CreateEventFn = ( + k: keyof EventMap, + c: Control, + i: EventMap[keyof EventMap]['defaultInit'] +) => Event; + +type CreateEventObject = { + [K in keyof EventMap]: ( + c: Control, + i?: EventMap[K]['defaultInit'] + ) => Event; +}; + +type fireEventFn = (c: Control, e: Event) => void; + +type fireEventObject = { + [K in keyof EventMap]: (c: Control, i?: EventMap[K]['defaultInit']) => void; +}; + +export const createEvent = createEventFn as CreateEventFn & CreateEventObject; +export const fireEvent = fireEventFn as fireEventFn & fireEventObject; diff --git a/src/eventMap.ts b/src/eventMap.ts new file mode 100644 index 0000000..e2aaddb --- /dev/null +++ b/src/eventMap.ts @@ -0,0 +1,66 @@ +import { Vector2 } from '@babylonjs/core'; +import { Vector2WithInfo } from '@babylonjs/gui'; + +export type EventMap = { + pointerMove: { + observableName: 'onPointerUpObservable'; + defaultInit: Vector2WithInfo; + }; + pointerEnter: { + observableName: 'onPointerEnterObservable'; + defaultInit: Vector2WithInfo; + }; + pointerOut: { + observableName: 'onPointerOutObservable'; + defaultInit: Vector2WithInfo; + }; + pointerDown: { + observableName: 'onPointerDownObservable'; + defaultInit: Vector2WithInfo; + }; + pointerUp: { + observableName: 'onPointerUpObservable'; + defaultInit: Vector2WithInfo; + }; + pointerClick: { + observableName: 'onPointerClickObservable'; + defaultInit: Vector2WithInfo; + }; + wheel: { + observableName: 'onWheelObservable'; + defaultInit: Vector2; + }; +}; + +const DEFAULT_VECTOR2_WITH_INFO = new Vector2WithInfo(Vector2.Zero()); + +export const eventMap: EventMap = { + pointerMove: { + observableName: 'onPointerUpObservable', + defaultInit: DEFAULT_VECTOR2_WITH_INFO, + }, + pointerEnter: { + observableName: 'onPointerEnterObservable', + defaultInit: DEFAULT_VECTOR2_WITH_INFO, + }, + pointerOut: { + observableName: 'onPointerOutObservable', + defaultInit: DEFAULT_VECTOR2_WITH_INFO, + }, + pointerDown: { + observableName: 'onPointerDownObservable', + defaultInit: DEFAULT_VECTOR2_WITH_INFO, + }, + pointerUp: { + observableName: 'onPointerUpObservable', + defaultInit: DEFAULT_VECTOR2_WITH_INFO, + }, + pointerClick: { + observableName: 'onPointerClickObservable', + defaultInit: DEFAULT_VECTOR2_WITH_INFO, + }, + wheel: { + observableName: 'onWheelObservable', + defaultInit: Vector2.Zero(), + }, +}; diff --git a/src/index.ts b/src/index.ts index 3cf1ef3..aa20066 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export * from './queries'; +export * from './event';