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

excaliburjs / Excalibur / 19716547003

26 Nov 2025 08:27PM UTC coverage: 88.647% (+0.08%) from 88.57%
19716547003

push

github

web-flow
feat!: debug draw improvements (#3585)

<!--
Hi, and thanks for contributing to Excalibur!
Before you go any further, please read our contributing guide: https://github.com/excaliburjs/Excalibur/blob/main/.github/CONTRIBUTING.md
especially the "Submitting Changes" section:
https://github.com/excaliburjs/Excalibur/blob/main/.github/CONTRIBUTING.md#submitting-changes
---
A quick summary checklist is included below for convenience:
-->

===:clipboard: PR Checklist :clipboard:===

- [ ] :pushpin: issue exists in github for these changes
- [x] :microscope: existing tests still pass
- [x] :see_no_evil: code conforms to the [style guide](https://github.com/excaliburjs/Excalibur/blob/main/STYLEGUIDE.md)
- [x] :triangular_ruler: new tests written and passing / old tests updated with new scenario(s)
- [x] :page_facing_up: changelog entry added (or not needed)

==================

Discussed in our core contributor group, we are wanting to switch the "design language" of debug draw to help users more clearly identify problems.

<img width="1107" height="851" alt="image" src="https://github.com/user-attachments/assets/76c5fd59-2ede-4847-8138-ecd7cca5e210" />


1. "Bounds" type drawings will now be dashed boxes
2. Debug Graphics for line, point, point are no longer zoom coupled, so they'll always render at X pixels on screen regardless of zoom to make it easier to zoom in/out and debug things.
3. Breaking change! in bounds debug drawing to allow specifying more props
4. Fixed dupe graphics bounds drawing in DebugDrawSystem
5. Improved performance of debug drawing!


TODOS:
* [x] Debug Specific Circle Renderer
* [x] Uncouple Debug Text from Camera zoom
* [x] Add configuration option for dashed

5288 of 7219 branches covered (73.25%)

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

3 existing lines in 1 file now uncovered.

14656 of 16533 relevant lines covered (88.65%)

24563.45 hits per line

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

90.83
/src/engine/Graphics/Context/debug-circle-renderer/debug-circle-renderer.ts
1
import { Color } from '../../../Color';
2
import type { Vector } from '../../../Math/vector';
3
import { vec } from '../../../Math/vector';
4
import { GraphicsDiagnostics } from '../../GraphicsDiagnostics';
5
import type { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL';
6
import { pixelSnapEpsilon } from '../ExcaliburGraphicsContextWebGL';
7
import { QuadIndexBuffer } from '../quad-index-buffer';
8
import type { RendererPlugin } from '../renderer';
9
import { Shader } from '../shader';
10
import { VertexBuffer } from '../vertex-buffer';
11
import { VertexLayout } from '../vertex-layout';
12

13
import frag from './debug-circle-renderer.frag.glsl?raw';
14
import vert from './debug-circle-renderer.vert.glsl?raw';
15

16
export class DebugCircleRenderer implements RendererPlugin {
17
  public readonly type = 'ex.debug-circle';
4✔
18
  public priority: number = 0;
4✔
19

20
  private _maxCircles: number = 10922; // max(uint16) / 6 verts
4✔
21

22
  private _shader!: Shader;
23
  private _context!: ExcaliburGraphicsContextWebGL;
24
  private _gl!: WebGLRenderingContext;
25
  private _buffer!: VertexBuffer;
26
  private _layout!: VertexLayout;
27
  private _quads!: QuadIndexBuffer;
28

29
  private _circleCount: number = 0;
4✔
30
  private _vertexIndex: number = 0;
4✔
31

32
  initialize(gl: WebGL2RenderingContext, context: ExcaliburGraphicsContextWebGL): void {
33
    this._gl = gl;
4✔
34
    this._context = context;
4✔
35
    this._shader = new Shader({
4✔
36
      graphicsContext: context,
37
      fragmentSource: frag,
38
      vertexSource: vert
39
    });
40
    this._shader.compile();
4✔
41

42
    // setup uniforms
43
    this._shader.use();
4✔
44
    this._shader.setUniformMatrix('u_matrix', context.ortho);
4✔
45

46
    this._buffer = new VertexBuffer({
4✔
47
      gl,
48
      size: 15 * 4 * this._maxCircles,
49
      type: 'dynamic'
50
    });
51

52
    this._layout = new VertexLayout({
4✔
53
      gl,
54
      shader: this._shader,
55
      vertexBuffer: this._buffer,
56
      attributes: [
57
        ['a_position', 2],
58
        ['a_uv', 2],
59
        ['a_opacity', 1],
60
        ['a_color', 4],
61
        ['a_strokeColor', 4],
62
        ['a_strokeThickness', 1],
63
        ['a_radius', 1]
64
      ]
65
    });
66

67
    this._quads = new QuadIndexBuffer(gl, this._maxCircles, true);
4✔
68
  }
69

70
  public dispose() {
71
    this._buffer.dispose();
4✔
72
    this._quads.dispose();
4✔
73
    this._shader.dispose();
4✔
74
    this._context = null as any;
4✔
75
    this._gl = null as any;
4✔
76
  }
77

78
  private _isFull() {
79
    if (this._circleCount >= this._maxCircles) {
229!
NEW
80
      return true;
×
81
    }
82
    return false;
229✔
83
  }
84

85
  draw(pos: Vector, radius: number, color: Color, stroke: Color = Color.Transparent, strokeThickness: number = 0): void {
454✔
86
    if (this._isFull()) {
229!
NEW
87
      this.flush();
×
88
    }
89
    this._circleCount++;
229✔
90

91
    // transform based on current context
92
    const transform = this._context.getTransform();
229✔
93
    const scale = transform.getScaleX();
229✔
94
    const opacity = this._context.opacity;
229✔
95
    const snapToPixel = this._context.snapToPixel;
229✔
96

97
    const topLeft = transform.multiply(pos.add(vec(-radius, -radius)));
229✔
98
    const topRight = transform.multiply(pos.add(vec(radius, -radius)));
229✔
99
    const bottomRight = transform.multiply(pos.add(vec(radius, radius)));
229✔
100
    const bottomLeft = transform.multiply(pos.add(vec(-radius, radius)));
229✔
101

102
    if (snapToPixel) {
229!
NEW
103
      topLeft.x = ~~(topLeft.x + pixelSnapEpsilon);
×
NEW
104
      topLeft.y = ~~(topLeft.y + pixelSnapEpsilon);
×
105

NEW
106
      topRight.x = ~~(topRight.x + pixelSnapEpsilon);
×
NEW
107
      topRight.y = ~~(topRight.y + pixelSnapEpsilon);
×
108

NEW
109
      bottomLeft.x = ~~(bottomLeft.x + pixelSnapEpsilon);
×
NEW
110
      bottomLeft.y = ~~(bottomLeft.y + pixelSnapEpsilon);
×
111

NEW
112
      bottomRight.x = ~~(bottomRight.x + pixelSnapEpsilon);
×
NEW
113
      bottomRight.y = ~~(bottomRight.y + pixelSnapEpsilon);
×
114
    }
115

116
    // TODO UV could be static vertex buffer
117
    const uvx0 = 0;
229✔
118
    const uvy0 = 0;
229✔
119
    const uvx1 = 1;
229✔
120
    const uvy1 = 1;
229✔
121

122
    // update data
123
    const vertexBuffer = this._layout.vertexBuffer.bufferData;
229✔
124

125
    // (0, 0) - 0
126
    vertexBuffer[this._vertexIndex++] = topLeft.x;
229✔
127
    vertexBuffer[this._vertexIndex++] = topLeft.y;
229✔
128
    vertexBuffer[this._vertexIndex++] = uvx0;
229✔
129
    vertexBuffer[this._vertexIndex++] = uvy0;
229✔
130
    vertexBuffer[this._vertexIndex++] = opacity;
229✔
131
    vertexBuffer[this._vertexIndex++] = color.r / 255;
229✔
132
    vertexBuffer[this._vertexIndex++] = color.g / 255;
229✔
133
    vertexBuffer[this._vertexIndex++] = color.b / 255;
229✔
134
    vertexBuffer[this._vertexIndex++] = color.a;
229✔
135
    vertexBuffer[this._vertexIndex++] = stroke.r / 255;
229✔
136
    vertexBuffer[this._vertexIndex++] = stroke.g / 255;
229✔
137
    vertexBuffer[this._vertexIndex++] = stroke.b / 255;
229✔
138
    vertexBuffer[this._vertexIndex++] = stroke.a;
229✔
139
    vertexBuffer[this._vertexIndex++] = strokeThickness;
229✔
140
    vertexBuffer[this._vertexIndex++] = radius * scale;
229✔
141

142
    // (0, 1) - 1
143
    vertexBuffer[this._vertexIndex++] = bottomLeft.x;
229✔
144
    vertexBuffer[this._vertexIndex++] = bottomLeft.y;
229✔
145
    vertexBuffer[this._vertexIndex++] = uvx0;
229✔
146
    vertexBuffer[this._vertexIndex++] = uvy1;
229✔
147
    vertexBuffer[this._vertexIndex++] = opacity;
229✔
148
    vertexBuffer[this._vertexIndex++] = color.r / 255;
229✔
149
    vertexBuffer[this._vertexIndex++] = color.g / 255;
229✔
150
    vertexBuffer[this._vertexIndex++] = color.b / 255;
229✔
151
    vertexBuffer[this._vertexIndex++] = color.a;
229✔
152
    vertexBuffer[this._vertexIndex++] = stroke.r / 255;
229✔
153
    vertexBuffer[this._vertexIndex++] = stroke.g / 255;
229✔
154
    vertexBuffer[this._vertexIndex++] = stroke.b / 255;
229✔
155
    vertexBuffer[this._vertexIndex++] = stroke.a;
229✔
156
    vertexBuffer[this._vertexIndex++] = strokeThickness;
229✔
157
    vertexBuffer[this._vertexIndex++] = radius * scale;
229✔
158

159
    // (1, 0) - 2
160
    vertexBuffer[this._vertexIndex++] = topRight.x;
229✔
161
    vertexBuffer[this._vertexIndex++] = topRight.y;
229✔
162
    vertexBuffer[this._vertexIndex++] = uvx1;
229✔
163
    vertexBuffer[this._vertexIndex++] = uvy0;
229✔
164
    vertexBuffer[this._vertexIndex++] = opacity;
229✔
165
    vertexBuffer[this._vertexIndex++] = color.r / 255;
229✔
166
    vertexBuffer[this._vertexIndex++] = color.g / 255;
229✔
167
    vertexBuffer[this._vertexIndex++] = color.b / 255;
229✔
168
    vertexBuffer[this._vertexIndex++] = color.a;
229✔
169
    vertexBuffer[this._vertexIndex++] = stroke.r / 255;
229✔
170
    vertexBuffer[this._vertexIndex++] = stroke.g / 255;
229✔
171
    vertexBuffer[this._vertexIndex++] = stroke.b / 255;
229✔
172
    vertexBuffer[this._vertexIndex++] = stroke.a;
229✔
173
    vertexBuffer[this._vertexIndex++] = strokeThickness;
229✔
174
    vertexBuffer[this._vertexIndex++] = radius * scale;
229✔
175

176
    // (1, 1) - 3
177
    vertexBuffer[this._vertexIndex++] = bottomRight.x;
229✔
178
    vertexBuffer[this._vertexIndex++] = bottomRight.y;
229✔
179
    vertexBuffer[this._vertexIndex++] = uvx1;
229✔
180
    vertexBuffer[this._vertexIndex++] = uvy1;
229✔
181
    vertexBuffer[this._vertexIndex++] = opacity;
229✔
182
    vertexBuffer[this._vertexIndex++] = color.r / 255;
229✔
183
    vertexBuffer[this._vertexIndex++] = color.g / 255;
229✔
184
    vertexBuffer[this._vertexIndex++] = color.b / 255;
229✔
185
    vertexBuffer[this._vertexIndex++] = color.a;
229✔
186
    vertexBuffer[this._vertexIndex++] = stroke.r / 255;
229✔
187
    vertexBuffer[this._vertexIndex++] = stroke.g / 255;
229✔
188
    vertexBuffer[this._vertexIndex++] = stroke.b / 255;
229✔
189
    vertexBuffer[this._vertexIndex++] = stroke.a;
229✔
190
    vertexBuffer[this._vertexIndex++] = strokeThickness;
229✔
191
    vertexBuffer[this._vertexIndex++] = radius * scale;
229✔
192
  }
193

194
  hasPendingDraws(): boolean {
195
    return this._circleCount !== 0;
2✔
196
  }
197

198
  flush(): void {
199
    // nothing to draw early exit
200
    if (this._circleCount === 0) {
4!
NEW
201
      return;
×
202
    }
203

204
    const gl = this._gl;
4✔
205
    // Bind the shader
206
    this._shader.use();
4✔
207

208
    // Bind the memory layout and upload data
209
    this._layout.use(true);
4✔
210

211
    // Update ortho matrix uniform
212
    this._shader.setUniformMatrix('u_matrix', this._context.ortho);
4✔
213

214
    // Bind index buffer
215
    this._quads.bind();
4✔
216

217
    // Draw all the quads
218
    gl.drawElements(gl.TRIANGLES, this._circleCount * 6, this._quads.bufferGlType, 0);
4✔
219

220
    GraphicsDiagnostics.DrawnImagesCount += this._circleCount;
4✔
221
    GraphicsDiagnostics.DrawCallCount++;
4✔
222

223
    // Reset
224
    this._circleCount = 0;
4✔
225
    this._vertexIndex = 0;
4✔
226
  }
227
}
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