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

excaliburjs / Excalibur / 19716357641

26 Nov 2025 08:18PM UTC coverage: 88.641% (+0.07%) from 88.576%
19716357641

Pull #3585

github

web-flow
Merge 7d0f94fd5 into 3b683c589
Pull Request #3585: feat!: debug draw improvements

5289 of 7219 branches covered (73.26%)

280 of 306 new or added lines in 18 files covered. (91.5%)

4 existing lines in 2 files now uncovered.

14655 of 16533 relevant lines covered (88.64%)

24567.88 hits per line

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

87.27
/src/engine/Debug/DebugSystem.ts
1
import type { Engine } from '../Engine';
2
import type { Scene } from '../Scene';
3
import type { Camera } from '../Camera';
4
import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent';
5
import { ColliderComponent } from '../Collision/ColliderComponent';
6
import type { Entity, Query, World } from '../EntityComponentSystem';
7
import { SystemPriority, TransformComponent } from '../EntityComponentSystem';
8
import { System, SystemType } from '../EntityComponentSystem/System';
9
import type { ExcaliburGraphicsContext } from '../Graphics/Context/ExcaliburGraphicsContext';
10
import { vec, Vector } from '../Math/vector';
11
import { toDegrees } from '../Math/util';
12
import { BodyComponent } from '../Collision/BodyComponent';
13
import { CollisionSystem } from '../Collision/CollisionSystem';
14
import { CompositeCollider } from '../Collision/Colliders/CompositeCollider';
15
import { Particle } from '../Particles/Particles';
16
import { DebugGraphicsComponent } from '../Graphics/DebugGraphicsComponent';
17
import { CoordPlane } from '../Math/coord-plane';
18
import { Debug } from '../Graphics/Debug';
19

20
export class DebugSystem extends System {
248✔
21
  static priority = SystemPriority.Lowest;
22

23
  public readonly systemType = SystemType.Draw;
1,304✔
24
  private _graphicsContext: ExcaliburGraphicsContext;
25
  private _collisionSystem: CollisionSystem;
26
  private _camera: Camera;
27
  private _engine: Engine;
28
  query: Query<typeof TransformComponent>;
29

30
  constructor(public world: World) {
1,304✔
31
    super();
1,304✔
32
    this.query = this.world.query([TransformComponent]);
1,304✔
33
  }
34

35
  public initialize(world: World, scene: Scene): void {
36
    this._graphicsContext = scene.engine.graphicsContext;
690✔
37
    this._camera = scene.camera;
690✔
38
    this._engine = scene.engine;
690✔
39
    this._collisionSystem = world.systemManager.get(CollisionSystem);
690✔
40
  }
41

42
  update(): void {
43
    if (!this._engine.isDebug) {
944✔
44
      return;
934✔
45
    }
46

47
    const filterSettings = this._engine.debug.filter;
10✔
48

49
    let id: number;
50
    let name: string;
51
    const entitySettings = this._engine.debug.entity;
10✔
52

53
    let tx: TransformComponent;
54
    const txSettings = this._engine.debug.transform;
10✔
55

56
    let motion: MotionComponent;
57
    const motionSettings = this._engine.debug.motion;
10✔
58

59
    let colliderComp: ColliderComponent;
60
    const colliderSettings = this._engine.debug.collider;
10✔
61

62
    const physicsSettings = this._engine.debug.physics;
10✔
63

64
    let debugDraw: DebugGraphicsComponent;
65

66
    let body: BodyComponent;
67
    const bodySettings = this._engine.debug.body;
10✔
68

69
    const cameraSettings = this._engine.debug.camera;
10✔
70
    for (let i = 0; i < this.query.entities.length; i++) {
10✔
71
      const entity = this.query.entities[i];
235✔
72
      if (entity.hasTag('offscreen')) {
235!
73
        // skip offscreen entities
74
        continue;
×
75
      }
76
      if (entity instanceof Particle) {
235!
77
        // Particles crush the renderer :(
78
        continue;
×
79
      }
80
      if (filterSettings.useFilter) {
235!
81
        const allIds = filterSettings.ids.length === 0;
×
82
        const idMatch = allIds || filterSettings.ids.includes(entity.id);
×
83
        if (!idMatch) {
×
84
          continue;
×
85
        }
86
        const allNames = filterSettings.nameQuery === '';
×
87
        const nameMatch = allNames || entity.name.includes(filterSettings.nameQuery);
×
88
        if (!nameMatch) {
×
89
          continue;
×
90
        }
91
      }
92

93
      let cursor = Vector.Zero;
235✔
94
      const lineHeight = vec(0, 16);
235✔
95
      id = entity.id;
235✔
96
      name = entity.name;
235✔
97
      tx = entity.get(TransformComponent);
235✔
98

99
      // This optionally sets our camera based on the entity coord plan (world vs. screen)
100
      this._pushCameraTransform(tx);
235✔
101

102
      this._graphicsContext.save();
235✔
103
      if (tx.coordPlane === CoordPlane.Screen) {
235!
104
        this._graphicsContext.translate(this._engine.screen.contentArea.left, this._engine.screen.contentArea.top);
×
105
      }
106

107
      this._applyTransform(entity);
235✔
108
      if (tx) {
235!
109
        if (txSettings.showAll || txSettings.showPosition) {
235✔
110
          this._graphicsContext.debug.drawPoint(Vector.Zero, { size: 4, color: txSettings.positionColor });
227✔
111
        }
112
        if (txSettings.showAll || txSettings.showPositionLabel) {
235✔
113
          this._graphicsContext.debug.drawText(`pos${tx.pos.toString(2)}`, cursor);
1✔
114
          cursor = cursor.add(lineHeight);
1✔
115
        }
116
        if (txSettings.showAll || txSettings.showZIndex) {
235✔
117
          this._graphicsContext.debug.drawText(`z(${tx.z.toFixed(1)})`, cursor);
1✔
118
          cursor = cursor.add(lineHeight);
1✔
119
        }
120

121
        if (entitySettings.showAll || entitySettings.showId) {
235✔
122
          this._graphicsContext.debug.drawText(`id(${id}) ${entity.parent ? 'child of id(' + entity.parent?.id + ')' : ''}`, cursor);
2!
123
          cursor = cursor.add(lineHeight);
2✔
124
        }
125

126
        if (entitySettings.showAll || entitySettings.showName) {
235✔
127
          this._graphicsContext.debug.drawText(`name(${name})`, cursor);
1✔
128
          cursor = cursor.add(lineHeight);
1✔
129
        }
130

131
        if (txSettings.showAll || txSettings.showRotation) {
235✔
132
          this._graphicsContext.drawLine(
1✔
133
            Vector.Zero,
134
            Vector.fromAngle(tx.rotation).scale(50).add(Vector.Zero),
135
            txSettings.rotationColor,
136
            2
137
          );
138
          this._graphicsContext.debug.drawText(`rot deg(${toDegrees(tx.rotation).toFixed(2)})`, cursor);
1✔
139
          cursor = cursor.add(lineHeight);
1✔
140
        }
141

142
        if (txSettings.showAll || txSettings.showScale) {
235✔
143
          this._graphicsContext.drawLine(Vector.Zero, tx.scale.add(Vector.Zero), txSettings.scaleColor, 2);
1✔
144
        }
145
      }
146

147
      debugDraw = entity.get(DebugGraphicsComponent);
235✔
148
      if (debugDraw) {
235✔
149
        if (!debugDraw.useTransform) {
3✔
150
          this._graphicsContext.restore();
2✔
151
        }
152
        debugDraw.draw(this._graphicsContext, this._engine.debug);
3✔
153
        if (!debugDraw.useTransform) {
3✔
154
          this._graphicsContext.save();
2✔
155
          this._applyTransform(entity);
2✔
156
        }
157
      }
158

159
      body = entity.get(BodyComponent);
235✔
160
      if (body) {
235✔
161
        if (bodySettings.showAll || bodySettings.showCollisionGroup) {
8✔
162
          this._graphicsContext.debug.drawText(`collision group(${body.group.name})`, cursor);
1✔
163
          cursor = cursor.add(lineHeight);
1✔
164
        }
165

166
        if (bodySettings.showAll || bodySettings.showCollisionType) {
8✔
167
          this._graphicsContext.debug.drawText(`collision type(${body.collisionType})`, cursor);
1✔
168
          cursor = cursor.add(lineHeight);
1✔
169
        }
170

171
        if (bodySettings.showAll || bodySettings.showMass) {
8✔
172
          this._graphicsContext.debug.drawText(`mass(${body.mass})`, cursor);
1✔
173
          cursor = cursor.add(lineHeight);
1✔
174
        }
175

176
        if (bodySettings.showAll || bodySettings.showMotion) {
8✔
177
          this._graphicsContext.debug.drawText(`motion(${body.sleepMotion})`, cursor);
1✔
178
          cursor = cursor.add(lineHeight);
1✔
179
        }
180

181
        if (bodySettings.showAll || bodySettings.showSleeping) {
8✔
182
          this._graphicsContext.debug.drawText(`sleeping(${body.canSleep ? body.isSleeping : 'cant sleep'})`, cursor);
1!
183
          cursor = cursor.add(lineHeight);
1✔
184
        }
185
      }
186

187
      this._graphicsContext.restore();
235✔
188

189
      // World space
190
      this._graphicsContext.save();
235✔
191
      if (tx.coordPlane === CoordPlane.Screen) {
235!
192
        this._graphicsContext.translate(this._engine.screen.contentArea.left, this._engine.screen.contentArea.top);
×
193
      }
194
      motion = entity.get(MotionComponent);
235✔
195
      if (motion) {
235✔
196
        if (motionSettings.showAll || motionSettings.showVelocity) {
8✔
197
          this._graphicsContext.debug.drawText(`vel${motion.vel.toString(2)}`, cursor.add(tx.globalPos));
1✔
198
          this._graphicsContext.drawLine(tx.globalPos, tx.globalPos.add(motion.vel), motionSettings.velocityColor, 2);
1✔
199
          cursor = cursor.add(lineHeight);
1✔
200
        }
201

202
        if (motionSettings.showAll || motionSettings.showAcceleration) {
8✔
203
          this._graphicsContext.drawLine(tx.globalPos, tx.globalPos.add(motion.acc), motionSettings.accelerationColor, 2);
1✔
204
        }
205
      }
206

207
      // Colliders live in world space already so after the restore()
208
      colliderComp = entity.get(ColliderComponent);
235✔
209
      if (colliderComp) {
235✔
210
        const collider = colliderComp.get();
9✔
211
        if ((colliderSettings.showAll || colliderSettings.showGeometry) && collider) {
9✔
212
          collider.debug(this._graphicsContext, colliderSettings.geometryColor, {
7✔
213
            lineWidth: colliderSettings.geometryLineWidth,
214
            pointSize: colliderSettings.geometryPointSize
215
          });
216
        }
217
        if (colliderSettings.showAll || colliderSettings.showBounds) {
9✔
218
          if (collider instanceof CompositeCollider) {
3✔
219
            const colliders = collider.getColliders();
1✔
220
            for (const collider of colliders) {
1✔
221
              const bounds = collider.bounds;
3✔
222
              const pos = vec(bounds.left, bounds.top);
3✔
223
              this._graphicsContext.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, {
3✔
224
                color: colliderSettings.boundsColor,
225
                dashed: true
226
              });
227
              if (colliderSettings.showAll || colliderSettings.showOwner) {
3!
228
                this._graphicsContext.debug.drawText(`owner id(${collider.owner.id})`, pos);
3✔
229
              }
230
            }
231
            colliderComp.bounds.debug(this._graphicsContext, {
1✔
232
              color: colliderSettings.boundsColor,
233
              dashed: true
234
            });
235
          } else if (collider) {
2✔
236
            const bounds = colliderComp.bounds;
1✔
237
            const pos = vec(bounds.left, bounds.top);
1✔
238
            this._graphicsContext.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, {
1✔
239
              color: colliderSettings.boundsColor,
240
              dashed: true
241
            });
242
            if (colliderSettings.showAll || colliderSettings.showOwner) {
1!
243
              this._graphicsContext.debug.drawText(`owner id(${colliderComp.owner.id})`, pos);
1✔
244
            }
245
          }
246
        }
247
      }
248

249
      this._graphicsContext.restore();
235✔
250
      this._popCameraTransform(tx);
235✔
251
    }
252

253
    this._graphicsContext.save();
10✔
254
    this._camera.draw(this._graphicsContext);
10✔
255
    if (physicsSettings.showAll || physicsSettings.showBroadphaseSpacePartitionDebug) {
10!
256
      this._collisionSystem.debug(this._graphicsContext);
×
257
    }
258
    if (physicsSettings.showAll || physicsSettings.showCollisionContacts || physicsSettings.showCollisionNormals) {
10!
259
      for (const [_, contact] of this._engine.debug.stats.currFrame.physics.contacts) {
10✔
260
        if (physicsSettings.showAll || physicsSettings.showCollisionContacts) {
×
261
          for (const point of contact.points) {
×
262
            this._graphicsContext.debug.drawPoint(point, {
×
263
              size: physicsSettings.contactSize,
264
              color: physicsSettings.collisionContactColor
265
            });
266
          }
267
        }
268

269
        if (physicsSettings.showAll || physicsSettings.showCollisionNormals) {
×
270
          for (const point of contact.points) {
×
271
            this._graphicsContext.debug.drawLine(point, contact.normal.scale(30).add(point), {
×
272
              color: physicsSettings.collisionNormalColor
273
            });
274
          }
275
        }
276
      }
277
    }
278
    this._graphicsContext.restore();
10✔
279

280
    if (cameraSettings) {
10!
281
      this._graphicsContext.save();
10✔
282
      this._camera.draw(this._graphicsContext);
10✔
283
      if (cameraSettings.showAll || cameraSettings.showFocus) {
10!
NEW
284
        this._graphicsContext.debug.drawCircle(this._camera.pos, 4, cameraSettings.focusColor);
×
285
      }
286
      if (cameraSettings.showAll || cameraSettings.showZoom) {
10!
287
        this._graphicsContext.debug.drawText(`zoom(${this._camera.zoom})`, this._camera.pos);
×
288
      }
289
      this._graphicsContext.restore();
10✔
290
    }
291
  }
292

293
  postupdate(engine: Scene<unknown>, elapsed: number): void {
294
    if (this._engine.isDebug) {
935✔
295
      this._graphicsContext.save();
1✔
296
      if (this._camera) {
1!
297
        this._camera.draw(this._graphicsContext);
1✔
298
      }
299
      Debug.flush(this._graphicsContext);
1✔
300
      this._graphicsContext.restore();
1✔
301
    }
302
  }
303

304
  /**
305
   * This applies the current entity transform to the graphics context
306
   * @param entity
307
   */
308
  private _applyTransform(entity: Entity): void {
309
    const ancestors = entity.getAncestors();
237✔
310
    for (const ancestor of ancestors) {
237✔
311
      const transform = ancestor?.get(TransformComponent);
462!
312
      if (transform) {
462!
313
        this._graphicsContext.translate(transform.pos.x, transform.pos.y);
462✔
314
        this._graphicsContext.scale(transform.scale.x, transform.scale.y);
462✔
315
        this._graphicsContext.rotate(transform.rotation);
462✔
316
      }
317
    }
318
  }
319

320
  /**
321
   * Applies the current camera transform if in world coordinates
322
   * @param transform
323
   */
324
  private _pushCameraTransform(transform: TransformComponent) {
325
    // Establish camera offset per entity
326
    if (transform.coordPlane === CoordPlane.World) {
235!
327
      this._graphicsContext.save();
235✔
328
      if (this._camera) {
235!
329
        this._camera.draw(this._graphicsContext);
235✔
330
      }
331
    }
332
  }
333

334
  /**
335
   * Resets the current camera transform if in world coordinates
336
   * @param transform
337
   */
338
  private _popCameraTransform(transform: TransformComponent) {
339
    if (transform.coordPlane === CoordPlane.World) {
235!
340
      // Apply camera world offset
341
      this._graphicsContext.restore();
235✔
342
    }
343
  }
344
}
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