@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
@@ -71,32 +71,18 @@ export enum Material {
71
71
  // --- Ingredients ---
72
72
  // Ice (frozen water — produced by cooling)
73
73
  Ice = 64,
74
+ // (IDs 67–68, 71–73, 76–77, 79–81, 83–88 were inert alchemy materials —
75
+ // retired; keep the gaps so old saves don't remap onto new materials.)
74
76
  // Liquids
75
77
  Ether = 65, // ethereal solvent — light, volatile, glowing
76
78
  Blood = 66, // viscous, necrotic-tinted
77
- Syrup = 67, // thick, dense, sweet (binding agent)
78
- NightshadeExtract = 68,// toxic liquid
79
79
  TrollBlood = 69, // viscous, healing + necrotic
80
80
  LiquidShadow = 70, // dense, light-absorbing
81
- LoveEssence = 71, // psychic liquid
82
- HateEssence = 72, // psychic liquid, corrosive
83
- DreamMist = 73, // ethereal + psychic, gas-like liquid
84
81
  VoidEssence = 74, // dense, necrotic, reality-bending
85
82
  // Powders (solid, fall)
86
83
  Sulfur = 75, // yellow powder, flammable, corrosive
87
- GroundEyeOfNewt = 76, // psychic powder
88
- GroundBatWing = 77, // ethereal powder
89
84
  BoneDust = 78, // necrotic powder
90
- IronFilings = 79, // kinetic + dense powder, magnetic
91
- MoonstoneDust = 80, // luminous + ethereal powder
92
- CrystalDust = 81, // luminous + kinetic powder
93
85
  MushroomSpores = 82, // toxic + psychic powder, grows
94
- DragonScale = 83, // thermal + kinetic, very dense
95
- PhoenixFeather = 84, // thermal + healing, light
96
- UnicornHorn = 85, // healing + luminous, rare
97
- MandrakeRoot = 86, // toxic + psychic, solid root
98
- SpiderSilk = 87, // kinetic + ethereal, very light
99
- GraveDust = 88, // necrotic + dense
100
86
  StarShard = 89, // luminous + ethereal + kinetic, glowing crystal
101
87
  TimeSand = 90, // kinetic + ethereal, reality-bending
102
88
  // Gases
@@ -283,30 +269,14 @@ export const MATERIALS: Record<number, MaterialDef> = {
283
269
  // --- Liquids ---
284
270
  [Material.Ether]: def(65, "Ether", [0.55, 0.45, 0.85, 0.8], { gravity: 1.5, gravityDir: 1, density: 0.7, liquid: true, flammable: true, burnTime: 40, albedo: 0.2, reflectivity: 0.4, brightness: 1.1 }),
285
271
  [Material.Blood]: def(66, "Blood", [0.7, 0.05, 0.05, 0.95], { gravity: 2, gravityDir: 1, density: 1.06, liquid: true, albedo: 0.3, reflectivity: 0.2, brightness: 0.8 }),
286
- [Material.Syrup]: def(67, "Syrup", [0.6, 0.4, 0.15, 0.95], { gravity: 1, gravityDir: 1, density: 1.4, liquid: true, albedo: 0.3, reflectivity: 0.3, brightness: 0.9 }),
287
- [Material.NightshadeExtract]: def(68, "Nightshade Extract", [0.3, 0.05, 0.35, 0.95], { gravity: 2, gravityDir: 1, density: 1.1, liquid: true, albedo: 0.2, reflectivity: 0.1, brightness: 0.7 }),
288
272
  [Material.TrollBlood]: def(69, "Troll Blood", [0.4, 0.2, 0.15, 0.95], { gravity: 2, gravityDir: 1, density: 1.2, liquid: true, albedo: 0.25, reflectivity: 0.15, brightness: 0.8 }),
289
273
  [Material.LiquidShadow]: def(70, "Liquid Shadow", [0.05, 0.05, 0.12, 0.98], { gravity: 3, gravityDir: 1, density: 2.0, liquid: true, albedo: 0.0, reflectivity: 0.0, brightness: 0.3 }),
290
- [Material.LoveEssence]: def(71, "Love Essence", [0.95, 0.4, 0.6, 0.9], { gravity: 1.5, gravityDir: 1, density: 0.9, liquid: true, albedo: 0.3, reflectivity: 0.4, brightness: 1.0 }),
291
- [Material.HateEssence]: def(72, "Hate Essence", [0.2, 0.1, 0.15, 0.9], { gravity: 2, gravityDir: 1, density: 1.3, liquid: true, albedo: 0.2, reflectivity: 0.2, brightness: 0.7 }),
292
- [Material.DreamMist]: def(73, "Dream Mist", [0.6, 0.7, 0.95, 0.6], { gravity: 1, gravityDir: -1, density: 0.3, liquid: true, albedo: 0.2, reflectivity: 0.5, brightness: 1.2 }),
293
274
  [Material.VoidEssence]: def(74, "Void Essence", [0.1, 0.0, 0.2, 0.95], { gravity: 4, gravityDir: 1, density: 3.0, liquid: true, albedo: 0.0, reflectivity: 0.1, brightness: 0.5 }),
294
275
 
295
276
  // --- Alchemy game: powders (solid, fall) ---
296
277
  [Material.Sulfur]: def(75, "Sulfur", [0.9, 0.85, 0.2, 1.0], { gravity: 1, gravityDir: 1, density: 2.0, solid: true, flammable: true, burnTime: 60, albedo: 0.5, reflectivity: 0.1, brightness: 0.9 }),
297
- [Material.GroundEyeOfNewt]: def(76, "Ground Eye of Newt", [0.5, 0.35, 0.2, 1.0], { gravity: 1, gravityDir: 1, density: 1.2, solid: true, albedo: 0.4, brightness: 0.8 }),
298
- [Material.GroundBatWing]: def(77, "Ground Bat Wing", [0.3, 0.25, 0.35, 1.0], { gravity: 0.8, gravityDir: 1, density: 0.6, solid: true, albedo: 0.3, brightness: 0.7 }),
299
278
  [Material.BoneDust]: def(78, "Bone Dust", [0.85, 0.82, 0.75, 1.0], { gravity: 1, gravityDir: 1, density: 1.5, solid: true, albedo: 0.5, brightness: 0.8 }),
300
- [Material.IronFilings]: def(79, "Iron Filings", [0.55, 0.55, 0.6, 1.0], { gravity: 1.5, gravityDir: 1, density: 7.8, solid: true, magnetic: true, albedo: 0.5, reflectivity: 0.3 }),
301
- [Material.MoonstoneDust]: def(80, "Moonstone Dust", [0.8, 0.85, 0.95, 1.0], { gravity: 1, gravityDir: 1, density: 1.0, solid: true, albedo: 0.4, reflectivity: 0.5, brightness: 1.3 }),
302
- [Material.CrystalDust]: def(81, "Crystal Dust", [0.6, 0.9, 0.95, 1.0], { gravity: 1, gravityDir: 1, density: 2.5, solid: true, albedo: 0.3, reflectivity: 0.6, brightness: 1.2 }),
303
279
  [Material.MushroomSpores]: def(82, "Mushroom Spores", [0.6, 0.5, 0.4, 0.9], { gravity: 0.5, gravityDir: 1, density: 0.4, solid: true, albedo: 0.3, brightness: 0.8 }),
304
- [Material.DragonScale]: def(83, "Dragon Scale", [0.5, 0.15, 0.1, 1.0], { gravity: 1.5, gravityDir: 1, density: 5.0, solid: true, albedo: 0.4, reflectivity: 0.3, brightness: 1.0 }),
305
- [Material.PhoenixFeather]: def(84, "Phoenix Feather", [0.95, 0.5, 0.15, 0.9], { gravity: 0.3, gravityDir: 1, density: 0.3, solid: true, flammable: true, burnTime: 200, albedo: 0.4, brightness: 1.4 }),
306
- [Material.UnicornHorn]: def(85, "Unicorn Horn", [0.95, 0.9, 1.0, 1.0], { gravity: 1, gravityDir: 1, density: 1.8, solid: true, albedo: 0.5, reflectivity: 0.5, brightness: 1.5 }),
307
- [Material.MandrakeRoot]: def(86, "Mandrake Root", [0.5, 0.4, 0.25, 1.0], { gravity: 1, gravityDir: 1, density: 1.3, solid: true, albedo: 0.4, brightness: 0.7 }),
308
- [Material.SpiderSilk]: def(87, "Spider Silk", [0.9, 0.9, 0.85, 0.8], { gravity: 0.2, gravityDir: 1, density: 0.2, solid: true, albedo: 0.4, reflectivity: 0.2, brightness: 0.9 }),
309
- [Material.GraveDust]: def(88, "Grave Dust", [0.3, 0.28, 0.25, 1.0], { gravity: 1, gravityDir: 1, density: 1.8, solid: true, albedo: 0.3, brightness: 0.6 }),
310
280
  [Material.StarShard]: def(89, "Star Shard", [0.7, 0.8, 1.0, 1.0], { gravity: 1, gravityDir: 1, density: 2.0, solid: true, albedo: 0.3, reflectivity: 0.7, brightness: 1.6 }),
311
281
  [Material.TimeSand]: def(90, "Time Sand", [0.4, 0.3, 0.6, 0.9], { gravity: 1, gravityDir: 1, density: 1.5, solid: true, albedo: 0.3, reflectivity: 0.4, brightness: 1.2 }),
312
282
 
@@ -2130,10 +2130,6 @@ export class SandWorld {
2130
2130
  }
2131
2131
  }
2132
2132
 
2133
- // --- PhoenixFeather near fire/lava: glows brighter, resists burning ---
2134
- // (flammable but long burnTime already set; here we just let it shimmer)
2135
- // --- DragonScale: very dense, sinks through liquids (handled by density) ---
2136
-
2137
2133
  // --- LiquidShadow + light source (fire/plasma) → dissipates to smoke ---
2138
2134
  if (mat === Material.LiquidShadow) {
2139
2135
  let lightNi = -1;
@@ -14,12 +14,10 @@ bun add @downdraft/engine/modules/devtools
14
14
  - `BaseSceneInspector`
15
15
  - `DEVTOOLS_REALM`
16
16
  - `DEVTOOLS_SAB_MAX_FEEDS`
17
- - `DebugOverlay`
18
17
  - `DebugRaycast`
19
18
  - `DevToolsDataBridge`
20
19
  - `GridRenderer`
21
20
  - `HeightRulerRenderer`
22
- - `LabelOverlay`
23
- - …and 29 more
21
+ - …and 24 more
24
22
 
25
23
  See the [Downdraft engine repository](https://github.com/knackstedt/downdraft) for architecture docs and examples.
@@ -3,23 +3,21 @@
3
3
  // engine systems, regardless of which thread they run on.
4
4
  //
5
5
  // Realm detection determines transport:
6
- // - Main realm: panels/feeds/commands registered directly on
6
+ // - Main realm: feeds/commands registered directly on
7
7
  // window.__sceneInspector via DevToolsDataBridge.
8
- // - Worker realm: panels/feeds/commands registered in a worker-side
9
- // registry. Data feeds are written to a devtools SAB region (zero-copy,
10
- // synchronous reads). Commands are forwarded via worker RPC. Panel
11
- // declarations are synced to the renderer via a one-time manifest RPC.
8
+ // - Worker realm: feeds/commands registered in a worker-side registry.
9
+ // Data feeds are written to a devtools SAB region (zero-copy,
10
+ // synchronous reads). Commands are forwarded via worker RPC.
12
11
  //
13
12
  // Plugins import `devtools` from @downdraft/engine/modules/devtools and call
14
- // registerPanel/registerDataFeed/registerCommand/registerSABStat — the same
15
- // API works in both realms.
13
+ // registerDataFeed/registerCommand/registerSABStat — the same API works in
14
+ // both realms.
16
15
  // ============================================================================
17
16
 
18
17
  import { resourceToken } from "@downdraft/engine/ecs/resource";
19
18
  import { createLogger } from "@downdraft/engine/util/logger";
20
19
  import type { DebugViewDescriptor } from "./debug-view-descriptors";
21
20
  import { BUILTIN_VIEW_DESCRIPTORS } from "./debug-view-descriptors";
22
- import type { IDevToolsOverlayToggle, IDevToolsPanelExtension } from "./types";
23
21
 
24
22
  const log = createLogger("info");
25
23
 
@@ -56,7 +54,7 @@ export const DEVTOOLS_REALM = detectRealm();
56
54
  // synchronously. No RPC polling needed.
57
55
  //
58
56
  // Layout:
59
- // [0..3] manifestVersion: Uint32 (bumped when panels/feeds change)
57
+ // [0..3] manifestVersion: Uint32 (bumped when feeds/commands change)
60
58
  // [4..7] feedCount: Uint32
61
59
  // [8..] feed entries: each is [lengthBytes: Uint32][JSON bytes...]
62
60
  // (lengthBytes = 0 means "not yet written" / "unavailable")
@@ -124,11 +122,7 @@ export interface DevToolsAPI {
124
122
  /** Current realm ("main" or "worker"). */
125
123
  readonly realm: "main" | "worker";
126
124
 
127
- // --- Panels & toggles (declarative, realm-transparent) ---
128
- registerPanel(ext: IDevToolsPanelExtension): void;
129
- registerOverlayToggle(toggle: IDevToolsOverlayToggle): void;
130
-
131
- // --- Data feeds (read by panel, realm chooses transport) ---
125
+ // --- Data feeds (read by devtools frontends, realm chooses transport) ---
132
126
  // In main realm: fn is called directly when the panel queries __sceneInspector.
133
127
  // In worker realm: fn is called each tick (or at writeRate), result JSON-serialized
134
128
  // to the devtools SAB. Renderer reads synchronously from SAB.
@@ -165,7 +159,7 @@ export interface DevToolsAPI {
165
159
  getViews(): DebugViewDescriptor[];
166
160
 
167
161
  // --- Manifest (renderer fetches from worker via RPC) ---
168
- /** Get the full manifest: panels, toggles, data feed names, commands, SAB stats. */
162
+ /** Get the full manifest: data feed names, commands, SAB stats. */
169
163
  getManifest(): DevToolsManifest;
170
164
 
171
165
  // --- Worker tick hook ---
@@ -178,8 +172,6 @@ export interface DevToolsAPI {
178
172
  }
179
173
 
180
174
  export interface DevToolsManifest {
181
- panels: IDevToolsPanelExtension[];
182
- toggles: IDevToolsOverlayToggle[];
183
175
  dataFeeds: { name: string; feedIndex: number; writeRateHz: number }[];
184
176
  commands: string[];
185
177
  sabStats: DevToolsSABStat[];
@@ -200,8 +192,6 @@ interface RegisteredDataFeed {
200
192
  class DevToolsAPIImpl implements DevToolsAPI {
201
193
  readonly realm: "main" | "worker";
202
194
 
203
- private panels = new Map<string, IDevToolsPanelExtension>();
204
- private toggles = new Map<string, IDevToolsOverlayToggle>();
205
195
  private dataFeeds = new Map<string, RegisteredDataFeed>();
206
196
  private dataFeedNames: string[] = []; // ordered list for feedIndex assignment
207
197
  private commands = new Map<string, (...args: any[]) => any>();
@@ -223,16 +213,6 @@ class DevToolsAPIImpl implements DevToolsAPI {
223
213
  this.realm = realm;
224
214
  }
225
215
 
226
- registerPanel(ext: IDevToolsPanelExtension): void {
227
- this.panels.set(ext.id, ext);
228
- this.bumpManifest();
229
- }
230
-
231
- registerOverlayToggle(toggle: IDevToolsOverlayToggle): void {
232
- this.toggles.set(toggle.id, toggle);
233
- this.bumpManifest();
234
- }
235
-
236
216
  registerDataFeed(name: string, fn: () => any, writeRateHz: number = 10): void {
237
217
  if (this.dataFeeds.has(name)) {
238
218
  // Update fn/rate, keep feedIndex and hasWritten
@@ -318,8 +298,6 @@ class DevToolsAPIImpl implements DevToolsAPI {
318
298
 
319
299
  getManifest(): DevToolsManifest {
320
300
  return {
321
- panels: Array.from(this.panels.values()),
322
- toggles: Array.from(this.toggles.values()),
323
301
  dataFeeds: this.dataFeedNames.map((name, i) => ({
324
302
  name,
325
303
  feedIndex: i,
@@ -414,8 +392,6 @@ class DevToolsAPIImpl implements DevToolsAPI {
414
392
  }
415
393
 
416
394
  clear(): void {
417
- this.panels.clear();
418
- this.toggles.clear();
419
395
  this.dataFeeds.clear();
420
396
  this.dataFeedNames = [];
421
397
  this.commands.clear();
@@ -443,7 +419,7 @@ const _devtools = new DevToolsAPIImpl(DEVTOOLS_REALM);
443
419
  * the appropriate transport for data feeds (direct call vs SAB) and
444
420
  * commands (direct call vs RPC forwarding).
445
421
  *
446
- * Plugins import this and call registerPanel/registerDataFeed/registerCommand.
422
+ * Plugins import this and call registerDataFeed/registerCommand.
447
423
  * The same code works in both realms.
448
424
  */
449
425
  export const devtools: DevToolsAPI = _devtools;
@@ -0,0 +1,78 @@
1
+ // ============================================================================
2
+ // fonts.ts — bind the host's real system fonts into the devtools dock doc.
3
+ //
4
+ // The OSR rasterizer ships a single embedded DejaVu Sans as the fallback for
5
+ // every generic family — legible, but `monospace` resolves to it too, so the
6
+ // console/REPL/tree read as proportional sans. Same fix as cruiser's
7
+ // libraries/app-shell/fonts.ts: resolve the user's actual sans + mono faces
8
+ // via fontconfig (fc-match -f %{file}), register them as `ui://` blobs on the
9
+ // HtmlUiHost before mount, and emit @font-face rules the dock stylesheet
10
+ // binds ahead of the generic fallbacks. Any failure (no fc-match, non-Linux
11
+ // host, unreadable file) leaves the embedded fallback in place.
12
+ // ============================================================================
13
+
14
+ import { execFileSync } from "node:child_process";
15
+ import { readFileSync } from "node:fs";
16
+
17
+ /** Resource sink — HtmlUiHost.registerResource, or anything shaped like it. */
18
+ type ResourceSink = { registerResource(url: string, bytes: Uint8Array | ArrayBuffer): void };
19
+
20
+ interface Face { family: string; pattern: string; urlName: string; weight: number }
21
+
22
+ // Blitz-dom only fetches a @font-face source whose format is detectable —
23
+ // either an explicit format() hint or a file extension on the url — so the
24
+ // urls carry the resolved file's real extension.
25
+ const FACES: Face[] = [
26
+ { family: "DevTools Sans", pattern: "sans-serif", urlName: "sans", weight: 400 },
27
+ { family: "DevTools Sans", pattern: "sans-serif:weight=bold", urlName: "sans-bold", weight: 700 },
28
+ { family: "DevTools Mono", pattern: "monospace", urlName: "mono", weight: 400 },
29
+ { family: "DevTools Mono", pattern: "monospace:weight=bold", urlName: "mono-bold", weight: 700 },
30
+ ];
31
+
32
+ /** `fc-match -f %{file}` for a fontconfig pattern; null when fc-match is
33
+ * absent or the match isn't a font file Blitz can load. */
34
+ function fcFontFile(pattern: string): string | null {
35
+ try {
36
+ const p = execFileSync("fc-match", ["-f", "%{file}", pattern], { timeout: 5000 }).toString().trim();
37
+ return /\.(ttf|otf|woff2?)$/i.test(p) ? p : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function fontExt(path: string): string {
44
+ return path.match(/\.(ttf|otf|woff2?)$/i)?.[0] ?? ".ttf";
45
+ }
46
+
47
+ /**
48
+ * Register system sans + mono faces as `ui://` resources and return the
49
+ * @font-face CSS to prepend to the dock stylesheet. Returns "" when nothing
50
+ * registered — the stylesheet's named families then fail to resolve and the
51
+ * generic fallbacks (embedded DejaVu) apply.
52
+ *
53
+ * Call before mount — the doc backend applies resources synchronously and in
54
+ * order, so the first doc resolve sees them.
55
+ */
56
+ export function registerDevtoolsFonts(sink: ResourceSink): string {
57
+ try {
58
+ // Both weights of a family are required: a 700 rule pointing at the
59
+ // regular file keeps bold text in-family instead of falling back.
60
+ const files = new Map<string, string>();
61
+ for (const f of FACES) files.set(f.urlName, fcFontFile(f.pattern) ?? "");
62
+ if (!files.get("sans")) return "";
63
+ if (!files.get("mono")) files.set("mono", files.get("sans")!);
64
+ if (!files.get("sans-bold")) files.set("sans-bold", files.get("sans")!);
65
+ if (!files.get("mono-bold")) files.set("mono-bold", files.get("mono")!);
66
+ let css = "";
67
+ for (const f of FACES) {
68
+ const file = files.get(f.urlName)!;
69
+ const url = `ui://devtools-fonts/${f.urlName}${fontExt(file)}`;
70
+ sink.registerResource(url, readFileSync(file));
71
+ css += `@font-face { font-family:'${f.family}'; font-weight:${f.weight}; src:url('${url}'); }\n`;
72
+ }
73
+ return css;
74
+ } catch {
75
+ // fc-match absent / unreadable font file — the embedded fallback applies.
76
+ return "";
77
+ }
78
+ }
@@ -16,12 +16,13 @@ import { createLogger } from "@downdraft/engine";
16
16
  import { CdpBridge, DevtoolsBackend } from "@downdraft/engine/libraries/devtools";
17
17
  import { HtmlUiHost, type OsrDomEvent, type UiPanelHandle } from "@downdraft/engine/modules/html-ui";
18
18
 
19
+ import { registerDevtoolsFonts } from "./fonts";
19
20
  import { ConsolePanel } from "./panels/console";
20
21
  import { ElementsPanel } from "./panels/elements";
21
22
  import { GpuPanel } from "./panels/gpu";
22
23
  import { PerformancePanel } from "./panels/performance";
23
24
  import { SnapshotPanel } from "./panels/snapshot";
24
- import type { DtPanel, DtPanelCtx } from "./panels/types";
25
+ import { esc, type DtPanel, type DtPanelCtx } from "./panels/types";
25
26
  import { DEVTOOLS_CSS } from "./theme";
26
27
 
27
28
  const log = createLogger("info");
@@ -35,12 +36,17 @@ export interface DevtoolsUiSurface {
35
36
  z?: number;
36
37
  scale?: number;
37
38
  maxFps?: number;
39
+ /** Ease wheel deltas at pump cadence (html-ui PanelSpec.smoothWheel). */
40
+ smoothWheel?: boolean;
38
41
  interactive?: boolean;
39
42
  onEvent?: (ev: OsrDomEvent) => void;
40
43
  }): UiPanelHandle;
41
44
  /** Doc-backend capabilities — panels use incremental DOM ops only when
42
45
  * the underlying cdylib provides them. */
43
46
  docCaps?(): { incrementalDom?: boolean };
47
+ /** Register a `ui://` resource (fonts) served to the dock doc — must run
48
+ * before mount. Absent on surfaces that don't own a resource table. */
49
+ registerResource?(url: string, bytes: Uint8Array | ArrayBuffer): void;
44
50
  }
45
51
 
46
52
  export interface BlitzDevtoolsOptions {
@@ -83,6 +89,9 @@ export class BlitzDevtoolsHost {
83
89
  private panelCtx: DtPanelCtx;
84
90
  private lastPanelRender = -1e9;
85
91
  private lastSlowLog = 0;
92
+ /** @font-face css from registerDevtoolsFonts — "" means "embedded font
93
+ * fallback", null means "not yet attempted". */
94
+ private fontCss: string | null = null;
86
95
 
87
96
  constructor(opts: BlitzDevtoolsOptions) {
88
97
  this.opts = opts;
@@ -154,19 +163,41 @@ export class BlitzDevtoolsHost {
154
163
 
155
164
  show(): void {
156
165
  if (!this._ready || this.visible || this.disposed) return;
157
- this.panel = this.opts.ui.mount(this.shellHtml(), {
158
- id: "devtools",
159
- rect: this.dockRect(),
160
- // Above game overlays — menus/loading screens shouldn't bury the dock.
161
- z: 1000,
162
- scale: 1.5,
163
- interactive: true,
164
- // Devtools UI doesn't need vsync-rate raster — cap it so mutation
165
- // bursts (console streams, snapshot refreshes) coalesce into ~66ms
166
- // rasters instead of one raster per message.
167
- maxFps: 15,
168
- onEvent: (ev) => this.handleDocEvent(ev),
169
- });
166
+ // Register real system fonts once per host — ui:// resources persist on
167
+ // the surface across dock remounts, and must land before the doc mounts
168
+ // so the first resolve sees them (embedded DejaVu fallback otherwise).
169
+ if (this.fontCss === null) {
170
+ const ui = this.opts.ui as DevtoolsUiSurface & { host?: HtmlUiHost };
171
+ const rr = ui.registerResource?.bind(ui)
172
+ ?? (ui.host ? ui.host.registerResource.bind(ui.host) : undefined);
173
+ this.fontCss = rr ? registerDevtoolsFonts({ registerResource: rr }) : "";
174
+ }
175
+ try {
176
+ this.panel = this.opts.ui.mount(this.shellHtml(), {
177
+ id: "devtools",
178
+ rect: this.dockRect(),
179
+ // Above game overlays — menus/loading screens shouldn't bury the dock.
180
+ z: 1000,
181
+ // Native-res raster: the vendored glifo/FreeType path grid-fits hinted
182
+ // glyph stems to whole pixels — supersample factors >1 just blur that
183
+ // back out on downscale (and cost 2.25x the texture memory).
184
+ scale: 1,
185
+ // Ease wheel detents at pump cadence instead of jumping per event.
186
+ smoothWheel: true,
187
+ interactive: true,
188
+ // Devtools UI doesn't need vsync-rate raster — cap it so mutation
189
+ // bursts (console streams, snapshot refreshes) coalesce into ~66ms
190
+ // rasters instead of one raster per message.
191
+ maxFps: 15,
192
+ onEvent: (ev) => this.handleDocEvent(ev),
193
+ });
194
+ } catch (err) {
195
+ // Mount fails when the ui worker is dead — stay hidden so the next F12
196
+ // retries cleanly instead of wedging on a half-mounted doc.
197
+ this.panel = null;
198
+ log.error("blitz-devtools-host", `dock mount failed: ${err}`);
199
+ return;
200
+ }
170
201
  this.syncProviders();
171
202
  this.renderTabs();
172
203
  this.activate(this.activeId);
@@ -190,8 +221,18 @@ export class BlitzDevtoolsHost {
190
221
  }
191
222
  const t1 = performance.now();
192
223
  if (!this.visible) return;
193
- this.syncProviders();
194
- this.flush();
224
+ // Guarded like backend.update() above — a throwing panel render can't be
225
+ // allowed to kill the game frame hook that drives this pump.
226
+ try {
227
+ this.syncProviders();
228
+ this.flush();
229
+ } catch (err) {
230
+ const tn = performance.now();
231
+ if (tn - this.lastSlowLog > 1000) {
232
+ this.lastSlowLog = tn;
233
+ log.warn("blitz-devtools-host", `panel render error: ${err}`);
234
+ }
235
+ }
195
236
  const t2 = performance.now();
196
237
  // ≤1/sec — these diagnostics used to feed the console panel's logger
197
238
  // sink, and each row forced another dock re-render: a self-feeding
@@ -244,12 +285,14 @@ export class BlitzDevtoolsHost {
244
285
  // ── Dock layout ──
245
286
 
246
287
  private dockRect() {
247
- const h = Math.min(this.dockH, this.surfaceH - 60);
248
- return { x: 0, y: this.surfaceH - h, w: this.surfaceW, h };
288
+ // A tiny surface (or a pathological resize event) must not produce a
289
+ // negative height — keep at least enough room for the tab strip.
290
+ const h = Math.max(48, Math.min(this.dockH, this.surfaceH - 60));
291
+ return { x: 0, y: Math.max(0, this.surfaceH - h), w: this.surfaceW, h };
249
292
  }
250
293
 
251
294
  private shellHtml(): string {
252
- return `<html><head><style>${DEVTOOLS_CSS}</style></head><body>
295
+ return `<html><head><style>${this.fontCss ?? ""}${DEVTOOLS_CSS}</style></head><body>
253
296
  <div id="dt-root">
254
297
  <div id="dt-grip" title="drag to resize"></div>
255
298
  <div id="dt-chrome">
@@ -266,8 +309,11 @@ export class BlitzDevtoolsHost {
266
309
 
267
310
  private renderTabs(): void {
268
311
  if (!this.panel) return;
312
+ // esc() both interpolations — provider names feed panel ids/titles and
313
+ // arrive as raw strings (a game registering "x\"><img" would otherwise
314
+ // inject markup into the tab strip).
269
315
  const tabs = [...this.panels.values()].map((p) =>
270
- `<span class="tab${p.id === this.activeId ? " active" : ""}" data-action="dt.tab" data-tab="${p.id}">${p.title}</span>`
316
+ `<span class="tab${p.id === this.activeId ? " active" : ""}" data-action="dt.tab" data-tab="${esc(p.id)}">${esc(p.title)}</span>`
271
317
  ).join("");
272
318
  this.panel.setInnerHtml("#dt-tabs",
273
319
  `<span class="brand">downdraft</span>${tabs}<span class="spacer"></span>
@@ -280,13 +326,21 @@ export class BlitzDevtoolsHost {
280
326
  this.activeId = id;
281
327
  const p = this.panels.get(id)!;
282
328
  this.renderTabs();
283
- this.panel.setInnerHtml("#dt-top", p.renderTop?.() ?? "");
284
- this.panel.setInnerHtml("#dt-bottom", p.renderBottom?.() ?? "");
329
+ try {
330
+ this.panel.setInnerHtml("#dt-top", p.renderTop?.() ?? "");
331
+ this.panel.setInnerHtml("#dt-bottom", p.renderBottom?.() ?? "");
332
+ } catch (err) {
333
+ log.warn("blitz-devtools-host", `panel ${id} chrome render error: ${err}`);
334
+ }
285
335
  p.dirty = true;
286
336
  p.shellDirty = false;
287
- this.panel.setInnerHtml("#dt-body", p.renderBody());
288
- p.afterRender?.();
289
- p.activate?.();
337
+ this.panel.setInnerHtml("#dt-body", this.safeBody(p));
338
+ try { p.afterRender?.(); } catch (err) {
339
+ log.warn("blitz-devtools-host", `panel ${id} afterRender error: ${err}`);
340
+ }
341
+ try { p.activate?.(); } catch (err) {
342
+ log.warn("blitz-devtools-host", `panel ${id} activate error: ${err}`);
343
+ }
290
344
  // Only the snapshot tab being viewed gets periodic provider refreshes;
291
345
  // fixed panels suspend auto-refresh entirely (manual Refresh still works).
292
346
  this.backend.providerWatch = p.providerSlot ?? -1;
@@ -346,10 +400,25 @@ export class BlitzDevtoolsHost {
346
400
  }
347
401
  if (p.dirty) {
348
402
  p.dirty = false;
349
- if (p.flushDom?.() !== true) {
350
- this.panel.setInnerHtml("#dt-body", p.renderBody());
403
+ let handled = false;
404
+ try { handled = p.flushDom?.() === true; } catch (err) {
405
+ log.warn("blitz-devtools-host", `panel ${p.id} flushDom error: ${err}`);
406
+ }
407
+ if (!handled) this.panel.setInnerHtml("#dt-body", this.safeBody(p));
408
+ try { p.afterRender?.(); } catch (err) {
409
+ log.warn("blitz-devtools-host", `panel ${p.id} afterRender error: ${err}`);
351
410
  }
352
- p.afterRender?.();
411
+ }
412
+ }
413
+
414
+ /** renderBody that can't take the dock down — the error surfaces in-panel
415
+ * (and the next push retries since dirty just flips back on). */
416
+ private safeBody(p: DtPanel): string {
417
+ try {
418
+ return p.renderBody();
419
+ } catch (err) {
420
+ log.warn("blitz-devtools-host", `panel ${p.id} renderBody error: ${err}`);
421
+ return `<div class="status-banner error">panel render error: ${esc(String(err))}</div>`;
353
422
  }
354
423
  }
355
424
 
@@ -387,20 +456,26 @@ export class BlitzDevtoolsHost {
387
456
  }
388
457
  }
389
458
 
459
+ // Left-click only — routing actions on contextmenu meant a right-click
460
+ // on a tab/button fired it instead of just focusing.
390
461
  const action = ev.d?.action;
391
- if ((ev.t === "click" || ev.t === "contextmenu") && action) {
462
+ if (ev.t === "click" && action) {
392
463
  if (action === "dt.tab") {
393
464
  const id = ev.d!.tab;
394
465
  if (id) this.activate(id);
395
466
  return;
396
467
  }
397
468
  if (action === "dt.close") { this.hide(); return; }
398
- this.panels.get(this.activeId)?.onAction?.(ev.d!, ev);
469
+ try { this.panels.get(this.activeId)?.onAction?.(ev.d!, ev); } catch (err) {
470
+ log.warn("blitz-devtools-host", `panel action error: ${err}`);
471
+ }
399
472
  return;
400
473
  }
401
474
 
402
475
  // Everything else (input, keydown/up, scroll, focus…) → active panel.
403
- this.panels.get(this.activeId)?.onEvent?.(ev);
476
+ try { this.panels.get(this.activeId)?.onEvent?.(ev); } catch (err) {
477
+ log.warn("blitz-devtools-host", `panel event error: ${err}`);
478
+ }
404
479
  }
405
480
  }
406
481
 
@@ -426,5 +501,6 @@ export function createSelfHostedDevtoolsUi(ctx: RendererModuleContext): Devtools
426
501
  host,
427
502
  mount: (markup, spec) => host.mount({ ...spec, html: markup }),
428
503
  docCaps: () => host.docCaps,
504
+ registerResource: (url, bytes) => host.registerResource(url, bytes),
429
505
  };
430
506
  }
@@ -115,8 +115,8 @@ export class ConsolePanel implements DtPanel {
115
115
 
116
116
  onBackendEvent(event: string, data: unknown): void {
117
117
  if (event === "console") {
118
- const e = data as { text?: string; severity?: number; thread?: string };
119
- this.rows.push({ text: e.text ?? "", severity: e.severity ?? 0, thread: e.thread ?? "main", seq: this.seq++ });
118
+ const e = data as { text?: unknown; severity?: number; thread?: string };
119
+ this.rows.push({ text: String(e?.text ?? ""), severity: e?.severity ?? 0, thread: e?.thread ?? "main", seq: this.seq++ });
120
120
  if (this.rows.length > MAX_ROWS) this.rows.splice(0, this.rows.length - MAX_ROWS);
121
121
  this.dirty = true;
122
122
  } else if (event === "console.clear") {
@@ -125,15 +125,24 @@ export class ConsolePanel implements DtPanel {
125
125
  this.dirty = true;
126
126
  } else if (event === "threads") {
127
127
  const list = (data as ThreadInfo[]) ?? [];
128
- this.threads = [{ id: "main", name: "main", kind: 0 }, ...list.filter((t) => t.id !== "main")];
129
- if (!this.threads.some((t) => t.id === this.replThread)) this.replThread = "main";
128
+ const next = [{ id: "main", name: "main", kind: 0 }, ...list.filter((t) => t.id !== "main")];
129
+ // Thread chips live in the bottom chrome — flag it so the row
130
+ // actually re-renders when a worker appears or disappears.
131
+ const changed = next.length !== this.threads.length
132
+ || next.some((t, i) => t.id !== this.threads[i]!.id || t.name !== this.threads[i]!.name);
133
+ this.threads = next;
134
+ if (!this.threads.some((t) => t.id === this.replThread)) {
135
+ this.replThread = "main";
136
+ this.shellDirty = true;
137
+ }
138
+ if (changed) this.shellDirty = true;
130
139
  }
131
140
  }
132
141
 
133
142
  onAction(data: Record<string, string>, _ev: OsrDomEvent): void {
134
143
  const act = data.action;
135
144
  if (act === "console.clear") {
136
- void this.ctx.call("console.clear");
145
+ void this.ctx.call("console.clear").catch(() => {});
137
146
  } else if (act === "console.sev") {
138
147
  this.minSev = (this.minSev + 1) % SEV_NAMES.length;
139
148
  this.needsFullRender = true;
@@ -176,8 +185,11 @@ export class ConsolePanel implements DtPanel {
176
185
  } else if (ev.k === "ArrowUp" && this.histIdx > 0) {
177
186
  this.replValue = this.history[--this.histIdx]!;
178
187
  this.ctx.setValue("[data-dt=repl]", this.replValue);
179
- } else if (ev.k === "ArrowDown" && this.histIdx < this.history.length - 1) {
180
- this.replValue = this.history[++this.histIdx]!;
188
+ } else if (ev.k === "ArrowDown" && this.histIdx >= 0 && this.histIdx < this.history.length) {
189
+ // Stepping past the newest entry restores the empty input —
190
+ // previously the last history item was unreachable-to-leave.
191
+ this.histIdx++;
192
+ this.replValue = this.histIdx < this.history.length ? this.history[this.histIdx]! : "";
181
193
  this.ctx.setValue("[data-dt=repl]", this.replValue);
182
194
  }
183
195
  }
@@ -212,7 +224,7 @@ export class ConsolePanel implements DtPanel {
212
224
  } else {
213
225
  const r = res?.result;
214
226
  this.addRow({
215
- text: typeof r === "string" ? r : JSON.stringify(r, null, 2),
227
+ text: typeof r === "string" ? r : (r === undefined ? "undefined" : JSON.stringify(r, null, 2)),
216
228
  severity: 0, thread: this.replThread,
217
229
  });
218
230
  }