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

excaliburjs / Excalibur / 25980973438

17 May 2026 04:08AM UTC coverage: 88.049% (-0.01%) from 88.06%
25980973438

Pull #3747

github

web-flow
Merge 75bc0d88f into 09d9eeca6
Pull Request #3747: fix: Enhance pointer event handling by adding coordPlane check for graphics tracking, also added world to screen space conversion on pointer event check

5564 of 7701 branches covered (72.25%)

17 of 18 new or added lines in 4 files covered. (94.44%)

2 existing lines in 2 files now uncovered.

15207 of 17271 relevant lines covered (88.05%)

24022.37 hits per line

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

98.78
/src/engine/input/pointer-system.ts
1
import type { Engine } from '../engine';
2
import type { Entity, World, Query } from '../entity-component-system';
3
import { System, TransformComponent, SystemType, SystemPriority } from '../entity-component-system';
4
import { GraphicsComponent } from '../graphics/graphics-component';
5
import type { Scene } from '../scene';
6
import { PointerComponent } from './pointer-component';
7
import type { PointerEventReceiver } from './pointer-event-receiver';
8
import { CoordPlane } from '../math/coord-plane';
9
import { SparseHashGrid } from '../collision/detection/sparse-hash-grid';
10
import { PointerEventsToObjectDispatcher } from './pointer-events-to-object-dispatcher';
11

12
/**
13
 * The PointerSystem is responsible for dispatching pointer events to entities
14
 * that need them.
15
 *
16
 * The PointerSystem can be optionally configured by the {@apilink PointerComponent}, by default Entities use
17
 * the {@apilink Collider}'s shape for pointer events.
18
 */
19
export class PointerSystem extends System {
254✔
20
  static priority = SystemPriority.Higher;
21

22
  public readonly systemType = SystemType.Update;
1,323✔
23

24
  private _engine: Engine;
25
  private _receivers: PointerEventReceiver[];
26
  private _engineReceiver: PointerEventReceiver;
27
  private _graphicsHashGrid = new SparseHashGrid<GraphicsComponent>({ size: 100 });
1,323✔
28
  private _graphics: GraphicsComponent[] = [];
1,323✔
29
  private _entityToPointer = new Map<Entity, PointerComponent>();
1,323✔
30
  private _pointerEventDispatcher = new PointerEventsToObjectDispatcher();
1,323✔
31

32
  query: Query<typeof TransformComponent | typeof PointerComponent>;
33

34
  constructor(public world: World) {
1,323✔
35
    super();
1,323✔
36
    this.query = this.world.query([TransformComponent, PointerComponent]);
1,323✔
37

38
    this.query.entityAdded$.subscribe((e) => {
1,323✔
39
      const tx = e.get(TransformComponent);
512✔
40
      const coordPlane = tx.coordPlane;
512✔
41
      const pointer = e.get(PointerComponent);
512✔
42
      this._pointerEventDispatcher.addObject(
512✔
43
        e,
44
        (pos) => {
45
          // If pointer bounds defined
46
          if (pointer && pointer.localBounds) {
65✔
47
            const pointerBounds = pointer.localBounds.transform(tx.get().matrix);
4✔
48
            return pointerBounds.contains(tx.coordPlane === CoordPlane.World ? pos.worldPos : pos.screenPos);
4!
49
          }
50
          return false;
61✔
51
        },
52
        () => e.isActive
41✔
53
      );
54
      this._entityToPointer.set(e, pointer);
512✔
55
      const maybeGfx = e.get(GraphicsComponent);
512✔
56
      const isInScreenSpace = coordPlane === CoordPlane.Screen;
512✔
57
      if (maybeGfx && isInScreenSpace) {
512✔
58
        this._graphics.push(maybeGfx);
8✔
59
        this._graphicsHashGrid.track(maybeGfx);
8✔
60
      }
61
      this._sortedTransforms.push(tx);
512✔
62
      this._sortedEntities.push(tx.owner);
512✔
63
      tx.zIndexChanged$.subscribe(this._zIndexUpdate);
512✔
64
      this._zHasChanged = true;
512✔
65
    });
66

67
    this.query.entityRemoved$.subscribe((e) => {
1,323✔
68
      this._pointerEventDispatcher.removeObject(e);
24✔
69
      const tx = e.get(TransformComponent);
24✔
70
      this._entityToPointer.delete(e);
24✔
71
      const maybeGfx = e.get(GraphicsComponent);
24✔
72
      if (maybeGfx) {
24!
73
        const index = this._graphics.indexOf(maybeGfx);
24✔
74
        if (index > -1) {
24!
UNCOV
75
          this._graphics.splice(index, 1);
×
76
        }
77
        this._graphicsHashGrid.untrack(maybeGfx);
24✔
78
      }
79
      tx.zIndexChanged$.unsubscribe(this._zIndexUpdate);
24✔
80
      const index = this._sortedTransforms.indexOf(tx);
24✔
81
      if (index > -1) {
24!
82
        this._sortedTransforms.splice(index, 1);
24✔
83
        this._sortedEntities.splice(index, 1);
24✔
84
      }
85
    });
86
  }
87

88
  /**
89
   * Optionally override component configuration for all entities
90
   */
91
  public overrideUseColliderShape = false;
1,323✔
92
  /**
93
   * Optionally override component configuration for all entities
94
   */
95
  public overrideUseGraphicsBounds = false;
1,323✔
96

97
  private _scene: Scene<unknown>;
98

99
  public initialize(world: World, scene: Scene): void {
100
    this._engine = scene.engine;
758✔
101
    this._scene = scene;
758✔
102
  }
103

104
  private _sortedTransforms: TransformComponent[] = [];
1,323✔
105
  private _sortedEntities: Entity[] = [];
1,323✔
106

107
  private _zHasChanged = false;
1,323✔
108
  private _zIndexUpdate = () => {
1,323✔
109
    this._zHasChanged = true;
2✔
110
  };
111

112
  public preupdate(): void {
113
    if (this._scene.camera.hasChanged()) {
1,841✔
114
      // if the camera has changed we want to force a transform update so pointers can be correctly calc'd
115
      this._scene.camera.updateTransform(this._scene.camera.pos);
538✔
116
    }
117

118
    // event receiver might change per frame
119
    this._receivers = [this._engine.input.pointers, this._scene.input.pointers];
1,841✔
120
    this._engineReceiver = this._engine.input.pointers;
1,841✔
121
    if (this._zHasChanged) {
1,841✔
122
      this._sortedTransforms.sort((a, b) => {
308✔
123
        return b.z - a.z;
112✔
124
      });
125
      this._sortedEntities = this._sortedTransforms.map((t) => t.owner);
412✔
126
      this._zHasChanged = false;
308✔
127
    }
128
  }
129

130
  public update(): void {
131
    // Update graphics
132
    this._graphicsHashGrid.update(this._graphics);
1,841✔
133

134
    // Locate all the pointer/entity mappings
135
    for (const [pointerId, pos] of this._engineReceiver.currentFramePointerCoords.entries()) {
1,841✔
136
      const colliders = this._scene.physics.query(pos.worldPos);
50✔
137
      for (let i = 0; i < colliders.length; i++) {
50✔
138
        const collider = colliders[i];
31✔
139
        const maybePointer = this._entityToPointer.get(collider.owner);
31✔
140
        if (maybePointer && (maybePointer.useColliderShape || this.overrideUseColliderShape)) {
31✔
141
          if (collider.contains(pos.worldPos)) {
30✔
142
            this._pointerEventDispatcher.addPointerToObject(collider.owner, pointerId);
27✔
143
          }
144
        }
145
      }
146

147
      const graphics = this._graphicsHashGrid.query(this._engine.worldToScreenCoordinates(pos.worldPos));
50✔
148
      for (let i = 0; i < graphics.length; i++) {
50✔
149
        const graphic = graphics[i];
1✔
150
        const maybePointer = this._entityToPointer.get(graphic.owner);
1✔
151
        if (maybePointer && (maybePointer.useGraphicsBounds || this.overrideUseGraphicsBounds)) {
1!
152
          this._pointerEventDispatcher.addPointerToObject(graphic.owner, pointerId);
1✔
153
        }
154
      }
155
    }
156

157
    this._pointerEventDispatcher.processPointerToObject(this._engineReceiver, this._sortedEntities);
1,841✔
158

159
    // Dispatch pointer events on entities
160
    this._pointerEventDispatcher.dispatchEvents(this._engineReceiver, this._sortedEntities);
1,841✔
161

162
    // Dispatch pointer events on top level pointers
163
    this._receivers.forEach((r) => r.update());
3,682✔
164

165
    // Clear last frame's events
166
    this._pointerEventDispatcher.clear();
1,841✔
167

168
    this._receivers.forEach((r) => r.clear());
3,682✔
169
  }
170
}
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