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

excaliburjs / Excalibur / 19716547001

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

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%)

24557.82 hits per line

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

98.91
/src/engine/Graphics/Context/debug-line-renderer/debug-line-renderer.ts
1
import type { Vector } from '../../../Math/vector';
2
import { vec } from '../../../Math/vector';
3
import type { Color } from '../../../Color';
4
import lineVertexSource from './debug-line-vertex.glsl?raw';
5
import lineFragmentSource from './debug-line-fragment.glsl?raw';
6
import type { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL';
7
import type { RendererPlugin } from '../renderer';
8
import { Shader, VertexBuffer, VertexLayout } from '../..';
9
import { GraphicsDiagnostics } from '../../GraphicsDiagnostics';
10

11
export class DebugLineRenderer implements RendererPlugin {
12
  public readonly type = 'ex.debug-line';
12✔
13
  public priority: number = 0;
12✔
14
  private _context!: ExcaliburGraphicsContextWebGL;
15
  private _gl!: WebGL2RenderingContext;
16
  private _shader!: Shader;
17
  private _maxLines: number = 10922;
12✔
18
  private _vertexBuffer!: VertexBuffer;
19
  private _layout!: VertexLayout;
20
  private _vertexIndex = 0;
12✔
21
  private _lineCount = 0;
12✔
22
  initialize(gl: WebGL2RenderingContext, context: ExcaliburGraphicsContextWebGL): void {
23
    this._gl = gl;
12✔
24
    this._context = context;
12✔
25
    this._shader = new Shader({
12✔
26
      graphicsContext: context,
27
      vertexSource: lineVertexSource,
28
      fragmentSource: lineFragmentSource
29
    });
30
    this._shader.compile();
12✔
31
    this._shader.use();
12✔
32

33
    this._shader.setUniformMatrix('u_matrix', this._context.ortho);
12✔
34

35
    this._vertexBuffer = new VertexBuffer({
12✔
36
      gl,
37
      size: 7 * 6 * this._maxLines, // 7 floats per vert, 6 verts per line
38
      type: 'dynamic'
39
    });
40

41
    this._layout = new VertexLayout({
12✔
42
      gl,
43
      vertexBuffer: this._vertexBuffer,
44
      shader: this._shader,
45
      attributes: [
46
        ['a_position', 2],
47
        ['a_color', 4],
48
        ['a_lengthSoFar', 1]
49
      ]
50
    });
51
  }
52

53
  public dispose() {
54
    this._vertexBuffer.dispose();
12✔
55
    this._shader.dispose();
12✔
56
    this._context = null as any;
12✔
57
    this._gl = null as any;
12✔
58
  }
59

60
  private _startScratch = vec(0, 0);
12✔
61
  private _endScratch = vec(0, 0);
12✔
62
  private _lengthSoFar = 0;
12✔
63
  private _currentlyDashed = false;
12✔
64
  draw(start: Vector, end: Vector, color: Color, width = 2, dashed: boolean = false): void {
889✔
65
    // Force a render if the batch is full or we switch form dashed -> not dashed or vice versa
66
    if (this._isFull() || this._currentlyDashed !== dashed) {
471✔
67
      this._currentlyDashed = dashed;
2✔
68
      this.flush();
2✔
69
    }
70

71
    this._lineCount++;
471✔
72

73
    const transform = this._context.getTransform();
471✔
74
    const finalStart = transform.multiply(start, this._startScratch);
471✔
75
    const finalEnd = transform.multiply(end, this._endScratch);
471✔
76

77
    const dir = finalEnd.sub(finalStart);
471✔
78
    const dist = finalStart.distance(finalEnd);
471✔
79
    const normal = dir.normal();
471✔
80
    const halfWidth = width / 2;
471✔
81

82
    const vertexBuffer = this._vertexBuffer.bufferData;
471✔
83
    // Start Bottom Vert
84
    vertexBuffer[this._vertexIndex++] = finalStart.x - normal.x * halfWidth;
471✔
85
    vertexBuffer[this._vertexIndex++] = finalStart.y - normal.y * halfWidth;
471✔
86
    vertexBuffer[this._vertexIndex++] = color.r / 255;
471✔
87
    vertexBuffer[this._vertexIndex++] = color.g / 255;
471✔
88
    vertexBuffer[this._vertexIndex++] = color.b / 255;
471✔
89
    vertexBuffer[this._vertexIndex++] = color.a;
471✔
90
    vertexBuffer[this._vertexIndex++] = this._lengthSoFar;
471✔
91

92
    // Start Top Vert
93
    vertexBuffer[this._vertexIndex++] = finalStart.x + normal.x * halfWidth;
471✔
94
    vertexBuffer[this._vertexIndex++] = finalStart.y + normal.y * halfWidth;
471✔
95
    vertexBuffer[this._vertexIndex++] = color.r / 255;
471✔
96
    vertexBuffer[this._vertexIndex++] = color.g / 255;
471✔
97
    vertexBuffer[this._vertexIndex++] = color.b / 255;
471✔
98
    vertexBuffer[this._vertexIndex++] = color.a;
471✔
99
    vertexBuffer[this._vertexIndex++] = this._lengthSoFar;
471✔
100

101
    // End Bottom Vert
102
    vertexBuffer[this._vertexIndex++] = finalEnd.x - normal.x * halfWidth;
471✔
103
    vertexBuffer[this._vertexIndex++] = finalEnd.y - normal.y * halfWidth;
471✔
104
    vertexBuffer[this._vertexIndex++] = color.r / 255;
471✔
105
    vertexBuffer[this._vertexIndex++] = color.g / 255;
471✔
106
    vertexBuffer[this._vertexIndex++] = color.b / 255;
471✔
107
    vertexBuffer[this._vertexIndex++] = color.a;
471✔
108
    vertexBuffer[this._vertexIndex++] = this._lengthSoFar + dist;
471✔
109

110
    // End Bottom Vert
111
    vertexBuffer[this._vertexIndex++] = finalEnd.x - normal.x * halfWidth;
471✔
112
    vertexBuffer[this._vertexIndex++] = finalEnd.y - normal.y * halfWidth;
471✔
113
    vertexBuffer[this._vertexIndex++] = color.r / 255;
471✔
114
    vertexBuffer[this._vertexIndex++] = color.g / 255;
471✔
115
    vertexBuffer[this._vertexIndex++] = color.b / 255;
471✔
116
    vertexBuffer[this._vertexIndex++] = color.a;
471✔
117
    vertexBuffer[this._vertexIndex++] = this._lengthSoFar + dist;
471✔
118

119
    // Start Top Vert
120
    vertexBuffer[this._vertexIndex++] = finalStart.x + normal.x * halfWidth;
471✔
121
    vertexBuffer[this._vertexIndex++] = finalStart.y + normal.y * halfWidth;
471✔
122
    vertexBuffer[this._vertexIndex++] = color.r / 255;
471✔
123
    vertexBuffer[this._vertexIndex++] = color.g / 255;
471✔
124
    vertexBuffer[this._vertexIndex++] = color.b / 255;
471✔
125
    vertexBuffer[this._vertexIndex++] = color.a;
471✔
126
    vertexBuffer[this._vertexIndex++] = this._lengthSoFar;
471✔
127

128
    // End Top Vert
129
    vertexBuffer[this._vertexIndex++] = finalEnd.x + normal.x * halfWidth;
471✔
130
    vertexBuffer[this._vertexIndex++] = finalEnd.y + normal.y * halfWidth;
471✔
131
    vertexBuffer[this._vertexIndex++] = color.r / 255;
471✔
132
    vertexBuffer[this._vertexIndex++] = color.g / 255;
471✔
133
    vertexBuffer[this._vertexIndex++] = color.b / 255;
471✔
134
    vertexBuffer[this._vertexIndex++] = color.a;
471✔
135
    vertexBuffer[this._vertexIndex++] = this._lengthSoFar + dist;
471✔
136
  }
137

138
  private _isFull() {
139
    if (this._lineCount >= this._maxLines) {
471!
NEW
140
      return true;
×
141
    }
142
    return false;
471✔
143
  }
144

145
  hasPendingDraws(): boolean {
146
    return this._lineCount !== 0;
8✔
147
  }
148

149
  flush(): void {
150
    // nothing to draw early exit
151
    if (this._lineCount === 0) {
14✔
152
      return;
2✔
153
    }
154

155
    const gl = this._gl;
12✔
156
    this._shader.use();
12✔
157
    this._layout.use(true);
12✔
158

159
    this._shader.setUniformMatrix('u_matrix', this._context.ortho);
12✔
160
    this._shader.setUniformBoolean('u_dashed', this._currentlyDashed);
12✔
161

162
    gl.drawArrays(gl.TRIANGLES, 0, this._lineCount * 6); // 6 verts per line
12✔
163

164
    GraphicsDiagnostics.DrawnImagesCount += this._lineCount;
12✔
165
    GraphicsDiagnostics.DrawCallCount++;
12✔
166

167
    // reset
168
    this._vertexIndex = 0;
12✔
169
    this._lineCount = 0;
12✔
170
    this._lengthSoFar = 0;
12✔
171
  }
172
}
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