@woosh/meep-engine 2.168.0 → 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.
- package/README.md +2 -2
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06/adv-core.md +511 -0
- package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
- package/src/REVIEW_2026_08_06/adv-view.md +446 -0
- package/src/REVIEW_2026_08_06/api-core.md +296 -0
- package/src/REVIEW_2026_08_06/api-engine.md +195 -0
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
- package/src/REVIEW_2026_08_06/core-binary.md +348 -0
- package/src/REVIEW_2026_08_06/core-collection.md +376 -0
- package/src/REVIEW_2026_08_06/core-math.md +221 -0
- package/src/REVIEW_2026_08_06/core-process.md +266 -0
- package/src/REVIEW_2026_08_06/core-science.md +223 -0
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
- package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
- package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
- package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
- package/src/REVIEW_2026_08_06/engine-input.md +197 -0
- package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
- package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
- package/src/REVIEW_2026_08_06/generation.md +176 -0
- package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
- package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
- package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
- package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
- package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
- package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
- package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
- package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
- package/src/REVIEW_2026_08_06/view.md +667 -0
- package/src/REVIEW_2026_08_06.md +571 -0
- package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/BinaryBuffer.js +12 -1
- package/src/core/binary/BitImage2.d.ts +5 -1
- package/src/core/binary/BitImage2.d.ts.map +1 -1
- package/src/core/binary/BitImage2.js +5 -1
- package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
- package/src/core/bvh8/build/NodeProxy.js +314 -308
- package/src/core/collection/list/List.d.ts +15 -0
- package/src/core/collection/list/List.d.ts.map +1 -1
- package/src/core/collection/list/List.js +34 -0
- package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
- package/src/core/collection/list/SortedListProjection.js +4 -1
- package/src/core/collection/map/BiMap.d.ts +25 -2
- package/src/core/collection/map/BiMap.d.ts.map +1 -1
- package/src/core/collection/map/BiMap.js +57 -3
- package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
- package/src/core/debug/matchers/IsAnything.js +5 -1
- package/src/core/events/signal/Signal.d.ts.map +1 -1
- package/src/core/events/signal/Signal.js +3 -1
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
- package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
- package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
- package/src/core/geom/2d/v2_morton_encode.js +23 -0
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
- package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
- package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cotangent.js +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cross.js +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_multiply.js +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_subtract.js +6 -2
- package/src/core/localization/Localization.js +1 -1
- package/src/core/math/random/randomSeed.d.ts +13 -0
- package/src/core/math/random/randomSeed.d.ts.map +1 -0
- package/src/core/math/random/randomSeed.js +16 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
- package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
- package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
- package/src/core/math/statistics/hammersley_sequence.js +25 -16
- package/src/core/process/Future.d.ts.map +1 -1
- package/src/core/process/Future.js +12 -0
- package/src/core/process/PromiseWatcher.d.ts.map +1 -1
- package/src/core/process/PromiseWatcher.js +9 -2
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +12 -2
- package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
- package/src/core/process/worker/WorkerProxy.js +5 -2
- package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
- package/src/core/process/worker/extractTransferables.js +4 -1
- package/src/engine/Engine.d.ts +12 -12
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +149 -10
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +4 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
- package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +196 -24
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextState.js +24 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
- package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +5 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -0
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +4 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +280 -253
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
- package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
- package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
- package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
- package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
- package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
- package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
- package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
- package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
- package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +38 -13
- package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +15 -3
- package/src/engine/physics/ecs/Joint.d.ts +7 -4
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +7 -4
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +11 -4
- package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
- package/src/engine/save/storage/IndexedDBStorage.js +21 -3
- package/src/engine/simulation/Ticker.d.ts +12 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +18 -0
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
- package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
- package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/VolumeField.js +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +8 -1
- package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
- package/src/generation/theme/TerrainTheme.js +2 -1
- package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
- package/src/generation/theme/ThemeEngine.js +6 -5
- package/src/view/ViewGroup.d.ts.map +1 -1
- package/src/view/ViewGroup.js +9 -7
- package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
- package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
- package/src/view/graphics/WebGLContextFailureView.js +76 -0
- package/src/view/minimap/Minimap.d.ts +8 -0
- package/src/view/minimap/Minimap.d.ts.map +1 -1
- package/src/view/minimap/Minimap.js +16 -3
- package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
- 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
|