@downdraft/engine 0.3.0 → 0.4.0-alpha.0

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 (82) hide show
  1. package/app/src/renderer/bootstrap.ts +0 -6
  2. package/app/src/renderer/feature-log.ts +1 -9
  3. package/app/src/renderer/mcp-harness.ts +8 -11
  4. package/app/src/renderer/standard-automation-tools.ts +8 -145
  5. package/core/src/module/host.ts +3 -6
  6. package/core/src/module/index.ts +1 -1
  7. package/core/src/module/module.ts +4 -47
  8. package/core/src/physics/interface.ts +5 -0
  9. package/core/src/platform/render-surface.ts +13 -0
  10. package/core/src/render/canvas-resize-watcher.ts +1 -10
  11. package/core/src/render/game-renderer.ts +40 -18
  12. package/core/src/render/index.ts +0 -2
  13. package/core/src/render/input-manager.ts +7 -7
  14. package/core/src/render/renderer-module-host.ts +2 -4
  15. package/core/src/ui-font-scale.ts +5 -38
  16. package/core/src/worker/rpc.ts +65 -0
  17. package/dist/.dd-manifest.json +859 -884
  18. package/dist/app/src/renderer/bootstrap.js +0 -4
  19. package/dist/app/src/renderer/feature-log.js +1 -10
  20. package/dist/app/src/renderer/mcp-harness.js +8 -11
  21. package/dist/app/src/renderer/standard-automation-tools.js +6 -138
  22. package/dist/core/src/module/host.js +3 -6
  23. package/dist/core/src/render/canvas-resize-watcher.js +1 -10
  24. package/dist/core/src/render/game-renderer.js +44 -18
  25. package/dist/core/src/render/index.js +0 -1
  26. package/dist/core/src/render/input-manager.js +7 -7
  27. package/dist/core/src/render/renderer-module-host.js +2 -4
  28. package/dist/core/src/ui-font-scale.js +5 -38
  29. package/dist/core/src/worker/rpc.js +59 -0
  30. package/dist/libraries/blitz-ui/src/native-module.js +8 -1
  31. package/dist/libraries/devtools/src/native-providers.js +0 -2
  32. package/dist/libraries/entities/src/model-renderer.js +87 -27
  33. package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
  34. package/dist/libraries/networking/src/index.js +1 -0
  35. package/dist/libraries/networking/src/ws-server.js +387 -0
  36. package/dist/libraries/physics-rapier/src/api.js +15 -0
  37. package/dist/libraries/physics-rapier/src/backend.js +15 -0
  38. package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
  39. package/dist/libraries/postfx/src/post-process-stack.js +1 -3
  40. package/dist/libraries/sand/src/materials.js +2 -32
  41. package/dist/libraries/sand/src/sand-world.js +0 -3
  42. package/dist/modules/devtools/src/api.js +8 -23
  43. package/dist/modules/devtools/src/data-bridge.js +0 -11
  44. package/dist/modules/devtools/src/index.js +0 -6
  45. package/dist/modules/devtools/src/init.js +8 -42
  46. package/dist/modules/devtools/src/scene-inspector.js +1 -4
  47. package/dist/modules/devtools/src/worker-sync.js +3 -11
  48. package/dist/test/src/vision-test.js +1 -3
  49. package/libraries/blitz-ui/src/native-module.ts +8 -1
  50. package/libraries/devtools/src/native-providers.ts +0 -2
  51. package/libraries/entities/src/model-renderer.ts +88 -27
  52. package/libraries/entities/src/shaders/model.wgsl +13 -4
  53. package/libraries/networking/src/index.ts +4 -0
  54. package/libraries/networking/src/ws-server.ts +397 -0
  55. package/libraries/physics-rapier/src/api.ts +15 -0
  56. package/libraries/physics-rapier/src/backend.ts +15 -0
  57. package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
  58. package/libraries/postfx/src/post-process-stack.ts +1 -2
  59. package/libraries/sand/src/materials.ts +2 -32
  60. package/libraries/sand/src/sand-world.ts +0 -4
  61. package/modules/devtools/README.md +1 -3
  62. package/modules/devtools/src/api.ts +10 -34
  63. package/modules/devtools/src/data-bridge.ts +0 -16
  64. package/modules/devtools/src/index.ts +1 -11
  65. package/modules/devtools/src/init.ts +8 -51
  66. package/modules/devtools/src/scene-inspector.ts +2 -6
  67. package/modules/devtools/src/types.ts +5 -90
  68. package/modules/devtools/src/worker-sync.ts +3 -13
  69. package/package.json +4 -5
  70. package/test/src/vision-test.ts +2 -4
  71. package/core/src/render/sab-canvas-overlay.ts +0 -122
  72. package/dist/core/src/render/sab-canvas-overlay.js +0 -83
  73. package/dist/modules/devtools/src/debug-overlay.js +0 -181
  74. package/dist/modules/devtools/src/doctor-panel.js +0 -163
  75. package/dist/modules/devtools/src/label-overlay.js +0 -129
  76. package/dist/modules/devtools/src/material-stats-panel.js +0 -171
  77. package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
  78. package/modules/devtools/src/debug-overlay.ts +0 -238
  79. package/modules/devtools/src/doctor-panel.ts +0 -182
  80. package/modules/devtools/src/label-overlay.ts +0 -154
  81. package/modules/devtools/src/material-stats-panel.ts +0 -188
  82. package/modules/devtools/src/sim-stats-panel.ts +0 -272
@@ -1,122 +0,0 @@
1
- // ============================================================================
2
- // SabCanvasOverlay — DOM <canvas> overlay backed by a SharedArrayBuffer bitmap
3
- //
4
- // Generalized from overburden's MapCanvas. Provides the generic shell:
5
- // - A visible <canvas> fixed-positioned over the game canvas
6
- // (pointer-events: none, configurable z-index, hidden while opacity <= 0).
7
- // - An offscreen "bitmap" canvas at a fixed data resolution that is repainted
8
- // only when a SAB sequence counter changes (via getSeq + redrawBitmap).
9
- // - Per-frame window resize tracking and opacity control.
10
- //
11
- // The game supplies the compositing + drawing logic via callbacks; the overlay
12
- // owns all DOM plumbing.
13
- // ============================================================================
14
-
15
- export interface SabCanvasOverlayOptions {
16
- /** DOM id for the visible canvas. Default: "sab-canvas-overlay" */
17
- id?: string;
18
- /** CSS z-index. Default: 55 (above the game surface, below DOM overlays) */
19
- zIndex?: number;
20
- /** Offscreen bitmap resolution. If omitted, no bitmap canvas is created. */
21
- bitmapWidth?: number;
22
- bitmapHeight?: number;
23
- /** CSS image-rendering for the visible canvas. Default: "pixelated" */
24
- imageRendering?: "pixelated" | "auto";
25
- /**
26
- * Monotonic sequence number for the backing data (e.g. a SAB seq counter).
27
- * The bitmap is repainted only when this value changes. Omit to repaint
28
- * every visible frame.
29
- */
30
- getSeq?: () => number;
31
- /**
32
- * Repaint the offscreen bitmap canvas. Called only when getSeq() reports
33
- * a new value (requires bitmapWidth/bitmapHeight).
34
- */
35
- redrawBitmap?: (ctx: CanvasRenderingContext2D, bitmapW: number, bitmapH: number) => void;
36
- /**
37
- * Draw the visible canvas each visible frame, after the bitmap has been
38
- * refreshed. `bitmap` is the offscreen bitmap canvas (or null when no
39
- * bitmap was configured).
40
- */
41
- draw: (ctx: CanvasRenderingContext2D, bitmap: HTMLCanvasElement | null, w: number, h: number) => void;
42
- }
43
-
44
- export class SabCanvasOverlay {
45
- readonly canvas: HTMLCanvasElement;
46
- protected ctx: CanvasRenderingContext2D;
47
- /** Offscreen bitmap canvas (fixed data resolution). Null when unconfigured. */
48
- readonly bitmapCanvas: HTMLCanvasElement | null = null;
49
- protected bitmapCtx: CanvasRenderingContext2D | null = null;
50
- private opts: SabCanvasOverlayOptions;
51
- private lastSeq = -1;
52
-
53
- constructor(opts: SabCanvasOverlayOptions) {
54
- this.opts = opts;
55
-
56
- this.canvas = document.createElement("canvas");
57
- this.canvas.id = opts.id ?? "sab-canvas-overlay";
58
- this.canvas.style.position = "fixed";
59
- this.canvas.style.top = "0";
60
- this.canvas.style.left = "0";
61
- this.canvas.style.width = "100%";
62
- this.canvas.style.height = "100%";
63
- this.canvas.style.pointerEvents = "none";
64
- this.canvas.style.zIndex = String(opts.zIndex ?? 55);
65
- this.canvas.style.display = "none"; // hidden until opacity > 0
66
- this.canvas.style.imageRendering = opts.imageRendering ?? "pixelated";
67
- this.ctx = this.canvas.getContext("2d")!;
68
-
69
- if (opts.bitmapWidth !== undefined && opts.bitmapHeight !== undefined) {
70
- this.bitmapCanvas = document.createElement("canvas");
71
- this.bitmapCanvas.width = opts.bitmapWidth;
72
- this.bitmapCanvas.height = opts.bitmapHeight;
73
- this.bitmapCtx = this.bitmapCanvas.getContext("2d")!;
74
- }
75
- }
76
-
77
- /** Attach the canvas to the DOM (call once during init). */
78
- mount(parent: HTMLElement = document.body): void {
79
- parent.appendChild(this.canvas);
80
- }
81
-
82
- /**
83
- * Per-frame update. Call from the render loop.
84
- * Hides the canvas while `opacity <= 0`; otherwise resizes the canvas to the
85
- * window, repaints the bitmap when the seq counter advanced, and invokes
86
- * the draw callback.
87
- */
88
- update(opacity: number): void {
89
- if (opacity <= 0) {
90
- this.canvas.style.display = "none";
91
- return;
92
- }
93
- this.canvas.style.display = "block";
94
- this.canvas.style.opacity = String(opacity);
95
-
96
- // Resize the visible canvas to match the window if needed.
97
- const w = window.innerWidth;
98
- const h = window.innerHeight;
99
- if (this.canvas.width !== w || this.canvas.height !== h) {
100
- this.canvas.width = w;
101
- this.canvas.height = h;
102
- }
103
-
104
- // Repaint the bitmap if the backing data has a new seq value.
105
- const seq = this.opts.getSeq?.();
106
- if (this.bitmapCtx && (seq === undefined || seq !== this.lastSeq)) {
107
- if (seq !== undefined) this.lastSeq = seq;
108
- this.opts.redrawBitmap?.(this.bitmapCtx, this.bitmapCanvas!.width, this.bitmapCanvas!.height);
109
- }
110
-
111
- this.opts.draw(this.ctx, this.bitmapCanvas, w, h);
112
- }
113
-
114
- /** Mark the bitmap stale so it repaints on the next visible frame. */
115
- invalidate(): void {
116
- this.lastSeq = -1;
117
- }
118
-
119
- dispose(): void {
120
- this.canvas.remove();
121
- }
122
- }
@@ -1,83 +0,0 @@
1
- // ============================================================================
2
- // SabCanvasOverlay — DOM <canvas> overlay backed by a SharedArrayBuffer bitmap
3
- //
4
- // Generalized from overburden's MapCanvas. Provides the generic shell:
5
- // - A visible <canvas> fixed-positioned over the game canvas
6
- // (pointer-events: none, configurable z-index, hidden while opacity <= 0).
7
- // - An offscreen "bitmap" canvas at a fixed data resolution that is repainted
8
- // only when a SAB sequence counter changes (via getSeq + redrawBitmap).
9
- // - Per-frame window resize tracking and opacity control.
10
- //
11
- // The game supplies the compositing + drawing logic via callbacks; the overlay
12
- // owns all DOM plumbing.
13
- // ============================================================================
14
- export class SabCanvasOverlay {
15
- canvas;
16
- ctx;
17
- /** Offscreen bitmap canvas (fixed data resolution). Null when unconfigured. */
18
- bitmapCanvas = null;
19
- bitmapCtx = null;
20
- opts;
21
- lastSeq = -1;
22
- constructor(opts) {
23
- this.opts = opts;
24
- this.canvas = document.createElement("canvas");
25
- this.canvas.id = opts.id ?? "sab-canvas-overlay";
26
- this.canvas.style.position = "fixed";
27
- this.canvas.style.top = "0";
28
- this.canvas.style.left = "0";
29
- this.canvas.style.width = "100%";
30
- this.canvas.style.height = "100%";
31
- this.canvas.style.pointerEvents = "none";
32
- this.canvas.style.zIndex = String(opts.zIndex ?? 55);
33
- this.canvas.style.display = "none"; // hidden until opacity > 0
34
- this.canvas.style.imageRendering = opts.imageRendering ?? "pixelated";
35
- this.ctx = this.canvas.getContext("2d");
36
- if (opts.bitmapWidth !== undefined && opts.bitmapHeight !== undefined) {
37
- this.bitmapCanvas = document.createElement("canvas");
38
- this.bitmapCanvas.width = opts.bitmapWidth;
39
- this.bitmapCanvas.height = opts.bitmapHeight;
40
- this.bitmapCtx = this.bitmapCanvas.getContext("2d");
41
- }
42
- }
43
- /** Attach the canvas to the DOM (call once during init). */
44
- mount(parent = document.body) {
45
- parent.appendChild(this.canvas);
46
- }
47
- /**
48
- * Per-frame update. Call from the render loop.
49
- * Hides the canvas while `opacity <= 0`; otherwise resizes the canvas to the
50
- * window, repaints the bitmap when the seq counter advanced, and invokes
51
- * the draw callback.
52
- */
53
- update(opacity) {
54
- if (opacity <= 0) {
55
- this.canvas.style.display = "none";
56
- return;
57
- }
58
- this.canvas.style.display = "block";
59
- this.canvas.style.opacity = String(opacity);
60
- // Resize the visible canvas to match the window if needed.
61
- const w = window.innerWidth;
62
- const h = window.innerHeight;
63
- if (this.canvas.width !== w || this.canvas.height !== h) {
64
- this.canvas.width = w;
65
- this.canvas.height = h;
66
- }
67
- // Repaint the bitmap if the backing data has a new seq value.
68
- const seq = this.opts.getSeq?.();
69
- if (this.bitmapCtx && (seq === undefined || seq !== this.lastSeq)) {
70
- if (seq !== undefined)
71
- this.lastSeq = seq;
72
- this.opts.redrawBitmap?.(this.bitmapCtx, this.bitmapCanvas.width, this.bitmapCanvas.height);
73
- }
74
- this.opts.draw(this.ctx, this.bitmapCanvas, w, h);
75
- }
76
- /** Mark the bitmap stale so it repaints on the next visible frame. */
77
- invalidate() {
78
- this.lastSeq = -1;
79
- }
80
- dispose() {
81
- this.canvas.remove();
82
- }
83
- }
@@ -1,181 +0,0 @@
1
- // ============================================================================
2
- // DebugOverlay — 2D canvas overlay for chunk grid + velocity arrows
3
- // Generic rendering logic; game provides data via IDebugOverlayData interface.
4
- // ============================================================================
5
- import { calculateViewProj, CanvasResizeWatcher } from "@downdraft/engine";
6
- export class DebugOverlay {
7
- canvas;
8
- overlay;
9
- ctx;
10
- showChunkGrid = false;
11
- showVelocityArrows = false;
12
- resizeWatcher;
13
- constructor(surface) {
14
- // DOM-overlay widget — the canvas/element members it touches
15
- // (parentElement, 2d overlay canvas) are DOM-host concepts; on native
16
- // they degrade to the compat stubs (display:none anyway).
17
- const canvas = surface;
18
- this.canvas = canvas;
19
- this.overlay = document.createElement("canvas");
20
- this.overlay.className = "debug-overlay";
21
- this.overlay.style.cssText = `
22
- position: absolute;
23
- top: 0; left: 0;
24
- width: 100%; height: 100%;
25
- pointer-events: none;
26
- z-index: 0;
27
- display: none;
28
- `;
29
- const parent = canvas.parentElement;
30
- if (parent) {
31
- parent.style.position = parent.style.position || "relative";
32
- parent.appendChild(this.overlay);
33
- }
34
- else {
35
- document.body.appendChild(this.overlay);
36
- }
37
- this.ctx = this.overlay.getContext("2d");
38
- this.resizeWatcher = new CanvasResizeWatcher(canvas, {
39
- onResize: (w, h) => {
40
- if (this.overlay.width !== w || this.overlay.height !== h) {
41
- this.overlay.width = w;
42
- this.overlay.height = h;
43
- }
44
- },
45
- });
46
- }
47
- setShowChunkGrid(show) {
48
- this.showChunkGrid = show;
49
- this.updateDisplay();
50
- }
51
- setShowVelocityArrows(show) {
52
- this.showVelocityArrows = show;
53
- this.updateDisplay();
54
- }
55
- isChunkGridVisible() {
56
- return this.showChunkGrid;
57
- }
58
- isVelocityArrowsVisible() {
59
- return this.showVelocityArrows;
60
- }
61
- updateDisplay() {
62
- const active = this.showChunkGrid || this.showVelocityArrows;
63
- this.overlay.style.display = active ? "block" : "none";
64
- if (!active) {
65
- this.ctx.clearRect(0, 0, this.overlay.width, this.overlay.height);
66
- }
67
- }
68
- projectToScreen(worldX, worldY, worldZ, viewProj, canvasW, canvasH) {
69
- const x = viewProj[0] * worldX + viewProj[4] * worldY + viewProj[8] * worldZ + viewProj[12];
70
- const y = viewProj[1] * worldX + viewProj[5] * worldY + viewProj[9] * worldZ + viewProj[13];
71
- const w = viewProj[3] * worldX + viewProj[7] * worldY + viewProj[11] * worldZ + viewProj[15];
72
- if (w <= 0)
73
- return { x: 0, y: 0, behind: true };
74
- const ndcX = x / w;
75
- const ndcY = y / w;
76
- const screenX = (ndcX + 1) * 0.5 * canvasW;
77
- const screenY = (1 - ndcY) * 0.5 * canvasH;
78
- return { x: screenX, y: screenY, behind: false };
79
- }
80
- update(camera, data) {
81
- if (!this.showChunkGrid && !this.showVelocityArrows)
82
- return;
83
- const ctx = this.ctx;
84
- const canvasW = this.overlay.width;
85
- const canvasH = this.overlay.height;
86
- ctx.clearRect(0, 0, canvasW, canvasH);
87
- const viewProj = calculateViewProj(camera);
88
- if (this.showChunkGrid) {
89
- this.renderChunkGrid(ctx, camera, viewProj, canvasW, canvasH, data);
90
- }
91
- if (this.showVelocityArrows) {
92
- this.renderVelocityArrows(ctx, viewProj, canvasW, canvasH, data);
93
- }
94
- }
95
- renderChunkGrid(ctx, camera, viewProj, canvasW, canvasH, data) {
96
- const playerPos = data.getPlayerPosition();
97
- if (!playerPos)
98
- return;
99
- const { chunkSize, chunksVisible } = data.getChunkGridConfig();
100
- const playerChunkX = Math.floor(playerPos.x / chunkSize);
101
- const playerChunkZ = Math.floor(playerPos.z / chunkSize);
102
- const half = Math.ceil(chunksVisible / 2);
103
- ctx.strokeStyle = "rgba(100, 200, 255, 0.35)";
104
- ctx.lineWidth = 1;
105
- ctx.font = "10px monospace";
106
- ctx.fillStyle = "rgba(100, 200, 255, 0.6)";
107
- for (let cx = playerChunkX - half; cx <= playerChunkX + half; cx++) {
108
- for (let cz = playerChunkZ - half; cz <= playerChunkZ + half; cz++) {
109
- const worldX = cx * chunkSize;
110
- const worldZ = cz * chunkSize;
111
- const corners = [
112
- this.projectToScreen(worldX, 0, worldZ, viewProj, canvasW, canvasH),
113
- this.projectToScreen(worldX + chunkSize, 0, worldZ, viewProj, canvasW, canvasH),
114
- this.projectToScreen(worldX + chunkSize, 0, worldZ + chunkSize, viewProj, canvasW, canvasH),
115
- this.projectToScreen(worldX, 0, worldZ + chunkSize, viewProj, canvasW, canvasH),
116
- ];
117
- if (corners.some(c => c === null || c.behind))
118
- continue;
119
- ctx.beginPath();
120
- ctx.moveTo(corners[0].x, corners[0].y);
121
- ctx.lineTo(corners[1].x, corners[1].y);
122
- ctx.lineTo(corners[2].x, corners[2].y);
123
- ctx.lineTo(corners[3].x, corners[3].y);
124
- ctx.closePath();
125
- ctx.stroke();
126
- const centerProj = this.projectToScreen(worldX + chunkSize / 2, 0, worldZ + chunkSize / 2, viewProj, canvasW, canvasH);
127
- if (centerProj && !centerProj.behind) {
128
- const dx = (worldX + chunkSize / 2) - camera.position[0];
129
- const dz = (worldZ + chunkSize / 2) - camera.position[2];
130
- const dist = Math.sqrt(dx * dx + dz * dz);
131
- if (dist < chunkSize * half) {
132
- ctx.fillText(`${cx},${cz}`, centerProj.x - 15, centerProj.y);
133
- }
134
- }
135
- }
136
- }
137
- }
138
- renderVelocityArrows(ctx, viewProj, canvasW, canvasH, data) {
139
- const entityCount = data.getEntityCount();
140
- ctx.strokeStyle = "rgba(255, 200, 50, 0.8)";
141
- ctx.fillStyle = "rgba(255, 200, 50, 0.8)";
142
- ctx.lineWidth = 2;
143
- for (let i = 0; i < entityCount; i++) {
144
- const pos = data.getEntityPosition(i);
145
- const vel = data.getEntityVelocity(i);
146
- if (!pos || !vel)
147
- continue;
148
- const speed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z);
149
- if (speed < 0.1)
150
- continue;
151
- const scale = 2.0;
152
- const endX = pos.x + vel.x * scale;
153
- const endY = pos.y + vel.y * scale;
154
- const endZ = pos.z + vel.z * scale;
155
- const startProj = this.projectToScreen(pos.x, pos.y, pos.z, viewProj, canvasW, canvasH);
156
- const endProj = this.projectToScreen(endX, endY, endZ, viewProj, canvasW, canvasH);
157
- if (!startProj || startProj.behind || !endProj || endProj.behind)
158
- continue;
159
- ctx.beginPath();
160
- ctx.moveTo(startProj.x, startProj.y);
161
- ctx.lineTo(endProj.x, endProj.y);
162
- ctx.stroke();
163
- const dx = endProj.x - startProj.x;
164
- const dy = endProj.y - startProj.y;
165
- const angle = Math.atan2(dy, dx);
166
- const headLen = 6;
167
- ctx.beginPath();
168
- ctx.moveTo(endProj.x, endProj.y);
169
- ctx.lineTo(endProj.x - headLen * Math.cos(angle - Math.PI / 6), endProj.y - headLen * Math.sin(angle - Math.PI / 6));
170
- ctx.lineTo(endProj.x - headLen * Math.cos(angle + Math.PI / 6), endProj.y - headLen * Math.sin(angle + Math.PI / 6));
171
- ctx.closePath();
172
- ctx.fill();
173
- ctx.font = "10px monospace";
174
- ctx.fillText(`${speed.toFixed(1)} m/s`, endProj.x + 4, endProj.y - 4);
175
- }
176
- }
177
- destroy() {
178
- this.resizeWatcher.destroy();
179
- this.overlay.remove();
180
- }
181
- }
@@ -1,163 +0,0 @@
1
- // ============================================================================
2
- // downdraft doctor — devtools panel for module graph diagnostics
3
- //
4
- // Displays the resolved module graph, resource table, system schedule,
5
- // diagnostics (unresolved requires, version conflicts, leaks), and the
6
- // cross-thread dependency report (sim + renderer).
7
- // ============================================================================
8
- /** Escape untrusted strings before interpolating into innerHTML — mod
9
- * manifests are third-party content and must not be able to inject markup. */
10
- function esc(s) {
11
- return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
12
- }
13
- export function createDoctorPanelExtension(opts = {}) {
14
- return {
15
- id: "downdraft-doctor",
16
- tabLabel: "Doctor",
17
- tabTooltip: "Module graph diagnostics: resolved dependencies, resource table, version conflicts, leak detection",
18
- order: 5,
19
- html: `
20
- <div class="dd-doctor">
21
- <div class="dd-doctor__header">
22
- <h2>downdraft doctor</h2>
23
- <button class="dd-doctor__refresh">Refresh</button>
24
- </div>
25
- <div class="dd-doctor__summary"></div>
26
- <div class="dd-doctor__plugins"></div>
27
- <div class="dd-doctor__user-plugins"></div>
28
- <div class="dd-doctor__resources"></div>
29
- <div class="dd-doctor__issues"></div>
30
- </div>
31
- `,
32
- css: `
33
- .dd-doctor { padding: 12px; font-family: monospace; font-size: 12px; color: #ccc; }
34
- .dd-doctor h2 { margin: 0 0 8px; font-size: 16px; color: #4fc3f7; }
35
- .dd-doctor__header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
36
- .dd-doctor__refresh { padding: 4px 12px; background: #333; color: #ccc; border: 1px solid #555; border-radius: 4px; cursor: pointer; }
37
- .dd-doctor__refresh:hover { background: #444; }
38
- .dd-doctor__summary { margin-bottom: 16px; padding: 8px; background: #1a1a2e; border-radius: 4px; }
39
- .dd-doctor__summary div { margin: 2px 0; }
40
- .dd-doctor__plugins h3, .dd-doctor__resources h3, .dd-doctor__issues h3, .dd-doctor__user-plugins h3 { color: #4fc3f7; margin: 12px 0 4px; font-size: 13px; }
41
- .dd-doctor__badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; margin-right: 4px; }
42
- .dd-doctor__badge--format { background: #2a3a4a; color: #64b5f6; }
43
- .dd-doctor__badge--tier { background: #3a2a4a; color: #ce93d8; }
44
- .dd-doctor__badge--thread { background: #2a4a3a; color: #81c784; }
45
- .dd-doctor__badge--perm { background: #4a3a2a; color: #ffb74d; }
46
- .dd-doctor__status--active { color: #81c784; }
47
- .dd-doctor__status--error { color: #ef5350; }
48
- .dd-doctor__status--loading, .dd-doctor__status--pending { color: #ffb74d; }
49
- .dd-doctor__status--disabled { color: #888; }
50
- .dd-doctor__table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
51
- .dd-doctor__table th { text-align: left; padding: 4px 8px; background: #1a1a2e; color: #888; font-weight: normal; }
52
- .dd-doctor__table td { padding: 4px 8px; border-bottom: 1px solid #222; }
53
- .dd-doctor__table .dd-thread-sim { color: #81c784; }
54
- .dd-doctor__table .dd-thread-renderer { color: #64b5f6; }
55
- .dd-doctor__table .dd-thread-shared { color: #ffb74d; }
56
- .dd-doctor__issue { padding: 6px 8px; margin: 4px 0; border-radius: 4px; }
57
- .dd-doctor__issue--error { background: #3e1e1e; color: #ef5350; }
58
- .dd-doctor__issue--warning { background: #3e3a1e; color: #ffb74d; }
59
- .dd-doctor__issue--ok { background: #1e3e2a; color: #81c784; }
60
- `,
61
- script: `(() => {
62
- const helpers = arguments[0];
63
- const container = helpers.container;
64
- const opts = window.__ddDoctorOpts || {};
65
- let refreshTimer = null;
66
-
67
- function render() {
68
- const simModules = opts.getSimModules ? opts.getSimModules() : [];
69
- const rendererModules = opts.getRendererModules ? opts.getRendererModules() : [];
70
- let report = opts.getReport ? opts.getReport() : null;
71
- if (!report && (simModules.length || rendererModules.length)) {
72
- // Build report inline if no pre-built report provider
73
- const { buildCrossThreadReport } = window.__ddCrossThread || {};
74
- if (buildCrossThreadReport) {
75
- report = buildCrossThreadReport(simModules, rendererModules);
76
- }
77
- }
78
- if (!report) {
79
- report = { modules: [...simModules, ...rendererModules], unresolved: [], shared: [], versionConflicts: [] };
80
- }
81
-
82
- // Summary
83
- const summary = container.querySelector('.dd-doctor__summary');
84
- const simCount = report.modules.filter(p => p.thread === 'sim').length;
85
- const rendererCount = report.modules.filter(p => p.thread === 'renderer').length;
86
- const sharedCount = report.shared.length;
87
- const unresolvedCount = report.unresolved.length;
88
- const conflictCount = report.versionConflicts.length;
89
- summary.innerHTML = [
90
- '<div>Sim modules: <b>' + simCount + '</b></div>',
91
- '<div>Renderer modules: <b>' + rendererCount + '</b></div>',
92
- '<div>Shared resources: <b>' + sharedCount + '</b></div>',
93
- '<div>Unresolved requires: <b style="color:' + (unresolvedCount ? '#ef5350' : '#81c784') + '">' + unresolvedCount + '</b></div>',
94
- '<div>Version conflicts: <b style="color:' + (conflictCount ? '#ef5350' : '#81c784') + '">' + conflictCount + '</b></div>',
95
- ].join('');
96
-
97
- // Modules table
98
- const pluginsDiv = container.querySelector('.dd-doctor__plugins');
99
- if (report.modules.length === 0) {
100
- pluginsDiv.innerHTML = '<h3>Modules</h3><p>No modules registered.</p>';
101
- } else {
102
- let html = '<h3>Modules (' + report.modules.length + ')</h3>';
103
- html += '<table class="dd-doctor__table"><thead><tr><th>Name</th><th>Version</th><th>Thread</th><th>Provides</th><th>Requires</th><th>Status</th></tr></thead><tbody>';
104
- for (const p of report.modules) {
105
- const threadClass = 'dd-thread-' + esc(p.thread);
106
- html += '<tr><td>' + esc(p.name) + '</td><td>' + esc(p.version) + '</td><td class="' + threadClass + '">' + esc(p.thread) + '</td><td>' + esc(p.provides.join(', ') || '—') + '</td><td>' + esc(p.requires.join(', ') || '—') + '</td><td>' + (p.active ? '✓ active' : 'inactive') + '</td></tr>';
107
- }
108
- html += '</tbody></table>';
109
- pluginsDiv.innerHTML = html;
110
- }
111
-
112
- // User-authored plugins (modding system)
113
- const userPluginsDiv = container.querySelector('.dd-doctor__user-plugins');
114
- const plugins = opts.getPlugins ? opts.getPlugins() : [];
115
- if (plugins.length === 0) {
116
- userPluginsDiv.innerHTML = '';
117
- } else {
118
- let phtml = '<h3>Plugins (' + plugins.length + ')</h3>';
119
- phtml += '<table class="dd-doctor__table"><thead><tr><th>ID</th><th>Version</th><th>Format</th><th>Tier</th><th>Thread</th><th>Permissions</th><th>Status</th></tr></thead><tbody>';
120
- for (const p of plugins) {
121
- const fmtBadge = '<span class="dd-doctor__badge dd-doctor__badge--format">' + esc(p.format) + '</span>';
122
- const tierBadge = '<span class="dd-doctor__badge dd-doctor__badge--tier">' + esc(p.tier) + '</span>';
123
- const threadBadge = '<span class="dd-doctor__badge dd-doctor__badge--thread">' + esc(p.thread) + '</span>';
124
- const permBadges = p.permissions.map(function(perm) { return '<span class="dd-doctor__badge dd-doctor__badge--perm">' + esc(perm) + '</span>'; }).join('');
125
- const statusClass = 'dd-doctor__status--' + esc(p.status);
126
- const statusText = esc(p.status + (p.error ? ': ' + p.error : ''));
127
- phtml += '<tr><td>' + esc(p.id) + '</td><td>' + esc(p.version) + '</td><td>' + fmtBadge + '</td><td>' + tierBadge + '</td><td>' + threadBadge + '</td><td>' + (permBadges || '—') + '</td><td class="' + statusClass + '">' + statusText + '</td></tr>';
128
- }
129
- phtml += '</tbody></table>';
130
- userPluginsDiv.innerHTML = phtml;
131
- }
132
-
133
- // Issues
134
- const issuesDiv = container.querySelector('.dd-doctor__issues');
135
- let issuesHtml = '<h3>Diagnostics</h3>';
136
- if (report.unresolved.length > 0) {
137
- issuesHtml += '<div class="dd-doctor__issue dd-doctor__issue--error">Unresolved requires: ' + esc(report.unresolved.join(', ')) + '</div>';
138
- }
139
- if (report.versionConflicts.length > 0) {
140
- for (const vc of report.versionConflicts) {
141
- issuesHtml += '<div class="dd-doctor__issue dd-doctor__issue--error">Version conflict: ' + esc(vc.name) + ' (sim=' + esc(vc.simVersion) + ', renderer=' + esc(vc.rendererVersion) + ')</div>';
142
- }
143
- }
144
- if (report.shared.length > 0) {
145
- issuesHtml += '<div class="dd-doctor__issue dd-doctor__issue--ok">Shared resources: ' + esc(report.shared.join(', ')) + '</div>';
146
- }
147
- if (report.unresolved.length === 0 && report.versionConflicts.length === 0) {
148
- issuesHtml += '<div class="dd-doctor__issue dd-doctor__issue--ok">✓ No issues detected</div>';
149
- }
150
- issuesDiv.innerHTML = issuesHtml;
151
- }
152
-
153
- container.querySelector('.dd-doctor__refresh').addEventListener('click', render);
154
- render();
155
-
156
- return {
157
- onActivate() { refreshTimer = setInterval(render, 2000); },
158
- onDeactivate() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } },
159
- onDestroy() { if (refreshTimer) clearInterval(refreshTimer); }
160
- };
161
- })`,
162
- };
163
- }
@@ -1,129 +0,0 @@
1
- // ============================================================================
2
- // LabelOverlay — HTML-based billboard labels for dev mode
3
- // Generic DOM management + projection; game provides labels via ILabelProvider.
4
- // ============================================================================
5
- import { calculateViewProj } from "@downdraft/engine";
6
- const MAX_LABELS = 512;
7
- export class LabelOverlay {
8
- container;
9
- labels = new Map();
10
- active = false;
11
- canvas;
12
- provider = null;
13
- constructor(surface) {
14
- // DOM-overlay widget — the canvas/element members it touches
15
- // (parentElement, 2d overlay canvas) are DOM-host concepts; on native
16
- // they degrade to the compat stubs (display:none anyway).
17
- const canvas = surface;
18
- this.canvas = canvas;
19
- this.container = document.createElement("div");
20
- this.container.className = "label-overlay";
21
- this.container.style.cssText = `
22
- position: absolute;
23
- top: 0; left: 0;
24
- width: 100%; height: 100%;
25
- pointer-events: none;
26
- overflow: hidden;
27
- z-index: 0;
28
- display: none;
29
- `;
30
- const parent = canvas.parentElement;
31
- if (parent) {
32
- parent.style.position = parent.style.position || "relative";
33
- parent.appendChild(this.container);
34
- }
35
- else {
36
- document.body.appendChild(this.container);
37
- }
38
- }
39
- setProvider(provider) {
40
- this.provider = provider;
41
- }
42
- setActive(active) {
43
- this.active = active;
44
- this.container.style.display = active ? "block" : "none";
45
- if (!active) {
46
- this.clearLabels();
47
- }
48
- }
49
- isActive() {
50
- return this.active;
51
- }
52
- clearLabels() {
53
- for (const entry of this.labels.values()) {
54
- entry.el.remove();
55
- }
56
- this.labels.clear();
57
- }
58
- getOrCreateLabel(key) {
59
- let entry = this.labels.get(key);
60
- if (entry)
61
- return entry.el;
62
- if (this.labels.size >= MAX_LABELS)
63
- return null;
64
- const el = document.createElement("div");
65
- el.className = "dev-label";
66
- el.style.cssText = `
67
- position: absolute;
68
- font-family: monospace;
69
- font-size: 11px;
70
- color: #fff;
71
- background: rgba(0, 0, 0, 0.65);
72
- border-radius: 3px;
73
- padding: 1px 5px;
74
- white-space: nowrap;
75
- pointer-events: none;
76
- transform: translate(-50%, -100%);
77
- will-change: transform, left, top;
78
- border: 1px solid rgba(255, 255, 255, 0.15);
79
- `;
80
- this.container.appendChild(el);
81
- this.labels.set(key, { el });
82
- return el;
83
- }
84
- projectToScreen(worldX, worldY, worldZ, viewProj, canvasW, canvasH) {
85
- const x = viewProj[0] * worldX + viewProj[4] * worldY + viewProj[8] * worldZ + viewProj[12];
86
- const y = viewProj[1] * worldX + viewProj[5] * worldY + viewProj[9] * worldZ + viewProj[13];
87
- const w = viewProj[3] * worldX + viewProj[7] * worldY + viewProj[11] * worldZ + viewProj[15];
88
- if (w <= 0)
89
- return { x: 0, y: 0, behind: true };
90
- const ndcX = x / w;
91
- const ndcY = y / w;
92
- const screenX = (ndcX + 1) * 0.5 * canvasW;
93
- const screenY = (1 - ndcY) * 0.5 * canvasH;
94
- return { x: screenX, y: screenY, behind: false };
95
- }
96
- update(camera) {
97
- if (!this.active || !this.provider)
98
- return;
99
- const viewProj = calculateViewProj(camera);
100
- const canvasW = this.canvas.clientWidth;
101
- const canvasH = this.canvas.clientHeight;
102
- const labels = this.provider.getLabels();
103
- const usedKeys = new Set();
104
- for (let _i = 0, _it = labels, _n = _it.length; _i < _n; _i++) {
105
- const label = _it[_i];
106
- usedKeys.add(label.key);
107
- const proj = this.projectToScreen(label.x, label.y, label.z, viewProj, canvasW, canvasH);
108
- if (!proj || proj.behind)
109
- continue;
110
- const el = this.getOrCreateLabel(label.key);
111
- if (!el)
112
- continue;
113
- el.textContent = label.text;
114
- el.style.left = `${proj.x}px`;
115
- el.style.top = `${proj.y}px`;
116
- el.style.color = label.color;
117
- el.style.display = "block";
118
- }
119
- for (const [key, entry] of this.labels.entries()) {
120
- if (!usedKeys.has(key)) {
121
- entry.el.style.display = "none";
122
- }
123
- }
124
- }
125
- destroy() {
126
- this.clearLabels();
127
- this.container.remove();
128
- }
129
- }