@forgeax/engine-debug-draw 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +256 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/errors.test-d.d.ts +2 -0
  5. package/dist/__tests__/errors.test-d.d.ts.map +1 -0
  6. package/dist/constants.d.ts +7 -0
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/debug-draw.d.ts +65 -0
  9. package/dist/debug-draw.d.ts.map +1 -0
  10. package/dist/errors.d.ts +65 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.mjs +835 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/shapes/aabb.d.ts +4 -0
  17. package/dist/shapes/aabb.d.ts.map +1 -0
  18. package/dist/shapes/arrow.d.ts +11 -0
  19. package/dist/shapes/arrow.d.ts.map +1 -0
  20. package/dist/shapes/axes.d.ts +18 -0
  21. package/dist/shapes/axes.d.ts.map +1 -0
  22. package/dist/shapes/frustum.d.ts +7 -0
  23. package/dist/shapes/frustum.d.ts.map +1 -0
  24. package/dist/shapes/line.d.ts +4 -0
  25. package/dist/shapes/line.d.ts.map +1 -0
  26. package/dist/shapes/sphere.d.ts +7 -0
  27. package/dist/shapes/sphere.d.ts.map +1 -0
  28. package/dist/types.d.ts +136 -0
  29. package/dist/types.d.ts.map +1 -0
  30. package/package.json +61 -0
  31. package/src/__tests__/errors.test-d.ts +89 -0
  32. package/src/constants.ts +13 -0
  33. package/src/debug-draw.ts +713 -0
  34. package/src/errors.ts +142 -0
  35. package/src/index.ts +14 -0
  36. package/src/shapes/aabb.ts +57 -0
  37. package/src/shapes/arrow.ts +75 -0
  38. package/src/shapes/axes.ts +60 -0
  39. package/src/shapes/frustum.ts +182 -0
  40. package/src/shapes/line.ts +14 -0
  41. package/src/shapes/sphere.ts +74 -0
  42. package/src/types.ts +176 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,835 @@
1
+ import { ok, err } from '@forgeax/engine-types';
2
+ import { mat4, vec3, quat } from '@forgeax/engine-math';
3
+
4
+ // src/constants.ts
5
+ var INITIAL_VERTEX_CAPACITY = 1024;
6
+ var MAX_VERTEX_CAPACITY = 1e6;
7
+ var VERTEX_STRIDE_BYTES = 16;
8
+ function makeError(code, expected, hint, detail) {
9
+ const error = {
10
+ code,
11
+ expected,
12
+ hint,
13
+ detail,
14
+ get message() {
15
+ return `[${code}] ${hint}`;
16
+ }
17
+ };
18
+ return error;
19
+ }
20
+ function pipelineCreateFailed(rhiError) {
21
+ return err(
22
+ makeError(
23
+ "pipeline-create-failed",
24
+ "PSO creation should succeed with valid WGSL + layout",
25
+ `Pipeline creation failed: ${rhiError}. Check WGSL syntax, vertex layout, and depth-stencil state.`,
26
+ { code: "pipeline-create-failed", rhiError }
27
+ )
28
+ );
29
+ }
30
+ function bufferAllocationFailed(rhiError) {
31
+ return err(
32
+ makeError(
33
+ "buffer-allocation-failed",
34
+ "GPU vertex buffer allocation should succeed for the requested byte size",
35
+ `Buffer allocation failed: ${rhiError}. Check available device memory and buffer usage flags.`,
36
+ { code: "buffer-allocation-failed", rhiError }
37
+ )
38
+ );
39
+ }
40
+ function flushedAfterDestroy() {
41
+ return err(
42
+ makeError(
43
+ "flushed-after-destroy",
44
+ "DebugDraw instance is alive and not yet destroyed",
45
+ "DebugDraw was destroyed; create a new instance via createDebugDraw().",
46
+ { code: "flushed-after-destroy" }
47
+ )
48
+ );
49
+ }
50
+ function viewProjRequired() {
51
+ return err(
52
+ makeError(
53
+ "viewProj-required",
54
+ "viewProj must be provided as a Mat4 for flush to transform vertices",
55
+ "Pass a viewProj Mat4 to flush(encoder, view, viewProj).",
56
+ { code: "viewProj-required" }
57
+ )
58
+ );
59
+ }
60
+
61
+ // src/shapes/aabb.ts
62
+ function a(v, i) {
63
+ return v[i];
64
+ }
65
+ function aabbVertices(min, max) {
66
+ const mnx = a(min, 0);
67
+ const mny = a(min, 1);
68
+ const mnz = a(min, 2);
69
+ const mxx = a(max, 0);
70
+ const mxy = a(max, 1);
71
+ const mxz = a(max, 2);
72
+ const c = [
73
+ [mnx, mny, mnz],
74
+ // 0
75
+ [mxx, mny, mnz],
76
+ // 1
77
+ [mnx, mxy, mnz],
78
+ // 2
79
+ [mxx, mxy, mnz],
80
+ // 3
81
+ [mnx, mny, mxz],
82
+ // 4
83
+ [mxx, mny, mxz],
84
+ // 5
85
+ [mnx, mxy, mxz],
86
+ // 6
87
+ [mxx, mxy, mxz]
88
+ // 7
89
+ ];
90
+ const edges = [
91
+ [0, 1],
92
+ [0, 2],
93
+ [1, 3],
94
+ [2, 3],
95
+ [4, 5],
96
+ [4, 6],
97
+ [5, 7],
98
+ [6, 7],
99
+ [0, 4],
100
+ [1, 5],
101
+ [2, 6],
102
+ [3, 7]
103
+ ];
104
+ const result = [];
105
+ for (const [ai, bi] of edges) {
106
+ const ac = c[ai];
107
+ const bc = c[bi];
108
+ result.push([ac[0], ac[1], ac[2]]);
109
+ result.push([bc[0], bc[1], bc[2]]);
110
+ }
111
+ return result;
112
+ }
113
+ var TIP_DIRS = [
114
+ [-1, 1, 0],
115
+ [-1, 0, 1],
116
+ [-1, -1, 0],
117
+ [-1, 0, -1]
118
+ ];
119
+ var UNIT_X = [1, 0, 0];
120
+ function arrowVertices(start, end, tipLength) {
121
+ const sx = start[0];
122
+ const sy = start[1];
123
+ const sz = start[2];
124
+ const ex = end[0];
125
+ const ey = end[1];
126
+ const ez = end[2];
127
+ const verts = [
128
+ [sx, sy, sz],
129
+ [ex, ey, ez]
130
+ ];
131
+ const dir = vec3.create();
132
+ vec3.set(dir, ex - sx, ey - sy, ez - sz);
133
+ const len = vec3.length(dir);
134
+ if (len < 1e-6) return verts;
135
+ const headLen = tipLength ?? len / 10;
136
+ vec3.normalize(dir, dir);
137
+ const rot = quat.fromUnitVectors(quat.create(), UNIT_X, dir);
138
+ const tipLocal = vec3.create();
139
+ const tipWorld = vec3.create();
140
+ for (const [tx, ty, tz] of TIP_DIRS) {
141
+ vec3.set(tipLocal, tx, ty, tz);
142
+ vec3.normalize(tipLocal, tipLocal);
143
+ vec3.scale(tipLocal, tipLocal, headLen);
144
+ quat.transformVec3(tipWorld, rot, tipLocal);
145
+ verts.push([ex, ey, ez]);
146
+ verts.push([
147
+ ex + tipWorld[0],
148
+ ey + tipWorld[1],
149
+ ez + tipWorld[2]
150
+ ]);
151
+ }
152
+ return verts;
153
+ }
154
+
155
+ // src/shapes/axes.ts
156
+ var AXES_COLORS = [
157
+ [1, 0, 0, 1],
158
+ [0, 1, 0, 1],
159
+ [0, 0, 1, 1]
160
+ ];
161
+ function axesArrowSets(worldMat, length) {
162
+ const m = worldMat;
163
+ const ox = m[12];
164
+ const oy = m[13];
165
+ const oz = m[14];
166
+ const origin = [ox, oy, oz];
167
+ const cols = [
168
+ [m[0], m[1], m[2]],
169
+ [m[4], m[5], m[6]],
170
+ [m[8], m[9], m[10]]
171
+ ];
172
+ const sets = [];
173
+ for (let i = 0; i < 3; i++) {
174
+ const c = cols[i];
175
+ const color = AXES_COLORS[i];
176
+ const end = [ox + c[0] * length, oy + c[1] * length, oz + c[2] * length];
177
+ sets.push({ vertices: arrowVertices(origin, end), color });
178
+ }
179
+ return sets;
180
+ }
181
+ function at(m, i) {
182
+ return m[i];
183
+ }
184
+ function frustumVertices(viewProj) {
185
+ const m = viewProj;
186
+ const det = at(m, 0) * (at(m, 5) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) - at(m, 9) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) + at(m, 13) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7))) - at(m, 4) * (at(m, 1) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) - at(m, 9) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) + at(m, 13) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3))) + at(m, 8) * (at(m, 1) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) - at(m, 5) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) + at(m, 13) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))) - at(m, 12) * (at(m, 1) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7)) - at(m, 5) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3)) + at(m, 9) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3)));
187
+ if (Math.abs(det) < 1e-10) {
188
+ return null;
189
+ }
190
+ const invDet = 1 / det;
191
+ const inv = mat4.create();
192
+ inv[0] = (at(m, 5) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) - at(m, 9) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) + at(m, 13) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7))) * invDet;
193
+ inv[1] = -(at(m, 1) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) - at(m, 9) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) + at(m, 13) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3))) * invDet;
194
+ inv[2] = (at(m, 1) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) - at(m, 5) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) + at(m, 13) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))) * invDet;
195
+ inv[3] = -(at(m, 1) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7)) - at(m, 5) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3)) + at(m, 9) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))) * invDet;
196
+ inv[4] = -(at(m, 4) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) - at(m, 8) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) + at(m, 12) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7))) * invDet;
197
+ inv[5] = (at(m, 0) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) - at(m, 8) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) + at(m, 12) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3))) * invDet;
198
+ inv[6] = -(at(m, 0) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) - at(m, 4) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) + at(m, 12) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))) * invDet;
199
+ inv[7] = (at(m, 0) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7)) - at(m, 4) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3)) + at(m, 8) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))) * invDet;
200
+ inv[8] = (at(m, 4) * (at(m, 9) * at(m, 15) - at(m, 13) * at(m, 11)) - at(m, 8) * (at(m, 5) * at(m, 15) - at(m, 13) * at(m, 7)) + at(m, 12) * (at(m, 5) * at(m, 11) - at(m, 9) * at(m, 7))) * invDet;
201
+ inv[9] = -(at(m, 0) * (at(m, 9) * at(m, 15) - at(m, 13) * at(m, 11)) - at(m, 8) * (at(m, 1) * at(m, 15) - at(m, 13) * at(m, 3)) + at(m, 12) * (at(m, 1) * at(m, 11) - at(m, 9) * at(m, 3))) * invDet;
202
+ inv[10] = (at(m, 0) * (at(m, 5) * at(m, 15) - at(m, 13) * at(m, 7)) - at(m, 4) * (at(m, 1) * at(m, 15) - at(m, 13) * at(m, 3)) + at(m, 12) * (at(m, 1) * at(m, 7) - at(m, 5) * at(m, 3))) * invDet;
203
+ inv[11] = -(at(m, 0) * (at(m, 5) * at(m, 11) - at(m, 9) * at(m, 7)) - at(m, 4) * (at(m, 1) * at(m, 11) - at(m, 9) * at(m, 3)) + at(m, 8) * (at(m, 1) * at(m, 7) - at(m, 5) * at(m, 3))) * invDet;
204
+ inv[12] = -(at(m, 4) * (at(m, 9) * at(m, 14) - at(m, 13) * at(m, 10)) - at(m, 8) * (at(m, 5) * at(m, 14) - at(m, 13) * at(m, 6)) + at(m, 12) * (at(m, 5) * at(m, 10) - at(m, 9) * at(m, 6))) * invDet;
205
+ inv[13] = (at(m, 0) * (at(m, 9) * at(m, 14) - at(m, 13) * at(m, 10)) - at(m, 8) * (at(m, 1) * at(m, 14) - at(m, 13) * at(m, 2)) + at(m, 12) * (at(m, 1) * at(m, 10) - at(m, 9) * at(m, 2))) * invDet;
206
+ inv[14] = -(at(m, 0) * (at(m, 5) * at(m, 14) - at(m, 13) * at(m, 6)) - at(m, 4) * (at(m, 1) * at(m, 14) - at(m, 13) * at(m, 2)) + at(m, 12) * (at(m, 1) * at(m, 6) - at(m, 5) * at(m, 2))) * invDet;
207
+ inv[15] = (at(m, 0) * (at(m, 5) * at(m, 10) - at(m, 9) * at(m, 6)) - at(m, 4) * (at(m, 1) * at(m, 10) - at(m, 9) * at(m, 2)) + at(m, 8) * (at(m, 1) * at(m, 6) - at(m, 5) * at(m, 2))) * invDet;
208
+ const ndc = [
209
+ [-1, -1, 0, 1],
210
+ [1, -1, 0, 1],
211
+ [-1, 1, 0, 1],
212
+ [1, 1, 0, 1],
213
+ [-1, -1, 1, 1],
214
+ [1, -1, 1, 1],
215
+ [-1, 1, 1, 1],
216
+ [1, 1, 1, 1]
217
+ ];
218
+ const corners = ndc.map(([nx, ny, nz, nw]) => {
219
+ const cx = at(inv, 0) * nx + at(inv, 4) * ny + at(inv, 8) * nz + at(inv, 12) * nw;
220
+ const cy = at(inv, 1) * nx + at(inv, 5) * ny + at(inv, 9) * nz + at(inv, 13) * nw;
221
+ const cz = at(inv, 2) * nx + at(inv, 6) * ny + at(inv, 10) * nz + at(inv, 14) * nw;
222
+ const cw = at(inv, 3) * nx + at(inv, 7) * ny + at(inv, 11) * nz + at(inv, 15) * nw;
223
+ const iw = 1 / cw;
224
+ return [cx * iw, cy * iw, cz * iw];
225
+ });
226
+ const edges = [
227
+ [0, 1],
228
+ [0, 2],
229
+ [1, 3],
230
+ [2, 3],
231
+ [4, 5],
232
+ [4, 6],
233
+ [5, 7],
234
+ [6, 7],
235
+ [0, 4],
236
+ [1, 5],
237
+ [2, 6],
238
+ [3, 7]
239
+ ];
240
+ const result = [];
241
+ for (const [ai, bi] of edges) {
242
+ const ac = corners[ai];
243
+ const bc = corners[bi];
244
+ result.push([ac[0], ac[1], ac[2]]);
245
+ result.push([bc[0], bc[1], bc[2]]);
246
+ }
247
+ return result;
248
+ }
249
+
250
+ // src/shapes/line.ts
251
+ function lineVertices(a3, b) {
252
+ return [
253
+ [a3[0], a3[1], a3[2]],
254
+ [b[0], b[1], b[2]]
255
+ ];
256
+ }
257
+
258
+ // src/shapes/sphere.ts
259
+ function a2(v, i) {
260
+ return v[i];
261
+ }
262
+ function sphereVertices(center, radius, segments) {
263
+ const cx = a2(center, 0);
264
+ const cy = a2(center, 1);
265
+ const cz = a2(center, 2);
266
+ const step = 2 * Math.PI / segments;
267
+ const result = [];
268
+ for (let plane = 0; plane < 3; plane++) {
269
+ for (let i = 0; i < segments; i++) {
270
+ const angle0 = i * step;
271
+ const angle1 = (i + 1) % segments;
272
+ let p0x;
273
+ let p0y;
274
+ let p0z;
275
+ let p1x;
276
+ let p1y;
277
+ let p1z;
278
+ if (plane === 0) {
279
+ p0x = cx + radius * Math.cos(angle0);
280
+ p0y = cy + radius * Math.sin(angle0);
281
+ p0z = cz;
282
+ p1x = cx + radius * Math.cos(angle1);
283
+ p1y = cy + radius * Math.sin(angle1);
284
+ p1z = cz;
285
+ } else if (plane === 1) {
286
+ p0x = cx + radius * Math.cos(angle0);
287
+ p0y = cy;
288
+ p0z = cz + radius * Math.sin(angle0);
289
+ p1x = cx + radius * Math.cos(angle1);
290
+ p1y = cy;
291
+ p1z = cz + radius * Math.sin(angle1);
292
+ } else {
293
+ p0x = cx;
294
+ p0y = cy + radius * Math.cos(angle0);
295
+ p0z = cz + radius * Math.sin(angle0);
296
+ p1x = cx;
297
+ p1y = cy + radius * Math.cos(angle1);
298
+ p1z = cz + radius * Math.sin(angle1);
299
+ }
300
+ result.push([p0x, p0y, p0z]);
301
+ result.push([p1x, p1y, p1z]);
302
+ }
303
+ }
304
+ return result;
305
+ }
306
+
307
+ // src/debug-draw.ts
308
+ var VERTEX_SHADER = (
309
+ /* wgsl */
310
+ `
311
+ struct VertexInput {
312
+ @location(0) position: vec3<f32>,
313
+ @location(1) color: vec4<f32>,
314
+ }
315
+
316
+ struct VertexOutput {
317
+ @builtin(position) position: vec4<f32>,
318
+ @location(0) color: vec4<f32>,
319
+ }
320
+
321
+ struct Uniforms {
322
+ viewProj: mat4x4<f32>,
323
+ }
324
+
325
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
326
+
327
+ @vertex
328
+ fn vs_main(in: VertexInput) -> VertexOutput {
329
+ var out: VertexOutput;
330
+ out.position = uniforms.viewProj * vec4<f32>(in.position, 1.0);
331
+ out.color = in.color;
332
+ return out;
333
+ }
334
+ `
335
+ );
336
+ var FRAGMENT_SHADER = (
337
+ /* wgsl */
338
+ `
339
+ struct FragmentInput {
340
+ @location(0) color: vec4<f32>,
341
+ }
342
+
343
+ @fragment
344
+ fn fs_main(in: FragmentInput) -> @location(0) vec4<f32> {
345
+ return in.color;
346
+ }
347
+ `
348
+ );
349
+ function at2(a3, i) {
350
+ return a3[i];
351
+ }
352
+ function normalizeCapacity(value, fallback) {
353
+ const finiteValue = Number.isFinite(value) ? Math.floor(value) : fallback;
354
+ return Math.max(1, finiteValue);
355
+ }
356
+ var DebugDraw = class {
357
+ stagingArr;
358
+ stagingLen = 0;
359
+ lastFlushedVertexCount = 0;
360
+ capVal;
361
+ gpuVbo = null;
362
+ gpuPipeline = null;
363
+ gpuUniformBuffer = null;
364
+ gpuBindGroup = null;
365
+ maxCapVal;
366
+ rhiDevice;
367
+ isDestroyed = false;
368
+ // Whether the destroy-after-shape warning has been emitted (plan-strategy D-11).
369
+ // private (no underscore, no @internal) — purely class-internal state, not part of
370
+ // package-internal API surface. Biome R-internal-A forbids `_x` on private fields;
371
+ // lint:internal R-internal-C requires `_x` for `@internal`. Drop both markers since
372
+ // there is no package-internal use for this field — accessing it from outside the
373
+ // class is meaningless.
374
+ destroyedWarnedOnce = false;
375
+ // Hard-cap diagnostics are per frame: flush() clears this flag with staging.
376
+ truncationWarned = false;
377
+ /**
378
+ * Depth texture view for less-equal depth mode.
379
+ * Set via {@link _setDepthView} before flush() when depthMode is 'less-equal'.
380
+ * The runtime auto-attach path receives depth from the render-graph context;
381
+ * low-path callers (test harnesses, smoke runners) set this explicitly.
382
+ */
383
+ depthView = null;
384
+ constructor(device, pipeline, vbo, uniformBuffer, bindGroup, initialCapacity, maxCapacity) {
385
+ const boundedMaxCapacity = normalizeCapacity(maxCapacity, MAX_VERTEX_CAPACITY);
386
+ const boundedInitialCapacity = Math.min(
387
+ normalizeCapacity(initialCapacity, INITIAL_VERTEX_CAPACITY),
388
+ boundedMaxCapacity
389
+ );
390
+ this.rhiDevice = device;
391
+ this.gpuPipeline = pipeline;
392
+ this.gpuVbo = vbo;
393
+ this.gpuUniformBuffer = uniformBuffer;
394
+ this.gpuBindGroup = bindGroup;
395
+ this.capVal = boundedInitialCapacity;
396
+ this.maxCapVal = boundedMaxCapacity;
397
+ this.stagingArr = new Float32Array(boundedInitialCapacity * (VERTEX_STRIDE_BYTES / 4));
398
+ }
399
+ /** @internal CPU staging vertex count (exposed for unit tests). */
400
+ get _stagingVertexCount() {
401
+ return this.stagingLen;
402
+ }
403
+ hasPendingWork() {
404
+ return !this.isDestroyed && this.stagingLen > 0;
405
+ }
406
+ /** @internal Vertex count passed to the most recent non-empty draw call. */
407
+ get _lastFlushVertexCount() {
408
+ return this.lastFlushedVertexCount;
409
+ }
410
+ /** @internal Current GPU vertex buffer capacity in vertex count. */
411
+ get _capacity() {
412
+ return this.capVal;
413
+ }
414
+ /** @internal Whether destroy() has been called. */
415
+ get _destroyed() {
416
+ return this.isDestroyed;
417
+ }
418
+ /**
419
+ * @internal Set the depth texture view for less-equal depth mode.
420
+ * Required before flush() when depthMode is 'less-equal'.
421
+ * Used by test harnesses and smoke runners that don't have a scene
422
+ * depth buffer; the runtime auto-attach path receives depth from
423
+ * the render-graph context.
424
+ */
425
+ _setDepthView(view) {
426
+ this.depthView = view;
427
+ }
428
+ /** @internal Read position of vertex at `index` in CPU staging (for unit tests). */
429
+ _getVertexPosition(index) {
430
+ const idx = index * 4;
431
+ return [
432
+ this.stagingArr[idx + 0],
433
+ this.stagingArr[idx + 1],
434
+ this.stagingArr[idx + 2]
435
+ ];
436
+ }
437
+ /** @internal Read color of vertex at `index` as packed u32 (for unit tests). */
438
+ _getVertexPackedColor(index) {
439
+ const byteOff = index * VERTEX_STRIDE_BYTES + 12;
440
+ const bytes = new Uint8Array(this.stagingArr.buffer, byteOff, 4);
441
+ return bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
442
+ }
443
+ postDestroyWarnOnce() {
444
+ if (!this.destroyedWarnedOnce) {
445
+ this.destroyedWarnedOnce = true;
446
+ console.warn(
447
+ "[DebugDraw] Shape call after destroy() is a no-op. Create a new instance via createDebugDraw()."
448
+ );
449
+ }
450
+ }
451
+ pushVertex(px, py, pz, rc, gc, bc, ac) {
452
+ if (this.isDestroyed) {
453
+ this.postDestroyWarnOnce();
454
+ return;
455
+ }
456
+ if (this.stagingLen >= this.maxCapVal) return;
457
+ const idx = this.stagingLen * 4;
458
+ this.stagingArr[idx + 0] = px;
459
+ this.stagingArr[idx + 1] = py;
460
+ this.stagingArr[idx + 2] = pz;
461
+ const u8r = Math.round(Math.max(0, Math.min(1, rc)) * 255);
462
+ const u8g = Math.round(Math.max(0, Math.min(1, gc)) * 255);
463
+ const u8bc = Math.round(Math.max(0, Math.min(1, bc)) * 255);
464
+ const u8a = Math.round(Math.max(0, Math.min(1, ac)) * 255);
465
+ const byteOff = this.stagingLen * VERTEX_STRIDE_BYTES + 12;
466
+ const colorView = new Uint8Array(this.stagingArr.buffer, byteOff, 4);
467
+ colorView[0] = u8r;
468
+ colorView[1] = u8g;
469
+ colorView[2] = u8bc;
470
+ colorView[3] = u8a;
471
+ this.stagingLen++;
472
+ }
473
+ warnTruncationOnce() {
474
+ if (this.truncationWarned) return;
475
+ this.truncationWarned = true;
476
+ console.warn(
477
+ `[DebugDraw] Vertex count would exceed MAX_VERTEX_CAPACITY=${this.maxCapVal}; vertices beyond the limit are discarded.`
478
+ );
479
+ }
480
+ ensureCapacity(needed) {
481
+ if (this.isDestroyed) return;
482
+ if (needed <= this.capVal) return;
483
+ if (needed > this.maxCapVal) {
484
+ this.warnTruncationOnce();
485
+ }
486
+ let newCap = this.capVal;
487
+ while (newCap < needed && newCap < this.maxCapVal) {
488
+ newCap = Math.min(newCap * 2, this.maxCapVal);
489
+ }
490
+ if (newCap > this.capVal) {
491
+ const newVbo = this.rhiDevice.createBuffer({
492
+ size: newCap * VERTEX_STRIDE_BYTES,
493
+ usage: 8 | 32,
494
+ // COPY_DST | VERTEX (mirrors createDebugDraw factory)
495
+ label: "debug-draw-vbo"
496
+ });
497
+ if (!newVbo.ok) {
498
+ console.warn(
499
+ `[DebugDraw] GPU vertex buffer grow to ${newCap} failed (${newVbo.error.code}); keeping ${this.capVal} -- excess vertices are truncated this frame.`
500
+ );
501
+ return;
502
+ }
503
+ console.warn(`[DebugDraw] Resizing vertex buffer from ${this.capVal} to ${newCap} vertices.`);
504
+ if (this.gpuVbo !== null) this.rhiDevice.destroyBuffer(this.gpuVbo);
505
+ this.gpuVbo = newVbo.value;
506
+ this.capVal = newCap;
507
+ const newStaging = new Float32Array(newCap * (VERTEX_STRIDE_BYTES / 4));
508
+ newStaging.set(this.stagingArr.subarray(0, this.stagingLen * 4));
509
+ this.stagingArr = newStaging;
510
+ }
511
+ }
512
+ colorToRGBA(color) {
513
+ if (Array.isArray(color)) {
514
+ return [at2(color, 0), at2(color, 1), at2(color, 2), color[3] ?? 1];
515
+ }
516
+ return [at2(color, 0), at2(color, 1), at2(color, 2), color[3] ?? 1];
517
+ }
518
+ // -- Public shape API --
519
+ line(a3, b, color) {
520
+ if (this.isDestroyed) {
521
+ this.postDestroyWarnOnce();
522
+ return;
523
+ }
524
+ const [r, g, bc, alpha] = this.colorToRGBA(color);
525
+ this.ensureCapacity(this.stagingLen + 2);
526
+ for (const [x, y, z] of lineVertices(a3, b)) {
527
+ this.pushVertex(x, y, z, r, g, bc, alpha);
528
+ }
529
+ }
530
+ aabb(min, max, color) {
531
+ if (this.isDestroyed) {
532
+ this.postDestroyWarnOnce();
533
+ return;
534
+ }
535
+ const [r, g, bc, alpha] = this.colorToRGBA(color);
536
+ const verts = aabbVertices(min, max);
537
+ this.ensureCapacity(this.stagingLen + verts.length);
538
+ for (const [x, y, z] of verts) {
539
+ this.pushVertex(x, y, z, r, g, bc, alpha);
540
+ }
541
+ }
542
+ sphere(center, radius, color, segments = 16) {
543
+ if (this.isDestroyed) {
544
+ this.postDestroyWarnOnce();
545
+ return;
546
+ }
547
+ const [r, g, bc, alpha] = this.colorToRGBA(color);
548
+ const verts = sphereVertices(center, radius, segments);
549
+ this.ensureCapacity(this.stagingLen + verts.length);
550
+ for (const [x, y, z] of verts) {
551
+ this.pushVertex(x, y, z, r, g, bc, alpha);
552
+ }
553
+ }
554
+ frustum(viewProj, color) {
555
+ if (this.isDestroyed) {
556
+ this.postDestroyWarnOnce();
557
+ return;
558
+ }
559
+ const verts = frustumVertices(viewProj);
560
+ if (verts === null) {
561
+ console.warn(
562
+ "[DebugDraw] frustum() received a near-singular viewProj matrix; skipping this frame."
563
+ );
564
+ return;
565
+ }
566
+ const [r, g, bc, alpha] = this.colorToRGBA(color);
567
+ this.ensureCapacity(this.stagingLen + verts.length);
568
+ for (const [x, y, z] of verts) {
569
+ this.pushVertex(x, y, z, r, g, bc, alpha);
570
+ }
571
+ }
572
+ arrow(start, end, color, tipLength) {
573
+ if (this.isDestroyed) {
574
+ this.postDestroyWarnOnce();
575
+ return;
576
+ }
577
+ const [r, g, bc, alpha] = this.colorToRGBA(color);
578
+ const verts = arrowVertices(start, end, tipLength);
579
+ this.ensureCapacity(this.stagingLen + verts.length);
580
+ for (const [x, y, z] of verts) {
581
+ this.pushVertex(x, y, z, r, g, bc, alpha);
582
+ }
583
+ }
584
+ axes(worldMat, length) {
585
+ if (this.isDestroyed) {
586
+ this.postDestroyWarnOnce();
587
+ return;
588
+ }
589
+ for (const { vertices, color } of axesArrowSets(worldMat, length)) {
590
+ const [r, g, bc, alpha] = this.colorToRGBA(color);
591
+ this.ensureCapacity(this.stagingLen + vertices.length);
592
+ for (const [x, y, z] of vertices) {
593
+ this.pushVertex(x, y, z, r, g, bc, alpha);
594
+ }
595
+ }
596
+ }
597
+ // -- flush (w13) --
598
+ flush(encoder, view, viewProj) {
599
+ if (this.isDestroyed) return flushedAfterDestroy();
600
+ if (viewProj === void 0 || viewProj === null) return viewProjRequired();
601
+ if (this.stagingLen === 0) {
602
+ this.lastFlushedVertexCount = 0;
603
+ return ok(void 0);
604
+ }
605
+ const passDesc = {
606
+ colorAttachments: [
607
+ {
608
+ // biome-ignore lint/suspicious/noExplicitAny: opaque RHI handle
609
+ view,
610
+ loadOp: "load",
611
+ storeOp: "store"
612
+ }
613
+ ]
614
+ };
615
+ if (this.depthView !== null) {
616
+ passDesc.depthStencilAttachment = {
617
+ // biome-ignore lint/suspicious/noExplicitAny: opaque depth view
618
+ view: this.depthView,
619
+ depthLoadOp: "load",
620
+ depthStoreOp: "store"
621
+ };
622
+ }
623
+ const pass = encoder.beginRenderPass(passDesc);
624
+ const encoded = this.encode(pass, viewProj);
625
+ pass.end();
626
+ return encoded;
627
+ }
628
+ encode(pass, viewProj) {
629
+ if (this.isDestroyed) return flushedAfterDestroy();
630
+ if (viewProj === void 0 || viewProj === null) return viewProjRequired();
631
+ if (this.stagingLen === 0) {
632
+ this.lastFlushedVertexCount = 0;
633
+ return ok(void 0);
634
+ }
635
+ const vertexCount = Math.min(this.stagingLen, this.maxCapVal);
636
+ const vbo = this.gpuVbo;
637
+ const pipeline = this.gpuPipeline;
638
+ const uniformBuf = this.gpuUniformBuffer;
639
+ const bindGroup = this.gpuBindGroup;
640
+ const byteCount = vertexCount * VERTEX_STRIDE_BYTES;
641
+ this.rhiDevice.queue.writeBuffer(
642
+ vbo,
643
+ 0,
644
+ new Uint8Array(this.stagingArr.buffer, 0, byteCount),
645
+ 0,
646
+ byteCount
647
+ );
648
+ const uniformData = new Float32Array(16);
649
+ for (let i = 0; i < 16; i++) uniformData[i] = viewProj[i];
650
+ this.rhiDevice.queue.writeBuffer(uniformBuf, 0, new Uint8Array(uniformData.buffer), 0, 64);
651
+ pass.setPipeline(pipeline);
652
+ pass.setBindGroup(0, bindGroup);
653
+ pass.setVertexBuffer(0, vbo);
654
+ pass.draw(vertexCount);
655
+ this.lastFlushedVertexCount = vertexCount;
656
+ this.stagingLen = 0;
657
+ this.truncationWarned = false;
658
+ return ok(void 0);
659
+ }
660
+ // -- destroy (w14) --
661
+ destroy() {
662
+ if (this.isDestroyed) return;
663
+ this.isDestroyed = true;
664
+ if (this.gpuVbo) {
665
+ this.rhiDevice.destroyBuffer(this.gpuVbo);
666
+ this.gpuVbo = null;
667
+ }
668
+ if (this.gpuUniformBuffer) {
669
+ this.rhiDevice.destroyBuffer(this.gpuUniformBuffer);
670
+ this.gpuUniformBuffer = null;
671
+ }
672
+ this.gpuBindGroup = null;
673
+ this.gpuPipeline = null;
674
+ this.stagingArr = new Float32Array(0);
675
+ this.stagingLen = 0;
676
+ this.lastFlushedVertexCount = 0;
677
+ }
678
+ };
679
+ async function createDebugDraw(opts) {
680
+ const device = opts.device;
681
+ const fmt = opts.format ?? "bgra8unorm";
682
+ const depthFormat = opts.depthFormat;
683
+ const depthMode = opts.depthMode ?? "always";
684
+ const maxCap = normalizeCapacity(
685
+ opts.maxVertexCapacity ?? MAX_VERTEX_CAPACITY,
686
+ MAX_VERTEX_CAPACITY
687
+ );
688
+ const initialCap = Math.min(
689
+ normalizeCapacity(
690
+ opts.initialVertexCapacity ?? INITIAL_VERTEX_CAPACITY,
691
+ INITIAL_VERTEX_CAPACITY
692
+ ),
693
+ maxCap
694
+ );
695
+ const vboByteSize = initialCap * VERTEX_STRIDE_BYTES;
696
+ const vboResult = device.createBuffer({
697
+ size: vboByteSize,
698
+ usage: 8 | 32,
699
+ // COPY_DST | VERTEX
700
+ label: "debug-draw-vbo"
701
+ });
702
+ if (!vboResult.ok) {
703
+ return bufferAllocationFailed(
704
+ `createBuffer(COPY_DST|VERTEX, ${vboByteSize}B): ${vboResult.error.code}`
705
+ );
706
+ }
707
+ const vbo = vboResult.value;
708
+ const uniformBufResult = device.createBuffer({
709
+ size: 64,
710
+ usage: 64 | 8,
711
+ // UNIFORM | COPY_DST
712
+ label: "debug-draw-uniform"
713
+ });
714
+ if (!uniformBufResult.ok) {
715
+ device.destroyBuffer(vbo);
716
+ return bufferAllocationFailed(
717
+ `createBuffer(UNIFORM|COPY_DST, 64B): ${uniformBufResult.error.code}`
718
+ );
719
+ }
720
+ const uniformBuf = uniformBufResult.value;
721
+ const vsResult = await opts.createShaderModule(device, {
722
+ label: "debug-draw-vs",
723
+ code: VERTEX_SHADER
724
+ });
725
+ if (!vsResult.ok) {
726
+ device.destroyBuffer(vbo);
727
+ return pipelineCreateFailed(`createShaderModule(vertex): ${vsResult.error.code}`);
728
+ }
729
+ const vsModule = vsResult.value;
730
+ const fsResult = await opts.createShaderModule(device, {
731
+ label: "debug-draw-fs",
732
+ code: FRAGMENT_SHADER
733
+ });
734
+ if (!fsResult.ok) {
735
+ device.destroyBuffer(vbo);
736
+ return pipelineCreateFailed(`createShaderModule(fragment): ${fsResult.error.code}`);
737
+ }
738
+ const fsModule = fsResult.value;
739
+ const bglResult = device.createBindGroupLayout({
740
+ label: "debug-draw-bind-group-layout",
741
+ entries: [
742
+ {
743
+ binding: 0,
744
+ visibility: 1,
745
+ buffer: { type: "uniform", minBindingSize: 64 }
746
+ }
747
+ ]
748
+ });
749
+ if (!bglResult.ok) {
750
+ device.destroyBuffer(vbo);
751
+ device.destroyBuffer(uniformBuf);
752
+ return pipelineCreateFailed(`createBindGroupLayout: ${bglResult.error.code}`);
753
+ }
754
+ const pipelineLayoutResult = device.createPipelineLayout({
755
+ label: "debug-draw-pipeline-layout",
756
+ bindGroupLayouts: [bglResult.value]
757
+ });
758
+ if (!pipelineLayoutResult.ok) {
759
+ device.destroyBuffer(vbo);
760
+ device.destroyBuffer(uniformBuf);
761
+ return pipelineCreateFailed(`createPipelineLayout: ${pipelineLayoutResult.error.code}`);
762
+ }
763
+ const depthStencil = depthMode === "less-equal" ? {
764
+ format: depthFormat ?? "depth24plus",
765
+ depthWriteEnabled: false,
766
+ depthCompare: "less-equal"
767
+ } : void 0;
768
+ const vertexBuffers = [
769
+ {
770
+ arrayStride: VERTEX_STRIDE_BYTES,
771
+ stepMode: "vertex",
772
+ attributes: [
773
+ {
774
+ format: "float32x3",
775
+ offset: 0,
776
+ shaderLocation: 0
777
+ },
778
+ {
779
+ format: "unorm8x4",
780
+ offset: 12,
781
+ shaderLocation: 1
782
+ }
783
+ ]
784
+ }
785
+ ];
786
+ const pipelineDesc = {
787
+ label: "debug-draw-pso",
788
+ layout: pipelineLayoutResult.value,
789
+ vertex: {
790
+ module: vsModule,
791
+ entryPoint: "vs_main",
792
+ buffers: [...vertexBuffers]
793
+ },
794
+ primitive: {
795
+ topology: "line-list"
796
+ },
797
+ depthStencil,
798
+ fragment: {
799
+ module: fsModule,
800
+ entryPoint: "fs_main",
801
+ targets: [{ format: fmt }]
802
+ }
803
+ };
804
+ const psoResult = device.createRenderPipeline(pipelineDesc);
805
+ if (!psoResult.ok) {
806
+ device.destroyBuffer(vbo);
807
+ device.destroyBuffer(uniformBuf);
808
+ return pipelineCreateFailed(`createRenderPipeline: ${psoResult.error.code}`);
809
+ }
810
+ const pipeline = psoResult.value;
811
+ const bgResult = device.createBindGroup({
812
+ layout: bglResult.value,
813
+ entries: [
814
+ {
815
+ binding: 0,
816
+ resource: {
817
+ kind: "buffer",
818
+ value: { buffer: uniformBuf, offset: 0, size: 64 }
819
+ }
820
+ }
821
+ ],
822
+ label: "debug-draw-bindgroup"
823
+ // biome-ignore lint/suspicious/noExplicitAny: forgeax opaque BGL -> createBindGroup descriptor
824
+ });
825
+ if (!bgResult.ok) {
826
+ device.destroyBuffer(vbo);
827
+ device.destroyBuffer(uniformBuf);
828
+ return pipelineCreateFailed(`createBindGroup: ${bgResult.error.code}`);
829
+ }
830
+ return ok(new DebugDraw(device, pipeline, vbo, uniformBuf, bgResult.value, initialCap, maxCap));
831
+ }
832
+
833
+ export { DebugDraw, INITIAL_VERTEX_CAPACITY, MAX_VERTEX_CAPACITY, VERTEX_STRIDE_BYTES, createDebugDraw };
834
+ //# sourceMappingURL=index.mjs.map
835
+ //# sourceMappingURL=index.mjs.map