• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 14804036802

02 May 2025 09:58PM UTC coverage: 5.927% (-83.4%) from 89.28%
14804036802

Pull #3404

github

web-flow
Merge 5c103d7f8 into 0f2ccaeb2
Pull Request #3404: feat: added Graph module to Math

234 of 8383 branches covered (2.79%)

229 of 246 new or added lines in 1 file covered. (93.09%)

13145 existing lines in 208 files now uncovered.

934 of 15759 relevant lines covered (5.93%)

4.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/engine/Input/PointerEventsToObjectDispatcher.ts
1
import { EventEmitter } from '../EventEmitter';
2
import { PointerEvent } from './PointerEvent';
3
import { GlobalCoordinates } from '../Math';
4
import { PointerEventReceiver } from './PointerEventReceiver';
5
import { RentalPool } from '../Util/RentalPool';
6

7
/**
8
 * Signals that an object has nested pointer events on nested objects that are not an Entity with
9
 * a PointerComponent. For example TileMap Tiles
10
 */
11
export interface HasNestedPointerEvents {
12
  _dispatchPointerEvents(receiver: PointerEventReceiver): void;
13
  _processPointerToObject(receiver: PointerEventReceiver): void;
14
}
15

16
/**
17
 *
18
 */
19
function hasNestedEvents(object: any): object is HasNestedPointerEvents {
UNCOV
20
  return object && object._dispatchPointerEvents && object._processPointerToObject;
×
21
}
22

23
export class PointerTargetObjectProxy<TObject extends { events: EventEmitter }> {
24
  public object!: TObject;
25
  public contains!: (point: GlobalCoordinates) => boolean;
26
  public active!: () => boolean;
27
  public get events(): EventEmitter {
UNCOV
28
    return this.object.events;
×
29
  }
30
  public init(object: TObject, contains: (point: GlobalCoordinates) => boolean, active: () => boolean): void {
UNCOV
31
    this.object = object;
×
UNCOV
32
    this.contains = contains;
×
UNCOV
33
    this.active = active;
×
34
  }
35
}
36

37
export class PointerEventsToObjectDispatcher<TObject extends { events: EventEmitter }> {
UNCOV
38
  private _proxyPool = new RentalPool(
×
UNCOV
39
    () => new PointerTargetObjectProxy<TObject>(),
×
40
    (p) => p,
×
41
    100
42
  );
UNCOV
43
  private _objectToProxy = new Map<TObject, PointerTargetObjectProxy<TObject>>();
×
UNCOV
44
  private _proxies: PointerTargetObjectProxy<TObject>[] = [];
×
45

46
  /**
47
   * Tracks an object to associate with pointers and their events
48
   * @param object
49
   * @param contains
50
   * @param active
51
   */
52
  public addObject(object: TObject, contains: (point: GlobalCoordinates) => boolean, active: () => boolean): void {
UNCOV
53
    const proxy = this._proxyPool.rent(false);
×
UNCOV
54
    proxy.init(object, contains, active);
×
UNCOV
55
    this._proxies.push(proxy);
×
UNCOV
56
    this._objectToProxy.set(object, proxy);
×
57
  }
58

59
  private _getProxy(object: TObject): PointerTargetObjectProxy<TObject> {
UNCOV
60
    const proxy = this._objectToProxy.get(object);
×
UNCOV
61
    if (proxy) {
×
UNCOV
62
      return proxy;
×
63
    }
64
    throw new Error('No PointerTargetProxy for object');
×
65
  }
66

67
  /**
68
   * Untracks an object associated with pointers and their events
69
   * @param object
70
   */
71
  public removeObject(object: TObject): void {
UNCOV
72
    const proxy = this._objectToProxy.get(object);
×
UNCOV
73
    if (proxy) {
×
UNCOV
74
      const index = this._proxies.indexOf(proxy);
×
UNCOV
75
      if (index > -1) {
×
UNCOV
76
        this._proxies.splice(index, 1);
×
77
      }
UNCOV
78
      this._proxyPool.return(proxy);
×
79
    }
80
  }
UNCOV
81
  private _lastFrameObjectToPointers = new Map<PointerTargetObjectProxy<any>, number[]>();
×
UNCOV
82
  private _currentFrameObjectToPointers = new Map<PointerTargetObjectProxy<any>, number[]>();
×
83
  private _objectCurrentlyUnderPointer(object: PointerTargetObjectProxy<any>, pointerId: number): boolean {
UNCOV
84
    return !!(this._currentFrameObjectToPointers.has(object) && this._currentFrameObjectToPointers.get(object)!.includes(pointerId));
×
85
  }
86

87
  private _objectWasUnderPointer(object: PointerTargetObjectProxy<any>, pointerId: number): boolean {
UNCOV
88
    return !!(this._lastFrameObjectToPointers.has(object) && this._lastFrameObjectToPointers.get(object)!.includes(pointerId));
×
89
  }
90

91
  private _entered(object: PointerTargetObjectProxy<any>, pointerId: number): boolean {
UNCOV
92
    return this._objectCurrentlyUnderPointer(object, pointerId) && !this._lastFrameObjectToPointers.has(object);
×
93
  }
94

95
  private _left(object: PointerTargetObjectProxy<any>, pointerId: number): boolean {
UNCOV
96
    return !this._currentFrameObjectToPointers.has(object) && this._objectWasUnderPointer(object, pointerId);
×
97
  }
98

99
  /**
100
   * Manually associate a pointer id with an object.
101
   *
102
   * This assumes you've checked that the pointer is indeed over the object.
103
   */
104
  public addPointerToObject(object: TObject, pointerId: number): void {
UNCOV
105
    const maybeProxy = this._objectToProxy.get(object);
×
UNCOV
106
    if (maybeProxy) {
×
UNCOV
107
      this._addPointerToProxy(maybeProxy, pointerId);
×
108
    }
109
  }
110

111
  private _addPointerToProxy(object: PointerTargetObjectProxy<any>, pointerId: number): void {
UNCOV
112
    if (!this._currentFrameObjectToPointers.has(object)) {
×
UNCOV
113
      this._currentFrameObjectToPointers.set(object, [pointerId]);
×
UNCOV
114
      return;
×
115
    }
UNCOV
116
    const pointers = this._currentFrameObjectToPointers.get(object)!;
×
UNCOV
117
    this._currentFrameObjectToPointers.set(object, pointers.concat(pointerId));
×
118
  }
119

120
  /**
121
   * Dispatches the appropriate pointer events in sortedObject order on tracked objects
122
   * @param receiver
123
   * @param sortedObjects
124
   */
125
  public dispatchEvents(receiver: PointerEventReceiver, sortedObjects: TObject[]) {
UNCOV
126
    const lastFrameEntities = new Set(this._lastFrameObjectToPointers.keys());
×
UNCOV
127
    const currentFrameEntities = new Set(this._currentFrameObjectToPointers.keys());
×
128
    // Filter preserves z order
129
    let lastMovePerPointer: Map<number, PointerEvent>;
130
    let lastUpPerPointer: Map<number, PointerEvent>;
131
    let lastDownPerPointer: Map<number, PointerEvent>;
132
    // Dispatch events in proxy z order
UNCOV
133
    for (let i = 0; i < sortedObjects.length; i++) {
×
UNCOV
134
      const object = sortedObjects[i];
×
UNCOV
135
      const proxy = this._getProxy(object);
×
UNCOV
136
      if (hasNestedEvents(object)) {
×
UNCOV
137
        object._dispatchPointerEvents(receiver);
×
138
      }
UNCOV
139
      if (lastFrameEntities.has(proxy) || currentFrameEntities.has(proxy)) {
×
UNCOV
140
        lastDownPerPointer = this._processDownAndEmit(receiver, proxy);
×
141

UNCOV
142
        lastUpPerPointer = this._processUpAndEmit(receiver, proxy);
×
143

UNCOV
144
        lastMovePerPointer = this._processMoveAndEmit(receiver, proxy);
×
145

UNCOV
146
        const lastUpDownMoveEvents = [...lastMovePerPointer.values(), ...lastDownPerPointer.values(), ...lastUpPerPointer.values()];
×
UNCOV
147
        this._processEnterLeaveAndEmit(receiver, proxy, lastUpDownMoveEvents);
×
148

UNCOV
149
        this._processCancelAndEmit(receiver, proxy);
×
150

UNCOV
151
        this._processWheelAndEmit(receiver, proxy);
×
152
      }
153
    }
154
  }
155

156
  /**
157
   * Given the tracked objects, update pointer containment given the provided contains()
158
   * @param receiver
159
   * @param objects
160
   */
161
  public processPointerToObject(receiver: PointerEventReceiver, objects: TObject[]) {
162
    // Pre-process find entities under pointers
UNCOV
163
    for (let objectIndex = 0; objectIndex < objects.length; objectIndex++) {
×
UNCOV
164
      const object = objects[objectIndex];
×
UNCOV
165
      const proxy = this._getProxy(object);
×
UNCOV
166
      if (hasNestedEvents(object)) {
×
UNCOV
167
        object._processPointerToObject(receiver);
×
168
      }
UNCOV
169
      for (const [pointerId, pos] of receiver.currentFramePointerCoords.entries()) {
×
UNCOV
170
        if (proxy.contains(pos)) {
×
UNCOV
171
          this._addPointerToProxy(proxy, pointerId);
×
172
        }
173
      }
174
    }
175
  }
176

177
  /**
178
   * Clear current frames pointer-object associations and track last frame pointer-object associations
179
   */
180
  public clear() {
UNCOV
181
    this._lastFrameObjectToPointers.clear();
×
UNCOV
182
    this._lastFrameObjectToPointers = new Map<PointerTargetObjectProxy<any>, number[]>(this._currentFrameObjectToPointers);
×
UNCOV
183
    this._currentFrameObjectToPointers.clear();
×
184
  }
185

186
  private _processDownAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>): Map<number, PointerEvent> {
UNCOV
187
    const lastDownPerPointer = new Map<number, PointerEvent>();
×
188
    // Loop through down and dispatch to entities
UNCOV
189
    for (const event of receiver.currentFrameDown) {
×
UNCOV
190
      if (event.active && this._objectCurrentlyUnderPointer(object, event.pointerId)) {
×
UNCOV
191
        object.events.emit('pointerdown', event as any);
×
UNCOV
192
        if (receiver.isDragStart(event.pointerId)) {
×
UNCOV
193
          object.events.emit('pointerdragstart', event as any);
×
194
        }
195
      }
UNCOV
196
      lastDownPerPointer.set(event.pointerId, event);
×
197
    }
UNCOV
198
    return lastDownPerPointer;
×
199
  }
200

201
  private _processUpAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>): Map<number, PointerEvent> {
UNCOV
202
    const lastUpPerPointer = new Map<number, PointerEvent>();
×
203
    // Loop through up and dispatch to entities
UNCOV
204
    for (const event of receiver.currentFrameUp) {
×
UNCOV
205
      if (event.active && this._objectCurrentlyUnderPointer(object, event.pointerId)) {
×
UNCOV
206
        object.events.emit('pointerup', event as any);
×
UNCOV
207
        if (receiver.isDragEnd(event.pointerId)) {
×
UNCOV
208
          object.events.emit('pointerdragend', event as any);
×
209
        }
210
      }
UNCOV
211
      lastUpPerPointer.set(event.pointerId, event);
×
212
    }
UNCOV
213
    return lastUpPerPointer;
×
214
  }
215

216
  private _processMoveAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>): Map<number, PointerEvent> {
UNCOV
217
    const lastMovePerPointer = new Map<number, PointerEvent>();
×
218
    // Loop through move and dispatch to entities
UNCOV
219
    for (const event of receiver.currentFrameMove) {
×
UNCOV
220
      if (event.active && object.active() && this._objectCurrentlyUnderPointer(object, event.pointerId)) {
×
221
        // move
UNCOV
222
        object.events.emit('pointermove', event as any);
×
223

UNCOV
224
        if (receiver.isDragging(event.pointerId)) {
×
UNCOV
225
          object.events.emit('pointerdragmove', event as any);
×
226
        }
227
      }
UNCOV
228
      lastMovePerPointer.set(event.pointerId, event);
×
229
    }
UNCOV
230
    return lastMovePerPointer;
×
231
  }
232

233
  private _processEnterLeaveAndEmit(
234
    receiver: PointerEventReceiver,
235
    object: PointerTargetObjectProxy<any>,
236
    lastUpDownMoveEvents: PointerEvent[]
237
  ) {
238
    // up, down, and move are considered for enter and leave
UNCOV
239
    for (const event of lastUpDownMoveEvents) {
×
240
      // enter
UNCOV
241
      if (event.active && object.active() && this._entered(object, event.pointerId)) {
×
UNCOV
242
        object.events.emit('pointerenter', event as any);
×
UNCOV
243
        if (receiver.isDragging(event.pointerId)) {
×
UNCOV
244
          object.events.emit('pointerdragenter', event as any);
×
245
        }
UNCOV
246
        break;
×
247
      }
UNCOV
248
      if (
×
249
        event.active &&
×
250
        object.active() &&
251
        // leave can happen on move
252
        (this._left(object, event.pointerId) ||
253
          // or leave can happen on pointer up
254
          (this._objectCurrentlyUnderPointer(object, event.pointerId) && event.type === 'up'))
255
      ) {
UNCOV
256
        object.events.emit('pointerleave', event as any);
×
UNCOV
257
        if (receiver.isDragging(event.pointerId)) {
×
UNCOV
258
          object.events.emit('pointerdragleave', event as any);
×
259
        }
UNCOV
260
        break;
×
261
      }
262
    }
263
  }
264

265
  private _processCancelAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>) {
266
    // cancel
UNCOV
267
    for (const event of receiver.currentFrameCancel) {
×
UNCOV
268
      if (event.active && object.active() && this._objectCurrentlyUnderPointer(object, event.pointerId)) {
×
UNCOV
269
        object.events.emit('pointercancel', event as any);
×
270
      }
271
    }
272
  }
273

274
  private _processWheelAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>) {
275
    // wheel
UNCOV
276
    for (const event of receiver.currentFrameWheel) {
×
277
      // Currently the wheel only fires under the primary pointer '0'
UNCOV
278
      if (event.active && object.active() && this._objectCurrentlyUnderPointer(object, 0)) {
×
UNCOV
279
        object.events.emit('pointerwheel', event as any);
×
280
      }
281
    }
282
  }
283
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc