@woosh/meep-engine 2.168.1 → 2.169.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 (287) hide show
  1. package/README.md +2 -2
  2. package/build/bundle-worker-image-decoder.js +1 -1
  3. package/package.json +1 -1
  4. package/src/REVIEW_2026_08_06/adv-core.md +511 -0
  5. package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
  6. package/src/REVIEW_2026_08_06/adv-view.md +446 -0
  7. package/src/REVIEW_2026_08_06/api-core.md +296 -0
  8. package/src/REVIEW_2026_08_06/api-engine.md +195 -0
  9. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
  10. package/src/REVIEW_2026_08_06/core-binary.md +348 -0
  11. package/src/REVIEW_2026_08_06/core-collection.md +376 -0
  12. package/src/REVIEW_2026_08_06/core-math.md +221 -0
  13. package/src/REVIEW_2026_08_06/core-process.md +266 -0
  14. package/src/REVIEW_2026_08_06/core-science.md +223 -0
  15. package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
  16. package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
  17. package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
  18. package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
  19. package/src/REVIEW_2026_08_06/engine-input.md +197 -0
  20. package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
  21. package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
  22. package/src/REVIEW_2026_08_06/generation.md +176 -0
  23. package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
  24. package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
  25. package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
  26. package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
  27. package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
  28. package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
  29. package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
  30. package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
  31. package/src/REVIEW_2026_08_06/view.md +667 -0
  32. package/src/REVIEW_2026_08_06.md +571 -0
  33. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  34. package/src/core/binary/BinaryBuffer.js +12 -1
  35. package/src/core/binary/BitImage2.d.ts +5 -1
  36. package/src/core/binary/BitImage2.d.ts.map +1 -1
  37. package/src/core/binary/BitImage2.js +5 -1
  38. package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
  39. package/src/core/bvh8/build/NodeProxy.js +314 -308
  40. package/src/core/collection/list/List.d.ts +15 -0
  41. package/src/core/collection/list/List.d.ts.map +1 -1
  42. package/src/core/collection/list/List.js +34 -0
  43. package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
  44. package/src/core/collection/list/SortedListProjection.js +4 -1
  45. package/src/core/collection/map/BiMap.d.ts +25 -2
  46. package/src/core/collection/map/BiMap.d.ts.map +1 -1
  47. package/src/core/collection/map/BiMap.js +57 -3
  48. package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
  49. package/src/core/debug/matchers/IsAnything.js +5 -1
  50. package/src/core/events/signal/Signal.d.ts.map +1 -1
  51. package/src/core/events/signal/Signal.js +3 -1
  52. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
  53. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
  54. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
  55. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
  56. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
  57. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
  58. package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
  59. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
  60. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
  61. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
  62. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
  63. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
  64. package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
  65. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
  66. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
  67. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
  68. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
  69. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
  70. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
  71. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
  72. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
  73. package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
  74. package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
  75. package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
  76. package/src/core/geom/2d/v2_morton_encode.js +23 -0
  77. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
  78. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
  79. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
  80. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
  81. package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
  82. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
  83. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
  84. package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
  85. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
  86. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
  87. package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
  88. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  89. package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
  90. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
  91. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
  92. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
  93. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
  94. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
  95. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
  96. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
  97. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
  98. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
  99. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
  100. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
  101. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
  102. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
  103. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
  104. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
  105. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
  106. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
  107. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
  108. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
  109. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
  110. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
  111. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
  112. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
  113. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
  114. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
  115. package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
  116. package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
  117. package/src/core/geom/vec3/v3_cotangent.js +6 -2
  118. package/src/core/geom/vec3/v3_cross.d.ts +6 -2
  119. package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
  120. package/src/core/geom/vec3/v3_cross.js +6 -2
  121. package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
  122. package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
  123. package/src/core/geom/vec3/v3_multiply.js +6 -2
  124. package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
  125. package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
  126. package/src/core/geom/vec3/v3_subtract.js +6 -2
  127. package/src/core/localization/Localization.js +1 -1
  128. package/src/core/math/random/randomSeed.d.ts +13 -0
  129. package/src/core/math/random/randomSeed.d.ts.map +1 -0
  130. package/src/core/math/random/randomSeed.js +16 -0
  131. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
  132. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
  133. package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
  134. package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
  135. package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
  136. package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
  137. package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
  138. package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
  139. package/src/core/math/statistics/hammersley_sequence.js +25 -16
  140. package/src/core/process/Future.d.ts.map +1 -1
  141. package/src/core/process/Future.js +12 -0
  142. package/src/core/process/PromiseWatcher.d.ts.map +1 -1
  143. package/src/core/process/PromiseWatcher.js +9 -2
  144. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  145. package/src/core/process/executor/ConcurrentExecutor.js +12 -2
  146. package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
  147. package/src/core/process/worker/WorkerProxy.js +5 -2
  148. package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
  149. package/src/core/process/worker/extractTransferables.js +4 -1
  150. package/src/engine/Engine.d.ts +12 -12
  151. package/src/engine/Engine.d.ts.map +1 -1
  152. package/src/engine/Engine.js +149 -10
  153. package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
  154. package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
  155. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
  156. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
  157. package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
  158. package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
  159. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
  160. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  161. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
  162. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
  163. package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
  164. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  165. package/src/engine/graphics/GraphicsEngine.js +196 -24
  166. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
  167. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
  168. package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
  169. package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
  170. package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
  171. package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
  172. package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
  173. package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
  174. package/src/engine/graphics/context/WebGLContextState.js +24 -0
  175. package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
  176. package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
  177. package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
  178. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
  179. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
  180. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
  181. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
  182. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
  183. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
  184. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
  185. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
  186. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
  187. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
  188. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
  189. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
  190. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
  191. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +3 -5
  192. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
  193. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
  194. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
  195. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
  196. package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
  197. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
  198. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
  199. package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
  200. package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
  201. package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
  202. package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
  203. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
  204. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
  205. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  206. package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
  207. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
  208. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
  209. package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
  210. package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
  211. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
  212. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
  213. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
  214. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
  215. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
  216. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
  217. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
  218. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
  219. package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
  220. package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
  221. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
  222. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
  223. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
  224. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
  225. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
  226. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  227. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
  228. package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
  229. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  230. package/src/engine/physics/constraint/solve_constraints.js +38 -13
  231. package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
  232. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  233. package/src/engine/physics/contact/ManifoldStore.js +15 -3
  234. package/src/engine/physics/ecs/Joint.d.ts +7 -4
  235. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  236. package/src/engine/physics/ecs/Joint.js +7 -4
  237. package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
  238. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  239. package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
  240. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  241. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  242. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
  243. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
  244. package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
  245. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
  246. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  247. package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
  248. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
  249. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
  250. package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
  251. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  252. package/src/engine/physics/queries/raycast.js +11 -4
  253. package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
  254. package/src/engine/save/storage/IndexedDBStorage.js +21 -3
  255. package/src/engine/simulation/Ticker.d.ts +12 -0
  256. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  257. package/src/engine/simulation/Ticker.js +18 -0
  258. package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
  259. package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
  260. package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
  261. package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
  262. package/src/engine/sound/simulation/core/VolumeField.js +6 -2
  263. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
  264. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
  265. package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
  266. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
  267. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
  268. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
  269. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
  270. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
  271. package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
  272. package/src/engine/ui/GUIEngine.d.ts.map +1 -1
  273. package/src/engine/ui/GUIEngine.js +8 -1
  274. package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
  275. package/src/generation/theme/TerrainTheme.js +2 -1
  276. package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
  277. package/src/generation/theme/ThemeEngine.js +6 -5
  278. package/src/view/ViewGroup.d.ts.map +1 -1
  279. package/src/view/ViewGroup.js +9 -7
  280. package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
  281. package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
  282. package/src/view/graphics/WebGLContextFailureView.js +76 -0
  283. package/src/view/minimap/Minimap.d.ts +8 -0
  284. package/src/view/minimap/Minimap.d.ts.map +1 -1
  285. package/src/view/minimap/Minimap.js +16 -3
  286. package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
  287. package/src/view/minimap/dom/MinimapMarkerView.js +3 -1
@@ -0,0 +1,667 @@
1
+ # Correctness review — `meep/src/view/`
2
+
3
+ Scope: all 111 non-spec source files under `H:/git/moh/app/src/mir-engine/meep/src/view/`.
4
+ Excluded: `editor/`, `prototype*.js`, `*.spec.js`, `.d.ts`.
5
+
6
+ Notes on evidence used while reviewing:
7
+ - `View.unlink()` **clears the `Linked` flag before dispatching `on.unlinked`** (`View.js:325` then `:347`). Several
8
+ findings below depend on that ordering.
9
+ - `Signal` keeps handlers in a `Map` keyed by the handler function; iteration is insertion-ordered, and a handler
10
+ removed mid-dispatch by an earlier handler is **not** invoked (`core/events/signal/Signal.js:378-425`).
11
+ - `Vector1`/`ObservedInteger` extend `Number` and implement `valueOf()`, so arithmetic directly on them is valid.
12
+ - `segment2_segment_compute_intersection_array_2d` leaves `result` **untouched** and returns `false` on a miss.
13
+
14
+ ---
15
+
16
+ ### VIEW-1 `DomTooltipObserver.unlink()` never actually detaches, because the view is already unlinked
17
+ - **Severity**: CRITICAL
18
+ - **File**: `src/view/tooltip/DomTooltipObserver.js:156-165`, driven from `src/view/tooltip/DomTooltipManager.js:109-117,178-194`
19
+ - **Defect**: `unlink()` only runs `__handleViewUnlinked()` `if (view.isLinked)`. On the `manage()` path the call
20
+ arrives from inside the `view.on.unlinked` dispatch, at which point `View.unlink()` has already cleared the
21
+ `Linked` flag — so the guard is always false and the teardown is skipped entirely. The observer's own
22
+ `on.unlinked` handler cannot compensate: `manage()`'s handler was subscribed first, and it calls
23
+ `observer.unlink()`, which does `view.on.unlinked.remove(this.__handleViewUnlinked, this)` mid-dispatch, deleting
24
+ the not-yet-visited Map entry so it never fires.
25
+ - **Failure scenario**: `gui.viewTooltips.manage(button, factory)` (used in ~20 places, e.g.
26
+ `interaction/createInterfaceCommandButton.js:106`, `view/game/scene/strategy/shop/ItemShopView.js:185`). Link the
27
+ panel, then unlink it. `mouseenter`/`mouseleave` listeners stay on `button.el`, and the 4 `SignalBinding`s on
28
+ `view.position.onChanged`, `view.size.onChanged` and the `DomSizeObserver` stay linked forever. Re-open the panel
29
+ and `manage()`'s linked handler calls `add()` again, creating a *second* observer with a second pair of DOM
30
+ listeners on the same element. Ten open/close cycles leave ten live observers per button. Worse: hover a button,
31
+ and inside the 128 ms `requestShow` delay close the panel — `remove()` calls `hide()` (no-op, tip not added yet),
32
+ the observer teardown is skipped so nothing calls `cancelShowRequest()`, the timeout fires, `tipManager.add(tip)`
33
+ builds a tooltip for a detached view, and because the element no longer receives mouse events nothing will ever
34
+ fire `on.exited` → the tooltip is stuck on screen for the rest of the session.
35
+ - **Fix**: drop the `view.isLinked` guard in `unlink()` and make `__handleViewUnlinked()` idempotent (track an
36
+ internal `#attached` boolean set in `__handleViewLinked`), and remove the `view.on.*` handlers *before* calling
37
+ `__handleViewUnlinked()`. Additionally call `this.cancelShowRequest()` in `DomTooltipManager.remove()`.
38
+ - **Confidence**: Confirmed
39
+
40
+ ### VIEW-2 `SvgImageView` cannot be imported and crashes on every `link()`
41
+ - **Severity**: CRITICAL
42
+ - **File**: `src/view/elements/image/SvgImageView.js:1,64-73`
43
+ - **Defect**: Two independent hard errors. (a) `import AABB2 from "../../../core/geom/AABB2.js"` — that file does not
44
+ exist; the real path is `core/geom/2d/aabb/AABB2.js`, so the module fails to resolve. (b) `update()` is a class
45
+ method but its body references `resize` and `self`, which are `const`/`let` bindings local to the *constructor*.
46
+ `link()` calls `this.update()` unconditionally.
47
+ - **Failure scenario**: any module that does `import SvgImageView from '.../SvgImageView.js'` fails at bundle/resolve
48
+ time. If the import path is fixed, `new SvgImageView(url, {assetManager}); parent.addChild(v)` on a linked parent →
49
+ `link()` → `update()` → `ReferenceError: resize is not defined` (caught by Signal only if it were dispatched from a
50
+ signal; here it propagates out of `addChild`).
51
+ - **Fix**: correct the import path; promote `resize` and the icon element to instance state (`#resize()` reading
52
+ `this.#elIcon`, `this.size`) so `update()` can reach them.
53
+ - **Confidence**: Confirmed
54
+
55
+ ### VIEW-3 `MinimapMarkerView` throws on construction — `this` used before `super()`
56
+ - **Severity**: CRITICAL
57
+ - **File**: `src/view/minimap/dom/MinimapMarkerView.js:17,51-64`
58
+ - **Defect**: `super(entity, this.marker, this.transform, this.worldScale)` dereferences `this` before the base
59
+ constructor has run, which is a `ReferenceError` in every ES2015+ engine. Even past that, the three handler methods
60
+ reference `setPosition` and `size`, which are constructor-local bindings, and they are bound via `bindSignal(...)`
61
+ **without a context argument**, so `this` inside them would be `undefined`.
62
+ - **Failure scenario**: `new MinimapMarkerView(entity, marker, transform, worldScale)` →
63
+ `ReferenceError: Must call super constructor in derived class before accessing 'this'`. The class is unusable.
64
+ - **Fix**: `super()` with no arguments; move `setPosition` to a method and pass `this` as the context to every
65
+ `bindSignal` call; read `this.size` instead of the free `size`.
66
+ - **Confidence**: Confirmed
67
+
68
+ ### VIEW-4 `MinimapTerrainView.__setTerrain` references an undefined identifier and has no context
69
+ - **Severity**: CRITICAL
70
+ - **File**: `src/view/minimap/dom/MinimapTerrainView.js:126,133-143`
71
+ - **Defect**: `this.terrain.onChanged.add(this.__setTerrain)` subscribes a *method* with no context, and the method
72
+ body calls `updatePreview()` — a function declared inside the constructor and therefore not in scope. It also
73
+ dereferences `terrain.preview` with no null check even though `ObservedValue(null)` is the initial state and
74
+ `set(null)` is a legal transition.
75
+ - **Failure scenario**: `minimapTerrainView.terrain.set(someTerrain)` → `__setTerrain` runs →
76
+ `ReferenceError: updatePreview is not defined` (swallowed by Signal's try/catch and logged) → the terrain preview
77
+ image is never set, the view renders an empty `<img>`. `terrain.set(null)` →
78
+ `TypeError: Cannot read properties of null (reading 'preview')`.
79
+ - **Fix**: make `updatePreview`/`setPreview` instance methods, subscribe with `add(this.__setTerrain, this)`, and
80
+ guard the `terrain === null` case.
81
+ - **Confidence**: Confirmed
82
+
83
+ ### VIEW-5 `ViewGroup.remove()` calls a non-existent `Set.remove`; `disconnect()` adds instead of removes
84
+ - **Severity**: CRITICAL
85
+ - **File**: `src/view/ViewGroup.js:34,74-89`
86
+ - **Defect**: `this.elements` is a `Set`; `Set` has `delete`, not `remove`. Separately, `disconnect()` iterates every
87
+ member and calls `this.container.addChild(view)` where the whole point of the method is to detach them.
88
+ - **Failure scenario**: `group.remove(view)` → `TypeError: this.elements.remove is not a function`.
89
+ `group.connect(a); group.connect(b)` → `connect` calls `disconnect()`, which re-adds every view to container `a`
90
+ (pushing a duplicate entry into `a.children` and re-appending the DOM node), sets `this.container = null`, then
91
+ `connect` adds them all to `b`. Container `a` is left with duplicate `children` entries pointing at views whose
92
+ DOM nodes now live under `b`; a later `a.removeAllChildren()` calls `a.el.removeChild(child.el)` on a node that is
93
+ no longer its child → `NotFoundError` DOMException.
94
+ - **Fix**: `this.elements.delete(view)`; `disconnect()` must call `this.container.removeChild(view)`.
95
+ - **Confidence**: Confirmed
96
+
97
+ ### VIEW-6 `MinimapView.clear()` adds children instead of removing them
98
+ - **Severity**: CRITICAL
99
+ - **File**: `src/view/minimap/Minimap.js:183-188` (called from `unlink()` at `:236`)
100
+ - **Defect**: The method is documented "remove all existing markers" / "clear out all camera views" but its body is
101
+ `this.__worldView.addChild(v)` for each camera view — the views are already children, so each `unlink()` pushes a
102
+ duplicate entry into `__worldView.children`.
103
+ - **Failure scenario**: Open the minimap, close it, open it again (each cycle = one link/unlink pair). After N
104
+ cycles `__worldView.children` holds each `MinimapCameraView` N+1 times. Now the camera entity is destroyed:
105
+ `cameraRemoved` → `__worldView.removeChild(cameraView)` splices out *one* entry and detaches the DOM node, leaving
106
+ stale duplicates. On the next `unlink()`, `clear()` runs `addChild` on that stale duplicate, which does
107
+ `this.el.appendChild(child.el)` — re-inserting the frustum polygon of a camera that no longer exists back into the
108
+ minimap, and re-linking it on the next `link()` so it keeps updating against a dead `Transform`.
109
+ - **Fix**: `this.__worldView.removeChild(v)` and iterate a copy (or iterate backwards) since `removeChild` mutates.
110
+ - **Confidence**: Confirmed
111
+
112
+ ### VIEW-7 `TaskProgressView` displays progress where remaining time belongs
113
+ - **Severity**: MAJOR
114
+ - **File**: `src/view/task/TaskProgressView.js:127-131`
115
+ - **Defect**: `const estimate = Number.isNaN(computedEstimate) ? 0 : computedProgressValue;` — the non-NaN branch uses
116
+ `computedProgressValue` (the 0..1 progress fraction) instead of `computedEstimate` (the estimator's output). The
117
+ computed estimate is discarded.
118
+ - **Failure scenario**: A 60-second load at 20% progress. `remainingTimeEstimator.estimate()` returns ~48, but the
119
+ `.remaining-time` label is fed `0.2` and formatted with `toFixed(0)` → it reads `0`. The loading screen shows
120
+ "0 seconds remaining" for the entire load, then jumps to "1" at completion.
121
+ - **Fix**: `const estimate = Number.isNaN(computedEstimate) ? 0 : computedEstimate;`
122
+ - **Confidence**: Confirmed
123
+
124
+ ### VIEW-8 `ColorPickerView` unsubscribes from the new colour instead of the old one
125
+ - **Severity**: MAJOR
126
+ - **File**: `src/view/elements/ColorPickerView.js:282-294`
127
+ - **Defect**:
128
+ ```js
129
+ this.model.onChanged.add(function (v, _v) {
130
+ if (v !== null) { modelValueChanged(...); v.onChanged.add(modelValueChanged); }
131
+ if (_v !== null) { v.onChanged.remove(modelValueChanged); } // <-- v, should be _v
132
+ });
133
+ ```
134
+ The removal targets the *new* value. The old value keeps the listener forever, and the new value gets `add`
135
+ immediately followed by `remove`.
136
+ - **Failure scenario**: `picker.model.set(colorA)` then `picker.model.set(colorB)`. Afterwards, changing `colorB`
137
+ externally does not move the H/S/L/A gauges (its listener was removed), while changing `colorA` — which the picker
138
+ no longer edits — does move them. Additionally `picker.model.set(null)` after having a colour executes
139
+ `null.onChanged.remove(...)` → `TypeError: Cannot read properties of null (reading 'onChanged')`.
140
+ - **Fix**: `if (_v !== null) { _v.onChanged.remove(modelValueChanged); }`, and perform the removal before the add.
141
+ - **Confidence**: Confirmed
142
+
143
+ ### VIEW-9 `ColorPickerView` value-channel write lock uses a key that does not exist
144
+ - **Severity**: MAJOR
145
+ - **File**: `src/view/elements/ColorPickerView.js:156-161,214,224,229,257,266`
146
+ - **Defect**: `writeLocks` is declared with keys `h, s, l, a`. The `colorV` handler checks and sets `writeLocks.v`
147
+ (undefined key), while `modelValueChanged` checks `writeLocks.l`. The lock protecting the value/lightness channel
148
+ therefore never engages in either direction.
149
+ - **Failure scenario**: Pick a saturated red (h≈0, s=1, v=1), then drag the **L** gauge to 0. `colorV.onChanged` runs
150
+ unguarded (`writeLocks.v` is `undefined`), writes rgb `(0,0,0)` into the model, which dispatches
151
+ `modelValueChanged`; that handler's guard reads `writeLocks.l` — also never set by the V handler — so it proceeds
152
+ and pushes `rgb2hsv(0,0,0)` back into the gauges, snapping H and S to 0. The user's hue and saturation are
153
+ destroyed by moving the lightness slider, and cannot be recovered by dragging L back up.
154
+ - **Fix**: use one consistent key (`v`) in the declaration, the `colorV` handler, and `modelValueChanged`.
155
+ - **Confidence**: Confirmed
156
+
157
+ ### VIEW-10 `Vector1Control` binds the write-back handler to the model's change signal
158
+ - **Severity**: MAJOR
159
+ - **File**: `src/view/controller/controls/Vector1Control.js:61-81`
160
+ - **Defect**: `subscribe(v1)` does `v1.onChanged.add(writeBack)`. `writeBack` is the *controller → model* direction
161
+ (`vector1.set(v)`); the *model → controller* direction is `syncForward`, which is only ever called once, inside
162
+ `subscribe`. `unsubscribe` symmetrically removes `writeBack`, so the mismatch is consistent and silent.
163
+ - **Failure scenario**: Bind a `Vector1Control` to a `Vector1` that some other system writes (e.g. an animated
164
+ property). The number field shows the value present at subscribe time and never updates again: each external change
165
+ invokes `writeBack(newValue)` which writes `newValue` straight back into the same vector (a no-op) instead of
166
+ refreshing `cNumber.value`.
167
+ - **Fix**: `v1.onChanged.add(syncForward)` in `subscribe` and `v1.onChanged.remove(syncForward)` in `unsubscribe`;
168
+ keep `lockForward` to break the echo.
169
+ - **Confidence**: Confirmed
170
+
171
+ ### VIEW-11 `ListController` removes the wrong view, so deleted rows stay on screen
172
+ - **Severity**: MAJOR
173
+ - **File**: `src/view/controller/controls/ListController.js:48-74`
174
+ - **Defect**: `handleItemAdded` wraps `guiControl` in a `vElementContainer` and adds *that* to `vContainer`.
175
+ `handleItemRemoved` then calls `vContainer.removeChild(guiControl)` — `guiControl` is a child of
176
+ `vElementContainer`, not of `vContainer`, so `children.indexOf()` returns `-1` and `removeChild` returns `false`
177
+ without doing anything.
178
+ - **Failure scenario**: Click the `.close` button next to a list item. The item leaves the model
179
+ (`list.removeOneOf(item)`), `elementControllers` drops the entry, `guiControl.model.set(null)` blanks the control —
180
+ but the row (control + close button) remains in the DOM permanently. Clicking its close button again calls
181
+ `list.removeOneOf(item)` on an item that is no longer present. Rebinding the controller to a different list via
182
+ `detachList` leaves every old row visible on top of the new ones.
183
+ - **Fix**: keep a `Map<item, vElementContainer>` (or store the wrapper on the control) and remove the wrapper.
184
+ - **Confidence**: Confirmed
185
+
186
+ ### VIEW-12 `HTMLElementCacheKey.initializeFromElement` wipes the styles it just read (shadowed loop variable)
187
+ - **Severity**: MAJOR
188
+ - **File**: `src/view/common/HTMLElementCacheKey.js:189-217`
189
+ - **Defect**:
190
+ ```js
191
+ let i = 0;
192
+ ...
193
+ for (let i = 0; i < styleCount; i++) { ... this.style[i] = pair; } // inner `i` shadows outer
194
+ this.style.splice(i, this.style.length - i); // outer i is still 0
195
+ ```
196
+ The truncation call meant to drop leftover entries instead removes the entire array. (The attribute loop below uses
197
+ `for (i = 0; ...)` without `let`, so it truncates correctly — proving the intent.) The key therefore always reports
198
+ "no inline styles", while `hash()`/`equals()` treat that as a real property of the element.
199
+ - **Failure scenario**: `imageViewA.css({ filter: 'grayscale(1)' })` on an `<img src="x.png" class="icon">`, then
200
+ `imageViewA.destroy()` → `key.initializeFromElement(el)` records the tag and attributes but **not** the filter →
201
+ the element is pooled under a key that claims no styles. Later `new ImageView('x.png', {classList:['icon']})`
202
+ builds a style-less key, `view_pool.get(key)` matches, and the recycled element is returned still carrying
203
+ `filter: grayscale(1)` → the new image renders greyed out.
204
+ - **Fix**: rename the inner loop variable (or hoist it) so the splice truncates from `styleCount`.
205
+ - **Confidence**: Confirmed
206
+
207
+ ### VIEW-13 `TabbedView` never attaches its toggle and panel containers — it renders nothing
208
+ - **Severity**: MAJOR
209
+ - **File**: `src/view/common/TabbedView.js:49-57`
210
+ - **Defect**: The constructor creates `this.el`, then `this.__panelContainer` and `this.__toggleContainer`, but never
211
+ calls `this.addChild(...)` on either, and neither does `link()`. Every toggle and panel is added to a container
212
+ that is not in the view tree.
213
+ - **Failure scenario**: `const t = new TabbedView({tabs}); parent.addChild(t);` → a bare empty `<div>`. Tab toggles
214
+ and the active panel exist as detached DOM and are never linked (so their signal bindings never activate either).
215
+ - **Fix**: `this.addChild(this.__toggleContainer); this.addChild(this.__panelContainer);` in the constructor.
216
+ - **Confidence**: Confirmed
217
+
218
+ ### VIEW-14 `MinimapFogOfWar.enforceAspectRatio` computes the wrong growth in the "too wide" branch
219
+ - **Severity**: MAJOR
220
+ - **File**: `src/view/minimap/gl/MinimapFogOfWar.js:16-38`
221
+ - **Defect**: When `aspectRatioDelta <= 0` the code grows the width by `-aabbWidth * aspectRatioDelta`. Solving,
222
+ the resulting ratio is `aabbHeight / (aabbWidth * (1 + aAR - vAR))`, which only equals `vAR` by coincidence. The
223
+ height branch is correct (`aabbWidth * delta` yields exactly `aabbWidth * vAR`); the width branch is not the
224
+ mirror of it.
225
+ - **Failure scenario**: minimap viewport 200×100 (`vAR = 0.5`), revealed fog region a 100×100 square (`aAR = 1`).
226
+ `delta = -0.5`, `d = 50`, new bounds 150×100 → ratio 0.667, not 0.5. `focusArea` is set to that region and drives
227
+ the orthographic camera in `MinimapWorldGL.updateCameraFocus`, so the minimap world is squashed horizontally by
228
+ ~33%: terrain, entity markers and the camera frustum polygon (which are positioned from `worldScale`) no longer
229
+ line up with the fog-of-war overlay.
230
+ - **Fix**: `const d = aabbHeight / viewportAspectRatio - aabbWidth;` (equivalently grow to `aabbHeight / vAR`).
231
+ - **Confidence**: Confirmed
232
+
233
+ ### VIEW-15 `ListView.insertOne` dereferences an undefined sibling view when a filter is in play
234
+ - **Severity**: MAJOR
235
+ - **File**: `src/view/common/ListView.js:182-197`
236
+ - **Defect**: For a non-terminal insert it looks up `this.model.get(index + 1)` and calls `nextChild.el` without
237
+ checking that a view exists for it. `getChildByElement` returns `undefined` whenever the next element was skipped
238
+ by `this.filter` (or when the list contains duplicate elements — `viewMapping` is keyed by element identity, so a
239
+ duplicate overwrites the previous entry).
240
+ - **Failure scenario**: `new ListView(model, {elementFactory, filter: e => e.visible})`. Model is `[A(visible),
241
+ C(hidden)]`; insert `B(visible)` at index 1. `insertOne` creates B's view, sees `model.length (3) !== index+1 (2)`,
242
+ fetches `C`, `getChildByElement(C)` → `undefined` → `TypeError: Cannot read properties of undefined (reading 'el')`
243
+ inside the `model.on.added` dispatch. Signal logs it and swallows it, so the symptom is a silently mis-ordered list
244
+ (B stays appended at the end) plus a console error on every filtered insert.
245
+ - **Fix**: scan forward from `index + 1` for the first element that has a view; if none, leave the child appended.
246
+ - **Confidence**: Confirmed
247
+
248
+ ### VIEW-16 `DomSizeObserver.attach()` unobserves the new element and never observes it
249
+ - **Severity**: MAJOR
250
+ - **File**: `src/view/util/DomSizeObserver.js:147-153`
251
+ - **Defect**:
252
+ ```js
253
+ attach(element) {
254
+ this.element = element; // old reference already lost
255
+ if (this.running) { this.__dom_resize_observer.unobserve(this.element); } // unobserves the NEW element
256
+ }
257
+ ```
258
+ The old element is never unobserved (it stays in the `ResizeObserver`), and the new element is never observed
259
+ because `attach` only ever calls `unobserve`.
260
+ - **Failure scenario**: `o.attach(elA); o.start();` then `o.attach(elB)` while running. `unobserve(elB)` is a no-op
261
+ (B was never observed), A remains observed. Resizing B produces no callback at all — `o.dimensions` goes stale.
262
+ Resizing A *does* fire the callback, which then measures **B** via `resizeAABB2ToFitBoundingClientRect(this.element…)`
263
+ for the position but takes `rect.width/height` from **A's** `contentRect` → `dimensions` is a mix of two elements.
264
+ - **Fix**: capture the previous element, `unobserve(previous)` if running, assign, then `observe(element)` if running.
265
+ - **Confidence**: Confirmed
266
+
267
+ ### VIEW-17 `SimpleWindowView` resize handler accumulates the full drag delta on every pointer move
268
+ - **Severity**: MAJOR
269
+ - **File**: `src/view/elements/SimpleWindow.js:47-73`
270
+ - **Defect**: `anchor` is captured once on pointer-down and never advanced, yet the move handler does
271
+ `view.size.add(position - anchor)` — an *incremental* mutation fed an *absolute* delta. Compare
272
+ `BottomLeftResizeHandleView` (`elements/BottomLeftResizeHandleView.js:31-40`), which correctly does
273
+ `lastPosition.copy(position)` each move.
274
+ - **Failure scenario**: `new SimpleWindowView(content, {resizable: true})`. Grab the bottom-right 10 px corner and
275
+ drag 10 px right over 5 pointer-move events (deltas 2, 4, 6, 8, 10 relative to the fixed anchor) → the width grows
276
+ by 30 px instead of 10. A slow 200 px drag over 100 events grows the window by ~10 000 px; the window runs away
277
+ from the cursor and cannot be resized back down.
278
+ - **Fix**: either update `anchor` to `position` at the end of each move, or capture `originalSize` on pointer-down and
279
+ use `view.size.copy(originalSize).add(delta)`.
280
+ - **Confidence**: Confirmed
281
+
282
+ ### VIEW-18 `ToastLogView` registers every notification twice in `children`
283
+ - **Severity**: MAJOR
284
+ - **File**: `src/view/elements/notify/ToastLogView.js:47,72-88`
285
+ - **Defect**: The constructor reassigns `this.children = []` (shadowing the base `View.children` array), and `addOne`
286
+ then does **both** `self.addChild(childView)` — which pushes onto `this.children` — **and**
287
+ `self.children.push(childView)`. Every toast appears twice in the array. `removeChildView` splices out one
288
+ occurrence and bypasses `View.removeChild`, so the second entry survives with its DOM node already detached.
289
+ - **Failure scenario**: Two notifications A then B. `children` = `[A, A, B, B]`, so `updatePositions` computes
290
+ `maxIndex = 3`: A ends at `-(3-1)*24 = -48 px` and B at `0`, instead of `-24` and `0` → a 24 px gap opens between
291
+ the two toasts and the stack floats one row too high. After A's fade-out, `children` = `[A, B, B]` and A's stale
292
+ entry keeps skewing every subsequent layout. If the view is ever destroyed, `View.destroy`/`removeAllChildren`
293
+ walks the duplicates and calls `this.el.removeChild(A.el)` on an already-detached node → `NotFoundError`.
294
+ - **Fix**: delete the `this.children = []` reassignment and the manual `push`; use `View.removeChild` in
295
+ `removeChildView`. Also cancel the pending `setTimeout` handles on unlink and unsubscribe
296
+ `model.elements.on.added` (currently a permanent subscription, `:90`).
297
+ - **Confidence**: Confirmed
298
+
299
+ ### VIEW-19 `RadialProgressView` subscribes to its external model permanently
300
+ - **Severity**: MAJOR
301
+ - **File**: `src/view/elements/radial/RadialProgressView.js:55-68`
302
+ - **Defect**: `model.on.changed.add(update)` / `model[0].onChanged.add(redraw)` / `model[1].onChanged.add(redraw)` are
303
+ raw subscriptions on caller-owned objects, made in the constructor and never removed — the class has no `unlink()`
304
+ or `destroy()` override, and does not use `bindSignal`.
305
+ - **Failure scenario**: `view/units/UnitRadialPreview.js:23` builds
306
+ `new RadialProgressView([unit.healthCurrent, unit.stats.healthMax], …)`. The unit outlives the preview widget
307
+ (health is model state). Every time a unit preview is created and discarded, the unit's `healthCurrent` signal
308
+ retains `redraw`, which closes over the view, its SVG element and its `thickness` observable — the whole widget is
309
+ unreclaimable, and every health tick redraws N dead widgets. Cycling a roster panel a few hundred times leaks
310
+ hundreds of SVG subtrees.
311
+ - **Fix**: route all three through `this.bindSignal(...)` so they follow the view's link lifecycle.
312
+ - **Confidence**: Confirmed
313
+
314
+ ### VIEW-20 `createInterfaceCommandButton` subscribes to the command's feature signals twice, one of them forever
315
+ - **Severity**: MAJOR
316
+ - **File**: `src/view/interaction/createInterfaceCommandButton.js:91-99`
317
+ - **Defect**:
318
+ ```js
319
+ if (interaction.features !== undefined) {
320
+ interaction.features.on.added.add(addCommandFeature); // permanent
321
+ interaction.features.on.removed.add(removeCommandFeature); // permanent
322
+ }
323
+ button.bindSignal(interaction.features.on.added, addCommandFeature); // link-scoped (correct)
324
+ button.bindSignal(interaction.features.on.removed, removeCommandFeature); // link-scoped (correct)
325
+ ```
326
+ `InteractionCommand` always constructs `this.features = new List(features)` (`interaction/InteractionCommand.js:40`),
327
+ so the `if` is always taken and the first pair is always added. Nothing ever removes them.
328
+ - **Failure scenario**: `InteractionCommand` instances are long-lived and shared — `InteractionCommandSet` keys them
329
+ by id. A command bar that is rebuilt on every selection change produces a new `ButtonView` per command each time;
330
+ each one permanently registers two closures on the shared command's feature list. After 100 selection changes,
331
+ `interaction.setFeature('highlighted', true)` invokes `addCommandFeature` on 100 discarded buttons, and all 100
332
+ button subtrees are unreclaimable.
333
+ - **Fix**: delete the raw `on.added`/`on.removed` subscriptions; the `bindSignal` pair already covers the live case
334
+ and `button.on.linked` already replays the current features.
335
+ - **Confidence**: Confirmed
336
+
337
+ ### VIEW-21 `MeshPreview` multiplies its pointer handlers on every link, and dereferences a null camera
338
+ - **Severity**: MAJOR
339
+ - **File**: `src/view/elements/MeshPreview.js:204-234`
340
+ - **Defect**: (a) `link()` attaches a fresh `self.pContents.then(...)` continuation each time. Because `pContents` is
341
+ already settled after the first load, every subsequent link immediately runs `__addInteraction`, which creates a
342
+ **new** `PointerDevice`, calls `start()` on it, and adds another `dragStart`/`drag` pair that mutates
343
+ `contents.group.rotation` — none of which is ever stopped in `unlink()`. (b) `__update_renderer_size` guards on
344
+ `this.renderer !== null` but then touches `this.camera.aspect`; `camera` is only assigned in the asset callback.
345
+ - **Failure scenario (a)**: Open a unit-detail panel containing a `MeshPreview`, close it, open it again. Two
346
+ `PointerDevice`s are now live on the pooled canvas; one drag rotates the model twice as fast. After ten cycles the
347
+ model spins ~10× per pixel of drag. (b): `preview.size.set(256, 256)` after linking but before the glTF resolves —
348
+ the bound `size.onChanged` fires with `renderer` set and `camera` still `null` →
349
+ `TypeError: Cannot set properties of null (setting 'aspect')`.
350
+ - **Fix**: attach the `pContents` continuation once in the constructor and have it store `contents.group`; create the
351
+ `PointerDevice` once and `start()`/`stop()` it from `link`/`unlink`. Guard `__update_renderer_size` on
352
+ `this.camera !== null` as well (it is genuinely null until the async load lands).
353
+ - **Confidence**: Confirmed
354
+
355
+ ### VIEW-22 `renderModel` caches the caller's live `size`/`focus` objects, so resizes return the stale image
356
+ - **Severity**: MAJOR
357
+ - **File**: `src/view/renderModel.js:15-29,85-92`
358
+ - **Defect**: The cache entry stores `size: size` and `focus: focus` by reference, and `findInCache` compares with
359
+ `piece.size.equals(size)` / component-wise on `focus`. When the caller passes a mutable vector, the cached entry's
360
+ key mutates in lockstep with the query, so the comparison always succeeds.
361
+ - **Failure scenario**: `MeshView.render()` (`common/MeshView.js:61`) passes `this.size` and `this.focus` straight
362
+ through. Render a mesh preview at 50×50 — cached with `piece.size === view.size`. Resize the view to 200×200:
363
+ `update()` sees the change and calls `render()`, `findInCache(url, this.size, this.focus)` compares `this.size`
364
+ against itself → hit → the 50×50 `dataURL` is returned and stretched to 200×200 (blurry), and no re-render is ever
365
+ scheduled. The cache also has no eviction (`cache.push` only), so it grows without bound.
366
+ - **Fix**: store `size.clone()` and a copied `AABB2` in the cache entry; add an eviction policy (the codebase already
367
+ has `core/cache/Cache`).
368
+ - **Confidence**: Confirmed
369
+
370
+ ### VIEW-23 `MeshView` never reacts to its own model
371
+ - **Severity**: MAJOR
372
+ - **File**: `src/view/common/MeshView.js:72-92`
373
+ - **Defect**: The view observes `this.size` only. `update()`'s change test compares `size` and `focus` against
374
+ `renderedParams` and ignores the model entirely, so even an explicit `update()` after a model change does nothing.
375
+ `this.model.onChanged` is never bound.
376
+ - **Failure scenario**: `meshView.model.set('models/sword.gltf')` on a linked `MeshView` currently showing
377
+ `models/axe.gltf` → nothing happens; a manual `meshView.update()` also short-circuits on "no change". The view keeps
378
+ displaying the axe until the element is resized.
379
+ - **Fix**: `this.bindSignal(this.model.onChanged, this.render, this)`, and include the rendered URL in
380
+ `renderedParams` so `update()` detects it.
381
+ - **Confidence**: Confirmed
382
+
383
+ ### VIEW-24 `RectangularPieProgressView` assumes a square element; on any other aspect the path becomes invalid
384
+ - **Severity**: MAJOR
385
+ - **File**: `src/view/elements/progress/RectangularPieProgressView.js:77-107`
386
+ - **Defect**: The fraction thresholds `.125 / .375 / .625 / .875` place the corner transitions at 45°, which is only
387
+ where the corners are for a square. For a non-square element the sweep ray exits through a different edge than the
388
+ branch tests, `segment2_segment_compute_intersection_array_2d` returns `false` and leaves `p` empty, and the path
389
+ string is built from `p[0]`/`p[1]` === `undefined`.
390
+ - **Failure scenario**: `v.size.set(100, 50); v.model.set(13); v.max.set(100)` (fraction 0.13 → branch 2, which tests
391
+ the **right** edge). `angle ≈ 43°` from the centre `(50,25)`: reaching `x = 100` needs `Δy = -47`, i.e. `y = -22`,
392
+ outside the edge segment — the ray actually exits through the **top** edge at `x ≈ 76.6`. No intersection is
393
+ written, so `d` becomes `"M 50 0 L 50 25 L undefined undefined L 100 50 L 0 50 L 0 0 Z"`. Browsers reject the whole
394
+ `d` attribute → the cooldown overlay disappears entirely for that fraction range.
395
+ (In-repo the only caller, `view/units/ability/CombatAbilityView.js:30`, sizes it 64×64, so the game path is safe —
396
+ but this is a public class with a `size` the caller controls.)
397
+ - **Fix**: compute the corner fractions from the actual aspect (`atan2(size_y, size_x)` etc.) instead of hard-coded
398
+ eighths, or pick the edge by testing all four and taking whichever intersects.
399
+ - **Confidence**: Confirmed
400
+
401
+ ### VIEW-25 `EmptyView.absolute` nests the caller's CSS instead of spreading it
402
+ - **Severity**: MAJOR
403
+ - **File**: `src/view/elements/EmptyView.js:54-72`
404
+ - **Defect**:
405
+ ```js
406
+ css: {
407
+ css, // shorthand -> a property literally named "css"
408
+ ...CSS_ABSOLUTE_POSITIONING
409
+ }
410
+ ```
411
+ The intent is clearly `...css`. `View.css(hash)` then executes `this.el.style['css'] = {…}`, which the CSSOM
412
+ ignores.
413
+ - **Failure scenario**: `EmptyView.absolute({ css: { width: '100px', backgroundColor: 'red' } })` produces an element
414
+ with only `position/top/left` applied; width and background are silently dropped, so the element collapses to zero
415
+ size. No error is raised anywhere.
416
+ - **Fix**: `css: { ...css, ...CSS_ABSOLUTE_POSITIONING }`.
417
+ - **Confidence**: Confirmed
418
+
419
+ ### VIEW-26 `TileGridView` subscribes to the model and the DnD context outside the view lifecycle
420
+ - **Severity**: MAJOR
421
+ - **File**: `src/view/elements/tiles2d/TileGrid.js:135-136,301-338,411-419`
422
+ - **Defect**: `model.tiles.on.added.add(this.addTile, this)` and `.on.removed.add(this.removeTile, this)` are raw
423
+ subscriptions (not `bindSignal`) with no counterpart anywhere. Likewise every slot registers a `DropTarget` into
424
+ `this.__dragAndDropContext` via `addTarget` (which immediately `link()`s DOM listeners) and every tile registers a
425
+ `Draggable` via `addElement`; neither `removeTarget` nor `removeElement` is ever called, and the context is a
426
+ constructor parameter, i.e. typically shared across grids.
427
+ - **Failure scenario**: An inventory panel builds a 10×6 `TileGridView` against a shared `DragAndDropContext` each
428
+ time it opens. Every open/close cycle permanently adds 60 `DropTarget`s (each with 4 DOM listeners) and one
429
+ `Draggable` per item to `context.targets`/`context.elements`, and leaves the grid subscribed to `model.tiles`. After
430
+ 20 cycles, `DragAndDropContext.finalizeDrag` iterates 1200+ targets on every drop, `getElementById` scans hundreds
431
+ of dead draggables, and adding an item to the inventory while all panels are closed calls `addTile` on 20 detached
432
+ grids. Removing an item calls `removeTile` on grids where `findTileView` returns `undefined`, so
433
+ `this.__hookTileRemoved(undefined, tile)` fires with an undefined view.
434
+ - **Fix**: route the two model subscriptions through `bindSignal`; add an `unlink()` that calls
435
+ `context.removeTarget(slotView)` for each slot and `context.removeElement(draggable)` for each tile, and rebuild on
436
+ `link()`.
437
+ - **Confidence**: Confirmed
438
+
439
+ ### VIEW-27 `LocalizedLabelView.update()` dereferences a null localization outside the try
440
+ - **Severity**: MAJOR
441
+ - **File**: `src/view/common/LocalizedLabelView.js:122-154`
442
+ - **Defect**: `localization = null` is the constructor default and the class explicitly tests for it at `:60`, so it
443
+ is an anticipated state. `update()` wraps only `localization.getString(...)` in `try/catch`; line 143
444
+ (`this.__localeKey = localization.locale.getValue()`) and the `$el.text(value)` fallback that follows are outside
445
+ it. The throw at 143 skips the fallback text.
446
+ - **Failure scenario**: `new TaskProgressView({ task })` — `localization` defaults to `null`
447
+ (`task/TaskProgressView.js:20,29-33`) — then link. The `on.linked` handler calls `update()`, `getString` throws,
448
+ the catch sets `value = "failed to localize @…"`, then line 143 throws `TypeError: Cannot read properties of null
449
+ (reading 'locale')` before the text is written. Signal's dispatch try/catch swallows it, so the loading screen's
450
+ task-name label renders completely blank with two console errors per link.
451
+ - **Fix**: return early (writing the raw `@id` fallback) when `localization === null`, and move the `__localeKey`
452
+ assignment after a successful `getString`.
453
+ - **Confidence**: Confirmed
454
+
455
+ ### VIEW-28 `MarkerGL` leaks atlas patch references acquired across shutdown / rapid icon changes
456
+ - **Severity**: MAJOR
457
+ - **File**: `src/view/minimap/gl/MarkerGL.js:57-87,109-118`
458
+ - **Defect**: `updateIcon` releases the *current* `this.patch`, then awaits `atlasManager.acquire(newURL)`. The
459
+ continuation assigns `this.patch = patch` and then bails on `!this.active` — but the freshly acquired reference is
460
+ never released on that path. Similarly, two `updateIcon` calls in flight both release `Reference.NULL` up front and
461
+ the first resolved patch is overwritten (and leaked) by the second.
462
+ - **Failure scenario**: A unit with a minimap marker is killed while its icon texture is still being fetched
463
+ (`MinimapMarkersGL.removeElement` → `markerGL.shutdown()` → `active = false`, releases `Reference.NULL`). The
464
+ `acquire` promise then resolves, stores the patch on the dead marker and returns without releasing it → the
465
+ `ManagedAtlas` never frees that icon's slot. Repeated over a battle with many short-lived markers, the atlas fills
466
+ up and later markers fail to get a patch.
467
+ - **Fix**: in the continuation, if `!this.active`, call `patch.release()` before returning; also release the previous
468
+ `this.patch` when overwriting it rather than at call time.
469
+ - **Confidence**: Confirmed
470
+
471
+ ### VIEW-29 `DropTarget` drag-enter tracking has no dedup and splices with `-1`
472
+ - **Severity**: MAJOR
473
+ - **File**: `src/view/common/dnd/DropTarget.js:72-92,212-236`
474
+ - **Defect**: `handleDragEnter` pushes into `isInState_ENTER` unconditionally, and
475
+ `attachDropValidityIndicators.handleDragEnter` unconditionally appends a `{draggable, bindings}` record with a fresh
476
+ `SignalBinding` on `draggable.on.dragFinalized`. `removeDraggable` removes exactly one record per call.
477
+ `handleDragLeave` additionally does `isInState_ENTER.splice(indexOf(e), 1)` without checking for `-1`, and
478
+ `splice(-1, 1)` removes the *last* element.
479
+ - **Failure scenario**: `Draggable.link()` explicitly routes `dragenter` to the parent drop target
480
+ (`common/dnd/Draggable.js:103-106`) *and* the event bubbles to the drop target's own listener — so dragging an item
481
+ over another item inside the same slot fires `handleDragEnter` twice for one enter. `current` gains two records and
482
+ two live bindings. On drop, `dragFinalized` fires once, `removeDraggable` clears one record, and `update()` still
483
+ sees a non-empty `current` → the slot keeps its `drop-target-hover-valid` class permanently after the drag ends,
484
+ and each further hover leaks another `SignalBinding` on the draggable.
485
+ - **Fix**: dedup on enter (skip if the draggable is already tracked), loop `removeDraggable` until it returns false on
486
+ finalize, and guard the `indexOf === -1` case before splicing.
487
+ - **Confidence**: Likely
488
+
489
+ ### VIEW-30 A throwing tooltip factory disables tooltips globally for the rest of the session
490
+ - **Severity**: MAJOR
491
+ - **File**: `src/view/tooltip/TooltipManager.js:129-138`
492
+ - **Defect**:
493
+ ```js
494
+ gml.pushState();
495
+ gml.setTooltipsEnabled(false);
496
+ const factoryProduct = tip.factory.call(tip.factoryContext); // may throw
497
+ gml.popState();
498
+ ```
499
+ There is no `try/finally`. `GMLEngine.compile`/`compileAsText` both use `try { … } finally { this.popState(); }`
500
+ (`tooltip/gml/GMLEngine.js:481-493,512-525`), so the omission here is clearly unintended.
501
+ - **Failure scenario**: A single tooltip factory throws (e.g. an item tooltip for an item whose data row is missing).
502
+ `popState()` is skipped, so `__tooltipsEnabled` stays `false` and one orphan frame stays on `__contextStack`. Every
503
+ reference compiler that consults `gml.getTooltipsEnabled()` from then on sees `false` → nested tooltips stop being
504
+ attached anywhere in the UI until reload.
505
+ - **Fix**: wrap the factory call in `try { … } finally { gml.popState(); }`.
506
+ - **Confidence**: Confirmed
507
+
508
+ ### VIEW-31 `RadialMenu.init()` leaves the previous element views attached, and shared icon views get stolen
509
+ - **Severity**: MAJOR
510
+ - **File**: `src/view/elements/radial/RadialMenu.js:194-223`, `src/view/elements/radial/RadialMenuElement.js:70`
511
+ - **Defect**: `init()` resets `this.elements = []` but never removes the previously created `RadialMenuElement`
512
+ children from `this.vElementContainer`. Separately, `RadialMenuElement`'s constructor does
513
+ `this.addChild(description.iconView)` — the icon `View` lives on the caller-owned
514
+ `RadialMenuElementDefinition`, and `View.addChild` re-parents it (`child.parent = this; this.el.appendChild(child.el)`).
515
+ - **Failure scenario**: (a) `menu.init(newItems)` on an existing menu — the old wedges, labels and icons stay in the
516
+ DOM underneath the new ones, and `selectByAngle`/`render` only touch the new set, so the stale wedges are frozen in
517
+ place and never highlight. (b) Building two `RadialMenu`s from the same `items` array (definitions are plain data
518
+ objects that are easy to reuse) — the second menu's `addChild` moves every `iconView`'s DOM node out of the first
519
+ menu, leaving the first menu iconless while both `children` arrays still list it.
520
+ - **Fix**: `this.vElementContainer.removeAllChildren()` at the top of `init()`; and either clone the icon view per
521
+ element or document/enforce one definition per menu.
522
+ - **Confidence**: Confirmed
523
+
524
+ ### VIEW-32 `ConfirmationDialogView` builds a class name that can contain a space
525
+ - **Severity**: MAJOR
526
+ - **File**: `src/view/elements/ConfirmationDialogView.js:38`
527
+ - **Defect**: `"button-" + option.name.replace(' ', '-')` — `String.replace` with a string pattern replaces only the
528
+ **first** occurrence. The result is handed to `ButtonView`'s `classList`, which reaches `DOMTokenList.add()`.
529
+ - **Failure scenario**: `new ConfirmationDialogView(content, [{name: 'cancel all now', displayName: 'Cancel', …}])`
530
+ → class token `"button-cancel-all now"` → `classList.add` throws
531
+ `InvalidCharacterError: The string contains invalid characters`, aborting the whole dialog construction so no
532
+ confirmation dialog is shown at all.
533
+ - **Fix**: `option.name.replace(/\s+/g, '-')`, or reuse `string_tag_to_css_class_name`.
534
+ - **Confidence**: Confirmed
535
+
536
+ ### VIEW-33 `VirtualListView` disables the scrollbar one row too early, and `AlignmentOption.Center` does nothing
537
+ - **Severity**: MINOR
538
+ - **File**: `src/view/common/VirtualListView.js:181-196`
539
+ - **Defect**: The "everything fits" test is
540
+ `row_height * (last_visible_line - first_visible_line) < this.size.y`, i.e. it measures `n-1` rows instead of `n`.
541
+ Separately, `AlignmentOption.Center` is exported and accepted as `verticalAlignment` but the offset block only
542
+ handles `End`, so `Center` silently behaves like `Start`.
543
+ - **Failure scenario**: `lineSize = 20, lineSpacing = 0`, 5 elements, `size.y = 90` (content 100 px). `l0 = 0`,
544
+ `l1 = 4 = numTotalElements - 1`, and `20 * (4 - 0) = 80 < 90` → `__setScrollBar(false)` and `rowWidth` is not
545
+ reduced. The list declares itself fully visible while the 5th row is clipped by 10 px and cannot be scrolled to.
546
+ - **Fix**: compare `row_height * (last - first + 1) <= this.size.y`; implement the `Center` branch
547
+ (`line_offset_y = (this.size.y - total_lines_height) / 2`).
548
+ - **Confidence**: Confirmed
549
+
550
+ ### VIEW-34 `DropDownSelectionView.setSelectedValue` uses the model index against always-appended options
551
+ - **Severity**: MINOR
552
+ - **File**: `src/view/elements/DropDownSelectionView.js:55-68,141-143`
553
+ - **Defect**: `handleAdded(el, index)` ignores `index` and always `appendChild`s, while `setSelectedValue` does
554
+ `this.el.selectedIndex = this.model.indexOf(v)`. The two orderings only agree when elements are appended.
555
+ (`getSelectedValue` correctly resolves through the id map, so the two accessors disagree.) `handleRemoved` also
556
+ never deletes the entry from `this.mapping`.
557
+ - **Failure scenario**: `model.insert(0, x)` on a populated list — `x`'s `<option>` lands last while `model.indexOf(x)`
558
+ is `0`, so `setSelectedValue(x)` selects whatever element was originally first.
559
+ - **Fix**: honour `index` in `handleAdded` (`insertBefore` the option at `index`), and delete the mapping entry in
560
+ `handleRemoved`.
561
+ - **Confidence**: Confirmed
562
+
563
+ ### VIEW-35 `MinimapCameraView.castTerrainRay` jitters an origin it never uses
564
+ - **Severity**: MINOR
565
+ - **File**: `src/view/minimap/dom/MinimapCameraView.js:99-117`
566
+ - **Defect**: The retry loop perturbs `oX/oY/oZ` but calls
567
+ `terrain.raycastFirstSync(rayContact, originX, originY, originZ, …)` with the *unperturbed* origin every iteration.
568
+ `oX/oY/oZ` are written and never read.
569
+ - **Failure scenario**: A frustum corner ray that lands exactly on a terrain triangle edge misses. The stated purpose
570
+ of the loop — "jitter origin in case of mathematical error at polygon edges" — never happens; instead the identical
571
+ raycast runs 10 times and then falls back to the flat `y = 0` plane. On elevated terrain that corner of the minimap
572
+ camera-frustum polygon is drawn at the ground-plane intersection instead of the terrain surface, so the frustum
573
+ outline visibly snaps outward for that corner.
574
+ - **Fix**: pass `oX, oY, oZ` to `raycastFirstSync`.
575
+ - **Confidence**: Confirmed
576
+
577
+ ### VIEW-36 `DOM.prototype.clear()` does not remove text nodes
578
+ - **Severity**: MINOR
579
+ - **File**: `src/view/DOM.js:131-137`
580
+ - **Defect**: The loop iterates `el.children` (elements only), so text nodes survive a method documented as
581
+ "removes all children".
582
+ - **Failure scenario**: `const d = domify('div'); d.text('Loading…'); d.clear(); d.append(other);` → the element still
583
+ reads "Loading…" followed by the appended content. This is reachable through
584
+ `LocalizedLabelView.update()` (`$el.text(value)` on the non-GML path) followed by a switch to a GML-compiled value,
585
+ which calls `removeAllChildren()` and then appends compiled views next to the leftover text node; also
586
+ `MinimapView.link()` (`minimap/Minimap.js:217-219`) relies on `clear()` before appending the WebGL canvas.
587
+ - **Fix**: `while (el.firstChild) el.removeChild(el.firstChild);`
588
+ - **Confidence**: Confirmed
589
+
590
+ ### VIEW-37 `setElementTransform.js` exports nothing
591
+ - **Severity**: MINOR
592
+ - **File**: `src/view/setElementTransform.js:13`
593
+ - **Defect**: The module declares `function setElementTransform(...)` with no `export` and no default export, so the
594
+ file's only purpose is unreachable.
595
+ - **Failure scenario**: `import { setElementTransform } from '.../setElementTransform.js'` →
596
+ `SyntaxError: The requested module does not provide an export named 'setElementTransform'` at build time.
597
+ - **Fix**: `export function setElementTransform(...)`.
598
+ - **Confidence**: Confirmed
599
+
600
+ ### VIEW-38 `DatGuiUtils.makeDatController` references an undeclared `p` in its cycle branch
601
+ - **Severity**: MINOR
602
+ - **File**: `src/view/controller/dat/DatGuiUtils.js:377-380`
603
+ - **Defect**: ``console.warn(`Detected cycle at "${p}" …`)`` — `p` is the loop variable of `makeDatControllerForObject`,
604
+ not of this function. Modules are strict-mode, so this is a `ReferenceError`, not `undefined`. The intended
605
+ identifier is `propertyName`.
606
+ - **Failure scenario**: `makeDatController` is exported (`:451`). A direct call
607
+ `makeDatController(gui, obj, 'child', pathContainingObjChild)` hits the cycle branch and throws
608
+ `ReferenceError: p is not defined` instead of warning and returning `0`, aborting the whole controller build.
609
+ - **Fix**: use `${propertyName}`.
610
+ - **Confidence**: Confirmed
611
+
612
+ ### VIEW-39 `GroupView.remove()` leaves the removed child linked; `update()` calls a method `View` does not have
613
+ - **Severity**: MINOR
614
+ - **File**: `src/view/elements/Group.js:8,11-30`
615
+ - **Defect**: `add`/`remove` bypass `View.addChild`/`View.removeChild`: `child.parent` is never set and `remove` never
616
+ calls `child.unlink()`. `update()` invokes `c.update()` on each child, but `View` defines no `update`.
617
+ - **Failure scenario**: `group.add(labelView); group.link(); group.remove(labelView);` — the label's DOM node is
618
+ detached but the view stays linked, so its `bindSignal` subscriptions keep firing and keep writing to a node that is
619
+ no longer in the document. `group.update()` with any plain `EmptyView` child →
620
+ `TypeError: c.update is not a function`.
621
+ - **Fix**: delegate to `View.addChild`/`View.removeChild` (and drop the `this.children = []` reassignment); remove
622
+ `update()` or make it guard on the method existing.
623
+ - **Confidence**: Confirmed
624
+
625
+ ### VIEW-40 `WindRoseDiagram` sizes itself for icons that may not exist, and re-registers tooltips on every draw
626
+ - **Severity**: MINOR
627
+ - **File**: `src/view/elements/windrose/WindRoseDiagram.js:90-94,197-336`
628
+ - **Defect**: (a) `const has_icons = axes.some(isDefined)` tests whether each *axis object* is defined, not whether it
629
+ has an `icon`; for any non-empty `axes` array this is always `true`. (b) `draw_canvas()` calls
630
+ `container.removeAllChildren()` and rebuilds every label/icon, then calls `this.__tooltips.add(view, …)` for each —
631
+ `DomTooltipManager.add` links a `DomTooltipObserver` immediately and is never paired with `remove`.
632
+ - **Failure scenario**: (a) `new WindRoseDiagram({axes: [{name:'a'},{name:'b'},{name:'c'}], radius_outer: 100,
633
+ icon_size: 32})` (no icons at all) → `size = 200 + 32*1.207*2 ≈ 277` instead of `200`; the widget claims 77 px more
634
+ in both axes than it draws, pushing neighbours around in a flex row. (b) `draw()` runs on every `on.linked`; after
635
+ 10 open/close cycles each axis label has 10 live `DomTooltipObserver`s with `mouseenter`/`mouseleave` listeners on
636
+ detached elements.
637
+ - **Fix**: `axes.some(a => a.icon !== undefined)`; use `tooltips.manage(view, …)` (which self-registers on
638
+ link/unlink) rather than `add`.
639
+ - **Confidence**: Confirmed
640
+
641
+ ### VIEW-41 `ImageView`/`VideoView` never re-apply an observed URL on link
642
+ - **Severity**: MINOR
643
+ - **File**: `src/view/elements/image/ImageView.js:65-71`, `src/view/elements/video/VideoView.js:77-83`
644
+ - **Defect**: The `src` is baked into the cache key at construction and thereafter only updated by the
645
+ `url.onChanged` → `__setSource` binding, which is a `bindSignal` and therefore inactive while unlinked. There is no
646
+ `on.linked` initial-apply.
647
+ - **Failure scenario**: An `ImageView` bound to an `ObservedString` icon URL sits in a panel. Close the panel (view
648
+ unlinks, binding detaches), the underlying model swaps the icon (equip a different item), reopen the panel — the
649
+ binding re-links but is never invoked, so the `<img>` still shows the previous icon until the URL happens to change
650
+ again while the panel is open.
651
+ - **Fix**: add `this.on.linked.add(() => this.__setSource(url.getValue()), this)` in the observed-URL branch (mirroring
652
+ `LabelView.link()`, which re-runs `updateTransform()`).
653
+ - **Confidence**: Confirmed
654
+
655
+ ### VIEW-42 `ButtonView` never applies its initial enabled/disabled class
656
+ - **Severity**: MINOR
657
+ - **File**: `src/view/elements/button/ButtonView.js:94,101-110`
658
+ - **Defect**: `__enabled` is initialised by direct field assignment, bypassing the setter, and the setter early-returns
659
+ when the value is unchanged. A button that is enabled from birth therefore carries **neither** `.enabled` nor
660
+ `.disabled`.
661
+ - **Failure scenario**: `.ui-button-view.disabled { opacity: .4 }` + `.ui-button-view.enabled { cursor: pointer }`
662
+ style rules — a freshly created enabled button matches neither. `createInterfaceCommandButton` masks this because
663
+ its `on.linked` handler calls `updateEnableStatus()`, but that only helps when the command's `enabled` differs from
664
+ `true`; a permanently-enabled command's button never gets the class.
665
+ - **Fix**: set the class explicitly at the end of the constructor (`this.setClass('enabled', true);
666
+ this.setClass('disabled', false);`) rather than relying on the setter's change detection.
667
+ - **Confidence**: Likely