• 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

86.43
/src/engine/Graphics/GraphicsSystem.ts
1
import type { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
2
import type { Scene } from '../Scene';
3
import { GraphicsComponent } from './GraphicsComponent';
4
import { vec, Vector } from '../Math/vector';
5
import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent';
6
import type { Entity } from '../EntityComponentSystem/Entity';
7
import type { Camera } from '../Camera';
8
import type { Query, World } from '../EntityComponentSystem';
9
import { System, SystemPriority, SystemType } from '../EntityComponentSystem';
10
import type { Engine } from '../Engine';
11
import { GraphicsGroup } from './GraphicsGroup';
12
import { ParallaxComponent } from './ParallaxComponent';
13
import { CoordPlane } from '../Math/coord-plane';
14
import { BodyComponent } from '../Collision/BodyComponent';
15
import { FontCache } from './FontCache';
16
import { PostDrawEvent, PostTransformDrawEvent, PreDrawEvent, PreTransformDrawEvent } from '../Events';
17
import { Transform } from '../Math/transform';
18
import { blendTransform } from './TransformInterpolation';
19
import { Graphic } from './Graphic';
20

21
export class GraphicsSystem extends System {
248✔
22
  static priority = SystemPriority.Average;
23

24
  public readonly systemType = SystemType.Draw;
1,306✔
25
  private _token = 0;
1,306✔
26
  // Set in the initialize
27
  private _graphicsContext!: ExcaliburGraphicsContext;
28
  private _camera!: Camera;
29
  private _engine!: Engine;
30
  private _sortedTransforms: TransformComponent[] = [];
1,306✔
31
  query: Query<typeof TransformComponent | typeof GraphicsComponent>;
32
  public get sortedTransforms() {
33
    return this._sortedTransforms;
1✔
34
  }
35

36
  constructor(public world: World) {
1,306✔
37
    super();
1,306✔
38
    this.query = this.world.query([TransformComponent, GraphicsComponent]);
1,306✔
39
    this.query.entityAdded$.subscribe((e) => {
1,306✔
40
      const tx = e.get(TransformComponent);
1,961✔
41
      this._sortedTransforms.push(tx);
1,961✔
42
      tx.zIndexChanged$.subscribe(this._zIndexUpdate);
1,961✔
43
      this._zHasChanged = true;
1,961✔
44
    });
45
    this.query.entityRemoved$.subscribe((e) => {
1,306✔
46
      const tx = e.get(TransformComponent);
34✔
47
      tx.zIndexChanged$.unsubscribe(this._zIndexUpdate);
34✔
48
      const index = this._sortedTransforms.indexOf(tx);
34✔
49
      if (index > -1) {
34!
50
        this._sortedTransforms.splice(index, 1);
34✔
51
      }
52
    });
53
  }
54

55
  public initialize(world: World, scene: Scene): void {
56
    this._camera = scene.camera;
683✔
57
    this._engine = scene.engine;
683✔
58
  }
59

60
  private _zHasChanged = false;
1,306✔
61
  private _zIndexUpdate = () => {
1,306✔
62
    this._zHasChanged = true;
898✔
63
  };
64

65
  public preupdate(): void {
66
    // Graphics context could be switched to fallback in a new frame
67
    this._graphicsContext = this._engine.graphicsContext;
946✔
68
    if (this._zHasChanged) {
946✔
69
      this._sortedTransforms.sort((a, b) => {
293✔
70
        return a.globalZ - b.globalZ;
5,307✔
71
      });
72
      this._zHasChanged = false;
293✔
73
    }
74
  }
75

76
  public update(elapsed: number): void {
77
    this._token++;
945✔
78
    let graphics: GraphicsComponent;
79
    FontCache.checkAndClearCache();
945✔
80

81
    // This is a performance enhancement, most things are in world space
82
    // so if we can only do this once saves a ton of transform updates
83
    this._graphicsContext.save();
945✔
84
    if (this._camera) {
945!
85
      this._camera.draw(this._graphicsContext);
945✔
86
    }
87
    for (let transformIndex = 0; transformIndex < this._sortedTransforms.length; transformIndex++) {
945✔
88
      const transform = this._sortedTransforms[transformIndex];
2,380✔
89
      const entity = transform.owner as Entity;
2,380✔
90

91
      // If the entity is offscreen skip
92
      if (entity.hasTag('ex.offscreen')) {
2,380✔
93
        continue;
77✔
94
      }
95

96
      graphics = entity.get(GraphicsComponent);
2,303✔
97
      // Exit if graphics set to not visible
98
      if (!graphics.isVisible) {
2,303✔
99
        continue;
5✔
100
      }
101

102
      // Optionally run the onPreTransformDraw graphics lifecycle draw
103
      if (graphics.onPreTransformDraw) {
2,298!
104
        graphics.onPreTransformDraw(this._graphicsContext, elapsed);
×
105
      }
106
      entity.events.emit('pretransformdraw', new PreTransformDrawEvent(this._graphicsContext, elapsed, entity));
2,298✔
107

108
      // This optionally sets our camera based on the entity coord plan (world vs. screen)
109
      if (transform.coordPlane === CoordPlane.Screen) {
2,298✔
110
        this._graphicsContext.restore();
21✔
111
      }
112

113
      this._graphicsContext.save();
2,298✔
114
      if (transform.coordPlane === CoordPlane.Screen) {
2,298✔
115
        this._graphicsContext.translate(this._engine.screen.contentArea.left, this._engine.screen.contentArea.top);
21✔
116
      }
117

118
      // Tick any graphics state (but only once) for animations and graphics groups
119
      graphics.update(elapsed, this._token);
2,298✔
120

121
      // Apply parallax
122
      const parallax = entity.get(ParallaxComponent);
2,298✔
123
      if (parallax) {
2,298✔
124
        // We use the Tiled formula
125
        // https://doc.mapeditor.org/en/latest/manual/layers/#parallax-scrolling-factor
126
        // cameraPos * (1 - parallaxFactor)
127
        const oneMinusFactor = Vector.One.sub(parallax.parallaxFactor);
5✔
128
        const parallaxOffset = this._camera.drawPos.scale(oneMinusFactor);
5✔
129
        this._graphicsContext.translate(parallaxOffset.x, parallaxOffset.y);
5✔
130
      }
131

132
      // Position the entity + estimate lag
133
      this._applyTransform(entity);
2,298✔
134

135
      // If there is a material enable it on the context
136
      if (graphics.material) {
2,298✔
137
        this._graphicsContext.material = graphics.material;
3✔
138
      }
139

140
      // Optionally run the onPreDraw graphics lifecycle draw
141
      if (graphics.onPreDraw) {
2,298!
142
        graphics.onPreDraw(this._graphicsContext, elapsed);
×
143
      }
144
      entity.events.emit('predraw', new PreDrawEvent(this._graphicsContext, elapsed, entity));
2,298✔
145

146
      // this._graphicsContext.opacity *= graphics.opacity;
147
      this._applyOpacity(entity);
2,298✔
148

149
      // Draw the graphics component
150
      this._drawGraphicsComponent(graphics, transform);
2,298✔
151

152
      // Optionally run the onPostDraw graphics lifecycle draw
153
      if (graphics.onPostDraw) {
2,298✔
154
        graphics.onPostDraw(this._graphicsContext, elapsed);
978✔
155
      }
156
      entity.events.emit('postdraw', new PostDrawEvent(this._graphicsContext, elapsed, entity));
2,298✔
157

158
      this._graphicsContext.restore();
2,298✔
159

160
      // Reset the transform back to the original world space
161
      if (transform.coordPlane === CoordPlane.Screen) {
2,298✔
162
        this._graphicsContext.save();
21✔
163
        if (this._camera) {
21!
164
          this._camera.draw(this._graphicsContext);
21✔
165
        }
166
      }
167

168
      // Optionally run the onPreTransformDraw graphics lifecycle draw
169
      if (graphics.onPostTransformDraw) {
2,298!
170
        graphics.onPostTransformDraw(this._graphicsContext, elapsed);
×
171
      }
172
      entity.events.emit('posttransformdraw', new PostTransformDrawEvent(this._graphicsContext, elapsed, entity));
2,298✔
173
    }
174
    this._graphicsContext.restore();
945✔
175
  }
176

177
  private _drawGraphicsComponent(graphicsComponent: GraphicsComponent, transformComponent: TransformComponent) {
178
    if (graphicsComponent.isVisible) {
2,298!
179
      const flipHorizontal = graphicsComponent.flipHorizontal;
2,298✔
180
      const flipVertical = graphicsComponent.flipVertical;
2,298✔
181

182
      const graphic = graphicsComponent.current;
2,298✔
183
      const options = graphicsComponent.currentOptions ?? {};
2,298!
184

185
      if (graphic) {
2,298✔
186
        let anchor = graphicsComponent.anchor;
1,065✔
187
        let offset = graphicsComponent.offset;
1,065✔
188
        let scaleX = 1;
1,065✔
189
        let scaleY = 1;
1,065✔
190
        // handle specific overrides
191
        if (options?.anchor) {
1,065!
192
          anchor = options.anchor;
×
193
        }
194
        if (options?.offset) {
1,065!
195
          offset = options.offset;
×
196
        }
197
        const globalScale = transformComponent.globalScale;
1,065✔
198
        scaleX *= graphic.scale.x * globalScale.x;
1,065✔
199
        scaleY *= graphic.scale.y * globalScale.y;
1,065✔
200

201
        // See https://github.com/excaliburjs/Excalibur/pull/619 for discussion on this formula
202
        const offsetX = -graphic.width * anchor.x + offset.x * scaleX;
1,065✔
203
        const offsetY = -graphic.height * anchor.y + offset.y * scaleY;
1,065✔
204

205
        const oldFlipHorizontal = graphic.flipHorizontal;
1,065✔
206
        const oldFlipVertical = graphic.flipVertical;
1,065✔
207
        if (flipHorizontal || flipVertical) {
1,065✔
208
          // flip any currently flipped graphics
209
          graphic.flipHorizontal = flipHorizontal ? !oldFlipHorizontal : oldFlipHorizontal;
4✔
210
          graphic.flipVertical = flipVertical ? !oldFlipVertical : oldFlipVertical;
4✔
211
        }
212

213
        graphic?.draw(this._graphicsContext, offsetX, offsetY);
1,065!
214

215
        if (flipHorizontal || flipVertical) {
1,065✔
216
          graphic.flipHorizontal = oldFlipHorizontal;
4✔
217
          graphic.flipVertical = oldFlipVertical;
4✔
218
        }
219

220
        // This debug code is in-situ to avoid recalculating the positioning of graphics
221
        if (this._engine?.isDebug && this._engine.debug.graphics.showBounds) {
1,065!
NEW
222
          this._graphicsContext.save();
×
223
          const offset = vec(offsetX, offsetY);
×
224
          if (graphic instanceof GraphicsGroup) {
×
225
            for (const member of graphic.members) {
×
226
              let g: Graphic;
227
              let pos: Vector = Vector.Zero;
×
228
              if (member instanceof Graphic) {
×
229
                g = member;
×
230
              } else {
231
                g = member.graphic;
×
232
                pos = member.offset;
×
233
              }
234

235
              if (graphic.useAnchor) {
×
NEW
236
                g?.localBounds
×
237
                  .translate(offset.add(pos))
238
                  .debug(this._graphicsContext, { color: this._engine.debug.graphics.boundsColor, dashed: true });
239
              } else {
NEW
240
                g?.localBounds
×
241
                  .translate(pos)
242
                  .debug(this._graphicsContext, { color: this._engine.debug.graphics.boundsColor, dashed: true });
243
              }
244
            }
245
          } else {
246
            /* istanbul ignore next */
NEW
247
            graphic?.localBounds
×
248
              .translate(offset)
249
              .debug(this._graphicsContext, { color: this._engine.debug.graphics.boundsColor, dashed: true });
250
          }
NEW
251
          this._graphicsContext.restore();
×
252
        }
253
      }
254
    }
255
  }
256

257
  private _targetInterpolationTransform = new Transform();
1,306✔
258
  /**
259
   * This applies the current entity transform to the graphics context
260
   * @param entity
261
   */
262
  private _applyTransform(entity: Entity): void {
263
    const ancestors = entity.getAncestors();
2,298✔
264
    for (let i = 0; i < ancestors.length; i++) {
2,298✔
265
      const ancestor = ancestors[i];
3,204✔
266
      const transform = ancestor?.get(TransformComponent);
3,204!
267
      const optionalBody = ancestor?.get(BodyComponent);
3,204!
268
      if (transform) {
3,204✔
269
        let tx = transform.get();
3,203✔
270
        if (optionalBody) {
3,203✔
271
          if (this._engine.fixedUpdateTimestep && optionalBody.__oldTransformCaptured && optionalBody.enableFixedUpdateInterpolate) {
2,224✔
272
            // Interpolate graphics if needed
273
            const blend = this._engine.currentFrameLagMs / this._engine.fixedUpdateTimestep;
392✔
274
            tx = blendTransform(optionalBody.oldTransform, transform.get(), blend, this._targetInterpolationTransform);
392✔
275
          }
276
        }
277
        this._graphicsContext.z = transform.globalZ;
3,203✔
278
        this._graphicsContext.translate(tx.pos.x, tx.pos.y);
3,203✔
279
        this._graphicsContext.scale(tx.scale.x, tx.scale.y);
3,203✔
280
        this._graphicsContext.rotate(tx.rotation);
3,203✔
281
      }
282
    }
283
  }
284

285
  private _applyOpacity(entity: Entity): void {
286
    const ancestors = entity.getAncestors();
2,298✔
287
    for (let i = 0; i < ancestors.length; i++) {
2,298✔
288
      const ancestor = ancestors[i];
3,204✔
289
      const maybeGraphics = ancestor?.get(GraphicsComponent);
3,204!
290
      this._graphicsContext.opacity *= maybeGraphics?.opacity ?? 1;
3,204✔
291
    }
292
  }
293
}
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