@woosh/meep-engine 2.169.1 → 2.169.2
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/editor/ecs/component/editors/NumericIntervalEditor.js +18 -35
- package/editor/ecs/component/editors/ObservedIntegerEditor.js +22 -43
- package/editor/ecs/component/editors/ObservedStringEditor.js +34 -51
- package/editor/ecs/component/editors/common/makeV3_editor.js +12 -30
- package/editor/ecs/component/editors/common/two_way_sync.js +50 -0
- package/editor/ecs/component/editors/geom/QuaternionEditor.js +20 -35
- package/editor/ecs/component/editors/geom/Vector1Editor.js +5 -28
- package/editor/ecs/component/editors/primitive/NumberEditor.js +15 -31
- package/editor/tools/paint/TerrainHeightPaintTool.js +22 -31
- package/editor/tools/paint/TerrainPaintTool.js +85 -13
- package/editor/tools/paint/TerrainTexturePaintTool.js +18 -29
- package/editor/view/ecs/components/TerrainController.js +1 -4
- package/package.json +1 -1
- package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +738 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +79 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +93 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +80 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +164 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +69 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +102 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +129 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +72 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +74 -0
- package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +169 -0
- package/src/DUPLICATION_AUDIT_2026_08_07.md +254 -0
- package/src/DUPLICATION_AUDIT_REMEDIATION.md +250 -0
- package/src/core/bvh2/bvh3/query/bvh_collect_user_data.js +2 -2
- package/src/core/bvh2/bvh3/query/bvh_query_leaves_generic.js +77 -77
- package/src/core/bvh2/bvh3/query/bvh_query_leaves_ray.js +90 -90
- package/src/core/bvh2/bvh3/query/bvh_query_leaves_ray_segment.js +94 -94
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_generic.js +86 -86
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_intersects_aabb.js +2 -2
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_intersects_capsule.js +2 -2
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_intersects_frustum.js +2 -2
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_intersects_sphere.js +2 -2
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_ray.js +96 -96
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_ray_segment.js +100 -100
- package/src/core/bvh2/bvh3/query/compute_tight_near_far_clipping_planes.js +2 -2
- package/src/core/bvh8/aabb3_round_to_float32.d.ts +16 -0
- package/src/core/bvh8/aabb3_round_to_float32.d.ts.map +1 -0
- package/src/core/bvh8/aabb3_round_to_float32.js +22 -0
- package/src/core/bvh8/bvh8_geometry_validate.d.ts.map +1 -1
- package/src/core/bvh8/bvh8_geometry_validate.js +1 -12
- package/src/core/bvh8/bvh8_geometry_validate_indirect.d.ts.map +1 -1
- package/src/core/bvh8/bvh8_geometry_validate_indirect.js +1 -12
- package/src/core/cache/Cache.d.ts +13 -0
- package/src/core/cache/Cache.d.ts.map +1 -1
- package/src/core/cache/Cache.js +13 -0
- package/src/core/color/operations/color_darken.d.ts +5 -2
- package/src/core/color/operations/color_darken.d.ts.map +1 -1
- package/src/core/color/operations/color_darken.js +24 -33
- package/src/core/color/operations/color_desaturate.d.ts +5 -2
- package/src/core/color/operations/color_desaturate.d.ts.map +1 -1
- package/src/core/color/operations/color_desaturate.js +24 -33
- package/src/core/color/operations/color_lighten.d.ts +5 -2
- package/src/core/color/operations/color_lighten.d.ts.map +1 -1
- package/src/core/color/operations/color_lighten.js +24 -33
- package/src/core/color/operations/color_saturate.d.ts +5 -2
- package/src/core/color/operations/color_saturate.d.ts.map +1 -1
- package/src/core/color/operations/color_saturate.js +24 -33
- package/src/core/color/operations/color_scale_okhsv_channel.d.ts +28 -0
- package/src/core/color/operations/color_scale_okhsv_channel.d.ts.map +1 -0
- package/src/core/color/operations/color_scale_okhsv_channel.js +45 -0
- package/src/core/geom/2d/hash-grid/SpatialHashGrid.d.ts.map +1 -1
- package/src/core/geom/2d/hash-grid/SpatialHashGrid.js +12 -0
- package/src/core/geom/2d/line/segment2_intersects_segment2_2d.d.ts.map +1 -1
- package/src/core/geom/2d/line/segment2_intersects_segment2_2d.js +25 -29
- package/src/core/geom/2d/line/segment2_segment_compute_intersection_array_2d.d.ts.map +1 -1
- package/src/core/geom/2d/line/segment2_segment_compute_intersection_array_2d.js +46 -51
- package/src/core/geom/2d/line/segment2_segment_intersection_fractions.d.ts +31 -0
- package/src/core/geom/2d/line/segment2_segment_intersection_fractions.d.ts.map +1 -0
- package/src/core/geom/2d/line/segment2_segment_intersection_fractions.js +52 -0
- package/src/core/geom/2d/quad-tree/qt_collect_by_circle.d.ts +6 -1
- package/src/core/geom/2d/quad-tree/qt_collect_by_circle.d.ts.map +1 -1
- package/src/core/geom/2d/quad-tree/qt_collect_by_circle.js +39 -65
- package/src/core/geom/2d/quad-tree/qt_match_data_by_circle.d.ts +15 -5
- package/src/core/geom/2d/quad-tree/qt_match_data_by_circle.d.ts.map +1 -1
- package/src/core/geom/2d/quad-tree/qt_match_data_by_circle.js +101 -70
- package/src/core/geom/2d/r-tree/StaticR2Tree.d.ts.map +1 -1
- package/src/core/geom/2d/r-tree/StaticR2Tree.js +7 -3
- package/src/core/geom/3d/aabb/aabb3_intersects_ray_segment.d.ts +7 -8
- package/src/core/geom/3d/aabb/aabb3_intersects_ray_segment.d.ts.map +1 -1
- package/src/core/geom/3d/aabb/aabb3_intersects_ray_segment.js +15 -51
- package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.js +13 -14
- package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.js +2 -17
- package/src/core/geom/3d/atlas/segment/atlas_chart_roundness_delta.d.ts +22 -0
- package/src/core/geom/3d/atlas/segment/atlas_chart_roundness_delta.d.ts.map +1 -0
- package/src/core/geom/3d/atlas/segment/atlas_chart_roundness_delta.js +30 -0
- package/src/core/geom/3d/atlas/segment/atlas_segment_charts.d.ts +19 -0
- package/src/core/geom/3d/atlas/segment/atlas_segment_charts.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_segment_charts.js +5 -9
- package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.js +4 -12
- package/src/core/geom/3d/frustum/read_frustum_corner.d.ts +5 -0
- package/src/core/geom/3d/frustum/read_frustum_corner.d.ts.map +1 -1
- package/src/core/geom/3d/frustum/read_frustum_corner.js +8 -5
- package/src/core/geom/3d/line/line3_segment_segment_closest_point_pair.d.ts +33 -0
- package/src/core/geom/3d/line/line3_segment_segment_closest_point_pair.d.ts.map +1 -0
- package/src/core/geom/3d/line/line3_segment_segment_closest_point_pair.js +71 -0
- package/src/core/geom/3d/mat4/m4_compose.d.ts +5 -0
- package/src/core/geom/3d/mat4/m4_compose.d.ts.map +1 -1
- package/src/core/geom/3d/mat4/m4_compose.js +22 -53
- package/src/core/geom/3d/mat4/m4_decompose.d.ts +8 -0
- package/src/core/geom/3d/mat4/m4_decompose.d.ts.map +1 -1
- package/src/core/geom/3d/mat4/m4_decompose.js +84 -76
- package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale.d.ts +2 -0
- package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale.d.ts.map +1 -1
- package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale.js +10 -44
- package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale_scalar.d.ts +27 -0
- package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale_scalar.d.ts.map +1 -0
- package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale_scalar.js +67 -0
- package/src/core/geom/3d/plane/orient3d_fast.d.ts.map +1 -1
- package/src/core/geom/3d/plane/orient3d_fast.js +2 -22
- package/src/core/geom/3d/plane/orient3d_fast_point.d.ts +28 -0
- package/src/core/geom/3d/plane/orient3d_fast_point.d.ts.map +1 -0
- package/src/core/geom/3d/plane/orient3d_fast_point.js +47 -0
- package/src/core/geom/3d/quaternion/quat3_nlerp.d.ts +48 -0
- package/src/core/geom/3d/quaternion/quat3_nlerp.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_nlerp.js +72 -0
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts +9 -0
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/AbstractShape3D.js +44 -1
- package/src/core/geom/3d/shape/BoxShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/BoxShape3D.js +2 -1
- package/src/core/geom/3d/shape/CapsuleShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/CapsuleShape3D.js +2 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +0 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +25 -56
- package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/CylinderShape3D.js +2 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +3 -5
- package/src/core/geom/3d/shape/MeshShape3D.d.ts +0 -11
- package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/MeshShape3D.js +27 -106
- package/src/core/geom/3d/shape/Triangle3D.d.ts +1 -1
- package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/Triangle3D.js +10 -31
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.js +8 -57
- package/src/core/geom/3d/topology/simplify/decimate_edge_collapse_snap.d.ts +8 -1
- package/src/core/geom/3d/topology/simplify/decimate_edge_collapse_snap.d.ts.map +1 -1
- package/src/core/geom/3d/topology/simplify/decimate_edge_collapse_snap.js +281 -272
- package/src/core/geom/3d/topology/simplify/quadratic/build_vertex_quadrics.d.ts.map +1 -1
- package/src/core/geom/3d/topology/simplify/quadratic/build_vertex_quadrics.js +3 -4
- package/src/core/geom/3d/topology/simplify/quadratic/quadric3_set_from_plane_through_point.d.ts +22 -0
- package/src/core/geom/3d/topology/simplify/quadratic/quadric3_set_from_plane_through_point.d.ts.map +1 -0
- package/src/core/geom/3d/topology/simplify/quadratic/quadric3_set_from_plane_through_point.js +27 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_boundary_loop.d.ts +24 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_boundary_loop.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_boundary_loop.js +73 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.js +2 -53
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +3 -4
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +3 -33
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_from_indexed_geometry.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_from_indexed_geometry.js +4 -37
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_poke.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_poke.js +3 -32
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_read_triangle.d.ts +32 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_read_triangle.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_read_triangle.js +66 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path.js +703 -728
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path_polyanya.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path_polyanya.js +13 -24
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_sample_interior_grid_points.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_sample_interior_grid_points.js +8 -54
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_surface_ray_parity.d.ts +35 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_surface_ray_parity.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_surface_ray_parity.js +82 -0
- package/src/core/geom/3d/triangle/tri3_closest_point.d.ts +29 -0
- package/src/core/geom/3d/triangle/tri3_closest_point.d.ts.map +1 -0
- package/src/core/geom/3d/triangle/tri3_closest_point.js +65 -0
- package/src/core/geom/3d/triangle/tri3_flatten_isometric.d.ts +42 -0
- package/src/core/geom/3d/triangle/tri3_flatten_isometric.d.ts.map +1 -0
- package/src/core/geom/3d/triangle/tri3_flatten_isometric.js +79 -0
- package/src/core/geom/Vector2.d.ts.map +1 -1
- package/src/core/geom/Vector2.js +2 -1
- package/src/core/geom/random/random_orthonormal_basis.d.ts +21 -0
- package/src/core/geom/random/random_orthonormal_basis.d.ts.map +1 -0
- package/src/core/geom/random/random_orthonormal_basis.js +31 -0
- package/src/core/geom/vec2/v2_rotate.d.ts +18 -0
- package/src/core/geom/vec2/v2_rotate.d.ts.map +1 -0
- package/src/core/geom/vec2/v2_rotate.js +21 -0
- package/src/core/geom/vec2/v2_rotate_cos_sin.d.ts +19 -0
- package/src/core/geom/vec2/v2_rotate_cos_sin.d.ts.map +1 -0
- package/src/core/geom/vec2/v2_rotate_cos_sin.js +21 -0
- package/src/core/graph/csr/csr_graph_from_undirected_edge_list.d.ts +30 -0
- package/src/core/graph/csr/csr_graph_from_undirected_edge_list.d.ts.map +1 -0
- package/src/core/graph/csr/csr_graph_from_undirected_edge_list.js +94 -0
- package/src/core/graph/layout/CircleLayout.d.ts.map +1 -1
- package/src/core/graph/layout/CircleLayout.js +6 -29
- package/src/core/graph/layout/box/BoxLayouter.d.ts.map +1 -1
- package/src/core/graph/layout/box/BoxLayouter.js +2 -26
- package/src/core/graph/layout/box/position_box_next_to_box.d.ts.map +1 -1
- package/src/core/graph/layout/box/position_box_next_to_box.js +2 -1
- package/src/core/graph/layout/computeDisconnectedSubGraphs.d.ts.map +1 -1
- package/src/core/graph/layout/computeDisconnectedSubGraphs.js +14 -3
- package/src/core/graph/layout/graph_peel_topological_layer.d.ts +25 -0
- package/src/core/graph/layout/graph_peel_topological_layer.d.ts.map +1 -0
- package/src/core/graph/layout/graph_peel_topological_layer.js +45 -0
- package/src/core/math/hash/hash_mix2.d.ts +21 -0
- package/src/core/math/hash/hash_mix2.d.ts.map +1 -0
- package/src/core/math/hash/hash_mix2.js +22 -0
- package/src/core/math/hash/hash_mix3.d.ts +16 -0
- package/src/core/math/hash/hash_mix3.d.ts.map +1 -0
- package/src/core/math/hash/hash_mix3.js +19 -0
- package/src/core/math/lookup/LUT_HEATMAP.d.ts +17 -0
- package/src/core/math/lookup/LUT_HEATMAP.d.ts.map +1 -0
- package/src/core/math/lookup/LUT_HEATMAP.js +30 -0
- package/src/core/math/noise/curl_noise_3d.d.ts +6 -0
- package/src/core/math/noise/curl_noise_3d.d.ts.map +1 -1
- package/src/core/math/noise/curl_noise_3d.js +18 -27
- package/src/core/math/statistics/hammersley_sequence.d.ts +28 -16
- package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
- package/src/core/math/statistics/hammersley_sequence.js +53 -34
- package/src/core/science/units/MEASUREMENT_UNITS_SI.d.ts +3 -0
- package/src/core/science/units/MEASUREMENT_UNITS_SI.d.ts.map +1 -1
- package/src/core/science/units/MEASUREMENT_UNITS_SI.js +5 -16
- package/src/engine/animation/TransitionFunctions.d.ts +31 -0
- package/src/engine/animation/TransitionFunctions.d.ts.map +1 -1
- package/src/engine/animation/TransitionFunctions.js +44 -0
- package/src/engine/animation/curve/animation_curve_binary_codec.d.ts +29 -0
- package/src/engine/animation/curve/animation_curve_binary_codec.d.ts.map +1 -0
- package/src/engine/animation/curve/animation_curve_binary_codec.js +60 -0
- package/src/engine/animation/curve/draw/canvas_point_to_curve.d.ts +16 -0
- package/src/engine/animation/curve/draw/canvas_point_to_curve.d.ts.map +1 -0
- package/src/engine/animation/curve/draw/canvas_point_to_curve.js +22 -0
- package/src/engine/animation/curve/draw/curve_point_to_canvas.d.ts +27 -0
- package/src/engine/animation/curve/draw/curve_point_to_canvas.d.ts.map +1 -0
- package/src/engine/animation/curve/draw/curve_point_to_canvas.js +38 -0
- package/src/engine/animation/curve/draw/position_canvas_to_curve.d.ts +2 -0
- package/src/engine/animation/curve/draw/position_canvas_to_curve.d.ts.map +1 -1
- package/src/engine/animation/curve/draw/position_canvas_to_curve.js +22 -24
- package/src/engine/animation/curve/draw/position_curve_to_canvas.d.ts +2 -0
- package/src/engine/animation/curve/draw/position_curve_to_canvas.d.ts.map +1 -1
- package/src/engine/animation/curve/draw/position_curve_to_canvas.js +22 -25
- package/src/engine/animation/curve/editor/canvas2dDrawWorldAxisLabels.d.ts.map +1 -1
- package/src/engine/animation/curve/editor/canvas2dDrawWorldAxisLabels.js +3 -2
- package/src/engine/animation/curve/editor/canvas2dPlotCurvePoints.d.ts.map +1 -1
- package/src/engine/animation/curve/editor/canvas2dPlotCurvePoints.js +14 -14
- package/src/engine/animation/removeEntityWithMeshParticlesEffect.d.ts.map +1 -1
- package/src/engine/animation/removeEntityWithMeshParticlesEffect.js +5 -0
- package/src/engine/asset/loaders/GLTFAssetLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/GLTFAssetLoader.js +25 -51
- package/src/engine/asset/loaders/material/StaticMaterialCache.d.ts +3 -2
- package/src/engine/asset/loaders/material/StaticMaterialCache.d.ts.map +1 -1
- package/src/engine/asset/loaders/material/StaticMaterialCache.js +16 -55
- package/src/engine/asset/loaders/material/TextureCoalescer.d.ts +32 -0
- package/src/engine/asset/loaders/material/TextureCoalescer.d.ts.map +1 -0
- package/src/engine/asset/loaders/material/TextureCoalescer.js +68 -0
- package/src/engine/asset/loaders/material/traverseMaterialTextures.d.ts +10 -2
- package/src/engine/asset/loaders/material/traverseMaterialTextures.d.ts.map +1 -1
- package/src/engine/asset/loaders/material/traverseMaterialTextures.js +10 -2
- package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/Mantle.js +10 -26
- package/src/engine/ecs/dynamic_actions/actions/definition/WeightedRandomActionDescription.d.ts.map +1 -1
- package/src/engine/ecs/dynamic_actions/actions/definition/WeightedRandomActionDescription.js +5 -0
- package/src/engine/ecs/evaluation/complex/PointFitnessGaussianBlur.d.ts +22 -2
- package/src/engine/ecs/evaluation/complex/PointFitnessGaussianBlur.d.ts.map +1 -1
- package/src/engine/ecs/evaluation/complex/PointFitnessGaussianBlur.js +92 -41
- package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.d.ts +3 -1
- package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.d.ts.map +1 -1
- package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.js +14 -16
- package/src/engine/ecs/gui/parallax/GuiElementParallaxSystem.d.ts.map +1 -1
- package/src/engine/ecs/gui/parallax/GuiElementParallaxSystem.js +16 -29
- package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts.map +1 -1
- package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +12 -74
- package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.d.ts.map +1 -1
- package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.js +10 -73
- package/src/engine/ecs/ik/ik_probe_terrain_contact.d.ts +38 -0
- package/src/engine/ecs/ik/ik_probe_terrain_contact.d.ts.map +1 -0
- package/src/engine/ecs/ik/ik_probe_terrain_contact.js +119 -0
- package/src/engine/ecs/terrain/util/paintTerrainOverlayViaLookupTable.d.ts +0 -1
- package/src/engine/ecs/terrain/util/paintTerrainOverlayViaLookupTable.d.ts.map +1 -1
- package/src/engine/ecs/terrain/util/paintTerrainOverlayViaLookupTable.js +2 -14
- package/src/engine/graphics/FULL_SCREEN_QUAD_VERTEX_SHADER.d.ts +12 -1
- package/src/engine/graphics/FULL_SCREEN_QUAD_VERTEX_SHADER.d.ts.map +1 -1
- package/src/engine/graphics/FULL_SCREEN_QUAD_VERTEX_SHADER.js +14 -10
- package/src/engine/graphics/ecs/camera/auto_set_camera_clipping_planes.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/auto_set_camera_clipping_planes.js +7 -28
- package/src/engine/graphics/ecs/highlight/renderer/HighlightDecodeShader.d.ts +1 -2
- package/src/engine/graphics/ecs/highlight/renderer/HighlightDecodeShader.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/renderer/HighlightDecodeShader.js +4 -12
- package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.js +2 -11
- package/src/engine/graphics/ecs/highlight/renderer/makeBlurShader_9.d.ts +1 -2
- package/src/engine/graphics/ecs/highlight/renderer/makeBlurShader_9.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/renderer/makeBlurShader_9.js +3 -11
- package/src/engine/graphics/ecs/highlight/renderer/makeDilationShader.d.ts +1 -2
- package/src/engine/graphics/ecs/highlight/renderer/makeDilationShader.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/renderer/makeDilationShader.js +3 -11
- package/src/engine/graphics/ecs/highlight/renderer/make_screen_space_quad_material.d.ts +33 -0
- package/src/engine/graphics/ecs/highlight/renderer/make_screen_space_quad_material.d.ts.map +1 -0
- package/src/engine/graphics/ecs/highlight/renderer/make_screen_space_quad_material.js +44 -0
- package/src/engine/graphics/ecs/highlight/system/HighlightSystemBase.d.ts +55 -0
- package/src/engine/graphics/ecs/highlight/system/HighlightSystemBase.d.ts.map +1 -0
- package/src/engine/graphics/ecs/highlight/system/HighlightSystemBase.js +205 -0
- package/src/engine/graphics/ecs/highlight/system/MeshHighlightSystem.d.ts +4 -45
- package/src/engine/graphics/ecs/highlight/system/MeshHighlightSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/system/MeshHighlightSystem.js +39 -162
- package/src/engine/graphics/ecs/highlight/system/RenderableHighlightSystem.d.ts +4 -51
- package/src/engine/graphics/ecs/highlight/system/RenderableHighlightSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/system/RenderableHighlightSystem.js +34 -164
- package/src/engine/graphics/ecs/highlight/system/ShadedGeometryHighlightSystem.d.ts +5 -57
- package/src/engine/graphics/ecs/highlight/system/ShadedGeometryHighlightSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/system/ShadedGeometryHighlightSystem.js +48 -175
- package/src/engine/graphics/ecs/light/shadow/extend_shadow_camera_near_for_casters.d.ts.map +1 -1
- package/src/engine/graphics/ecs/light/shadow/extend_shadow_camera_near_for_casters.js +7 -25
- package/src/engine/graphics/ecs/make_bvh_depth_range_computer.d.ts +26 -0
- package/src/engine/graphics/ecs/make_bvh_depth_range_computer.d.ts.map +1 -0
- package/src/engine/graphics/ecs/make_bvh_depth_range_computer.js +33 -0
- package/src/engine/graphics/ecs/mesh/MeshSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh/MeshSystem.js +2 -5
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +2 -5
- package/src/engine/graphics/ecs/render_layers_compute_depth_range.d.ts +35 -0
- package/src/engine/graphics/ecs/render_layers_compute_depth_range.d.ts.map +1 -0
- package/src/engine/graphics/ecs/render_layers_compute_depth_range.js +64 -0
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -5
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +2 -5
- package/src/engine/graphics/ecs/water/WaterSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/WaterSystem.js +2 -5
- package/src/engine/graphics/geometry/buffered/query/bvh32_geometry_nearest.d.ts.map +1 -1
- package/src/engine/graphics/geometry/buffered/query/bvh32_geometry_nearest.js +22 -94
- package/src/engine/graphics/geometry/buffered/query/bvh32_geometry_overlap_clipping_volume.d.ts.map +1 -1
- package/src/engine/graphics/geometry/buffered/query/bvh32_geometry_overlap_clipping_volume.js +17 -57
- package/src/engine/graphics/geometry/buffered/query/bvh32_geometry_raycast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/buffered/query/bvh32_geometry_raycast.js +17 -57
- package/src/engine/graphics/geometry/buffered/query/nearest_point_on_triangle_record.d.ts +30 -0
- package/src/engine/graphics/geometry/buffered/query/nearest_point_on_triangle_record.d.ts.map +1 -0
- package/src/engine/graphics/geometry/buffered/query/nearest_point_on_triangle_record.js +87 -0
- package/src/engine/graphics/geometry/buffered/query/query_bvh_geometry_nearest.d.ts.map +1 -1
- package/src/engine/graphics/geometry/buffered/query/query_bvh_geometry_nearest.js +11 -55
- package/src/engine/graphics/geometry/buffered/read_triangle_vertices.d.ts +29 -0
- package/src/engine/graphics/geometry/buffered/read_triangle_vertices.d.ts.map +1 -0
- package/src/engine/graphics/geometry/buffered/read_triangle_vertices.js +102 -0
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderDepthV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderDepthV0.js +17 -122
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderLitV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderLitV0.js +15 -194
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderNormalsV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderNormalsV0.js +12 -108
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderPerPixelV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderPerPixelV0.js +14 -183
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderV0.js +12 -154
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderViewportDepthV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderViewportDepthV0.js +10 -94
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderWireframeV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderWireframeV0.js +8 -72
- package/src/engine/graphics/impostors/octahedral/shader/glsl/common.glsl +10 -0
- package/src/engine/graphics/impostors/voxel/bake/read_atlas_to_samples.d.ts.map +1 -1
- package/src/engine/graphics/impostors/voxel/bake/read_atlas_to_samples.js +79 -58
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.js +11 -22
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.js +7 -16
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderNormalsV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderNormalsV0.js +5 -9
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.js +4 -19
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderViewportDepthV0.d.ts.map +1 -1
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderViewportDepthV0.js +5 -9
- package/src/engine/graphics/material/SplatMaterial.d.ts.map +1 -1
- package/src/engine/graphics/material/SplatMaterial.js +2 -17
- package/src/engine/graphics/material/WaterMaterial.d.ts.map +1 -1
- package/src/engine/graphics/material/WaterMaterial.js +3 -18
- package/src/engine/graphics/material/manager/compilers/CoalesceTextures.d.ts +4 -4
- package/src/engine/graphics/material/manager/compilers/CoalesceTextures.d.ts.map +1 -1
- package/src/engine/graphics/material/manager/compilers/CoalesceTextures.js +25 -70
- package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts.map +1 -1
- package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.js +2 -5
- package/src/engine/graphics/particles/node-based/codegen/glsl/getTypeByteSize.d.ts +8 -11
- package/src/engine/graphics/particles/node-based/codegen/glsl/getTypeByteSize.d.ts.map +1 -1
- package/src/engine/graphics/particles/node-based/codegen/glsl/getTypeByteSize.js +20 -21
- package/src/engine/graphics/particles/node-based/nodes/particle_data_type_component_count.d.ts +39 -0
- package/src/engine/graphics/particles/node-based/nodes/particle_data_type_component_count.d.ts.map +1 -0
- package/src/engine/graphics/particles/node-based/nodes/particle_data_type_component_count.js +45 -0
- package/src/engine/graphics/particles/node-based/particle/ParticleAttributeSpecification.d.ts +6 -8
- package/src/engine/graphics/particles/node-based/particle/ParticleAttributeSpecification.d.ts.map +1 -1
- package/src/engine/graphics/particles/node-based/particle/ParticleAttributeSpecification.js +2 -13
- package/src/engine/graphics/particles/particular/engine/utils/distributeParticlesOnObject3D.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/engine/utils/distributeParticlesOnObject3D.js +16 -78
- package/src/engine/graphics/particles/particular/engine/utils/distribute_points_on_indexed_triangles.d.ts +45 -0
- package/src/engine/graphics/particles/particular/engine/utils/distribute_points_on_indexed_triangles.d.ts.map +1 -0
- package/src/engine/graphics/particles/particular/engine/utils/distribute_points_on_indexed_triangles.js +168 -0
- package/src/engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.js +11 -115
- package/src/engine/graphics/postprocess/threejs/shaders/CopyShader.d.ts +24 -16
- package/src/engine/graphics/postprocess/threejs/shaders/CopyShader.d.ts.map +1 -1
- package/src/engine/graphics/postprocess/threejs/shaders/CopyShader.js +15 -47
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts +4 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +4 -0
- package/src/engine/graphics/sh3/build_injected_shader.d.ts +51 -0
- package/src/engine/graphics/sh3/build_injected_shader.d.ts.map +1 -0
- package/src/engine/graphics/sh3/build_injected_shader.js +56 -0
- package/src/engine/graphics/sh3/gi/material/MaterialTransformer.d.ts.map +1 -1
- package/src/engine/graphics/sh3/gi/material/MaterialTransformer.js +10 -1
- package/src/engine/graphics/sh3/gi/material/chunk_lpv_get_irradiance_at.d.ts.map +1 -1
- package/src/engine/graphics/sh3/gi/material/chunk_lpv_get_irradiance_at.js +17 -0
- package/src/engine/graphics/sh3/gi/material/common.d.ts +1 -1
- package/src/engine/graphics/sh3/gi/material/common.js +6 -222
- package/src/engine/graphics/sh3/gi/material/space_fragment/build_fragment_shader.d.ts.map +1 -1
- package/src/engine/graphics/sh3/gi/material/space_fragment/build_fragment_shader.js +7 -11
- package/src/engine/graphics/sh3/gi/material/space_fragment/build_vertex_shader.d.ts.map +1 -1
- package/src/engine/graphics/sh3/gi/material/space_fragment/build_vertex_shader.js +27 -29
- package/src/engine/graphics/sh3/gi/material/space_vertex/build_fragment_shader.d.ts.map +1 -1
- package/src/engine/graphics/sh3/gi/material/space_vertex/build_fragment_shader.js +7 -11
- package/src/engine/graphics/sh3/gi/material/space_vertex/build_vertex_shader.d.ts.map +1 -1
- package/src/engine/graphics/sh3/gi/material/space_vertex/build_vertex_shader.js +7 -12
- package/src/engine/graphics/sh3/grid_dilate_average.d.ts +38 -0
- package/src/engine/graphics/sh3/grid_dilate_average.d.ts.map +1 -0
- package/src/engine/graphics/sh3/grid_dilate_average.js +175 -0
- package/src/engine/graphics/sh3/lightmap/lightmap_dilate.d.ts +4 -0
- package/src/engine/graphics/sh3/lightmap/lightmap_dilate.d.ts.map +1 -1
- package/src/engine/graphics/sh3/lightmap/lightmap_dilate.js +7 -70
- package/src/engine/graphics/sh3/lpg/light_probe_grid_dilate.d.ts +6 -0
- package/src/engine/graphics/sh3/lpg/light_probe_grid_dilate.d.ts.map +1 -1
- package/src/engine/graphics/sh3/lpg/light_probe_grid_dilate.js +14 -113
- package/src/engine/graphics/sh3/lpg/material/chunk_lpg_get_irradiance_at.d.ts.map +1 -1
- package/src/engine/graphics/sh3/lpg/material/chunk_lpg_get_irradiance_at.js +7 -0
- package/src/engine/graphics/sh3/lpg/material/space_fragment/build_fragment_shader.d.ts.map +1 -1
- package/src/engine/graphics/sh3/lpg/material/space_fragment/build_fragment_shader.js +5 -8
- package/src/engine/graphics/sh3/lpg/material/space_fragment/build_vertex_shader.d.ts.map +1 -1
- package/src/engine/graphics/sh3/lpg/material/space_fragment/build_vertex_shader.js +5 -8
- package/src/engine/graphics/sh3/lpg/material/space_vertex/build_fragment_shader.d.ts.map +1 -1
- package/src/engine/graphics/sh3/lpg/material/space_vertex/build_fragment_shader.js +5 -8
- package/src/engine/graphics/sh3/lpg/material/space_vertex/build_vertex_shader.d.ts.map +1 -1
- package/src/engine/graphics/sh3/lpg/material/space_vertex/build_vertex_shader.js +5 -8
- package/src/engine/graphics/sh3/lpv/depth/chunk_probe_depth_sampling.d.ts +38 -0
- package/src/engine/graphics/sh3/lpv/depth/chunk_probe_depth_sampling.d.ts.map +1 -0
- package/src/engine/graphics/sh3/lpv/depth/chunk_probe_depth_sampling.js +265 -0
- package/src/engine/graphics/sh3/lpv/depth/octahedral/shader/visualize.frag.glsl +11 -0
- package/src/engine/graphics/sh3/lpv/depth/octahedral/v2/VLPDepthMapVisualisation.d.ts.map +1 -1
- package/src/engine/graphics/sh3/lpv/depth/octahedral/v2/VLPDepthMapVisualisation.js +5 -1
- package/src/engine/graphics/sh3/lpv/depth/octahedral/v2/visualise.frag.glsl +189 -419
- package/src/engine/graphics/sh3/shader/visualize.frag.d.ts +1 -1
- package/src/engine/graphics/sh3/shader/visualize.frag.js +7 -0
- package/src/engine/graphics/shaders/chunks/glsl_octahedral_impostor.d.ts +102 -0
- package/src/engine/graphics/shaders/chunks/glsl_octahedral_impostor.d.ts.map +1 -0
- package/src/engine/graphics/shaders/chunks/glsl_octahedral_impostor.js +277 -0
- package/src/engine/graphics/shaders/chunks/glsl_packing.d.ts +29 -0
- package/src/engine/graphics/shaders/chunks/glsl_packing.d.ts.map +1 -0
- package/src/engine/graphics/shaders/chunks/glsl_packing.js +30 -0
- package/src/engine/graphics/shaders/chunks/glsl_voxel_impostor_splat.d.ts +40 -0
- package/src/engine/graphics/shaders/chunks/glsl_voxel_impostor_splat.d.ts.map +1 -0
- package/src/engine/graphics/shaders/chunks/glsl_voxel_impostor_splat.js +57 -0
- package/src/engine/graphics/shaders/lib/make_clouds_uniforms.d.ts +22 -0
- package/src/engine/graphics/shaders/lib/make_clouds_uniforms.d.ts.map +1 -0
- package/src/engine/graphics/shaders/lib/make_clouds_uniforms.js +39 -0
- package/src/engine/graphics/texture/attribute_spec_to_three_format.d.ts +6 -0
- package/src/engine/graphics/texture/attribute_spec_to_three_format.d.ts.map +1 -1
- package/src/engine/graphics/texture/attribute_spec_to_three_format.js +9 -9
- package/src/engine/graphics/texture/channel_count_to_format_prefix.d.ts +14 -0
- package/src/engine/graphics/texture/channel_count_to_format_prefix.d.ts.map +1 -0
- package/src/engine/graphics/texture/channel_count_to_format_prefix.js +26 -0
- package/src/engine/graphics/texture/computeThreeTextureInternalFormatFromDataType.d.ts.map +1 -1
- package/src/engine/graphics/texture/computeThreeTextureInternalFormatFromDataType.js +2 -20
- package/src/engine/graphics/texture/normalized_internal_format.d.ts.map +1 -1
- package/src/engine/graphics/texture/normalized_internal_format.js +2 -20
- package/src/engine/graphics/texture/sampler/convertTexture2Sampler2D.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/convertTexture2Sampler2D.js +9 -3
- package/src/engine/input/devices/gamepad/GamepadDeviceLayout.d.ts +54 -0
- package/src/engine/input/devices/gamepad/GamepadDeviceLayout.d.ts.map +1 -0
- package/src/engine/input/devices/gamepad/GamepadDeviceLayout.js +323 -0
- package/src/engine/input/devices/gamepad/GamepadHandle.d.ts +21 -2
- package/src/engine/input/devices/gamepad/GamepadHandle.d.ts.map +1 -1
- package/src/engine/input/devices/gamepad/GamepadHandle.js +39 -11
- package/src/engine/input/devices/gamepad/GamepadLayout.d.ts +128 -0
- package/src/engine/input/devices/gamepad/GamepadLayout.d.ts.map +1 -0
- package/src/engine/input/devices/gamepad/GamepadLayout.js +160 -0
- package/src/engine/input/devices/gamepad/GamepadStandardMapping.d.ts +10 -0
- package/src/engine/input/devices/gamepad/GamepadStandardMapping.d.ts.map +1 -1
- package/src/engine/input/devices/gamepad/GamepadStandardMapping.js +11 -0
- package/src/engine/input/devices/gamepad/GamepadStick.d.ts +5 -1
- package/src/engine/input/devices/gamepad/GamepadStick.d.ts.map +1 -1
- package/src/engine/input/devices/gamepad/GamepadStick.js +5 -1
- package/src/engine/input/devices/gamepad/gamepad_layout_database.d.ts +27 -0
- package/src/engine/input/devices/gamepad/gamepad_layout_database.d.ts.map +1 -0
- package/src/engine/input/devices/gamepad/gamepad_layout_database.js +144 -0
- package/src/engine/input/devices/gamepad/gamepad_read_hat_axis.d.ts +25 -0
- package/src/engine/input/devices/gamepad/gamepad_read_hat_axis.d.ts.map +1 -0
- package/src/engine/input/devices/gamepad/gamepad_read_hat_axis.js +61 -0
- package/src/engine/input/devices/gamepad/prototypeGamepadTester.d.ts +2 -0
- package/src/engine/input/devices/gamepad/prototypeGamepadTester.d.ts.map +1 -0
- package/src/engine/input/devices/gamepad/prototypeGamepadTester.js +2124 -0
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +3 -33
- package/src/engine/intelligence/behavior/util/RandomDelayBehavior.d.ts.map +1 -1
- package/src/engine/intelligence/behavior/util/RandomDelayBehavior.js +5 -0
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +18 -19
- package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.js +18 -13
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +2 -1
- package/src/engine/network/sim/ActionLog.d.ts +9 -6
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +43 -80
- package/src/engine/network/sim/SmoothingState.d.ts.map +1 -1
- package/src/engine/network/sim/SmoothingState.js +27 -28
- package/src/engine/network/state/FrameBufferRing.d.ts +84 -0
- package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -0
- package/src/engine/network/state/FrameBufferRing.js +168 -0
- package/src/engine/network/state/InputRing.d.ts +14 -12
- package/src/engine/network/state/InputRing.d.ts.map +1 -1
- package/src/engine/network/state/InputRing.js +37 -76
- package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -1
- package/src/engine/network/transport/LoopbackTransport.js +10 -31
- package/src/engine/network/transport/Transport.d.ts +42 -2
- package/src/engine/network/transport/Transport.d.ts.map +1 -1
- package/src/engine/network/transport/Transport.js +87 -3
- package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/NodeUDPTransport.js +7 -33
- package/src/engine/network/transport/adapters/SimulatedTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/SimulatedTransport.js +17 -28
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +6 -32
- package/src/engine/network/transport/adapters/WebSocketTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/WebSocketTransport.js +6 -32
- package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/WebTransportTransport.js +11 -22
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +18 -48
- 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/fluid/solver/v3_grid_advect_maccormack_scalar.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_scalar.js +7 -0
- package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_velocity.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_velocity.js +11 -18
- package/src/engine/physics/fluid/solver/v3_grid_advect_sl_velocity.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_advect_sl_velocity.js +7 -0
- package/src/engine/physics/fluid/solver/v3_grid_apply_advection_forward.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_apply_advection_forward.js +15 -57
- package/src/engine/physics/fluid/solver/v3_grid_apply_scalar_advection.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_apply_scalar_advection.js +7 -0
- package/src/engine/physics/fluid/solver/v3_grid_limit8_at_indices.d.ts +28 -0
- package/src/engine/physics/fluid/solver/v3_grid_limit8_at_indices.d.ts.map +1 -0
- package/src/engine/physics/fluid/solver/v3_grid_limit8_at_indices.js +38 -0
- package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.js +12 -14
- package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.js +9 -0
- package/src/engine/physics/mls-mpm/MLS_MPM.d.ts +13 -1
- package/src/engine/physics/mls-mpm/MLS_MPM.d.ts.map +1 -1
- package/src/engine/physics/mls-mpm/MLS_MPM.js +15 -4
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_triangle_contact.js +9 -0
- package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/capsule_contacts.js +10 -11
- package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/sphere_triangle_contact.js +7 -12
- package/src/engine/physics/solver/apply_impulse_to_body.d.ts +36 -0
- package/src/engine/physics/solver/apply_impulse_to_body.d.ts.map +1 -0
- package/src/engine/physics/solver/apply_impulse_to_body.js +86 -0
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +1 -53
- package/src/engine/plugin/EnginePlugin.d.ts +3 -0
- package/src/engine/plugin/EnginePlugin.d.ts.map +1 -1
- package/src/engine/plugin/EnginePlugin.js +3 -0
- package/src/engine/save/storage/IndexedDBStorage.d.ts +1 -0
- package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
- package/src/engine/save/storage/IndexedDBStorage.js +50 -50
- package/src/engine/sound/ecs/SoundControllerSystem.d.ts.map +1 -1
- package/src/engine/sound/ecs/SoundControllerSystem.js +2 -10
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +2 -1
- package/src/engine/sound/simulation/core/AcousticOccluderIndex.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/AcousticOccluderIndex.js +4 -6
- package/src/engine/sound/simulation/core/OcclusionSolver.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/OcclusionSolver.js +2 -9
- package/src/engine/sound/simulation/probe/ProbeVisibilityRecheck.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/ProbeVisibilityRecheck.js +9 -14
- package/src/engine/sound/simulation/probe/bakeReverbBands.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeReverbBands.js +2 -6
- package/src/engine/sound/simulation/probe/bakeVisibility.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeVisibility.js +4 -34
- package/src/engine/sound/simulation/probe/probe_densify_portals.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/probe_densify_portals.js +3 -6
- package/src/engine/sound/simulation/probe/probe_edge_key.d.ts +31 -0
- package/src/engine/sound/simulation/probe/probe_edge_key.d.ts.map +1 -0
- package/src/engine/sound/simulation/probe/probe_edge_key.js +38 -0
- package/src/engine/sound/simulation/probe/probe_sdf_grid.d.ts +10 -3
- package/src/engine/sound/simulation/probe/probe_sdf_grid.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/probe_sdf_grid.js +10 -22
- package/src/engine/sound/simulation/probe/probe_visibility_augment.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/probe_visibility_augment.js +12 -48
- package/src/engine/sound/simulation/probe/probe_visibility_bridge_components.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/probe_visibility_bridge_components.js +4 -3
- package/src/engine/sound/simulation/render/foaReverbImpulseResponse.d.ts +5 -4
- package/src/engine/sound/simulation/render/foaReverbImpulseResponse.d.ts.map +1 -1
- package/src/engine/sound/simulation/render/foaReverbImpulseResponse.js +23 -31
- package/src/engine/sound/simulation/render/reverbImpulseResponse.d.ts +1 -8
- package/src/engine/sound/simulation/render/reverbImpulseResponse.d.ts.map +1 -1
- package/src/engine/sound/simulation/render/reverbImpulseResponse.js +21 -31
- package/src/engine/sound/simulation/render/reverb_band_tails.d.ts +55 -0
- package/src/engine/sound/simulation/render/reverb_band_tails.d.ts.map +1 -0
- package/src/engine/sound/simulation/render/reverb_band_tails.js +93 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +6 -22
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.js +7 -3
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.js +7 -22
- package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.js +28 -0
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +1 -5
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.js +1 -13
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.js +2 -8
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +3 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +26 -14
- package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -1
- package/src/engine/sound/sopra/runtime/BusGraph.js +21 -33
- package/src/engine/sound/sopra/runtime/EventInstance.js +1 -1
- package/src/engine/ui/apply_pointer_tilt_3d.d.ts +25 -0
- package/src/engine/ui/apply_pointer_tilt_3d.d.ts.map +1 -0
- package/src/engine/ui/apply_pointer_tilt_3d.js +50 -0
- package/src/engine/ui/bindings/processors/DomElementProcessorTilt3D.d.ts.map +1 -1
- package/src/engine/ui/bindings/processors/DomElementProcessorTilt3D.js +9 -26
- package/src/generation/filtering/numeric/CellFilterCache.d.ts.map +1 -1
- package/src/generation/filtering/numeric/CellFilterCache.js +4 -21
- package/src/generation/filtering/numeric/complex/CellFilterSobel.d.ts.map +1 -1
- package/src/generation/filtering/numeric/complex/CellFilterSobel.js +11 -20
- package/src/generation/filtering/numeric/math/CellFilterPower.d.ts +1 -1
- package/src/generation/filtering/numeric/math/CellFilterPower.d.ts.map +1 -1
- package/src/generation/filtering/numeric/math/CellFilterPower.js +1 -5
- package/src/generation/filtering/numeric/process/computeCellFilterGradient.d.ts.map +1 -1
- package/src/generation/filtering/numeric/process/computeCellFilterGradient.js +13 -15
- package/src/generation/filtering/numeric/process/computeCellFilterSobel.d.ts +20 -0
- package/src/generation/filtering/numeric/process/computeCellFilterSobel.d.ts.map +1 -0
- package/src/generation/filtering/numeric/process/computeCellFilterSobel.js +36 -0
- package/src/generation/filtering/numeric/util/CellFilterDisplaced.d.ts.map +1 -1
- package/src/generation/filtering/numeric/util/CellFilterDisplaced.js +11 -6
- package/src/generation/filtering/numeric/util/populateSampler2DFromCellFilter.d.ts.map +1 -1
- package/src/generation/filtering/numeric/util/populateSampler2DFromCellFilter.js +18 -36
- package/src/generation/filtering/numeric/util/write_cell_filter_to_sampler2d.d.ts +21 -0
- package/src/generation/filtering/numeric/util/write_cell_filter_to_sampler2d.d.ts.map +1 -0
- package/src/generation/filtering/numeric/util/write_cell_filter_to_sampler2d.js +47 -0
- package/src/generation/filtering/util/cell_filter_weights_pick.d.ts +25 -0
- package/src/generation/filtering/util/cell_filter_weights_pick.d.ts.map +1 -0
- package/src/generation/filtering/util/cell_filter_weights_pick.js +54 -0
- package/src/generation/grid/generation/discrete/GridTaskConnectRooms.d.ts.map +1 -1
- package/src/generation/grid/generation/discrete/GridTaskConnectRooms.js +14 -1
- package/src/generation/markers/GridActionRuleSet.d.ts.map +1 -1
- package/src/generation/markers/GridActionRuleSet.js +10 -7
- package/src/generation/markers/GridCellActionPlaceMarker.d.ts.map +1 -1
- package/src/generation/markers/GridCellActionPlaceMarker.js +10 -12
- package/src/generation/markers/actions/probability/MarkerNodeActionSelectWeighted.d.ts.map +1 -1
- package/src/generation/markers/actions/probability/MarkerNodeActionSelectWeighted.js +7 -42
- package/src/generation/markers/transform/MarkerNodeTransformerOffsetPosition.d.ts.map +1 -1
- package/src/generation/markers/transform/MarkerNodeTransformerOffsetPosition.js +11 -6
- package/src/generation/placement/GridCellPlacementRule.d.ts.map +1 -1
- package/src/generation/placement/GridCellPlacementRule.js +10 -7
- package/src/generation/placement/action/GridCellActionPlaceTags.d.ts.map +1 -1
- package/src/generation/placement/action/GridCellActionPlaceTags.js +10 -4
- package/src/generation/placement/action/random/weighted/CellActionSelectWeightedRandom.d.ts.map +1 -1
- package/src/generation/placement/action/random/weighted/CellActionSelectWeightedRandom.js +2 -38
- package/src/generation/placement/action/util/GridCellDisplacedAction.d.ts.map +1 -1
- package/src/generation/placement/action/util/GridCellDisplacedAction.js +11 -6
- package/src/generation/rules/cell/CellMatcherGridPattern.d.ts.map +1 -1
- package/src/generation/rules/cell/CellMatcherGridPattern.js +11 -4
- package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
- package/src/generation/theme/ThemeEngine.js +7 -12
- package/src/view/elements/Group.d.ts +24 -3
- package/src/view/elements/Group.d.ts.map +1 -1
- package/src/view/elements/Group.js +56 -34
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Domain D — engine/graphics feature subsystems (ecs, particles, trail, impostors, sh3, shadows)
|
|
2
|
+
|
|
3
|
+
Source root for all paths: `H:/git/moh/app/src/mir-engine/meep/src/`
|
|
4
|
+
|
|
5
|
+
Every finding below was verified by opening the cited files. Line numbers are from the working tree
|
|
6
|
+
at the time of the audit.
|
|
7
|
+
|
|
8
|
+
## Findings
|
|
9
|
+
|
|
10
|
+
| # | Severity | Concept | Sites | Evidence | Verdict | Proposed common path | Effort/Risk |
|
|
11
|
+
|---|---|---|---|---|---|---|---|
|
|
12
|
+
| 1 | CRITICAL | **Octahedral impostor GLSL library copy-pasted 9× inside the domain, plus 3 more name-colliding variants.** `VecToSphereOct` / `VecToHemiSphereOct` / `VectorToGrid` / `OctaSphereDec` / `OctaHemiSphereDec` / `GridToVector` / `FrameToRay` / `ComputeFrameXform` / `BilinearWeights` / `apply_frame_xform` — ~90 lines of GLSL | `engine/graphics/impostors/octahedral/shader/ImpostorShaderV0.js:71`, `ImpostorShaderDepthV0.js:59`, `ImpostorShaderNormalsV0.js:52`, `ImpostorShaderViewportDepthV0.js:46`, `ImpostorShaderWireframeV0.js:31`, `ImpostorShaderPerPixelV0.js:39` **and** `:178` (both stages), `ImpostorShaderLitV0.js:65` **and** `:328` (both stages). Incompatible same-named variants: `shader/glsl/common.glsl:25,57`, `shader/glsl/v1/common.glsl:1,45`, `shader/glsl/v1/vertex.glsl:42,86` | 9 byte-identical copies (verified with `awk` extraction — all identical). Whole-file line overlap against `ImpostorShaderV0.js`: Depth 70%, PerPixel 85%, Wireframe 86%, ViewportDepth 65%, Normals 61%. **Drift already present**: all 9 GPU copies use `sign(v)` where the CPU reference `core/geom/3d/octahedra/octahedral_direction_to_uv.js:25` deliberately uses `sign_not_zero()` — `sign(0.0)==0.0` shrinks the L1 norm and mis-folds a direction with an exactly-zero component. The bake side (`impostors/octahedral/grid/OctahedralUvEncoder.js:7`) and the sample side must agree; they are two independent transcriptions of one invariant. `glsl/common.glsl` declares `VecToSphereOct`/`GridToVector`/`VectorToGrid` with *different* semantics (y-fold via `_ImposterFullSphere`, HLSL intrinsics `frac`/`saturate`/`tex2Dgrad`) | UNJUSTIFIED | One `engine/graphics/impostors/octahedral/shader/chunk_octahedral.js` exporting a GLSL string (or a `.glsl` file — precedent exists, `ImpostorShaderV1.js:10` already imports `./glsl/v1/vertex.glsl` through the raw loader), interpolated into every shader. Delete `glsl/common.glsl`'s colliding declarations or rename them | Medium effort / Low risk — string extraction, shaders compile-checked by the existing prototype harness |
|
|
13
|
+
| 2 | CRITICAL | **The production GI probe-depth GLSL library is copy-pasted into two debug visualisers.** `sign_not_zero` (3 overloads), `quadBlendWieghts`, `VecToSphereOct`, `SampleBlended`, `lpv_probe_getDepth{,Bilinear,Triangular}`, `lpv_bilinear_lerp` (2 overloads), `lpv_sample_bilinear`, `wrapOctahedralTexelCoordinates`, `lpv_probe_getPosition` | `engine/graphics/sh3/gi/material/common.js:76-298` (production) ⟷ `engine/graphics/sh3/lpv/depth/octahedral/v2/visualise.frag.glsl:26-408` ⟷ `engine/graphics/sh3/lpv/depth/octahedral/shader/visualize.frag.glsl:1-97` | Measured line-level overlap: **220 of 320** non-empty lines of `v2/visualise.frag.glsl` (68.8%) appear verbatim in `gi/material/common.js`; 51 of 97 (52.6%) for `shader/visualize.frag.glsl`. Smoking gun: the misspelling `quadBlendWieghts` is present in both (`common.js:95`, `v2/visualise.frag.glsl:45`). The whole point of the visualiser is to show what the production sampler sees — a forked copy can silently show something else | UNJUSTIFIED | Extract `engine/graphics/sh3/lpv/depth/chunk_probe_depth.js` (GLSL string) consumed by `gi/material/common.js` and by both visualiser shaders. Note `VecToSphereOct` here folds on `v.y<0` returning `.xz` while `lpv/depth/.../shader/visualize.frag.glsl:29` folds on `direction.z<0` returning `.xy` — reconcile before extracting | Medium effort / Medium risk — the two visualisers may currently rely on their divergent fold |
|
|
14
|
+
| 3 | HIGH | **`AbstractMaterialTransformer.transform()` marker-guard / clone / compose protocol copied 3×, already diverged** | `engine/graphics/render/forward_plus/plugin/MaterialTransformer.js:91` `transform`; `engine/graphics/sh3/gi/material/MaterialTransformer.js:294` `transform`; `engine/graphics/sh3/lpg/material/LightProbeGridMaterialTransformer.js:207` `transform` | The 11-line "already transformed / different instance → throw" guard is byte-identical in all three. The clone step is not: FP does `result = source.clone()` (`:107`), gi does **`result.clone();`** (`MaterialTransformer.js:311`) — a discarded expression statement, so gi mutates the caller's material in place; lpg dropped the clone entirely (`:219-223`). `AbstractMaterialTransformer` (`engine/graphics/material/manager/AbstractMaterialTransformer.js:1`) is an empty 10-line stub that owns none of this. `#onBeforeCompile` is also identical between gi (`:133`) and lpg (`:77`) — same `switch(ProbeResolutionStage)` dispatch, 14 lines | UNJUSTIFIED | Move the guard + clone + `composeCompile` wiring into `AbstractMaterialTransformer` as a concrete `transform(source)` calling an abstract `_install(material)`; subclasses supply only the marker symbol and the compile hook. Typed inheritance, no registry | Low effort / Medium risk — fixing gi's dropped clone changes behaviour (correctly) |
|
|
15
|
+
| 4 | HIGH | **`Trail3DSystem` is a whole-file fork of `Trail2DSystem`, and the fork's bug fix never went back** | `engine/graphics/ecs/trail2d/Trail2DSystem.js` (274 lines) ⟷ `engine/graphics/ecs/trail3d/Trail3DSystem.js` (280 lines). Method pairs: `constructor` `:66`/`:90`, `startup` `:96`/`:110`, `__build_trail`/`#build_trail` `:129`/`:141`, `link` `:191`/`:200`, `unlink` `:210`/`:221`, `updateTrailEntity` `:224`/`:233`, `update` `:260`/`:266` | Every method is identical modulo `ribbon`↔`tube`, `Point`↔`Knot`, plugin class and layer name. **Divergence with consequences**: `Trail3DSystem.js:47` extracts `compute_head_position(result, trail, transform)` = `offset.applyMatrix4(transform.matrix)` and its 15-line comment states build-time seeding and the per-frame head update *must* agree or "the head's first move draws a segment bridging the two — a one-frame streak". `Trail2DSystem.js:157-159` still seeds with `trail_offset.x + transform_position.x` (translation only) while `Trail2DSystem.js:235-236` updates the head with `applyMatrix4` — under any rotation or scale the two disagree, which is exactly the documented defect | UNJUSTIFIED | The two ribbon/tube backends genuinely differ, but the *system* does not. Extract the shared body into `TrailSystemBase` (typed abstract class holding bvh/renderLayer/plugin-ref lifecycle + `updateTrailEntity`) with abstract `#seed_geometry` / `#obtain_plugin`. At minimum promote `compute_head_position` into `engine/graphics/trail/` and call it from both build paths | Medium effort / Medium risk — Trail2D seeding change is a behaviour fix, covered by `Trail2DSystem.spec.js` |
|
|
16
|
+
| 5 | HIGH | **Trail spawn/aging state machine duplicated** — reseed-on-clear, `refitTimeDelta = maxAge/count`, head-moved detection, distance accumulation, `TrailSpawnMode.Distance ? distance>=spawnDistance : time>=refitTimeDelta`, rotate, head age/alpha/thickness write | `engine/graphics/ecs/trail2d/Trail2D.js:298` `updateHead` ⟷ `engine/graphics/ecs/trail3d/Trail3D.js:417` `updateHead`; also `Trail2D.js:210` `clear` ⟷ `Trail3D.js:275` `clear` | MinHash sim 0.766 on 309/322 tokens; reading them confirms the *policy* is line-for-line the same (~45 lines), differing only by accessor names and 3D's extra `#updateHeadFrame` call. The alpha fade law `(1 - clamp01(head_age/maxAge)) * this.color.w` is restated in both and must match the simulators (`TubeXFixedPhysicsSimulator.js:28`, `RibbonXFixedPhysicsSimulator.js:27`) — 4 copies of one law | UNJUSTIFIED | Extract `trail_update_head(trail, buffer_adapter, x, y, z, head_age)` in `engine/graphics/trail/`, where the adapter is a small typed interface (`getCount/getHeadIndex/getPosition/setPosition/rotate/setAge/setAlpha/setThickness`). Simplest alternative: rename `TubeX`'s `*Knot*` methods to `*Point*` so one function serves both | Medium effort / Medium risk — per-frame code, but not per-particle; specs exist for both components |
|
|
17
|
+
| 6 | HIGH | **Voxel-impostor splat expansion GLSL copied 5×, carrying a stated cross-shader invariant** | `engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.js:45-61`, `VoxelImpostorShaderNormalsV0.js:24-31`, `VoxelImpostorShaderDepthV0.js:31-41`, `VoxelImpostorShaderViewportDepthV0.js:22-30`, `VoxelImpostorShaderLitV0.js:57-68` | The block (`model_scale = length(modelMatrix[0].xyz)` → `radius_OS` → `centre_OS = iPosition - iNormal * radius_OS` → view-space corner offset) is identical in all five. `VoxelImpostorShaderDepthV0.js:35-37` states: *"Inward-shift must match the colour pass exactly so the cast silhouette agrees with the visible one"* — the invariant is documented but not enforced, because it lives in five separate string literals | UNJUSTIFIED | `engine/graphics/impostors/voxel/shader/chunk_voxel_splat_vertex.js` exporting the shared vertex body; the five materials interpolate it and add only their own varyings | Low effort / Low risk |
|
|
18
|
+
| 7 | HIGH | **Spherical-harmonic cosine-lobe irradiance evaluator implemented 4× and the copies disagree on sign convention** | `core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.js:36-48` (JS); `engine/graphics/sh3/shader/visualize.frag.js:11` `shGetIrradianceAt` (GLSL); `engine/graphics/sh3/gi/material/chunk_lpv_get_irradiance_at.js:5` `lpv_get_irradiance_at` (GLSL); `engine/graphics/sh3/lpg/material/chunk_lpg_get_irradiance_at.js:9` `lpg_get_irradiance_at` (GLSL, first two bands only) | Same magic constants (`0.8862269254527579`, `1.0233267079464885`, `0.8580855308097834`, `0.4290427654048917`, `0.7431238683011272`) in all four. Signs differ: the core JS and `visualize.frag.js` negate coefficients 1, 3, 5, 7 (three.js convention); the two chunk files use `+=` throughout. `lpg/material/chunk_lpg_get_irradiance_at.js:3` even documents itself as *"first two bands of chunk_lpv_get_irradiance_at"* — an acknowledged copy. (The sign divergence is already logged in `src/REVIEW_2026_08_06/adv-gfx.md:359`; cited here only as proof the duplication produced a real defect) | UNJUSTIFIED | One `engine/graphics/sh3/chunk_sh_irradiance.js` GLSL module exporting an L2 evaluator plus a truncated L1 evaluator over `vec3[4]`, next to the single JS `sh3_sample_irradiance_by_direction`. Pin one sign convention and state it in a comment at both | Low effort / High risk — changing signs changes rendered output; needs a bake-vs-shader A/B first |
|
|
19
|
+
| 8 | MEDIUM | **Three highlight systems are the same `System` + `Supplier` scaffold with a 15-line hole** | `engine/graphics/ecs/highlight/system/MeshHighlightSystem.js` (162 ln), `RenderableHighlightSystem.js` (164 ln), `ShadedGeometryHighlightSystem.js` (175 ln). Duplicated members: `class Supplier` `:9`/`:9`/`:8`, `collect` `:33`/`:38`/`:44`, `shutdown` `:121`/`:125`/`:137`, `startup` `:137`/`:143`/`:154`, and the whole `constructor` | `diff -u` between Renderable and ShadedGeometry shows the *only* semantic differences are the traversal component pair and how the three.js object is obtained; between Mesh and Renderable, only the visibility-flag check and the object accessor. ~130 of ~165 lines are common to all three | UNJUSTIFIED | Abstract `HighlightSystemBase extends System` owning the `OutlineRenderPlugin` reference lifecycle and the `Supplier` (which delegates to an abstract `visit(highlight, component, entity)`); three thin typed subclasses. Plain inheritance — no registry, no strings | Low effort / Low risk (`ShadedGeometryHighlightSystem.spec.js` exists) |
|
|
20
|
+
| 9 | MEDIUM | **Neighbour-average flood-fill ("dilate") written twice — 2D lightmap and 3D probe grid** | `engine/graphics/sh3/lightmap/lightmap_dilate.js:18` `lightmap_dilate` (4-connected, `channels` values/texel) ⟷ `engine/graphics/sh3/lpg/light_probe_grid_dilate.js:29` `light_probe_grid_dilate` (6-connected, 12 values/probe) | Identical algorithm: skip covered cells; accumulate valid neighbours into a `Float64Array` accumulator; if `n===0` continue; write `acc[c]/n`; mark newly covered; repeat. Even the guard shape (`x > 0 && coverage[index-1] !== 0`) matches per-axis. Only the pass-snapshot mechanism differs (`newly[]` array vs `__source_validity` copy) and the termination (iteration cap vs run-to-convergence). Invisible to the mechanical pass — different identifiers and one extra loop level put it under the MinHash threshold | UNJUSTIFIED | One `core/collection/grid/grid_dilate_average(data, coverage, size_x, size_y, size_z, channels, iterations)`; the lightmap call passes `size_z = 1`. Note `lightmap_dilate.js:24` allocates `newly` per iteration — folding into the LPG's snapshot form removes that too | Medium effort / Low risk — both have specs (`lightmap_dilate.spec.js`, `light_probe_grid_dilate.spec.js`) |
|
|
21
|
+
| 10 | MEDIUM | **Area-weighted triangle surface sampling copy-pasted instead of reusing the function next door** | `engine/graphics/particles/particular/engine/utils/distributeParticlesOnObject3D.js:169-233` ⟷ `engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.js:112-187` `distributePointsOnMesh` | ~65 lines verbatim: same `poly:` label, same `roundFair(placementCount, random)`, same index/vertex fetch, same barycentric picking including the identical source-URL comment (`https://math.stackexchange.com/questions/538458/...`) and the same `r1 = random() * (1 - r0)` sampling. `distributePointsOnMesh` is already the generic, callback-driven form and is exported — `distributeParticlesOnObject3D` just doesn't call it. There is no other area-weighted triangle sampler in the codebase (grep for the URL and for `random point on the triangle` returns exactly these two files) | UNJUSTIFIED | Split `distributePointsOnMesh` into `distribute_points_on_indexed_triangles(count, points, indices, polygon_count, transform, seed, callback)` plus the two thin `Mesh` / `Object3D` front-ends. `distributeParticlesOnObject3D`'s `visitObject` (`:20`) already flattens the hierarchy into `buffer_points`/`buffer_indices` — it only needs to hand those to the shared loop | Low effort / Low risk |
|
|
22
|
+
| 11 | MEDIUM | **Two-record swap across an array of `BufferAttribute`s written twice** | `engine/graphics/particles/particular/engine/emitter/ParticlePool.js:202` `swap` ⟷ `engine/graphics/particles/particular/group/ParticleGroup.js:550` `swapAttributeValues` | MinHash sim 0.953. 25 lines, identical down to `const offsetA = indexA * itemSize`; only the temp variable name (`temp` vs `elementA`) and a trailing `attribute.needsUpdate = true` differ. Both operate on the same shape (array of three.js `BufferAttribute`) | UNJUSTIFIED | `engine/graphics/geometry/buffer_attributes_swap_records(attributes, index_a, index_b)` in `engine/graphics/geometry/`; both call it, `ParticleGroup` sets `needsUpdate` itself afterwards | Low effort / Low risk |
|
|
23
|
+
| 12 | MEDIUM | **Nearest-point-on-triangle leaf handler duplicated across the two BVH nearest queries** | `engine/graphics/geometry/buffered/query/query_bvh_geometry_nearest.js:164-203` ⟷ `engine/graphics/geometry/buffered/query/bvh32_geometry_nearest.js:203-242` | The traversal halves legitimately differ (pointer-based `BVH` vs implicit-index `BinaryUint32BVH`), but the leaf halves are byte-identical for 40 lines: `tri3_closest_point_barycentric` → `w = 1-u-v` → `contact = a*u + b*v + c*w` → `v3_distance_sqr` → early-out → `result.position.set` → `tri3_compute_normal` → `result.index`. The barycentric-reconstruction convention (which of u/v/w pairs with which vertex) is the subtle part and it is stated twice | UNJUSTIFIED | Extract `tri3_nearest_accept(result, x, y, z, ax..cz, triangle_index, best_distance_sqr) -> number` (returns the new best, or the old one). Both queries call it from their leaf branch. Function-call overhead in the leaf is negligible next to the barycentric solve | Low effort / Low risk (both have specs) |
|
|
24
|
+
| 13 | MEDIUM | **`load-from-storage-else-build-and-cache` + scene-bounds traversal duplicated between LPG and LPV** | `engine/graphics/sh3/lpg/light_probe_grid_obtain_storage_cached.js:18` ⟷ `engine/graphics/sh3/lpv/lpv_obtain_storage_cached_volume.js:18` | MinHash sim 0.953, 198/200 tokens. Lines 44-65 vs 46-67 are byte-identical: build an `AABB3`, `setNegativelyInfiniteBounds()`, `ecd.traverseEntities([ShadedGeometry, Transform], ...)`, allocate a fresh `AABB3` **inside the visitor** (a per-entity allocation, duplicated), `mesh_bounds.union(box)`. The un-awaited `engine.storage.promiseStoreBinary(key, buffer.data)` with no error path is also duplicated (`:81` / `:83`) | UNJUSTIFIED | Promote `scene_compute_shaded_geometry_bounds(out_aabb3, ecd)` into `engine/graphics/ecs/mesh-v2/` next to the existing `sg_hierarchy_compute_bounding_box_via_parent_entity.js:28`, using a module-static scratch box. The load/build/store protocol can stay per-subsystem (the adapter types are genuinely different) | Low effort / Low risk |
|
|
25
|
+
| 14 | MEDIUM | **Bake-task-with-progress-overlay scaffold duplicated** | `engine/graphics/sh3/lpg/build_light_probe_grid_for_scene.js:18` `light_probe_grid_bake_via_task` ⟷ `engine/graphics/sh3/lpv/build_probes_for_scene.js:20` `lpv_volume_bake_via_task` | MinHash sim 0.922. ~25 lines identical: `makeSimpleTaskProgressView({task, localization, size: new Vector2(512,64)})`, a throwaway `Entity` with `GUIElement.fromView` + `ViewportPosition.fromJSON({position:(0,1), anchor:(0,1), offset:(16,-16)})`, `task.promise().finally(() => progress_entity.destroy())`, `engine.executor.runGroup(task)`, `await task.promise()`. Only the baker class differs. `makeSimpleTaskProgressView` has exactly two non-prototype callers — both of these | PROMOTION | `engine/run_task_with_progress_overlay(task, engine, ecd)` beside `engine/makeSimpleTaskProgressView.js`. Both bakers call it | Low effort / Low risk |
|
|
26
|
+
| 15 | MEDIUM | **Camera depth-plane construction + render-layer depth-range accumulation duplicated** | `engine/graphics/ecs/camera/auto_set_camera_clipping_planes.js:60-84` ⟷ `engine/graphics/ecs/light/shadow/extend_shadow_camera_near_for_casters.js:69-90` | Two verbatim blocks. (a) The three.js matrix convention `nx=-m[8], ny=-m[9], nz=-m[10]`, `plane_constant = -(nx*m[12]+ny*m[13]+nz*m[14])` — a vendor-layout invariant restated twice. (b) `scratch_range.near = +Infinity; far = -Infinity;` then `layers.traverse(layer => { if(!layer.state.visible) return; const compute = layer.compute_depth_range; if (compute === null) return; compute(...) })` — identical including the null guard and the sentinel initialisation | PROMOTION | `engine/graphics/render/layers/render_layers_compute_depth_range(out_range, layers, planes, matrix_world_elements)` doing both halves. Both callers shrink to a call plus their own clamping policy | Low effort / Low risk (`auto_set_camera_clipping_planes.spec.js` exists) |
|
|
27
|
+
| 16 | MEDIUM | **`compute_depth_range` BVH closure written 7× verbatim** | `engine/graphics/ecs/trail2d/Trail2DSystem.js:110`, `engine/graphics/ecs/trail3d/Trail3DSystem.js:124`, `engine/graphics/ecs/mesh/MeshSystem.js:162`, `engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js:269`, `engine/graphics/ecs/water/WaterSystem.js:110`, `engine/graphics/particles/ecs/ParticleEmitterSystem.js:104`, `engine/ecs/renderable/RenderSystem.js:74` | All seven are `const bvh = <this bvh>; layer.compute_depth_range = (result, frustum, nx, ny, nz, c) => { bvh_query_depth_range_in_frustum(result, bvh, frustum, nx, ny, nz, c); };`. The sibling helper `make_bvh_visibility_builder` already exists in `engine/graphics/render/` and is used by six of these — the depth-range half was never given the same treatment | PROMOTION | `engine/graphics/render/make_bvh_depth_range_computer(bvh)` next to `make_bvh_visibility_builder.js`; all seven become one-liners | Low effort / Low risk |
|
|
28
|
+
| 17 | MEDIUM | **`DataTexture3D` configuration boilerplate restated 4×** | `engine/graphics/sh3/gi/material/makeLookupTexture3D.js:8`, `engine/graphics/sh3/lpg/light_probe_grid_textures.js:39` `make_light_probe_grid_luma_texture` and `:78` `make_light_probe_grid_chroma_texture`, `engine/graphics/render/forward_plus/LightManager.js:169` | MinHash sim 0.938 on the first three. Each restates the same 12-property incantation — `flipY=false`, `generateMipmaps=false`, `mag/minFilter`, `wrapS/T/R = ClampToEdgeWrapping`, `type`, `format`, `internalFormat`, `unpackAlignment = 4`, `needsUpdate = true` — differing only in filter mode and the type/format triple. House rule "confine vendor imports to boundaries": this is repeated three.js translation logic | UNJUSTIFIED | `engine/graphics/texture/make_data_texture_3d({data, width, height, depth, type, format, internal_format, filter, name})` — explicit typed parameters, no registry | Low effort / Low risk (`light_probe_grid_textures.spec.js` exists) |
|
|
29
|
+
| 18 | MEDIUM | **Axis tick-label drawing duplicated, and the fix landed only in the copy** | `engine/graphics/canvas/canvas2d_draw_linear_scale.js:18` ⟷ `engine/animation/curve/editor/canvas2dDrawWorldAxisLabels.js:17` | MinHash sim 0.75. Lines 26-42 are byte-identical (spacing guard, delta/`v2_length`/zero guard, direction normalisation). `canvas2d_draw_linear_scale.js:44-46` uses `Math.floor` with a comment explaining that `ceil` *"would place a final tick past position_1 with an extrapolated value"*; `canvas2dDrawWorldAxisLabels.js:44` still uses `Math.ceil` — the documented defect is live in the copy | UNJUSTIFIED | `engine/graphics/canvas/canvas2d_draw_axis_ticks({ctx, position_x0..y1, spacing, label})` where `label(index, fraction)` returns the string. Both callers keep their own value/format policy; uniform control flow, no sentinel parameter | Low effort / Low risk |
|
|
30
|
+
| 19 | MEDIUM | **Full-screen-quad `ShaderMaterial` option block restated 4× in one directory, with a typo divergence** | `engine/graphics/ecs/highlight/renderer/makeBlurShader_9.js:17-24`, `makeDilationShader.js:16-23`, `HighlightDecodeShader.js:57-63`, `OutlineRenderer.js:146-153` | The same 8 properties (`blending: NoBlending, lights: false, fog: false, depthTest: false, depthWrite: false, transparent: true, vertexColors: false, vertexShader: ScreenSpaceQuadShader.vertexShader()`). `makeBlurShader_9.js:18` writes **`light: false`** — three.js has no `light` property, so `Material.setValues` warns and the intended flag is never set. Classic copy-paste transcription error | UNJUSTIFIED | `engine/graphics/shaders/make_screen_space_quad_material({uniforms, fragmentShader})` beside `ScreenSpaceQuadShader.js`; the four call sites keep only their uniforms + fragment source | Low effort / Low risk |
|
|
31
|
+
| 20 | MEDIUM | **Octahedral depth-probe ray kernel duplicated between the two bakers** | `engine/graphics/sh3/lpv/depth/octahedral/bake_octahedral_depth_map.js:95-133` ⟷ `bake_octahedral_depth_map_fibonacci.js:98-141` | ~40 lines byte-identical: `scene.trace` → `distance < 0 ? max_depth` → read `ray_hit[0..5]` → sink the contact along the hit normal by `NORMAL_BIAS` → `v3_distance` from the ray origin → `weight = pow(max2(0, dot(ray_dir, probe_dir)), DEPTH_SHARPNESS)`. The two sampling strategies (per-texel Hammersley jitter vs global Fibonacci splatting) genuinely differ; the shading kernel does not, and if the bias/weight law drifts the two bakers produce different depth for the same scene | UNJUSTIFIED | `trace_biased_depth_sample(scratch_ray, scene, max_depth, ray_direction, probe_direction) -> {distance, weight}` (or two out-params) in the same directory; both bakers call it inside their own sampling loop | Low effort / Low risk |
|
|
32
|
+
| 21 | MEDIUM | **Per-entity event-rule subscription bookkeeping duplicated** | `engine/graphics/ecs/animation/AnimationControllerSystem.js:24` `registerRule` / `:64` `unlink` ⟷ `engine/sound/ecs/SoundControllerSystem.js:62` `registerRule` / `:103` `unlink` | `unlink` is byte-identical (9 lines). `registerRule`'s tail — `if (typeof r.stopEvent === "string") { addEntityEventListener(...); callbacks.push({name, handler}) }` then the same for `startEvent`, then `component.rules.forEach(registerRule)` — is identical modulo the parameter name. Only the two handler bodies (create an `AnimationClip` vs a `SoundTrack`) are subsystem-specific | UNJUSTIFIED | `engine/ecs/EntityEventRuleBinder` (typed class): `bind(dataset, entity, rules, {on_start, on_stop})` and `release(entity)`. Both systems keep only their handler bodies | Low effort / Low risk |
|
|
33
|
+
| 22 | MEDIUM | **The impostor atlas uniform contract restated in 8 material classes** | `engine/graphics/impostors/octahedral/shader/ImpostorShaderV0.js:410`, `ImpostorShaderDepthV0.js:369`, `ImpostorShaderNormalsV0.js:356`, `ImpostorShaderViewportDepthV0.js:332`, `ImpostorShaderPerPixelV0.js:368`, `ImpostorShaderWireframeV0.js:162`, `ImpostorShaderStandard.js:256`, `ImpostorShaderV1.js:14` — all `constructor` | Token-identical constructor bodies at 117 tokens ×4 and 99 tokens ×3. Same `tBase / tGeometry / tMaterial / uFrames / uRadius / uOffset / uIsFullSphere / uDepthScale` set with the same JSDoc text, so an atlas format change (e.g. a fourth channel plane) must be made in eight places. This is not per-class `hash()`/`equals()` shape — it is one data contract restated | UNJUSTIFIED | `make_impostor_atlas_uniforms()` returning a fresh uniform object; each constructor spreads it and adds its own extras (`uParallaxStrength` etc.). Still typed and explicit — no string keys, no registry | Low effort / Low risk |
|
|
34
|
+
| 23 | MEDIUM | **Diffuse-texture async binding duplicated between the two trail plugins, already diverged** | `engine/graphics/trail/tube/TubeXPlugin.js:48` `#build_material` (lines 57-77) ⟷ `engine/graphics/trail/x/RibbonXPlugin.js:96` `__build_material` (lines 110-130) | Identical: `if (typeof spec.diffuse === "string")` → `am.get({path, type: GameAssetType.Texture, callback: asset => { texture = asset.create(); texture.generateMipmaps = true; material.uniforms.uDiffuse.value = texture; }, failure: console.warn})` → `material.defines.USE_TEXTURE = true`. Tube adds `material.needsUpdate = true` (`:76`); Ribbon does not. Tube also routes through `getMaterialManager().obtain()` (`:84`, with a comment explaining Forward+ requires it); Ribbon returns the raw material (`:133`) | UNJUSTIFIED | `engine/graphics/material/bind_diffuse_texture_async(asset_manager, material, path)` handling the define + `needsUpdate`. The material-manager routing difference should be reconciled separately | Low effort / Low risk |
|
|
35
|
+
| 24 | MEDIUM | **The four gi/lpg shader-builder glue functions and two transform-shader files are interchangeable** | `engine/graphics/sh3/gi/material/space_fragment/build_fragment_shader.js:40` and `build_vertex_shader.js:12`; `space_vertex/build_fragment_shader.js:19` and `build_vertex_shader.js:51`; the four identically-named `engine/graphics/sh3/lpg/material/...` counterparts; `gi/material/space_fragment/space_fragment_transform_shader.js:8` ⟷ `lpg/.../space_fragment_transform_shader.js:8`; `gi/.../space_vertex_transform_shader.js:8` ⟷ `lpg/.../space_vertex_transform_shader.js:8` | All eight builders are `result = PREAMBLE (+ PREAMBLE_COMMON) + source; result = insert_after(result, ANCHOR, CHUNK); return result;`. Both `*_transform_shader.js` pairs are 12-line files that are 90% line-identical. The anchors (`#include <lights_fragment_begin>`, `#include <fog_vertex>`) are the load-bearing part and appear in 4 separate string literals | PROMOTION | `engine/graphics/material/build_injected_shader(source, preamble, anchor, chunk)` in the material layer; the eight modules collapse to data (a preamble string, an anchor constant, a chunk string). Anchors become named constants in one place, matching the Forward+ marker contract | Low effort / Low risk (`LightProbeGridMaterialTransformer.spec.js` covers the LPG path) |
|
|
36
|
+
| 25 | LOW | **`packDepthToRGBA` — a verbatim reimplementation of three.js's `<packing>` chunk — appears twice** | `engine/graphics/impostors/octahedral/shader/ImpostorShaderDepthV0.js:264-272` ⟷ `engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.js:55-63` | Identical `PackUpscale`/`PackFactors`/`ShiftRight8` constants and body. Both files are `RawShaderMaterial` so three's automatic chunk prefix does not apply — but `ShaderChunk.packing` is importable and splice-able. `VoxelImpostorShaderDepthV0.js:13` even says *"Mirrors three.js's packDepthToRGBA … identical encoding to ImpostorShaderDepthV0"*. If three changes the shadow-map depth encoding, both copies silently break | UNJUSTIFIED | Import `ShaderChunk.packing` from three at one boundary module (`engine/graphics/shaders/chunk_packing.js`) and interpolate it — house rule "confine vendor imports to boundaries" | Low effort / Low risk |
|
|
37
|
+
| 26 | LOW | **Bake-camera frame reconstruction duplicated inside one file** | `engine/graphics/impostors/voxel/bake/read_atlas_to_samples.js:78-96` (`decode_normal_to_object_space`) ⟷ `:145-165` (`reconstruct_position`) | ~20 lines identical: the `abs(Dy) > 0.99999` polar nudge (`Dz + 0.0001`, renormalise), `right = normalize(cross((0,1,0), D))` written out as `(Dz, 0, -Dx)`, `up = cross(D, right)`. `:145` comments *"Same polar nudge as the normal decoder, so right/up agree"* — an explicit must-agree invariant left as two copies. Separately, the same frame concept appears 9× in GLSL as `up_OS`/`tangent_OS`/`binormal_OS` (`ImpostorShaderV0.js:245`, `DepthV0:197`, `NormalsV0:175`, `ViewportDepthV0:163`, `WireframeV0:135`, `PerPixelV0:142` and `:320`, `LitV0:191`) with a *different* degeneracy threshold (`0.999`) and a *different* fallback (swap the up vector rather than nudge Z) | UNJUSTIFIED | Extract a file-local `bake_frame_from_direction(out9, D_frame)` used by both functions in `read_atlas_to_samples.js`; fold the GLSL copies into finding #1's shared chunk | Low effort / Low risk |
|
|
38
|
+
| 27 | LOW | **Two byte-size/component-count switch tables over the *same* enum** | `engine/graphics/particles/node-based/codegen/glsl/getTypeByteSize.js:8` ⟷ `engine/graphics/particles/node-based/particle/ParticleAttributeSpecification.js:23` `computeComponentCount` | Both switch on `ParticleDataTypes` over `Float/Vector2/Vector3/Vector4` and both throw on everything else; the first returns exactly 4× the second. The relationship is implicit — adding a non-4-byte component type makes them silently disagree. (`engine/graphics/particles/particular/group/itemSizeFromAttributeType.js:7` is a third table but over a different, older enum, so it is a separate concern) | UNJUSTIFIED | Keep `computeComponentCount` as the single table; `getTypeByteSize(type)` becomes `component_count(type) * BYTES_PER_COMPONENT`, or the byte width becomes a second explicit case list only if a type ever needs one | Low effort / Low risk |
|
|
39
|
+
| 28 | LOW | **`TubeXFixedPhysicsSimulator` / `RibbonXFixedPhysicsSimulator`** | `engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.js:18` `update` ⟷ `engine/graphics/trail/x/simulator/RibbonXFixedPhysicsSimulator.js:17` `update` | Token-identical (75 tokens ×2). 12-line loop: increment age, `clamp01(age/max_age)`, `setAlpha((1-relative_age)*alpha)`, write the flow UV. Only the method names differ (`incrementKnotAttribute_Scalar` vs `incrementPointAttribute_Scalar`, `setKnotAlpha` vs `setPointAlpha`) | UNJUSTIFIED (small) | Falls out of finding #5: unify `TubeX`'s `*Knot*` accessors with `RibbonX`'s `*Point*` names and keep one `TrailXFixedPhysicsSimulator`. Not worth an adapter interface on its own | Low effort / Low risk (`RibbonXFixedPhysicsSimulator.spec.js` exists) |
|
|
40
|
+
| 29 | LOW | **`SimulationStep*` per-particle loop scaffolding ×4** | `engine/graphics/particles/particular/engine/simulator/SimulationStepApplyForce.js:7`, `SimulationStepCurlNoiseAcceleration.js:20`, `SimulationStepCurlNoiseVelocity.js:21`, `SimulationStepFixedPhysics.js:53` — all `execute` | The 12-line preamble + layer-mask gate (`readAttributeScalar(i, PARTICLE_ATTRIBUTE_LAYER_POSITION)`, `if ((layerMask & (1 << layer_position)) === 0) continue;`, `params = layer_parameters[layer_position]`) is identical in all four. The Acceleration/Velocity pair additionally share the 6-line `power_*`/`scale_*` unpack and the `noise[k] * power_k` application | **JUSTIFIED (hot loop)** for the scaffolding — this is a genuine per-particle JS inner loop; hoisting the body into a callback would make the call site megamorphic across four step types and defeat the reason the mask test is inlined. The `power/scale` unpack shared by the two curl-noise steps is *marginal* — a shared `CurlNoiseStepBase` holding the six-parameter unpack would remove it without adding a per-particle indirection | If touched at all: `AbstractCurlNoiseSimulationStep` holding `parameter_schema` and a `#unpack(params)`; leave the loops inline | Low effort / Low risk — but measure first; not worth doing blind |
|
|
41
|
+
|
|
42
|
+
### The headline lead, quantified: `sh3/gi` + `sh3/lpv` vs `sh3/lpg`
|
|
43
|
+
|
|
44
|
+
I measured verbatim line overlap for every corresponding file pair (non-empty, whitespace-normalised):
|
|
45
|
+
|
|
46
|
+
| lpg file | lpg lines | gi/lpv lines | verbatim-shared | % of lpg |
|
|
47
|
+
|---|---|---|---|---|
|
|
48
|
+
| `lpg/material/LightProbeGridMaterialTransformer.js` | 187 | 256 | 104 | 56% |
|
|
49
|
+
| `lpg/material/space_fragment/build_fragment_shader.js` | 30 | 32 | 24 | 80% |
|
|
50
|
+
| `lpg/material/space_fragment/build_vertex_shader.js` | 38 | 22 | 19 | 50% |
|
|
51
|
+
| `lpg/material/space_vertex/build_fragment_shader.js` | 22 | 21 | 19 | 86% |
|
|
52
|
+
| `lpg/material/space_vertex/build_vertex_shader.js` | 33 | 48 | 20 | 61% |
|
|
53
|
+
| `lpg/material/space_fragment/space_fragment_transform_shader.js` | 10 | 10 | 9 | 90% |
|
|
54
|
+
| `lpg/material/space_vertex/space_vertex_transform_shader.js` | 10 | 10 | 9 | 90% |
|
|
55
|
+
| `lpg/light_probe_grid_textures.js` | 126 | 28 | 20 | 16% |
|
|
56
|
+
| `lpg/light_probe_grid_obtain_storage_cached.js` | 61 | 63 | 40 | 66% |
|
|
57
|
+
| `lpg/build_light_probe_grid_for_scene.js` | 66 | 103 | 43 | 65% |
|
|
58
|
+
| `lpg/material/common.js` | 55 | 426 | 10 | 18% |
|
|
59
|
+
| `lpg/material/chunk_lpg_get_irradiance_at.js` | 18 | 21 | 9 | 50% |
|
|
60
|
+
| **total** | **656** | **1040** | **326** | **50%** |
|
|
61
|
+
|
|
62
|
+
**Verdict: the fork is roughly 250 deduplicable lines out of ~1700; the rest is genuinely different and
|
|
63
|
+
should stay separate.** Concretely:
|
|
64
|
+
|
|
65
|
+
- **Mechanically copied (findings #3, #7, #13, #14, #17, #24)**: the transformer skeleton (marker guard,
|
|
66
|
+
`#onBeforeCompile` stage dispatch, `intensity` accessor, `#uniform_version` gate) ~70 lines ×2 (plus a
|
|
67
|
+
third copy in `render/forward_plus`); the six shader-builder glue functions ~60 lines; the two
|
|
68
|
+
`*_transform_shader.js` files ~24 lines; the storage-cached scene-bounds + load/build/store protocol
|
|
69
|
+
~45 lines ×2; the bake-via-task overlay ~25 lines ×2; the `DataTexture3D` incantation ~25 lines ×3.
|
|
70
|
+
- **Genuinely different, leave alone**: `gi/material/common.js`'s 426 lines are the tetrahedral walk
|
|
71
|
+
(`lpv_walk_to_tetrahedron`, `lpv_guess_initial_tet`, barycentric masking, octahedral depth visibility) —
|
|
72
|
+
the LPG replaces all of it with a 3-line 3D-texture fetch. `lpg/light_probe_grid_textures.js`'s YCoCg
|
|
73
|
+
half-float/8-bit split encoding has no LPV counterpart. The SH truncation (L2→L1) is a real design
|
|
74
|
+
difference. `ProbeResolutionStage` is *already* correctly shared (`lpg/material/LightProbeGridMaterialTransformer.js:6`
|
|
75
|
+
imports it from `gi/material/`), which shows the seam is reachable.
|
|
76
|
+
- **Where the seam belongs**: one `AbstractProbeFieldMaterialTransformer` in
|
|
77
|
+
`engine/graphics/material/manager/` owning the marker/clone/compose/stage-dispatch, plus the four
|
|
78
|
+
small promotions in findings #13/#14/#17/#24. Nothing about the tetrahedral-vs-grid lookup needs to
|
|
79
|
+
be unified.
|
|
80
|
+
|
|
81
|
+
## Dismissed leads
|
|
82
|
+
|
|
83
|
+
- `TubeX.js` (557 ln) vs `RibbonX.js` (578 ln) — genuine rewrite, not a copy: `#private` fields, ring topology with a duplicated seam vertex, `v3_orthonormal_matrix_from_normal`, documented throughout. Only the accessor naming (`Knot` vs `Point`) is gratuitously divergent.
|
|
84
|
+
- `particles/node-based/` vs `particles/particular/` — **not a fork**. `node-based` is a GLSL-codegen design with zero importers outside its own tree; `particular` is the live CPU simulator. The two `ParticleSpecification` classes (`node-based/particle/ParticleSpecification.js` vs `particular/group/ParticleSpecification.js`) share only a name — different fields, different purpose. The `particular` one is already marked `@deprecated use AttributeGroupSpec`. This is an unfinished parallel system plus an export-name collision, not duplication.
|
|
85
|
+
- `particles/node-based/nodes/math/Vector3{Add,Divide,Multiply,Subtract}.js` (13 lines each, 79-84% file similarity) — per-node typed classes; house rule explicitly prefers this over a string-keyed operator registry.
|
|
86
|
+
- `AnimationGraph.getStateByDefinition` / `getTransitionByDefinition` / `TextureAtlas.getPatch` / `StaticKnowledgeDatabase.getDescriptorById` — generic "linear scan a small array by field equality"; too small and too type-specific to share.
|
|
87
|
+
- `AnimationGraph.equals` / `AnimationTransitionDefinition.equals` / `AnimationState.equals` — per-class `equals()`; deliberate per house rules.
|
|
88
|
+
- `AttributeGroupSpec.getAttributeByName` / `ParticleSpecification.getByName` / `ParameterTrackSet.getTrackByName` — same 8-line linear scan, but over three unrelated element types in three unrelated collections, and `ParticleSpecification` is deprecated. Not worth a generic.
|
|
89
|
+
- `SGMeshHighlightSystem.js` — despite the name, a different system (propagates highlight down an SGMesh hierarchy), not a fourth copy of finding #8.
|
|
90
|
+
- `engine/graphics/ecs/water` vs `water2` — `water2` contains only a shader plus a browser harness; no forked logic.
|
|
91
|
+
- The "read indices a/b/c then fetch 9 floats at `*3`" block (8+ sites incl. `computeMeshSurfaceArea.js:20`, `tri3_ray_intersection_indexed.js`, `construct_ray_hit_from_geometry.js`) — all inner-loop; a shared reader would need nine out-params or an out-array, which costs more than it saves. `geometry_construct_triangle_normal.js:12` already exists for the one case where the payload justifies it.
|
|
92
|
+
- `setFlag`/`clearFlag`/`writeFlag`/`getFlag` across 30 files (brief §E) — engine-wide component convention, not a domain issue.
|
|
93
|
+
- `inline_lerp` / `inline_length3` hits in `lightmap_seam_fix.js`, `read_atlas_to_samples.js`, `voxelize_samples.js` — per-texel/per-sample loops; inlining is the right call.
|
|
94
|
+
- **Browser harnesses (out of scope, one note as instructed)**: `makeConfig`, `configuration`, `init`, `debug_button`, `catmullRom`, `sample_style_entity` and `makeMovingShadedGeometry` are copy-pasted across at least 12 harnesses — `testPathDisplaySystem.js`, `prototypeAnimatedPathMask.js`, `testShadowMapRendering.js`, `testPointShadowMapRendering.js`, `prototypeBaker.js`, `prototype_voxel_impostors.js`, `prototypeLightProbeGrid.js`, `prototypeSH3Probe.js`, `prototypeShadedGeometry.js`, `prototype_sg_raycast.js`, `prototypeHiZ.js`, `prototypeNormalFrameBuffer.js`, `prototypeParticleVolume.js`, `testClippingPlaneComputation.js`, `testWaterShader.js`, `ptototypeFPPlugin.js`, `prototypeDecalEditor.js`, `prototypeDecalSystem.js`. A shared internal `engine/graphics/harness/` module (engine bootstrap + dat.GUI config + debug button + a spline sampler) would remove several hundred lines. Not counted as a finding.
|
|
95
|
+
|
|
96
|
+
## Notes
|
|
97
|
+
|
|
98
|
+
Three structural forces produce nearly all the duplication in this domain.
|
|
99
|
+
|
|
100
|
+
**1. "V2 by copy".** Every second-generation subsystem here was started by duplicating the first and
|
|
101
|
+
editing: `trail3d` from `trail2d`, `lpg` from `lpv`/`gi`, `mesh-v2` from `mesh`, `water2` from `water`,
|
|
102
|
+
`decal/v2`. That is a defensible way to start, but nothing was ever pulled back afterwards, so the
|
|
103
|
+
newer copy accumulates fixes the older one never receives. Findings #3, #4 and #18 are all the same
|
|
104
|
+
failure mode with the same signature: **the newer file carries a comment explaining a bug, and the
|
|
105
|
+
older file still has that bug.** `Trail3DSystem.js:31-45` documents the head-position mismatch that
|
|
106
|
+
`Trail2DSystem.js:157` still exhibits; `canvas2d_draw_linear_scale.js:44` documents the `ceil`
|
|
107
|
+
over-extrapolation that `canvas2dDrawWorldAxisLabels.js:44` still exhibits; `MaterialTransformer.js:107`
|
|
108
|
+
clones the material where `sh3/gi/material/MaterialTransformer.js:311` throws the clone away. When
|
|
109
|
+
auditing this codebase, an explanatory comment in one of a pair is a reliable index into a live defect
|
|
110
|
+
in the other.
|
|
111
|
+
|
|
112
|
+
**2. No shader-chunk mechanism.** GLSL lives in template literals inside `.js` files, and the codebase
|
|
113
|
+
has no include mechanism for it (three's `ShaderChunk` is used only through three's own automatic
|
|
114
|
+
prefix, which `RawShaderMaterial` skips). The result: every shader that needs octahedral mapping,
|
|
115
|
+
depth packing, or SH evaluation gets its own transcription. The webpack raw-loader path already exists
|
|
116
|
+
(`ImpostorShaderV1.js:10-11` imports `./glsl/v1/vertex.glsl`), and the `sh3/gi` side already
|
|
117
|
+
demonstrates the string-export pattern (`chunk_lpv_get_irradiance_at.js`, `common.js` as a default
|
|
118
|
+
string export) — the tooling is there, it just wasn't applied to the impostor family.
|
|
119
|
+
|
|
120
|
+
**3. `System` boilerplate has no base.** `System` is a bare interface, so every renderable system
|
|
121
|
+
re-implements the same three chores: create a render layer, wire `buildVisibleSet` + `compute_depth_range`
|
|
122
|
+
to a BVH, acquire/release a plugin `Reference` in `startup`/`shutdown`. `make_bvh_visibility_builder`
|
|
123
|
+
shows someone already noticed half of it. Findings #8 and #16 are the tail of that.
|
|
124
|
+
|
|
125
|
+
### GLSL duplication
|
|
126
|
+
|
|
127
|
+
This is where the mechanical pass is blind, and it is by far the largest volume of duplication in the
|
|
128
|
+
domain. Token-based clone detection sees a shader as one string literal, so none of the following
|
|
129
|
+
appeared in the brief:
|
|
130
|
+
|
|
131
|
+
- **`VecToSphereOct` is declared 17 times** across `.js` template literals and `.glsl` files, in **four
|
|
132
|
+
mutually incompatible conventions**: the impostor family folds on `octahedron.y < 0` and returns
|
|
133
|
+
`.xz` using `sign()` (9 copies); `sh3/gi/material/common.js:118` uses an `l1norm` formulation, folds
|
|
134
|
+
on `v.y < 0`, returns `v.xz`; `sh3/lpv/depth/octahedral/{shader,stitching,v2}` folds on
|
|
135
|
+
`direction.z < 0` and returns `.xy` using `sign_not_zero` (4 copies); `impostors/octahedral/shader/glsl/common.glsl:57`
|
|
136
|
+
and `glsl/v1/*` are HLSL-derived and use `frac`/`saturate`/`tex2Dgrad` (3 copies). Meanwhile
|
|
137
|
+
`core/geom/3d/octahedra/octahedral_direction_to_uv.js` is the *single* JS implementation and is the
|
|
138
|
+
one the bake path uses. A shader author reading `VecToSphereOct` in this repo cannot tell which
|
|
139
|
+
mapping they will get.
|
|
140
|
+
- **The whole octahedral-impostor sampling library (~90 lines)** appears 9 times (finding #1),
|
|
141
|
+
including twice within `ImpostorShaderPerPixelV0.js` and twice within `ImpostorShaderLitV0.js`
|
|
142
|
+
because each embeds it in both the vertex and the fragment stage.
|
|
143
|
+
- **The probe-depth library (~220 lines)** is copied from production into a debug visualiser
|
|
144
|
+
(finding #2), typo included.
|
|
145
|
+
- **The billboard card frame** (`up_OS` pole fallback → `tangent_OS = normalize(cross(up_OS, normal_OS))`
|
|
146
|
+
→ `binormal_OS = cross(normal_OS, tangent_OS)`) appears 9 times in the octahedral shaders, and a
|
|
147
|
+
differently-thresholded CPU cousin appears twice in `read_atlas_to_samples.js` (finding #26).
|
|
148
|
+
- **The voxel splat expansion** appears 5 times (finding #6), carrying an invariant its own comments
|
|
149
|
+
say must be kept in sync by hand.
|
|
150
|
+
- **`packDepthToRGBA`** — three.js's own `<packing>` chunk — is retyped twice (finding #25).
|
|
151
|
+
- **The SH cosine-lobe evaluator** appears 3 times in GLSL with two different sign conventions
|
|
152
|
+
(finding #7).
|
|
153
|
+
- Smaller repeats not worth individual findings: `vec3 inverseTransformDirection(...)` retyped in
|
|
154
|
+
`sh3/shader/visualize.frag.js:5`, `sh3/lpv/depth/octahedral/shader/visualize.frag.glsl:1` and
|
|
155
|
+
`.../v2/visualise.frag.glsl:1` (three.js provides it in the standard prefix, but these are
|
|
156
|
+
`RawShaderMaterial`); the `#ifdef USE_TEXTURE { texel = texture2D(uDiffuse, vUv); diffuseColor *= texel; }`
|
|
157
|
+
block in `RibbonMaterialX.js:95` and `TubeMaterialX.js:99`; `sign_not_zero` with three overloads in
|
|
158
|
+
`gi/material/common.js:76-93` and `v2/visualise.frag.glsl:26-43`.
|
|
159
|
+
|
|
160
|
+
The single highest-leverage change in this domain is not any individual extraction — it is establishing
|
|
161
|
+
one GLSL chunk directory (`engine/graphics/shaders/chunks/`) with the octahedral, SH, packing and
|
|
162
|
+
frame-construction primitives as named string exports, and then deleting the transcriptions. That one
|
|
163
|
+
move retires findings #1, #2, #6, #7, #25 and #26 and removes roughly 1,400 lines of copied shader
|
|
164
|
+
source.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Domain E — engine/physics
|
|
2
|
+
|
|
3
|
+
Scope: `engine/physics/**` (+ `engine/ecs/ik/*` where the task named it). Every finding below was verified by opening the cited files. `*.spec.js`, `prototype*.js`, `test*.js` excluded.
|
|
4
|
+
|
|
5
|
+
The mechanical brief was thin and its three "A. token-identical" hits turned out to be the *least* interesting things in the domain (two are explicitly, correctly justified). The real duplication here is semantic re-derivation, and it clusters in four places: (1) the fluid solver's collocated-vs-MAC twin kernel families, (2) the narrowphase contact-emission epilogues, (3) the two solvers (contacts / constraints) which independently implement the same impulse application, (4) core primitives that return *parameters* where every single caller wants *points*.
|
|
6
|
+
|
|
7
|
+
## Findings
|
|
8
|
+
|
|
9
|
+
| # | Severity | Concept | Sites | Evidence | Verdict | Proposed common path | Effort/Risk |
|
|
10
|
+
|---|---|---|---|---|---|---|---|
|
|
11
|
+
| 1 | HIGH | **Solver impulse application written twice, verbatim.** `Δv = P·invM; Δω = Iw⁻¹·(r×P)` with the identical `invM===0` guard, identical sign folding, identical `world_inverse_inertia_apply_raw` argument order and identical AV write-back. | `engine/physics/constraint/solve_constraints.js:250` `apply_impulse(ss, base, invM, rx,ry,rz, Px,Py,Pz, sign)` ⟷ `engine/physics/solver/solve_contacts.js:401` `apply_impulse_to_body(ss, base, invM, rx,ry,rz, Px,Py,Pz, sign, scratch_inertia)` | Bodies are line-for-line identical (15 statements each). The *only* difference is that the contacts version takes `scratch_inertia` as an 11th parameter where the constraints version closes over a module-static one. Both call the same kernel with the same operand order. | UNJUSTIFIED | Promote one copy to `engine/physics/solver/apply_impulse_to_body.js` (next to `friction_cone.js`) with the explicit `scratch` parameter; both solvers import it. Keep the parameter form — it is already the more explicit of the two. | Low effort. **Determinism: LOW risk** — identical operand order means bit-identical output; `determinism.spec.js` is the gate. This is the single most load-bearing routine in the step (per-row × per-iteration), so re-measure the rain/KEVA benches: a cross-module call here must stay inlined. |
|
|
12
|
+
| 2 | HIGH | **SAT minimum-overlap magnitude computed three ways, one of which diverges.** Same invariant ("how far along this axis must I push to separate?"), three independent expressions. | `engine/physics/narrowphase/box_box_manifold.js:135` `overlap = (rA + rB) - dist`; `engine/physics/narrowphase/box_triangle_contact.js:209-211` `overlap = min(bmax - tmin, tmax - bmin)`; `engine/physics/narrowphase/convex_convex_manifold.js:261` `overlap = Math.min(maxA,maxB) - Math.max(minA,minB)` | The first two are the minimum-translation (push-out) distance and agree. The third is the *interval intersection length*, which is smaller whenever one projection interval contains the other (A=[0,10], B=[4,5] → push-out 5, intersection 1). SAT then picks the axis by minimum overlap, so an understated value makes a containment axis *more* likely to win and be reported as the depth. `convex_convex_manifold` is documented as being for "the SMALL convex pieces of a decomposed mesh" — exactly where a thin piece is contained in a fat one. The three sites also carry three different degeneracy tests (`PARALLEL_EPS_SQR` absolute in two files, `PARALLEL_EPS_REL * ref2` relative in the third) and only two of three have the `TIE_REL/TIE_ABS` deadband. | UNJUSTIFIED (and needs a correctness decision before unification) | Extract `sat_track_min_overlap(state, ux,uy,uz, minA,maxA, minB,maxB, kind, sign_ref)` covering: normalise-and-reject, separation test, push-out overlap, deadband tie-break, centre-delta sign orientation. Each caller keeps its own *projection* (box half-extent vs 3 vertices vs vertex loop) — that part is legitimately shape-specific. | Medium effort. **Determinism: HIGH risk** and behaviour-changing: fixing `convex_convex`'s formula changes chosen axes on containment configs. Do it test-first against `convex_convex_manifold`'s own suite, then the determinism gate. |
|
|
13
|
+
| 3 | HIGH | **Whole collocated advection family shadows the MAC family and has zero callers.** `FluidSimulator` uses only the `v3_mac_*` kernels (imports at `engine/physics/fluid/FluidSimulator.js:6-11`). | Zero non-spec importers: `v3_grid_advect_sl_velocity.js`, `v3_grid_apply_advection_forward.js`, `v3_grid_apply_scalar_advection.js`, `v3_grid_advect_maccormack_scalar.js`, `v3_grid_advect_maccormack_velocity.js` (≈555 LOC) | Verified by importer count across `engine/` and `app/` (spec files excluded). Each is a collocated-grid re-implementation of an algorithm the MAC file next to it also implements (SL trace, MacCormack forward/backward/correct, Selle limiter, solid pass-through). A fix in the MAC limiter or the solid handling silently misses five files. | UNJUSTIFIED (as duplication) — but **NOT dead code**: meep is a library, these are public surface. | Do not delete. Collapse the *internal* redundancy first (findings 4 and 5), then leave the collocated family as thin, correct, exported kernels. Add a one-line "the simulator uses the MAC family; this is the collocated variant" note to each so the next reviewer does not re-derive this. | Low effort for 4/5; the family itself stays. Determinism: none (unreferenced by the sim). |
|
|
14
|
+
| 4 | MEDIUM | **MacCormack pass 1 re-implements the SL kernel that sits in the same directory.** The MAC file does this correctly (calls the SL kernel); the grid file does not. | `engine/physics/fluid/solver/v3_grid_advect_maccormack_scalar.js:66-82` (pass 1) ⟷ `v3_grid_apply_scalar_advection.js:38-58`; `v3_grid_advect_maccormack_velocity.js:73-94` (pass 1) ⟷ `v3_grid_advect_sl_velocity.js:69-96` | Both pairs are statement-for-statement identical: same solid pass-through, same `s = x - vel[c]*dt`, same sampler call. Contrast `v3_mac_advect_scalar.js:146`, which *does* call `v3_mac_advect_sl_scalar(fwd, ...)` for its forward pass — proving the delegation is viable and this is an oversight, not a design choice. | UNJUSTIFIED | Replace each pass-1 loop with the call to its SL sibling, exactly as the MAC scalar file already does. | Low effort. **Determinism: LOW** — identical arithmetic and identical loop order; the only change is a function boundary. |
|
|
15
|
+
| 5 | MEDIUM | **`v3_grid_apply_advection_forward` is a slower copy of `v3_grid_advect_sl_velocity`, and says so in its own docstring.** | `engine/physics/fluid/solver/v3_grid_apply_advection_forward.js:32` ⟷ `v3_grid_advect_sl_velocity.js:38` | The docstring at `v3_grid_apply_advection_forward.js:16` states: it "is exactly `v3_grid_advect_sl_velocity(outputs, inputs, inputs...)`". The body differs only by issuing three separate `scs3d_sample_linear` calls instead of one fused `scs3d_sample_linear3` — i.e. it recomputes the trilinear corner indices and weights three times per cell. MinHash flagged the pair at 0.813. | UNJUSTIFIED | Keep the export (public surface, meaningful name); make the body a one-line delegation to `v3_grid_advect_sl_velocity`. | Low effort. **Determinism: MEDIUM** — the fused 3-channel sampler may associate weights differently from three scalar samples. Diff numerically before/after; `quality.spec.js` is the fluid gate. |
|
|
16
|
+
| 6 | MEDIUM | **Selle monotone limiter written four times, twice under the same name `limit8` with different signatures in the same directory.** | `engine/physics/fluid/solver/v3_grid_advect_maccormack_velocity.js:155` `limit8(value, data, c0..c7)`; `v3_mac_advect_maccormack_velocity.js:293` `limit8(value, data, lx,ly,lz, dim_x,dim_y,dim_z, row, slice)`; hand-inlined at `v3_grid_advect_maccormack_scalar.js:107-132`; hand-inlined **with solid masking** at `v3_mac_advect_scalar.js:191-232` | The two named `limit8`s are different functions with the same name two files apart — a genuine reader hazard, and invisible to the mechanical pass because neither is exported (brief §G is empty). The grid-scalar inline is a *byte-level* match for the MAC-velocity `limit8` signature (`limit8(corrected, source, x-vx, y-vy, z-vz, res_x,res_y,res_z, res_x, slice_size)` reproduces it exactly). The MAC-scalar inline is a genuine superset (skips solid corners, `Infinity` init, fallback to the forward value). | UNJUSTIFIED for 3 of 4 | Extract two files: `v3_limit8_lattice.js` (position-form, absorbs the MAC-velocity version, the grid-scalar inline and — via a precomputed-index overload or by the caller passing the position — the grid-velocity version) and `v3_limit8_lattice_masked.js` for the solid-aware variant. Do **not** merge masked and unmasked behind a flag (uniform-control-flow rule). | Low-medium effort. **Determinism: LOW** for the unmasked merge (identical comparison order). These are per-cell kernels over the whole grid, so verify the extracted call stays inlined — the bodies are branchy but loop-free, which is the inlinable shape. |
|
|
17
|
+
| 7 | MEDIUM | **Vorticity confinement: the MAC kernel collocates to cell centres and then re-derives the collocated kernel's two passes verbatim.** | `engine/physics/fluid/solver/v3_grid_apply_vorticity_confinement.js:78-130` (pass 1 curl+\|ω\|, pass 2 N×ω) ⟷ `v3_mac_apply_vorticity_confinement.js:98-164` (pass 2, pass 3) | The MAC file's pass 1 (lines 70-90) averages faces into `scratch_cu/cv/cw` cell-centred arrays; its passes 2 and 3 are then character-identical to the grid file's passes 1 and 2 — same central-difference curl expressions, same `scratch_wmag` fill, same `magnitude_floor = 1e-10`, same `s = epsilon_dt / g_len`, same `N × ω` cross. Only the final deposit differs (cell write vs `0.5 *` scatter to two faces per axis). ≈60 lines. | UNJUSTIFIED | Extract `v3_grid_compute_vorticity(scratch_wx,wy,wz,wmag, vel_x,vel_y,vel_z, res, solid)` and `v3_grid_compute_confinement_force(out3, scratch_*, c, res_x, cell_slice, epsilon_dt)`; the MAC kernel calls both and keeps only its collocate-in / scatter-out ends. | Medium effort. **Determinism: LOW** (same expressions, same order). Per-cell whole-grid kernel — measure; the force helper writing 3 floats to a module-static may cost vs the current fully-inlined form. |
|
|
18
|
+
| 8 | MEDIUM | **Two-bone and one-bone IK solvers share a ~75-line terrain-probe + influence prologue verbatim.** | `engine/ecs/ik/TwoBoneInverseKinematicsSolver.js:76-151` (`solve`) ⟷ `engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js:74-151` (`solve`) | Identical: the ray direction from parent→bone, the primary `terrain.raycastFirstSync`, the early return, the secondary cast back along the contact normal from the bone + normal, the second early return, `constraint.offset * <scale>` → `v3_displace_in_direction`, the `delta_contact_c_*` triple, `v3_dot` side test, `v3_length`, the `dot_contact_side < 0 ? 1 : 1 - clamp01(constraint.distance.normalizeValue(inverseLerp(0, <scale>, d)))` influence, and the `influence <= 0` bail. Only two things differ and both are plain inputs: the ray origin (parent bone world position vs root bone A world position) and the length scale (`boneWorldScale.length() * 1/√3` vs limb length A→B→C). The mechanical brief missed this entirely because its path filter was `^engine/physics/`. | UNJUSTIFIED | Extract `ik_probe_surface_influence(contact_out, terrain, origin_xyz, direction_xyz, effector_xyz, length_scale, constraint) -> number` returning influence (0 = no contact / no influence) and leaving the contact in the supplied `SurfacePoint3`. Both solvers keep only their own post-influence rotation math, which is genuinely different. | Low-medium effort. No determinism exposure (animation-side, not the physics step). Note: the task hypothesis "two competing two-bone IK solvers" is **false** — `TwoBoneInverseKinematicsSolver` *imports* `two_joint_ik` (line 11). The real duplication is this sibling prologue. |
|
|
19
|
+
| 9 | MEDIUM | **Closest-point-pair → contact epilogue written five times.** Given two witness points and two radii, produce (unit normal B→A, depth, witness on A, witness on B) with a degenerate-distance tie-break. | `engine/physics/narrowphase/sphere_sphere_contact.js:27-51`; `sphere_triangle_contact.js:99-141`; `capsule_contacts.js:89-116` (`capsule_sphere_contact`); `capsule_contacts.js:158-197` (`capsule_capsule_contact`); `capsule_contacts.js:293-324` (`capsule_capsule_multi_contacts` inner loop) | All five: `d = pa - pb`, `dist_sqr`, `sum = ra + rb`, `if (dist_sqr >= sum*sum) return false`, `dist = sqrt`, `if (dist > 0) normalize else <tie-break>`, `out[3] = sum - dist`, `out[4..6] = pa - n*ra`, `out[7..9] = pb + n*rb`. The tie-break direction is the only real variation: `(1,0,0)` at two sites, segment-centre delta at one, body-centre delta at two — i.e. it is an *input*, not a mode. | UNJUSTIFIED | `narrowphase/emit_pair_contact.js`: `emit_pair_contact(out, pax,pay,paz, pbx,pby,pbz, ra, rb, fbx,fby,fbz) -> boolean`, fallback direction always supplied by the caller (no null-sentinel). `sphere_sphere_contact` writes only lanes 0-3 today and would need to keep that shorter contract or gain witness lanes — check its callers first. | Low-medium effort. Per-contact hot path but not per-cell; a leaf function of this size is a normal V8 inline candidate. **Determinism: LOW** (identical order), but the tie-break unification changes `capsule_sphere_contact`'s `(1,0,0)` if you "improve" it — don't; pass `(1,0,0)` explicitly. |
|
|
20
|
+
| 10 | MEDIUM | **Cap-endpoint contact append + spatial dedup written twice, with the same magic constant.** | `engine/physics/narrowphase/capsule_contacts.js:582-619` (endpoint loop inside `capsule_box_multi_contacts`) ⟷ `capsule_triangle_contact.js:360-395` (`try_endpoint_contact`) | Both: run a sphere-vs-X query at a cap centre, unpack the 10-float `(n, depth, A, B)` scratch, loop `k < count` comparing the capsule-side world position against every already-emitted contact, reject below `DEDUPE_DIST_SQR`, then write the `(A, B, n, depth)` stride-10 record. `DEDUPE_DIST_SQR = 1e-6` is declared independently in both files (`capsule_contacts.js:510`, `capsule_triangle_contact.js:64`) with near-identical justifying comments, and both files independently declare a stride-10 constant (`CAPSULE_BOX_CONTACT_STRIDE`, `CAPSULE_TRIANGLE_CONTACT_STRIDE`). | UNJUSTIFIED | `narrowphase/append_deduped_contact.js`: `append_deduped_contact(out, count, scratch10) -> number`, with `DEDUPE_DIST_SQR` and the stride-10 candidate layout defined once (the layout is already shared by three files — put it next to `decomposition/triangle_buffer_layout.js`). | Low effort. **Determinism: LOW.** Real payoff is the single definition of the tolerance: today a tuning change to one leaves the other. |
|
|
21
|
+
| 11 | MEDIUM | **Reference-face manifold emission epilogue written three times.** Project each clipped point, reject non-penetrating, push `(x,y,z,depth)` candidates, `reduce_manifold_contacts`, then per kept point project onto the reference plane and swap A/B by `ref_is_A`. | `engine/physics/narrowphase/convex_convex_manifold.js:596-622` (`emit_face_manifold`); `box_box_manifold.js:578-670`; `box_triangle_contact.js:455-497` (`emit_box_face_manifold`) | All three build a stride-4 (or 7) candidate array, call `reduce_manifold_contacts(cand, n, stride, MAX_CONTACTS, depth_lane)`, then run the identical "ref-side witness = point − ref_out · dist_to_plane" projection and the identical `if (ref_is_A) {A=proj,B=pt} else {A=pt,B=proj}` swap into `4 + k*CONTACT_STRIDE`. A previous review already lifted the *reduction* out of these three (see the "Each previously carried its own byte-identical copy" note at `reduce_manifold_contacts.js:14-18`) — this is the un-lifted residue of the same block. | UNJUSTIFIED | Finish the extraction the previous review started: `emit_reference_face_manifold(out, cand, n, stride, ref_is_A, r0x,r0y,r0z, rmx,rmy,rmz)` doing reduce + project + swap + write. The *clipping* stays per-shape. | Medium effort. **Determinism: LOW** if the projection expression is copied verbatim. The three sites use different candidate strides (4 and 7), so the stride stays a parameter — `reduce_manifold_contacts` already set that precedent. |
|
|
22
|
+
| 12 | MEDIUM | **A general 2D half-plane Sutherland–Hodgman clipper exists in core, unused, while narrowphase ships its own.** | `core/geom/2d/polygon/polygon2_clip_halfplane.js:20` (exported, spec'd, **zero consumers**) ⟷ `engine/physics/narrowphase/box_triangle_contact.js:110` `clip_against_half_plane_uv` | Same algorithm: walk edges, evaluate a signed side function at both endpoints, emit the start point if inside, emit the crossing at `t = sa/(sa-sb)`. The core version parameterises the line by point+direction and tests `cross(dir, P-e)`; the physics version by point+normal and tests `(P-e)·n` — algebraically the same half-plane with `n = perp(dir)`. Note `box_box_manifold.js:3` and `box_triangle_contact.js:3` *already* import the sibling `polygon2_clip_axis_halfplane` from core, so the boundary crossing is established practice. | UNJUSTIFIED | `box_triangle_contact` imports `polygon2_clip_halfplane` and converts its normal to a direction (or add a normal-form export next to it). | Low effort. **Determinism: MEDIUM** — the core version carries `POINT_EPS = 1e-9` inside/outside slack; the physics version uses exact `<= 0`. That changes the surviving vertex count on boundary-coincident points, hence the manifold. Diff on the box-triangle suite before adopting. |
|
|
23
|
+
| 13 | MEDIUM | **Effector face-lattice splat scaffolding written six times (3 axes × 2 effectors).** | `engine/physics/fluid/effector/ImpulseFluidEffector.js:105-132, 134-161, 163-191` ⟷ `WakeFluidEffector.js:249-273, 275-298, 300-~330` | Each block: derive the per-lattice index range as `max(0, floor(box_min [+0.5 on own axis]))` … `min(res_axis [−1 off-axis], ceil(box_max [+0.5]))`, triple loop, compute the lattice offset (`sx*res_y` / `res_x*(res_y+1)` / `res_x*res_y`), evaluate a falloff weight at the face position (offset −0.5 on the own axis), `continue` on zero weight, `continue` on `pin[f] !== 0`, accumulate `v[f] += impulse_axis * w`. The only per-block variation is the weight function — and `WakeFluidEffector` already proves this can be a callback (`tube_weight` closure at line 230). | UNJUSTIFIED | `fluid/effector/mac_splat_axis.js`: `mac_splat_axis(velocity, pin, axis, res_x,res_y,res_z, box6, deposit, weight_fn)`. The two effectors then differ only in their weight function and box, which is the actual content. | Medium effort. **Determinism: LOW.** Perf: this is a *bounded* splat (influence radius), not a whole-grid kernel, and the Wake path already pays a closure call per face — so the reuse-over-micro-opt rule applies cleanly here. |
|
|
24
|
+
| 14 | PROMOTION | **`tri3_closest_point_barycentric` returns barycentrics; all 12 callers immediately reconstruct the world point.** The missing primitive is `tri3_closest_point`. | Physics: `sphere_triangle_contact.js:89-96`; `capsule_triangle_contact.js:186-191`, `:210-215`, `:234-239`; `compute_penetration.js:331-335`. Core/graphics (context): `core/geom/3d/shape/Triangle3D.js:169-175` and `:212-…`; `core/geom/3d/topology/struct/binary/query/bt_mesh_find_nearest_face.js:131-137`; `core/geom/3d/shape/MeshShape3D.js:439`, `:491`; `core/geom/3d/shape/ConvexHullShape3D.js:254`, `:285`; `engine/graphics/geometry/buffered/query/bvh32_geometry_nearest.js:203`; `.../query_bvh_geometry_nearest.js:164` | Every site writes the same four lines: `const c = 1 - a - b; qx = a*ax + b*bx + c*cx; qy = …; qz = …`. Several then compute the same `d = p - q; d_sqr` follow-up. This is the block the brief flagged as shared between `Triangle3D.js:162`, `capsule_triangle_contact.js:181`, `bt_mesh_find_nearest_face.js` and the two BVH nearest queries. | PROMOTION | Add `core/geom/3d/triangle/tri3_closest_point.js` — `tri3_closest_point(out, out_offset, px,py,pz, ax..cz)` writing the world point (wrapping the barycentric version) — and `tri3_point_distance_sqr(...)` for the distance-only callers. Keep the barycentric export (public surface; a few callers genuinely want the weights). | Low effort per site, 12 sites. **Determinism: LOW** if the reconstruction expression is copied verbatim (`a*ax + b*bx + c*cx` in that order — `bt_mesh_find_nearest_face.js:135` writes `ax*wa + bx*wb + cx*wc`, which is the same order of operands; check each before converting). |
|
|
25
|
+
| 15 | PROMOTION | **`line3_compute_segment_segment_closest_points` returns `(s, t)`; all four physics callers immediately lerp both segments.** | `engine/physics/narrowphase/capsule_contacts.js:142-156`; `capsule_triangle_contact.js:105-117` (`test_edge`); `convex_convex_manifold.js:501-516` (`emit_edge_pair_contact`); `box_triangle_contact.js:874-885` (`emit_edge_cross_contact`) | All four: call the core function into a length-2 scratch, then six `p = a0 + (a1 - a0) * s` lines. This is the bulk of the brief's 39 `inline_lerp` hits in narrowphase. `core/geom/3d/line/` already contains a differently-named `line3_closest_points_segment_segment.js` — worth checking whether *it* is the point-returning variant that was never adopted. | PROMOTION | Add `line3_segment_segment_closest_point_pair(out6, a0x..a1z, b0x..b1z)` writing both world points (and returning the squared distance, which three of the four sites compute next anyway). | Low effort. **Determinism: LOW** — the lerp form `a + (b-a)*t` is used identically at all four sites; preserve it exactly (do not "improve" to `(1-t)*a + t*b`). |
|
|
26
|
+
| 16 | LOW/MEDIUM | **Two hash-combine conventions coexist inside `engine/physics`, producing different values for the same inputs.** | Hand-rolled `h = (h*31 + x) | 0`, seeded with the first field: `engine/physics/ecs/Collider.js:157`, `RigidBody.js:279`, `Joint.js:442`. Canonical `combine_hash(...)`: `engine/physics/fluid/effector/GlobalFluidEffector.js:182`, `ImpulseFluidEffector.js:228`. A third variant `h = ((h<<5) - h) + v; h \|= 0` seeded with `n`: `engine/physics/fluid/ecs/FluidEffectorsComponent.js:77` | `core/collection/array/combine_hash.js` → `computeIntegerArrayHash` → `computeStridedIntegerArrayHash` is the canonical fold: `hash = length`, then `hash = ((hash<<5) - hash) + (v >>> 0)`, returning **uint32**. `(h<<5)-h` and `h*31` are the same multiply; the divergences are the seed (`length` vs first field) and the output signedness (`>>> 0` vs `\| 0`). Three conventions in one package. | UNJUSTIFIED as a *convention* split; the hand-rolled fold itself is JUSTIFIED (`combine_hash(...args)` allocates a rest-array per call — `RigidBody.hash()` folds 12 fields and runs during dirty-checking) | Do **not** route the ECS components through `combine_hash`. Instead add a non-allocating `hash_combine_step(h, v)`-style primitive next to `computeStridedIntegerArrayHash` and have all three physics families use it, so the seed and signedness are decided once. Then `FluidEffectorsComponent` and the two effectors stop being a third and fourth answer. | Low effort. No determinism exposure (hashes are change-detection, not simulation state) — but do check nothing persists these values across versions before changing seed/signedness. |
|
|
27
|
+
| 17 | LOW/MEDIUM | **`solve_constraints` re-derives `angular_effective_mass_raw`, which was extracted for exactly this purpose.** | `engine/physics/constraint/solve_constraints.js:191-204` `axis_effective_mass` ⟷ `engine/physics/inertia/world_inverse_inertia.js:102` `angular_effective_mass_raw` | The core of `axis_effective_mass` is `cx,cy,cz = r × d; world_inverse_inertia_apply_raw(scratch, …, cx,cy,cz); k += cx*s[0] + cy*s[1] + cz*s[2]` — character-for-character the body of `angular_effective_mass_raw`. That kernel's own docstring (lines 80-84) says it is "The shared kernel behind the contact/joint solver's SoA path and the raycast-vehicle's component path — each previously inlined the cross product + apply_raw + dot themselves." `solve_contacts.js:526` and `RaycastVehicle.js:54` adopted it; `solve_constraints.js` was missed. | UNJUSTIFIED (incomplete prior extraction) | `axis_effective_mass` becomes `invM === 0 ? 0 : invM + angular_effective_mass_raw(iix,iiy,iiz, qx,qy,qz,qw, rx,ry,rz, dx,dy,dz, scratch_inertia)`, keeping the existing zero-inertia short-circuit. | Low effort. **Determinism: LOW** — identical operand order by construction. Note `angular_axis_effective_mass` at `solve_constraints.js:146` is genuinely different (`a·Iw⁻¹·a`, no lever arm) and should be left alone. |
|
|
28
|
+
| 18 | LOW/MEDIUM | **EPA→MPR fallback + MTV sign-validation + normalise written three times.** | `engine/physics/narrowphase/narrowphase_step.js:1128-1138` and `:1245-1255` (concave-triangle path and general convex fallback, same file) ⟷ `engine/physics/queries/shape_cast.js:394-408` (MPR-only variant) | The two `narrowphase_step` copies are identical: `gjk_epa_penetration` → `pen_depth > 0 && Number.isFinite(pen_depth)` → scale unit axis by depth, else `mpr()` → `depth = Math.sqrt(ex*ex+ey*ey+ez*ez)` → `!(depth > 0) \|\| !Number.isFinite(depth)` bail. All three then dot the MTV against a centre-delta reference, flip on negative, and emit `-e * (1/depth)` as the B→A normal (`shape_cast.js:401-407` even says "same pattern as `narrowphase_step`'s EPA path"). | UNJUSTIFIED for the two same-file copies | `narrowphase/resolve_penetration_mtv.js`: `resolve_penetration_mtv(out4, posed_a, posed_b, ref_x, ref_y, ref_z) -> boolean` writing unit normal + depth. `shape_cast` keeps its MPR-only path (it deliberately skips EPA — documented at `shape_cast.js:361-366` — so folding it in would need a mode flag; leave it). | Low effort for the two `narrowphase_step` copies. **Determinism: LOW.** Do not unify the third site — that would need the sentinel-parameter shape the house rules forbid. |
|
|
29
|
+
| 19 | LOW | **The `(p + 0.5) \| 0` + clamp + solid-lookup convention is written six times.** | `engine/physics/fluid/solver/v3_mac_clip_trace.js:83` `point_in_solid`; `v3_mac_advect_scalar.js:23` `end_in_solid`; `v3_mac_advect_maccormack_velocity.js:23` `end_in_solid`; inline ×3 at `v3_mac_advect_sl_velocity.js:112-118`, `:155-161`, `:200-206` | The two `end_in_solid` copies are byte-identical (brief §A, 120 tokens ×3 with `point_in_solid`). | **JUSTIFIED — with one exception.** `v3_mac_clip_trace.js:25-29` carries an explicit, measured performance contract: the loop in `v3_mac_clip_trace` blocks V8 inlining and "a per-face call costs several times the entire surrounding advection body (measured 8.6× on the SL kernel)", so hot kernels must inline the fluid test. The three inline copies in `v3_mac_advect_sl_velocity` are unambiguously correct. **But** the two `end_in_solid` *functions* are already function calls — the perf argument that justifies inlining does not justify a private copy of a loop-free leaf. | Export `point_in_solid` from `v3_mac_clip_trace.js` and have the two advect kernels import it instead of redeclaring it; leave the three genuine inline expansions alone. | Low effort, **but measure first** — that is the whole point of the contract comment. If the imported leaf does not inline as well as the module-local one, revert and instead cross-reference the copies in a comment. Determinism: none (integer indexing). |
|
|
30
|
+
| 20 | LOW | **Unnormalised triangle face normal (`(B−A) × (C−A)`) inline-derived at five physics sites.** | `engine/physics/narrowphase/sphere_triangle_contact.js:73-78`; `capsule_triangle_contact.js:162-166` and `:320-325`; `refine_ray_concave.js:142-146`; `compute_penetration.js:263-271` | Same three cross-product lines each time, three of the five followed by the same `mag = sqrt(...)`, degeneracy bail, `1/mag` normalise. Context: `core/geom/3d/triangle/` already offers **three** names for this — `compute_triangle_normal.js`, `tri3_compute_normal.js`, `v3_compute_triangle_normal.js` — which is likely why nobody reaches for one. | UNJUSTIFIED, but low value | Pick the canonical core name (a domain-A decision) and use it. Worth doing only as part of that consolidation, not on its own. | Low effort. **Determinism: LOW** if operand order matches. Per-triangle in a concave dispatch loop — inlining is defensible here; this is a readability finding, not a perf one. |
|
|
31
|
+
| 21 | LOW | **"Perpendicular axis pair from an axis index" written three times.** | `engine/physics/narrowphase/box_box_manifold.js:417-432`; `box_triangle_contact.js:835-842`; and again in `box_triangle_contact.js`'s `emit_box_face_manifold` (half-extent selection at `:441-442` region) | Identical `if (idx===0){u=1;v=2}else if(idx===1){u=2;v=0}else{u=0;v=1}` plus the identical `half_u = u===0 ? hx : (u===1 ? hy : hz)` ternary chain. | UNJUSTIFIED, low value | Two module-static `Uint8Array` lookup tables (`PERP_U = [1,2,0]`, `PERP_V = [2,0,1]`) in a shared `narrowphase/box_axis_tables.js`, plus a 3-float half-extent read. Typed constants, no string keys. | Trivial effort, **determinism: none** (integer selection only). |
|
|
32
|
+
| 22 | LOW | **World→grid direction transform and uniform-scale extraction re-derived across the fluid effectors.** | Direction transform `m[0]*x + m[4]*y + m[8]*z` (etc.): `GlobalFluidEffector.js:99-101` and `:117-119`, `ImpulseFluidEffector.js:75-77`. Uniform scale `sqrt(m[0]²+m[1]²+m[2]²)`: `ImpulseFluidEffector.js:88`, `WakeFluidEffector.js:172` | `core/geom/vec3/` has `v3_matrix4_multiply` (point, with perspective divide) and `v3_matrix4_rotate_unit` (direction, **but it normalises the output**) — neither fits, because the effectors need the *unnormalised* linear part so world units/s² become grid units/s². So the re-derivation is caused by a genuine gap in the core surface. `v3_matrix4_rotate.js` is just a deprecated re-export of `v3_matrix4_rotate_unit`. | PROMOTION | Add `core/geom/vec3/v3_matrix4_transform_direction.js` (linear part, no normalise, no divide) and `m4_uniform_scale.js`. Three and two sites respectively. | Trivial effort. **Determinism: LOW.** Cold code (once per effector per frame), so this is a pure clarity/gap-closing win. |
|
|
33
|
+
|
|
34
|
+
## Dismissed leads
|
|
35
|
+
|
|
36
|
+
- **"Two competing two-bone IK solvers"** (`engine/ecs/ik/TwoBoneInverseKinematicsSolver.js` vs `engine/physics/inverse_kinematics/two_joint_ik.js`) — not duplicates. The former *imports and calls* the latter at `TwoBoneInverseKinematicsSolver.js:11,157`; it is the ECS/terrain wrapper, `two_joint_ik` is the pure analytic core. Correctly factored. The real finding in that pair of files is #8.
|
|
37
|
+
- **FABRIK vs `two_joint_ik`** — `engine/physics/inverse_kinematics/fabrik/fabrik3d_solve_primitive.js` is an iterative N-joint positional solver; `two_joint_ik` is an analytic 2-joint rotational solver on a different data model (Vector3/Quaternion objects vs flat Float32Array). No shared body. FABRIK has no internal callers, but meep is a library.
|
|
38
|
+
- **`GlobalFluidEffector.equals`/`hash` vs `ImpulseFluidEffector.equals`/`hash`** (brief §A, 118 and 71 tokens) — same *shape*, different *fields* (`drag`/`force`/`wind` vs `radius`/`position`/`force`). Unifying needs a field-list registry, i.e. exactly the string-keyed metaprogramming the house rules forbid. Correctly written out per class.
|
|
39
|
+
- **`v3_grid_patch_edges_uniform` vs `v3_grid_patch_edges_constant`** (brief §C, 0.75) — genuinely different operations (replicate the adjacent valid slab vs write a constant), and the constant version exploits `fill` contiguity the replication version cannot. Shared surface is a ~10-line dispatch preamble whose `|shift| >= res` branches *differ* (return vs full fill). Not worth unifying.
|
|
40
|
+
- **`v3_grid_compute_divergence` vs `v3_mac_compute_divergence`** — different discretisations (central differences with one-sided boundary handling on collocated cells vs exact face differences on the staggered lattice). Different algorithms, not a copy. Same for the two `subtract_pressure_gradient` kernels.
|
|
41
|
+
- **`Collider.setFlag/clearFlag/getFlag/writeFlag` vs `RigidBody.*`** (brief §E) — the same four one-line bodies appear in **32** engine classes. A codebase-wide idiom over a `this.flags` int, no shared state, one line each; a base class would impose an inheritance constraint on unrelated component hierarchies. Not a physics finding.
|
|
42
|
+
- **`reduce_manifold_contacts`** — already extracted by a previous review; `reduce_manifold_contacts.js:14-18` documents that box_box, convex_convex, box_triangle and the dispatcher "each previously carried its own byte-identical copy". `box_triangle_contact.js:146 reduce_contacts` is a 1-line binding, not a copy. Do not re-flag.
|
|
43
|
+
- **`mls-mpm` vs `fluid`** — `MLS_MPM.js` is 2D with quadratic B-spline P2G/G2P weights; the fluid stack is 3D trilinear on a MAC lattice. No shared indexing or weight code. It is also plainly a prototype (`Math.random()` at `:388-389`, per-step array allocation in `advance`, a `testMLS_MPM.js` harness beside it). No reuse opportunity.
|
|
44
|
+
- **"Second broadphase / AABB tree inside physics"** — none. `engine/physics/broadphase/generate_pairs.js:1` queries `core/bvh2`, and `compute_fat_world_aabb.js:1` uses `core/geom/3d/aabb/aabb3_transform_oriented`. Clean.
|
|
45
|
+
- **`solve_contacts.js:458 apply_impulse_to_pseudo` vs `:401 apply_impulse_to_body`** — same math, but the destination is a different buffer with a different stride and split lin/ang lanes. Unifying needs `(dst, lin_offset, ang_offset)` threading through the hottest routine in the solver for one caller's benefit. Borderline; folded into finding #1 only if that extraction happens anyway.
|
|
46
|
+
- **`SBS_AV_X/SBS_INV_I_X` block shared by `solve_constraints.js` and `solve_contacts.js`** (brief §E) — that is an import list of shared lane constants from `body/SolverBodyState.js`. Correct reuse, not duplication.
|
|
47
|
+
- **`convex_convex_manifold.js:243` vs `box_box_manifold.js:117` vs `box_triangle_contact.js:182` — the *projection* halves of `test_axis`** — legitimately shape-specific (symmetric half-extent projection vs three explicit vertex dots vs a vertex-buffer min/max loop). Only the tracking half is duplicated; that is finding #2.
|
|
48
|
+
- **The `g_bb_*` / `g_bt_*` / `g_ct_*` module-static accumulator idiom** appearing in three narrowphase files — each carries a comment explaining it replaced a per-call closure allocation ("D3"). Same *technique*, different state. Correct as-is.
|
|
49
|
+
|
|
50
|
+
## Notes
|
|
51
|
+
|
|
52
|
+
**Why duplication accumulates here, structurally:**
|
|
53
|
+
|
|
54
|
+
1. **Twin kernel families.** The fluid solver carries a full collocated (`v3_grid_*`) and a full staggered-MAC (`v3_mac_*`) implementation of advection. The MAC family superseded the collocated one (memory: "MAC staggered, exact projection; MacCormack velocity default") but the collocated family stayed as library surface — so every algorithm in it (SL trace, MacCormack, Selle limiter, solid pass-through) now exists twice, and the two copies have already begun to drift (findings 4, 5, 6, 7). This is the largest single source of duplication in the domain by line count.
|
|
55
|
+
|
|
56
|
+
2. **Core primitives that return parameters, not points.** `tri3_closest_point_barycentric` returns `(α, β)` and `line3_compute_segment_segment_closest_points` returns `(s, t)`. Sixteen call sites across physics, core and graphics then write the same reconstruction arithmetic. This is the highest-leverage class of finding (#14, #15) because the fix is additive — two new core functions, no behaviour change, and the existing exports stay.
|
|
57
|
+
|
|
58
|
+
3. **Partial prior extractions.** Three separate extractions were done and each missed one site: `reduce_manifold_contacts` left the surrounding emit epilogue behind (#11), `angular_effective_mass_raw` was adopted by `solve_contacts` and `RaycastVehicle` but not `solve_constraints` (#17), and `polygon2_clip_halfplane` was written in core but never consumed (#12). Grepping for "each previously carried its own copy"-style comments is a reliable way to find the residue.
|
|
59
|
+
|
|
60
|
+
4. **Axis triplication.** The MAC kernels and the effectors write x/y/z as three near-identical blocks with different strides. In the *kernels* (`v3_mac_advect_sl_velocity.js`, `v3_mac_advect_maccormack_velocity.js`) this is legitimate — see below. In the *effectors* it is not (#13), because those loops are radius-bounded, not grid-bounded.
|
|
61
|
+
|
|
62
|
+
**Where inlining is legitimately correct (do not "fix" these):**
|
|
63
|
+
|
|
64
|
+
- `v3_mac_clip_trace.js:25-29` is an explicit measured performance contract (8.6× on the SL kernel) and the three inline solid tests in `v3_mac_advect_sl_velocity.js` honour it. These run per *face* over the whole lattice — millions of evaluations per step. Finding #19 targets only the two redundant *function declarations*, and even that is gated on measurement.
|
|
65
|
+
- The x/y/z block triplication inside `v3_mac_advect_sl_velocity.js` and `v3_mac_advect_maccormack_velocity.js`: each block has different index strides (`sx*res_y`, `res_x*sy`, `cell_slice`) and different loop bounds (`<= res_x` on the own axis). Parameterising would put stride variables in the innermost loop of a whole-grid kernel. Correct as written; I did not flag it.
|
|
66
|
+
- `capsule_contacts.js` / `capsule_triangle_contact.js` / the manifold builders are **per-contact**, not per-cell — a few thousand calls per frame, not millions. Extracting 20-line leaf helpers there (#9, #10, #11) is squarely on the reuse side of the line, and the codebase already does exactly this (`reduce_manifold_contacts`, `angular_effective_mass_raw`).
|
|
67
|
+
- `solve_contacts` / `solve_constraints` inner rows are per-contact-row × per-iteration — the hottest code in the engine. Finding #1 is still worth doing because it is a *verbatim* duplicate of the most correctness-critical routine present, but it must be benchmarked (rain and KEVA-mini) and must preserve operand order exactly for `determinism.spec.js`.
|
|
68
|
+
|
|
69
|
+
**Determinism summary:** findings 2, 5 and 12 can change simulation output and need before/after numerical diffs. Findings 1, 4, 6, 7, 9, 10, 11, 14, 15, 17, 18 are order-preserving by construction *provided* the extracted body is copied verbatim rather than rewritten — that is the whole risk. Findings 16, 20, 21, 22 have no simulation-state exposure.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Domain F — engine/sound (legacy + sopra + acoustic simulation)
|
|
2
|
+
|
|
3
|
+
All paths relative to `H:/git/moh/app/src/mir-engine/meep/src/`. Every finding below was verified by
|
|
4
|
+
opening the cited files.
|
|
5
|
+
|
|
6
|
+
## Findings
|
|
7
|
+
|
|
8
|
+
| # | Severity | Concept | Sites | Evidence | Verdict | Proposed common path | Effort/Risk |
|
|
9
|
+
|---|---|---|---|---|---|---|---|
|
|
10
|
+
| 1 | HIGH | "Is this segment clear of occluders?" shadow-ray test written **3×**, each with its own module-static `Ray3` | `engine/sound/simulation/probe/probe_edge_visible.js:20` `probe_edge_visible`; `engine/sound/simulation/probe/probe_refine_connectivity.js:168` `segment_clear`; `engine/sound/simulation/probe/AcousticProbeField.js:328-345` (filter arrow in `nearestVisibleIndex`) | Brief C sim=0.75. All three do: zero-length ⇒ visible; `inv = 1/d`; `setOrigin/setDirection/tMax = d`; `!index.anyHit(RAY)`. `probe_refine_connectivity.js:167` comment literally says *"mirrors probe_edge_visible"*. `probe_edge_visible.js:8-9` claims it is *"the one visibility test shared by the offline bake and the runtime recheck, so they agree exactly"* — a claim the other two sites falsify | UNJUSTIFIED | `engine/sound/simulation/core/segment_clear.js` — `segment_clear(index, ax,ay,az, bx,by,bz)` with ONE scratch ray; `probe_edge_visible` becomes a 4-line probe-index wrapper; `nearestVisibleIndex`'s filter calls it directly | S / low |
|
|
11
|
+
| 2 | HIGH | Undirected edge-list → CSR (degree count → prefix sum → fill via write cursor) written twice; plus a hand-rolled sorted-row `isNeighbour` binary search | `engine/sound/simulation/probe/bakeVisibility.js:109-141` (weighted, → `CSRGraph.fromArrays`); `engine/sound/simulation/probe/probe_visibility_augment.js:50-70` (unweighted + per-row `.sort()`), with `isNeighbour` at `:167-182` | Both consume the same `{a,b}` edge arrays produced by stages A–D of the same bake. `core/graph/csr/` has `CSRGraph`, `csr_graph_find_edge`, `csr_graph_build_distance_field`, `csr_graph_find_shortest_path` — but **no builder**, so every caller re-derives one | PROMOTION | `core/graph/csr/csr_graph_from_undirected_edge_list.js(vertex_count, a, b, weights?, {sorted_rows})`; `isNeighbour` → `csr_graph_find_edge !== -1` (give the core one a sorted-row fast path) | M / low |
|
|
12
|
+
| 3 | HIGH | Listener resolution + `sopra.update(currentTime)` duplicated in two ECS systems that share ONE `SopraEngine` | `engine/sound/ecs/emitter/SoundEmitterSystem.js:137-158` `update`; `engine/sound/ecs/audio/AudioEmitterSystem.js:246-315` `update` | Both do `ecd.getAnyComponent(SoundListener)` → `getComponent(entity, Transform)` → `sopra.setListener(pos)` → `sopra.update(this.webAudioContext.currentTime)`. The engine is shared (`SoundEngine.obtainSopra`, `SoundEngine.js:82-88`), and BOTH class docs state coexistence is the design (`SoundEngine.js:76-78`, `AudioEmitterSystem.js:35-36`). Co-registering them double-runs `SopraEngine.update` (`sopra/SopraEngine.js:340-351`: snapshot + iterate all active instances + `#updateDuckers`). Harmless-but-wasted today because `EventInstance.#evaluate` guards on `entry.voice === null`; the copy-pasted "missing Transform" tolerance comment has already diverged (AudioEmitterSystem additionally skips `liveSet.refresh`) | UNJUSTIFIED | One owner for the tick: a `SopraTickSystem` (or fold listener-resolve + tick into `SoundListenerSystem`, which already owns the `SoundListener`+`Transform` pair). Emitter systems then only push emitter state | M / medium |
|
|
13
|
+
| 4 | HIGH | The container child-hash fold is copy-pasted into all 4 concrete containers instead of living on the base that owns `children` — and has already diverged | `engine/sound/sopra/definition/clip/SequenceContainerAudioClip.js:26-35`; `RandomContainerAudioClip.js:130-144`; `SwitchContainerAudioClip.js:83-96`; `BlendContainerAudioClip.js:126-139` (all `hash()`) | Brief A [70×2] + E. `ContainerAudioClip` owns `children` and DOES implement the parallel `equals()` fold (`ContainerAudioClip.js:80-99`) but has no `hash()`. Divergence already present: `BlendContainerAudioClip.hash()` folds `parameter`+`defaultValue` but **not** `blends`, though `equals()` compares them (`:117-121`) and the adapter serializes them — two blend containers differing only in curves hash identically. `EventDescription` polyphony buckets are keyed by event content, so clip hashes are load-bearing | UNJUSTIFIED | `ContainerAudioClip.hash()` = `super.hash()` folded with each child's `hash()`; each subclass folds only its own fields on top (and Blend adds `blends`) | S / low |
|
|
14
|
+
| 5 | MEDIUM | `AnimationCurve` binary keyframe codec hand-written twice | `engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js:24-33` + `:55-64`; `engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.js:20-33` + `:52-66` | Both carry the same excuse comment: *"AnimationCurve has no binary adapter of its own"*. Identical wire format (`uintVar` count, then `time/value/inTangent/outTangent` float32s). A payload change (wrap modes, tangent packing) must land in both or one silently mis-reads | PROMOTION | `engine/animation/curve/AnimationCurveSerializationAdapter.js` (or a `serialize_animation_curve` / `deserialize_animation_curve` pair beside `AnimationCurve.js`), registered so `this.objectAdapter` handles it | S / low |
|
|
15
|
+
| 6 | MEDIUM | 3-band decaying-noise IR synthesis kernel duplicated | `engine/sound/simulation/render/reverbImpulseResponse.js:26` `reverbImpulseResponse` (kernel `:33-72`); `engine/sound/simulation/render/foaReverbImpulseResponse.js:33` `foaReverbImpulseResponse` (kernel `:41-96`) | Brief E (4-line window). Identical: bin-length derivation from `decayBands × sampleRate`, one-pole coefficients `a = 1 − e^(−2πf/fs)`, the energy-preserving `low/mid/high` split, and the three `Math.pow(1 − i/len, decayPower)` envelopes. Only the write-out differs (omni sum vs direction-weighted B-format). Retuning the crossover or envelope must land in both | UNJUSTIFIED (the shared part is a per-buffer producer, not a per-sample call) | A `write_band_tails(outLow, outMid, outHigh, length, lenLow, lenMid, lenHigh, decayPower, sampleRate, random)` both consume; scalar sums, FOA weights by direction | M / medium (numeric output must stay bit-identical — the specs pin it) |
|
|
16
|
+
| 7 | MEDIUM | A **third** decaying-noise IR generator, unseeded | `engine/sound/sopra/definition/effect/ReverbEffect.js:45-66` `build` | Brief F `raw_math_random`: `data[i] = (Math.random() * 2 - 1) * Math.pow(1 - t, this.decayPower)` — the single-band case of finding 6. `reverbImpulseResponse.js:18` documents *"with three equal bands this reduces to the plain scalar tail"*, i.e. it already subsumes this. Divergence with teeth: the acoustic path is explicitly seeded (`ProbeReverbRenderer.js:97`, `FoaProbeReverbRenderer.js:161` → `seededRandom(1)`), so a sopra bus reverb is the only non-reproducible IR in the engine | UNJUSTIFIED | `ReverbEffect.build` → `reverbImpulseResponse(ctx, [d,d,d], this.decayPower, this.#random)` with a seeded RNG field | S / low |
|
|
17
|
+
| 8 | MEDIUM | Probe-reverb renderer scaffolding duplicated, with a divergent re-bake trigger | `engine/sound/simulation/render/ProbeReverbRenderer.js:108-136` (ctor) + `:192-230` `setBands`; `engine/sound/simulation/render/FoaProbeReverbRenderer.js:172-236` (ctor) + `:296-333` `#rebake` | Same double-buffered convolver bank, same `#active` flip, same `rampGain(..., FADE_EPSILON, crossfadeSeconds, ctx)` crossfade, same `max(bands) >= minDecay` audibility gate, same `setProbeField`/`input`/`output` surface. But the scalar re-bakes on per-band hysteresis (`changeThreshold`, `:198-208`) while the FOA re-bakes on nearest-probe-index change plus a `#dirty` force-flag (`:123-131`, `:257-260`) that the scalar **lacks** — a `setProbeField` swap between two fields whose bands agree at the listener leaves the scalar renderer's IR stale | UNJUSTIFIED for the crossfade/gate scaffolding (JUSTIFIED for the differing WebAudio graph shape) | Extract an N-channel `ConvolverCrossfadeBank` (build, `swapTo(buffers[])`, `mute()`) both renderers own; give both the same `#dirty` semantics | M / medium |
|
|
18
|
+
| 9 | MEDIUM | "Randomly orient a Fibonacci sample set" written twice, bypassing `random_point_on_sphere` | `engine/sound/simulation/core/OcclusionSolver.js:43-51` (in `solve`); `engine/sound/simulation/probe/bakeReverbBands.js:69-74` | Identical four lines: `z = 2·random()−1`, `azimuth = 2π·random()`, `ring = sqrt(max(0, 1−z²))`, then `v3_orthonormal_matrix_from_normal(ROTATION, 0, ring·cos, ring·sin, z)`. `core/geom/random/random_point_on_sphere.js` already produces the uniform direction | UNJUSTIFIED | `core/geom/random/random_orthonormal_basis(out9, out_offset, random)` beside `v3_orthonormal_matrix_from_normal`; at minimum call `random_point_on_sphere` for the direction | S / low |
|
|
19
|
+
| 10 | MEDIUM | Canonical undirected edge key `min·n + max` written **4×** (and decoded in a 5th place) | `engine/sound/simulation/probe/ProbeVisibilityRecheck.js:72-75` `#edgeId` (decode at `:112-113`); `engine/sound/simulation/probe/probe_densify_portals.js:141-143` `edgeKey`; `engine/sound/simulation/probe/probe_visibility_bridge_components.js:84-86` (inline `rlo·n + rhi`); `engine/sound/simulation/probe/probe_visibility_augment.js:147` (inline `u·n + v`, correctness relies on the caller's `u<v` canonicalisation at `:96`) | A bug-prone invariant (must use the probe count as the radix AND canonicalise the pair) replicated across four modules of one bake pipeline; changing the radix (e.g. for >65k probes packing) silently breaks the ones you miss | UNJUSTIFIED | `engine/sound/simulation/probe/probe_edge_key.js` — `probe_edge_key(a, b, n)` + `probe_edge_decode(key, n, out2)` | S / low |
|
|
20
|
+
| 11 | MEDIUM | `SoundControllerSystem` ≡ `AnimationControllerSystem` — the whole "component.rules → per-entity event-listener registration + teardown" ECS lifecycle | `engine/sound/ecs/SoundControllerSystem.js:49-101` `link` / `:103-113` `unlink`; `engine/graphics/ecs/animation/AnimationControllerSystem.js:18-62` `link` / `:64-72` `unlink` | Brief A [59×2] (`unlink` is token-identical) + E. Both hold `this.removers = []`, build `callbacks = this.removers[entity] = []`, register `{name, handler}` pairs for `rule.startEvent`/`rule.stopEvent` via `dataset.addEntityEventListener`, and tear down identically. Only the two handler bodies differ | UNJUSTIFIED (cross-domain, reported here per instructions) | A typed `EntityEventRuleBinding` value (owns the `{name, handler}` list, `bind(dataset, entity, name, handler)` / `release(dataset, entity)`) that both systems compose — no string-keyed registry involved | S / low |
|
|
21
|
+
| 11b | LOW | `randomFromArray` re-derived inline | `engine/sound/ecs/SoundControllerSystem.js:13-24` `pickTackFromRule` | Brief F `raw_math_random`: `Math.floor(Math.random() * tracks.length)`. `core/math/random/randomFromArray.js` exists and is used correctly two directories away in `engine/sound/material/concrete/RandomSoundMaterial.js:17` | UNJUSTIFIED | `randomFromArray(Math.random, rule.tracks)` | XS / none |
|
|
22
|
+
| 12 | MEDIUM | Emitter → `sopra.playEvent(...)` option mapping duplicated; drives a 9-positional-argument API | `engine/sound/ecs/audio/AudioEmitterSystem.js:209-233` `#play`; `engine/sound/ecs/audio/LiveEmitterSet.js:281-323` `#promote` (fed by `LiveEmitterSet.add` at `:158-163`, called with 9 positional args at `AudioEmitterSystem.js:139`) | Both derive `oneShot = !event.rootClip.loops()`, build the same `{busId, position, oneShot, acoustic, pathing, sourceRadius, orientation, directivity}` bag, then re-apply `setGainDb`. `LiveEmitterSet`'s record type (`:77`) is a hand-mirrored copy of that bag. Every new per-source acoustic property (`orientation`, `directivity` were the last two) must be threaded through the record, the 9-arg `add` signature, `#promote`, AND `#play` | UNJUSTIFIED | One typed `AudioEmitterPlaySpec` built from `(emitter, transform, acousticSim)`; `LiveEmitterSet.add(entity, spec)` stores it, `#promote` and `#play` both hand it to `playEvent` | M / low |
|
|
23
|
+
| 13 | MEDIUM | Three hand-written "dynamic BVH of handle-addressed objects" wrappers inside one domain | `engine/sound/simulation/core/AcousticOccluderIndex.js:40-122`; `engine/sound/simulation/core/AcousticRegionIndex.js:23-134`; `engine/sound/ecs/audio/SpatialAudioIndex.js:24-138` | Identical skeleton in all three: `#bvh = new BVH()`, a parallel `BvhClient` store, retire-a-slot-by-null, a `#fitLeaf(client, x)` that ends in `client.resize(x0…z1)`, a reused `#candidates` gather buffer, and a query that maps returned handles back to user objects. Only the leaf-bounds policy and the refit trigger (reactive `onChanged` / per-frame `refit()` / explicit `setTransform`) differ | UNJUSTIFIED | `core/bvh2/bvh3/BvhObjectIndex.js` — typed container holding `T[]` + clients, with `computeBounds(out, item)` supplied by the owner; the three sound indices become thin policy classes | M / medium |
|
|
24
|
+
| 13b | LOW | AABB union re-derived | `engine/sound/simulation/core/AcousticOccluderIndex.js:252-279` `computeWorldBounds` | Hand-written 6-component min/max fold; `core/geom/3d/aabb/aabb3_array_combine.js` does exactly this | UNJUSTIFIED | `aabb3_array_combine` | XS / none |
|
|
25
|
+
| 14 | MEDIUM | Trilinear sampling of a Float32Array 3D scalar grid re-implemented | `engine/sound/simulation/probe/probe_sdf_grid.js:104-122` `grid_sample_trilinear` (+ `clampInt` at `:181-183`) | `engine/graphics/texture/3d/scs3d_sample_linear.js:15` is the same function: same `z·(rx·ry) + y·rx + x` layout, same edge clamping, same trilinear blend. `SingleChannelSampler3D` is the matching container. `probe_sdf_grid.js:1` already imports `clamp` yet still hand-rolls `clampInt` | UNJUSTIFIED | Back `SdfGrid` with `SingleChannelSampler3D` and call `scs3d_sample_linear`; delete `clampInt` | S / low |
|
|
26
|
+
| 15 | LOW | `{type: ctor.typeName, data: x.toJSON()}` wrapper written 3× with two different `constructor` lookups | `engine/sound/material/concrete/json/serializeSoundMaterialToJSON.js:1-6` (`material.constructor.typeName`); `engine/sound/sopra/serialization/sopraJSON.js:22-27` `serializeAudioClipToJSON` and `:68-73` `serializeAudioEffectToJSON` (`Object.getPrototypeOf(x).constructor.typeName`) | Brief A [70×2], C sim=1. The **switch-based deserializers** (`deserializeSoundMaterialFromJSON.js:12`, `sopraJSON.js:33`, `sopraJSON.js:79`) are **JUSTIFIED** — the alternative in core (`core/json/abstractJSONDeserializer.js`) is precisely the string-keyed `Map<string, fn>` registry the house rules reject, and `sopraJSON.js:11-16` documents deliberate call-time class binding to survive an import cycle. Only the three one-line serializers are gratuitous, and they disagree on how to reach the constructor | UNJUSTIFIED (serializers only) | One `type_tagged_to_json(value)` helper (typed, no registry); pick one constructor lookup | XS / none |
|
|
27
|
+
| 16 | LOW | `dB2Volume` re-derived inline in a file that already imports it | `engine/sound/sopra/runtime/EventInstance.js:33-35` `dbTiltToGain` → `Math.pow(10, -excessDb / 20)`; `dB2Volume` imported at `:2` | Called 4× per acoustic instance per frame (`:542-543`, `:561-562`) — not per-sample, so no inlining justification | UNJUSTIFIED | `excessDb <= 0 ? 1 : dB2Volume(-excessDb)` | XS / none |
|
|
28
|
+
| 17 | LOW | `BusGraph` repeats the same lookup-or-throw preamble 6× | `engine/sound/sopra/runtime/BusGraph.js:143` `getInput`, `:159` `getOutput`, `:173` `getVolume`, `:187` `setVolume`, `:214` `rampVolume`, `:232` `approachVolume` | Six copies of `const bus = this.buses.get(id); if (bus === undefined) { throw new Error(\`sopra: no bus '${id}'\`); }` — ~30 lines | UNJUSTIFIED | A `#bus(id)` private that does the lookup + throw | XS / none |
|
|
29
|
+
| 18 | LOW / PROMOTION | The `setFlag`/`clearFlag`/`writeFlag`/`getFlag` quad, byte-identical in **35 files** | In this domain: `engine/sound/ecs/emitter/SoundEmitter.js:156-189`; `engine/sound/ecs/emitter/SoundTrack.js:138-171`; `engine/sound/asset/SoundAssetPlaybackSpec.js`; `engine/ecs/speaker/Voice.js:13-46` | Brief E (30-file window). `grep -l "clearFlag(flag) {"` returns 35 files spanning core/, engine/ecs, graphics, physics, sound, view | PROMOTION (codebase-wide, not sound-specific — flagged for the cross-domain roll-up, not for a sound-local fix) | A `FlagCarrier` base (or a shared `flags_set/clear/write/read` quartet on the flat `flags` word); JS single inheritance makes the base awkward where a class already extends, so the function quartet is the safer shape | L / medium |
|
|
30
|
+
| 19 | LOW | Well-conditioned perpendicular-axis construction re-derived | `engine/sound/simulation/probe/probe_detect_portals.js:170-184` `throatIsAperture` | Hand-rolls "cross with world-up, swap reference if `|ey| > 0.9`, normalize". `core/geom/vec3/v3_orthonormal_matrix_from_normal.js` builds exactly this stable basis and is already used twice in this same package (`OcclusionSolver.js:48`, `bakeReverbBands.js:74`) | UNJUSTIFIED | `v3_orthonormal_matrix_from_normal` and read the tangent column | XS / low |
|
|
31
|
+
| 20 | LOW | Vector normalize written by hand, inconsistently with the same file | `engine/sound/simulation/AcousticSimulator.js:598-621` `#directivityGainToward` (`Math.sqrt(dx*dx+dy*dy+dz*dz)` at `:605`) vs `:454, :455, :459, :460` in `#applyPath` (`Math.hypot`) | Brief F `inline_length3`. Cold path (once per non-omni source per frame). `v3_length` / `v3_normalize_array` exist; the file already imports `line3_compute_segment_closest_point_t` from the same family | UNJUSTIFIED (cold) | `v3_length`, or at least pick one of `Math.hypot`/`sqrt` within the file | XS / none |
|
|
32
|
+
| 21 | LOW / PROMOTION | Union-find connected components over probe connectivity written twice | `engine/sound/simulation/probe/probe_visibility_bridge_components.js:35-39` (from an `{a,b}` edge list); `engine/sound/simulation/probe/probe_refine_connectivity.js:50-56` (from a `CSRGraph`) | Both then use `uf_find(parent, i) === uf_find(parent, j)` as the same-component predicate. `core/graph/graph_compute_disconnected_clusters.js` exists but only for the object-graph `Graph`, not `CSRGraph` | PROMOTION | `core/graph/csr/csr_graph_connected_components(parent, graph)` beside `csr_graph_build_distance_field`; the edge-list variant folds into finding 2's builder | S / low |
|
|
33
|
+
|
|
34
|
+
## Strangler-migration debt
|
|
35
|
+
|
|
36
|
+
Context, not a defect list. Legacy-vs-sopra parallelism is the deliberate migration shape; what matters is
|
|
37
|
+
where the seam has stalled and where a fix would have to land twice.
|
|
38
|
+
|
|
39
|
+
| Legacy | sopra / native counterpart | Is legacy still wired? | Note |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| `ecs/emitter/SoundEmitterSystem` + `SoundEmitterComponentContext` | `ecs/audio/AudioEmitterSystem` + `AudioEmitter` + `LiveEmitterSet` | **YES** — `mir-engine/model/game/makeMirEngineConfig.js:182` | The seam is genuinely healthy here: legacy is a *translator* (`sopra/legacy/soundEmitterToEventDescription.js`) onto sopra, not a second renderer. Conversely `AudioEmitter*` has **zero** consumers outside `engine/sound/` — the new path is unused in the game |
|
|
42
|
+
| `ecs/SoundControllerSystem` | none — its own JSDoc names a *"forthcoming `AudioEventTrigger`"* (`SoundControllerSystem.js:27-30`) | **YES** — `makeMirEngineConfig.js:183` | `@deprecated` and still registered with no replacement in the tree. Findings 11/11b live here |
|
|
43
|
+
| `material/**` (`AbstractSoundMaterialDefinition`, `Single`/`Random`/`Composition`, `json/`) | sopra clips — `SwitchContainerAudioClip.js:11` *"Supersedes the dormant material/ surface-switch concept"*; `BlendContainerAudioClip.js:13` *"Supersedes the dormant material/ composition concept"* | **NO** — grep finds zero references outside `engine/sound/material/` (only a doc mention in `sopraJSON.js:13`) | Dead but public surface (meep is a library). Origin of the JSON copies in finding 15 |
|
|
44
|
+
| `SoundEmitterChannels` (`effects`/`music`/`ambient`) | sopra `SopraDefaultBus` + `BusDefinition` | **YES** — 7 app files import it (UnitMaker, StoryManager, WorldEventManager, …) | Already proxied 1:1: `SoundEmitterSystem.get/setChannelVolume` (`:91-102`) forwards to `sopra.get/setBusVolume` |
|
|
45
|
+
| `ecs/SoundListenerSystem` (WebAudio `AudioListener` pose) | `sopra.setListener(position)` inside the emitter systems | **YES** — `makeMirEngineConfig.js:184` | THREE readers of the same `SoundListener`→`Transform` relation. sopra's listener is position-only; the WebAudio listener also carries orientation. See finding 3 |
|
|
46
|
+
| `ProbeReverbRenderer` (scalar) | `FoaProbeReverbRenderer` (directional/B-format) | scalar is what `configureAcousticSimulation.js:31` documents; FOA is referenced only by `prototypeAcousticSolver.js:515` + its spec | Neither is deprecated; both live. Findings 6/8 mean an IR or crossfade fix must land in both |
|
|
47
|
+
| `ecs/emitter/SoundPanningModelType` | `sopra/definition/SopraPanningModel` | **YES** — translated by `soundEmitterToEventDescription.js:57-61` | Identical 2-value enums; translation is one ternary. Fine |
|
|
48
|
+
| `SoundAttenuationFunction` + `core/math/physics/irradiance/interpolate_irradiance_*` | authored `AnimationCurve` via `sopra/util/buildAttenuationCurve.js` | **YES** | This is the *good* seam: the legacy falloffs are REUSED to sample the new curve rather than reimplemented. Called out as a model, not debt |
|
|
49
|
+
|
|
50
|
+
## Dismissed leads
|
|
51
|
+
|
|
52
|
+
- Brief A/C `makePhysicsGroundResolver` / `recomputeMove` / `bindMoveKey` / `buildInputBindings` / `logHelp` (`engine/control/first-person/prototype_first_person_controller.js` ⟷ `engine/sound/simulation/prototypeAcousticSolver.js`) — both sites are `prototype*.js` harnesses, out of scope. **One line as instructed:** a real shared debug fly/walk camera *with* a physics ground resolver would be worth promoting to `engine/control/`, since it is now copied verbatim for every new subsystem demo.
|
|
53
|
+
- `DuckingRuleSerializationAdapter` / `CompressorEffectSerializationAdapter` / `ParameterDefinitionSerializationAdapter` `serialize`+`deserialize` (brief A [50×3] and [45×3]) — read all three: different field names and types (`writeUTF8String`+3×float32+float32 / 5×float32 / string+3×float32+uint8), zero shared logic. Exactly the per-class shape boilerplate the house rules bless.
|
|
54
|
+
- `SequenceContainerAudioClipSerializationAdapter.deserialize` ⟷ `SwitchContainerAudioClipSerializationAdapter.deserialize` (brief C sim=0.781) — same verdict; Switch additionally reads `parameter`/`defaultValue`.
|
|
55
|
+
- The container adapters' repeated `gain`/`pitch` writes and children loop (brief E) — wire order is per-class protocol; hoisting it into a base couples four independent binary formats. Deliberate.
|
|
56
|
+
- `MixerSnapshotSerializationAdapter` bus-entry codec vs `BusDefinitionSerializationAdapter` sends codec — structurally identical (`string` + `float32` pairs), semantically distinct record types. Shape only.
|
|
57
|
+
- `engine/ecs/speaker/Voice.js` vs `engine/sound/sopra/runtime/Voice.js` (brief G) — pure name collision. The first is a speech-bubble ECS component (flags only, `Voice.serializable = false`); the second is a WebAudio `AudioBufferSourceNode` holder. No shared logic beyond the flag quad (finding 18). The `speaker/` package has no importers, but meep is a library — not a defect.
|
|
58
|
+
- `echogram_band_rt60.js:49` `10 * Math.log10(edc / total)` — **energy** dB, deliberately not `volume2dB`'s amplitude `20·log10`. Correct as written.
|
|
59
|
+
- Brief F `inline_lerp` in `probe_sdf_grid.js:118-121` — the innermost trilinear blend; folds into finding 14 anyway. `AcousticSimulator.js:453, :458` and `probe_detect_portals.js:94, :109` are 3-component point-on-segment evaluations, not scalar `lerp` calls.
|
|
60
|
+
- Brief F `inline_length3`, the remaining ~14 hits — hot per-ray / per-edge / per-candidate-pair loops (`OcclusionSolver.js:79` per ray, `bakeVisibility.js:106` per graph edge, `probe_visibility_augment.js:92,108,111` per 2-ring chord). Inlining is the right call there; only the cold `AcousticSimulator` site (finding 20) is worth changing.
|
|
61
|
+
- Brief F `epsilon_compare` `ProbeReverbRenderer.js:198-200` — a tunable re-bake hysteresis (`changeThreshold = 0.1 s`), not float-tolerance equality. `epsilonEquals` would be semantically wrong.
|
|
62
|
+
- Brief F `binary_search_inline` `probe_visibility_augment.js:171` — folded into finding 2.
|
|
63
|
+
- `EventInstance.js:47-92` `build3BandCrossover` / `set3BandGains` / `disconnect3Band` — already correctly factored and shared by BOTH the direct and corner-leak paths (`:457`, `:476`). No duplication; a model for finding 8.
|
|
64
|
+
- `ProbeFieldPathingSolution.js:62` correctly delegates to `csr_graph_build_distance_field`, and `AcousticProbeField.shortestPath:519` to `csr_graph_find_shortest_path`. **No Dijkstra / graph-search duplication in `simulation/`** — the probe pathing is a clean `core/graph` consumer.
|
|
65
|
+
- No hand-rolled biquad coefficient math, equal-power panning law, or crossfade curve anywhere in the domain — WebAudio nodes (`createBiquadFilter`, `PannerNode`, `linearRampToValueAtTime`) do all of it. The one filter-adjacent formula, the one-pole coefficient `1 − e^(−2πf/fs)`, is finding 6.
|
|
66
|
+
- The 3-band value classes (`AcousticMedium`, `AcousticMaterial`, `AcousticSolution`, `AcousticSourceState`) unroll their 3-element copies by hand in `equals`/`clone`/`hash`/`toJSON` — typed, explicit, per-field. Consistent with "typed over magic"; not duplication of *logic*.
|
|
67
|
+
|
|
68
|
+
## Notes — why duplication accumulates here
|
|
69
|
+
|
|
70
|
+
1. **Three generations coexist in one package.** `material/` (dead, superseded), `ecs/emitter/` (legacy, wired,
|
|
71
|
+
now a translator), and `sopra/` + `ecs/audio/` (new, unwired outside the engine). Each generation
|
|
72
|
+
re-solved the same problems — polymorphic type-tagged JSON, weighted random selection, composition
|
|
73
|
+
with per-child weights — because the previous one was not being deleted. The three JSON dispatchers
|
|
74
|
+
(finding 15) and the material-vs-clip parallel hierarchy are the visible residue.
|
|
75
|
+
|
|
76
|
+
2. **The migration seam stalled at the game boundary, not inside the engine.** Inside `engine/sound/`
|
|
77
|
+
the strangler is genuinely done — sopra is the single renderer and legacy is a translator. But
|
|
78
|
+
`makeMirEngineConfig.js` still registers the three legacy systems and nothing registers
|
|
79
|
+
`AudioEmitterSystem`, so the new path's code has no production caller keeping it honest. That is
|
|
80
|
+
what let finding 3 (two systems each owning the sopra tick) and finding 12 (two copies of the
|
|
81
|
+
play-options mapping) survive: only one of each pair actually runs.
|
|
82
|
+
|
|
83
|
+
3. **`core/graph/csr/` ships consumers but no producer.** It has Dijkstra, distance fields, and edge
|
|
84
|
+
lookup, but no `from_edge_list` builder — so the probe bake writes its own twice (finding 2), and the
|
|
85
|
+
companions it also needs (connected components, canonical edge key) got written locally 2–4× each
|
|
86
|
+
(findings 10, 21). One missing primitive generated four findings.
|
|
87
|
+
|
|
88
|
+
4. **Ray-scratch locality beats reuse.** Every module that casts a shadow ray declares its own
|
|
89
|
+
module-static `Ray3` and inlines the normalize-and-set sequence (finding 1). The pattern is
|
|
90
|
+
allocation-free by construction, which makes the copy feel cheap, but it means the "one shared
|
|
91
|
+
visibility test" the code documents does not actually exist.
|
|
92
|
+
|
|
93
|
+
5. **Per-sample DSP genuinely justifies inlining — but the justification got over-applied.** The IR
|
|
94
|
+
synthesis kernels (findings 6, 7) share far more than the inner loop: window sizing, filter
|
|
95
|
+
coefficients, and envelope shape are all per-*bake* work that could be shared without a per-sample
|
|
96
|
+
call. The `EventInstance` crossover helpers show the team already knows how to draw that line
|
|
97
|
+
correctly; the reverb bakers just predate it.
|
|
98
|
+
|
|
99
|
+
6. **The base class stops one level short.** `AbstractAudioClip` folds `gain`/`pitch` into `hash()`, and
|
|
100
|
+
`ContainerAudioClip` folds `children` into `equals()` — but not into `hash()`. Four subclasses filled
|
|
101
|
+
the gap by copy-paste, and one of them (Blend) has already drifted (finding 4). The same shape
|
|
102
|
+
appears in the container adapters, where the base clip fields are re-written per subclass.
|