@downdraft/engine 0.2.0 → 0.3.1

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 (138) 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/index.ts +2 -2
  6. package/core/src/module/host.ts +3 -6
  7. package/core/src/module/index.ts +1 -1
  8. package/core/src/module/module.ts +4 -47
  9. package/core/src/physics/interface.ts +5 -0
  10. package/core/src/platform/render-surface.ts +13 -0
  11. package/core/src/platform/rpc.ts +4 -1
  12. package/core/src/render/canvas-resize-watcher.ts +1 -10
  13. package/core/src/render/game-renderer.ts +164 -79
  14. package/core/src/render/index.ts +0 -2
  15. package/core/src/render/input-manager.ts +7 -7
  16. package/core/src/render/renderer-module-host.ts +2 -4
  17. package/core/src/sab/grid-buffer.ts +55 -2
  18. package/core/src/sab/record-buffer.ts +48 -5
  19. package/core/src/sab/sim-channel.ts +181 -17
  20. package/core/src/sab/slot-buffer.ts +55 -1
  21. package/core/src/sab/types.ts +11 -0
  22. package/core/src/telemetry/gpu-resource-tracker.ts +24 -0
  23. package/core/src/ui-font-scale.ts +5 -38
  24. package/core/src/util/index.ts +1 -0
  25. package/core/src/util/session-timer.ts +23 -0
  26. package/core/src/worker/base-worker-host.ts +2 -1
  27. package/core/src/worker/buffer-sync.ts +20 -13
  28. package/core/src/worker/rpc.ts +119 -5
  29. package/core/src/worker/sim-worker-base.ts +5 -2
  30. package/core/src/worker/task-pool.ts +4 -0
  31. package/dist/.dd-manifest.json +893 -908
  32. package/dist/app/src/renderer/bootstrap.js +0 -4
  33. package/dist/app/src/renderer/feature-log.js +1 -10
  34. package/dist/app/src/renderer/mcp-harness.js +8 -11
  35. package/dist/app/src/renderer/standard-automation-tools.js +6 -138
  36. package/dist/core/src/index.js +2 -2
  37. package/dist/core/src/module/host.js +3 -6
  38. package/dist/core/src/platform/rpc.js +2 -1
  39. package/dist/core/src/render/canvas-resize-watcher.js +1 -10
  40. package/dist/core/src/render/game-renderer.js +166 -74
  41. package/dist/core/src/render/index.js +0 -1
  42. package/dist/core/src/render/input-manager.js +7 -7
  43. package/dist/core/src/render/renderer-module-host.js +2 -4
  44. package/dist/core/src/sab/grid-buffer.js +53 -1
  45. package/dist/core/src/sab/record-buffer.js +49 -5
  46. package/dist/core/src/sab/sim-channel.js +173 -17
  47. package/dist/core/src/sab/slot-buffer.js +54 -1
  48. package/dist/core/src/telemetry/gpu-resource-tracker.js +26 -0
  49. package/dist/core/src/ui-font-scale.js +5 -38
  50. package/dist/core/src/util/index.js +1 -0
  51. package/dist/core/src/util/session-timer.js +22 -0
  52. package/dist/core/src/worker/base-worker-host.js +2 -1
  53. package/dist/core/src/worker/buffer-sync.js +16 -9
  54. package/dist/core/src/worker/rpc.js +105 -8
  55. package/dist/core/src/worker/sim-worker-base.js +5 -2
  56. package/dist/core/src/worker/task-pool.js +5 -0
  57. package/dist/libraries/blitz-ui/src/native-module.js +8 -1
  58. package/dist/libraries/devtools/src/backend.js +35 -9
  59. package/dist/libraries/devtools/src/collectors.js +73 -22
  60. package/dist/libraries/devtools/src/native-providers.js +0 -2
  61. package/dist/libraries/entities/src/model-renderer.js +87 -27
  62. package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
  63. package/dist/libraries/networking/src/index.js +1 -0
  64. package/dist/libraries/networking/src/ws-server.js +387 -0
  65. package/dist/libraries/physics-rapier/src/api.js +15 -0
  66. package/dist/libraries/physics-rapier/src/backend.js +15 -0
  67. package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
  68. package/dist/libraries/postfx/src/post-process-stack.js +1 -3
  69. package/dist/libraries/sand/src/materials.js +2 -32
  70. package/dist/libraries/sand/src/sand-world.js +0 -3
  71. package/dist/modules/devtools/src/api.js +8 -23
  72. package/dist/modules/devtools/src/blitz/fonts.js +75 -0
  73. package/dist/modules/devtools/src/blitz/host.js +124 -30
  74. package/dist/modules/devtools/src/blitz/panels/console.js +19 -7
  75. package/dist/modules/devtools/src/blitz/panels/elements.js +217 -28
  76. package/dist/modules/devtools/src/blitz/panels/gpu.js +5 -2
  77. package/dist/modules/devtools/src/blitz/panels/performance.js +26 -12
  78. package/dist/modules/devtools/src/blitz/panels/snapshot.js +31 -8
  79. package/dist/modules/devtools/src/blitz/panels/types.js +2 -1
  80. package/dist/modules/devtools/src/blitz/theme.js +26 -4
  81. package/dist/modules/devtools/src/data-bridge.js +0 -11
  82. package/dist/modules/devtools/src/index.js +0 -6
  83. package/dist/modules/devtools/src/init.js +8 -42
  84. package/dist/modules/devtools/src/scene-inspector.js +1 -4
  85. package/dist/modules/devtools/src/worker-sync.js +3 -11
  86. package/dist/modules/html-ui/src/doc-backend.js +52 -3
  87. package/dist/modules/html-ui/src/host.js +6 -3
  88. package/dist/test/src/vision-test.js +1 -3
  89. package/libraries/blitz-ui/native/Cargo.toml +6 -0
  90. package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
  91. package/libraries/blitz-ui/src/native-module.ts +8 -1
  92. package/libraries/devtools/src/backend.ts +38 -12
  93. package/libraries/devtools/src/collectors.ts +75 -22
  94. package/libraries/devtools/src/native-providers.ts +0 -2
  95. package/libraries/entities/src/model-renderer.ts +88 -27
  96. package/libraries/entities/src/shaders/model.wgsl +13 -4
  97. package/libraries/networking/src/index.ts +4 -0
  98. package/libraries/networking/src/ws-server.ts +397 -0
  99. package/libraries/physics-rapier/src/api.ts +15 -0
  100. package/libraries/physics-rapier/src/backend.ts +15 -0
  101. package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
  102. package/libraries/postfx/src/post-process-stack.ts +1 -2
  103. package/libraries/sand/src/materials.ts +2 -32
  104. package/libraries/sand/src/sand-world.ts +0 -4
  105. package/modules/devtools/README.md +1 -3
  106. package/modules/devtools/src/api.ts +10 -34
  107. package/modules/devtools/src/blitz/fonts.ts +78 -0
  108. package/modules/devtools/src/blitz/host.ts +107 -31
  109. package/modules/devtools/src/blitz/panels/console.ts +20 -8
  110. package/modules/devtools/src/blitz/panels/elements.ts +212 -31
  111. package/modules/devtools/src/blitz/panels/gpu.ts +4 -2
  112. package/modules/devtools/src/blitz/panels/performance.ts +24 -12
  113. package/modules/devtools/src/blitz/panels/snapshot.ts +30 -8
  114. package/modules/devtools/src/blitz/panels/types.ts +2 -1
  115. package/modules/devtools/src/blitz/theme.ts +26 -4
  116. package/modules/devtools/src/data-bridge.ts +0 -16
  117. package/modules/devtools/src/index.ts +1 -11
  118. package/modules/devtools/src/init.ts +8 -51
  119. package/modules/devtools/src/scene-inspector.ts +2 -6
  120. package/modules/devtools/src/types.ts +5 -90
  121. package/modules/devtools/src/worker-sync.ts +3 -13
  122. package/modules/html-ui/src/doc-backend.ts +67 -4
  123. package/modules/html-ui/src/host.ts +11 -3
  124. package/modules/html-ui/src/protocol.ts +1 -1
  125. package/package.json +4 -5
  126. package/test/src/vision-test.ts +2 -4
  127. package/core/src/render/sab-canvas-overlay.ts +0 -122
  128. package/dist/core/src/render/sab-canvas-overlay.js +0 -83
  129. package/dist/modules/devtools/src/debug-overlay.js +0 -181
  130. package/dist/modules/devtools/src/doctor-panel.js +0 -163
  131. package/dist/modules/devtools/src/label-overlay.js +0 -129
  132. package/dist/modules/devtools/src/material-stats-panel.js +0 -171
  133. package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
  134. package/modules/devtools/src/debug-overlay.ts +0 -238
  135. package/modules/devtools/src/doctor-panel.ts +0 -182
  136. package/modules/devtools/src/label-overlay.ts +0 -154
  137. package/modules/devtools/src/material-stats-panel.ts +0 -188
  138. package/modules/devtools/src/sim-stats-panel.ts +0 -272
@@ -5,10 +5,10 @@
5
5
  // 1. Creating an IDevToolsDataRenderer adapter
6
6
  // 2. Instantiating DevToolsDataBridge (or BaseSceneInspector)
7
7
  // 3. Calling init()
8
- // 4. Wiring sim stats provider, panel extensions, overlay toggles, etc.
8
+ // 4. Wiring sim stats provider, debug overlay provider, etc.
9
9
  //
10
- // Also merges globally-registered panels/feeds/commands from the `devtools`
11
- // singleton (registered by plugins via ctx.devtools.registerPanel() etc.)
10
+ // Also merges globally-registered feeds/commands from the `devtools`
11
+ // singleton (registered by plugins via ctx.devtools.registerDataFeed() etc.)
12
12
  // and from worker manifests (via syncWorkerManifests).
13
13
  //
14
14
  // Returns the bridge instance for games that need post-init calls
@@ -24,8 +24,6 @@ import type {
24
24
  IAssetResolver,
25
25
  IDebugModeProvider,
26
26
  IDebugOverlayProvider,
27
- IDevToolsOverlayToggle,
28
- IDevToolsPanelExtension,
29
27
  IDevToolsRenderer,
30
28
  IPerformanceMetricsProvider,
31
29
  ISimStatsProvider,
@@ -33,10 +31,6 @@ import type {
33
31
  import { syncWorkerManifests, type WorkerSyncEntry } from "./worker-sync";
34
32
 
35
33
  export interface InitDevToolsOptions {
36
- // --- Panel extensions & toggles (game-declared) ---
37
- panels?: IDevToolsPanelExtension[];
38
- overlayToggles?: IDevToolsOverlayToggle[];
39
-
40
34
  // --- Providers (game-specific) ---
41
35
  simStatsProvider?: ISimStatsProvider & { start?: () => void; stop?: () => void };
42
36
  debugOverlayProvider?: IDebugOverlayProvider;
@@ -67,7 +61,7 @@ export interface InitDevToolsOptions {
67
61
 
68
62
  /**
69
63
  * Initialize DevTools with one call. Creates the bridge, wires all providers,
70
- merges globally-registered panels/feeds/commands, syncs worker manifests,
64
+ * merges globally-registered feeds/commands, syncs worker manifests,
71
65
  * and exposes the API on window.__sceneInspector.
72
66
  *
73
67
  * Returns the bridge instance.
@@ -81,20 +75,7 @@ export async function initDevTools(renderer: any, options: InitDevToolsOptions =
81
75
  || !!options.bridgeClass
82
76
  || isSceneRenderer(renderer);
83
77
 
84
- // 3. Register game-declared panels/toggles into the global registry
85
- // (these merge with plugin-registered ones)
86
- if (options.panels) {
87
- options.panels.forEach((panel) => {
88
- devtools.registerPanel(panel);
89
- });
90
- }
91
- if (options.overlayToggles) {
92
- options.overlayToggles.forEach((toggle) => {
93
- devtools.registerOverlayToggle(toggle);
94
- });
95
- }
96
-
97
- // 4. Sync worker manifests (fetches panels/feeds/commands from sim workers)
78
+ // 4. Sync worker manifests (fetches feeds/commands from sim workers)
98
79
  if (options.workerHosts && options.workerHosts.length > 0) {
99
80
  await syncWorkerManifests(options.workerHosts);
100
81
  }
@@ -136,10 +117,8 @@ export async function initDevTools(renderer: any, options: InitDevToolsOptions =
136
117
  wirePerformanceMetricsProvider(bridge, options.performanceMetricsProvider);
137
118
  }
138
119
 
139
- // 7. Merge globally-registered panels/toggles/feeds/commands into the bridge
140
- // The bridge's getPanelExtensions()/getOverlayToggles() already returns
141
- // game-declared ones; we augment the __sceneInspector API with
142
- // plugin-registered data feeds and commands.
120
+ // 7. Merge globally-registered feeds/commands/SAB stats into the bridge —
121
+ // augments the __sceneInspector API with plugin-registered methods.
143
122
  mergeGlobalRegistrations(bridge);
144
123
 
145
124
  // 8. Initialize the profiling system (if enabled)
@@ -193,10 +172,9 @@ function wirePerformanceMetricsProvider(bridge: DevToolsDataBridge, provider: an
193
172
  }
194
173
 
195
174
  /**
196
- * Merge globally-registered panels, data feeds, and commands from the `devtools`
175
+ * Merge globally-registered data feeds and commands from the `devtools`
197
176
  * singleton into the bridge's __sceneInspector API.
198
177
  *
199
- * - Panels: merged into getPanelExtensions() (game panels + plugin panels)
200
178
  * - Data feeds: added as methods on __sceneInspector (called synchronously)
201
179
  * - Commands: added as methods on __sceneInspector (forwarded to worker or called directly)
202
180
  * - SAB stats: added as methods on __sceneInspector (read from SAB)
@@ -204,27 +182,6 @@ function wirePerformanceMetricsProvider(bridge: DevToolsDataBridge, provider: an
204
182
  function mergeGlobalRegistrations(bridge: DevToolsDataBridge): void {
205
183
  const manifest = devtools.getManifest();
206
184
 
207
- // Augment getPanelExtensions to merge global registry panels
208
- const originalGetPanels = (bridge as any).getPanelExtensions?.bind(bridge);
209
- (bridge as any).getPanelExtensions = (): IDevToolsPanelExtension[] => {
210
- const gamePanels = originalGetPanels?.() ?? [];
211
- // Merge: game panels take precedence on id collisions
212
- const merged = new Map<string, IDevToolsPanelExtension>();
213
- manifest.panels.forEach((p) => { merged.set(p.id, p);; });
214
- gamePanels.forEach((p: any) => { merged.set(p.id, p);; });
215
- return Array.from(merged.values()).sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
216
- };
217
-
218
- // Augment getOverlayToggles to merge global registry toggles
219
- const originalGetToggles = (bridge as any).getOverlayToggles?.bind(bridge);
220
- (bridge as any).getOverlayToggles = (): IDevToolsOverlayToggle[] => {
221
- const gameToggles = originalGetToggles?.() ?? [];
222
- const merged = new Map<string, IDevToolsOverlayToggle>();
223
- manifest.toggles.forEach((t) => { merged.set(t.id, t);; });
224
- gameToggles.forEach((t: any) => { merged.set(t.id, t);; });
225
- return Array.from(merged.values());
226
- };
227
-
228
185
  // Add data feed methods to the __sceneInspector API
229
186
  // These are called synchronously by the panel via callInspector()
230
187
  const api = (window as any).__sceneInspector;
@@ -44,8 +44,6 @@ export abstract class BaseSceneInspector extends DevToolsDataBridge {
44
44
  return null;
45
45
  }
46
46
 
47
- // getPanelExtensions() and getOverlayToggles() inherited from DevToolsDataBridge.
48
-
49
47
  // --- Init ---
50
48
 
51
49
  init(renderer: IDevToolsRenderer): void {
@@ -568,9 +566,7 @@ export abstract class BaseSceneInspector extends DevToolsDataBridge {
568
566
  const cosX = Math.cos(angleX), sinX = Math.sin(angleX);
569
567
 
570
568
  const THUMB_SIZE = 192;
571
- const canvas = document.createElement("canvas");
572
- canvas.width = THUMB_SIZE;
573
- canvas.height = THUMB_SIZE;
569
+ const canvas = new OffscreenCanvas(THUMB_SIZE, THUMB_SIZE);
574
570
  const ctx = canvas.getContext("2d");
575
571
  if (!ctx) return null;
576
572
 
@@ -617,7 +613,7 @@ export abstract class BaseSceneInspector extends DevToolsDataBridge {
617
613
  }
618
614
  }
619
615
 
620
- const dataUrl = canvas.toDataURL("image/png");
616
+ const dataUrl = (canvas as unknown as { toDataURL(type?: string): string }).toDataURL("image/png");
621
617
  this.evictThumbnailCache();
622
618
  this.thumbnailCache.set(path, dataUrl);
623
619
  return dataUrl;
@@ -69,7 +69,9 @@ export interface IAssetResolver {
69
69
  getTextureUrl(uri: string): string | null;
70
70
  }
71
71
 
72
- /** Optional debug overlay toggles — games that support chunk grid / velocity arrows implement this. */
72
+ /** Optional debug draw toggles — games with GPU-side debug overlays
73
+ * (chunk grid, velocity arrows) implement this. Drives the game's own
74
+ * debug rendering, not a DOM overlay. */
73
75
  export interface IDebugOverlayProvider {
74
76
  setShowChunkGrid(show: boolean): void;
75
77
  getShowChunkGrid(): boolean;
@@ -91,73 +93,7 @@ export interface IPerformanceMetricsProvider {
91
93
  getPerformanceMetrics(): any;
92
94
  }
93
95
 
94
- /** Game-specific tab provider — games register custom DevTools tabs via this interface. */
95
- export interface IGameDevToolsProvider {
96
- getTabs(): GameDevToolsTab[];
97
- }
98
-
99
- export interface GameDevToolsTab {
100
- id: string;
101
- label: string;
102
- onActivate(container: HTMLElement): void;
103
- onDeactivate(): void;
104
- }
105
-
106
- /**
107
- * Game-specific DevTools panel extension.
108
- * Games declare tabs with HTML content, CSS, and a JS script that runs
109
- * in the DevTools panel context. The script receives DevToolsPanel helpers
110
- * and returns lifecycle callbacks.
111
- */
112
- export interface IDevToolsPanelExtension {
113
- /** Unique id for this extension (e.g. "debug-info", "game-panel"). */
114
- id: string;
115
- /** Tab label shown in the tab bar. */
116
- tabLabel: string;
117
- /** Tab tooltip. */
118
- tabTooltip?: string;
119
- /** Order/priority for tab placement. Core tabs: 0-100, game tabs: 100+. Default: 100. */
120
- order?: number;
121
- /** __sceneInspector methods that must exist for this tab to be shown. */
122
- requiredMethods?: string[];
123
- /** HTML content for the panel body (inserted into a container div). */
124
- html: string;
125
- /** CSS to inject into the panel document. */
126
- css?: string;
127
- /**
128
- * JS to eval in the panel context. Receives a DevToolsPanelHelpers object
129
- * and returns { onActivate?, onDeactivate?, onRefresh?, onDestroy? }.
130
- */
131
- script?: string;
132
- }
133
-
134
- /**
135
- * Game-specific overlay toggle for the overlays dropdown menu.
136
- */
137
- export interface IDevToolsOverlayToggle {
138
- /** Unique id (e.g. "chunk-grid", "vel-arrows"). */
139
- id: string;
140
- /** Label shown next to the checkbox. */
141
- label: string;
142
- /** __sceneInspector methods that must exist for this toggle to be shown. */
143
- requiredMethods?: string[];
144
- /**
145
- * JS to eval in the panel context. Receives DevToolsPanelHelpers.
146
- * Should set up a checkbox listener on #chk-{id} that calls callInspector.
147
- */
148
- script?: string;
149
- }
150
-
151
- // --- Debug Overlay Data Providers ---
152
-
153
- /** Provides entity data for the 2D debug overlay (chunk grid + velocity arrows). */
154
- export interface IDebugOverlayData {
155
- getEntityCount(): number;
156
- getEntityPosition(i: number): { x: number; y: number; z: number } | null;
157
- getEntityVelocity(i: number): { x: number; y: number; z: number } | null;
158
- getPlayerPosition(): { x: number; z: number } | null;
159
- getChunkGridConfig(): { chunkSize: number; chunksVisible: number };
160
- }
96
+ // --- Debug Raycast Providers ---
161
97
 
162
98
  /** Provides camera state for debug overlays. */
163
99
  export interface IDebugCamera {
@@ -170,8 +106,6 @@ export interface IDebugCamera {
170
106
  far: number;
171
107
  }
172
108
 
173
- // --- Debug Raycast Providers ---
174
-
175
109
  /** Result of a raycast against entities. */
176
110
  export interface IRaycastResult {
177
111
  entityIndex: number;
@@ -196,23 +130,6 @@ export interface IRaycastProvider {
196
130
  ): IRaycastResult | null;
197
131
  }
198
132
 
199
- // --- Label Overlay Providers ---
200
-
201
- /** A single label entry for the HTML label overlay. */
202
- export interface ILabelEntry {
203
- key: string;
204
- text: string;
205
- color: string;
206
- x: number;
207
- y: number;
208
- z: number;
209
- }
210
-
211
- /** Provides label data for the HTML label overlay. */
212
- export interface ILabelProvider {
213
- getLabels(): ILabelEntry[];
214
- }
215
-
216
133
  // --- Scene Sync Providers ---
217
134
 
218
135
  /** Entity snapshot for scene store sync. */
@@ -251,7 +168,6 @@ export interface IGameDevToolsExtension {
251
168
  debugOverlayProvider?: IDebugOverlayProvider;
252
169
  debugModeProvider?: IDebugModeProvider;
253
170
  performanceMetricsProvider?: IPerformanceMetricsProvider;
254
- tabProvider?: IGameDevToolsProvider;
255
171
  }
256
172
 
257
173
  // --- Sim Stats & Controls ---
@@ -274,8 +190,7 @@ export interface ISimStats {
274
190
 
275
191
  /**
276
192
  * Provides sim stats and control methods for the DevTools "Sim" tab.
277
- * Any sim-driven game (2D or 3D) can implement this; the reusable
278
- * `createSimStatsPanelExtension()` factory renders the tab.
193
+ * Any sim-driven game (2D or 3D) can implement this.
279
194
  */
280
195
  export interface ISimStatsProvider {
281
196
  getSimStats(): ISimStats;
@@ -1,16 +1,14 @@
1
1
  // ============================================================================
2
2
  // Renderer-side worker manifest sync — fetches the devtools manifest from
3
- // one or more sim workers via worker RPC, then wires the worker-registered panels,
4
- // data feeds, and commands into the renderer-side __sceneInspector API.
3
+ // one or more sim workers via worker RPC, then wires the worker-registered
4
+ // data feeds and commands into the renderer-side __sceneInspector API.
5
5
  //
6
6
  // Data feeds from workers are read from the devtools SAB (zero-copy).
7
7
  // Commands are forwarded via worker RPC.
8
- // Panel declarations are merged into the global registry.
9
8
  // ============================================================================
10
9
 
11
10
  import { createLogger } from "@downdraft/engine/util/logger";
12
- import { _devtoolsImpl, devtools, type DevToolsManifest } from "./api";
13
- import type { IDevToolsPanelExtension } from "./types";
11
+ import { _devtoolsImpl, type DevToolsManifest } from "./api";
14
12
 
15
13
  const log = createLogger("info");
16
14
 
@@ -79,14 +77,6 @@ async function syncOneWorker(entry: WorkerSyncEntry): Promise<void> {
79
77
  }
80
78
  }
81
79
 
82
- // Register panels (namespaced by prefix to avoid collisions)
83
- manifest.panels.forEach((panel) => {
84
- const namespaced: IDevToolsPanelExtension = prefix
85
- ? { ...panel, id: `${prefix}:${panel.id}` }
86
- : panel;
87
- devtools.registerPanel(namespaced);
88
- });
89
-
90
80
  // Register data feed readers — these read from the SAB synchronously
91
81
  // We register them as main-realm data feeds that read from the worker SAB
92
82
  if (sab) {
@@ -76,6 +76,19 @@ interface DocState {
76
76
  gpuTex: { ptr: number | bigint; tex: unknown; w: number; h: number } | null;
77
77
  stats: { frames: number; resolveMs: number; paintMs: number; diffMs: number; bytes: number };
78
78
  statsLast: number;
79
+ /** smoothWheel (PanelSpec): wheel deltas accumulate here and drain as
80
+ * eased per-tick steps in pump() instead of each event repainting an
81
+ * instant jump. Deltas/positions stay in doc CSS px (scale applied at
82
+ * dispatch); DOM sign convention (+y = scroll down). */
83
+ smoothWheel: boolean;
84
+ wheelRx: number;
85
+ wheelRy: number;
86
+ wheelX: number;
87
+ wheelY: number;
88
+ wheelMods?: string[];
89
+ /** performance.now() of the last step — the ease is dt-based so the curve
90
+ * holds regardless of pump cadence. */
91
+ wheelT: number;
79
92
  }
80
93
 
81
94
  // Bound-buffer header (u32 indices) — mirrors BoundBuf layout in lib.rs.
@@ -211,9 +224,32 @@ export function createDocCore(emit: (m: WorkerToUi) => void, gpuHooks?: DocGpuHo
211
224
  }
212
225
  }
213
226
 
227
+ // Smooth-wheel ease: fraction of the remainder applied per elapsed ms.
228
+ // τ≈45ms → ~30% per 16ms tick, ~95% drained in ~135ms — snappy but
229
+ // animated. A 1/MAX_TICKS floor bounds any backlog at ~320ms of drain —
230
+ // a fast wheel can't make scroll trail input indefinitely. Dispatching
231
+ // real wheel events keeps Blitz's hit-testing, scroll chaining, scrollbar
232
+ // reveal, and scroll DOM events intact.
233
+ const WHEEL_TAU_MS = 45;
234
+ const WHEEL_MAX_TICKS = 20;
235
+ function stepWheel(s: DocState, now: number): void {
236
+ const dt = Math.min(50, Math.max(1, now - s.wheelT));
237
+ s.wheelT = now;
238
+ const f = Math.max(1 - Math.exp(-dt / WHEEL_TAU_MS), 1 / WHEEL_MAX_TICKS);
239
+ // Snap the tail: a step below ~¾px/axis applies the whole residue instead
240
+ // of asymptoting through invisible sub-pixel repaints.
241
+ const dx = Math.abs(s.wheelRx) * f < 0.75 ? s.wheelRx : s.wheelRx * f;
242
+ const dy = Math.abs(s.wheelRy) * f < 0.75 ? s.wheelRy : s.wheelRy * f;
243
+ s.wheelRx -= dx;
244
+ s.wheelRy -= dy;
245
+ const k = s.scale;
246
+ s.doc.wheel(s.wheelX * k, s.wheelY * k, dx * k, dy * k, s.wheelMods);
247
+ }
248
+
214
249
  function pump() {
215
250
  const now = performance.now();
216
251
  for (const [id, s] of docs.entries()) {
252
+ if (s.smoothWheel && (s.wheelRx !== 0 || s.wheelRy !== 0)) stepWheel(s, now);
217
253
  const events = s.doc.pollEvents();
218
254
  if (events.length) emit({ type: "events", id, events });
219
255
  if (!s.doc.pending()) continue;
@@ -284,6 +320,8 @@ export function createDocCore(emit: (m: WorkerToUi) => void, gpuHooks?: DocGpuHo
284
320
  doc, scale: m.scale, cssW: m.cssW, cssH: m.cssH,
285
321
  maxFps: m.maxFps ?? 0, lastFrame: -1e9,
286
322
  sab: null, sabU32: null, sabDisabled: false, gpuTex: null,
323
+ smoothWheel: m.smoothWheel === true,
324
+ wheelRx: 0, wheelRy: 0, wheelX: 0, wheelY: 0, wheelT: 0,
287
325
  stats: freshStats(), statsLast: 0,
288
326
  };
289
327
  docs.set(m.id, s);
@@ -295,9 +333,16 @@ export function createDocCore(emit: (m: WorkerToUi) => void, gpuHooks?: DocGpuHo
295
333
  if (s) s.maxFps = m.maxFps;
296
334
  break;
297
335
  }
298
- case "setHtml":
336
+ case "setHtml": {
299
337
  if (/float\s*:/.test(m.html)) emit({ type: "error", id: m.id, message: "CSS float hangs Blitz layout — use flex instead" });
300
- docs.get(m.id)?.doc.setHtml(m.html); break;
338
+ const s = docs.get(m.id);
339
+ if (!s) break;
340
+ // Drop any in-flight smooth-wheel drain — its remainder belongs to
341
+ // the document being replaced.
342
+ s.wheelRx = s.wheelRy = 0;
343
+ s.doc.setHtml(m.html);
344
+ break;
345
+ }
301
346
  case "resize": {
302
347
  const s = docs.get(m.id);
303
348
  if (s) {
@@ -403,9 +448,27 @@ export function createDocCore(emit: (m: WorkerToUi) => void, gpuHooks?: DocGpuHo
403
448
  const k = s.scale;
404
449
  switch (msg.kind) {
405
450
  case "move": s.doc.pointerMove(msg.x * k, msg.y * k, msg.mods); break;
406
- case "down": s.doc.pointerDown(msg.x * k, msg.y * k, msg.button, msg.mods); break;
451
+ case "down":
452
+ // A press interrupts any in-flight wheel scroll (mirrors
453
+ // Blitz's interrupt_animation semantics for user input).
454
+ s.wheelRx = 0; s.wheelRy = 0;
455
+ s.doc.pointerDown(msg.x * k, msg.y * k, msg.button, msg.mods);
456
+ break;
407
457
  case "up": s.doc.pointerUp(msg.x * k, msg.y * k, msg.button, msg.mods); break;
408
- case "wheel": s.doc.wheel(msg.x * k, msg.y * k, msg.deltaX * k, msg.deltaY * k, msg.mods); break;
458
+ case "wheel":
459
+ if (s.smoothWheel) {
460
+ // A fresh gesture back-dates wheelT so the first pump tick
461
+ // applies a full ~16ms step immediately — no dead frame.
462
+ if (s.wheelRx === 0 && s.wheelRy === 0) s.wheelT = performance.now() - 16;
463
+ s.wheelRx += msg.deltaX;
464
+ s.wheelRy += msg.deltaY;
465
+ s.wheelX = msg.x;
466
+ s.wheelY = msg.y;
467
+ s.wheelMods = msg.mods;
468
+ } else {
469
+ s.doc.wheel(msg.x * k, msg.y * k, msg.deltaX * k, msg.deltaY * k, msg.mods);
470
+ }
471
+ break;
409
472
  case "key": s.doc.key(msg.down, msg.key, msg.code, msg.text, msg.mods); break;
410
473
  }
411
474
  break;
@@ -10,6 +10,7 @@ import type { OsrDomEvent } from "@downdraft/engine/libraries/blitz-ui/native-os
10
10
  import type { InputEventControl, RendererInputBus } from "@downdraft/engine/module/renderer-module";
11
11
  import { getHostCapabilities, getNativeHost } from "@downdraft/engine/platform/runtime";
12
12
  import { createLogger } from "@downdraft/engine/util/logger";
13
+ import { untrackedTimeout } from "@downdraft/engine/util/session-timer";
13
14
  import { PanelBlitPass } from "./composite";
14
15
  import { createLocalBackend, createWorkerBackend, type DocBackend } from "./doc-backend";
15
16
  import type { DocInputMsg, DocMutation, NavNodeInfo, UiPointerMsg, WorkerToUi } from "./protocol";
@@ -40,6 +41,10 @@ export interface PanelSpec {
40
41
  zoom?: number;
41
42
  /** Raster-rate cap for animating docs (0 = uncapped). Static docs repaint on demand regardless. */
42
43
  maxFps?: number;
44
+ /** Ease wheel deltas out at frame cadence instead of applying each detent
45
+ * as an instant jump — scrolling animates smoothly and repaints stay at
46
+ * one per pump tick regardless of input rate. Default false. */
47
+ smoothWheel?: boolean;
43
48
  /** Initial markup (or `vdom` rendered beforehand by the caller). */
44
49
  html: string;
45
50
  /** When false the panel never receives pointer input and `panelAt` skips
@@ -306,7 +311,7 @@ export class HtmlUiHost {
306
311
  this.order.push(id);
307
312
  // Doc viewport scale = raster px per CSS px = scale * zoom; layout size
308
313
  // is rect/zoom CSS px so the raster still covers rect*scale texture px.
309
- this.send({ type: "create", id, cssW: spec.rect.w / zoom, cssH: spec.docH ?? spec.rect.h / zoom, scale: scale * zoom, html: spec.html, maxFps: spec.maxFps });
314
+ this.send({ type: "create", id, cssW: spec.rect.w / zoom, cssH: spec.docH ?? spec.rect.h / zoom, scale: scale * zoom, html: spec.html, maxFps: spec.maxFps, smoothWheel: spec.smoothWheel });
310
315
  // GPU-direct: create the panel texture eagerly so the worker's texBind
311
316
  // lands before its first upload — the host knows raster dims at mount.
312
317
  if (this.gpuReady) this.bindPanelTex(panel);
@@ -470,6 +475,9 @@ export class HtmlUiHost {
470
475
  * `incrementalDom` is false until then, and forever on old cdylibs). */
471
476
  get docCaps(): { incrementalDom: boolean } { return this.caps; }
472
477
 
478
+ /** Which doc backend is live — "worker" (raster off-thread) or "local". */
479
+ get backendKind(): "worker" | "local" { return this.backend.kind; }
480
+
473
481
  /** Compositor surface for GameRenderer's end-of-frame UI pass. */
474
482
  get compositor(): { hasContent(): boolean; render(pass: GPURenderPassEncoder, w: number, h: number): void } {
475
483
  return {
@@ -511,7 +519,7 @@ export class HtmlUiHost {
511
519
  this.terminating = true;
512
520
  this.send({ type: "gpuDetach" });
513
521
  const backend = this.backend;
514
- setTimeout(() => {
522
+ untrackedTimeout(() => {
515
523
  if (!this.gpuDetachAcked) backend.dispose();
516
524
  this.destroyRetired();
517
525
  }, 150);
@@ -912,7 +920,7 @@ export class HtmlUiHost {
912
920
  || (p.postedBindPtr !== null && BigInt(ptr) === p.postedBindPtr));
913
921
  if (borrowed) {
914
922
  const key = BigInt(ptr);
915
- const timer = setTimeout(() => {
923
+ const timer = untrackedTimeout(() => {
916
924
  if (this.retiredTex.delete(key)) t.destroy();
917
925
  }, 1000);
918
926
  this.retiredTex.set(key, { tex: t, timer });
@@ -52,7 +52,7 @@ export interface NavNodeInfo {
52
52
 
53
53
  /** Host → backend */
54
54
  export type UiToWorker =
55
- | { type: "create"; id: string; cssW: number; cssH: number; scale: number; html: string; maxFps?: number }
55
+ | { type: "create"; id: string; cssW: number; cssH: number; scale: number; html: string; maxFps?: number; smoothWheel?: boolean }
56
56
  | { type: "fps"; id: string; maxFps: number }
57
57
  | { type: "setHtml"; id: string; html: string }
58
58
  | { type: "resize"; id: string; cssW: number; cssH: number; scale: number }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downdraft/engine",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/knackstedt/downdraft.git",
@@ -844,13 +844,12 @@
844
844
  },
845
845
  "dependencies": {
846
846
  "@bokuweb/zstd-wasm": "^0.0.27",
847
+ "@dimforge/rapier3d-compat": "^0.19.3",
847
848
  "@gltf-transform/core": "^4.4.2",
848
849
  "@gltf-transform/extensions": "^4.4.2",
849
850
  "@gltf-transform/functions": "^4.4.2",
850
851
  "@h00w/basis-universal-transcoder": "^2.1.0",
851
- "@jitl/quickjs-singlefile-browser-release-sync": "^0.32.0",
852
852
  "acorn": "^8.15.0",
853
- "buffer": "^6.0.3",
854
853
  "comment-json": "^5.0.0",
855
854
  "dns-packet": "^5.6.1",
856
855
  "draco3dgltf": "^1.5.7",
@@ -859,13 +858,13 @@
859
858
  "ktx2-encoder": "^0.6.0",
860
859
  "meshoptimizer": "^1.2.0",
861
860
  "quickjs-emscripten": "^0.32.0",
861
+ "recast-navigation": "^0.43.1",
862
862
  "wgpu-matrix": "^3.4.2",
863
863
  "xxh3-ts": "^2.0.1",
864
- "xxhash-wasm": "^0.4.2",
865
864
  "zustand": "^5.0.2"
866
865
  },
867
866
  "peerDependencies": {
868
- "@downdraft/platform-native": "^0.2.0"
867
+ "@downdraft/platform-native": "^0.3.1"
869
868
  },
870
869
  "peerDependenciesMeta": {
871
870
  "@downdraft/platform-native": {
@@ -251,14 +251,12 @@ export class VisionTest {
251
251
  height: number,
252
252
  ): Promise<string> {
253
253
  const data = await this.captureTexture(texture, width, height);
254
- const canvas = document.createElement("canvas");
255
- canvas.width = width;
256
- canvas.height = height;
254
+ const canvas = new OffscreenCanvas(width, height);
257
255
  const ctx = canvas.getContext("2d")!;
258
256
  const imageData = ctx.createImageData(width, height);
259
257
  imageData.data.set(data);
260
258
  ctx.putImageData(imageData, 0, 0);
261
- return canvas.toDataURL("image/png");
259
+ return (canvas as unknown as { toDataURL(type?: string): string }).toDataURL("image/png");
262
260
  }
263
261
 
264
262
  destroy(): void {
@@ -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
- }