From 0c5a29b403c52cfb4640f9dfb1531d6d2ca04f28 Mon Sep 17 00:00:00 2001 From: jankuss Date: Fri, 12 Feb 2021 11:00:55 -0800 Subject: [PATCH 1/8] Disable animation queueing --- src/objects/animation/ObjectAnimation.ts | 14 +++++++------- storybook/stories/avatar/Avatar.stories.ts | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/objects/animation/ObjectAnimation.ts b/src/objects/animation/ObjectAnimation.ts index b1922096..8e57a42d 100644 --- a/src/objects/animation/ObjectAnimation.ts +++ b/src/objects/animation/ObjectAnimation.ts @@ -1,6 +1,8 @@ import { IAnimationTicker } from "../../interfaces/IAnimationTicker"; import { RoomPosition } from "../../types/RoomPosition"; +type FinishCurrentCallback = () => void; + export class ObjectAnimation { private _current: RoomPosition | undefined; private _diff: RoomPosition | undefined; @@ -11,6 +13,7 @@ export class ObjectAnimation { data: T; }[] = []; private _nextPosition: RoomPosition | undefined; + private _finishCurrent: FinishCurrentCallback | undefined; constructor( private _animationTicker: IAnimationTicker, @@ -32,13 +35,9 @@ export class ObjectAnimation { newPos: { roomX: number; roomY: number; roomZ: number }, data: T ) { - if (this._diff != null) { - this._enqueued.push({ - currentPosition: currentPos, - newPosition: newPos, - data: data, - }); - return; + if (this._finishCurrent != null) { + this._finishCurrent(); + this._finishCurrent = undefined; } this._callbacks.onStart(data); @@ -66,6 +65,7 @@ export class ObjectAnimation { cancel(); }; + this._finishCurrent = handleFinish; this._callbacks.onUpdatePosition( { diff --git a/storybook/stories/avatar/Avatar.stories.ts b/storybook/stories/avatar/Avatar.stories.ts index 4228fbe3..2c16e84d 100644 --- a/storybook/stories/avatar/Avatar.stories.ts +++ b/storybook/stories/avatar/Avatar.stories.ts @@ -103,10 +103,10 @@ export function Walking() { setTimeout(() => { avatar.walk(1, 2, 0, { direction: 4 }); - avatar.walk(2, 2, 0, { direction: 2 }); - avatar.walk(3, 2, 0, { direction: 2 }); - avatar.walk(4, 2, 0); - avatar.walk(4, 3, 0, { direction: 4, headDirection: 5 }); + + setTimeout(() => { + avatar.walk(2, 2, 0, { direction: 2 }); + }, 500); }, 3000); room.x = application.screen.width / 2 - room.roomWidth / 2; From fa0d5d2a0dc2876ff70e8b1dbba27f4758ecca74 Mon Sep 17 00:00:00 2001 From: jankuss Date: Fri, 12 Feb 2021 19:25:10 -0800 Subject: [PATCH 2/8] Add quad tree --- src/interfaces/IHitDetection.ts | 1 + src/objects/hitdetection/HitDetection.ts | 32 +++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/interfaces/IHitDetection.ts b/src/interfaces/IHitDetection.ts index e8a2d7a1..1c76c6f4 100644 --- a/src/interfaces/IHitDetection.ts +++ b/src/interfaces/IHitDetection.ts @@ -24,6 +24,7 @@ export interface HitDetectionElement { hits(x: number, y: number): boolean; getHitDetectionZIndex(): number; createDebugSprite?(): PIXI.Sprite | undefined; + getQuadTreeItem(): Quadtree.QuadtreeItem; } export interface HitDetectionNode { diff --git a/src/objects/hitdetection/HitDetection.ts b/src/objects/hitdetection/HitDetection.ts index c1c75d5d..6217a822 100644 --- a/src/objects/hitdetection/HitDetection.ts +++ b/src/objects/hitdetection/HitDetection.ts @@ -6,13 +6,18 @@ import { HitEventType, IHitDetection, } from "../../interfaces/IHitDetection"; +import QuadTree from "quadtree-lib"; +import { Rectangle } from "../room/IRoomRectangle"; -export class HitDetection implements IHitDetection { +export class HitDetection extends PIXI.Container implements IHitDetection { private _counter = 0; private _map: Map = new Map(); private _container: PIXI.Container | undefined; + private _quadTree: QuadTree | undefined; + private _hitAreaRect: Rectangle | undefined; constructor(private _app: PIXI.Application) { + super(); _app.view.addEventListener("click", (event) => this.handleClick(event), { capture: true, }); @@ -38,13 +43,26 @@ export class HitDetection implements IHitDetection { return new HitDetection(application); } + updateHitArea(rectangle: Rectangle) { + this._hitAreaRect = rectangle; + this._updateQuadTree(); + } + register(rectangle: HitDetectionElement): HitDetectionNode { const id = this._counter++; this._map.set(id, rectangle); + if (this._quadTree != null) { + this._quadTree.push(rectangle.getQuadTreeItem()); + } + return { remove: () => { this._map.delete(id); + + if (this._quadTree != null) { + this._quadTree.remove(rectangle.getQuadTreeItem()); + } }, }; } @@ -61,6 +79,18 @@ export class HitDetection implements IHitDetection { this._triggerEvent(event.clientX, event.clientY, "pointerup", event); } + private _updateQuadTree() { + if (this._hitAreaRect == null) + throw new Error("Invalid hit area rectangle"); + + const tree = new QuadTree(this._hitAreaRect); + + this._quadTree = tree; + this._map.forEach((element) => { + tree.push(element.getQuadTreeItem()); + }); + } + private _triggerEvent( x: number, y: number, From 85827532e9122986258c559ca4e20bf29053b3fb Mon Sep 17 00:00:00 2001 From: jankuss Date: Tue, 16 Feb 2021 09:23:31 -0800 Subject: [PATCH 3/8] Improve event handling --- package-lock.json | 24 ++ package.json | 6 +- src/index.ts | 2 - src/interfaces/IHitDetection.ts | 9 +- src/interfaces/IRoomContext.ts | 4 +- src/objects/RoomObject.ts | 4 +- src/objects/Shroom.ts | 4 - src/objects/avatar/Avatar.ts | 4 +- src/objects/avatar/BaseAvatar.ts | 37 +- src/objects/events/EventEmitter.ts | 44 +++ src/objects/events/EventManager.test.ts | 336 ++++++++++++++++++ src/objects/events/EventManager.ts | 164 +++++++++ src/objects/events/EventManagerContainer.ts | 57 +++ src/objects/events/EventManagerNode.ts | 60 ++++ src/objects/events/interfaces/IEventGroup.ts | 12 + .../events/interfaces/IEventHandler.ts | 9 + .../events/interfaces/IEventHittable.ts | 10 + .../events/interfaces/IEventManager.ts | 7 + .../events/interfaces/IEventManagerEvent.ts | 7 + .../events/interfaces/IEventManagerNode.ts | 5 + src/objects/events/interfaces/IEventTarget.ts | 4 + src/objects/furniture/BaseFurniture.tsx | 32 +- src/objects/furniture/FloorFurniture.tsx | 2 +- src/objects/furniture/WallFurniture.tsx | 2 +- .../furniture/util/IFurnitureEventHandlers.ts | 9 +- src/objects/hitdetection/ClickHandler.ts | 13 +- src/objects/hitdetection/HitDetection.test.ts | 129 ------- src/objects/hitdetection/HitDetection.ts | 219 ------------ src/objects/hitdetection/HitSprite.ts | 148 +++++--- src/objects/room/Room.ts | 13 +- src/objects/room/RoomModelVisualization.ts | 10 +- src/objects/room/parts/TileCursor.ts | 109 ++++-- 32 files changed, 995 insertions(+), 500 deletions(-) create mode 100644 src/objects/events/EventEmitter.ts create mode 100644 src/objects/events/EventManager.test.ts create mode 100644 src/objects/events/EventManager.ts create mode 100644 src/objects/events/EventManagerContainer.ts create mode 100644 src/objects/events/EventManagerNode.ts create mode 100644 src/objects/events/interfaces/IEventGroup.ts create mode 100644 src/objects/events/interfaces/IEventHandler.ts create mode 100644 src/objects/events/interfaces/IEventHittable.ts create mode 100644 src/objects/events/interfaces/IEventManager.ts create mode 100644 src/objects/events/interfaces/IEventManagerEvent.ts create mode 100644 src/objects/events/interfaces/IEventManagerNode.ts create mode 100644 src/objects/events/interfaces/IEventTarget.ts delete mode 100644 src/objects/hitdetection/HitDetection.test.ts delete mode 100644 src/objects/hitdetection/HitDetection.ts diff --git a/package-lock.json b/package-lock.json index 2a4340f3..8b99c9b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1268,6 +1268,11 @@ "@sinonjs/commons": "^1.7.0" } }, + "@timohausmann/quadtree-js": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.3.tgz", + "integrity": "sha512-uXGWcikTrN7fjriXwZa93+MorzrdHOUEMBgiwaemA4Kix9Gu2KW0zw6tvDPa+s2EKeKusf64vj29pUmT0eObFA==" + }, "@tweenjs/tween.js": { "version": "18.6.4", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz", @@ -1481,6 +1486,12 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", "dev": true }, + "@types/rbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.0.tgz", + "integrity": "sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg==", + "dev": true + }, "@types/react": { "version": "16.14.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.3.tgz", @@ -6776,6 +6787,19 @@ "integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==", "dev": true }, + "quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, + "rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "requires": { + "quickselect": "^2.0.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", diff --git a/package.json b/package.json index d7cef83b..21519434 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@types/jsdom": "^16.2.6", "@types/node": "^14.14.13", "@types/node-fetch": "^2.5.7", + "@types/rbush": "^3.0.0", "@types/react": "^16.9.56", "@types/tween.js": "^18.6.1", "@types/ws": "^7.4.0", @@ -40,6 +41,7 @@ }, "dependencies": { "@gizeta/swf-reader": "^1.0.0", + "@timohausmann/quadtree-js": "^1.2.3", "axios": "^0.21.1", "bin-pack": "^1.0.2", "bluebird": "^3.7.2", @@ -55,6 +57,7 @@ "jszip": "^3.5.0", "node-fetch": "^2.6.1", "quadtree-lib": "^1.0.9", + "rbush": "^3.0.1", "react": "^16.14.0", "rxjs": "^6.6.3", "stream": "0.0.2", @@ -74,7 +77,8 @@ "dump": "yarn ts-node-dev src/downloading/cli/index.tsx dump", "test": "jest", "build": "rm -rf dist && tsc", - "prepublishOnly": "yarn build" + "prepublishOnly": "yarn build", + "storybook": "cd storybook && yarn storybook" }, "bin": { "shroom": "dist/cli/index.js" diff --git a/src/index.ts b/src/index.ts index 3227b865..b56c7bcb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,12 +7,10 @@ export { RoomCamera } from "./objects/room/RoomCamera"; export { loadRoomTexture } from "./util/loadRoomTexture"; export { parseTileMapString } from "./util/parseTileMapString"; export { IFurniture, IFurnitureBehavior } from "./objects/furniture/IFurniture"; -export { HitEvent } from "./interfaces/IHitDetection"; export { IFurnitureData } from "./interfaces/IFurnitureData"; export { FurnitureData } from "./objects/furniture/FurnitureData"; export { Shroom } from "./objects/Shroom"; export { Landscape } from "./objects/room/Landscape"; -export { HitDetection } from "./objects/hitdetection/HitDetection"; export { AnimationTicker } from "./objects/animation/AnimationTicker"; export { FurnitureLoader } from "./objects/furniture/FurnitureLoader"; export { AvatarLoader } from "./objects/avatar/AvatarLoader"; diff --git a/src/interfaces/IHitDetection.ts b/src/interfaces/IHitDetection.ts index 1c76c6f4..6c4ee82f 100644 --- a/src/interfaces/IHitDetection.ts +++ b/src/interfaces/IHitDetection.ts @@ -1,3 +1,6 @@ +import { InteractionEvent } from "pixi.js"; +import { Rectangle } from "../objects/room/IRoomRectangle"; + export interface Rect { x: number; y: number; @@ -9,7 +12,9 @@ export interface Rect { export type HitEventType = "click" | "pointerdown" | "pointerup"; export interface HitEvent { - mouseEvent: MouseEvent; + mouseEvent: MouseEvent | TouchEvent | PointerEvent; + interactionEvent: InteractionEvent; + tag?: string; target: HitDetectionElement; @@ -24,11 +29,11 @@ export interface HitDetectionElement { hits(x: number, y: number): boolean; getHitDetectionZIndex(): number; createDebugSprite?(): PIXI.Sprite | undefined; - getQuadTreeItem(): Quadtree.QuadtreeItem; } export interface HitDetectionNode { remove(): void; + updateDimensions(element: Rectangle | undefined): void; } export interface IHitDetection { diff --git a/src/interfaces/IRoomContext.ts b/src/interfaces/IRoomContext.ts index 22f746a0..893e6379 100644 --- a/src/interfaces/IRoomContext.ts +++ b/src/interfaces/IRoomContext.ts @@ -1,10 +1,10 @@ +import { IEventManager } from "../objects/events/interfaces/IEventManager"; import { ILandscapeContainer } from "../objects/room/ILandscapeContainer"; import { Room } from "../objects/room/Room"; import { IAnimationTicker } from "./IAnimationTicker"; import { IAvatarLoader } from "./IAvatarLoader"; import { IConfiguration } from "./IConfiguration"; import { IFurnitureLoader } from "./IFurnitureLoader"; -import { IHitDetection } from "./IHitDetection"; import { IRoomGeometry } from "./IRoomGeometry"; import { IRoomObjectContainer } from "./IRoomObjectContainer"; import { IRoomVisualization } from "./IRoomVisualization"; @@ -17,10 +17,10 @@ export interface IRoomContext { animationTicker: IAnimationTicker; visualization: IRoomVisualization; roomObjectContainer: IRoomObjectContainer; - hitDetection: IHitDetection; configuration: IConfiguration; tilemap: ITileMap; landscapeContainer: ILandscapeContainer; application: PIXI.Application; room: Room; + eventManager: IEventManager; } diff --git a/src/objects/RoomObject.ts b/src/objects/RoomObject.ts index 7c19d2a5..666f7578 100644 --- a/src/objects/RoomObject.ts +++ b/src/objects/RoomObject.ts @@ -72,8 +72,8 @@ export abstract class RoomObject implements IRoomObject { return this.getRoomContext().avatarLoader; } - protected get hitDetection() { - return this.getRoomContext().hitDetection; + protected get eventManager() { + return this.getRoomContext().eventManager; } protected get tilemap() { diff --git a/src/objects/Shroom.ts b/src/objects/Shroom.ts index 48f05b7e..ea5cc3e6 100644 --- a/src/objects/Shroom.ts +++ b/src/objects/Shroom.ts @@ -2,7 +2,6 @@ import { AnimationTicker } from "./animation/AnimationTicker"; import { AvatarLoader } from "./avatar/AvatarLoader"; import { FurnitureLoader } from "./furniture/FurnitureLoader"; import { FurnitureData } from "./furniture/FurnitureData"; -import { HitDetection } from "./hitdetection/HitDetection"; import { Dependencies } from "./room/Room"; export class Shroom { @@ -31,7 +30,6 @@ export class Shroom { avatarLoader, furnitureData, furnitureLoader, - hitDetection, }: { resourcePath?: string; } & Partial) { @@ -45,7 +43,6 @@ export class Shroom { return { for: (application: PIXI.Application) => { - const _hitDetection = hitDetection ?? HitDetection.create(application); const _animationTicker = animationTicker ?? AnimationTicker.create(application); @@ -53,7 +50,6 @@ export class Shroom { animationTicker: _animationTicker, avatarLoader: _avatarLoader, furnitureLoader: _furnitureLoader, - hitDetection: _hitDetection, configuration: _configuration, furnitureData: _furnitureData, application, diff --git a/src/objects/avatar/Avatar.ts b/src/objects/avatar/Avatar.ts index ea6b0c9f..cc7305a3 100644 --- a/src/objects/avatar/Avatar.ts +++ b/src/objects/avatar/Avatar.ts @@ -376,14 +376,14 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { this._placeholderSprites.dependencies = { animationTicker: this.animationTicker, avatarLoader: this.avatarLoader, - hitDetection: this.hitDetection, + eventManager: this.eventManager, }; } this._loadingAvatarSprites.dependencies = { animationTicker: this.animationTicker, avatarLoader: this.avatarLoader, - hitDetection: this.hitDetection, + eventManager: this.eventManager, }; this._updateAvatarSprites(); diff --git a/src/objects/avatar/BaseAvatar.ts b/src/objects/avatar/BaseAvatar.ts index a8d7c015..542ef438 100644 --- a/src/objects/avatar/BaseAvatar.ts +++ b/src/objects/avatar/BaseAvatar.ts @@ -7,7 +7,6 @@ import { import { ClickHandler } from "../hitdetection/ClickHandler"; import { HitSprite } from "../hitdetection/HitSprite"; import { isSetEqual } from "../../util/isSetEqual"; -import { IHitDetection } from "../../interfaces/IHitDetection"; import { IAnimationTicker } from "../../interfaces/IAnimationTicker"; import { Shroom } from "../Shroom"; import { AvatarFigurePartType } from "./enum/AvatarFigurePartType"; @@ -17,6 +16,13 @@ import { DefaultAvatarDrawPart, } from "./types"; import { AvatarDrawDefinition } from "./structure/AvatarDrawDefinition"; +import { IEventManager } from "../events/interfaces/IEventManager"; +import { NOOP_EVENT_MANAGER } from "../events/EventManager"; +import { + AVATAR_EVENT, + EventGroupIdentifier, + IEventGroup, +} from "../events/interfaces/IEventGroup"; const bodyPartTypes: Set = new Set([ AvatarFigurePartType.Head, @@ -47,15 +53,16 @@ export interface BaseAvatarOptions { } export interface BaseAvatarDependencies { - hitDetection: IHitDetection; + eventManager: IEventManager; animationTicker: IAnimationTicker; avatarLoader: IAvatarLoader; } -export class BaseAvatar extends PIXI.Container { +export class BaseAvatar extends PIXI.Container implements IEventGroup { private _container: PIXI.Container | undefined; private _avatarLoaderResult: AvatarLoaderResult | undefined; private _avatarDrawDefinition: AvatarDrawDefinition | undefined; + private _avatarDestroyed = false; private _lookOptions: LookOptions | undefined; private _nextLookOptions: LookOptions | undefined; @@ -182,10 +189,17 @@ export class BaseAvatar extends PIXI.Container { static fromShroom(shroom: Shroom, options: BaseAvatarOptions) { const avatar = new BaseAvatar({ ...options }); - avatar.dependencies = shroom.dependencies; + avatar.dependencies = { + ...shroom.dependencies, + eventManager: NOOP_EVENT_MANAGER, + }; return avatar; } + getEventGroupIdentifier(): EventGroupIdentifier { + return AVATAR_EVENT; + } + destroy(): void { super.destroy(); this._destroyAssets(); @@ -250,6 +264,7 @@ export class BaseAvatar extends PIXI.Container { drawDefinition: AvatarDrawDefinition, currentFrame: number ) { + if (this._destroyed) throw new Error("BaseAvatar was destroyed already"); if (!this.mounted) return; this._sprites.forEach((value) => { @@ -342,24 +357,24 @@ export class BaseAvatar extends PIXI.Container { if (texture == null) return; const sprite = new HitSprite({ - hitDetection: this.dependencies.hitDetection, + eventManager: this.dependencies.eventManager, mirrored: asset.mirror, group: this, }); sprite.hitTexture = texture; - sprite.x = asset.x; sprite.y = asset.y; - sprite.addEventListener("click", (event) => { + + sprite.events.addEventListener("click", (event) => { this._clickHandler.handleClick(event); }); - sprite.addEventListener("pointerdown", (event) => { + sprite.events.addEventListener("pointerdown", (event) => { this._clickHandler.handlePointerDown(event); }); - sprite.addEventListener("pointerup", (event) => { + sprite.events.addEventListener("pointerup", (event) => { this._clickHandler.handlePointerUp(event); }); @@ -391,6 +406,7 @@ export class BaseAvatar extends PIXI.Container { skipCaching: this._skipCaching, }) .then((result) => { + if (this._destroyed) return; if (requestId !== this._updateId) return; this._avatarLoaderResult = result; @@ -424,6 +440,9 @@ export class BaseAvatar extends PIXI.Container { this._cancelTicker(); } + if (this._cancelTicker != null) { + this._cancelTicker(); + } this._cancelTicker = this.dependencies.animationTicker.subscribe(() => { if (this._refreshLook) { this._refreshLook = false; diff --git a/src/objects/events/EventEmitter.ts b/src/objects/events/EventEmitter.ts new file mode 100644 index 00000000..042f19fe --- /dev/null +++ b/src/objects/events/EventEmitter.ts @@ -0,0 +1,44 @@ +export class EventEmitter> { + private _map = new Map>>(); + + addEventListener( + name: K, + callback: EventCallback + ) { + const key = name.toString(); + const currentEventCallbackSet = + this._map.get(key) ?? new Set>(); + currentEventCallbackSet.add(callback); + + this._map.set(key, currentEventCallbackSet); + } + + removeEventListener( + name: K, + callback: EventCallback + ) { + const key = name.toString(); + const currentEventCallbackSet = this._map.get(key); + + if (currentEventCallbackSet != null) { + currentEventCallbackSet.delete(callback); + } + } + + trigger(name: K, value: TMap[K]) { + const key = name.toString(); + const currentEventCallbackSet = this._map.get(key); + + currentEventCallbackSet?.forEach((callback) => callback(value)); + } +} + +type EventCallback> = ( + event: TMap[K] +) => void; + +window.addEventListener; + +type BaseTypeMap = { + [k in keyof T]: unknown; +}; diff --git a/src/objects/events/EventManager.test.ts b/src/objects/events/EventManager.test.ts new file mode 100644 index 00000000..23861455 --- /dev/null +++ b/src/objects/events/EventManager.test.ts @@ -0,0 +1,336 @@ +import { BehaviorSubject } from "rxjs"; +import { Rectangle } from "../room/IRoomRectangle"; +import { EventManager } from "./EventManager"; +import { + AVATAR_EVENT, + FURNITURE_EVENT, + IEventGroup, +} from "./interfaces/IEventGroup"; +import { IEventTarget } from "./interfaces/IEventTarget"; + +test("handles click when mounted", () => { + const manager = new EventManager(); + + const target: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const node = manager.register(target); + manager.click(10, 10); + expect(target.triggerClick).toHaveBeenCalledTimes(1); + + node.destroy(); + manager.click(10, 10); + expect(target.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("handles click on hit elements", () => { + const manager = new EventManager(); + + const target: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const node = manager.register(target); + manager.click(10, 10); + expect(target.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("doesn't handle click on not hit elements", () => { + const manager = new EventManager(); + + const target: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => false, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const node = manager.register(target); + manager.click(10, 10); + expect(target.triggerClick).toHaveBeenCalledTimes(0); +}); + +test("handles click on multiple elements", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const target3: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 75, y: 75, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const target4: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 79, y: 79, width: 100, height: 100 }), + hits: () => false, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + manager.register(target3); + manager.register(target4); + + manager.click(80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(1); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); + expect(target3.triggerClick).toHaveBeenCalledTimes(1); + expect(target4.triggerClick).toHaveBeenCalledTimes(0); +}); + +test("handles click on multiple elements", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + + manager.click(80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(1); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("only handles first element when elements from same group", () => { + const manager = new EventManager(); + + const group: IEventGroup = { getEventGroupIdentifier: () => FURNITURE_EVENT }; + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => group, + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => group, + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + + manager.click(80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(0); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("event.skip() skips elements", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn((event) => event.skip([AVATAR_EVENT])), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const target3: IEventTarget = { + getEventZOrder: () => 9, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 75, y: 75, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + const target4: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 79, y: 79, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn((event) => event.skip([FURNITURE_EVENT])), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + manager.register(target3); + manager.register(target4); + + manager.click(80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(0); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); + expect(target3.triggerClick).toHaveBeenCalledTimes(0); + expect(target4.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("move triggers correct events", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR_EVENT }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: (x) => x <= 100, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + }; + + manager.register(target1); + manager.move(80, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(85, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(90, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(95, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(100, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(105, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(1); + + manager.move(100, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(2); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(1); + + manager.move(105, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(2); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(2); +}); diff --git a/src/objects/events/EventManager.ts b/src/objects/events/EventManager.ts new file mode 100644 index 00000000..a6160176 --- /dev/null +++ b/src/objects/events/EventManager.ts @@ -0,0 +1,164 @@ +import { EventManagerNode } from "./EventManagerNode"; +import { IEventManagerNode } from "./interfaces/IEventManagerNode"; +import { IEventTarget } from "./interfaces/IEventTarget"; +import RBush from "rbush"; +import { IEventManagerEvent } from "./interfaces/IEventManagerEvent"; +import { EventGroupIdentifier } from "./interfaces/IEventGroup"; +import { IEventManager } from "./interfaces/IEventManager"; + +export class EventManager { + private _nodes = new Map(); + private _bush = new RBush(); + private _currentOverElements: Set = new Set(); + + click(x: number, y: number) { + const elements = this._performHitTest(x, y); + new Propagation(elements.activeNodes, (target, event) => + target.triggerClick(event) + ); + } + + pointerDown(x: number, y: number) { + const elements = this._performHitTest(x, y); + + new Propagation(elements.activeNodes, (target, event) => + target.triggerPointerDown(event) + ); + } + + pointerUp(x: number, y: number) { + const elements = this._performHitTest(x, y); + + new Propagation(elements.activeNodes, (target, event) => + target.triggerPointerUp(event) + ); + } + + move(x: number, y: number) { + const elements = this._performHitTest(x, y); + const current = new Set(elements.activeNodes); + const previous = this._currentOverElements; + + const added = new Set(); + current.forEach((node) => { + if (!previous.has(node)) { + added.add(node); + } + }); + + const removed = new Set(); + previous.forEach((node) => { + if (!current.has(node)) { + removed.add(node); + } + }); + + this._currentOverElements = current; + + new Propagation(Array.from(added), (target, event) => + target.triggerPointerOver(event) + ); + new Propagation(Array.from(removed), (target, event) => + target.triggerPointerOut(event) + ); + } + + register(target: IEventTarget): IEventManagerNode { + if (this._nodes.has(target)) throw new Error("Target already registered"); + + const node = new EventManagerNode(target, this._bush); + this._nodes.set(target, node); + + return node; + } + + remove(target: IEventTarget) { + const current = this._nodes.get(target); + if (current == null) throw new Error("Target isn't in the event manager"); + + current.destroy(); + } + + private _performHitTest(x: number, y: number) { + const qualifyingElements = this._bush.search({ + minX: x, + minY: y, + maxX: x, + maxY: y, + }); + + const sortedElements = qualifyingElements + .sort((a, b) => b.target.getEventZOrder() - a.target.getEventZOrder()) + .filter((node) => node.target.hits(x, y)); + + const groups = new Map(); + const groupElements: EventManagerNode[] = []; + + sortedElements.forEach((node) => { + const group = node.target.getGroup(); + if (group != null && groups.has(group)) return; + + groupElements.push(node); + + if (group != null) { + groups.set(group, node); + } + }); + + return { + activeNodes: groupElements, + activeGroups: groups, + }; + } +} + +class Propagation { + private _skip = new Set(); + private _stopped = false; + + constructor( + private path: EventManagerNode[], + private _trigger: (target: IEventTarget, event: IEventManagerEvent) => void + ) { + this._propagate(); + } + + private _propagate() { + const event = this._createEvent(); + for (let i = 0; i < this.path.length; i++) { + if (this._stopped) return; + const node = this.path[i]; + + if (this._skip.has(node.target.getGroup().getEventGroupIdentifier())) + continue; + + this._trigger(this.path[i].target, event); + } + } + + private _createEvent(): IEventManagerEvent { + return { + stopPropagation: () => { + this._stopped = true; + }, + skip: (identifiers) => { + identifiers.forEach((identifier) => { + this._skip.add(identifier); + }); + }, + }; + } +} + +export const NOOP_EVENT_MANAGER: IEventManager = { + register: (): IEventManagerNode => { + return { + destroy: () => { + // Do nothing + }, + }; + }, + remove: () => { + // Do nothing + }, +}; diff --git a/src/objects/events/EventManagerContainer.ts b/src/objects/events/EventManagerContainer.ts new file mode 100644 index 00000000..1c3f5e29 --- /dev/null +++ b/src/objects/events/EventManagerContainer.ts @@ -0,0 +1,57 @@ +import * as PIXI from "pixi.js"; +import { EventManager } from "./EventManager"; + +export class EventManagerContainer extends PIXI.Container { + constructor( + private _application: PIXI.Application, + private _eventManager: EventManager + ) { + super(); + + this.interactive = true; + this._updateRectangle(); + + _application.ticker.add(this._updateRectangle); + + this.addListener("click", (event) => { + const position = event.data.getLocalPosition(this._application.stage); + + this._eventManager.click(position.x, position.y); + }); + + this.addListener("pointermove", (event) => { + const position = event.data.getLocalPosition(this._application.stage); + + this._eventManager.move(position.x, position.y); + }); + + this.addListener("pointerup", (event) => { + const position = event.data.getLocalPosition(this._application.stage); + + this._eventManager.pointerUp(position.x, position.y); + }); + + this.addListener("pointerdown", (event) => { + const position = event.data.getLocalPosition(this._application.stage); + + this._eventManager.pointerDown(position.x, position.y); + }); + } + + destroy() { + super.destroy(); + + this._application.ticker.remove(this._updateRectangle); + } + + private _updateRectangle = () => { + const renderer = this._application.renderer; + + this.hitArea = new PIXI.Rectangle( + 0, + 0, + renderer.width / renderer.resolution, + renderer.height / renderer.resolution + ); + }; +} diff --git a/src/objects/events/EventManagerNode.ts b/src/objects/events/EventManagerNode.ts new file mode 100644 index 00000000..5eab33c8 --- /dev/null +++ b/src/objects/events/EventManagerNode.ts @@ -0,0 +1,60 @@ +import RBush from "rbush"; +import { Subscription } from "rxjs"; +import { Rectangle } from "../room/IRoomRectangle"; +import { IEventManagerNode } from "./interfaces/IEventManagerNode"; +import { IEventTarget } from "./interfaces/IEventTarget"; + +export class EventManagerNode implements IEventManagerNode { + private _rectangle: Rectangle | undefined; + private _subscription: Subscription; + + public get minX() { + if (this._rectangle == null) throw new Error("Rectangle wasn't set"); + + return this._rectangle.x; + } + + public get maxX() { + if (this._rectangle == null) throw new Error("Rectangle wasn't set"); + + return this._rectangle.x + this._rectangle.width; + } + + public get minY() { + if (this._rectangle == null) throw new Error("Rectangle wasn't set"); + + return this._rectangle.y; + } + + public get maxY() { + if (this._rectangle == null) throw new Error("Rectangle wasn't set"); + + return this._rectangle.y + this._rectangle.height; + } + + constructor( + public readonly target: IEventTarget, + private _bush: RBush + ) { + this._subscription = target.getRectangleObservable().subscribe((value) => { + this._updateRectangle(value); + }); + } + + destroy(): void { + this._bush.remove(this); + this._subscription.unsubscribe(); + } + + private _updateRectangle(rectangle: Rectangle | undefined): void { + if (this._rectangle != null) { + this._bush.remove(this); + } + + this._rectangle = rectangle; + + if (rectangle != null) { + this._bush.insert(this); + } + } +} diff --git a/src/objects/events/interfaces/IEventGroup.ts b/src/objects/events/interfaces/IEventGroup.ts new file mode 100644 index 00000000..a9b99386 --- /dev/null +++ b/src/objects/events/interfaces/IEventGroup.ts @@ -0,0 +1,12 @@ +export interface IEventGroup { + getEventGroupIdentifier(): EventGroupIdentifier; +} + +export type EventGroupIdentifier = + | typeof FURNITURE_EVENT + | typeof AVATAR_EVENT + | typeof TILE_CURSOR_EVENT; + +export const FURNITURE_EVENT = Symbol("FURNITURE"); +export const AVATAR_EVENT = Symbol("AVATAR"); +export const TILE_CURSOR_EVENT = Symbol("TILE_CURSOR"); diff --git a/src/objects/events/interfaces/IEventHandler.ts b/src/objects/events/interfaces/IEventHandler.ts new file mode 100644 index 00000000..83d13fd4 --- /dev/null +++ b/src/objects/events/interfaces/IEventHandler.ts @@ -0,0 +1,9 @@ +import { IEventManagerEvent } from "./IEventManagerEvent"; + +export interface IEventHandler { + triggerClick(event: IEventManagerEvent): void; + triggerPointerDown(event: IEventManagerEvent): void; + triggerPointerUp(event: IEventManagerEvent): void; + triggerPointerOver(event: IEventManagerEvent): void; + triggerPointerOut(event: IEventManagerEvent): void; +} diff --git a/src/objects/events/interfaces/IEventHittable.ts b/src/objects/events/interfaces/IEventHittable.ts new file mode 100644 index 00000000..c339a3d9 --- /dev/null +++ b/src/objects/events/interfaces/IEventHittable.ts @@ -0,0 +1,10 @@ +import { Observable } from "rxjs"; +import { Rectangle } from "../../room/IRoomRectangle"; +import { IEventGroup } from "./IEventGroup"; + +export interface IEventHittable { + getGroup(): IEventGroup; + getRectangleObservable(): Observable; + getEventZOrder(): number; + hits(x: number, y: number): void; +} diff --git a/src/objects/events/interfaces/IEventManager.ts b/src/objects/events/interfaces/IEventManager.ts new file mode 100644 index 00000000..3d33d0e4 --- /dev/null +++ b/src/objects/events/interfaces/IEventManager.ts @@ -0,0 +1,7 @@ +import { IEventManagerNode } from "./IEventManagerNode"; +import { IEventTarget } from "./IEventTarget"; + +export interface IEventManager { + register(target: IEventTarget): IEventManagerNode; + remove(target: IEventTarget): void; +} diff --git a/src/objects/events/interfaces/IEventManagerEvent.ts b/src/objects/events/interfaces/IEventManagerEvent.ts new file mode 100644 index 00000000..193b080b --- /dev/null +++ b/src/objects/events/interfaces/IEventManagerEvent.ts @@ -0,0 +1,7 @@ +import { EventGroupIdentifier } from "./IEventGroup"; + +export interface IEventManagerEvent { + tag?: string; + stopPropagation(): void; + skip(identifiers: EventGroupIdentifier[]): void; +} diff --git a/src/objects/events/interfaces/IEventManagerNode.ts b/src/objects/events/interfaces/IEventManagerNode.ts new file mode 100644 index 00000000..7ab96682 --- /dev/null +++ b/src/objects/events/interfaces/IEventManagerNode.ts @@ -0,0 +1,5 @@ +import { Rectangle } from "../../room/IRoomRectangle"; + +export interface IEventManagerNode { + destroy(): void; +} diff --git a/src/objects/events/interfaces/IEventTarget.ts b/src/objects/events/interfaces/IEventTarget.ts new file mode 100644 index 00000000..c3256c65 --- /dev/null +++ b/src/objects/events/interfaces/IEventTarget.ts @@ -0,0 +1,4 @@ +import { IEventHandler } from "./IEventHandler"; +import { IEventHittable } from "./IEventHittable"; + +export interface IEventTarget extends IEventHittable, IEventHandler {} diff --git a/src/objects/furniture/BaseFurniture.tsx b/src/objects/furniture/BaseFurniture.tsx index c5a125a1..42587a13 100644 --- a/src/objects/furniture/BaseFurniture.tsx +++ b/src/objects/furniture/BaseFurniture.tsx @@ -21,6 +21,14 @@ import { IFurnitureVisualization } from "./IFurnitureVisualization"; import { FurnitureSprite } from "./FurnitureSprite"; import { AnimatedFurnitureVisualization } from "./visualization/AnimatedFurnitureVisualization"; import { getDirectionForFurniture } from "./util/getDirectionForFurniture"; +import { IEventManager } from "../events/interfaces/IEventManager"; +import { + EventGroupIdentifier, + FURNITURE_EVENT, + IEventGroup, +} from "../events/interfaces/IEventGroup"; +import { IEventManagerNode } from "../events/interfaces/IEventManagerNode"; +import { NOOP_EVENT_MANAGER } from "../events/EventManager"; const highlightFilter = new HighlightFilter(0x999999, 0xffffff); @@ -38,8 +46,8 @@ interface BaseFurnitureDependencies { visualization: IFurnitureRoomVisualization; animationTicker: IAnimationTicker; furnitureLoader: IFurnitureLoader; - hitDetection: IHitDetection; application: PIXI.Application; + eventManager: IEventManager; } export interface BaseFurnitureProps { @@ -52,7 +60,7 @@ export interface BaseFurnitureProps { type ResolveLoadFurniResult = (result: LoadFurniResult) => void; -export class BaseFurniture implements IFurnitureEventHandlers { +export class BaseFurniture implements IFurnitureEventHandlers, IEventGroup { private _sprites: Map = new Map(); private _loadFurniResult: LoadFurniResult | undefined; @@ -93,7 +101,7 @@ export class BaseFurniture implements IFurnitureEventHandlers { visualization: IFurnitureRoomVisualization; animationTicker: IAnimationTicker; furnitureLoader: IFurnitureLoader; - hitDetection: IHitDetection; + eventManager: IEventManager; }; constructor({ @@ -129,9 +137,9 @@ export class BaseFurniture implements IFurnitureEventHandlers { placeholder: context.configuration.placeholder, animationTicker: context.animationTicker, furnitureLoader: context.furnitureLoader, - hitDetection: context.hitDetection, visualization: context.visualization, application: context.application, + eventManager: context.eventManager, }, ...props, }); @@ -146,9 +154,9 @@ export class BaseFurniture implements IFurnitureEventHandlers { dependencies: { animationTicker: shroom.dependencies.animationTicker, furnitureLoader: shroom.dependencies.furnitureLoader, - hitDetection: shroom.dependencies.hitDetection, placeholder: shroom.dependencies.configuration.placeholder, application: shroom.dependencies.application, + eventManager: NOOP_EVENT_MANAGER, visualization: { container, addMask: () => { @@ -320,6 +328,10 @@ export class BaseFurniture implements IFurnitureEventHandlers { this._getMaskId = value; } + getEventGroupIdentifier(): EventGroupIdentifier { + return FURNITURE_EVENT; + } + destroy() { this._destroySprites(); @@ -406,7 +418,7 @@ export class BaseFurniture implements IFurnitureEventHandlers { if (this._unknownSprite == null) { this._unknownSprite = new FurnitureSprite({ - hitDetection: this.dependencies.hitDetection, + eventManager: this.dependencies.eventManager, group: this, }); @@ -597,7 +609,7 @@ export class BaseFurniture implements IFurnitureEventHandlers { part: FurniDrawPart ): FurnitureSprite { const sprite = new FurnitureSprite({ - hitDetection: this.dependencies.hitDetection, + eventManager: this.dependencies.eventManager, mirrored: asset.flipH, tag: layer?.tag, group: this, @@ -606,15 +618,15 @@ export class BaseFurniture implements IFurnitureEventHandlers { const ignoreMouse = layer?.ignoreMouse != null && layer.ignoreMouse; sprite.ignoreMouse = ignoreMouse; - sprite.addEventListener("click", (event) => { + sprite.events.addEventListener("click", (event) => { this._clickHandler.handleClick(event); }); - sprite.addEventListener("pointerup", (event) => { + sprite.events.addEventListener("pointerup", (event) => { this._clickHandler.handlePointerUp(event); }); - sprite.addEventListener("pointerdown", (event) => { + sprite.events.addEventListener("pointerdown", (event) => { this._clickHandler.handlePointerDown(event); }); diff --git a/src/objects/furniture/FloorFurniture.tsx b/src/objects/furniture/FloorFurniture.tsx index b924bfb0..f433e340 100644 --- a/src/objects/furniture/FloorFurniture.tsx +++ b/src/objects/furniture/FloorFurniture.tsx @@ -108,9 +108,9 @@ export class FloorFurniture this._baseFurniture.dependencies = { animationTicker: this.animationTicker, furnitureLoader: this.furnitureLoader, - hitDetection: this.hitDetection, placeholder: this.configuration.placeholder, visualization: this.roomVisualization, + eventManager: this.eventManager, }; this._moveAnimation = new ObjectAnimation( diff --git a/src/objects/furniture/WallFurniture.tsx b/src/objects/furniture/WallFurniture.tsx index 6987fd4f..aaad0384 100644 --- a/src/objects/furniture/WallFurniture.tsx +++ b/src/objects/furniture/WallFurniture.tsx @@ -188,9 +188,9 @@ export class WallFurniture extends RoomObject { this._baseFurniture.dependencies = { animationTicker: this.animationTicker, furnitureLoader: this.furnitureLoader, - hitDetection: this.hitDetection, placeholder: undefined, visualization: this.roomVisualization, + eventManager: this.eventManager, }; this._updatePosition(); diff --git a/src/objects/furniture/util/IFurnitureEventHandlers.ts b/src/objects/furniture/util/IFurnitureEventHandlers.ts index 03517544..a03603e7 100644 --- a/src/objects/furniture/util/IFurnitureEventHandlers.ts +++ b/src/objects/furniture/util/IFurnitureEventHandlers.ts @@ -1,8 +1,9 @@ import { HitEvent } from "../../../interfaces/IHitDetection"; +import { IEventManagerEvent } from "../../events/interfaces/IEventManagerEvent"; export interface IFurnitureEventHandlers { - onClick?: (event: HitEvent) => void; - onDoubleClick?: (event: HitEvent) => void; - onPointerDown?: (event: HitEvent) => void; - onPointerUp?: (event: HitEvent) => void; + onClick?: (event: IEventManagerEvent) => void; + onDoubleClick?: (event: IEventManagerEvent) => void; + onPointerDown?: (event: IEventManagerEvent) => void; + onPointerUp?: (event: IEventManagerEvent) => void; } diff --git a/src/objects/hitdetection/ClickHandler.ts b/src/objects/hitdetection/ClickHandler.ts index 40c247ef..0da96a8c 100644 --- a/src/objects/hitdetection/ClickHandler.ts +++ b/src/objects/hitdetection/ClickHandler.ts @@ -1,9 +1,10 @@ import { HitEvent } from "../../interfaces/IHitDetection"; +import { IEventManagerEvent } from "../events/interfaces/IEventManagerEvent"; import { HitEventHandler } from "./HitSprite"; export class ClickHandler { private _doubleClickInfo?: { - initialEvent: HitEvent; + initialEvent: IEventManagerEvent; timeout: number; }; @@ -46,7 +47,7 @@ export class ClickHandler { this._onPointerUp = value; } - handleClick(event: HitEvent) { + handleClick(event: IEventManagerEvent) { if (this._doubleClickInfo == null) { this.onClick && this.onClick(event); @@ -59,15 +60,15 @@ export class ClickHandler { } } - handlePointerDown(event: HitEvent) { + handlePointerDown(event: IEventManagerEvent) { this.onPointerDown && this.onPointerDown(event); } - handlePointerUp(event: HitEvent) { + handlePointerUp(event: IEventManagerEvent) { this.onPointerUp && this.onPointerUp(event); } - private _performDoubleClick(event: HitEvent) { + private _performDoubleClick(event: IEventManagerEvent) { if (this._doubleClickInfo == null) return; this.onDoubleClick && @@ -85,7 +86,7 @@ export class ClickHandler { this._doubleClickInfo = undefined; } - private _startDoubleClick(event: HitEvent) { + private _startDoubleClick(event: IEventManagerEvent) { this._doubleClickInfo = { initialEvent: event, timeout: window.setTimeout(() => this._resetDoubleClick(), 350), diff --git a/src/objects/hitdetection/HitDetection.test.ts b/src/objects/hitdetection/HitDetection.test.ts deleted file mode 100644 index c457d78b..00000000 --- a/src/objects/hitdetection/HitDetection.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { HitDetection } from "./HitDetection"; - -const view = { - getBoundingClientRect: () => ({ x: 0, y: 0 }), - addEventListener: () => { - // Do nothing - }, -}; - -test("detects hits on multiple elements", () => { - const hitDetection = new HitDetection({ - view, - } as any); - - let firstClicked = false; - let secondClicked = false; - - hitDetection.register({ - getHitDetectionZIndex: () => 10, - hits: () => true, - trigger: () => { - firstClicked = true; - }, - }); - - hitDetection.register({ - getHitDetectionZIndex: () => 5, - hits: () => true, - trigger: () => { - secondClicked = true; - }, - }); - - hitDetection.handleClick({ clientX: 2, clientY: 2 } as any); - - expect(firstClicked).toBe(true); - expect(secondClicked).toBe(true); -}); - -test("doesn't detect if element out of bounds", () => { - const hitDetection = new HitDetection({ - view, - } as any); - - let firstClicked = false; - let secondClicked = false; - - hitDetection.register({ - getHitDetectionZIndex: () => 10, - hits: () => true, - trigger: () => { - firstClicked = true; - }, - }); - - hitDetection.register({ - getHitDetectionZIndex: () => 5, - hits: () => false, - trigger: () => { - secondClicked = true; - }, - }); - - hitDetection.handleClick({ clientX: 101, clientY: 101 } as any); - - expect(firstClicked).toBe(true); - expect(secondClicked).toBe(false); -}); - -test("doesn't detect if element in bounds but doesn't hit", () => { - const hitDetection = new HitDetection({ - view, - } as any); - - let firstClicked = false; - let secondClicked = false; - - hitDetection.register({ - getHitDetectionZIndex: () => 10, - hits: () => true, - trigger: () => { - firstClicked = true; - }, - }); - - hitDetection.register({ - getHitDetectionZIndex: () => 5, - hits: () => false, - trigger: () => { - secondClicked = true; - }, - }); - - hitDetection.handleClick({ clientX: 2, clientY: 2 } as any); - - expect(firstClicked).toBe(true); - expect(secondClicked).toBe(false); -}); - -test("intercepts event if element stops propagating", () => { - const hitDetection = new HitDetection({ - view, - } as any); - - let firstClicked = false; - let secondClicked = false; - - hitDetection.register({ - getHitDetectionZIndex: () => 10, - hits: () => true, - trigger: (type, event) => { - firstClicked = true; - event.stopPropagation(); - }, - }); - - hitDetection.register({ - getHitDetectionZIndex: () => 5, - hits: () => true, - trigger: () => { - secondClicked = true; - }, - }); - - hitDetection.handleClick({ clientX: 2, clientY: 2 } as any); - - expect(firstClicked).toBe(true); - expect(secondClicked).toBe(false); -}); diff --git a/src/objects/hitdetection/HitDetection.ts b/src/objects/hitdetection/HitDetection.ts deleted file mode 100644 index 6217a822..00000000 --- a/src/objects/hitdetection/HitDetection.ts +++ /dev/null @@ -1,219 +0,0 @@ -import * as PIXI from "pixi.js"; -import { - HitDetectionElement, - HitDetectionNode, - HitEvent, - HitEventType, - IHitDetection, -} from "../../interfaces/IHitDetection"; -import QuadTree from "quadtree-lib"; -import { Rectangle } from "../room/IRoomRectangle"; - -export class HitDetection extends PIXI.Container implements IHitDetection { - private _counter = 0; - private _map: Map = new Map(); - private _container: PIXI.Container | undefined; - private _quadTree: QuadTree | undefined; - private _hitAreaRect: Rectangle | undefined; - - constructor(private _app: PIXI.Application) { - super(); - _app.view.addEventListener("click", (event) => this.handleClick(event), { - capture: true, - }); - - _app.view.addEventListener("pointerdown", (event) => - this.handlePointerDown(event) - ); - - _app.view.addEventListener("pointerup", (event) => - this.handlePointerUp(event) - ); - - _app.view.addEventListener( - "contextmenu", - (event) => this.handleClick(event), - { - capture: true, - } - ); - } - - static create(application: PIXI.Application) { - return new HitDetection(application); - } - - updateHitArea(rectangle: Rectangle) { - this._hitAreaRect = rectangle; - this._updateQuadTree(); - } - - register(rectangle: HitDetectionElement): HitDetectionNode { - const id = this._counter++; - this._map.set(id, rectangle); - - if (this._quadTree != null) { - this._quadTree.push(rectangle.getQuadTreeItem()); - } - - return { - remove: () => { - this._map.delete(id); - - if (this._quadTree != null) { - this._quadTree.remove(rectangle.getQuadTreeItem()); - } - }, - }; - } - - handleClick(event: MouseEvent) { - this._triggerEvent(event.clientX, event.clientY, "click", event); - } - - handlePointerDown(event: PointerEvent) { - this._triggerEvent(event.clientX, event.clientY, "pointerdown", event); - } - - handlePointerUp(event: PointerEvent) { - this._triggerEvent(event.clientX, event.clientY, "pointerup", event); - } - - private _updateQuadTree() { - if (this._hitAreaRect == null) - throw new Error("Invalid hit area rectangle"); - - const tree = new QuadTree(this._hitAreaRect); - - this._quadTree = tree; - this._map.forEach((element) => { - tree.push(element.getQuadTreeItem()); - }); - } - - private _triggerEvent( - x: number, - y: number, - eventType: HitEventType, - domEvent: MouseEvent - ) { - const start = performance.now(); - - const elements = this._performHitTest(x, y); - - const event = new HitEventPropagation(eventType, domEvent, elements); - event.resumePropagation(); - } - - private _performHitTest(x: number, y: number) { - const rect = this._app.view.getBoundingClientRect(); - - x = x - rect.x; - y = y - rect.y; - - const entries = Array.from(this._map.values()); - const ordered = entries.sort( - (a, b) => b.getHitDetectionZIndex() - a.getHitDetectionZIndex() - ); - - return ordered.filter((element) => { - return element.hits(x, y); - }); - } - - private _debugHitDetection() { - this._container?.destroy(); - const container = new PIXI.Container(); - - this._container = container; - - this._map.forEach((element) => { - if (element.createDebugSprite == null) return; - - const sprite = element.createDebugSprite(); - - if (sprite == null) return; - - container.addChild(sprite); - }); - - this._app.stage.addChild(container); - } -} - -class HitEventPropagation { - private _currentIndex = 0; - private _stopped = false; - private _groups: Set = new Set(); - - constructor( - private _eventType: HitEventType, - private _mouseEvent: MouseEvent, - private _path: HitDetectionElement[] - ) {} - - public get mouseEvent() { - return this._mouseEvent; - } - - stopPropagation(): void { - this._stopped = true; - } - - resumePropagation(): void { - this._stopped = false; - this._propagateEvent(); - } - - private _propagateEvent() { - for (let i = this._currentIndex; i < this._path.length; i++) { - this._currentIndex = i + 1; - - if (this._stopped) break; - - const element = this._path[i]; - - const group = element.group; - if (group == null || !this._groups.has(group)) { - element.trigger(this._eventType, new TargetedHitEvent(this, element)); - - if (group != null) { - this._groups.add(group); - } - } - } - } -} - -class TargetedHitEvent implements HitEvent { - private _tag: string | undefined; - - constructor( - private _base: HitEventPropagation, - private _target: HitDetectionElement - ) {} - - public get target() { - return this._target; - } - - public get mouseEvent() { - return this._base.mouseEvent; - } - - public get tag() { - return this._tag; - } - - public set tag(value) { - this._tag = value; - } - - stopPropagation(): void { - return this._base.stopPropagation(); - } - - resumePropagation(): void { - return this._base.resumePropagation(); - } -} diff --git a/src/objects/hitdetection/HitSprite.ts b/src/objects/hitdetection/HitSprite.ts index bc402b0d..900f9c2f 100644 --- a/src/objects/hitdetection/HitSprite.ts +++ b/src/objects/hitdetection/HitSprite.ts @@ -1,27 +1,31 @@ import * as PIXI from "pixi.js"; -import { - HitDetectionElement, - HitDetectionNode, - HitEvent, - HitEventType, - IHitDetection, - Rect, -} from "../../interfaces/IHitDetection"; +import { BehaviorSubject, Observable } from "rxjs"; +import { HitEvent, HitEventType, Rect } from "../../interfaces/IHitDetection"; +import { EventEmitter } from "../events/EventEmitter"; +import { IEventGroup } from "../events/interfaces/IEventGroup"; +import { IEventManager } from "../events/interfaces/IEventManager"; +import { IEventManagerEvent } from "../events/interfaces/IEventManagerEvent"; +import { IEventTarget } from "../events/interfaces/IEventTarget"; import { Hitmap } from "../furniture/util/loadFurni"; +import { Rectangle } from "../room/IRoomRectangle"; import { HitTexture } from "./HitTexture"; -export type HitEventHandler = (event: HitEvent) => void; +export type HitEventHandler = (event: IEventManagerEvent) => void; + +export class HitSprite extends PIXI.Sprite implements IEventTarget { + private _group: IEventGroup; -export class HitSprite extends PIXI.Sprite implements HitDetectionElement { - private _group: unknown; - private _hitDetectionNode: HitDetectionNode | undefined; - private _handlers = new Map>(); private _hitTexture: HitTexture | undefined; private _tag: string | undefined; private _mirrored: boolean; - private _mirrorNotVisually: boolean; private _ignore = false; private _ignoreMouse = false; + private _eventManager: IEventManager; + private _rectangleSubject = new BehaviorSubject( + undefined + ); + + private _eventEmitter = new EventEmitter(); private _getHitmap: | (() => ( @@ -31,33 +35,71 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { ) => boolean) | undefined; + public get events() { + return this._eventEmitter; + } + constructor({ - hitDetection, + eventManager, mirrored = false, - mirroredNotVisually = false, getHitmap, tag, group, }: { - hitDetection: IHitDetection; + eventManager: IEventManager; getHitmap?: () => Hitmap; mirrored?: boolean; - mirroredNotVisually?: boolean; tag?: string; - group?: unknown; + group: IEventGroup; }) { super(); - if (group != null) { - this._group = group; - } + this._group = group; this._mirrored = mirrored; - this._mirrorNotVisually = mirroredNotVisually; this._getHitmap = getHitmap; this._tag = tag; - this._hitDetectionNode = hitDetection.register(this); this.mirrored = this._mirrored; + this._eventManager = eventManager; + + eventManager.register(this); + } + + getGroup(): IEventGroup { + return this._group; + } + + getRectangleObservable(): Observable { + return this._rectangleSubject; + } + + getEventZOrder(): number { + return this.zIndex; + } + + triggerClick(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("click", event); + } + + triggerPointerDown(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointerdown", event); + } + + triggerPointerUp(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointerup", event); + } + + triggerPointerOver(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointerover", event); + } + + triggerPointerOut(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointerout", event); } createDebugSprite(): PIXI.Sprite | undefined { @@ -123,7 +165,7 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { transform: { x: number; y: number } ) => value.hits(x, y, transform, { - mirrorHorizonally: this._mirrored || this._mirrorNotVisually, + mirrorHorizonally: this._mirrored, }); } } @@ -132,41 +174,19 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { return this.zIndex; } - trigger(type: HitEventType, event: HitEvent): void { - const handlers = this._handlers.get(type); - - event.tag = this._tag; - - handlers?.forEach((handler) => handler(event)); - } - - removeAllEventListeners() { - this._handlers = new Map(); - } - - addEventListener(type: HitEventType, handler: HitEventHandler) { - const existingHandlers = this._handlers.get(type) ?? new Set(); - existingHandlers.add(handler); - - this._handlers.set(type, existingHandlers); - } - - removeEventListener(type: HitEventType, handler: HitEventHandler) { - const existingHandlers = this._handlers.get(type); - existingHandlers?.delete(handler); - } - destroy() { super.destroy(); - this._hitDetectionNode?.remove(); + this._eventManager.remove(this); } getHitBox(): Rect { - if (this._mirrored || this._mirrorNotVisually) { + const pos = this.getGlobalPosition(); + + if (this._mirrored) { return { - x: this.worldTransform.tx - this.texture.width, - y: this.worldTransform.ty, + x: pos.x - this.texture.width, + y: pos.y, width: this.texture.width, height: this.texture.height, zIndex: this.zIndex, @@ -174,8 +194,8 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { } return { - x: this.worldTransform.tx, - y: this.worldTransform.ty, + x: pos.x, + y: pos.y, width: this.texture.width, height: this.texture.height, zIndex: this.zIndex, @@ -195,11 +215,25 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { if (inBoundsX && inBoundsY) { const hits = this._getHitmap(); return hits(x, y, { - x: this.worldTransform.tx, - y: this.worldTransform.ty, + x: this.getGlobalPosition().x, + y: this.getGlobalPosition().y, }); } return false; } + + updateTransform() { + super.updateTransform(); + + this._rectangleSubject.next(this.getHitBox()); + } } + +type HitSpriteEventMap = { + click: IEventManagerEvent; + pointerup: IEventManagerEvent; + pointerdown: IEventManagerEvent; + pointerover: IEventManagerEvent; + pointerout: IEventManagerEvent; +}; diff --git a/src/objects/room/Room.ts b/src/objects/room/Room.ts index af72ecf3..966e4a9f 100644 --- a/src/objects/room/Room.ts +++ b/src/objects/room/Room.ts @@ -4,7 +4,6 @@ import { IAvatarLoader } from "../../interfaces/IAvatarLoader"; import { IConfiguration } from "../../interfaces/IConfiguration"; import { IFurnitureData } from "../../interfaces/IFurnitureData"; import { IFurnitureLoader } from "../../interfaces/IFurnitureLoader"; -import { IHitDetection } from "../../interfaces/IHitDetection"; import { IRoomGeometry } from "../../interfaces/IRoomGeometry"; import { IRoomObject } from "../../interfaces/IRoomObject"; import { IRoomObjectContainer } from "../../interfaces/IRoomObjectContainer"; @@ -15,16 +14,15 @@ import { parseTileMapString } from "../../util/parseTileMapString"; import { Shroom } from "../Shroom"; import { ITileMap } from "../../interfaces/ITileMap"; import { RoomObjectContainer } from "./RoomObjectContainer"; -import { Subject } from "rxjs"; import { RoomModelVisualization } from "./RoomModelVisualization"; import { ParsedTileMap } from "./ParsedTileMap"; import { getTileColors, getWallColors } from "./util/getTileColors"; +import { EventManager } from "../events/EventManager"; export interface Dependencies { animationTicker: IAnimationTicker; avatarLoader: IAvatarLoader; furnitureLoader: IFurnitureLoader; - hitDetection: IHitDetection; configuration: IConfiguration; furnitureData?: IFurnitureData; application: PIXI.Application; @@ -60,7 +58,7 @@ export class Room private _animationTicker: IAnimationTicker; private _avatarLoader: IAvatarLoader; private _furnitureLoader: IFurnitureLoader; - private _hitDetection: IHitDetection; + private _eventManager: EventManager; private _configuration: IConfiguration; private _wallTexture: Promise | PIXI.Texture | undefined; @@ -88,7 +86,6 @@ export class Room avatarLoader, furnitureLoader, tilemap, - hitDetection, configuration, application, }: { @@ -103,12 +100,12 @@ export class Room this._animationTicker = animationTicker; this._furnitureLoader = furnitureLoader; this._avatarLoader = avatarLoader; - this._hitDetection = hitDetection; + this._eventManager = new EventManager(); this._configuration = configuration; this.application = application; this._visualization = new RoomModelVisualization( - this._hitDetection, + this._eventManager, this.application, new ParsedTileMap(normalizedTileMap) ); @@ -121,7 +118,7 @@ export class Room furnitureLoader: this._furnitureLoader, roomObjectContainer: this, avatarLoader: this._avatarLoader, - hitDetection: this._hitDetection, + eventManager: this._eventManager, configuration: this._configuration, tilemap: this, landscapeContainer: this._visualization, diff --git a/src/objects/room/RoomModelVisualization.ts b/src/objects/room/RoomModelVisualization.ts index 83bb2264..e048e21b 100644 --- a/src/objects/room/RoomModelVisualization.ts +++ b/src/objects/room/RoomModelVisualization.ts @@ -1,6 +1,5 @@ import * as PIXI from "pixi.js"; import { Subject } from "rxjs"; -import { IHitDetection } from "../../interfaces/IHitDetection"; import { IRoomVisualization, MaskNode, @@ -9,6 +8,8 @@ import { import { RoomPosition } from "../../types/RoomPosition"; import { getZOrder } from "../../util/getZOrder"; import { ParsedTileType, ParsedTileWall } from "../../util/parseTileMap"; +import { EventManager } from "../events/EventManager"; +import { EventManagerContainer } from "../events/EventManagerContainer"; import { ILandscapeContainer } from "./ILandscapeContainer"; import { IRoomRectangle, Rectangle } from "./IRoomRectangle"; import { ParsedTileMap } from "./ParsedTileMap"; @@ -87,7 +88,7 @@ export class RoomModelVisualization private _rebuildRoom = false; constructor( - private _hitDetection: IHitDetection, + private _eventManager: EventManager, private _application: PIXI.Application, public readonly parsedTileMap: ParsedTileMap ) { @@ -112,6 +113,9 @@ export class RoomModelVisualization this._tileLayer.sortableChildren = true; this.addChild(this._positionalContainer); + this._positionalContainer.addChild( + new EventManagerContainer(this._application, this._eventManager) + ); this._updateHeightmap(); @@ -530,7 +534,7 @@ export class RoomModelVisualization const position: RoomPosition = { roomX: x, roomY: y, roomZ: z }; const cursor = new TileCursor( - this._hitDetection, + this._eventManager, position, () => { this._onTileClick.next(position); diff --git a/src/objects/room/parts/TileCursor.ts b/src/objects/room/parts/TileCursor.ts index cc02a4e5..8f8f6eb5 100644 --- a/src/objects/room/parts/TileCursor.ts +++ b/src/objects/room/parts/TileCursor.ts @@ -1,22 +1,28 @@ import * as PIXI from "pixi.js"; -import { - HitDetectionElement, - HitDetectionNode, - HitEvent, - IHitDetection, -} from "../../../interfaces/IHitDetection"; +import { BehaviorSubject, Observable } from "rxjs"; import { RoomPosition } from "../../../types/RoomPosition"; - -export class TileCursor extends PIXI.Container implements HitDetectionElement { +import { + EventGroupIdentifier, + IEventGroup, + TILE_CURSOR_EVENT, +} from "../../events/interfaces/IEventGroup"; +import { IEventManager } from "../../events/interfaces/IEventManager"; +import { IEventManagerEvent } from "../../events/interfaces/IEventManagerEvent"; +import { IEventTarget } from "../../events/interfaces/IEventTarget"; +import { Rectangle } from "../IRoomRectangle"; + +export class TileCursor + extends PIXI.Container + implements IEventTarget, IEventGroup { private _roomX: number; private _roomY: number; private _roomZ: number; private _graphics: PIXI.Graphics; private _hover = false; - private _node: HitDetectionNode; + private _subject = new BehaviorSubject(undefined); constructor( - hitDetection: IHitDetection, + private _eventManager: IEventManager, private _position: RoomPosition, private onClick: (position: RoomPosition) => void, private onOver: (position: RoomPosition) => void, @@ -31,31 +37,50 @@ export class TileCursor extends PIXI.Container implements HitDetectionElement { this.addChild(this._graphics); - this._node = hitDetection.register(this); + this._eventManager.register(this); } - createDebugSprite() { - return undefined; + getEventGroupIdentifier(): EventGroupIdentifier { + return TILE_CURSOR_EVENT; } - trigger(type: "click", event: HitEvent): void { - switch (type) { - case "click": - this.onClick({ - roomX: this._roomX, - roomY: this._roomY, - roomZ: this._roomZ, - }); - break; - } + getGroup(): IEventGroup { + return this; + } + + getRectangleObservable(): Observable { + return this._subject; + } + + getEventZOrder(): number { + return -1000; + } + + triggerClick(event: IEventManagerEvent): void {} + + triggerPointerDown(event: IEventManagerEvent): void {} + + triggerPointerUp(event: IEventManagerEvent): void {} + + triggerPointerOver(event: IEventManagerEvent): void { + this._updateHover(true); + this.onOver({ roomX: this._roomX, roomY: this._roomY, roomZ: this._roomZ }); + } + + triggerPointerOut(event: IEventManagerEvent): void { + this._updateHover(false); + this.onOut({ roomX: this._roomX, roomY: this._roomY, roomZ: this._roomZ }); + } + + createDebugSprite() { + return undefined; } hits(x: number, y: number): boolean { - const tx = this.worldTransform.tx; - const ty = this.worldTransform.ty; + const pos = this.getGlobalPosition(); - const diffX = x - tx; - const diffY = y - ty; + const diffX = x - pos.x; + const diffY = y - pos.y; return this._pointInside( [diffX, diffY], @@ -74,23 +99,31 @@ export class TileCursor extends PIXI.Container implements HitDetectionElement { destroy() { super.destroy(); - this._node.remove(); + this._graphics.destroy(); + this._eventManager.remove(this); + } + + updateTransform() { + super.updateTransform(); + + this._subject.next(this._getCurrentRectangle()); + } + + private _getCurrentRectangle(): Rectangle { + const position = this.getGlobalPosition(); + + return { + x: position.x, + y: position.y, + width: 64, + height: 32, + }; } private _createGraphics() { const graphics = new PIXI.Graphics(); - graphics.hitArea = new PIXI.Polygon([ - new PIXI.Point(points.p1.x, points.p1.y), - new PIXI.Point(points.p2.x, points.p2.y), - new PIXI.Point(points.p3.x, points.p3.y), - new PIXI.Point(points.p4.x, points.p4.y), - ]); - graphics.interactive = true; - graphics.addListener("mouseover", () => this._updateHover(true)); - graphics.addListener("mouseout", () => this._updateHover(false)); - return graphics; } From fa16a97657db6e462f175f2bba1bdc57febdbd65 Mon Sep 17 00:00:00 2001 From: jankuss Date: Wed, 17 Feb 2021 13:09:23 -0800 Subject: [PATCH 4/8] Improve event handling --- src/objects/avatar/BaseAvatar.ts | 4 +- src/objects/events/EventManager.test.ts | 126 ++++++++++++--- src/objects/events/EventManager.ts | 102 +++++++++++-- src/objects/events/EventManagerContainer.ts | 74 +++++---- src/objects/events/EventManagerNode.ts | 4 +- src/objects/events/EventOverOutHandler.ts | 143 ++++++++++++++++++ src/objects/events/interfaces/IEventGroup.ts | 14 +- .../events/interfaces/IEventHandler.ts | 1 + .../events/interfaces/IEventManagerEvent.ts | 7 +- src/objects/furniture/BaseFurniture.tsx | 27 +++- src/objects/furniture/FloorFurniture.tsx | 20 +++ .../furniture/FurnitureVisualizationView.ts | 12 +- .../furniture/util/IFurnitureEventHandlers.ts | 2 + src/objects/hitdetection/HitSprite.ts | 8 +- src/objects/room/RoomModelVisualization.ts | 5 +- src/objects/room/parts/TileCursor.ts | 35 ++--- src/objects/room/parts/WallLeft.ts | 40 ++--- src/util/isPointInside.ts | 21 +++ storybook/stories/Documentation.stories.ts | 8 + .../stories/furniture/Furniture.stories.ts | 25 +++ 20 files changed, 561 insertions(+), 117 deletions(-) create mode 100644 src/objects/events/EventOverOutHandler.ts create mode 100644 src/util/isPointInside.ts diff --git a/src/objects/avatar/BaseAvatar.ts b/src/objects/avatar/BaseAvatar.ts index 542ef438..9a652e01 100644 --- a/src/objects/avatar/BaseAvatar.ts +++ b/src/objects/avatar/BaseAvatar.ts @@ -19,7 +19,7 @@ import { AvatarDrawDefinition } from "./structure/AvatarDrawDefinition"; import { IEventManager } from "../events/interfaces/IEventManager"; import { NOOP_EVENT_MANAGER } from "../events/EventManager"; import { - AVATAR_EVENT, + AVATAR, EventGroupIdentifier, IEventGroup, } from "../events/interfaces/IEventGroup"; @@ -197,7 +197,7 @@ export class BaseAvatar extends PIXI.Container implements IEventGroup { } getEventGroupIdentifier(): EventGroupIdentifier { - return AVATAR_EVENT; + return AVATAR; } destroy(): void { diff --git a/src/objects/events/EventManager.test.ts b/src/objects/events/EventManager.test.ts index 23861455..953f2bd2 100644 --- a/src/objects/events/EventManager.test.ts +++ b/src/objects/events/EventManager.test.ts @@ -2,9 +2,10 @@ import { BehaviorSubject } from "rxjs"; import { Rectangle } from "../room/IRoomRectangle"; import { EventManager } from "./EventManager"; import { - AVATAR_EVENT, - FURNITURE_EVENT, + AVATAR, + FURNITURE, IEventGroup, + TILE_CURSOR, } from "./interfaces/IEventGroup"; import { IEventTarget } from "./interfaces/IEventTarget"; @@ -13,7 +14,7 @@ test("handles click when mounted", () => { const target: IEventTarget = { getEventZOrder: () => 10, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), hits: () => true, @@ -22,6 +23,7 @@ test("handles click when mounted", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const node = manager.register(target); @@ -38,7 +40,7 @@ test("handles click on hit elements", () => { const target: IEventTarget = { getEventZOrder: () => 10, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), hits: () => true, @@ -47,6 +49,7 @@ test("handles click on hit elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const node = manager.register(target); @@ -59,7 +62,7 @@ test("doesn't handle click on not hit elements", () => { const target: IEventTarget = { getEventZOrder: () => 10, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), hits: () => false, @@ -68,6 +71,7 @@ test("doesn't handle click on not hit elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const node = manager.register(target); @@ -80,7 +84,7 @@ test("handles click on multiple elements", () => { const target1: IEventTarget = { getEventZOrder: () => 2, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), hits: () => true, @@ -89,11 +93,12 @@ test("handles click on multiple elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const target2: IEventTarget = { getEventZOrder: () => 5, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), hits: () => true, @@ -102,11 +107,12 @@ test("handles click on multiple elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const target3: IEventTarget = { getEventZOrder: () => 10, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 75, y: 75, width: 100, height: 100 }), hits: () => true, @@ -115,11 +121,12 @@ test("handles click on multiple elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const target4: IEventTarget = { getEventZOrder: () => 10, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 79, y: 79, width: 100, height: 100 }), hits: () => false, @@ -128,6 +135,7 @@ test("handles click on multiple elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; manager.register(target1); @@ -147,7 +155,7 @@ test("handles click on multiple elements", () => { const target1: IEventTarget = { getEventZOrder: () => 2, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), hits: () => true, @@ -156,11 +164,12 @@ test("handles click on multiple elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const target2: IEventTarget = { getEventZOrder: () => 5, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), hits: () => true, @@ -169,6 +178,7 @@ test("handles click on multiple elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; manager.register(target1); @@ -182,7 +192,7 @@ test("handles click on multiple elements", () => { test("only handles first element when elements from same group", () => { const manager = new EventManager(); - const group: IEventGroup = { getEventGroupIdentifier: () => FURNITURE_EVENT }; + const group: IEventGroup = { getEventGroupIdentifier: () => FURNITURE }; const target1: IEventTarget = { getEventZOrder: () => 2, @@ -195,6 +205,7 @@ test("only handles first element when elements from same group", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const target2: IEventTarget = { @@ -208,6 +219,7 @@ test("only handles first element when elements from same group", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; manager.register(target1); @@ -223,7 +235,7 @@ test("event.skip() skips elements", () => { const target1: IEventTarget = { getEventZOrder: () => 2, - getGroup: () => ({ getEventGroupIdentifier: () => AVATAR_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR }), getRectangleObservable: () => new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), hits: () => true, @@ -232,24 +244,26 @@ test("event.skip() skips elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const target2: IEventTarget = { getEventZOrder: () => 5, - getGroup: () => ({ getEventGroupIdentifier: () => AVATAR_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR }), getRectangleObservable: () => new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), hits: () => true, - triggerClick: jest.fn((event) => event.skip([AVATAR_EVENT])), + triggerClick: jest.fn((event) => event.skip([AVATAR])), triggerPointerDown: jest.fn(), triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const target3: IEventTarget = { getEventZOrder: () => 9, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 75, y: 75, width: 100, height: 100 }), hits: () => true, @@ -258,19 +272,21 @@ test("event.skip() skips elements", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; const target4: IEventTarget = { getEventZOrder: () => 10, - getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), getRectangleObservable: () => new BehaviorSubject({ x: 79, y: 79, width: 100, height: 100 }), hits: () => true, - triggerClick: jest.fn((event) => event.skip([FURNITURE_EVENT])), + triggerClick: jest.fn((event) => event.skip([FURNITURE])), triggerPointerDown: jest.fn(), triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; manager.register(target1); @@ -290,7 +306,7 @@ test("move triggers correct events", () => { const target1: IEventTarget = { getEventZOrder: () => 2, - getGroup: () => ({ getEventGroupIdentifier: () => AVATAR_EVENT }), + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR }), getRectangleObservable: () => new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), hits: (x) => x <= 100, @@ -299,6 +315,7 @@ test("move triggers correct events", () => { triggerPointerOut: jest.fn(), triggerPointerOver: jest.fn(), triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), }; manager.register(target1); @@ -334,3 +351,74 @@ test("move triggers correct events", () => { expect(target1.triggerPointerOver).toHaveBeenCalledTimes(2); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(2); }); + +test("event.skipExcept() skips elements except the specified", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => TILE_CURSOR }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR }), + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn((event) => event.skipExcept(TILE_CURSOR)), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target3: IEventTarget = { + getEventZOrder: () => 9, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 75, y: 75, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target4: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 79, y: 79, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn((event) => event.skipExcept(AVATAR, TILE_CURSOR)), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + manager.register(target3); + manager.register(target4); + + manager.click(80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(1); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); + expect(target3.triggerClick).toHaveBeenCalledTimes(0); + expect(target4.triggerClick).toHaveBeenCalledTimes(1); +}); diff --git a/src/objects/events/EventManager.ts b/src/objects/events/EventManager.ts index a6160176..6925f14c 100644 --- a/src/objects/events/EventManager.ts +++ b/src/objects/events/EventManager.ts @@ -2,14 +2,22 @@ import { EventManagerNode } from "./EventManagerNode"; import { IEventManagerNode } from "./interfaces/IEventManagerNode"; import { IEventTarget } from "./interfaces/IEventTarget"; import RBush from "rbush"; -import { IEventManagerEvent } from "./interfaces/IEventManagerEvent"; -import { EventGroupIdentifier } from "./interfaces/IEventGroup"; +import { + EventGroupIdentifierParam, + IEventManagerEvent, +} from "./interfaces/IEventManagerEvent"; +import { + EventGroupIdentifier, + IEventGroup, + TILE_CURSOR, +} from "./interfaces/IEventGroup"; import { IEventManager } from "./interfaces/IEventManager"; export class EventManager { private _nodes = new Map(); private _bush = new RBush(); private _currentOverElements: Set = new Set(); + private _pointerDownElements: Set = new Set(); click(x: number, y: number) { const elements = this._performHitTest(x, y); @@ -21,6 +29,8 @@ export class EventManager { pointerDown(x: number, y: number) { const elements = this._performHitTest(x, y); + this._pointerDownElements = new Set(elements.activeNodes); + new Propagation(elements.activeNodes, (target, event) => target.triggerPointerDown(event) ); @@ -29,14 +39,34 @@ export class EventManager { pointerUp(x: number, y: number) { const elements = this._performHitTest(x, y); + const elementsSet = new Set(elements.activeNodes); + const clickedNodes = new Set(); + this._pointerDownElements.forEach((node) => { + if (elementsSet.has(node)) { + clickedNodes.add(node); + } + }); + new Propagation(elements.activeNodes, (target, event) => target.triggerPointerUp(event) ); + + new Propagation(Array.from(clickedNodes), (target, event) => { + target.triggerClick(event); + }); } move(x: number, y: number) { const elements = this._performHitTest(x, y); - const current = new Set(elements.activeNodes); + const current = new Set( + elements.activeNodes.filter( + (node, index) => + // Only interested in the top most element + index === 0 || + // or the tile cursor + node.target.getGroup().getEventGroupIdentifier() === TILE_CURSOR + ) + ); const previous = this._currentOverElements; const added = new Set(); @@ -53,14 +83,35 @@ export class EventManager { } }); + const addedGroups = new Set(); + added.forEach((node) => { + addedGroups.add(node.target.getGroup()); + }); + + const removedButGroupPresent = new Set(); + const actualRemoved = new Set(); + + removed.forEach((node) => { + if (addedGroups.has(node.target.getGroup())) { + removedButGroupPresent.add(node); + } + + actualRemoved.add(node); + }); + this._currentOverElements = current; + new Propagation(Array.from(removedButGroupPresent), (target, event) => { + target.triggerPointerTargetChanged(event); + }); + + new Propagation(Array.from(actualRemoved), (target, event) => + target.triggerPointerOut(event) + ); + new Propagation(Array.from(added), (target, event) => target.triggerPointerOver(event) ); - new Propagation(Array.from(removed), (target, event) => - target.triggerPointerOut(event) - ); } register(target: IEventTarget): IEventManagerNode { @@ -114,6 +165,7 @@ export class EventManager { class Propagation { private _skip = new Set(); + private _allow = new Set(); private _stopped = false; constructor( @@ -129,8 +181,19 @@ class Propagation { if (this._stopped) return; const node = this.path[i]; - if (this._skip.has(node.target.getGroup().getEventGroupIdentifier())) + if ( + this._skip.has(node.target.getGroup().getEventGroupIdentifier()) && + !this._allow.has(node.target.getGroup().getEventGroupIdentifier()) + ) { + continue; + } + + if ( + this._allow.size > 0 && + !this._allow.has(node.target.getGroup().getEventGroupIdentifier()) + ) { continue; + } this._trigger(this.path[i].target, event); } @@ -141,10 +204,27 @@ class Propagation { stopPropagation: () => { this._stopped = true; }, - skip: (identifiers) => { - identifiers.forEach((identifier) => { - this._skip.add(identifier); - }); + skip: (...identifiers) => { + const add = (identifier: EventGroupIdentifierParam) => { + if (Array.isArray(identifier)) { + identifier.forEach((value) => add(value)); + } else { + this._skip.add(identifier); + } + }; + + add(identifiers); + }, + skipExcept: (...identifiers) => { + const add = (identifier: EventGroupIdentifierParam) => { + if (Array.isArray(identifier)) { + identifier.forEach((value) => add(value)); + } else { + this._allow.add(identifier); + } + }; + + add(identifiers); }, }; } diff --git a/src/objects/events/EventManagerContainer.ts b/src/objects/events/EventManagerContainer.ts index 1c3f5e29..29c4a13e 100644 --- a/src/objects/events/EventManagerContainer.ts +++ b/src/objects/events/EventManagerContainer.ts @@ -1,57 +1,75 @@ import * as PIXI from "pixi.js"; import { EventManager } from "./EventManager"; -export class EventManagerContainer extends PIXI.Container { +export class EventManagerContainer { + private _box: PIXI.TilingSprite | undefined; + constructor( private _application: PIXI.Application, private _eventManager: EventManager ) { - super(); - - this.interactive = true; this._updateRectangle(); _application.ticker.add(this._updateRectangle); - this.addListener("click", (event) => { - const position = event.data.getLocalPosition(this._application.stage); + const interactionManager: PIXI.InteractionManager = this._application + .renderer.plugins.interaction; + + interactionManager.addListener( + "click", + (event: PIXI.InteractionEvent) => { + const position = event.data.getLocalPosition(this._application.stage); - this._eventManager.click(position.x, position.y); - }); + this._eventManager.click(position.x, position.y); + }, + true + ); - this.addListener("pointermove", (event) => { - const position = event.data.getLocalPosition(this._application.stage); + interactionManager.addListener( + "pointermove", + (event: PIXI.InteractionEvent) => { + const position = event.data.getLocalPosition(this._application.stage); - this._eventManager.move(position.x, position.y); - }); + this._eventManager.move(position.x, position.y); + }, + true + ); - this.addListener("pointerup", (event) => { - const position = event.data.getLocalPosition(this._application.stage); + interactionManager.addListener( + "pointerup", + (event: PIXI.InteractionEvent) => { + const position = event.data.getLocalPosition(this._application.stage); - this._eventManager.pointerUp(position.x, position.y); - }); + this._eventManager.pointerUp(position.x, position.y); + }, + true + ); - this.addListener("pointerdown", (event) => { - const position = event.data.getLocalPosition(this._application.stage); + interactionManager.addListener( + "pointerdown", + (event: PIXI.InteractionEvent) => { + const position = event.data.getLocalPosition(this._application.stage); - this._eventManager.pointerDown(position.x, position.y); - }); + this._eventManager.pointerDown(position.x, position.y); + }, + true + ); } destroy() { - super.destroy(); - this._application.ticker.remove(this._updateRectangle); } private _updateRectangle = () => { + //this._box?.destroy(); + const renderer = this._application.renderer; + const width = renderer.width / renderer.resolution; + const height = renderer.height / renderer.resolution; - this.hitArea = new PIXI.Rectangle( - 0, - 0, - renderer.width / renderer.resolution, - renderer.height / renderer.resolution - ); + this._box = new PIXI.TilingSprite(PIXI.Texture.WHITE, width, height); + this._box.alpha = 0.3; + + //this._application.stage.addChild(this._box); }; } diff --git a/src/objects/events/EventManagerNode.ts b/src/objects/events/EventManagerNode.ts index 5eab33c8..fb5444c2 100644 --- a/src/objects/events/EventManagerNode.ts +++ b/src/objects/events/EventManagerNode.ts @@ -42,7 +42,9 @@ export class EventManagerNode implements IEventManagerNode { } destroy(): void { - this._bush.remove(this); + if (this._rectangle != null) { + this._bush.remove(this); + } this._subscription.unsubscribe(); } diff --git a/src/objects/events/EventOverOutHandler.ts b/src/objects/events/EventOverOutHandler.ts new file mode 100644 index 00000000..b23a8362 --- /dev/null +++ b/src/objects/events/EventOverOutHandler.ts @@ -0,0 +1,143 @@ +import { HitSpriteEventMap } from "../hitdetection/HitSprite"; +import { EventEmitter } from "./EventEmitter"; +import { IEventManagerEvent } from "./interfaces/IEventManagerEvent"; + +type EventOverOutCallback = (event: IEventManagerEvent) => void; + +export class EventOverOutHandler { + private _eventEmitters: Map< + EventEmitter, + RegisteredOverOutHandler + > = new Map(); + private _overElements: Set> = new Set(); + private _hover = false; + private _onOverCallback: EventOverOutCallback | undefined; + private _onOutCallback: EventOverOutCallback | undefined; + private _timeout: any; + + public get onOver() { + return this._onOverCallback; + } + + public set onOver(value) { + this._onOverCallback = value; + } + + public get onOut() { + return this._onOutCallback; + } + + public set onOut(value) { + this._onOutCallback = value; + } + + register(emitter: EventEmitter) { + if (this._eventEmitters.has(emitter)) return; + + this._eventEmitters.set( + emitter, + new RegisteredOverOutHandler( + emitter, + this._onOver, + this._onOut, + this._onTargetChanged + ) + ); + } + + remove(emitter: EventEmitter) { + const handler = this._eventEmitters.get(emitter); + if (handler == null) return; + + this._eventEmitters.delete(emitter); + this._overElements.delete(emitter); + + handler.destroy(); + } + + private _onOver = ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => { + this._overElements.add(emitter); + this._update(event); + }; + + private _onOut = ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => { + this._overElements.delete(emitter); + this._update(event); + }; + + private _onTargetChanged = ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => { + this._overElements.delete(emitter); + this._update(event, false); + }; + + private _update(event: IEventManagerEvent, triggerEvents = true) { + if (this._overElements.size > 0 && !this._hover) { + this._hover = true; + if (triggerEvents) { + this.onOver && this.onOver(event); + } + } + + if (this._overElements.size < 1 && this._hover) { + this._hover = false; + if (triggerEvents) { + this.onOut && this.onOut(event); + } + } + } +} + +class RegisteredOverOutHandler { + constructor( + private emitter: EventEmitter, + private onOver: ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => void, + private onOut: ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => void, + private onTargetChanged: ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => void + ) { + emitter.addEventListener("pointerout", this._handlePointerOut); + emitter.addEventListener("pointerover", this._handlePointerOver); + emitter.addEventListener( + "pointertargetchanged", + this._handlePointerTargetChanged + ); + } + + destroy() { + this.emitter.removeEventListener("pointerout", this._handlePointerOut); + this.emitter.removeEventListener("pointerover", this._handlePointerOver); + this.emitter.removeEventListener( + "pointertargetchanged", + this._handlePointerTargetChanged + ); + } + + private _handlePointerOut = (event: IEventManagerEvent) => { + this.onOut(this.emitter, event); + }; + + private _handlePointerOver = (event: IEventManagerEvent) => { + this.onOver(this.emitter, event); + }; + + private _handlePointerTargetChanged = (event: IEventManagerEvent) => { + this.onTargetChanged(this.emitter, event); + }; +} diff --git a/src/objects/events/interfaces/IEventGroup.ts b/src/objects/events/interfaces/IEventGroup.ts index a9b99386..be2f948e 100644 --- a/src/objects/events/interfaces/IEventGroup.ts +++ b/src/objects/events/interfaces/IEventGroup.ts @@ -3,10 +3,12 @@ export interface IEventGroup { } export type EventGroupIdentifier = - | typeof FURNITURE_EVENT - | typeof AVATAR_EVENT - | typeof TILE_CURSOR_EVENT; + | typeof FURNITURE + | typeof AVATAR + | typeof TILE_CURSOR + | typeof WALL; -export const FURNITURE_EVENT = Symbol("FURNITURE"); -export const AVATAR_EVENT = Symbol("AVATAR"); -export const TILE_CURSOR_EVENT = Symbol("TILE_CURSOR"); +export const FURNITURE = Symbol("FURNITURE"); +export const AVATAR = Symbol("AVATAR"); +export const TILE_CURSOR = Symbol("TILE_CURSOR"); +export const WALL = Symbol("WALL"); diff --git a/src/objects/events/interfaces/IEventHandler.ts b/src/objects/events/interfaces/IEventHandler.ts index 83d13fd4..a9b47c70 100644 --- a/src/objects/events/interfaces/IEventHandler.ts +++ b/src/objects/events/interfaces/IEventHandler.ts @@ -6,4 +6,5 @@ export interface IEventHandler { triggerPointerUp(event: IEventManagerEvent): void; triggerPointerOver(event: IEventManagerEvent): void; triggerPointerOut(event: IEventManagerEvent): void; + triggerPointerTargetChanged(event: IEventManagerEvent): void; } diff --git a/src/objects/events/interfaces/IEventManagerEvent.ts b/src/objects/events/interfaces/IEventManagerEvent.ts index 193b080b..6d622844 100644 --- a/src/objects/events/interfaces/IEventManagerEvent.ts +++ b/src/objects/events/interfaces/IEventManagerEvent.ts @@ -3,5 +3,10 @@ import { EventGroupIdentifier } from "./IEventGroup"; export interface IEventManagerEvent { tag?: string; stopPropagation(): void; - skip(identifiers: EventGroupIdentifier[]): void; + skip(...identifiers: EventGroupIdentifierParam[]): void; + skipExcept(...identifiers: EventGroupIdentifierParam[]): void; } + +export type EventGroupIdentifierParam = + | EventGroupIdentifierParam[] + | EventGroupIdentifier; diff --git a/src/objects/furniture/BaseFurniture.tsx b/src/objects/furniture/BaseFurniture.tsx index cead3d16..16ac163a 100644 --- a/src/objects/furniture/BaseFurniture.tsx +++ b/src/objects/furniture/BaseFurniture.tsx @@ -24,11 +24,12 @@ import { getDirectionForFurniture } from "./util/getDirectionForFurniture"; import { IEventManager } from "../events/interfaces/IEventManager"; import { EventGroupIdentifier, - FURNITURE_EVENT, + FURNITURE, IEventGroup, } from "../events/interfaces/IEventGroup"; import { NOOP_EVENT_MANAGER } from "../events/EventManager"; import { FurnitureVisualizationView } from "./FurnitureVisualizationView"; +import { EventOverOutHandler } from "../events/EventOverOutHandler"; const highlightFilter = new HighlightFilter(0x999999, 0xffffff); @@ -72,7 +73,10 @@ export class BaseFurniture implements IFurnitureEventHandlers, IEventGroup { private _type: FurnitureFetch; private _unknownTexture: PIXI.Texture | undefined; private _unknownSprite: FurnitureSprite | undefined; + private _clickHandler = new ClickHandler(); + private _overOutHandler = new EventOverOutHandler(); + private _loadFurniResultPromise: Promise; private _validDirections: number[] | undefined; private _resolveLoadFurniResult: ResolveLoadFurniResult | undefined; @@ -266,6 +270,22 @@ export class BaseFurniture implements IFurnitureEventHandlers, IEventGroup { this._clickHandler.onPointerUp = value; } + public get onPointerOut() { + return this._overOutHandler.onOut; + } + + public set onPointerOut(value) { + this._overOutHandler.onOut = value; + } + + public get onPointerOver() { + return this._overOutHandler.onOver; + } + + public set onPointerOver(value) { + this._overOutHandler.onOver = value; + } + public get x() { return this._x; } @@ -330,7 +350,7 @@ export class BaseFurniture implements IFurnitureEventHandlers, IEventGroup { } getEventGroupIdentifier(): EventGroupIdentifier { - return FURNITURE_EVENT; + return FURNITURE; } destroy() { @@ -476,8 +496,9 @@ export class BaseFurniture implements IFurnitureEventHandlers, IEventGroup { this._view?.destroy(); const view = new FurnitureVisualizationView( - this.dependencies.hitDetection, + this.dependencies.eventManager, this._clickHandler, + this._overOutHandler, this.dependencies.visualization.container, loadFurniResult ); diff --git a/src/objects/furniture/FloorFurniture.tsx b/src/objects/furniture/FloorFurniture.tsx index f433e340..4a66ff3a 100644 --- a/src/objects/furniture/FloorFurniture.tsx +++ b/src/objects/furniture/FloorFurniture.tsx @@ -33,6 +33,8 @@ export class FloorFurniture private _onDoubleClick: HitEventHandler | undefined; private _onPointerDown: HitEventHandler | undefined; private _onPointerUp: HitEventHandler | undefined; + private _onPointerOver: HitEventHandler | undefined; + private _onPointerOut: HitEventHandler | undefined; constructor( options: { @@ -205,6 +207,24 @@ export class FloorFurniture this._baseFurniture.onPointerUp = this.onPointerUp; } + public get onPointerOver() { + return this._onPointerOver; + } + + public set onPointerOver(value) { + this._onPointerOver = value; + this._baseFurniture.onPointerOver = this.onPointerOver; + } + + public get onPointerOut() { + return this._onPointerOut; + } + + public set onPointerOut(value) { + this._onPointerOut = value; + this._baseFurniture.onPointerOut = this.onPointerOut; + } + /** * ID of the furniture */ diff --git a/src/objects/furniture/FurnitureVisualizationView.ts b/src/objects/furniture/FurnitureVisualizationView.ts index b496c44d..3fb75bcc 100644 --- a/src/objects/furniture/FurnitureVisualizationView.ts +++ b/src/objects/furniture/FurnitureVisualizationView.ts @@ -1,8 +1,9 @@ import * as PIXI from "pixi.js"; +import { EventOverOutHandler } from "../events/EventOverOutHandler"; import { EventGroupIdentifier, - FURNITURE_EVENT, + FURNITURE, IEventGroup, } from "../events/interfaces/IEventGroup"; import { IEventManager } from "../events/interfaces/IEventManager"; @@ -88,12 +89,13 @@ export class FurnitureVisualizationView constructor( private _eventManager: IEventManager, private _clickHandler: ClickHandler, + private _overOutHandler: EventOverOutHandler, private _container: PIXI.Container, private _furniture: LoadFurniResult ) {} getEventGroupIdentifier(): EventGroupIdentifier { - return FURNITURE_EVENT; + return FURNITURE; } getLayers(): IFurnitureVisualizationLayer[] { @@ -143,6 +145,7 @@ export class FurnitureVisualizationView part, this._eventManager, this._clickHandler, + this._overOutHandler, (id) => this._furniture.getTexture(id) ) ); @@ -265,6 +268,7 @@ class FurnitureVisualizationLayer private _part: FurniDrawPart, private _eventManager: IEventManager, private _clickHandler: ClickHandler, + private _overOutHandler: EventOverOutHandler, private _getTexture: (id: string) => HitTexture | undefined ) { this.frameRepeat = _part.frameRepeat; @@ -317,11 +321,13 @@ class FurnitureVisualizationLayer this._mountedSprites.add(sprite); this._container.addChild(sprite); + this._overOutHandler.register(sprite.events); } private _destroySprites() { this._sprites.forEach((sprite) => { this._container.removeChild(sprite); + this._overOutHandler.remove(sprite.events); sprite.destroy(); }); this._sprites = new Map(); @@ -477,6 +483,8 @@ class FurnitureVisualizationLayer this._setSpriteVisible(sprite, false); this._sprites.set(frameIndex, sprite); + this._overOutHandler.register(sprite.events); + return sprite; } diff --git a/src/objects/furniture/util/IFurnitureEventHandlers.ts b/src/objects/furniture/util/IFurnitureEventHandlers.ts index a03603e7..b8cc0e23 100644 --- a/src/objects/furniture/util/IFurnitureEventHandlers.ts +++ b/src/objects/furniture/util/IFurnitureEventHandlers.ts @@ -6,4 +6,6 @@ export interface IFurnitureEventHandlers { onDoubleClick?: (event: IEventManagerEvent) => void; onPointerDown?: (event: IEventManagerEvent) => void; onPointerUp?: (event: IEventManagerEvent) => void; + onPointerOver?: (event: IEventManagerEvent) => void; + onPointerOut?: (event: IEventManagerEvent) => void; } diff --git a/src/objects/hitdetection/HitSprite.ts b/src/objects/hitdetection/HitSprite.ts index 900f9c2f..d6a5de03 100644 --- a/src/objects/hitdetection/HitSprite.ts +++ b/src/objects/hitdetection/HitSprite.ts @@ -77,6 +77,11 @@ export class HitSprite extends PIXI.Sprite implements IEventTarget { return this.zIndex; } + triggerPointerTargetChanged(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointertargetchanged", event); + } + triggerClick(event: IEventManagerEvent): void { event.tag = this._tag; this._eventEmitter.trigger("click", event); @@ -230,10 +235,11 @@ export class HitSprite extends PIXI.Sprite implements IEventTarget { } } -type HitSpriteEventMap = { +export type HitSpriteEventMap = { click: IEventManagerEvent; pointerup: IEventManagerEvent; pointerdown: IEventManagerEvent; pointerover: IEventManagerEvent; pointerout: IEventManagerEvent; + pointertargetchanged: IEventManagerEvent; }; diff --git a/src/objects/room/RoomModelVisualization.ts b/src/objects/room/RoomModelVisualization.ts index e048e21b..2f394f8b 100644 --- a/src/objects/room/RoomModelVisualization.ts +++ b/src/objects/room/RoomModelVisualization.ts @@ -113,9 +113,8 @@ export class RoomModelVisualization this._tileLayer.sortableChildren = true; this.addChild(this._positionalContainer); - this._positionalContainer.addChild( - new EventManagerContainer(this._application, this._eventManager) - ); + + new EventManagerContainer(this._application, this._eventManager); this._updateHeightmap(); diff --git a/src/objects/room/parts/TileCursor.ts b/src/objects/room/parts/TileCursor.ts index 8f8f6eb5..20391677 100644 --- a/src/objects/room/parts/TileCursor.ts +++ b/src/objects/room/parts/TileCursor.ts @@ -1,10 +1,11 @@ import * as PIXI from "pixi.js"; import { BehaviorSubject, Observable } from "rxjs"; import { RoomPosition } from "../../../types/RoomPosition"; +import { isPointInside } from "../../../util/isPointInside"; import { EventGroupIdentifier, IEventGroup, - TILE_CURSOR_EVENT, + TILE_CURSOR, } from "../../events/interfaces/IEventGroup"; import { IEventManager } from "../../events/interfaces/IEventManager"; import { IEventManagerEvent } from "../../events/interfaces/IEventManagerEvent"; @@ -41,7 +42,7 @@ export class TileCursor } getEventGroupIdentifier(): EventGroupIdentifier { - return TILE_CURSOR_EVENT; + return TILE_CURSOR; } getGroup(): IEventGroup { @@ -56,7 +57,15 @@ export class TileCursor return -1000; } - triggerClick(event: IEventManagerEvent): void {} + triggerPointerTargetChanged(event: IEventManagerEvent): void {} + + triggerClick(event: IEventManagerEvent): void { + this.onClick({ + roomX: this._roomX, + roomY: this._roomY, + roomZ: this._roomZ, + }); + } triggerPointerDown(event: IEventManagerEvent): void {} @@ -153,25 +162,7 @@ export class TileCursor } private _pointInside(point: [number, number], vs: [number, number][]) { - // ray-casting algorithm based on - // https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html/pnpoly.html - - const x = point[0]; - const y = point[1]; - - let inside = false; - for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { - const xi = vs[i][0]; - const yi = vs[i][1]; - const xj = vs[j][0]; - const yj = vs[j][1]; - - const intersect = - yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; - if (intersect) inside = !inside; - } - - return inside; + return isPointInside(point, vs); } } diff --git a/src/objects/room/parts/WallLeft.ts b/src/objects/room/parts/WallLeft.ts index 5728447b..aa09f01b 100644 --- a/src/objects/room/parts/WallLeft.ts +++ b/src/objects/room/parts/WallLeft.ts @@ -74,24 +74,7 @@ export class WallLeft extends PIXI.Container implements IRoomPart { this.removeChildren(); - const hitArea = new PIXI.Polygon([ - new PIXI.Point( - this._getOffsetX() + this._borderWidth, - this._wallWidth / 2 - (this.props.cutawayHeight ?? 0) - ), - new PIXI.Point( - this._getOffsetX() + this._wallWidth + this._borderWidth, - -(this.props.cutawayHeight ?? 0) - ), - new PIXI.Point( - this._getOffsetX() + this._wallWidth + this._borderWidth, - -this._wallHeight - ), - new PIXI.Point( - this._getOffsetX() + this._borderWidth, - -this._wallHeight + this._wallWidth / 2 - ), - ]); + const hitArea = new PIXI.Polygon(this._getDisplayPoints()); this.hitArea = hitArea; @@ -135,6 +118,27 @@ export class WallLeft extends PIXI.Container implements IRoomPart { this.props.hitAreaContainer.addChild(this._hitAreaElement); } + private _getDisplayPoints() { + return [ + new PIXI.Point( + this._getOffsetX() + this._borderWidth, + this._wallWidth / 2 - (this.props.cutawayHeight ?? 0) + ), + new PIXI.Point( + this._getOffsetX() + this._wallWidth + this._borderWidth, + -(this.props.cutawayHeight ?? 0) + ), + new PIXI.Point( + this._getOffsetX() + this._wallWidth + this._borderWidth, + -this._wallHeight + ), + new PIXI.Point( + this._getOffsetX() + this._borderWidth, + -this._wallHeight + this._wallWidth / 2 + ), + ]; + } + private _getOffsetX() { return this.scale.x * this._offsets.x - this._borderWidth; } diff --git a/src/util/isPointInside.ts b/src/util/isPointInside.ts new file mode 100644 index 00000000..4c416cfa --- /dev/null +++ b/src/util/isPointInside.ts @@ -0,0 +1,21 @@ +export function isPointInside( + point: [number, number], + vs: [number, number][] +): boolean { + const x = point[0]; + const y = point[1]; + + let inside = false; + for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { + const xi = vs[i][0]; + const yi = vs[i][1]; + const xj = vs[j][0]; + const yj = vs[j][1]; + + const intersect = + yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + + return inside; +} diff --git a/storybook/stories/Documentation.stories.ts b/storybook/stories/Documentation.stories.ts index e0c6b902..b32bd017 100644 --- a/storybook/stories/Documentation.stories.ts +++ b/storybook/stories/Documentation.stories.ts @@ -102,6 +102,14 @@ export function ImplementingFurnitureLogic() { } }); + furniture.onPointerOver = () => { + console.log("OVER"); + }; + + furniture.onPointerOut = () => { + console.log("OUT"); + }; + room.addRoomObject(furniture); application.stage.addChild(room); diff --git a/storybook/stories/furniture/Furniture.stories.ts b/storybook/stories/furniture/Furniture.stories.ts index 0e1069e8..1671462c 100644 --- a/storybook/stories/furniture/Furniture.stories.ts +++ b/storybook/stories/furniture/Furniture.stories.ts @@ -17,6 +17,10 @@ import { import { createShroom } from "../common/createShroom"; import { action } from "@storybook/addon-actions"; import fetch from "node-fetch"; +import { + AVATAR, + FURNITURE, +} from "../../../dist/objects/events/interfaces/IEventGroup"; export default { title: "Furniture / General", @@ -996,6 +1000,8 @@ export function LoadTest() { `, }); + let activeFurniture: FloorFurniture | undefined; + fetch(`./furni.json`) .then((response) => response.json()) .then((furnitures: any[]) => { @@ -1022,6 +1028,20 @@ export function LoadTest() { obj.onClick = (event) => { console.log("Clicked"); + event.skip(FURNITURE, AVATAR); + }; + + obj.onPointerOver = (event) => { + if (activeFurniture != null) return; + + activeFurniture = obj; + console.log("Active Furniture", obj.type, obj.roomX, obj.roomY); + }; + + obj.onPointerOut = (event) => { + if (activeFurniture === obj) { + activeFurniture = undefined; + } }; room.addRoomObject(obj); @@ -1040,6 +1060,11 @@ export function LoadTest() { room.x = application.screen.width / 2 - room.roomWidth / 2; room.y = application.screen.height / 2 - room.roomHeight / 2; room.addRoomObject(furniture1); + + room.onTileClick = (event) => { + console.log(event); + }; + application.stage.addChild(room); }); } From dafa966d58797d67ba09eae8aeafe68efc41f3bc Mon Sep 17 00:00:00 2001 From: jankuss Date: Wed, 17 Feb 2021 13:10:36 -0800 Subject: [PATCH 5/8] Change screen position logic --- src/objects/avatar/Avatar.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/objects/avatar/Avatar.ts b/src/objects/avatar/Avatar.ts index cc7305a3..0089cd3c 100644 --- a/src/objects/avatar/Avatar.ts +++ b/src/objects/avatar/Avatar.ts @@ -288,12 +288,12 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { * for placing UI relative to the user. */ get screenPosition() { - const worldTransform = this._avatarSprites.worldTransform; + const worldTransform = this._avatarSprites.getGlobalPosition(); if (worldTransform == null) return; return { - x: worldTransform.tx, - y: worldTransform.ty, + x: worldTransform.x, + y: worldTransform.y, }; } From 54e5c1a3cd805bc53c949380686111a1447e9d2a Mon Sep 17 00:00:00 2001 From: jankuss Date: Wed, 17 Feb 2021 14:04:07 -0800 Subject: [PATCH 6/8] Remove unused code & update CHANGELOG.md --- CHANGELOG.md | 26 ++++++++++++ src/index.ts | 10 +++++ src/interfaces/IHitDetection.ts | 41 ------------------- src/objects/avatar/Avatar.ts | 24 +++++++++++ src/objects/avatar/BaseAvatar.ts | 31 +++++++++++++- src/objects/events/EventManager.test.ts | 39 ++++++++++-------- src/objects/events/EventManager.ts | 34 +++++++++------ src/objects/events/EventManagerContainer.ts | 16 ++------ src/objects/events/EventOverOutHandler.ts | 13 ++++-- src/objects/events/interfaces/IEventGroup.ts | 4 +- .../events/interfaces/IEventManagerEvent.ts | 3 ++ src/objects/furniture/BaseFurniture.tsx | 1 - .../furniture/util/IFurnitureEventHandlers.ts | 1 - src/objects/hitdetection/ClickHandler.ts | 1 - src/objects/hitdetection/HitSprite.ts | 5 +-- storybook/stories/avatar/Avatar.stories.ts | 8 ++++ 16 files changed, 158 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a591294..faaf3c20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.0] - 2021-02-17 + +### Added + +- **Avatars** + + - Added ability to display effects for an avatar. To use, set the `avatar.effect` property + - Improved avatar loading by only loading the required libraries for a specific avatar + +- **Event handling** + - Added `onPointerOver` and `onPointerOut` event handling for Avatars and Furniture + - Added ability to skip event handling for certain kinds of room objects while propagating. + Use this in an event handler: `event.skip(FURNITURE, AVATAR)` or `event.skipExcept(TILE_CURSOR)` + +### Changed + +- Change furniture visualization handling for better testability +- Reorganize stories in storybook +- Deprecate `BasicFurnitureVisualization` in favor of `StaticFurnitureVisualization` + +### Removed + +- Remove use of `offsets.json` for avatars +- Remove `resumePropagation` for events +- Disable animation queueing for avatars and furnitures + ## [0.6.5] - 2021-01-25 ### Fixed diff --git a/src/index.ts b/src/index.ts index 59996698..eb5ca25a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +import { IEventManagerEvent } from "./objects/events/interfaces/IEventManagerEvent"; + export { RoomObject } from "./objects/RoomObject"; export { Avatar } from "./objects/avatar/Avatar"; export { FloorFurniture } from "./objects/furniture/FloorFurniture"; @@ -32,3 +34,11 @@ export { IFurnitureVisualization } from "./objects/furniture/IFurnitureVisualiza export { WallLeft } from "./objects/room/parts/WallLeft"; export { WallRight } from "./objects/room/parts/WallRight"; export { RoomModelVisualization } from "./objects/room/RoomModelVisualization"; + +export { + AVATAR, + TILE_CURSOR, + FURNITURE, +} from "./objects/events/interfaces/IEventGroup"; + +export type HitEvent = IEventManagerEvent; diff --git a/src/interfaces/IHitDetection.ts b/src/interfaces/IHitDetection.ts index 6c4ee82f..e69de29b 100644 --- a/src/interfaces/IHitDetection.ts +++ b/src/interfaces/IHitDetection.ts @@ -1,41 +0,0 @@ -import { InteractionEvent } from "pixi.js"; -import { Rectangle } from "../objects/room/IRoomRectangle"; - -export interface Rect { - x: number; - y: number; - width: number; - height: number; - zIndex: number; -} - -export type HitEventType = "click" | "pointerdown" | "pointerup"; - -export interface HitEvent { - mouseEvent: MouseEvent | TouchEvent | PointerEvent; - interactionEvent: InteractionEvent; - - tag?: string; - target: HitDetectionElement; - - stopPropagation(): void; - resumePropagation(): void; -} - -export interface HitDetectionElement { - group?: unknown; - - trigger(type: HitEventType, event: HitEvent): void; - hits(x: number, y: number): boolean; - getHitDetectionZIndex(): number; - createDebugSprite?(): PIXI.Sprite | undefined; -} - -export interface HitDetectionNode { - remove(): void; - updateDimensions(element: Rectangle | undefined): void; -} - -export interface IHitDetection { - register(rectangle: HitDetectionElement): HitDetectionNode; -} diff --git a/src/objects/avatar/Avatar.ts b/src/objects/avatar/Avatar.ts index 0089cd3c..862fedbe 100644 --- a/src/objects/avatar/Avatar.ts +++ b/src/objects/avatar/Avatar.ts @@ -44,6 +44,8 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { private _onDoubleClick: HitEventHandler | undefined = undefined; private _onPointerDown: HitEventHandler | undefined = undefined; private _onPointerUp: HitEventHandler | undefined = undefined; + private _onPointerOver: HitEventHandler | undefined = undefined; + private _onPointerOut: HitEventHandler | undefined = undefined; constructor({ look, @@ -135,6 +137,24 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { this._updateEventHandlers(); } + public get onPointerOver() { + return this._onPointerOver; + } + + public set onPointerOver(value) { + this._onPointerOver = value; + this._updateEventHandlers(); + } + + public get onPointerOut() { + return this._onPointerOut; + } + + public set onPointerOut(value) { + this._onPointerOut = value; + this._updateEventHandlers(); + } + /** * The x position of the avatar in the room. * The y-Axis is marked in the following graphic: @@ -454,12 +474,16 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { this._placeholderSprites.onDoubleClick = this._onDoubleClick; this._placeholderSprites.onPointerDown = this._onPointerDown; this._placeholderSprites.onPointerUp = this._onPointerUp; + this._placeholderSprites.onPointerOut = this._onPointerOut; + this._placeholderSprites.onPointerOver = this._onPointerOver; } this._loadingAvatarSprites.onClick = this._onClick; this._loadingAvatarSprites.onDoubleClick = this._onDoubleClick; this._loadingAvatarSprites.onPointerDown = this._onPointerDown; this._loadingAvatarSprites.onPointerUp = this._onPointerUp; + this._loadingAvatarSprites.onPointerOut = this._onPointerOut; + this._loadingAvatarSprites.onPointerOver = this._onPointerOver; } private _getPlaceholderLookOptions(): LookOptions { diff --git a/src/objects/avatar/BaseAvatar.ts b/src/objects/avatar/BaseAvatar.ts index 9a652e01..2f5ad564 100644 --- a/src/objects/avatar/BaseAvatar.ts +++ b/src/objects/avatar/BaseAvatar.ts @@ -23,6 +23,7 @@ import { EventGroupIdentifier, IEventGroup, } from "../events/interfaces/IEventGroup"; +import { EventOverOutHandler } from "../events/EventOverOutHandler"; const bodyPartTypes: Set = new Set([ AvatarFigurePartType.Head, @@ -73,6 +74,7 @@ export class BaseAvatar extends PIXI.Container implements IEventGroup { private _currentFrame = 0; private _clickHandler: ClickHandler = new ClickHandler(); + private _overOutHandler: EventOverOutHandler = new EventOverOutHandler(); private _refreshFrame = false; private _refreshLook = false; @@ -147,6 +149,22 @@ export class BaseAvatar extends PIXI.Container implements IEventGroup { this._clickHandler.onPointerUp = value; } + get onPointerOut() { + return this._overOutHandler.onOut; + } + + set onPointerOut(value) { + this._overOutHandler.onOut = value; + } + + get onPointerOver() { + return this._overOutHandler.onOver; + } + + set onPointerOver(value) { + this._overOutHandler.onOver = value; + } + get lookOptions() { if (this._nextLookOptions != null) { return this._nextLookOptions; @@ -210,7 +228,10 @@ export class BaseAvatar extends PIXI.Container implements IEventGroup { } private _destroyAssets() { - this._sprites.forEach((sprite) => sprite.destroy()); + this._sprites.forEach((sprite) => { + this._overOutHandler.remove(sprite.events); + sprite.destroy(); + }); this._sprites = new Map(); this._container?.destroy(); } @@ -294,6 +315,10 @@ export class BaseAvatar extends PIXI.Container implements IEventGroup { if (sprite == null) { sprite = this._createAsset(part, asset); + + if (sprite != null) { + this._overOutHandler.register(sprite.events); + } } if (sprite == null) return; @@ -315,6 +340,10 @@ export class BaseAvatar extends PIXI.Container implements IEventGroup { if (sprite == null) { sprite = this._createAsset(part, asset); + + if (sprite != null) { + this._overOutHandler.register(sprite.events); + } } if (sprite == null) return; diff --git a/src/objects/events/EventManager.test.ts b/src/objects/events/EventManager.test.ts index 953f2bd2..ddeb7889 100644 --- a/src/objects/events/EventManager.test.ts +++ b/src/objects/events/EventManager.test.ts @@ -1,3 +1,4 @@ +import { InteractionEvent } from "pixi.js"; import { BehaviorSubject } from "rxjs"; import { Rectangle } from "../room/IRoomRectangle"; import { EventManager } from "./EventManager"; @@ -9,6 +10,10 @@ import { } from "./interfaces/IEventGroup"; import { IEventTarget } from "./interfaces/IEventTarget"; +const interactionEvent: InteractionEvent = { + data: {}, +} as any; + test("handles click when mounted", () => { const manager = new EventManager(); @@ -27,11 +32,11 @@ test("handles click when mounted", () => { }; const node = manager.register(target); - manager.click(10, 10); + manager.click(interactionEvent, 10, 10); expect(target.triggerClick).toHaveBeenCalledTimes(1); node.destroy(); - manager.click(10, 10); + manager.click(interactionEvent, 10, 10); expect(target.triggerClick).toHaveBeenCalledTimes(1); }); @@ -53,7 +58,7 @@ test("handles click on hit elements", () => { }; const node = manager.register(target); - manager.click(10, 10); + manager.click(interactionEvent, 10, 10); expect(target.triggerClick).toHaveBeenCalledTimes(1); }); @@ -75,7 +80,7 @@ test("doesn't handle click on not hit elements", () => { }; const node = manager.register(target); - manager.click(10, 10); + manager.click(interactionEvent, 10, 10); expect(target.triggerClick).toHaveBeenCalledTimes(0); }); @@ -143,7 +148,7 @@ test("handles click on multiple elements", () => { manager.register(target3); manager.register(target4); - manager.click(80, 80); + manager.click(interactionEvent, 80, 80); expect(target1.triggerClick).toHaveBeenCalledTimes(1); expect(target2.triggerClick).toHaveBeenCalledTimes(1); expect(target3.triggerClick).toHaveBeenCalledTimes(1); @@ -184,7 +189,7 @@ test("handles click on multiple elements", () => { manager.register(target1); manager.register(target2); - manager.click(80, 80); + manager.click(interactionEvent, 80, 80); expect(target1.triggerClick).toHaveBeenCalledTimes(1); expect(target2.triggerClick).toHaveBeenCalledTimes(1); }); @@ -225,7 +230,7 @@ test("only handles first element when elements from same group", () => { manager.register(target1); manager.register(target2); - manager.click(80, 80); + manager.click(interactionEvent, 80, 80); expect(target1.triggerClick).toHaveBeenCalledTimes(0); expect(target2.triggerClick).toHaveBeenCalledTimes(1); }); @@ -294,7 +299,7 @@ test("event.skip() skips elements", () => { manager.register(target3); manager.register(target4); - manager.click(80, 80); + manager.click(interactionEvent, 80, 80); expect(target1.triggerClick).toHaveBeenCalledTimes(0); expect(target2.triggerClick).toHaveBeenCalledTimes(1); expect(target3.triggerClick).toHaveBeenCalledTimes(0); @@ -319,35 +324,35 @@ test("move triggers correct events", () => { }; manager.register(target1); - manager.move(80, 80); + manager.move(interactionEvent, 80, 80); expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); - manager.move(85, 80); + manager.move(interactionEvent, 85, 80); expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); - manager.move(90, 80); + manager.move(interactionEvent, 90, 80); expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); - manager.move(95, 80); + manager.move(interactionEvent, 95, 80); expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); - manager.move(100, 80); + manager.move(interactionEvent, 100, 80); expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); - manager.move(105, 80); + manager.move(interactionEvent, 105, 80); expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(1); - manager.move(100, 80); + manager.move(interactionEvent, 100, 80); expect(target1.triggerPointerOver).toHaveBeenCalledTimes(2); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(1); - manager.move(105, 80); + manager.move(interactionEvent, 105, 80); expect(target1.triggerPointerOver).toHaveBeenCalledTimes(2); expect(target1.triggerPointerOut).toHaveBeenCalledTimes(2); }); @@ -416,7 +421,7 @@ test("event.skipExcept() skips elements except the specified", () => { manager.register(target3); manager.register(target4); - manager.click(80, 80); + manager.click(interactionEvent, 80, 80); expect(target1.triggerClick).toHaveBeenCalledTimes(1); expect(target2.triggerClick).toHaveBeenCalledTimes(1); expect(target3.triggerClick).toHaveBeenCalledTimes(0); diff --git a/src/objects/events/EventManager.ts b/src/objects/events/EventManager.ts index 6925f14c..a255e806 100644 --- a/src/objects/events/EventManager.ts +++ b/src/objects/events/EventManager.ts @@ -12,6 +12,7 @@ import { TILE_CURSOR, } from "./interfaces/IEventGroup"; import { IEventManager } from "./interfaces/IEventManager"; +import { InteractionEvent } from "pixi.js"; export class EventManager { private _nodes = new Map(); @@ -19,24 +20,24 @@ export class EventManager { private _currentOverElements: Set = new Set(); private _pointerDownElements: Set = new Set(); - click(x: number, y: number) { + click(event: InteractionEvent, x: number, y: number) { const elements = this._performHitTest(x, y); - new Propagation(elements.activeNodes, (target, event) => + new Propagation(event, elements.activeNodes, (target, event) => target.triggerClick(event) ); } - pointerDown(x: number, y: number) { + pointerDown(event: InteractionEvent, x: number, y: number) { const elements = this._performHitTest(x, y); this._pointerDownElements = new Set(elements.activeNodes); - new Propagation(elements.activeNodes, (target, event) => + new Propagation(event, elements.activeNodes, (target, event) => target.triggerPointerDown(event) ); } - pointerUp(x: number, y: number) { + pointerUp(event: InteractionEvent, x: number, y: number) { const elements = this._performHitTest(x, y); const elementsSet = new Set(elements.activeNodes); @@ -47,16 +48,16 @@ export class EventManager { } }); - new Propagation(elements.activeNodes, (target, event) => + new Propagation(event, elements.activeNodes, (target, event) => target.triggerPointerUp(event) ); - new Propagation(Array.from(clickedNodes), (target, event) => { + new Propagation(event, Array.from(clickedNodes), (target, event) => { target.triggerClick(event); }); } - move(x: number, y: number) { + move(event: InteractionEvent, x: number, y: number) { const elements = this._performHitTest(x, y); const current = new Set( elements.activeNodes.filter( @@ -101,15 +102,19 @@ export class EventManager { this._currentOverElements = current; - new Propagation(Array.from(removedButGroupPresent), (target, event) => { - target.triggerPointerTargetChanged(event); - }); + new Propagation( + event, + Array.from(removedButGroupPresent), + (target, event) => { + target.triggerPointerTargetChanged(event); + } + ); - new Propagation(Array.from(actualRemoved), (target, event) => + new Propagation(event, Array.from(actualRemoved), (target, event) => target.triggerPointerOut(event) ); - new Propagation(Array.from(added), (target, event) => + new Propagation(event, Array.from(added), (target, event) => target.triggerPointerOver(event) ); } @@ -169,6 +174,7 @@ class Propagation { private _stopped = false; constructor( + private event: InteractionEvent, private path: EventManagerNode[], private _trigger: (target: IEventTarget, event: IEventManagerEvent) => void ) { @@ -201,6 +207,8 @@ class Propagation { private _createEvent(): IEventManagerEvent { return { + interactionEvent: this.event, + mouseEvent: this.event.data.originalEvent, stopPropagation: () => { this._stopped = true; }, diff --git a/src/objects/events/EventManagerContainer.ts b/src/objects/events/EventManagerContainer.ts index 29c4a13e..8817c0bc 100644 --- a/src/objects/events/EventManagerContainer.ts +++ b/src/objects/events/EventManagerContainer.ts @@ -15,22 +15,12 @@ export class EventManagerContainer { const interactionManager: PIXI.InteractionManager = this._application .renderer.plugins.interaction; - interactionManager.addListener( - "click", - (event: PIXI.InteractionEvent) => { - const position = event.data.getLocalPosition(this._application.stage); - - this._eventManager.click(position.x, position.y); - }, - true - ); - interactionManager.addListener( "pointermove", (event: PIXI.InteractionEvent) => { const position = event.data.getLocalPosition(this._application.stage); - this._eventManager.move(position.x, position.y); + this._eventManager.move(event, position.x, position.y); }, true ); @@ -40,7 +30,7 @@ export class EventManagerContainer { (event: PIXI.InteractionEvent) => { const position = event.data.getLocalPosition(this._application.stage); - this._eventManager.pointerUp(position.x, position.y); + this._eventManager.pointerUp(event, position.x, position.y); }, true ); @@ -50,7 +40,7 @@ export class EventManagerContainer { (event: PIXI.InteractionEvent) => { const position = event.data.getLocalPosition(this._application.stage); - this._eventManager.pointerDown(position.x, position.y); + this._eventManager.pointerDown(event, position.x, position.y); }, true ); diff --git a/src/objects/events/EventOverOutHandler.ts b/src/objects/events/EventOverOutHandler.ts index b23a8362..20a95a78 100644 --- a/src/objects/events/EventOverOutHandler.ts +++ b/src/objects/events/EventOverOutHandler.ts @@ -14,6 +14,7 @@ export class EventOverOutHandler { private _onOverCallback: EventOverOutCallback | undefined; private _onOutCallback: EventOverOutCallback | undefined; private _timeout: any; + private _targetChanged = false; public get onOver() { return this._onOverCallback; @@ -76,20 +77,24 @@ export class EventOverOutHandler { event: IEventManagerEvent ) => { this._overElements.delete(emitter); - this._update(event, false); + this._targetChanged = true; + this._update(event); }; - private _update(event: IEventManagerEvent, triggerEvents = true) { + private _update(event: IEventManagerEvent) { if (this._overElements.size > 0 && !this._hover) { this._hover = true; - if (triggerEvents) { + + if (!this._targetChanged) { this.onOver && this.onOver(event); } + + this._targetChanged = false; } if (this._overElements.size < 1 && this._hover) { this._hover = false; - if (triggerEvents) { + if (!this._targetChanged) { this.onOut && this.onOut(event); } } diff --git a/src/objects/events/interfaces/IEventGroup.ts b/src/objects/events/interfaces/IEventGroup.ts index be2f948e..f7e45ea6 100644 --- a/src/objects/events/interfaces/IEventGroup.ts +++ b/src/objects/events/interfaces/IEventGroup.ts @@ -5,10 +5,8 @@ export interface IEventGroup { export type EventGroupIdentifier = | typeof FURNITURE | typeof AVATAR - | typeof TILE_CURSOR - | typeof WALL; + | typeof TILE_CURSOR; export const FURNITURE = Symbol("FURNITURE"); export const AVATAR = Symbol("AVATAR"); export const TILE_CURSOR = Symbol("TILE_CURSOR"); -export const WALL = Symbol("WALL"); diff --git a/src/objects/events/interfaces/IEventManagerEvent.ts b/src/objects/events/interfaces/IEventManagerEvent.ts index 6d622844..fb11ac7f 100644 --- a/src/objects/events/interfaces/IEventManagerEvent.ts +++ b/src/objects/events/interfaces/IEventManagerEvent.ts @@ -1,7 +1,10 @@ +import { InteractionEvent } from "pixi.js"; import { EventGroupIdentifier } from "./IEventGroup"; export interface IEventManagerEvent { tag?: string; + mouseEvent: MouseEvent | TouchEvent | PointerEvent; + interactionEvent: InteractionEvent; stopPropagation(): void; skip(...identifiers: EventGroupIdentifierParam[]): void; skipExcept(...identifiers: EventGroupIdentifierParam[]): void; diff --git a/src/objects/furniture/BaseFurniture.tsx b/src/objects/furniture/BaseFurniture.tsx index 16ac163a..73c88c78 100644 --- a/src/objects/furniture/BaseFurniture.tsx +++ b/src/objects/furniture/BaseFurniture.tsx @@ -14,7 +14,6 @@ import { import { FurnitureAsset } from "./data/interfaces/IFurnitureAssetsData"; import { FurnitureLayer } from "./data/interfaces/IFurnitureVisualizationData"; import { IAnimationTicker } from "../../interfaces/IAnimationTicker"; -import { IHitDetection } from "../../interfaces/IHitDetection"; import { IRoomContext } from "../../interfaces/IRoomContext"; import { Shroom } from "../Shroom"; import { IFurnitureVisualization } from "./IFurnitureVisualization"; diff --git a/src/objects/furniture/util/IFurnitureEventHandlers.ts b/src/objects/furniture/util/IFurnitureEventHandlers.ts index b8cc0e23..3f9306ac 100644 --- a/src/objects/furniture/util/IFurnitureEventHandlers.ts +++ b/src/objects/furniture/util/IFurnitureEventHandlers.ts @@ -1,4 +1,3 @@ -import { HitEvent } from "../../../interfaces/IHitDetection"; import { IEventManagerEvent } from "../../events/interfaces/IEventManagerEvent"; export interface IFurnitureEventHandlers { diff --git a/src/objects/hitdetection/ClickHandler.ts b/src/objects/hitdetection/ClickHandler.ts index 0da96a8c..af721667 100644 --- a/src/objects/hitdetection/ClickHandler.ts +++ b/src/objects/hitdetection/ClickHandler.ts @@ -1,4 +1,3 @@ -import { HitEvent } from "../../interfaces/IHitDetection"; import { IEventManagerEvent } from "../events/interfaces/IEventManagerEvent"; import { HitEventHandler } from "./HitSprite"; diff --git a/src/objects/hitdetection/HitSprite.ts b/src/objects/hitdetection/HitSprite.ts index d6a5de03..b618a883 100644 --- a/src/objects/hitdetection/HitSprite.ts +++ b/src/objects/hitdetection/HitSprite.ts @@ -1,6 +1,5 @@ import * as PIXI from "pixi.js"; import { BehaviorSubject, Observable } from "rxjs"; -import { HitEvent, HitEventType, Rect } from "../../interfaces/IHitDetection"; import { EventEmitter } from "../events/EventEmitter"; import { IEventGroup } from "../events/interfaces/IEventGroup"; import { IEventManager } from "../events/interfaces/IEventManager"; @@ -185,7 +184,7 @@ export class HitSprite extends PIXI.Sprite implements IEventTarget { this._eventManager.remove(this); } - getHitBox(): Rect { + getHitBox(): Rectangle { const pos = this.getGlobalPosition(); if (this._mirrored) { @@ -194,7 +193,6 @@ export class HitSprite extends PIXI.Sprite implements IEventTarget { y: pos.y, width: this.texture.width, height: this.texture.height, - zIndex: this.zIndex, }; } @@ -203,7 +201,6 @@ export class HitSprite extends PIXI.Sprite implements IEventTarget { y: pos.y, width: this.texture.width, height: this.texture.height, - zIndex: this.zIndex, }; } diff --git a/storybook/stories/avatar/Avatar.stories.ts b/storybook/stories/avatar/Avatar.stories.ts index 4228fbe3..776599cf 100644 --- a/storybook/stories/avatar/Avatar.stories.ts +++ b/storybook/stories/avatar/Avatar.stories.ts @@ -68,6 +68,14 @@ export function Default() { room.addRoomObject(avatar); avatars.push(avatar); + + avatar.onPointerOver = () => { + console.log("OVER"); + }; + + avatar.onPointerOut = () => { + console.log("OUT"); + }; } } From 95ae81056fe30c02f8effd21e079ead285127d85 Mon Sep 17 00:00:00 2001 From: jankuss Date: Wed, 17 Feb 2021 14:07:53 -0800 Subject: [PATCH 7/8] Improve CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faaf3c20..fc0bbe7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove use of `offsets.json` for avatars - Remove `resumePropagation` for events -- Disable animation queueing for avatars and furnitures +- Disable animation queueing for avatars and furnitures `move`/`walk` methods ## [0.6.5] - 2021-01-25 From ddff96cab63c4e11449393a917d854941eba03cb Mon Sep 17 00:00:00 2001 From: jankuss Date: Wed, 17 Feb 2021 14:24:37 -0800 Subject: [PATCH 8/8] Fix coloring of guild furniture --- src/objects/furniture/FurnitureVisualizationView.ts | 8 +++++++- .../visualization/AnimatedFurnitureVisualization.ts | 13 ++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/objects/furniture/FurnitureVisualizationView.ts b/src/objects/furniture/FurnitureVisualizationView.ts index 3fb75bcc..7195680c 100644 --- a/src/objects/furniture/FurnitureVisualizationView.ts +++ b/src/objects/furniture/FurnitureVisualizationView.ts @@ -296,6 +296,10 @@ class FurnitureVisualizationLayer if (newFrame != null) { this._setSpriteVisible(newFrame, true); this._addSprite(newFrame); + + if (this._color != null) { + newFrame.tint = this._color; + } } } @@ -379,7 +383,9 @@ class FurnitureVisualizationLayer private _getSprite(frameIndex: number) { const current = this._sprites.get(frameIndex); - if (current != null) return current; + if (current != null) { + return current; + } const spriteInfo = this._getSpriteInfo(frameIndex); if (spriteInfo == null) return; diff --git a/src/objects/furniture/visualization/AnimatedFurnitureVisualization.ts b/src/objects/furniture/visualization/AnimatedFurnitureVisualization.ts index 6e684671..c773c457 100644 --- a/src/objects/furniture/visualization/AnimatedFurnitureVisualization.ts +++ b/src/objects/furniture/visualization/AnimatedFurnitureVisualization.ts @@ -164,6 +164,13 @@ export class AnimatedFurnitureVisualization extends FurnitureVisualization { this._updateFurniture(); } + private _updateLayers() { + if (this.modifier != null) { + const modifier = this.modifier; + this.view.getLayers().forEach((layer) => modifier(layer)); + } + } + private _updateFurniture() { if (!this.mounted) return; @@ -171,11 +178,7 @@ export class AnimatedFurnitureVisualization extends FurnitureVisualization { this.view.setDisplayAnimation(this.animationId?.toString()); this.view.updateDisplay(); - if (this.modifier != null) { - const modifier = this.modifier; - this.view.getLayers().forEach((layer) => modifier(layer)); - } - + this._updateLayers(); this._update(); }