@woosh/meep-engine 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1869) hide show
  1. package/LICENSE +188 -188
  2. package/README.md +152 -152
  3. package/editor/Editor.js +598 -598
  4. package/editor/EditorCameraNavigation.js +205 -205
  5. package/editor/EditorKeyMap.js +182 -182
  6. package/editor/SelectionVisualizer.js +127 -127
  7. package/editor/actions/concrete/ActionInvalidateSampler.js +27 -27
  8. package/editor/actions/concrete/PatchTerrainHeightAction.js +85 -85
  9. package/editor/camera/camera_framing.js +104 -104
  10. package/editor/clipboard.js +184 -184
  11. package/editor/ecs/component/editors/ObservedIntegerEditor.js +22 -22
  12. package/editor/ecs/component/editors/ObservedStringEditor.js +34 -34
  13. package/editor/ecs/component/editors/common/two_way_sync.js +50 -50
  14. package/editor/ecs/component/editors/ecs/GridObstacleEditor.js +19 -19
  15. package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.js +35 -35
  16. package/editor/ecs/component/editors/ecs/ParameterTrackEditor.js +17 -17
  17. package/editor/ecs/component/editors/ecs/SGMeshEditor.js +24 -24
  18. package/editor/ecs/component/editors/ecs/ShadedGeometryEditor.js +33 -33
  19. package/editor/ecs/component/editors/ecs/Trail3DEditor.js +45 -45
  20. package/editor/ecs/component/editors/ecs/buildGridObstaclePreview.js +19 -19
  21. package/editor/ecs/component/editors/ecs/terrain/TerrainEditor.js +72 -72
  22. package/editor/ecs/component/editors/shade/MeshletGeometryEditor.js +30 -30
  23. package/editor/ecs/component/editors/shade/ShadeImageEditor.js +70 -70
  24. package/editor/ecs/component/editors/shade/ShadeMaterialEditor.js +34 -34
  25. package/editor/ecs/component/editors/shade/ShadeTextureEditor.js +46 -46
  26. package/editor/ecs/component/editors/shade/StandardShadeMaterialEditor.js +24 -24
  27. package/editor/ecs/component/prototypeObjectEditor.js +265 -265
  28. package/editor/ecs/component/registerBasicTypeEditors.js +100 -100
  29. package/editor/ecs/component/registerEngineComponentEditors.js +95 -95
  30. package/editor/entity_world_bounds.js +27 -27
  31. package/editor/particles/lut_ops.js +148 -148
  32. package/editor/particles/particleEditEvents.js +12 -12
  33. package/editor/particles/rebuildParticleEmitter.js +31 -31
  34. package/editor/persistence/EditorLayoutStore.js +71 -71
  35. package/editor/persistence/SceneMetadata.js +52 -52
  36. package/editor/persistence/SceneSlotManager.js +144 -144
  37. package/editor/persistence/buildNewSceneDataset.js +67 -67
  38. package/editor/persistence/files.js +68 -68
  39. package/editor/persistence/scene_import.js +143 -143
  40. package/editor/persistence/scene_serialization.js +79 -79
  41. package/editor/persistence/string_buffer_codec.js +41 -41
  42. package/editor/process/GridDisplayProcess.js +125 -125
  43. package/editor/process/SymbolicDisplayProcess.js +106 -108
  44. package/editor/process/symbolic/CameraSymbolicDisplay.js +146 -146
  45. package/editor/process/symbolic/LightSymbolicDisplay.js +172 -172
  46. package/editor/process/symbolic/PathSymbolicDisplay.js +118 -118
  47. package/editor/process/symbolic/PositionedMarkerSymbolicDisplay.js +79 -79
  48. package/editor/process/symbolic/SoundEmitterSymbolicDisplay.js +57 -57
  49. package/editor/process/symbolic/SymbolicDisplay.js +62 -62
  50. package/editor/process/symbolic/gizmo_draw_wire_box_oriented.js +85 -85
  51. package/editor/process/symbolic/gizmo_draw_wire_circle.js +53 -53
  52. package/editor/prototypeEditorShell.js +156 -156
  53. package/editor/selection/editor_pick.js +130 -130
  54. package/editor/selection/screen_rect_frustum.js +119 -119
  55. package/editor/style/controls.scss +168 -168
  56. package/editor/style/inspector.scss +223 -223
  57. package/editor/templates/entity_templates.js +167 -167
  58. package/editor/tools/GridPaintTool.js +273 -273
  59. package/editor/tools/SelectionTool.js +307 -307
  60. package/editor/tools/TransformToolV2.js +378 -378
  61. package/editor/tools/engine/ToolEngine.js +188 -188
  62. package/editor/tools/paint/TerrainTexturePaintTool.js +250 -250
  63. package/editor/tools/v2/BlenderCameraOrientationGizmo.js +501 -501
  64. package/editor/tools/v2/TransformControls.js +971 -971
  65. package/editor/tools/v2/handle/gizmo_drag_plane.js +141 -141
  66. package/editor/tools/v2/handle/gizmo_draw.js +399 -399
  67. package/editor/tools/v2/handle/gizmo_handles.js +364 -364
  68. package/editor/tools/v2/handle/gizmo_ray_shapes.js +239 -239
  69. package/editor/view/ecs/HierarchicalEntityListView.js +339 -339
  70. package/editor/view/ecs/components/DatGuiController.js +2 -2
  71. package/editor/view/ecs/components/common/AutoCanvasView.js +100 -100
  72. package/editor/view/ecs/components/common/TextController.js +59 -59
  73. package/editor/view/library/MeshLibraryView.js +175 -175
  74. package/editor/view/library/mesh_library_drag.js +63 -63
  75. package/editor/view/library/model_thumbnail_source.js +86 -86
  76. package/editor/view/makeEntityDecorators.js +125 -125
  77. package/editor/view/node-graph/NodeGraphSelection.js +89 -89
  78. package/editor/view/node-graph/NodeView.js +211 -211
  79. package/editor/view/node-graph/actions/ConnectionDeleteAction.js +36 -36
  80. package/editor/view/node-graph/actions/NodesMoveAction.js +41 -41
  81. package/editor/view/node-graph/connection_wire_geometry.js +107 -107
  82. package/editor/view/particles/ColorLutGradientView.js +291 -291
  83. package/editor/view/particles/ScalarLutCurveView.js +279 -279
  84. package/editor/view/prepareMeshLibrary.js +188 -188
  85. package/editor/view/shell/SceneTreePanelView.js +119 -119
  86. package/editor/view/v2/SplitView.js +2 -2
  87. package/package.json +94 -94
  88. package/samples/generation/generators/interactive/mir_generator_place_buff_objects.js +362 -362
  89. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +738 -738
  90. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +79 -79
  91. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +93 -93
  92. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +80 -80
  93. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +164 -164
  94. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +69 -69
  95. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +102 -102
  96. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +129 -129
  97. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +72 -72
  98. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +74 -74
  99. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +169 -169
  100. package/src/DUPLICATION_AUDIT_2026_08_07.md +254 -254
  101. package/src/DUPLICATION_AUDIT_REMEDIATION.md +250 -250
  102. package/src/REVIEW_2026_08_06/adv-core.md +511 -511
  103. package/src/REVIEW_2026_08_06/adv-gfx.md +480 -480
  104. package/src/REVIEW_2026_08_06/adv-view.md +446 -446
  105. package/src/REVIEW_2026_08_06/api-core.md +296 -296
  106. package/src/REVIEW_2026_08_06/api-engine.md +195 -195
  107. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -507
  108. package/src/REVIEW_2026_08_06/core-binary.md +348 -348
  109. package/src/REVIEW_2026_08_06/core-collection.md +376 -376
  110. package/src/REVIEW_2026_08_06/core-math.md +221 -221
  111. package/src/REVIEW_2026_08_06/core-process.md +266 -266
  112. package/src/REVIEW_2026_08_06/core-science.md +223 -223
  113. package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -279
  114. package/src/REVIEW_2026_08_06/engine-animation.md +167 -167
  115. package/src/REVIEW_2026_08_06/engine-asset.md +166 -166
  116. package/src/REVIEW_2026_08_06/engine-ecs.md +920 -920
  117. package/src/REVIEW_2026_08_06/engine-input.md +197 -197
  118. package/src/REVIEW_2026_08_06/engine-misc.md +248 -248
  119. package/src/REVIEW_2026_08_06/engine-terrain.md +52 -52
  120. package/src/REVIEW_2026_08_06/generation.md +176 -176
  121. package/src/REVIEW_2026_08_06/geom-2d.md +434 -434
  122. package/src/REVIEW_2026_08_06/geom-3d.md +117 -117
  123. package/src/REVIEW_2026_08_06/gfx-gi.md +143 -143
  124. package/src/REVIEW_2026_08_06/gfx-render.md +740 -740
  125. package/src/REVIEW_2026_08_06/gfx-texture.md +690 -690
  126. package/src/REVIEW_2026_08_06/prior-criticals.md +49 -49
  127. package/src/REVIEW_2026_08_06/prior-highs.md +138 -138
  128. package/src/REVIEW_2026_08_06/test-gaps.md +364 -364
  129. package/src/REVIEW_2026_08_06/view.md +667 -667
  130. package/src/REVIEW_2026_08_06.md +610 -610
  131. package/src/avif/codec/dec/avif_dec.cpp +233 -233
  132. package/src/avif/codec/dec/avif_dec.js +16 -16
  133. package/src/avif/encoder_worker.js +5 -18
  134. package/src/core/binary/BitImage2.js +246 -246
  135. package/src/core/binary/align_32.js +21 -21
  136. package/src/core/binary/data_view/DataType2DataViewReaders.js +23 -23
  137. package/src/core/binary/data_view/DataType2DataViewWriters.js +25 -25
  138. package/src/core/binary/hash/XXH128Hash.js +55 -55
  139. package/src/core/binary/hash/XXH3_SECRET.js +25 -25
  140. package/src/core/binary/hash/xxh3_128.js +341 -341
  141. package/src/core/binary/hash/xxh3_64.js +241 -241
  142. package/src/core/binary/hash/xxh3_common.js +476 -476
  143. package/src/core/binary/lz4/LZ4_MAX_INPUT_SIZE.js +9 -9
  144. package/src/core/binary/lz4/lz4_compress_block.js +385 -385
  145. package/src/core/binary/lz4/lz4_compress_bound.js +25 -25
  146. package/src/core/binary/lz4/lz4_decompress_block.js +139 -139
  147. package/src/core/binary/lz4/lz4_decompress_bound.js +30 -30
  148. package/src/core/binary/meshopt/MESHOPT_GROUP_SIZE.js +18 -18
  149. package/src/core/binary/meshopt/MeshoptCompressionFilter.js +30 -30
  150. package/src/core/binary/meshopt/MeshoptCompressionMode.js +21 -21
  151. package/src/core/binary/meshopt/__meshopt_test_streams.js +237 -237
  152. package/src/core/binary/meshopt/meshopt_decode.js +73 -73
  153. package/src/core/binary/meshopt/meshopt_decode_byte_group.js +67 -67
  154. package/src/core/binary/meshopt/meshopt_decode_data_block.js +59 -59
  155. package/src/core/binary/meshopt/meshopt_decode_filter_color.js +69 -69
  156. package/src/core/binary/meshopt/meshopt_decode_filter_exponential.js +43 -43
  157. package/src/core/binary/meshopt/meshopt_decode_filter_octahedral.js +60 -60
  158. package/src/core/binary/meshopt/meshopt_decode_filter_quaternion.js +71 -71
  159. package/src/core/binary/meshopt/meshopt_decode_index_buffer.js +294 -294
  160. package/src/core/binary/meshopt/meshopt_decode_index_sequence.js +115 -115
  161. package/src/core/binary/meshopt/meshopt_decode_vertex_buffer.js +381 -381
  162. package/src/core/binary/meshopt/meshopt_read_uint_var.js +59 -59
  163. package/src/core/binary/reinterpret_float32_as_uint32.js +13 -13
  164. package/src/core/binary/reinterpret_uint32_as_float32.js +14 -14
  165. package/src/core/binary/type/DataType2TypedArrayConstructorMapping.js +35 -35
  166. package/src/core/bvh2/bvh3/ebvh_geometry_query_any_triangle_ray.js +149 -149
  167. package/src/core/bvh2/bvh3/ebvh_sort_for_traversal_depth_first.js +96 -96
  168. package/src/core/bvh2/bvh3/query/bvh_query_depth_range_in_frustum.js +101 -101
  169. package/src/core/bvh2/bvh3/query/bvh_query_leaves_generic.js +77 -77
  170. package/src/core/bvh2/bvh3/query/bvh_query_leaves_ray.js +90 -90
  171. package/src/core/bvh2/bvh3/query/bvh_query_leaves_ray_segment.js +94 -94
  172. package/src/core/bvh2/bvh3/query/bvh_query_user_data_generic.js +86 -86
  173. package/src/core/bvh2/bvh3/query/bvh_query_user_data_intersects_capsule.js +101 -101
  174. package/src/core/bvh2/bvh3/query/bvh_query_user_data_intersects_sphere.js +92 -92
  175. package/src/core/bvh2/bvh3/query/bvh_query_user_data_ray.js +96 -96
  176. package/src/core/bvh2/bvh3/query/bvh_query_user_data_ray_segment.js +100 -100
  177. package/src/core/bvh2/visual/BVHGeometry.js +131 -131
  178. package/src/core/bvh8/BVH8.js +436 -436
  179. package/src/core/bvh8/aabb3_round_to_float32.js +22 -22
  180. package/src/core/bvh8/build/BVH8Converter.js +588 -588
  181. package/src/core/bvh8/build/TriangleCluster.js +122 -122
  182. package/src/core/bvh8/build/aabb3_compute_merge_cost.js +28 -28
  183. package/src/core/bvh8/build/aabb3_from_triangle_by_index.js +17 -17
  184. package/src/core/bvh8/build/bvh8_build_for_geometry.js +303 -303
  185. package/src/core/bvh8/build/bvh8_from_proxy.js +256 -256
  186. package/src/core/bvh8/build/byte.js +9 -9
  187. package/src/core/bvh8/build/encode_bounds_e.js +11 -11
  188. package/src/core/bvh8/bvh8_convert_to_dot.js +132 -132
  189. package/src/core/bvh8/bvh8_count_primitives.js +98 -98
  190. package/src/core/bvh8/bvh8_geometry_validate.js +138 -138
  191. package/src/core/bvh8/bvh8_geometry_validate_indirect.js +166 -166
  192. package/src/core/bvh8/bvh8_get_node_bounds.js +34 -34
  193. package/src/core/bvh8/bvh8_get_node_child_bounds.js +52 -52
  194. package/src/core/bvh8/bvh8_node_child_surface_area.js +18 -18
  195. package/src/core/bvh8/bvh8_node_count_triangles.js +27 -27
  196. package/src/core/bvh8/bvh8_quality.js +72 -72
  197. package/src/core/bvh8/bvh8_validate_structure.js +87 -87
  198. package/src/core/clipboard/obtainClipBoard.js +29 -29
  199. package/src/core/clipboard/safeClipboardReadText.js +55 -55
  200. package/src/core/clipboard/safeClipboardWriteText.js +23 -23
  201. package/src/core/collection/array/typed/typed_array_value_denormalize.js +52 -52
  202. package/src/core/collection/array/typed/typed_array_value_normalize.js +51 -51
  203. package/src/core/collection/union-find/union_find.js +79 -79
  204. package/src/core/color/PQ/PQ_constants.d.ts +43 -0
  205. package/src/core/color/PQ/PQ_constants.d.ts.map +1 -0
  206. package/src/core/color/PQ/PQ_constants.js +50 -0
  207. package/src/core/color/PQ/PQ_to_linear.d.ts +24 -0
  208. package/src/core/color/PQ/PQ_to_linear.d.ts.map +1 -0
  209. package/src/core/color/PQ/PQ_to_linear.js +35 -0
  210. package/src/core/color/PQ/linear_to_PQ.d.ts +27 -0
  211. package/src/core/color/PQ/linear_to_PQ.d.ts.map +1 -0
  212. package/src/core/color/PQ/linear_to_PQ.js +32 -0
  213. package/src/core/color/YCoCg/rgb_to_ycocg.js +20 -20
  214. package/src/core/color/YCoCg/ycocg_to_rgb.js +17 -17
  215. package/src/core/color/colormap/MAGMA_LUT.js +26 -26
  216. package/src/core/color/illuminant/D65_TRISTIMULUS_XYZ.js +19 -19
  217. package/src/core/color/lab/lab_apply_hunt_adjustment.js +27 -27
  218. package/src/core/color/lab/lab_distance_hyab.js +23 -23
  219. package/src/core/color/lab/xyz_to_lab.js +62 -62
  220. package/src/core/color/operations/color_darken.js +24 -24
  221. package/src/core/color/operations/color_desaturate.js +24 -24
  222. package/src/core/color/operations/color_lighten.js +24 -24
  223. package/src/core/color/operations/color_saturate.js +24 -24
  224. package/src/core/color/operations/color_scale_okhsv_channel.js +45 -45
  225. package/src/core/color/tonemap/tonemap_aces.js +41 -41
  226. package/src/core/color/ycxcz/xyz_to_ycxcz.js +28 -28
  227. package/src/core/color/ycxcz/ycxcz_to_xyz.js +19 -19
  228. package/src/core/dom/isImageBitmap.js +12 -12
  229. package/src/core/function/frameThrottle.js +23 -23
  230. package/src/core/geom/2d/aabb/aabb2_array_combine.js +7 -7
  231. package/src/core/geom/2d/aabb/aabb2_compute_area.js +7 -7
  232. package/src/core/geom/2d/aabb/aabb2_compute_center_from_multiple.js +7 -7
  233. package/src/core/geom/2d/aabb/aabb2_compute_overlap.js +7 -7
  234. package/src/core/geom/2d/aabb/aabb2_from_v2_array.js +7 -7
  235. package/src/core/geom/2d/aabb/aabb2_intersects_point.js +7 -7
  236. package/src/core/geom/2d/aabb/aabb2_overlap_exists.js +7 -7
  237. package/src/core/geom/2d/circle/circle_compute_circle_intersection.js +7 -7
  238. package/src/core/geom/2d/circle/circle_compute_circle_penetration.js +7 -7
  239. package/src/core/geom/2d/circle/circle_intersects_point.js +7 -7
  240. package/src/core/geom/2d/compute_polygon_area_2d.js +7 -7
  241. package/src/core/geom/2d/convex-hull/fixed_convex_hull_humus.js +7 -7
  242. package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.js +7 -7
  243. package/src/core/geom/2d/convex-hull/orientation3_array.js +7 -7
  244. package/src/core/geom/2d/intersect_ray_2d.js +7 -7
  245. package/src/core/geom/2d/line/line2_distance_to_point_sqr.js +7 -7
  246. package/src/core/geom/2d/line/line2_segment_intersection_fraction_2d.js +44 -44
  247. package/src/core/geom/2d/line/line_segment_compute_line_segment_intersection_2d.js +7 -7
  248. package/src/core/geom/2d/line/line_segment_compute_line_segment_intersection_array_2d.js +7 -7
  249. package/src/core/geom/2d/line/line_segment_compute_line_segment_intersection_vectors_2d.js +7 -7
  250. package/src/core/geom/2d/line/line_segment_compute_line_segment_intersection_vectors_array_2d.js +7 -7
  251. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.js +7 -7
  252. package/src/core/geom/2d/line/line_segment_line_segment_intersection_exists_2d.js +7 -7
  253. package/src/core/geom/2d/line/segment2_intersects_segment2_2d.js +25 -25
  254. package/src/core/geom/2d/line/segment2_segment_compute_intersection_array_2d.js +46 -46
  255. package/src/core/geom/2d/line/segment2_segment_intersection_fractions.js +52 -52
  256. package/src/core/geom/2d/oriented-box/min_area_rect_2d.js +69 -69
  257. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -433
  258. package/src/core/geom/2d/polygon/polygon2_clip_axis_halfplane.js +51 -51
  259. package/src/core/geom/2d/polygon/polygon2_clip_halfplane.js +50 -50
  260. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -84
  261. package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -52
  262. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -86
  263. package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -1280
  264. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -77
  265. package/src/core/geom/2d/quad-tree/qt_collect_by_circle.js +39 -39
  266. package/src/core/geom/2d/quad-tree/qt_match_data_by_circle.js +101 -101
  267. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.js +7 -7
  268. package/src/core/geom/2d/rectangle_to_aabb.js +7 -7
  269. package/src/core/geom/2d/spline_bezier2_2d.js +7 -7
  270. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +99 -99
  271. package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -35
  272. package/src/core/geom/2d/triangle2d_compute_area.js +7 -7
  273. package/src/core/geom/2d/triangle2d_contains_point.js +7 -7
  274. package/src/core/geom/2d/triangle2d_get_barycentric.js +7 -7
  275. package/src/core/geom/2d/v2_morton_encode.js +23 -23
  276. package/src/core/geom/3d/aabb/aabb3_array_intersects_frustum_degree.js +7 -7
  277. package/src/core/geom/3d/aabb/aabb3_box_surface_area_2.js +7 -7
  278. package/src/core/geom/3d/aabb/aabb3_build_frustum.js +5 -5
  279. package/src/core/geom/3d/aabb/aabb3_closest_point_on_surface.js +57 -57
  280. package/src/core/geom/3d/aabb/aabb3_combined_surface_area.js +7 -7
  281. package/src/core/geom/3d/aabb/aabb3_compute_half_surface_area.js +7 -7
  282. package/src/core/geom/3d/aabb/aabb3_compute_projected_area.js +7 -7
  283. package/src/core/geom/3d/aabb/aabb3_compute_surface_area.js +7 -7
  284. package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.js +7 -7
  285. package/src/core/geom/3d/aabb/aabb3_estimate_projected_area2.js +7 -7
  286. package/src/core/geom/3d/aabb/aabb3_from_threejs_geometry.js +7 -7
  287. package/src/core/geom/3d/aabb/aabb3_from_triangle_group.js +46 -46
  288. package/src/core/geom/3d/aabb/aabb3_intersects_frustum_degree.js +7 -7
  289. package/src/core/geom/3d/aabb/aabb3_intersects_line_segment.js +7 -7
  290. package/src/core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.js +7 -7
  291. package/src/core/geom/3d/aabb/aabb3_nearest_point_on_surface.js +7 -7
  292. package/src/core/geom/3d/aabb/aabb3_ray_segment_entry_distance.js +95 -95
  293. package/src/core/geom/3d/aabb/aabb3_raycast.js +7 -7
  294. package/src/core/geom/3d/aabb/aabb3_transform_oriented.js +93 -93
  295. package/src/core/geom/3d/aabb/aabb3_transform_oriented_inverse.js +82 -82
  296. package/src/core/geom/3d/aabb/compute_aabb_from_points.js +7 -7
  297. package/src/core/geom/3d/aabb/compute_triangle_group_aabb3.js +7 -7
  298. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +486 -486
  299. package/src/core/geom/3d/atlas/BFF_PLAN.md +174 -174
  300. package/src/core/geom/3d/atlas/Chart.js +77 -77
  301. package/src/core/geom/3d/atlas/REVIEW_02.md +392 -392
  302. package/src/core/geom/3d/atlas/atlas_bench_lib.js +315 -315
  303. package/src/core/geom/3d/atlas/atlas_chart_type.js +16 -16
  304. package/src/core/geom/3d/atlas/atlas_compute_charts.js +188 -188
  305. package/src/core/geom/3d/atlas/atlas_extract_chart.js +107 -107
  306. package/src/core/geom/3d/atlas/atlas_generate.js +50 -50
  307. package/src/core/geom/3d/atlas/atlas_test_fixtures.js +266 -266
  308. package/src/core/geom/3d/atlas/atlas_write_chart_uvs.js +36 -36
  309. package/src/core/geom/3d/atlas/data/atlas_build_edge_dihedrals.js +62 -62
  310. package/src/core/geom/3d/atlas/data/atlas_build_edge_lengths.js +33 -33
  311. package/src/core/geom/3d/atlas/data/atlas_build_face_areas.js +50 -50
  312. package/src/core/geom/3d/atlas/io/atlas_mesh_weld_colocals.js +39 -39
  313. package/src/core/geom/3d/atlas/io/bt_loop_uv.js +35 -35
  314. package/src/core/geom/3d/atlas/pack/ChartPlacement.js +46 -46
  315. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.js +269 -269
  316. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.js +209 -209
  317. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_ortho.js +38 -38
  318. package/src/core/geom/3d/atlas/param/atlas_chart_stretch_metric.js +86 -86
  319. package/src/core/geom/3d/atlas/param/atlas_piecewise_resegment.js +183 -183
  320. package/src/core/geom/3d/atlas/param/fit_plane_basis.js +66 -66
  321. package/src/core/geom/3d/atlas/segment/RegionGraph.js +40 -40
  322. package/src/core/geom/3d/atlas/segment/atlas_build_region_graph.js +136 -136
  323. package/src/core/geom/3d/atlas/segment/atlas_chart_roundness_delta.js +30 -30
  324. package/src/core/geom/3d/atlas/segment/atlas_compute_planar_regions.js +77 -77
  325. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +116 -116
  326. package/src/core/geom/3d/atlas/segment/atlas_segment_charts.js +271 -271
  327. package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.js +191 -191
  328. package/src/core/geom/3d/box/box3_projected_half_extent.js +35 -35
  329. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_closed.js +67 -67
  330. package/src/core/geom/3d/capsule/capsule_intersects_aabb3_iterative.js +137 -137
  331. package/src/core/geom/3d/compute_circle_bounding_box.js +7 -7
  332. package/src/core/geom/3d/cone/computeConeBoundingBox.js +7 -7
  333. package/src/core/geom/3d/cone/computeConePlaneSide.js +7 -7
  334. package/src/core/geom/3d/cone/compute_bounding_cone_of_2_cones.js +7 -7
  335. package/src/core/geom/3d/cylinder/cylinder3_raycast.d.ts +38 -0
  336. package/src/core/geom/3d/cylinder/cylinder3_raycast.d.ts.map +1 -0
  337. package/src/core/geom/3d/cylinder/cylinder3_raycast.js +105 -0
  338. package/src/core/geom/3d/equirectangular/equirectangular_direction_to_uv.js +18 -18
  339. package/src/core/geom/3d/equirectangular/equirectangular_uv_to_direction.js +24 -24
  340. package/src/core/geom/3d/frustum/clipping_volume_matrix4_project.js +7 -7
  341. package/src/core/geom/3d/frustum/frustum3_computeNearestPointToPoint.js +7 -7
  342. package/src/core/geom/3d/frustum/frustum3_compute_closest_point.js +117 -117
  343. package/src/core/geom/3d/frustum/frustum_matrix4_project.js +7 -7
  344. package/src/core/geom/3d/frustum/hexahedron_from_aabb.js +7 -7
  345. package/src/core/geom/3d/frustum/read_frustum_corner.js +17 -17
  346. package/src/core/geom/3d/frustum/read_three_planes_to_array.js +7 -7
  347. package/src/core/geom/3d/frustum/slice_frustum_linear_to_points.js +7 -7
  348. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +146 -146
  349. package/src/core/geom/3d/gjk/gjk.js +430 -430
  350. package/src/core/geom/3d/gjk/gjk_epa_penetration.js +520 -520
  351. package/src/core/geom/3d/gjk/minkowski_support.js +71 -71
  352. package/src/core/geom/3d/gjk/mpr.js +368 -368
  353. package/src/core/geom/3d/line/line3_closest_points_segment_segment.js +7 -7
  354. package/src/core/geom/3d/line/line3_compute_nearest_point_to_point.js +7 -7
  355. package/src/core/geom/3d/line/line3_compute_point_segment_point_distance_unfolded.js +64 -64
  356. package/src/core/geom/3d/line/line3_compute_segment_closest_point_to_aabb3_t.js +153 -153
  357. package/src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.js +7 -7
  358. package/src/core/geom/3d/line/line3_compute_segment_nearest_point_to_point_t.js +7 -7
  359. package/src/core/geom/3d/line/line3_compute_segment_segment_closest_points.js +88 -88
  360. package/src/core/geom/3d/line/line3_segment_segment_closest_point_pair.js +71 -71
  361. package/src/core/geom/3d/line/segment3_point_distance_to_segments.js +25 -25
  362. package/src/core/geom/3d/mat4/allocate_m4.js +7 -7
  363. package/src/core/geom/3d/mat4/apply_mat4_transform_to_direction_v3_array.js +7 -7
  364. package/src/core/geom/3d/mat4/apply_mat4_transform_to_v3_array.js +7 -7
  365. package/src/core/geom/3d/mat4/compose_matrix4_array.js +7 -7
  366. package/src/core/geom/3d/mat4/decompose_matrix_4_array.js +7 -7
  367. package/src/core/geom/3d/mat4/eulerAnglesFromMatrix.js +7 -7
  368. package/src/core/geom/3d/mat4/m4_compose.js +22 -22
  369. package/src/core/geom/3d/mat4/m4_compute_normal_matrix3.js +7 -7
  370. package/src/core/geom/3d/mat4/m4_decompose.js +64 -64
  371. package/src/core/geom/3d/mat4/m4_decompose_array.js +157 -157
  372. package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale.js +32 -32
  373. package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale_scalar.js +67 -67
  374. package/src/core/geom/3d/mat4/m4_linear_matrix3.js +29 -29
  375. package/src/core/geom/3d/mat4/m4_look_at.js +96 -96
  376. package/src/core/geom/3d/mat4/m4_make_rotation_x.js +47 -47
  377. package/src/core/geom/3d/mat4/m4_make_rotation_y.js +47 -47
  378. package/src/core/geom/3d/mat4/m4_make_rotation_z.js +47 -47
  379. package/src/core/geom/3d/mat4/m4_normal_matrix3.js +39 -39
  380. package/src/core/geom/3d/mat4/m4_perspective.js +66 -66
  381. package/src/core/geom/3d/mat4/m4_transpose.js +48 -48
  382. package/src/core/geom/3d/morton/mortonEncode_LUT.js +7 -7
  383. package/src/core/geom/3d/morton/mortonEncode_magicbits.js +7 -7
  384. package/src/core/geom/3d/normal/hemioct/encode_unit3_hemioct.js +5 -5
  385. package/src/core/geom/3d/normal/hemioct/encode_unit_to_hemioct.js +28 -28
  386. package/src/core/geom/3d/normal/lambert_azimuth/decode_azimuth_to_unit.js +30 -30
  387. package/src/core/geom/3d/normal/lambert_azimuth/encode_unit_to_azimuth.js +19 -19
  388. package/src/core/geom/3d/normal/spherical/decode_spheremap_to_unit.js +38 -38
  389. package/src/core/geom/3d/normal/spherical/encode_unit_to_spheremap.js +30 -30
  390. package/src/core/geom/3d/plane/is_point_within_planes.js +7 -7
  391. package/src/core/geom/3d/plane/orient3d_fast_point.js +47 -47
  392. package/src/core/geom/3d/plane/plane3_compute_convex_3_plane_intersection.js +7 -7
  393. package/src/core/geom/3d/plane/plane3_compute_plane_intersection.js +7 -7
  394. package/src/core/geom/3d/plane/plane3_intersect_plane.js +7 -7
  395. package/src/core/geom/3d/plane/plane3_three_compute_convex_3_plane_intersection.js +7 -7
  396. package/src/core/geom/3d/point/point3_raycast.d.ts +33 -0
  397. package/src/core/geom/3d/point/point3_raycast.d.ts.map +1 -0
  398. package/src/core/geom/3d/point/point3_raycast.js +47 -0
  399. package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -60
  400. package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -94
  401. package/src/core/geom/3d/polyhedron/convex_polyhedron3_raycast.d.ts +40 -0
  402. package/src/core/geom/3d/polyhedron/convex_polyhedron3_raycast.d.ts.map +1 -0
  403. package/src/core/geom/3d/polyhedron/convex_polyhedron3_raycast.js +94 -0
  404. package/src/core/geom/3d/quadric/QUADRIC3_SIZE.js +14 -14
  405. package/src/core/geom/3d/quadric/Quadric3.js +282 -282
  406. package/src/core/geom/3d/quadric/quadric3_add.js +33 -33
  407. package/src/core/geom/3d/quadric/quadric3_clear.js +25 -25
  408. package/src/core/geom/3d/quadric/quadric3_copy.js +27 -27
  409. package/src/core/geom/3d/quadric/quadric3_distance.js +36 -36
  410. package/src/core/geom/3d/quadric/quadric3_evaluate.js +25 -25
  411. package/src/core/geom/3d/quadric/quadric3_optimize.js +45 -45
  412. package/src/core/geom/3d/quadric/quadric3_scale.js +32 -32
  413. package/src/core/geom/3d/quadric/quadric3_set_from_plane_through_point.js +30 -30
  414. package/src/core/geom/3d/quadric/quadric3_set_from_vector4.js +35 -35
  415. package/src/core/geom/3d/quadric/quadric3_sub.js +36 -36
  416. package/src/core/geom/3d/quadric/quadric3_to_tensor_m3.js +28 -28
  417. package/src/core/geom/3d/quadric/quadric3_to_tensor_m3_inverse.js +56 -56
  418. package/src/core/geom/3d/quadric/quadric3_to_vector3.js +19 -19
  419. package/src/core/geom/3d/quaternion/quat3_createFromAxisAngle.js +7 -7
  420. package/src/core/geom/3d/quaternion/quat3_integrate.js +62 -62
  421. package/src/core/geom/3d/quaternion/quat3_multiply.js +31 -31
  422. package/src/core/geom/3d/quaternion/quat3_nlerp.js +72 -72
  423. package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -69
  424. package/src/core/geom/3d/quaternion/quat_decode_from_uint32.js +7 -7
  425. package/src/core/geom/3d/quaternion/quat_encode_to_uint32.js +7 -7
  426. package/src/core/geom/3d/ray/ray3_array_compose.js +7 -7
  427. package/src/core/geom/3d/ray/ray3_compute_nearest_point_to_point.js +7 -7
  428. package/src/core/geom/3d/ray/ray3_distance_to_point.js +7 -7
  429. package/src/core/geom/3d/ray/ray3_shift_origin_along_direction.js +7 -7
  430. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +17 -0
  431. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  432. package/src/core/geom/3d/shape/ConvexHullShape3D.js +43 -0
  433. package/src/core/geom/3d/shape/CylinderShape3D.d.ts +1 -0
  434. package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -1
  435. package/src/core/geom/3d/shape/CylinderShape3D.js +21 -0
  436. package/src/core/geom/3d/shape/PointShape3D.d.ts +7 -0
  437. package/src/core/geom/3d/shape/PointShape3D.d.ts.map +1 -1
  438. package/src/core/geom/3d/shape/PointShape3D.js +88 -63
  439. package/src/core/geom/3d/shape/PosedShape3D.d.ts +6 -0
  440. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  441. package/src/core/geom/3d/shape/PosedShape3D.js +27 -0
  442. package/src/core/geom/3d/shape/TransformedShape3D.d.ts +10 -0
  443. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  444. package/src/core/geom/3d/shape/TransformedShape3D.js +13 -0
  445. package/src/core/geom/3d/shape/Triangle3D.d.ts +1 -0
  446. package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -1
  447. package/src/core/geom/3d/shape/Triangle3D.js +26 -0
  448. package/src/core/geom/3d/shape/UnionShape3D.d.ts +6 -0
  449. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  450. package/src/core/geom/3d/shape/UnionShape3D.js +7 -0
  451. package/src/core/geom/3d/shape/UnitCubeShape3D.js +45 -45
  452. package/src/core/geom/3d/shape/UnitSphereShape3D.js +44 -44
  453. package/src/core/geom/3d/shape/shape3_mesh_from_geometry.js +64 -64
  454. package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +7 -7
  455. package/src/core/geom/3d/shape/util/compute_signed_distance_gradient_by_sampling.js +51 -51
  456. package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.js +7 -7
  457. package/src/core/geom/3d/sphere/compute_bounding_sphere_of_2_spheres.js +7 -7
  458. package/src/core/geom/3d/sphere/harmonics/sh3_convolve_with_zh.js +53 -53
  459. package/src/core/geom/3d/sphere/harmonics/sh3_ggx_zh.js +30 -30
  460. package/src/core/geom/3d/sphere/harmonics/sh3_rotate_bl.js +7 -7
  461. package/src/core/geom/3d/sphere/harmonics/sh_compute_K.js +7 -7
  462. package/src/core/geom/3d/sphere/sphere_array_intersects_point.js +7 -7
  463. package/src/core/geom/3d/sphere/sphere_array_intersects_ray.js +7 -7
  464. package/src/core/geom/3d/sphere/sphere_intersects_point.js +7 -7
  465. package/src/core/geom/3d/sphere/sphere_matrix4_transform.js +5 -5
  466. package/src/core/geom/3d/sphere/sphere_projected_sphere_radius_sqr.js +7 -7
  467. package/src/core/geom/3d/sphere/sphere_radius_sqr_from_v3_array_transformed.js +7 -7
  468. package/src/core/geom/3d/sphere/spherical_from_cartesian.js +7 -7
  469. package/src/core/geom/3d/tetrahedra/compute_tetrahedral_mesh_from_surface.js +174 -174
  470. package/src/core/geom/3d/tetrahedra/compute_tetrahedron_quality.js +7 -7
  471. package/src/core/geom/3d/tetrahedra/compute_tetrahedron_volume.js +7 -7
  472. package/src/core/geom/3d/tetrahedra/delaunay/fill_in_a_cavity.js +7 -7
  473. package/src/core/geom/3d/tetrahedra/graphics/build_tetrahedral_mesh_geometry.js +39 -39
  474. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -48
  475. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.js +173 -173
  476. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_compute_tet_quality.js +22 -22
  477. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_find_tets_around_edge.js +146 -146
  478. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_23.js +232 -232
  479. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_32.js +255 -255
  480. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +387 -387
  481. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +140 -140
  482. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.js +7 -7
  483. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +97 -97
  484. package/src/core/geom/3d/tetrahedra/tetrahedron_compute_quality.js +66 -66
  485. package/src/core/geom/3d/tetrahedra/triangle/triangle_decode_depth.js +59 -59
  486. package/src/core/geom/3d/tetrahedra/triangle/triangle_rasterized_depth_sample_index.js +54 -54
  487. package/src/core/geom/3d/tetrahedra/validate_neighbour.js +70 -70
  488. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.js +314 -314
  489. package/src/core/geom/3d/topology/struct/binary/BinaryDataLayer.js +364 -364
  490. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +567 -567
  491. package/src/core/geom/3d/topology/struct/binary/BinaryTopology.js +1085 -1085
  492. package/src/core/geom/3d/topology/struct/binary/io/bt_boundary_loop.js +73 -73
  493. package/src/core/geom/3d/topology/struct/binary/io/bt_loop_kill.js +35 -35
  494. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_append.js +119 -119
  495. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.js +84 -84
  496. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compact.js +172 -172
  497. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_face_normals.js +56 -56
  498. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadratics.js +7 -7
  499. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +227 -227
  500. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_decouple.js +161 -161
  501. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js +490 -490
  502. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +120 -120
  503. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_from_indexed_geometry.js +120 -120
  504. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_collapse.js +52 -52
  505. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_create.js +115 -115
  506. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_flip.js +142 -142
  507. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_get_or_create.js +21 -21
  508. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_kill_parallels.js +92 -92
  509. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_split.js +179 -179
  510. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.js +28 -28
  511. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_kill_only_edge.js +8 -8
  512. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_mesh_fuse_duplicate_edges.js +83 -83
  513. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_mesh_kill_short_edges.js +89 -89
  514. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_kill.js +25 -25
  515. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_poke.js +127 -127
  516. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -268
  517. package/src/core/geom/3d/topology/struct/binary/io/face/bt_kill_only_face.js +8 -8
  518. package/src/core/geom/3d/topology/struct/binary/io/face/bt_mesh_kill_degenerate_faces.js +72 -72
  519. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_kill_only_vert.js +7 -7
  520. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_merge_verts_by_distance.js +7 -7
  521. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_mesh_relative_merge_distance.js +48 -48
  522. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_mesh_split_pinched_vertices.js +256 -256
  523. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_mesh_vertex_merge_distance.js +77 -77
  524. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vert_fuse_duplicate_edges.js +7 -7
  525. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vert_kill.js +7 -7
  526. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_fuse_duplicate_edges.js +148 -148
  527. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_kill_only.js +7 -7
  528. package/src/core/geom/3d/topology/struct/binary/query/bt_collect_boundary_segments.js +29 -29
  529. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_breaks_link_condition.js +107 -107
  530. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_flip_is_legal.js +41 -41
  531. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_flip_would_fold.js +120 -120
  532. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_get.js +42 -42
  533. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_is_boundary.js +20 -20
  534. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_other_face.d.ts +32 -0
  535. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_other_face.d.ts.map +1 -0
  536. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_other_face.js +59 -0
  537. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_read_diamond.js +96 -96
  538. package/src/core/geom/3d/topology/struct/binary/query/bt_face_compute_newell_normal.js +72 -72
  539. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_attached_faces.js +7 -7
  540. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_edge_adjacent_faces.js +39 -39
  541. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_edge_adjacent_faces_weighted.js +93 -93
  542. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_neighbour_faces.js +7 -7
  543. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_shared_loop.js +48 -48
  544. package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.js +45 -45
  545. package/src/core/geom/3d/topology/struct/binary/query/bt_face_read_triangle.js +66 -66
  546. package/src/core/geom/3d/topology/struct/binary/query/bt_face_third_vertex.d.ts +26 -0
  547. package/src/core/geom/3d/topology/struct/binary/query/bt_face_third_vertex.d.ts.map +1 -0
  548. package/src/core/geom/3d/topology/struct/binary/query/bt_face_third_vertex.js +55 -0
  549. package/src/core/geom/3d/topology/struct/binary/query/bt_faces_shared_loop.js +7 -7
  550. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -44
  551. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.js +32 -32
  552. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js +53 -53
  553. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path.js +703 -703
  554. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_sample_interior_grid_points.js +181 -181
  555. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_segment_penetrates_surface.js +133 -133
  556. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_surface_ray_parity.js +82 -82
  557. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -599
  558. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_walk_boundary_loops.js +108 -108
  559. package/src/core/geom/3d/topology/struct/binary/query/bt_query_edge_is_boundary.js +7 -7
  560. package/src/core/geom/3d/triangle/computeTriangleClosestPointToPointBarycentric.js +7 -7
  561. package/src/core/geom/3d/triangle/computeTriangleRayIntersection.js +7 -7
  562. package/src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js +7 -7
  563. package/src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentricEdge.js +7 -7
  564. package/src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentricGeometry.js +7 -7
  565. package/src/core/geom/3d/triangle/compute_triangle_area_3d.js +7 -7
  566. package/src/core/geom/3d/triangle/compute_triangle_normal.js +7 -7
  567. package/src/core/geom/3d/triangle/rayTriangleIntersection.js +7 -7
  568. package/src/core/geom/3d/triangle/tri3_closest_point.js +65 -65
  569. package/src/core/geom/3d/triangle/tri3_closest_point_barycentric.js +171 -171
  570. package/src/core/geom/3d/triangle/tri3_flatten_isometric.js +79 -79
  571. package/src/core/geom/3d/triangle/tri3_ray_intersection.js +160 -160
  572. package/src/core/geom/3d/triangle/tri3_ray_intersection_barycentric.js +96 -96
  573. package/src/core/geom/3d/triangle/tri3_raycast.d.ts +42 -0
  574. package/src/core/geom/3d/triangle/tri3_raycast.d.ts.map +1 -0
  575. package/src/core/geom/3d/triangle/tri3_raycast.js +94 -0
  576. package/src/core/geom/3d/triangle/triangle3_incenter.js +7 -7
  577. package/src/core/geom/3d/triangle/triangle_compute_plane_side.js +7 -7
  578. package/src/core/geom/3d/triangle/triangle_compute_signed_volume.js +7 -7
  579. package/src/core/geom/3d/triangle/triangle_intersects_clipping_volume.js +7 -7
  580. package/src/core/geom/3d/triangle/triangle_mesh_compute_signed_volume.js +7 -7
  581. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +7 -7
  582. package/src/core/geom/3d/util/make_justified_point_grid.js +7 -7
  583. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +381 -381
  584. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +684 -684
  585. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -554
  586. package/src/core/geom/mat3/m3_cm_from_translation.js +7 -7
  587. package/src/core/geom/mat3/m3_cm_invert.js +7 -7
  588. package/src/core/geom/mat3/m3_make_rotation.js +5 -5
  589. package/src/core/geom/mat3/m3_make_rotation_TBN.js +31 -31
  590. package/src/core/geom/mat3/m3_multiply_vec3.js +7 -7
  591. package/src/core/geom/packing/computeBoundingSphereOfSpheres.js +7 -7
  592. package/src/core/geom/packing/max-rect/cost/costByBestShortSide.js +7 -7
  593. package/src/core/geom/packing/max-rect/cost/costByRemainingArea.js +7 -7
  594. package/src/core/geom/packing/max-rect/cutArea.js +7 -7
  595. package/src/core/geom/packing/max-rect/findBestContainer.js +7 -7
  596. package/src/core/geom/packing/max-rect/packMaxRectangles.js +7 -7
  597. package/src/core/geom/packing/max-rect/packOneBox.js +7 -7
  598. package/src/core/geom/packing/max-rect/removeRedundantBoxes.js +7 -7
  599. package/src/core/geom/packing/max-rect/removeRedundantBoxesArray.js +7 -7
  600. package/src/core/geom/random/randomPointInBox.js +7 -7
  601. package/src/core/geom/random/randomPointInCircle.js +7 -7
  602. package/src/core/geom/random/randomPointInPoint.js +7 -7
  603. package/src/core/geom/random/randomPointInSphere.js +7 -7
  604. package/src/core/geom/random/randomPointOnBox.js +7 -7
  605. package/src/core/geom/random/randomPointOnSphere.js +7 -7
  606. package/src/core/geom/random/random_orthonormal_basis.js +31 -31
  607. package/src/core/geom/vec/vector_axpy.js +15 -15
  608. package/src/core/geom/vec/vector_axpy_offset.js +20 -20
  609. package/src/core/geom/vec/vector_copy.js +7 -7
  610. package/src/core/geom/vec/vector_dot_offset.js +25 -25
  611. package/src/core/geom/vec/vector_scale_array.js +7 -7
  612. package/src/core/geom/vec2/v2_rotate.js +21 -21
  613. package/src/core/geom/vec2/v2_rotate_cos_sin.js +21 -21
  614. package/src/core/geom/vec3/v3_angle_atan2_between.js +7 -7
  615. package/src/core/geom/vec3/v3_angle_between.js +7 -7
  616. package/src/core/geom/vec3/v3_angle_cos_between.js +7 -7
  617. package/src/core/geom/vec3/v3_array_add_in_place.js +7 -7
  618. package/src/core/geom/vec3/v3_array_centroid.js +7 -7
  619. package/src/core/geom/vec3/v3_array_copy.js +7 -7
  620. package/src/core/geom/vec3/v3_array_displace_in_direction.js +7 -7
  621. package/src/core/geom/vec3/v3_array_equals.js +7 -7
  622. package/src/core/geom/vec3/v3_array_immediate_add.js +7 -7
  623. package/src/core/geom/vec3/v3_array_matrix4_rotate.js +7 -7
  624. package/src/core/geom/vec3/v3_array_normalize.js +7 -7
  625. package/src/core/geom/vec3/v3_array_scale.js +7 -7
  626. package/src/core/geom/vec3/v3_compute_interior_angle.js +7 -7
  627. package/src/core/geom/vec3/v3_cotangent.js +33 -33
  628. package/src/core/geom/vec3/v3_dot_array_array.js +7 -7
  629. package/src/core/geom/vec3/v3_matrix3_multiply.js +30 -30
  630. package/src/core/geom/vec3/v3_matrix3_rotate.js +49 -49
  631. package/src/core/geom/vec3/v3_matrix4_rotate.js +7 -7
  632. package/src/core/geom/vec3/v3_multiply.js +23 -23
  633. package/src/core/geom/vec3/v3_multiply_scalar.js +7 -7
  634. package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.js +49 -49
  635. package/src/core/geom/vec3/v3_quat3_apply.js +7 -7
  636. package/src/core/geom/vec3/v3_quat3_apply_inverse.js +7 -7
  637. package/src/core/geom/vec3/v3_quaternion_apply.js +39 -39
  638. package/src/core/geom/vec3/v3_quaternion_apply_inverse.js +41 -41
  639. package/src/core/geom/vec3/v3_reflect.js +24 -24
  640. package/src/core/geom/vec3/v3_scale_array.js +18 -18
  641. package/src/core/geom/vec3/v3_triple_cross.js +45 -45
  642. package/src/core/geom/vec3/v3_triple_cross_product.js +7 -7
  643. package/src/core/geom/vec3/v3_uniform_sample_cone.js +21 -21
  644. package/src/core/geom/vec4/v4_multiply_mat4.js +7 -7
  645. package/src/core/graph/csr/csr_graph_build_distance_field.js +112 -112
  646. package/src/core/graph/csr/csr_graph_find_edge.js +25 -25
  647. package/src/core/graph/csr/csr_graph_find_shortest_path.js +105 -105
  648. package/src/core/graph/csr/csr_graph_from_undirected_edge_list.js +94 -94
  649. package/src/core/graph/graph_k_means_cluster.js +250 -250
  650. package/src/core/graph/layout/CircleLayout.js +1 -1
  651. package/src/core/graph/layout/box/BoxLayouter.js +1 -1
  652. package/src/core/graph/layout/graph_peel_topological_layer.js +45 -45
  653. package/src/core/graph/metis/BinaryTopologyFaceGraph.js +49 -49
  654. package/src/core/graph/metis/bt_mesh_build_face_graph.js +111 -111
  655. package/src/core/graph/metis/cluster_mesh_metis.js +137 -137
  656. package/src/core/graph/metis/native/bisection/bisect_graph.js +100 -100
  657. package/src/core/graph/metis/native/bisection/compute_2way_params.js +86 -86
  658. package/src/core/graph/metis/native/bisection/fm_2way.js +297 -297
  659. package/src/core/graph/metis/native/bisection/grow_bisection.js +137 -137
  660. package/src/core/graph/metis/native/bisection/split_graph_two_way.js +119 -119
  661. package/src/core/graph/metis/native/coarsen/coarsen_graph.js +94 -94
  662. package/src/core/graph/metis/native/coarsen/create_coarse_graph.js +158 -158
  663. package/src/core/graph/metis/native/coarsen/match_shem.js +175 -175
  664. package/src/core/graph/metis/native/initial/initial_kway_bfs.js +122 -122
  665. package/src/core/graph/metis/native/initial/initial_kway_recursive_bisection.js +170 -170
  666. package/src/core/graph/metis/native/metis_partition_kway.js +126 -126
  667. package/src/core/graph/metis/native/refine/RefinementScratch.js +53 -53
  668. package/src/core/graph/metis/native/refine/fm_kway.js +487 -487
  669. package/src/core/graph/metis/native/refine/project_kway.js +43 -43
  670. package/src/core/graph/metis/native/refine/refine_kway.js +43 -43
  671. package/src/core/lang/reactive/pegjs/parser.js +3458 -3458
  672. package/src/core/math/complex/complex_horner_eval.js +31 -31
  673. package/src/core/math/computeWholeDivisorLow.js +33 -33
  674. package/src/core/math/f32_next_down.js +24 -24
  675. package/src/core/math/f32_next_up.js +27 -27
  676. package/src/core/math/hash/hash_mix2.js +22 -22
  677. package/src/core/math/hash/hash_mix3.js +19 -19
  678. package/src/core/math/linalg/cg/cg_solve.js +106 -106
  679. package/src/core/math/linalg/cg/cg_solve_normal_equations.js +59 -59
  680. package/src/core/math/linalg/cubic_residual_times_derivative_accumulate.js +43 -43
  681. package/src/core/math/linalg/eigen/matrix_householder_in_place.js +122 -122
  682. package/src/core/math/linalg/eigen/matrix_qr_in_place.js +190 -190
  683. package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.js +117 -117
  684. package/src/core/math/linalg/givens/givens_apply_rows.js +28 -28
  685. package/src/core/math/linalg/givens/givens_apply_rows_offset.js +30 -30
  686. package/src/core/math/linalg/givens/givens_rotation_coefficients.js +39 -39
  687. package/src/core/math/linalg/lu_factor_linear_system.js +108 -108
  688. package/src/core/math/linalg/lu_solve_linear_system.js +53 -53
  689. package/src/core/math/linalg/polynomial_add_into.js +29 -29
  690. package/src/core/math/linalg/polynomial_complex_roots_aberth_ehrlich.js +159 -159
  691. package/src/core/math/linalg/polynomial_cubic_derivative_eval.js +16 -16
  692. package/src/core/math/linalg/polynomial_cubic_horner_eval.js +19 -19
  693. package/src/core/math/linalg/polynomial_cubic_second_derivative_eval.js +14 -14
  694. package/src/core/math/linalg/polynomial_multiply.js +41 -41
  695. package/src/core/math/linalg/polynomial_real_roots_in_interval.js +211 -211
  696. package/src/core/math/linalg/polynomial_root_bound_cauchy.js +30 -30
  697. package/src/core/math/linalg/polynomial_scale_into.js +17 -17
  698. package/src/core/math/linalg/polynomial_sub_into.js +29 -29
  699. package/src/core/math/linalg/solve_linear_system_GEPP_2x2.js +96 -96
  700. package/src/core/math/linalg/sor_optimal_omega.js +42 -42
  701. package/src/core/math/linalg/sparse/SparseMatrixCSR.js +88 -88
  702. package/src/core/math/linalg/sparse/spm_matvec.js +22 -22
  703. package/src/core/math/linalg/sparse/spm_matvec_transpose.js +29 -29
  704. package/src/core/math/lookup/LUT_HEATMAP.js +30 -30
  705. package/src/core/math/lookup/ParameterLookupTable.js +495 -495
  706. package/src/core/math/lookup/ParameterLookupTableFlags.js +6 -6
  707. package/src/core/math/noise/curl_noise_3d.js +18 -18
  708. package/src/core/math/physics/brdf/cone_cosine_from_roughness.js +28 -28
  709. package/src/core/math/physics/brdf/reflection_sample_weight.js +48 -48
  710. package/src/core/math/physics/kinematics/computeInterceptPoint.js +79 -79
  711. package/src/core/math/random/randomSeed.js +16 -16
  712. package/src/core/math/random/random_pick_weighted_index.js +26 -26
  713. package/src/core/math/round_half_away_from_zero.js +13 -13
  714. package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -107
  715. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_1d.js +125 -125
  716. package/src/core/math/spline/spline3_hermite_intersects_spline3_hermite.js +50 -50
  717. package/src/core/math/spline/spline3_hermite_reverse.js +49 -49
  718. package/src/core/math/spline/spline3_hermite_to_monomial.js +37 -37
  719. package/src/core/math/statistics/hammersley_sequence.js +53 -53
  720. package/src/core/model/reactive/model/util/createRandomReactiveExpression.js +185 -185
  721. package/src/core/path/convertPathToURL.js +107 -107
  722. package/src/core/process/delay.js +16 -16
  723. package/src/core/science/units/MEASUREMENT_UNITS_SCIENTIFIC.js +87 -87
  724. package/src/core/science/units/MEASUREMENT_UNITS_SI.js +17 -17
  725. package/src/core/science/units/NamedUnit.js +48 -48
  726. package/src/core/science/units/UNIT_DIMENSION_MAPPING.js +29 -29
  727. package/src/core/science/units/UnitDimension.js +28 -28
  728. package/src/core/science/units/format_quantity.js +74 -74
  729. package/src/core/science/units/magnitude_prefix.js +127 -127
  730. package/src/core/science/units/si/AMPERE.js +13 -13
  731. package/src/core/science/units/si/CANDELA.js +13 -13
  732. package/src/core/science/units/si/DIMENSIONLESS.js +14 -14
  733. package/src/core/science/units/si/KELVIN.js +13 -13
  734. package/src/core/science/units/si/KILOGRAM.js +13 -13
  735. package/src/core/science/units/si/METER.js +13 -13
  736. package/src/core/science/units/si/MOLE.js +13 -13
  737. package/src/core/science/units/si/SECOND.js +13 -13
  738. package/src/core/science/units/si/derived/BECQUEREL.js +16 -16
  739. package/src/core/science/units/si/derived/COULOMB.js +13 -13
  740. package/src/core/science/units/si/derived/FARAD.js +13 -13
  741. package/src/core/science/units/si/derived/GRAY.js +16 -16
  742. package/src/core/science/units/si/derived/HENRY.js +13 -13
  743. package/src/core/science/units/si/derived/HERTZ.js +16 -16
  744. package/src/core/science/units/si/derived/JOULE.js +13 -13
  745. package/src/core/science/units/si/derived/KATAL.js +13 -13
  746. package/src/core/science/units/si/derived/LUMEN.js +14 -14
  747. package/src/core/science/units/si/derived/LUX.js +13 -13
  748. package/src/core/science/units/si/derived/NEWTON.js +13 -13
  749. package/src/core/science/units/si/derived/OHM.js +13 -13
  750. package/src/core/science/units/si/derived/PASCAL.js +13 -13
  751. package/src/core/science/units/si/derived/SIEMENS.js +13 -13
  752. package/src/core/science/units/si/derived/SIEVERT.js +16 -16
  753. package/src/core/science/units/si/derived/TESLA.js +13 -13
  754. package/src/core/science/units/si/derived/VOLT.js +13 -13
  755. package/src/core/science/units/si/derived/WATT.js +13 -13
  756. package/src/core/science/units/si/derived/WEBER.js +13 -13
  757. package/src/core/science/units/unit_matrix_to_string.js +403 -403
  758. package/src/engine/Engine.js +726 -726
  759. package/src/engine/EngineHarness.js +590 -590
  760. package/src/engine/REVIEW_2026_07_03.md +211 -211
  761. package/src/engine/animation/AnimatedActions.js +81 -81
  762. package/src/engine/animation/AnimationUtils.js +252 -252
  763. package/src/engine/animation/async/TimeSeries.js +300 -300
  764. package/src/engine/animation/curve/animation_curve_binary_codec.js +60 -60
  765. package/src/engine/animation/curve/draw/canvas_point_to_curve.js +22 -22
  766. package/src/engine/animation/curve/draw/curve_point_to_canvas.js +38 -38
  767. package/src/engine/animation/curve/draw/position_canvas_to_curve.js +22 -22
  768. package/src/engine/animation/curve/draw/position_curve_to_canvas.js +22 -22
  769. package/src/engine/asset/GameAssetType.d.ts +0 -1
  770. package/src/engine/asset/GameAssetType.js +0 -1
  771. package/src/engine/asset/load_model_scene_bundle.js +27 -27
  772. package/src/engine/asset/loaders/font/FontAsset.js +21 -21
  773. package/src/engine/asset/loaders/font/FontAssetLoader.js +20 -20
  774. package/src/engine/asset/loaders/gltf_test_fixtures.js +358 -358
  775. package/src/engine/asset/loaders/image/ImageBitmapAssetLoader.js +52 -52
  776. package/src/engine/asset/loaders/image/ImageRGBADataLoader.js +94 -94
  777. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterAverage.js +59 -59
  778. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterNone.js +55 -55
  779. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterPaeth.js +74 -74
  780. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterSub.js +34 -34
  781. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterUp.js +46 -46
  782. package/src/engine/control/first-person/DESIGN.md +640 -640
  783. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +563 -563
  784. package/src/engine/control/first-person/FirstPersonMotionPhase.js +134 -134
  785. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +2045 -2045
  786. package/src/engine/control/first-person/mastery/DecisionPoint.js +30 -30
  787. package/src/engine/control/first-person/math/Spring.js +71 -71
  788. package/src/engine/control/first-person/math/computeJumpFromApex.js +23 -23
  789. package/src/engine/control/first-person/math/computeLRCBreathRate.js +41 -41
  790. package/src/engine/control/first-person/math/computeMassRatios.js +44 -44
  791. package/src/engine/control/first-person/math/criticallyDampedSpring.js +34 -34
  792. package/src/engine/control/first-person/math/dampedSpringStep.js +72 -72
  793. package/src/engine/control/first-person/math/jumpDynamics.js +108 -108
  794. package/src/engine/control/first-person/math/stepTowards.js +20 -20
  795. package/src/engine/control/first-person/pose/FirstPersonPose.js +137 -137
  796. package/src/engine/control/first-person/pose/FirstPersonPosture.js +27 -27
  797. package/src/engine/control/first-person/test/buildTestPlayer.js +36 -36
  798. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +531 -531
  799. package/src/engine/ecs/EntityManager.d.ts +7 -3
  800. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  801. package/src/engine/ecs/EntityManager.js +10 -5
  802. package/src/engine/ecs/HIERARCHY_REVIEW.md +717 -717
  803. package/src/engine/ecs/async/SystemWorkerHost.js +329 -329
  804. package/src/engine/ecs/async/SystemWorkerLoopback.js +143 -143
  805. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +246 -246
  806. package/src/engine/ecs/async/WorkerSystem.js +702 -702
  807. package/src/engine/ecs/async/WorkerSystemProtocol.js +150 -150
  808. package/src/engine/ecs/fow/FogOfWar.js +707 -707
  809. package/src/engine/ecs/fow/FogOfWarEdgeMode.js +40 -40
  810. package/src/engine/ecs/fow/fog_of_war_mask_world_bounds.js +24 -24
  811. package/src/engine/ecs/fow/fog_of_war_uv_transform.js +30 -30
  812. package/src/engine/ecs/gui/hud/testHudEdgeStick.js +132 -134
  813. package/src/engine/ecs/hierarchy/EntityChildIndex.js +140 -140
  814. package/src/engine/ecs/ik/IKProblem.js +42 -42
  815. package/src/engine/ecs/ik/InverseKinematicsSystem.js +193 -193
  816. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +134 -134
  817. package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.js +97 -97
  818. package/src/engine/ecs/ik/ik_bone_chain.js +85 -85
  819. package/src/engine/ecs/ik/ik_probe_terrain_contact.js +119 -119
  820. package/src/engine/ecs/parent/EntityNode.d.ts +1 -1
  821. package/src/engine/ecs/parent/EntityNode.js +2 -2
  822. package/src/engine/ecs/parent/ParentEntitySystem.js +255 -255
  823. package/src/engine/ecs/speaker/VoiceSystem.d.ts +2 -2
  824. package/src/engine/ecs/speaker/VoiceSystem.d.ts.map +1 -1
  825. package/src/engine/ecs/speaker/VoiceSystem.js +13 -7
  826. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +269 -269
  827. package/src/engine/ecs/storage/binary/object-v2/BinaryObjectDeSerializer.js +315 -315
  828. package/src/engine/ecs/storage/binary/object-v2/BinaryObjectSerializer.js +331 -331
  829. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +199 -199
  830. package/src/engine/ecs/storage/binary/object-v2/ObjectBinaryFormat.js +57 -57
  831. package/src/engine/ecs/storage/binary/object-v2/ObjectSerializationAdapter.js +56 -56
  832. package/src/engine/ecs/storage/binary/object-v2/README.md +110 -110
  833. package/src/engine/ecs/storage/binary/object-v2/objectStringCodec.js +48 -48
  834. package/src/engine/ecs/storage/binary/object-v2/serializerTestHarness.js +44 -44
  835. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  836. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +5 -1
  837. package/src/engine/ecs/terrain/TerrainClouds.js +97 -97
  838. package/src/engine/ecs/terrain/ecs/StubTerrainBuildWorker.js +65 -65
  839. package/src/engine/ecs/terrain/ecs/Terrain.d.ts +4 -0
  840. package/src/engine/ecs/terrain/ecs/Terrain.d.ts.map +1 -1
  841. package/src/engine/ecs/terrain/ecs/Terrain.js +790 -790
  842. package/src/engine/ecs/terrain/ecs/TerrainSystem.js +209 -209
  843. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +316 -316
  844. package/src/engine/ecs/terrain/ecs/layers/TerrainLayers.js +416 -416
  845. package/src/engine/ecs/terrain/ecs/splat/SplatMapping.js +525 -525
  846. package/src/engine/ecs/terrain/overlay/TerrainOverlay.js +302 -302
  847. package/src/engine/ecs/terrain/tiles/TerrainTile.js +561 -561
  848. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +896 -896
  849. package/src/engine/ecs/terrain/tiles/tile_geometry_to_meshlets.js +36 -36
  850. package/src/engine/ecs/terrain/util/loadVisibleTerrainTiles.js +129 -129
  851. package/src/engine/ecs/transform/Transform64.js +814 -814
  852. package/src/engine/ecs/transform/t64_copy_from_transform.js +35 -35
  853. package/src/engine/ecs/transform/t64_equals_transform.js +55 -55
  854. package/src/engine/ecs/transform/t64_look_rotation.js +36 -36
  855. package/src/engine/ecs/transform/t64_transform_direction.js +25 -25
  856. package/src/engine/ecs/transform/transform_copy_from_t64.js +28 -28
  857. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js +345 -345
  858. package/src/engine/ecs/transform-attachment/transform_attachment_find_descendant_by_name.js +65 -65
  859. package/src/engine/ecs/transform-attachment/transform_attachment_parent_of.js +24 -24
  860. package/src/engine/ecs/util/hideEntityGracefully.js +208 -208
  861. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -446
  862. package/src/engine/graphics/FULL_SCREEN_QUAD_VERTEX_SHADER.js +14 -14
  863. package/src/engine/graphics/FrameThrottle.js +2 -2
  864. package/src/engine/graphics/ecs/animation/animator/graph/AnimationState.js +207 -207
  865. package/src/engine/graphics/ecs/animation/animator/graph/AnimationTransition.js +151 -151
  866. package/src/engine/graphics/ecs/animation/animator/graph/definition/serialization/AnimationGraphDefinitionAssetLoader.js +22 -22
  867. package/src/engine/graphics/ecs/camera/Camera.js +165 -165
  868. package/src/engine/graphics/ecs/camera/camera_active_frustum.js +25 -25
  869. package/src/engine/graphics/ecs/camera/camera_find_active.js +28 -28
  870. package/src/engine/graphics/ecs/camera/camera_traverse_active.js +18 -18
  871. package/src/engine/graphics/ecs/camera/pp/PerfectPanner.js +170 -170
  872. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -68
  873. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +296 -296
  874. package/src/engine/graphics/ecs/decal/v2/Decal.js +220 -220
  875. package/src/engine/graphics/ecs/decal/v2/DecalSerializationAdapter.js +56 -56
  876. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_0_1.js +38 -38
  877. package/src/engine/graphics/ecs/light/shadow/compute_view_frustum_aabb_in_space.js +62 -62
  878. package/src/engine/graphics/ecs/light/shadow/extend_shadow_camera_near_for_casters.js +77 -77
  879. package/src/engine/graphics/ecs/make_bvh_depth_range_computer.js +33 -33
  880. package/src/engine/graphics/ecs/mesh-v2/DeferredBoundsQueue.js +88 -88
  881. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js +428 -428
  882. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.js +303 -303
  883. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.d.ts +1 -1
  884. package/src/engine/graphics/ecs/mesh-v2/aggregate/serialization/SGMeshSerializationAdapter.js +39 -39
  885. package/src/engine/graphics/ecs/mesh-v2/sg_hierarchy_compute_bounding_box_via_parent_entity.js +36 -36
  886. package/src/engine/graphics/ecs/path/tube/build/GeometryStream.js +94 -94
  887. package/src/engine/graphics/ecs/path/tube/build/build_stream_catmullrom.js +310 -310
  888. package/src/engine/graphics/ecs/path/tube/build/build_stream_linear.js +73 -73
  889. package/src/engine/graphics/ecs/path/tube/build/computeFrenetFrames.js +199 -199
  890. package/src/engine/graphics/ecs/path/tube/build/makeTubeStream.js +217 -217
  891. package/src/engine/graphics/ecs/path/tube/build/make_cap.js +340 -340
  892. package/src/engine/graphics/ecs/path/tube/build/make_ring_faces.js +40 -40
  893. package/src/engine/graphics/ecs/path/tube/build/make_ring_vertices.js +153 -153
  894. package/src/engine/graphics/ecs/render_layers_compute_depth_range.js +64 -64
  895. package/src/engine/graphics/ecs/trail3d/Trail3D.js +483 -483
  896. package/src/engine/graphics/ecs/trail3d/Trail3DFlags.js +26 -26
  897. package/src/engine/graphics/ecs/trail3d/advance_trail.js +42 -42
  898. package/src/engine/graphics/ecs/trail3d/make_gradient_stroke.js +58 -58
  899. package/src/engine/graphics/ecs/trail3d/seed_trail_stroke.js +90 -90
  900. package/src/engine/graphics/ecs/trail3d/seed_trail_tube.js +75 -75
  901. package/src/engine/graphics/ecs/trail3d/trail_head_position.js +19 -19
  902. package/src/engine/graphics/ecs/water/Water.js +104 -104
  903. package/src/engine/graphics/geometry/AttributeData.js +84 -84
  904. package/src/engine/graphics/geometry/buffered/geometry_compute_vertex_normals_indexed.js +49 -49
  905. package/src/engine/graphics/geometry/buffered/query/GeometrySpatialQueryAccelerator.js +288 -288
  906. package/src/engine/graphics/geometry/buffered/query/nearest_point_on_triangle_record.js +87 -87
  907. package/src/engine/graphics/geometry/buffered/read_triangle_vertices.js +102 -102
  908. package/src/engine/graphics/geometry/bvh/buffered/bvh32_from_buffered_geometry.js +52 -52
  909. package/src/engine/graphics/geometry/bvh/buffered/bvh32_from_indexed_geometry.js +20 -20
  910. package/src/engine/graphics/geometry/bvh/buffered/bvh32_from_unindexed_geometry.js +19 -19
  911. package/src/engine/graphics/geometry/optimization/geometry_optimize_uv_tension.js +135 -135
  912. package/src/engine/graphics/impostors/voxel/README.md +149 -149
  913. package/src/engine/graphics/impostors/voxel/VoxelImpostorDescription.js +141 -141
  914. package/src/engine/graphics/particles/node-based/codegen/glsl/getTypeByteSize.js +20 -20
  915. package/src/engine/graphics/particles/node-based/nodes/FloatConstant.d.ts +2 -0
  916. package/src/engine/graphics/particles/node-based/nodes/FloatConstant.d.ts.map +1 -1
  917. package/src/engine/graphics/particles/node-based/nodes/ReadFloatUniform.d.ts +2 -0
  918. package/src/engine/graphics/particles/node-based/nodes/ReadFloatUniform.d.ts.map +1 -1
  919. package/src/engine/graphics/particles/node-based/nodes/ReadVelocity.d.ts +2 -0
  920. package/src/engine/graphics/particles/node-based/nodes/ReadVelocity.d.ts.map +1 -1
  921. package/src/engine/graphics/particles/node-based/nodes/ShaderNode.d.ts +1 -2
  922. package/src/engine/graphics/particles/node-based/nodes/ShaderNode.d.ts.map +1 -1
  923. package/src/engine/graphics/particles/node-based/nodes/Vector3Constant.d.ts +2 -0
  924. package/src/engine/graphics/particles/node-based/nodes/Vector3Constant.d.ts.map +1 -1
  925. package/src/engine/graphics/particles/node-based/nodes/Vector3Merge.d.ts +2 -0
  926. package/src/engine/graphics/particles/node-based/nodes/Vector3Merge.d.ts.map +1 -1
  927. package/src/engine/graphics/particles/node-based/nodes/Vector3Split.d.ts +2 -0
  928. package/src/engine/graphics/particles/node-based/nodes/Vector3Split.d.ts.map +1 -1
  929. package/src/engine/graphics/particles/node-based/nodes/WriteVelocity.d.ts +2 -0
  930. package/src/engine/graphics/particles/node-based/nodes/WriteVelocity.d.ts.map +1 -1
  931. package/src/engine/graphics/particles/node-based/nodes/attribute/ReadVector3Attribute.d.ts +2 -0
  932. package/src/engine/graphics/particles/node-based/nodes/attribute/ReadVector3Attribute.d.ts.map +1 -1
  933. package/src/engine/graphics/particles/node-based/nodes/attribute/WriteVector3Attribute.d.ts +2 -0
  934. package/src/engine/graphics/particles/node-based/nodes/attribute/WriteVector3Attribute.d.ts.map +1 -1
  935. package/src/engine/graphics/particles/node-based/nodes/math/AddFloatNode.d.ts +2 -0
  936. package/src/engine/graphics/particles/node-based/nodes/math/AddFloatNode.d.ts.map +1 -1
  937. package/src/engine/graphics/particles/node-based/nodes/math/Vector3Add.d.ts +2 -0
  938. package/src/engine/graphics/particles/node-based/nodes/math/Vector3Add.d.ts.map +1 -1
  939. package/src/engine/graphics/particles/node-based/nodes/math/Vector3Divide.d.ts +2 -0
  940. package/src/engine/graphics/particles/node-based/nodes/math/Vector3Divide.d.ts.map +1 -1
  941. package/src/engine/graphics/particles/node-based/nodes/math/Vector3Multiply.d.ts +2 -0
  942. package/src/engine/graphics/particles/node-based/nodes/math/Vector3Multiply.d.ts.map +1 -1
  943. package/src/engine/graphics/particles/node-based/nodes/math/Vector3Subtract.d.ts +2 -0
  944. package/src/engine/graphics/particles/node-based/nodes/math/Vector3Subtract.d.ts.map +1 -1
  945. package/src/engine/graphics/particles/node-based/nodes/noise/CurlNoiseNode.d.ts +2 -0
  946. package/src/engine/graphics/particles/node-based/nodes/noise/CurlNoiseNode.d.ts.map +1 -1
  947. package/src/engine/graphics/particles/node-based/nodes/particle_data_type_component_count.js +45 -45
  948. package/src/engine/graphics/particles/particular/engine/ParticularEngine.js +229 -229
  949. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts +2 -10
  950. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts.map +1 -1
  951. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js +1149 -1149
  952. package/src/engine/graphics/particles/particular/engine/emitter/ParticlePool.js +472 -472
  953. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTable.js +2 -2
  954. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTableFlags.js +2 -2
  955. package/src/engine/graphics/particles/particular/engine/parameter/ParameterSet.d.ts.map +1 -1
  956. package/src/engine/graphics/particles/particular/engine/parameter/ParameterSet.js +3 -5
  957. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.js +135 -135
  958. package/src/engine/graphics/particles/particular/engine/utils/distribute_points_on_indexed_triangles.js +168 -168
  959. package/src/engine/graphics/render/forward_plus/plugin/forwardPlusMaterialMarker.js +18 -18
  960. package/src/engine/graphics/render/forward_plus/read_frustum_corner.js +2 -2
  961. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +219 -219
  962. package/src/engine/graphics/render/frame_graph/FrameGraph.js +900 -900
  963. package/src/engine/graphics/render/frame_graph/FrameGraphScope.js +72 -72
  964. package/src/engine/graphics/render/frame_graph/FrameGraphState.js +39 -39
  965. package/src/engine/graphics/render/frame_graph/ResourceEntry.js +96 -96
  966. package/src/engine/graphics/render/gizmo/GizmoShapeBuffer.js +105 -105
  967. package/src/engine/graphics/render/gizmo/GizmoShapeRenderingInterface.js +162 -162
  968. package/src/engine/graphics/render/layers/RenderLayer.js +95 -95
  969. package/src/engine/graphics/sh3/build_injected_shader.js +56 -56
  970. package/src/engine/graphics/sh3/grid_dilate_average.js +175 -175
  971. package/src/engine/graphics/sh3/path_tracer/BufferedGeometryBVH.js +506 -506
  972. package/src/engine/graphics/sh3/path_tracer/PathTracedMesh.js +161 -161
  973. package/src/engine/graphics/sh3/path_tracer/PathTracedRenderer.js +205 -205
  974. package/src/engine/graphics/sh3/path_tracer/PathTracedScene.js +698 -698
  975. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +490 -490
  976. package/src/engine/graphics/sh3/path_tracer/geometry/compute_triangle_group_aabb3.js +2 -2
  977. package/src/engine/graphics/sh3/path_tracer/make_sky_hosek.js +45 -45
  978. package/src/engine/graphics/sh3/path_tracer/populate_path_traced_scene_from_ecd.js +73 -73
  979. package/src/engine/graphics/sh3/path_tracer/ray_hit_apply_transform.js +32 -32
  980. package/src/engine/graphics/sh3/path_tracer/texture/apply_texture_clamping_to_coordinate.js +28 -28
  981. package/src/engine/graphics/sh3/path_tracer/texture/sample_material.js +142 -142
  982. package/src/engine/graphics/sh3/sky/hosek/render_hosek_sky_to_equirectangular.js +70 -70
  983. package/src/engine/graphics/sh3/sky/hosek/sky_hosek_compute_irradiance_by_direction.js +388 -388
  984. package/src/engine/graphics/shaders/chunks/glsl_octahedral_impostor.js +277 -277
  985. package/src/engine/graphics/shaders/chunks/glsl_voxel_impostor_splat.js +57 -57
  986. package/src/engine/graphics/shadows/vsm.glsl.js +43 -43
  987. package/src/engine/graphics/texture/3d/scs3d_sample_linear3.js +81 -81
  988. package/src/engine/graphics/texture/EnvironmentTextureProjection.js +15 -15
  989. package/src/engine/graphics/texture/channel_count_to_format_prefix.js +26 -26
  990. package/src/engine/graphics/texture/isImageBitmap.js +2 -2
  991. package/src/engine/graphics/texture/reflection/convolve_equirectangular_reflection.js +189 -189
  992. package/src/engine/graphics/texture/reflection/equirectangular_reflection_roughness.js +51 -51
  993. package/src/engine/graphics/texture/sampler/HarmonicDiffusionGrid.js +145 -145
  994. package/src/engine/graphics/texture/sampler/Sampler2DArray.js +172 -172
  995. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +466 -466
  996. package/src/engine/graphics/texture/sampler/compare/flip/FlipParameters.js +18 -18
  997. package/src/engine/graphics/texture/sampler/compare/flip/FlipPooling.js +33 -33
  998. package/src/engine/graphics/texture/sampler/compare/flip/flip_build_feature_kernel.js +147 -147
  999. package/src/engine/graphics/texture/sampler/compare/flip/flip_build_spatial_kernel.js +177 -177
  1000. package/src/engine/graphics/texture/sampler/compare/flip/flip_compute_color_difference.js +104 -104
  1001. package/src/engine/graphics/texture/sampler/compare/flip/flip_compute_feature_difference.js +76 -76
  1002. package/src/engine/graphics/texture/sampler/compare/flip/flip_compute_pixels_per_degree.js +34 -34
  1003. package/src/engine/graphics/texture/sampler/compare/flip/flip_constants.js +77 -77
  1004. package/src/engine/graphics/texture/sampler/compare/flip/flip_filter_feature.js +48 -48
  1005. package/src/engine/graphics/texture/sampler/compare/flip/flip_filter_spatial.js +86 -86
  1006. package/src/engine/graphics/texture/sampler/compare/flip/flip_pool.js +71 -71
  1007. package/src/engine/graphics/texture/sampler/compare/flip/flip_srgb_to_ycxcz.js +55 -55
  1008. package/src/engine/graphics/texture/sampler/compare/flip/sampler2d_flip_ldr.js +80 -80
  1009. package/src/engine/graphics/texture/sampler/filter/SeparableKernel.js +82 -82
  1010. package/src/engine/graphics/texture/sampler/filter/sampler2d_convolve_separable.js +122 -122
  1011. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +148 -148
  1012. package/src/engine/graphics/texture/sampler/sampler2d_channel_apply_lut.js +82 -82
  1013. package/src/engine/graphics/texture/sampler/sampler2d_combine.js +67 -67
  1014. package/src/engine/graphics/texture/sampler/sampler2d_linear_f16_to_srgb_u8.js +54 -54
  1015. package/src/engine/graphics/texture/sampler/sampler2d_sample_equirectangular_direction.js +63 -63
  1016. package/src/engine/graphics/texture/sampler/sampler2d_srgb_to_linear.js +33 -33
  1017. package/src/engine/graphics/texture/sampler/sampler2d_srgb_u8_to_linear_f16.js +68 -68
  1018. package/src/engine/graphics/texture/shade_image_preview.js +103 -103
  1019. package/src/engine/graphics/trail/TrailSpawnMode.js +20 -20
  1020. package/src/engine/graphics/trail/tube/TubeX.js +573 -573
  1021. package/src/engine/graphics/trail/tube/TubeXMaterialSpec.js +58 -58
  1022. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.js +34 -34
  1023. package/src/engine/graphics/trail/tube/tube_attributes_spec.js +69 -69
  1024. package/src/engine/graphics/trail/tube/tube_frame.js +83 -83
  1025. package/src/engine/graphics3/AnimationGraphSystem3.js +329 -329
  1026. package/src/engine/graphics3/AnimationSystem3.js +352 -352
  1027. package/src/engine/graphics3/CameraSystem3.js +170 -170
  1028. package/src/engine/graphics3/DecalSystem3.js +441 -441
  1029. package/src/engine/graphics3/FogOfWarSystem3.js +265 -265
  1030. package/src/engine/graphics3/GraphicsEngine3.js +776 -776
  1031. package/src/engine/graphics3/HighlightSystem3.js +241 -241
  1032. package/src/engine/graphics3/LightSystem3.js +211 -211
  1033. package/src/engine/graphics3/MeshSystem3.js +646 -646
  1034. package/src/engine/graphics3/ParticleEmitterSystem3.js +289 -289
  1035. package/src/engine/graphics3/PathDisplaySystem3.js +309 -309
  1036. package/src/engine/graphics3/PickingQuery.js +48 -48
  1037. package/src/engine/graphics3/PickingResult.js +47 -47
  1038. package/src/engine/graphics3/PickingSystem3.js +273 -273
  1039. package/src/engine/graphics3/ShadeCameraAdapter.js +55 -55
  1040. package/src/engine/graphics3/ShadedGeometrySystem3.js +169 -169
  1041. package/src/engine/graphics3/TerrainSystem3.js +566 -566
  1042. package/src/engine/graphics3/TooltipComponentSystem3.js +328 -328
  1043. package/src/engine/graphics3/Trail3DSystem3.js +221 -221
  1044. package/src/engine/graphics3/VolumetricLightMap.d.ts +90 -0
  1045. package/src/engine/graphics3/VolumetricLightMap.d.ts.map +1 -0
  1046. package/src/engine/graphics3/VolumetricLightMap.js +160 -0
  1047. package/src/engine/graphics3/VolumetricLightMapSerializationAdapter.d.ts +32 -0
  1048. package/src/engine/graphics3/VolumetricLightMapSerializationAdapter.d.ts.map +1 -0
  1049. package/src/engine/graphics3/VolumetricLightMapSerializationAdapter.js +67 -0
  1050. package/src/engine/graphics3/VolumetricLightMapSystem3.d.ts +98 -0
  1051. package/src/engine/graphics3/VolumetricLightMapSystem3.d.ts.map +1 -0
  1052. package/src/engine/graphics3/VolumetricLightMapSystem3.js +261 -0
  1053. package/src/engine/graphics3/WaterSystem3.js +293 -293
  1054. package/src/engine/graphics3/animation/ClipPlayback.js +84 -84
  1055. package/src/engine/graphics3/animation/wrap_clip_time.js +23 -23
  1056. package/src/engine/graphics3/animation/write_graph_playback.js +48 -48
  1057. package/src/engine/graphics3/camera_sync_from_transform.js +29 -29
  1058. package/src/engine/graphics3/debug/gizmo_view_from_camera.js +48 -48
  1059. package/src/engine/graphics3/decal/DECAL_CLUSTER.js +61 -61
  1060. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.js +117 -117
  1061. package/src/engine/graphics3/decal/DecalAtlas.js +194 -194
  1062. package/src/engine/graphics3/decal/DecalAtlasFill.js +32 -32
  1063. package/src/engine/graphics3/decal/GPUDecalRenderer.js +360 -360
  1064. package/src/engine/graphics3/decal/chunk_decal_intersects_frustum.js +59 -59
  1065. package/src/engine/graphics3/decal/chunk_decal_sampling_info.js +57 -57
  1066. package/src/engine/graphics3/decal/chunk_decal_surface_frame.js +54 -54
  1067. package/src/engine/graphics3/decal/decal_atlas_border.js +69 -69
  1068. package/src/engine/graphics3/decal/decal_gpu_records.js +120 -120
  1069. package/src/engine/graphics3/decal/decal_material_samplers.js +109 -109
  1070. package/src/engine/graphics3/decal/graph_build_decal_clusters.js +212 -212
  1071. package/src/engine/graphics3/decal/shader_cluster_assign_decals.js +207 -207
  1072. package/src/engine/graphics3/decal/shader_cull_decals_frustum.js +108 -108
  1073. package/src/engine/graphics3/highlight/GPUHighlightRenderer.js +151 -151
  1074. package/src/engine/graphics3/highlight/HIGHLIGHT_OUTLINE.js +23 -23
  1075. package/src/engine/graphics3/highlight/chunk_highlight_lookup.js +53 -53
  1076. package/src/engine/graphics3/highlight/pack_highlight_color.js +44 -44
  1077. package/src/engine/graphics3/highlight/pack_highlight_table.js +74 -74
  1078. package/src/engine/graphics3/highlight/shader_highlight_outline.js +150 -150
  1079. package/src/engine/graphics3/load_cube_environment.js +68 -68
  1080. package/src/engine/graphics3/make_default_environment.js +107 -107
  1081. package/src/engine/graphics3/particles/particle_gpu_records.js +221 -221
  1082. package/src/engine/graphics3/path/build_tube_path_streams.js +98 -98
  1083. package/src/engine/graphics3/path/tube_stream_write_geometry.js +90 -90
  1084. package/src/engine/graphics3/preview/ModelPreviewFraming.js +35 -35
  1085. package/src/engine/graphics3/preview/make_model_preview_scene.js +154 -154
  1086. package/src/engine/graphics3/preview/make_model_thumbnail.js +48 -48
  1087. package/src/engine/graphics3/preview/model_preview_framing.js +70 -70
  1088. package/src/engine/graphics3/shade_bundle_bounds.js +55 -55
  1089. package/src/engine/graphics3/shade_camera_projection_ray.js +77 -77
  1090. package/src/engine/graphics3/shade_node_to_entity_composition.js +84 -84
  1091. package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.js +481 -481
  1092. package/src/engine/graphics3/terrain/chunk_terrain_grid_overlay.js +61 -61
  1093. package/src/engine/graphics3/terrain/chunk_terrain_splat_mix.js +66 -66
  1094. package/src/engine/graphics3/terrain/pack_terrain_row_table.js +53 -53
  1095. package/src/engine/graphics3/terrain/shader_terrain_splat.js +215 -215
  1096. package/src/engine/graphics3/trail/trail_tube_write_geometry.js +158 -158
  1097. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +717 -717
  1098. package/src/engine/input/analog/apply_radial_dead_zone.js +38 -38
  1099. package/src/engine/input/devices/GamepadDevice.js +466 -466
  1100. package/src/engine/input/devices/ManualInputDevice.js +152 -152
  1101. package/src/engine/input/devices/PointerCoordinates.js +26 -26
  1102. package/src/engine/input/devices/events/GamepadEvents.js +10 -10
  1103. package/src/engine/input/devices/gamepad/GamepadAxes.js +19 -19
  1104. package/src/engine/input/devices/gamepad/GamepadButtons.js +29 -29
  1105. package/src/engine/input/devices/gamepad/GamepadCoordinates.js +17 -17
  1106. package/src/engine/input/devices/gamepad/GamepadDeviceLayout.js +323 -323
  1107. package/src/engine/input/devices/gamepad/GamepadHandle.js +286 -286
  1108. package/src/engine/input/devices/gamepad/GamepadLayout.js +160 -160
  1109. package/src/engine/input/devices/gamepad/GamepadStandardMapping.js +31 -31
  1110. package/src/engine/input/devices/gamepad/GamepadStick.js +169 -169
  1111. package/src/engine/input/devices/gamepad/GamepadStickCalibration.js +119 -119
  1112. package/src/engine/input/devices/gamepad/GamepadStickCalibrator.js +250 -250
  1113. package/src/engine/input/devices/gamepad/gamepad_layout_database.js +144 -144
  1114. package/src/engine/input/devices/gamepad/gamepad_read_hat_axis.js +61 -61
  1115. package/src/engine/input/devices/gamepad/gamepad_write_dpad_vector.js +30 -30
  1116. package/src/engine/input/devices/gamepad/prototypeGamepadTester.js +2124 -2124
  1117. package/src/engine/input/devices/mouse/MouseButtons.js +22 -22
  1118. package/src/engine/input/ecs/components/Input.js +118 -118
  1119. package/src/engine/input/ecs/components/InputController.js +78 -78
  1120. package/src/engine/input/ecs/ism/ISM_DESIGN.md +1523 -1523
  1121. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +627 -627
  1122. package/src/engine/input/ecs/ism/InputActionEvent.js +40 -40
  1123. package/src/engine/input/ecs/ism/InputBinding.js +71 -71
  1124. package/src/engine/input/ecs/ism/InputCoordinateKind.js +25 -25
  1125. package/src/engine/input/ecs/ism/InputCoordinateSource.js +86 -86
  1126. package/src/engine/input/ecs/ism/InputDirection.js +33 -33
  1127. package/src/engine/input/ecs/ism/InputMapProgram.js +236 -236
  1128. package/src/engine/input/ecs/ism/InputMapSystem.js +976 -976
  1129. package/src/engine/input/ecs/ism/InputOcclusion.js +31 -31
  1130. package/src/engine/input/ecs/ism/InputSource.js +67 -67
  1131. package/src/engine/input/ecs/ism/InputSourceTable.js +107 -107
  1132. package/src/engine/input/ecs/ism/coordinate/InputCoordinateChannel.js +93 -93
  1133. package/src/engine/input/ecs/ism/coordinate/InputCoordinateChannelType.js +11 -11
  1134. package/src/engine/input/ecs/ism/coordinate/InputCoordinateDevice.js +153 -153
  1135. package/src/engine/input/ecs/ism/coordinate/InputCoordinateSwitches.js +160 -160
  1136. package/src/engine/input/ecs/ism/device/GAMEPAD_INPUT_DEVICE_ID.js +9 -9
  1137. package/src/engine/input/ecs/ism/device/GamepadInputDeviceAdapter.js +88 -88
  1138. package/src/engine/input/ecs/ism/device/InputDeviceAdapter.js +72 -72
  1139. package/src/engine/input/ecs/ism/device/KEYBOARD_INPUT_DEVICE_ID.js +9 -9
  1140. package/src/engine/input/ecs/ism/device/KeyboardInputDeviceAdapter.js +63 -63
  1141. package/src/engine/input/ecs/ism/device/ManualInputDeviceAdapter.js +55 -55
  1142. package/src/engine/input/ecs/ism/device/POINTER_INPUT_DEVICE_ID.js +9 -9
  1143. package/src/engine/input/ecs/ism/device/PointerInputDeviceAdapter.js +160 -160
  1144. package/src/engine/input/ecs/ism/map/InputBinding.js +88 -88
  1145. package/src/engine/input/ecs/ism/map/InputCoordinateBinding.js +102 -102
  1146. package/src/engine/input/ecs/ism/map/InputLayer.js +220 -220
  1147. package/src/engine/input/ecs/ism/map/InputMap.js +331 -331
  1148. package/src/engine/input/ecs/ism/prototypeInputMap.js +309 -309
  1149. package/src/engine/input/ecs/ism/trigger/InputTrigger.js +148 -148
  1150. package/src/engine/input/ecs/ism/trigger/InputTriggerChord.js +173 -173
  1151. package/src/engine/input/ecs/ism/trigger/InputTriggerGamepadButton.js +37 -37
  1152. package/src/engine/input/ecs/ism/trigger/InputTriggerHold.js +176 -176
  1153. package/src/engine/input/ecs/ism/trigger/InputTriggerKey.js +39 -39
  1154. package/src/engine/input/ecs/ism/trigger/InputTriggerMouseButton.js +38 -38
  1155. package/src/engine/input/ecs/ism/trigger/InputTriggerRelease.js +120 -120
  1156. package/src/engine/input/ecs/ism/trigger/InputTriggerSequence.js +315 -315
  1157. package/src/engine/input/ecs/ism/trigger/InputTriggerSwitch.js +105 -105
  1158. package/src/engine/input/ecs/ism/trigger/InputTriggerType.js +17 -17
  1159. package/src/engine/input/ecs/systems/InputControllerSystem.js +253 -253
  1160. package/src/engine/input/ecs/systems/InputSystem.js +162 -162
  1161. package/src/engine/intelligence/behavior/primitive/FailingBehaviorSerializationAdapter.js +25 -25
  1162. package/src/engine/intelligence/behavior/primitive/SucceedingBehaviorSerializationAdapter.js +26 -26
  1163. package/src/engine/intelligence/behavior/util/AsynchronousDelayAction.js +55 -55
  1164. package/src/engine/interpolation/BinaryInterpolationAdapter.js +56 -56
  1165. package/src/engine/interpolation/Interpoland.js +49 -49
  1166. package/src/engine/interpolation/Interpolated.js +149 -149
  1167. package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -61
  1168. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -57
  1169. package/src/engine/interpolation/pose_interpoland.js +27 -27
  1170. package/src/engine/navigation/ecs/path_following/PathFollower.js +222 -222
  1171. package/src/engine/navigation/mesh/NavigationMesh.js +145 -145
  1172. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +661 -0
  1173. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +229 -229
  1174. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.d.ts.map +1 -1
  1175. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.js +628 -520
  1176. package/src/engine/network/CONGESTION_CONTROL.md +87 -87
  1177. package/src/engine/network/README.md +52 -52
  1178. package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +43 -43
  1179. package/src/engine/network/adapters/TransformInterpolationAdapter.js +84 -84
  1180. package/src/engine/network/adapters/TransformReplicationAdapter.js +87 -87
  1181. package/src/engine/network/adapters/Vector3InterpolationAdapter.js +46 -46
  1182. package/src/engine/network/convertPathToURL.js +2 -2
  1183. package/src/engine/network/core/MalformedPacketError.js +26 -26
  1184. package/src/engine/network/core/quantize/quantize_float.js +73 -73
  1185. package/src/engine/network/core/quantize/quantize_position.js +65 -65
  1186. package/src/engine/network/core/sequence/ack_bitfield.js +77 -77
  1187. package/src/engine/network/core/sequence/seq16.js +69 -69
  1188. package/src/engine/network/core/sequence/seq32.js +73 -73
  1189. package/src/engine/network/ecs/NetworkSystem.js +84 -84
  1190. package/src/engine/network/ecs/components/NetworkIdentity.js +73 -73
  1191. package/src/engine/network/ecs/owner_authorization.js +55 -55
  1192. package/src/engine/network/ecs/serialization/NetworkIdentitySerializationAdapter.js +64 -64
  1193. package/src/engine/network/replication/ScopeFilter.js +71 -71
  1194. package/src/engine/network/sim/ActionLog.js +152 -152
  1195. package/src/engine/network/sim/SimAction.js +273 -273
  1196. package/src/engine/network/sim/Snapshotter.js +222 -222
  1197. package/src/engine/network/state/FrameBufferRing.js +168 -168
  1198. package/src/engine/network/state/InputRing.js +134 -134
  1199. package/src/engine/network/state/PriorityAccumulator.js +180 -180
  1200. package/src/engine/network/time/TimeDilation.js +62 -62
  1201. package/src/engine/network/transport/Channel.js +336 -336
  1202. package/src/engine/network/transport/Transport.js +203 -203
  1203. package/src/engine/network/transport/adapters/NodeUDPTransport.js +186 -186
  1204. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +105 -105
  1205. package/src/engine/network/transport/fragments/fragment_send.js +147 -147
  1206. package/src/engine/network/transport/fragments/packet_size.js +101 -101
  1207. package/src/engine/network/xhr.js +23 -23
  1208. package/src/engine/physics/BULLET_REVIEW.md +945 -945
  1209. package/src/engine/physics/CANNON_REVIEW.md +1300 -1300
  1210. package/src/engine/physics/CONSTRAINT_SOLVER_BENCH_LOG.md +208 -208
  1211. package/src/engine/physics/CONSTRAINT_SOLVER_IMPROVEMENTS_PLAN.md +364 -364
  1212. package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -287
  1213. package/src/engine/physics/JOLT_REVIEW.md +913 -913
  1214. package/src/engine/physics/PLAN.md +1120 -1094
  1215. package/src/engine/physics/RAPIER_REVIEW.md +934 -934
  1216. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -642
  1217. package/src/engine/physics/REVIEW_002.md +151 -151
  1218. package/src/engine/physics/REVIEW_003.md +166 -166
  1219. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +97 -97
  1220. package/src/engine/physics/constraint/DofMode.js +35 -35
  1221. package/src/engine/physics/constraint/solve_constraints.js +1 -1
  1222. package/src/engine/physics/contact/combine_material.js +35 -35
  1223. package/src/engine/physics/ecs/BodyKind.js +29 -29
  1224. package/src/engine/physics/ecs/Collider.js +177 -177
  1225. package/src/engine/physics/ecs/ColliderFlags.js +15 -15
  1226. package/src/engine/physics/ecs/ColliderSerializationAdapter.js +37 -37
  1227. package/src/engine/physics/ecs/Joint.js +475 -475
  1228. package/src/engine/physics/ecs/JointSerializationAdapter.js +72 -72
  1229. package/src/engine/physics/ecs/PhysicsSystem.d.ts +3 -0
  1230. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  1231. package/src/engine/physics/ecs/PhysicsSystem.js +7 -2
  1232. package/src/engine/physics/ecs/RigidBodySerializationUpgrader_0_1.js +37 -37
  1233. package/src/engine/physics/ecs/SleepState.js +12 -12
  1234. package/src/engine/physics/ecs/find_non_finite_physics_state.js +76 -76
  1235. package/src/engine/physics/fluid/FluidField.js +867 -867
  1236. package/src/engine/physics/fluid/FluidSimulator.js +832 -832
  1237. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +243 -243
  1238. package/src/engine/physics/fluid/SliceVisualiser.js +190 -190
  1239. package/src/engine/physics/fluid/ecs/FluidComponent.js +239 -239
  1240. package/src/engine/physics/fluid/ecs/FluidEffectorsComponent.js +89 -89
  1241. package/src/engine/physics/fluid/ecs/FluidObstacle.js +97 -97
  1242. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  1243. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.js +348 -348
  1244. package/src/engine/physics/fluid/ecs/FluidSystem.js +142 -142
  1245. package/src/engine/physics/fluid/ecs/FluidWorkerCore.js +123 -123
  1246. package/src/engine/physics/fluid/ecs/WorkerFluidSystem.js +395 -395
  1247. package/src/engine/physics/fluid/ecs/fluid.worker.js +12 -12
  1248. package/src/engine/physics/fluid/ecs/fluid_build_world_to_grid.js +32 -32
  1249. package/src/engine/physics/fluid/ecs/fluid_gather_visible_effectors.js +47 -47
  1250. package/src/engine/physics/fluid/ecs/fluid_reanchor_field.js +85 -85
  1251. package/src/engine/physics/fluid/ecs/fluid_sync_effectors_from_transform.js +16 -16
  1252. package/src/engine/physics/fluid/ecs/fluid_worker_protocol.js +111 -111
  1253. package/src/engine/physics/fluid/ecs/makeFluidWorker.js +18 -18
  1254. package/src/engine/physics/fluid/effector/AbstractFluidEffector.js +81 -81
  1255. package/src/engine/physics/fluid/effector/GlobalFluidEffector.js +202 -202
  1256. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +257 -257
  1257. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +405 -405
  1258. package/src/engine/physics/fluid/prototype.js +126 -126
  1259. package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_scalar.js +143 -143
  1260. package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_velocity.js +162 -162
  1261. package/src/engine/physics/fluid/solver/v3_grid_advect_sl_velocity.js +107 -107
  1262. package/src/engine/physics/fluid/solver/v3_grid_apply_vorticity_confinement.js +131 -131
  1263. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +90 -90
  1264. package/src/engine/physics/fluid/solver/v3_grid_limit8_at_indices.js +38 -38
  1265. package/src/engine/physics/fluid/solver/v3_grid_patch_edges_constant.js +77 -77
  1266. package/src/engine/physics/fluid/solver/v3_grid_patch_edges_uniform.js +113 -113
  1267. package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.js +92 -92
  1268. package/src/engine/physics/fluid/solver/v3_grid_shift_in_place.js +107 -107
  1269. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +135 -135
  1270. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +511 -511
  1271. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +108 -108
  1272. package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.js +317 -317
  1273. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.js +236 -236
  1274. package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.js +217 -217
  1275. package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.js +174 -174
  1276. package/src/engine/physics/fluid/solver/v3_mac_clip_trace.js +95 -95
  1277. package/src/engine/physics/fluid/solver/v3_mac_compute_divergence.js +77 -77
  1278. package/src/engine/physics/fluid/solver/v3_mac_compute_face_solid.js +131 -131
  1279. package/src/engine/physics/fluid/solver/v3_mac_subtract_pressure_gradient.js +104 -104
  1280. package/src/engine/physics/inertia/world_inverse_inertia.js +160 -160
  1281. package/src/engine/physics/integration/integrate_position.js +97 -97
  1282. package/src/engine/physics/narrowphase/box_box_manifold.js +681 -681
  1283. package/src/engine/physics/narrowphase/box_triangle_contact.js +906 -906
  1284. package/src/engine/physics/narrowphase/capsule_triangle_contact.js +395 -395
  1285. package/src/engine/physics/narrowphase/compute_penetration.js +369 -369
  1286. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -55
  1287. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -37
  1288. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -117
  1289. package/src/engine/physics/narrowphase/narrowphase_step.js +1738 -1738
  1290. package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +74 -74
  1291. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +12 -11
  1292. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
  1293. package/src/engine/physics/narrowphase/refine_ray_hit.js +228 -36
  1294. package/src/engine/physics/narrowphase/sphere_sphere_contact.js +52 -52
  1295. package/src/engine/physics/narrowphase/sphere_triangle_contact.js +138 -138
  1296. package/src/engine/physics/persistence/solver_caches.js +309 -309
  1297. package/src/engine/physics/queries/overlap_shape.js +187 -187
  1298. package/src/engine/physics/queries/raycast.d.ts +7 -4
  1299. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  1300. package/src/engine/physics/queries/raycast.js +11 -5
  1301. package/src/engine/physics/queries/shape_cast.js +417 -417
  1302. package/src/engine/physics/solver/apply_impulse_to_body.js +86 -86
  1303. package/src/engine/physics/solver/friction_cone.js +37 -37
  1304. package/src/engine/physics/solver/solve_contacts.js +1 -1
  1305. package/src/engine/platform/WebEnginePlatform.d.ts.map +1 -1
  1306. package/src/engine/platform/WebEnginePlatform.js +1 -2
  1307. package/src/engine/scene/SerializedScene.d.ts +2 -2
  1308. package/src/engine/scene/SerializedScene.d.ts.map +1 -1
  1309. package/src/engine/scene/transitionToScene.js +121 -121
  1310. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  1311. package/src/engine/simulation/Ticker.js +33 -0
  1312. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +345 -345
  1313. package/src/engine/sound/ecs/audio/SpatialAudioIndex.js +140 -140
  1314. package/src/engine/sound/simulation/AcousticSimulator.js +646 -646
  1315. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +432 -432
  1316. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +250 -250
  1317. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +322 -322
  1318. package/src/engine/sound/simulation/README.md +277 -277
  1319. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +261 -261
  1320. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +121 -121
  1321. package/src/engine/sound/simulation/configureAcousticSimulation.js +48 -48
  1322. package/src/engine/sound/simulation/core/AcousticMedium.js +113 -113
  1323. package/src/engine/sound/simulation/core/AcousticMediumSerializationAdapter.js +27 -27
  1324. package/src/engine/sound/simulation/core/AcousticOccluderIndex.js +312 -312
  1325. package/src/engine/sound/simulation/core/AcousticRegionIndex.js +159 -159
  1326. package/src/engine/sound/simulation/core/AcousticSolution.js +90 -90
  1327. package/src/engine/sound/simulation/core/AcousticSourceState.js +109 -109
  1328. package/src/engine/sound/simulation/core/AcousticVolumeRegion.js +73 -73
  1329. package/src/engine/sound/simulation/core/OcclusionSolver.js +112 -112
  1330. package/src/engine/sound/simulation/core/RayHit.js +50 -50
  1331. package/src/engine/sound/simulation/core/VolumeField.js +149 -149
  1332. package/src/engine/sound/simulation/definition/AcousticDirectivity.js +114 -114
  1333. package/src/engine/sound/simulation/definition/AcousticDirectivitySerializationAdapter.js +30 -30
  1334. package/src/engine/sound/simulation/definition/AcousticMaterial.js +122 -122
  1335. package/src/engine/sound/simulation/definition/AcousticMaterialSerializationAdapter.js +32 -32
  1336. package/src/engine/sound/simulation/ecs/AcousticBody.js +48 -48
  1337. package/src/engine/sound/simulation/ecs/AcousticBodySerializationAdapter.js +34 -34
  1338. package/src/engine/sound/simulation/ecs/AcousticProbeFieldSystem.js +64 -64
  1339. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.js +75 -75
  1340. package/src/engine/sound/simulation/ecs/AcousticVolume.js +74 -74
  1341. package/src/engine/sound/simulation/ecs/AcousticVolumeSerializationAdapter.js +37 -37
  1342. package/src/engine/sound/simulation/ecs/AcousticVolumeSystem.js +66 -66
  1343. package/src/engine/sound/simulation/probe/AcousticProbeField.js +578 -578
  1344. package/src/engine/sound/simulation/probe/AcousticProbeFieldSerializationAdapter.js +60 -60
  1345. package/src/engine/sound/simulation/probe/ProbeFieldPathingSolution.js +64 -64
  1346. package/src/engine/sound/simulation/probe/ProbeVisibilityRecheck.js +132 -132
  1347. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +103 -103
  1348. package/src/engine/sound/simulation/probe/acoustic_trace_energy.js +136 -136
  1349. package/src/engine/sound/simulation/probe/bakeProbeField.js +169 -169
  1350. package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +110 -110
  1351. package/src/engine/sound/simulation/probe/bakeReverbBands.js +110 -110
  1352. package/src/engine/sound/simulation/probe/bakeVisibility.js +148 -148
  1353. package/src/engine/sound/simulation/probe/echogram_band_rt60.js +83 -83
  1354. package/src/engine/sound/simulation/probe/probe_delaunay_edges.js +215 -215
  1355. package/src/engine/sound/simulation/probe/probe_densify_portals.js +140 -140
  1356. package/src/engine/sound/simulation/probe/probe_detect_portals.js +196 -196
  1357. package/src/engine/sound/simulation/probe/probe_edge_key.js +38 -38
  1358. package/src/engine/sound/simulation/probe/probe_edge_visible.js +41 -41
  1359. package/src/engine/sound/simulation/probe/probe_place_sdf_cover.js +82 -82
  1360. package/src/engine/sound/simulation/probe/probe_refine_connectivity.js +181 -181
  1361. package/src/engine/sound/simulation/probe/probe_sdf_grid.js +171 -171
  1362. package/src/engine/sound/simulation/probe/probe_visibility_augment.js +146 -146
  1363. package/src/engine/sound/simulation/probe/probe_visibility_bridge_components.js +167 -167
  1364. package/src/engine/sound/simulation/render/FoaProbeReverbRenderer.js +354 -354
  1365. package/src/engine/sound/simulation/render/ProbeReverbRenderer.js +231 -231
  1366. package/src/engine/sound/simulation/render/foaReverbImpulseResponse.js +105 -105
  1367. package/src/engine/sound/simulation/render/reverbImpulseResponse.js +66 -66
  1368. package/src/engine/sound/simulation/render/reverb_band_tails.js +93 -93
  1369. package/src/engine/sound/simulation/serialization/populateAcousticSerializationRegistry.js +25 -25
  1370. package/src/engine/sound/sopra/README.md +643 -643
  1371. package/src/engine/sound/sopra/SopraEngine.js +432 -432
  1372. package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.js +71 -71
  1373. package/src/engine/sound/sopra/asset/BufferProvider.js +29 -29
  1374. package/src/engine/sound/sopra/asset/StubBufferProvider.js +58 -58
  1375. package/src/engine/sound/sopra/definition/BusDefinition.js +142 -142
  1376. package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.js +54 -54
  1377. package/src/engine/sound/sopra/definition/DuckingRule.js +106 -106
  1378. package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.js +31 -31
  1379. package/src/engine/sound/sopra/definition/EventDescription.js +262 -262
  1380. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +55 -55
  1381. package/src/engine/sound/sopra/definition/MixerSnapshot.js +83 -83
  1382. package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.js +39 -39
  1383. package/src/engine/sound/sopra/definition/ParameterDefinition.js +117 -117
  1384. package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.js +31 -31
  1385. package/src/engine/sound/sopra/definition/VoiceStealMode.js +18 -18
  1386. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +109 -109
  1387. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.js +185 -185
  1388. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.js +59 -59
  1389. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.js +128 -128
  1390. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +226 -226
  1391. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.js +54 -54
  1392. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +191 -191
  1393. package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.js +39 -39
  1394. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.js +79 -79
  1395. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.js +42 -42
  1396. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.js +77 -77
  1397. package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.js +27 -27
  1398. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +5 -0
  1399. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
  1400. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.js +125 -125
  1401. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.js +41 -41
  1402. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +24 -24
  1403. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +120 -120
  1404. package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.js +31 -31
  1405. package/src/engine/sound/sopra/definition/effect/EqEffect.js +128 -128
  1406. package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.js +29 -29
  1407. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +113 -113
  1408. package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.js +25 -25
  1409. package/src/engine/sound/sopra/runtime/BusGraph.js +231 -231
  1410. package/src/engine/sound/sopra/runtime/ParameterStore.js +98 -98
  1411. package/src/engine/sound/sopra/runtime/SopraPlaybackContext.js +68 -68
  1412. package/src/engine/sound/sopra/runtime/Voice.js +145 -145
  1413. package/src/engine/sound/sopra/runtime/VoiceManager.js +136 -136
  1414. package/src/engine/sound/sopra/runtime/VoicePool.js +17 -17
  1415. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +42 -42
  1416. package/src/engine/sound/sopra/serialization/sopraJSON.js +99 -99
  1417. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +49 -49
  1418. package/src/engine/sound/sopra/util/MockAudioContext.js +224 -224
  1419. package/src/engine/sound/sopra/util/fadeOutAndStop.js +60 -60
  1420. package/src/engine/ui/DraggableAspect.js +115 -115
  1421. package/src/engine/ui/apply_pointer_tilt_3d.js +50 -50
  1422. package/src/generation/COORDINATES.md +54 -54
  1423. package/src/generation/GridGenerationResource.js +24 -24
  1424. package/src/generation/GridTaskGroup.js +214 -214
  1425. package/src/generation/REVIEW_01_ACTION_PLAN.md +628 -628
  1426. package/src/generation/automata/CaveGeneratorCellularAutomata.js +79 -79
  1427. package/src/generation/automata/CellularAutomata.js +22 -22
  1428. package/src/generation/compute_writes_union.js +25 -25
  1429. package/src/generation/filtering/CellFilter.js +117 -117
  1430. package/src/generation/filtering/core/CellFilterBinaryOperation.js +37 -37
  1431. package/src/generation/filtering/core/CellFilterOperationTertiary.js +43 -43
  1432. package/src/generation/filtering/core/CellFilterUnaryOperation.js +29 -29
  1433. package/src/generation/filtering/numeric/process/computeCellFilterSobel.js +36 -36
  1434. package/src/generation/filtering/numeric/process/computeFilterSurfaceNormal.js +42 -42
  1435. package/src/generation/filtering/numeric/util/populateSampler2DFromCellFilter.js +18 -18
  1436. package/src/generation/filtering/numeric/util/write_cell_filter_to_sampler2d.js +47 -47
  1437. package/src/generation/filtering/util/cell_filter_weights_pick.js +54 -54
  1438. package/src/generation/grid/GridData.js +233 -233
  1439. package/src/generation/grid/coords/grid_to_texel.js +10 -10
  1440. package/src/generation/grid/coords/texel_to_grid.js +10 -10
  1441. package/src/generation/grid/generation/GridTaskApplyActionToCells.js +90 -90
  1442. package/src/generation/grid/generation/GridTaskDensityMarkerDistribution.js +388 -388
  1443. package/src/generation/grid/generation/GridTaskExecuteRuleTimes.js +127 -127
  1444. package/src/generation/grid/generation/GridTaskSequence.js +58 -58
  1445. package/src/generation/grid/generation/NoopGridTaskGenerator.js +12 -12
  1446. package/src/generation/grid/generation/discrete/GridTaskActionRuleSet.js +48 -48
  1447. package/src/generation/grid/generation/discrete/GridTaskCellularAutomata.js +146 -146
  1448. package/src/generation/grid/generation/discrete/GridTaskConnectRooms.js +540 -540
  1449. package/src/generation/grid/generation/discrete/layer/GridTaskBuildSourceDistanceMap.js +222 -222
  1450. package/src/generation/grid/generation/grid/GridTaskAddNodesFixed.js +44 -44
  1451. package/src/generation/grid/generation/road/GridTaskGenerateRoads.js +614 -614
  1452. package/src/generation/markers/GridCellActionPlaceMarker.js +244 -244
  1453. package/src/generation/markers/GridCellActionPlaceMarkerGroup.js +86 -86
  1454. package/src/generation/markers/actions/terrain/MarkerNodeActionPaintTerrain.js +266 -266
  1455. package/src/generation/markers/emitter/MarkerNodeEmitterGridCellAction.js +49 -49
  1456. package/src/generation/placement/GridCellActionTransformNearbyMarkers.js +120 -120
  1457. package/src/generation/placement/action/GridCellAction.js +45 -45
  1458. package/src/generation/placement/action/GridCellActionPlaceTags.js +140 -140
  1459. package/src/generation/placement/action/GridCellActionWriteFilterToLayer.js +89 -89
  1460. package/src/generation/placement/action/random/CellActionSelectRandom.js +69 -69
  1461. package/src/generation/placement/action/random/weighted/CellActionSelectWeightedRandom.js +91 -91
  1462. package/src/generation/placement/action/util/GridCellActionSequence.js +61 -61
  1463. package/src/generation/placement/action/util/GridCellDisplacedAction.js +61 -61
  1464. package/src/generation/rules/cell/CellMatcherRemovalPreservesConnectivity.js +183 -183
  1465. package/src/generation/theme/ThemeEngine.js +666 -666
  1466. package/src/shade/RENDERER_CONTRACT.md +568 -568
  1467. package/src/shade/descriptor/texture/format/gpu_texture_format_info.js +267 -267
  1468. package/src/shade/device/ShadeDeviceFailure.js +104 -104
  1469. package/src/shade/device/ShadeDeviceFailureReason.js +41 -41
  1470. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +255 -255
  1471. package/src/shade/device/mock/REVIEW_2026_08_22.md +527 -527
  1472. package/src/shade/device/mock/SoftwareGPUBindGroup.js +23 -23
  1473. package/src/shade/device/mock/SoftwareGPUBindGroupLayout.js +22 -22
  1474. package/src/shade/device/mock/SoftwareGPUError.js +63 -63
  1475. package/src/shade/device/mock/SoftwareGPUPipeline.js +75 -75
  1476. package/src/shade/device/mock/SoftwareGPUQuerySet.js +113 -113
  1477. package/src/shade/device/mock/SoftwareGPUSampler.js +30 -30
  1478. package/src/shade/device/mock/SoftwareGPUShaderModule.js +32 -32
  1479. package/src/shade/device/mock/SoftwareGPUTextureView.js +22 -22
  1480. package/src/shade/device/mock/SoftwareGPUUncapturedErrorEvent.js +23 -23
  1481. package/src/shade/device/mock/normalize_dynamic_offsets.js +26 -26
  1482. package/src/shade/device/mock/texture_copy.js +287 -287
  1483. package/src/shade/device/timing/GPUTimer.js +285 -285
  1484. package/src/shade/device/timing/GPUTimerArray.js +205 -205
  1485. package/src/shade/playground/add_random_lights.js +49 -49
  1486. package/src/shade/playground/add_random_meshes.js +83 -83
  1487. package/src/shade/playground/animation_prototype.js +508 -508
  1488. package/src/shade/playground/animation_prototype_skinning.js +647 -647
  1489. package/src/shade/playground/animation_prototype_stress.js +765 -765
  1490. package/src/shade/playground/enable_picker.js +59 -59
  1491. package/src/shade/playground/gltf_skinned_prototype.js +560 -560
  1492. package/src/shade/playground/gltf_skinned_stress_prototype.js +387 -387
  1493. package/src/shade/playground/gltf_viewer.js +282 -282
  1494. package/src/shade/playground/load_default_environment.js +19 -19
  1495. package/src/shade/playground/main.js +2727 -2727
  1496. package/src/shade/playground/make_animated_instances.js +117 -117
  1497. package/src/shade/playground/prototype_virtual_texture.js +274 -274
  1498. package/src/shade/playground/prototype_volumetrics_01.js +206 -206
  1499. package/src/shade/playground/sample.js +234 -234
  1500. package/src/shade/playground/sample_load_gltf.js +102 -102
  1501. package/src/shade/playground/spread_node3d_grid.js +19 -19
  1502. package/src/shade/renderer/Renderer.js +2436 -2436
  1503. package/src/shade/renderer/ShadeIndirectLightingMode.js +14 -14
  1504. package/src/shade/renderer/animation/ShadeAnimationChannel.js +414 -414
  1505. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.js +531 -531
  1506. package/src/shade/renderer/animation/pose/PosePlayback.js +55 -55
  1507. package/src/shade/renderer/animation/pose/pose_evaluate_local.js +179 -179
  1508. package/src/shade/renderer/animation/pose/pose_evaluate_world.js +76 -76
  1509. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +1024 -1024
  1510. package/src/shade/renderer/animation/skinning/SKINNING_BINDING_STRUCT.js +79 -79
  1511. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +56 -56
  1512. package/src/shade/renderer/binding/BindGroupManager.js +282 -282
  1513. package/src/shade/renderer/binding/adapt_cache_capacity.js +25 -25
  1514. package/src/shade/renderer/buffer/GPUBufferAllocator.js +116 -116
  1515. package/src/shade/renderer/buffer/buffer_pool_ops.js +123 -123
  1516. package/src/shade/renderer/buffer/table/GPU_DATABASE_PAGE_SIZE.js +9 -9
  1517. package/src/shade/renderer/buffer/table/GPU_DATABASE_UPLOAD_BUFFER_LIMIT.js +8 -8
  1518. package/src/shade/renderer/buffer/table/PageBufferPool.js +71 -71
  1519. package/src/shade/renderer/buffer/table/PageSlotAllocator.js +95 -95
  1520. package/src/shade/renderer/camera/Camera.js +281 -281
  1521. package/src/shade/renderer/camera/GPUCameraContext.js +233 -233
  1522. package/src/shade/renderer/camera/orbital/README.md +84 -84
  1523. package/src/shade/renderer/deferred/shader_composite_volumetrics.js +75 -75
  1524. package/src/shade/renderer/dynamic/DYNAMIC_MESH_VERTEX.js +25 -25
  1525. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.js +342 -342
  1526. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.js +167 -167
  1527. package/src/shade/renderer/dynamic/shader_dynamic_mesh.js +170 -170
  1528. package/src/shade/renderer/extension/FrameContext.js +263 -263
  1529. package/src/shade/renderer/extension/FramePhase.js +104 -104
  1530. package/src/shade/renderer/extension/FrameRecord.js +126 -126
  1531. package/src/shade/renderer/extension/GBufferTextures.js +83 -83
  1532. package/src/shade/renderer/extension/PresentTarget.js +34 -34
  1533. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +918 -918
  1534. package/src/shade/renderer/extension/RenderExtension.js +69 -69
  1535. package/src/shade/renderer/extension/RenderExtensionRegistry.js +319 -319
  1536. package/src/shade/renderer/extension/SceneColor.js +40 -40
  1537. package/src/shade/renderer/extension/ViewTextures.js +78 -78
  1538. package/src/shade/renderer/fow/GPUFogOfWarRenderer.js +182 -182
  1539. package/src/shade/renderer/geometry/Geometry.js +608 -608
  1540. package/src/shade/renderer/geometry/geometry_build_from_meshlet_geometry.js +342 -342
  1541. package/src/shade/renderer/geometry/meshlet_geometry_transfer.js +41 -41
  1542. package/src/shade/renderer/geometry/primitives/make_box_geometry.js +153 -153
  1543. package/src/shade/renderer/geometry/primitives/make_cylinder_geometry.js +240 -240
  1544. package/src/shade/renderer/geometry/primitives/make_plane_geometry.js +145 -145
  1545. package/src/shade/renderer/geometry/primitives/make_polyhedron_geometry.js +327 -327
  1546. package/src/shade/renderer/geometry/primitives/make_torus_geometry.js +107 -107
  1547. package/src/shade/renderer/geometry/primitives/make_torus_knot_geometry.js +165 -165
  1548. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +607 -607
  1549. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +66 -66
  1550. package/src/shade/renderer/geometry/virtual/build/VGeoBuildResult.js +32 -32
  1551. package/src/shade/renderer/geometry/virtual/build/VGeoFaceGraph.js +103 -103
  1552. package/src/shade/renderer/geometry/virtual/build/VGeoGatheredFaces.js +25 -25
  1553. package/src/shade/renderer/geometry/virtual/build/VGeoLevel.js +33 -33
  1554. package/src/shade/renderer/geometry/virtual/build/VGeoLevelOptions.js +62 -62
  1555. package/src/shade/renderer/geometry/virtual/build/VGeoLevelSummary.js +49 -49
  1556. package/src/shade/renderer/geometry/virtual/build/VGeoSubmesh.js +20 -20
  1557. package/src/shade/renderer/geometry/virtual/build/VirtualGeometryMesh.js +214 -214
  1558. package/src/shade/renderer/geometry/virtual/build/gltf/GltfByteSource.js +122 -122
  1559. package/src/shade/renderer/geometry/virtual/build/gltf/GltfCollectReport.js +29 -29
  1560. package/src/shade/renderer/geometry/virtual/build/gltf/GltfDocument.js +210 -210
  1561. package/src/shade/renderer/geometry/virtual/build/gltf/GltfGeometry.js +48 -48
  1562. package/src/shade/renderer/geometry/virtual/build/gltf/GltfGeometryInstance.js +25 -25
  1563. package/src/shade/renderer/geometry/virtual/build/gltf/GltfGeometrySelection.js +28 -28
  1564. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_collect_geometries.js +296 -296
  1565. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.js +166 -166
  1566. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_read_geometry.js +199 -199
  1567. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.js +173 -173
  1568. package/src/shade/renderer/geometry/virtual/build/vgeo_bounding_sphere.js +28 -28
  1569. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +200 -200
  1570. package/src/shade/renderer/geometry/virtual/build/vgeo_build_levels.js +1786 -1786
  1571. package/src/shade/renderer/geometry/virtual/build/vgeo_build_page_nodes.js +157 -157
  1572. package/src/shade/renderer/geometry/virtual/build/vgeo_build_root_group.js +26 -26
  1573. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_normals.js +75 -75
  1574. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_tangents.js +143 -143
  1575. package/src/shade/renderer/geometry/virtual/build/vgeo_containing_sphere.js +44 -44
  1576. package/src/shade/renderer/geometry/virtual/build/vgeo_extract_submesh.js +41 -41
  1577. package/src/shade/renderer/geometry/virtual/build/vgeo_gather_faces.js +71 -71
  1578. package/src/shade/renderer/geometry/virtual/build/vgeo_graph_components.js +123 -123
  1579. package/src/shade/renderer/geometry/virtual/build/vgeo_narrow_indices.js +26 -26
  1580. package/src/shade/renderer/geometry/virtual/build/vgeo_normalize_source.js +112 -112
  1581. package/src/shade/renderer/geometry/virtual/build/vgeo_partition_graph.js +56 -56
  1582. package/src/shade/renderer/geometry/virtual/build/vgeo_sphere_inflate_to_contain.js +70 -70
  1583. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set.js +187 -187
  1584. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set_packed.js +207 -207
  1585. package/src/shade/renderer/geometry/virtual/format/VGEO_DEFECT.js +18 -18
  1586. package/src/shade/renderer/geometry/virtual/format/VGeoDefect.js +38 -38
  1587. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +64 -64
  1588. package/src/shade/renderer/geometry/virtual/format/attribute/VGEO_ATTRIBUTE.js +13 -13
  1589. package/src/shade/renderer/geometry/virtual/format/attribute/VGEO_ATTRIBUTES.js +50 -50
  1590. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attribute_allocate.js +28 -28
  1591. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attribute_mask.js +23 -23
  1592. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.js +31 -31
  1593. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_vertex.js +32 -32
  1594. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.js +46 -46
  1595. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_like.js +19 -19
  1596. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_test_fixtures.js +27 -27
  1597. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_asset_id_preimage.js +62 -62
  1598. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_asset_id.js +13 -13
  1599. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_directory_block_checksum.js +41 -41
  1600. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_frame_checksum.js +41 -41
  1601. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_header_checksum.js +25 -25
  1602. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.js +20 -20
  1603. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_RECORD_SIZE.js +5 -5
  1604. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_MAX_CHILD_REF.js +5 -5
  1605. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_MAX_CLUSTER_TRIANGLE_COUNT.js +5 -5
  1606. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_MAX_CLUSTER_VERTEX_COUNT.js +5 -5
  1607. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_NO_CHILD.js +5 -5
  1608. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_NO_GROUP.js +5 -5
  1609. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_SAME_PAGE.js +6 -6
  1610. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.js +81 -81
  1611. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryGroup.js +37 -37
  1612. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryPageNode.js +55 -55
  1613. package/src/shade/renderer/geometry/virtual/format/directory/VGEO_DIRECTORY_BLOCK_RECORDS.js +5 -5
  1614. package/src/shade/renderer/geometry/virtual/format/directory/VGEO_DIRECTORY_FLAG_ROOT_PAGE.js +5 -5
  1615. package/src/shade/renderer/geometry/virtual/format/directory/VGEO_DIRECTORY_RECORD_SIZE.js +5 -5
  1616. package/src/shade/renderer/geometry/virtual/format/directory/vgeo_directory_block_count.js +12 -12
  1617. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CHILD_OFFSET.js +11 -11
  1618. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CHILD_TABLE_ENTRY_SIZE.js +5 -5
  1619. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.js +11 -11
  1620. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_RAW.js +5 -5
  1621. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_FRAME_ALIGNMENT.js +5 -5
  1622. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_FRAME_HEADER_SIZE.js +5 -5
  1623. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_FRAME_OFFSET.js +13 -13
  1624. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_PAGE_MAGIC.js +5 -5
  1625. package/src/shade/renderer/geometry/virtual/format/frame/VGeoFrameBlob.js +21 -21
  1626. package/src/shade/renderer/geometry/virtual/format/frame/VGeoFrameExtent.js +59 -59
  1627. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_codec_name.js +27 -27
  1628. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.js +84 -84
  1629. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.js +56 -56
  1630. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.js +35 -35
  1631. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_scan_frames.js +74 -74
  1632. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.js +6 -6
  1633. package/src/shade/renderer/geometry/virtual/format/header/VGEO_HASH_ALGO_XXH3.js +5 -5
  1634. package/src/shade/renderer/geometry/virtual/format/header/VGEO_HEADER_OFFSET.js +31 -31
  1635. package/src/shade/renderer/geometry/virtual/format/header/VGEO_HEADER_SIZE.js +5 -5
  1636. package/src/shade/renderer/geometry/virtual/format/header/VGEO_LEVEL_OFFSET.js +20 -20
  1637. package/src/shade/renderer/geometry/virtual/format/header/VGEO_LEVEL_RECORD_SIZE.js +9 -9
  1638. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAGIC.js +5 -5
  1639. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.js +5 -5
  1640. package/src/shade/renderer/geometry/virtual/format/header/VGEO_PAYLOAD_ENCODING_SHADE_MESHLET_V1.js +5 -5
  1641. package/src/shade/renderer/geometry/virtual/format/header/VGeoLevelRecord.js +41 -41
  1642. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_RECORD_SIZE.js +5 -5
  1643. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_GROUP_CLUSTER_COUNT.js +6 -6
  1644. package/src/shade/renderer/geometry/virtual/format/page/VGEO_METRIC_OFFSET.js +21 -21
  1645. package/src/shade/renderer/geometry/virtual/format/page/VGEO_METRIC_SIZE.js +5 -5
  1646. package/src/shade/renderer/geometry/virtual/format/page/VGEO_NODE_FANOUT.js +9 -9
  1647. package/src/shade/renderer/geometry/virtual/format/page/VGEO_NODE_FLAG_LEAF.js +5 -5
  1648. package/src/shade/renderer/geometry/virtual/format/page/VGEO_NODE_OFFSET.js +10 -10
  1649. package/src/shade/renderer/geometry/virtual/format/page/VGEO_NODE_RECORD_SIZE.js +5 -5
  1650. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_HEADER_SIZE.js +5 -5
  1651. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.js +16 -16
  1652. package/src/shade/renderer/geometry/virtual/format/payload/VGEO_ENCODING_FLAGS_MASK.js +6 -6
  1653. package/src/shade/renderer/geometry/virtual/format/payload/VGeoEncodedPayload.js +18 -18
  1654. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.js +200 -200
  1655. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_payload_attribute_offset.js +39 -39
  1656. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_payload_size.js +20 -20
  1657. package/src/shade/renderer/geometry/virtual/format/prototypeVGEOFormatViewer.js +2063 -2063
  1658. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +1622 -1622
  1659. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +485 -485
  1660. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_empty_gpu_structure.d.ts +23 -0
  1661. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_empty_gpu_structure.d.ts.map +1 -0
  1662. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_empty_gpu_structure.js +34 -0
  1663. package/src/shade/renderer/gpu_primitive/sort/ffx/graph_radix_sort.js +115 -115
  1664. package/src/shade/renderer/gpu_primitive/sort/one_sweep/verify_radix_sort_one_sweep.js +110 -110
  1665. package/src/shade/renderer/light/environment/octahedral_resample_kernel.js +50 -50
  1666. package/src/shade/renderer/light/environment/octahedral_uv_to_direction.js +30 -30
  1667. package/src/shade/renderer/light/environment/resample_cube_to_octahedral.js +157 -157
  1668. package/src/shade/renderer/light/make_sunlight.js +35 -35
  1669. package/src/shade/renderer/light/model/io/light_record_point.js +39 -39
  1670. package/src/shade/renderer/loader/gltf/GLTF_PRIMITIVE_MODE.js +23 -23
  1671. package/src/shade/renderer/loader/gltf/MESHOPT_COMPRESSION_PLAN.md +793 -793
  1672. package/src/shade/renderer/loader/gltf/ext/GltfBufferViewExtension.js +54 -54
  1673. package/src/shade/renderer/loader/gltf/ext/GltfBufferViewExtensionSet.js +109 -109
  1674. package/src/shade/renderer/loader/gltf/ext/gltf_apply_buffer_view_extensions.js +69 -69
  1675. package/src/shade/renderer/loader/gltf/ext/gltf_buffer_view_extensions.js +22 -22
  1676. package/src/shade/renderer/loader/gltf/ext/gltf_geometry_extensions.js +39 -39
  1677. package/src/shade/renderer/loader/gltf/ext/meshopt/MeshoptBufferViewExtension.js +301 -301
  1678. package/src/shade/renderer/loader/gltf/gltf_component_type.js +223 -223
  1679. package/src/shade/renderer/loader/gltf/gltf_node_world_matrices.js +137 -137
  1680. package/src/shade/renderer/loader/gltf/gltf_parse_container.js +241 -241
  1681. package/src/shade/renderer/loader/gltf/gltf_read_accessor.js +294 -294
  1682. package/src/shade/renderer/loader/gltf/gltf_used_image_indices.js +49 -49
  1683. package/src/shade/renderer/loader/gltf/load_gltf.js +1253 -1253
  1684. package/src/shade/renderer/loader/gltf/tiny-gltf.js +646 -646
  1685. package/src/shade/renderer/loader/usd/UsdError.js +33 -33
  1686. package/src/shade/renderer/loader/usd/UsdExtensionRegistry.js +69 -69
  1687. package/src/shade/renderer/loader/usd/load_usd.js +226 -226
  1688. package/src/shade/renderer/loader/usd/parse_usda.js +931 -931
  1689. package/src/shade/renderer/loader/usd/unpack_usdz.js +106 -106
  1690. package/src/shade/renderer/loader/usd/usd_build_scene.js +658 -658
  1691. package/src/shade/renderer/loader/usd/usd_compose_transform.js +258 -258
  1692. package/src/shade/renderer/loader/usd/usd_decode_image.js +384 -384
  1693. package/src/shade/renderer/loader/usd/usd_lz4.js +182 -182
  1694. package/src/shade/renderer/loader/usd/usd_triangulate.js +159 -159
  1695. package/src/shade/renderer/material/ShadeMaterial.d.ts +2 -2
  1696. package/src/shade/renderer/material/ShadeMaterial.js +2 -2
  1697. package/src/shade/renderer/material/standard/fragment_gbuffer.js +334 -334
  1698. package/src/shade/renderer/material/standard/fragment_gbuffer_vt.js +169 -169
  1699. package/src/shade/renderer/material/standard/material_gbuffer_vertex.js +45 -45
  1700. package/src/shade/renderer/material/standard_material_to_struct.js +23 -23
  1701. package/src/shade/renderer/particles/prototypeParticleSystem.js +450 -450
  1702. package/src/shade/renderer/particles/runtime/spawn_request_encoding.js +52 -52
  1703. package/src/shade/renderer/pool/TransientResourcePool.js +392 -392
  1704. package/src/shade/renderer/postprocess/bloom/bloom_chain_resolution.js +29 -29
  1705. package/src/shade/renderer/postprocess/bloom/graph_bloom_downsample.js +109 -109
  1706. package/src/shade/renderer/postprocess/bloom/graph_postprocess_bloom.js +155 -155
  1707. package/src/shade/renderer/postprocess/cas/shader_ffx_rcas.js +114 -114
  1708. package/src/shade/renderer/postprocess/dof/DOF_AUTOFOCUS_PARAMS_STRUCT.js +25 -25
  1709. package/src/shade/renderer/postprocess/dof/DOF_FOCUS_STATE_STRUCT.js +23 -23
  1710. package/src/shade/renderer/postprocess/dof/DOF_PRESETS.js +27 -27
  1711. package/src/shade/renderer/postprocess/dof/chunk_dof_coc_band_diameter.js +41 -41
  1712. package/src/shade/renderer/postprocess/dof/chunk_dof_coc_band_radius.js +32 -32
  1713. package/src/shade/renderer/postprocess/dof/chunk_dof_coc_coefficient.js +26 -26
  1714. package/src/shade/renderer/postprocess/dof/compute_dof_autofocus_params.js +64 -64
  1715. package/src/shade/renderer/postprocess/dof/compute_dof_uniform.js +54 -54
  1716. package/src/shade/renderer/postprocess/dof/gather/DOF2_CONFIG.js +91 -91
  1717. package/src/shade/renderer/postprocess/dof/gather/DOF2_PRESETS.js +9 -9
  1718. package/src/shade/renderer/postprocess/dof/gather/DOF2_SETTINGS_STRUCT.js +66 -66
  1719. package/src/shade/renderer/postprocess/dof/gather/DOF2_TEXTURE_FORMAT.js +22 -22
  1720. package/src/shade/renderer/postprocess/dof/gather/DepthOfFieldU.js +292 -292
  1721. package/src/shade/renderer/postprocess/dof/gather/chunk_dof2_coc_to_near_far.js +25 -25
  1722. package/src/shade/renderer/postprocess/dof/gather/chunk_dof2_gather_weight.js +30 -30
  1723. package/src/shade/renderer/postprocess/dof/gather/compute_dof2_uniform.js +54 -54
  1724. package/src/shade/renderer/postprocess/dof/gather/graph_postprocess_dof2.js +194 -194
  1725. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_fill.js +68 -68
  1726. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_far.js +140 -140
  1727. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_near.js +142 -142
  1728. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_recombine.js +99 -99
  1729. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_setup.js +102 -102
  1730. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_tile_dilate.js +75 -75
  1731. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_tile_max.js +73 -73
  1732. package/src/shade/renderer/postprocess/dof/raymarch/DOF_SETTINGS_STRUCT.js +49 -49
  1733. package/src/shade/renderer/postprocess/dof/raymarch/DepthOfField.js +298 -298
  1734. package/src/shade/renderer/postprocess/dof/raymarch/graph_postprocess_dof.js +244 -244
  1735. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_composite.js +91 -91
  1736. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_firefly_cleanup.js +61 -61
  1737. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_prefilter.js +90 -90
  1738. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch_compute.js +267 -267
  1739. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_slice_compute.js +271 -271
  1740. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_tile_dilate.js +70 -70
  1741. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_tile_max.js +72 -72
  1742. package/src/shade/renderer/postprocess/dof/shader_dof_autofocus.js +99 -99
  1743. package/src/shade/renderer/postprocess/gtao/generate_magic_square_noise.js +51 -51
  1744. package/src/shade/renderer/postprocess/motion_blur/MotionBlur.js +56 -56
  1745. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +78 -78
  1746. package/src/shade/renderer/postprocess/upscale-dither/UpscaleDither.js +95 -95
  1747. package/src/shade/renderer/postprocess/upscale-dither/fragment_shader_upscale_dither.js +204 -204
  1748. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.js +58 -58
  1749. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +230 -230
  1750. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.js +278 -278
  1751. package/src/shade/renderer/rasterize/native/oit/chunk_material_side_should_draw.js +26 -26
  1752. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments_ibl.js +178 -178
  1753. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +235 -235
  1754. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +208 -208
  1755. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +206 -206
  1756. package/src/shade/renderer/scene/DynamicMesh.js +70 -70
  1757. package/src/shade/renderer/scene/DynamicMeshBatch.js +90 -90
  1758. package/src/shade/renderer/scene/GPUSceneContext.js +901 -901
  1759. package/src/shade/renderer/scene/SceneNodeBinding.js +56 -56
  1760. package/src/shade/renderer/scene/SkinnedMesh.js +55 -55
  1761. package/src/shade/renderer/scene/TransformAuthority.js +33 -33
  1762. package/src/shade/renderer/scene/optimization/deduplicate_geometries.js +38 -38
  1763. package/src/shade/renderer/scene/optimization/deduplicate_images.js +93 -93
  1764. package/src/shade/renderer/scene/optimization/geometry/internal/anchor_grow_topological.d.ts.map +1 -1
  1765. package/src/shade/renderer/scene/optimization/geometry/internal/anchor_grow_topological.js +12 -3
  1766. package/src/shade/renderer/scene/optimization/geometry/internal/match_refit_validate.js +127 -127
  1767. package/src/shade/renderer/scene/optimization/geometry/internal/match_solve_transform.js +492 -492
  1768. package/src/shade/renderer/scene/optimization/geometry/internal/match_validate_topology.js +3 -3
  1769. package/src/shade/renderer/scene/optimization/geometry/internal/seed_correspondence_from_window.js +3 -3
  1770. package/src/shade/renderer/scene/optimization/optimize_texture_set.js +81 -81
  1771. package/src/shade/renderer/scene/optimization/source_to_sampler2d.js +21 -21
  1772. package/src/shade/renderer/scene/serialization/STANDARD_MATERIAL_SERIALIZED_BYTE_SIZE.js +12 -12
  1773. package/src/shade/renderer/scene/serialization/deserialize_scene.d.ts.map +1 -1
  1774. package/src/shade/renderer/scene/serialization/deserialize_scene.js +0 -2
  1775. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_transform_point.js +26 -26
  1776. package/src/shade/renderer/shader/chunk/noise/chunk_interleaved_gradient_noise.js +20 -20
  1777. package/src/shade/renderer/shader/chunk/spherical_harmonics/dering/chunk_sh2_dering_optimize_positive.js +61 -61
  1778. package/src/shade/renderer/shader/chunk/texture/chunk_texture_filter_fxaa_lite.js +225 -225
  1779. package/src/shade/renderer/shader/chunk/texture/dither/chunk_dither_color_8bit_gradient_noise.js +25 -25
  1780. package/src/shade/renderer/shadow/map/DirectionalLightShadowmap.js +384 -384
  1781. package/src/shade/renderer/shadow/map/PointLightShadowmap.js +105 -105
  1782. package/src/shade/renderer/shadow/map/SpotLightShadowmap.js +63 -63
  1783. package/src/shade/renderer/shadow/map/TetrahedronPointLightShadowmap.js +89 -89
  1784. package/src/shade/renderer/shadow/map/shadowmap_update_score.js +232 -232
  1785. package/src/shade/renderer/texture/GPUTextureAllocator.js +85 -85
  1786. package/src/shade/renderer/texture/texture_pool_ops.js +171 -171
  1787. package/src/shade/renderer/texture/virtual/PageTableQuadAllocator.js +253 -253
  1788. package/src/shade/renderer/texture/virtual/README.md +276 -276
  1789. package/src/shade/renderer/texture/virtual/VTPageTable.js +330 -330
  1790. package/src/shade/renderer/texture/virtual/VTPhysicalCache.js +349 -349
  1791. package/src/shade/renderer/texture/virtual/VT_CONSTANTS.js +154 -154
  1792. package/src/shade/renderer/texture/virtual/VT_STACK_INFO_STRUCT.js +20 -20
  1793. package/src/shade/renderer/texture/virtual/VirtualTextureManager.js +1276 -1276
  1794. package/src/shade/renderer/texture/virtual/VirtualTextureStack.js +106 -106
  1795. package/src/shade/renderer/texture/virtual/build_chunk_vt_lookup.js +150 -150
  1796. package/src/shade/renderer/texture/virtual/build_shader_vt_feedback.js +232 -232
  1797. package/src/shade/renderer/texture/virtual/chunk_vt_request_key.js +28 -28
  1798. package/src/shade/renderer/texture/virtual/source/VTSourceImage.js +207 -207
  1799. package/src/shade/renderer/texture/virtual/source/VTSourceProcedural.js +135 -135
  1800. package/src/shade/renderer/texture/virtual/source/VTSourceTiled.js +132 -132
  1801. package/src/shade/renderer/water/GPUWaterRenderer.js +167 -167
  1802. package/src/shade/renderer/water/shader_water.js +246 -246
  1803. package/src/shade/renderer/water/water_height_texture_from_sampler.js +56 -56
  1804. package/src/shade/vitest.setup.mjs +89 -89
  1805. package/src/shade/wgsl/validate_wgsl_source.js +316 -316
  1806. package/src/view/controller/controls/NumericIntervalControl.js +90 -90
  1807. package/src/view/controller/controls/Vector2Control.js +83 -83
  1808. package/src/view/controller/controls/Vector3Control.js +90 -90
  1809. package/src/view/elements/Group.js +56 -56
  1810. package/src/view/elements/drawCheckers.js +33 -33
  1811. package/src/view/elements/windrose/WindRoseDiagram.js +369 -369
  1812. package/src/view/graphics/WebGLContextFailureView.js +76 -76
  1813. package/src/view/layout/DockPanel.js +51 -51
  1814. package/src/view/layout/SplitView.js +365 -365
  1815. package/src/view/layout/show_popup_menu.js +81 -81
  1816. package/src/view/minimap/MinimapCanvasView.js +411 -411
  1817. package/src/view/minimap/MinimapImageCache.js +88 -88
  1818. package/src/view/minimap/MinimapMarkerCollection.d.ts +0 -1
  1819. package/src/view/minimap/MinimapMarkerCollection.d.ts.map +1 -1
  1820. package/src/view/minimap/MinimapMarkerCollection.js +180 -180
  1821. package/src/view/minimap/dom/MinimapCameraView.js +206 -206
  1822. package/src/view/minimap/minimap_focus_area.js +97 -97
  1823. package/editor/process/symbolic/SocketsSymbolicDisplay.js +0 -192
  1824. package/src/core/geom/3d/topology/struct/binary/io/moved_tests.txt +0 -37
  1825. package/src/engine/ecs/attachment/Attachment.d.ts +0 -40
  1826. package/src/engine/ecs/attachment/Attachment.d.ts.map +0 -1
  1827. package/src/engine/ecs/attachment/Attachment.js +0 -57
  1828. package/src/engine/ecs/attachment/AttachmentBinding.d.ts +0 -28
  1829. package/src/engine/ecs/attachment/AttachmentBinding.d.ts.map +0 -1
  1830. package/src/engine/ecs/attachment/AttachmentBinding.js +0 -41
  1831. package/src/engine/ecs/attachment/AttachmentSystem.d.ts +0 -61
  1832. package/src/engine/ecs/attachment/AttachmentSystem.d.ts.map +0 -1
  1833. package/src/engine/ecs/attachment/AttachmentSystem.js +0 -307
  1834. package/src/engine/ecs/attachment/BoneAttachmentBinding.d.ts +0 -27
  1835. package/src/engine/ecs/attachment/BoneAttachmentBinding.d.ts.map +0 -1
  1836. package/src/engine/ecs/attachment/BoneAttachmentBinding.js +0 -69
  1837. package/src/engine/ecs/attachment/TransformAttachmentBinding.d.ts +0 -4
  1838. package/src/engine/ecs/attachment/TransformAttachmentBinding.d.ts.map +0 -1
  1839. package/src/engine/ecs/attachment/TransformAttachmentBinding.js +0 -51
  1840. package/src/engine/ecs/sockets/AttachmentSocket.d.ts +0 -32
  1841. package/src/engine/ecs/sockets/AttachmentSocket.d.ts.map +0 -1
  1842. package/src/engine/ecs/sockets/AttachmentSocket.js +0 -60
  1843. package/src/engine/ecs/sockets/AttachmentSocketType.d.ts +0 -6
  1844. package/src/engine/ecs/sockets/AttachmentSocketType.d.ts.map +0 -1
  1845. package/src/engine/ecs/sockets/AttachmentSocketType.js +0 -8
  1846. package/src/engine/ecs/sockets/AttachmentSockets.d.ts +0 -57
  1847. package/src/engine/ecs/sockets/AttachmentSockets.d.ts.map +0 -1
  1848. package/src/engine/ecs/sockets/AttachmentSockets.js +0 -130
  1849. package/src/engine/ecs/sockets/BoneAttachmentSocket.d.ts +0 -29
  1850. package/src/engine/ecs/sockets/BoneAttachmentSocket.d.ts.map +0 -1
  1851. package/src/engine/ecs/sockets/BoneAttachmentSocket.js +0 -56
  1852. package/src/engine/ecs/sockets/serialization/AttachmentSocketsAssetLoader.d.ts +0 -6
  1853. package/src/engine/ecs/sockets/serialization/AttachmentSocketsAssetLoader.d.ts.map +0 -1
  1854. package/src/engine/ecs/sockets/serialization/AttachmentSocketsAssetLoader.js +0 -19
  1855. package/src/shade/renderer/animation/_dump_resolve.d.mts +0 -2
  1856. package/src/shade/renderer/animation/_dump_resolve.d.mts.map +0 -1
  1857. package/src/shade/renderer/animation/_dump_resolve.mjs +0 -5
  1858. package/src/shade/renderer/scene/optimization/geometry/internal/bt_face_loop_helpers.d.ts +0 -32
  1859. package/src/shade/renderer/scene/optimization/geometry/internal/bt_face_loop_helpers.d.ts.map +0 -1
  1860. package/src/shade/renderer/scene/optimization/geometry/internal/bt_face_loop_helpers.js +0 -77
  1861. package/src/shade/renderer/scene/optimization/geometry/internal/bt_find_edge_between.d.ts +0 -12
  1862. package/src/shade/renderer/scene/optimization/geometry/internal/bt_find_edge_between.d.ts.map +0 -1
  1863. package/src/shade/renderer/scene/optimization/geometry/internal/bt_find_edge_between.js +0 -35
  1864. package/src/shade/renderer/scene/serialization/linear_to_pq.d.ts +0 -2
  1865. package/src/shade/renderer/scene/serialization/linear_to_pq.d.ts.map +0 -1
  1866. package/src/shade/renderer/scene/serialization/linear_to_pq.js +0 -14
  1867. package/src/shade/renderer/scene/serialization/pq_to_linear.d.ts +0 -9
  1868. package/src/shade/renderer/scene/serialization/pq_to_linear.d.ts.map +0 -1
  1869. package/src/shade/renderer/scene/serialization/pq_to_linear.js +0 -30
@@ -1,690 +1,690 @@
1
- # Correctness review — engine/graphics/{texture, particles/particular, geometry}
2
-
3
- Scope: every non-spec, non-`prototype*` source file under
4
- `engine/graphics/texture/`, `engine/graphics/particles/particular/`, `engine/graphics/geometry/`.
5
- (The brief named `graphics/particular/`; the actual path is `graphics/particles/particular/`.)
6
-
7
- Rules applied: correctness only; every finding carries a concrete failure scenario; `assert.*`
8
- is treated as compiled out; no defensive-guard requests; unreferenced exports treated as public
9
- surface.
10
-
11
- **Executed verification.** GFXT-1, -2, -3, -4, -5, -6/-7, -17, -24, -40 and -42 were confirmed by
12
- running jest against the real classes (11/11 assertions pass). GFXT-51, -52, -53, -54 (ParticleGroup),
13
- GFXT-60, -61, -62, -63, -64, -67 (merge / uv-tension) and GFXT-36 (barycentric sampling) were
14
- likewise reproduced by execution during review. The remainder are confirmed by inspection; the two
15
- findings resting on unexecuted GL or scale-dependent behaviour (GFXT-29, GFXT-65) and the one
16
- resting on a transform-convention argument (GFXT-46) are marked `Likely`.
17
-
18
- **Note on stray files.** Two untracked scratch specs —
19
- `src/engine/graphics/texture/sampler/__advcheck_gfxt.spec.js` and `__advcheck_gfxt2.spec.js` —
20
- were present in the source tree at the end of this review. They are the verification specs whose
21
- results are cited above. They were not written by this review pass and were left in place rather
22
- than deleted, in case a concurrent session still owns them; they are scratch files and should be
23
- removed before committing. Sibling `__advcheck_*.spec.js` files also exist under `src/core/collection/`
24
- and `src/engine/`, along with `src/REVIEW_2026_08_06.md`. No source file in the reviewed tree was
25
- modified by this review.
26
-
27
- ---
28
-
29
- ## texture/sampler — pixel arithmetic, filters, conversion
30
-
31
- ### GFXT-1 `sampler2d_paint` reads the source X coordinate from `sourceY`
32
- - **Severity**: CRITICAL
33
- - **File**: `engine/graphics/texture/sampler/sampler2d_paint.js:68`
34
- - **Defect**: The horizontal source coordinate is built from `sourceY` instead of `sourceX`, so every blend reads from the wrong column of the source image.
35
- - **Failure scenario**: `sampler2d_paint(dst, brushAtlas, /*sourceX*/ 64, /*sourceY*/ 0, 0, 0, 32, 32)` — intended to stamp the brush at atlas column 64 — computes `s_x = x + 0`, so it stamps atlas columns 0..31 instead. With `sourceX = 0, sourceY = 8` the stamp is shifted 8 px right and reads 8 px past the intended patch on the last columns.
36
- - **Fix**: `const s_x = Math.round(x + sourceX);`
37
- - **Confidence**: Confirmed
38
-
39
- ### GFXT-2 `bitSet2Sampler2D` calls a method `Sampler2D` does not have
40
- - **Severity**: CRITICAL
41
- - **File**: `engine/graphics/texture/sampler/util/bitSet2Sampler2D.js:35,37`
42
- - **Defect**: The function writes texels via `target.set(x, y, value)`. `Sampler2D` exposes `write(x, y, texel)`; there is no `set` method on the class or its prototype.
43
- - **Failure scenario**: `bitSet2Sampler2D(bits, Sampler2D.uint8(4, 8, 8), [255,0,0,255], [0,0,0,255])` throws `TypeError: target.set is not a function` on the first texel. The function can never have worked.
44
- - **Fix**: `target.write(x, y, vTrue)` / `target.write(x, y, vFalse)`.
45
- - **Confidence**: Confirmed
46
-
47
- ### GFXT-3 Signed chamfer SDF keeps a stale `v`, so it stores the last improving candidate, not the minimum
48
- - **Severity**: CRITICAL
49
- - **File**: `engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js:72-96` (forward pass), `:103-127` (backward pass)
50
- - **Defect**: Both relaxation passes read `const v = getD(x, y)` once and then compare each of the four candidates against that same stale `v`, calling `setD` on every candidate that beats it. The stored value ends up being the *last* candidate below the original value rather than `min(v, v0, v1, v2, v3)`. The sibling `computeUnsignedDistanceField_Chamfer` does this correctly (it reassigns `v = v0` etc. after each improvement).
51
- - **Failure scenario**: pixel with `D = 255` whose forward-pass neighbours hold `D(x-1,y-1) = 0`, `D(x,y-1) = 100`, `D(x+1,y-1) = 200`, `D(x-1,y) = 50`, with `d1 = 1`, `d2 = 1.414`. Candidates are `1.414, 101, 201.414, 51`; all four are `< 255`, so the cell is written four times and ends at `51`. Correct answer is `1.414`. The error then propagates to every downstream cell in both passes.
52
- - **Fix**: after each `setD(x, y, vN)` also assign `v = vN` (mirror the unsigned version), or compute `min2` of all five values and write once.
53
- - **Confidence**: Confirmed
54
-
55
- ### GFXT-4 `convertTexture2Sampler2D` flips the source DataTexture's own pixel buffer in place
56
- - **Severity**: CRITICAL
57
- - **File**: `engine/graphics/texture/sampler/convertTexture2Sampler2D.js:130-132` and `:163-165` (via `data_texture_to_sampler` at `:25-38`, `sampler2d_ensure_uint8_RGBA.js:14`)
58
- - **Defect**: For a `DataTexture`, `data_texture_to_sampler` wraps `texture.image.data` in a `Sampler2D`; `sampler2d_ensure_uint8_RGBA` returns that same object unchanged when the texture is already 4-channel uint8, and the dimension check `converted.width !== _width` never fires because `_width/_height` default to the image's own dimensions. `data` is therefore the live texture buffer, and `sampler2d_flipY_in_place(data, …)` mutates it.
59
- - **Failure scenario**: `convertTexture2Sampler2D(someRGBA8DataTexture)` (no width/height override, default `flipY = true`) vertically flips the pixels of the *input* texture. Anything still rendering that texture is now upside-down; calling the function a second time flips it back, so the corruption alternates per call.
60
- - **Fix**: copy before flipping on the shared path — e.g. have `data_texture_to_sampler` clone when `converted === source`, or flip into a freshly allocated buffer.
61
- - **Confidence**: Confirmed
62
-
63
- ### GFXT-5 `sampler2d_combine` reads input1 with input0's channel count
64
- - **Severity**: MAJOR
65
- - **File**: `engine/graphics/texture/sampler/sampler2d_combine.js:52`
66
- - **Defect**: The loop that fills `arg1` from `input1` is bounded by `itemSize0` instead of `itemSize1`, so when the two inputs have different channel counts the second operand is over- or under-read.
67
- - **Failure scenario**: `input0` RGBA (`itemSize 4`), `input1` a 1-channel mask, 4×4 image. For texel `i = 0` the code reads `data1[0..3]`, i.e. mask texels 0,1,2,3, and hands `operation` an `arg1` of `[m0, m1, m2, m3]` where `[m0]` was intended. Every masked texel gets the wrong mask value, and the stale tail of `arg1` persists for the remaining texels.
68
- - **Fix**: bound that loop with `itemSize1`, and clear/resize `arg1` per iteration if `itemSize1` can shrink.
69
- - **Confidence**: Confirmed
70
-
71
- ### GFXT-6 `sampler2d_compute_texel_value_conversion_scale_to_uint8` accumulates `max` from `min`
72
- - **Severity**: MAJOR
73
- - **File**: `engine/graphics/texture/sampler/sampler2d_compute_texel_value_conversion_scale_to_uint8.js:44`
74
- - **Defect**: `max = max2(min, sampler2d_channel_compute_max(sampler, i).value)` uses `min` as the running accumulator, so the per-channel maxima of all but the last channel are discarded. The final `max` is just the last channel's maximum.
75
- - **Failure scenario**: a `Float32` 2-channel sampler where channel 0 spans `[0, 1000]` and channel 1 spans `[0, 5]`. Iteration 0 sets `max = 1000`; iteration 1 overwrites it with `max2(0, 5) = 5`. `span = 5`, `scale = 51`. Every channel-0 value above 5 maps past 255 and saturates — the channel comes out uniformly white through `sampler2d_to_data_url`.
76
- - **Fix**: `max = max2(max, sampler2d_channel_compute_max(sampler, i).value)`.
77
- - **Confidence**: Confirmed
78
-
79
- ### GFXT-7 `sampler2d_to_html_canvas` adds the offset that its producer supplies as a value to subtract
80
- - **Severity**: MAJOR
81
- - **File**: `engine/graphics/texture/sampler/sampler2d_to_html_canvas.js:61,81` (producer: `sampler2d_compute_texel_value_conversion_scale_to_uint8.js:51`, composed in `sampler2d_to_data_url.js:16-18`)
82
- - **Defect**: The canvas writer computes `(source_value + offset) * scale`, but the offset it is fed is `offset = min` and the scale is `255 / (max - min)`. Normalising to `[0,255]` requires `(v - min) * scale`. The two disagree in sign, so the mapping is only correct when `min === 0`.
83
- - **Failure scenario**: `sampler2d_to_data_url(s)` on a `Float32` sampler holding `[10, 11, 12, 13]` (1 channel, 2×2). `min = 10`, `scale = 85`, `offset = 10`. Texel 0 becomes `(10 + 10) * 85 = 1700`, clamped to 255; every other texel likewise. The PNG is uniformly white instead of a four-step gradient. A sampler spanning `[-1, 1]` comes out uniformly black.
84
- - **Fix**: return `offset: -min` from the producer (keeping the canvas writer's additive convention), or change the canvas writer to `(source_value - offset) * scale`. Pick one convention and apply it in both places.
85
- - **Confidence**: Confirmed
86
-
87
- ### GFXT-8 `saturated_value_by_constructor` has no `Uint8ClampedArray` arm, so RGB→RGBA fills alpha with 1
88
- - **Severity**: MAJOR
89
- - **File**: `engine/graphics/texture/sampler/saturated_value_by_constructor.js:8-26` (consumer: `sampler2d_transfer_data.js:41,52`)
90
- - **Defect**: The switch covers `Uint8Array` (255) but not `Uint8ClampedArray`, which is a distinct constructor and is not an `instanceof Uint8Array`. It falls through to `default: return 1`. `Sampler2D.uint8clamped()` is a first-class factory on the class and `ImageData.data` is always a `Uint8ClampedArray`.
91
- - **Failure scenario**: `sampler2d_transfer_data(rgbSampler, Sampler2D.uint8clamped(4, 64, 64))` fills the alpha channel with `1` instead of `255`. Drawn to a canvas or uploaded as an RGBA texture, the image is ~0.4% opaque — effectively invisible.
92
- - **Fix**: add `case Uint8ClampedArray: return 255;` (and, for completeness, `Float16Array` alongside the float arms).
93
- - **Confidence**: Confirmed
94
-
95
- ### GFXT-9 `sampler2d_to_uint8_RGBA` treats Float16 and plain-array samplers as already-uint8
96
- - **Severity**: MAJOR
97
- - **File**: `engine/graphics/texture/sampler/sampler2d_to_uint8_RGBA.js:7-32,60-64`
98
- - **Defect**: `compute_gradient_and_intercept_uint8` has arms for `Float32Array`/`Float64Array` (gradient 255) but none for `Float16Array` or a plain `Array`, both of which are supported `Sampler2D` storages (`Sampler2D.float16()`, `sampler2d_to_f16.js`, and the array branches in `clone`/`hash`/`computeByteSize`). They fall through to `default` — gradient 1, intercept 0 — which also satisfies the fast-path test on line 60.
99
- - **Failure scenario**: `sampler2d_to_uint8_RGBA(Sampler2D.uint8(4,w,h), someFloat16RGBASampler)` with values in `[0,1]` takes the `output.data.set(input.data)` shortcut and writes 0 or 1 into each uint8 channel. A white image (1.0) becomes value 1 — effectively black. The same happens on the `VirtualTextureTileLoader.js:242` tile path for any non-uint8 tile source.
100
- - **Fix**: add `case Float16Array:` next to the float arms (gradient 255), and derive the plain-array case from `sampler2d_compute_texel_value_conversion_scale_to_uint8`'s convention (it already treats a plain numeric array as Float32).
101
- - **Confidence**: Confirmed
102
-
103
- ### GFXT-10 `Sampler2DSerializationAdapter` rejects `Uint8ClampedArray`-backed samplers
104
- - **Severity**: MAJOR
105
- - **File**: `engine/graphics/texture/sampler/serialization/Sampler2DSerializationAdapter.js:26`
106
- - **Defect**: `if (value.data instanceof Uint8Array)` is false for a `Uint8ClampedArray`; the adapter then throws `TypeError: Unsupported data type`.
107
- - **Failure scenario**: serialising any sampler produced by `Sampler2D.uint8clamped(...)`, `sampler2d_to_html_canvas` (line 31), `canvasDataToSampler`, or `html_canvas_to_sampler2d` (all `Uint8ClampedArray`) throws instead of writing the 8-bit path it clearly intends to cover. Deserialisation always produces a `Uint8Array`, so a clamped sampler cannot round-trip at all.
108
- - **Fix**: test `value.data instanceof Uint8Array || value.data instanceof Uint8ClampedArray` (or use `compute_binary_data_type_from_typed_array`), and record which of the two to restore.
109
- - **Confidence**: Confirmed
110
-
111
- ### GFXT-11 Binary/JSON serialisation of a `Sampler2D` ignores the view's `byteOffset`
112
- - **Severity**: MAJOR
113
- - **File**: `engine/graphics/texture/sampler/serialization/TextureBinaryBufferSerializer.js:102,114`; same defect in `Sampler2D.js:942` (`toJSON`)
114
- - **Defect**: `serializeTexture` writes `new Uint8Array(array.buffer)` starting at byte 0 of the *underlying ArrayBuffer*, ignoring `array.byteOffset`; `toJSON` base64-encodes `this.data.buffer` wholesale for the same reason. When the sampler's data is a subarray view, the wrong bytes are serialised.
115
- - **Failure scenario**: `SplatMapping.js:307` builds per-layer samplers as `new Sampler2D(this.weightData.subarray(startAddress, endAddress), 1, width, height)`. Serialising layer 2 of a 4-layer 256×256 splat map writes `width*height*1` bytes from offset 0 — i.e. layer 0's weights — and deserialising reproduces layer 0's data under layer 2's identity. `toJSON` additionally throws on an `Array`-backed sampler, since a plain array has no `.buffer`.
116
- - **Fix**: `buffer.writeBytes(new Uint8Array(array.buffer, array.byteOffset, byteSize), 0, byteSize)`; in `toJSON`, encode `this.data.buffer.slice(byteOffset, byteOffset + byteLength)` (and handle plain arrays explicitly).
117
- - **Confidence**: Confirmed
118
-
119
- ### GFXT-12 `Sampler2D.resize` crashes on an array-backed sampler
120
- - **Severity**: MAJOR
121
- - **File**: `engine/graphics/texture/sampler/Sampler2D.js:801-829`
122
- - **Defect**: `resize` obtains a constructor via `typedArrayConstructorByInstance` (which returns `Array` for a plain array), allocates `new Array(length)` — a sparse array of holes — and in the width-preserving fast path calls `oldData.subarray(...)`, which plain arrays do not have. Plain `number[]` storage is explicitly supported everywhere else in the class (constructor default `data = []`, the `Array.isArray` branches in `clone`, `computeByteSize`, and `hash`).
123
- - **Failure scenario**: `new Sampler2D([1,2,3,4], 1, 2, 2).resize(2, 4)` throws `TypeError: oldData.subarray is not a function`. Taking the other branch, `new Sampler2D([1,2,3,4], 1, 2, 2).resize(3, 3)` yields a sparse 9-slot array; `readChannel(2, 0, 0)` returns `undefined` instead of 0, and any arithmetic on it yields `NaN`.
124
- - **Fix**: branch on `Array.isArray(oldData)` — allocate `new Array(length).fill(0)` and use `slice`/element copy instead of `subarray`.
125
- - **Confidence**: Confirmed
126
-
127
- ### GFXT-13 `sampler2d_copy_rectangle` samples at pixel corners, so an identity copy blurs and shifts
128
- - **Severity**: MAJOR
129
- - **File**: `engine/graphics/texture/sampler/sampler2d_copy_rectangle.js:28,32`
130
- - **Defect**: The source UV for output row `y` is `y * (v_span / target_height) + v0`, i.e. the *corner* of the output pixel. `sampleBilinearUV` then converts UV to texel space with `v * height - 0.5`, so the net sample position is half a destination pixel above/left of the pixel centre. The `+ 0.5` destination-pixel-centre term is missing.
131
- - **Failure scenario**: `sampler2d_copy_rectangle(src, dst, 0, 0, 1, 1)` with `src` and `dst` both 4×4 (a straight full-rect copy) produces rows `[t0, (t0+t1)/2, (t1+t2)/2, (t2+t3)/2]` per column instead of `[t0, t1, t2, t3]`. The copy is blurred and shifted half a texel up-left, and the last row/column of the source is never fully reached.
132
- - **Fix**: `const source_v = (y + 0.5) * v_multiplier + v0;` and `const source_u = (x + 0.5) * u_multiplier + u0;`.
133
- - **Confidence**: Confirmed
134
-
135
- ### GFXT-14 `sampler2d_scale_down_mipmap` builds its mip chain from the width only
136
- - **Severity**: MAJOR
137
- - **File**: `engine/graphics/texture/sampler/resize/sampler2d_scale_down_mipmap.js:20`
138
- - **Defect**: `while (current_mip.width > dest_width && current_mip.width > 1)` never consults the height. Each iteration halves *both* dimensions, so for a non-square source (or an aspect-changing resize) the chain stops based on X alone and Y is either over- or under-reduced.
139
- - **Failure scenario**: source 256×64, destination 32×32. The loop runs three times (256→128→64→32), producing mips 128×32, 64×16, 32×8; `mip_mix = inverseLerp(64, 32, 32) = 1`, so the output is taken entirely from the 32×8 mip and then stretched back to 32 rows. Four vertical levels of detail are destroyed. In the reverse case — source 256×256, destination 256×8 — the loop body never executes, `inverseLerp(256, 256, 256)` returns 0 (its documented `range === 0` fallback), and the result is a raw bilinear resample of the full-resolution source with no prefiltering at all, so the vertical downscale aliases badly.
140
- - **Fix**: drive the loop on both axes (`current_mip.width > dest_width || current_mip.height > dest_height`, clamping each halving at 1) and compute `mip_mix` from whichever axis is actually reducing.
141
- - **Confidence**: Confirmed
142
-
143
- ### GFXT-15 `sampler2d_scale_down_lanczos` centres its kernel on a texel corner, not a texel centre
144
- - **Severity**: MAJOR
145
- - **File**: `engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js:127,134`
146
- - **Defect**: `center_x` is computed in continuous source space as `(u + 0.5) * ratio_x`, where source texel `i` has its centre at `i + 0.5`. The tap distance must therefore be `(i + 0.5) - center_x`, but the code uses `f_x = i - center_x` (likewise `f_y`). The near-identical `filter/sampler2d_scale_down_generic.js:63,70` gets this right (`(j - center_y) + 0.5`), which confirms the intent.
147
- - **Failure scenario**: 512×512 → 256×256 (`ratio = 2`). For output pixel `u = 0`, `center_x = 1.0`. The correct tap distances to source texels 0 and 1 are `-0.5` and `+0.5` (symmetric); the code produces `-1.0` and `0.0`, centring the kernel on texel 1. Every output pixel is biased toward the higher-index source texel, so the whole downscaled image is shifted half a source texel up and left relative to a correct Lanczos resample.
148
- - **Fix**: `const f_y = (j - center_y) + 0.5;` and `const f_x = (i - center_x) + 0.5;`, matching the generic downsampler.
149
- - **Confidence**: Confirmed
150
-
151
- ### GFXT-16 Downsamplers discard negative filter lobes, silently substituting a different filter
152
- - **Severity**: MAJOR
153
- - **File**: `engine/graphics/texture/sampler/filter/sampler2d_scale_down_generic.js:80-82`; same code at `resize/sampler2d_scale_down_lanczos.js:144-146`
154
- - **Defect**: `if (weight <= 0) { continue; }` skips the tap entirely and excludes it from the normalising sum `z`. That is harmless for strictly non-negative kernels (`box`, `triangle`, `gaussian`, `kaiser_1`) but silently truncates every windowed-sinc / BC-spline kernel to its positive lobes only.
155
- - **Failure scenario**: `sampler2d_scale_down_generic(src, dst, filter_lanczos3, 3)` on a 512×512 → 128×128 downscale. `filter_lanczos3(1.5) = -0.135` and every tap with `|x| ∈ (1,2) ∪ (2,3)` is dropped, so the effective kernel is a positive-lobes-only blob normalised over its own positive weights. The result is a soft, Gaussian-like downscale with none of Lanczos-3's edge acutance — the caller asked for Lanczos and received something else. `mitchell` (which dips to about `-0.035` around `x = 1.5`) is affected the same way, as is the built-in `lanczos` with `lobes = 2`.
156
- - **Fix**: only skip taps that are exactly zero (`if (weight === 0) continue;`) and accumulate negative weights into `z` normally.
157
- - **Confidence**: Confirmed
158
-
159
- ### GFXT-17 `cubic2` evaluates the wrong polynomial in both branches
160
- - **Severity**: MAJOR
161
- - **File**: `engine/graphics/texture/sampler/filter/cubic2.js:17,21,25,29`
162
- - **Defect**: Three separate errors. (a) Each term is written `Math.pow(coefficient * Math.abs(x), n)` — the coefficient is raised to the power along with `|x|` instead of multiplying `|x|^n`. (b) The two branches are swapped: the `|x| < 1` arm evaluates the `1 ≤ |x| < 2` polynomial and vice versa. (c) The `/6` normalisation is applied to only one term in one branch and omitted entirely in another. Compare `mitchell.js`, which implements the same BC-spline family correctly.
163
- - **Failure scenario**: with `b = 0, c = 1`, `cubic2(0)` must be `(6 - 2b)/6 = 1`; it returns `pow(0,3) + pow(0,2) + 0 + 24 = 24`. `cubic2(1.5)` must be `-0.125`; it returns `pow(9,3) + pow(-18,2) + 6 = 1059`. Used as a weight function the filter produces weights three to four orders of magnitude off and of the wrong shape. Separately, `cubic2` never sets a `.support` property, so `sampler2d_scale_down_generic(input, output, cubic2)` (whose `support` parameter defaults to `weight_computer.support`) computes `range2_x = Math.ceil(ratio_x * undefined / 2) = NaN`, iterates zero taps, and divides by `z = 0` — every output texel becomes `NaN`.
164
- - **Fix**: rewrite as `(A*|x|**3 + B*|x|**2 + C*|x| + D) / 6` with the standard BC-spline coefficients in the correct branches (mirror `mitchell.js`), and add `cubic2.support = 2;`.
165
- - **Confidence**: Confirmed
166
-
167
- ### GFXT-30 `sampler2d_copy_with_margins` addresses the *source* with the *destination* channel count
168
- - **Severity**: MAJOR
169
- - **File**: `engine/graphics/texture/sampler/sampler2d_copy_with_margins.js:45,66,73,91,110,123,144,151`
170
- - **Defect**: Every source column offset is computed as `… * dItemSize` (the destination's channel count). `sItemSize` is read on line 29 and then used only to derive `_itemSize` and `sRowSize`, never for source column addressing. The function is explicitly built to tolerate differing channel counts (`_itemSize = Math.min(dItemSize, sItemSize)`), so this is not an implicit precondition.
171
- - **Failure scenario**: copying a 3-channel RGB patch into a 4-channel RGBA atlas. For the top-margin row, `sOffset = sA + (x + sourceX) * 4` addresses source element `4x` where element `3x` was meant. At `x = 3` it reads element 12 — texel 4's red — instead of element 9, texel 3's red; and because the read then takes 3 consecutive elements it splices `(R4, G4, B4)` where `(R3, G3, B3)` belonged. The margins are colour-shifted garbage sampled a third of a row too far right, and past `x = source.width * 3/4` they run off the end of the row into the next scanline. (The interior patch itself is correct — it goes through `destination.copy`, which uses `sItemSize`.)
172
- - **Fix**: replace `dItemSize` with `sItemSize` in every source-offset expression (lines 45, 66, 73, 91, 110, 123, 144, 151); leave the destination offsets on `dItemSize`.
173
- - **Confidence**: Confirmed
174
-
175
- ### GFXT-18 `TextureBinaryBufferSerializer` can write a Float16 texture it cannot read back
176
- - **Severity**: MINOR
177
- - **File**: `engine/graphics/texture/sampler/serialization/TextureBinaryBufferSerializer.js:37-72`
178
- - **Defect**: `DataTypeIndices` assigns `Float16: 8` and `serializeTexture` happily writes it, but `readTypedArray`'s switch has no `BinaryDataType.Float16` arm, so it returns `null` and throws.
179
- - **Failure scenario**: `serializeTexture(buf, Sampler2D.float16(1, 8, 8))` succeeds; `deserializeTexture(buf)` throws `TypeError: Unsupported data type float16`. The data is written to disk in a format that can never be loaded.
180
- - **Fix**: add `case BinaryDataType.Float16: result = new Float16Array(length); buffer.readUint16Array(new Uint16Array(result.buffer), 0, length); break;` (or the matching typed reader), or reject Float16 at serialise time.
181
- - **Confidence**: Confirmed
182
-
183
- ### GFXT-19 `computeSignedDistanceField_NaiveFlood` skips the top neighbours of cell (0,1)
184
- - **Severity**: MINOR
185
- - **File**: `engine/graphics/texture/sampler/distance/computeSignedDistanceField_NaiveFlood.js:38`
186
- - **Defect**: `const top = index > width;` should be `index >= width`. Row 0 correctly has no top neighbours (`index` in `[0, width-1]`), but `index === width` — which is `(x=0, y=1)` — also fails the test even though it does have a row above it.
187
- - **Failure scenario**: an 8×8 input whose only seed pixel is at (0,0). Cell (0,1) never sees its top-row neighbours, so it is not seeded from (0,0) or (1,0) during the initial open-set scan and is reached only later through (1,1). Its distance comes out one step larger than the correct value, and that error propagates down column 0.
188
- - **Fix**: `const top = index >= width;`
189
- - **Confidence**: Confirmed
190
-
191
- ### GFXT-20 `Sampler2D` constructor validates `width` twice and never validates `height`
192
- - **Severity**: MINOR
193
- - **File**: `engine/graphics/texture/sampler/Sampler2D.js:45`
194
- - **Defect**: `if (!Number.isInteger(height) || width < 0)` — the second clause re-tests `width`. A negative integer `height` passes both this guard and the buffer-underflow check below it (the product `width * height * itemSize` goes negative, so `data.length < …` is trivially false).
195
- - **Failure scenario**: `new Sampler2D(new Uint8Array(0), 1, 4, -1)` constructs successfully with `height = -1` instead of throwing `height must be a non-negative integer`. Downstream, `zeroFill`/`fill` iterate zero rows and `resize` allocates a `new Constructor(negative)`, which throws a `RangeError` far from the actual mistake.
196
- - **Fix**: `if (!Number.isInteger(height) || height < 0)`.
197
- - **Confidence**: Confirmed
198
-
199
- ### GFXT-21 `sampler2d_channel_compute_min_indices` seeds the result with index 0 rather than the channel offset
200
- - **Severity**: MINOR
201
- - **File**: `engine/graphics/texture/sampler/sampler2d_channel_compute_min_indices.js:36-40`
202
- - **Defect**: `bestValue` is initialised from `data[channel]` (element index `channel`) but the corresponding reported index is hard-coded to `0`. Every index written later in the loop is a true element index `i`, so the seed is inconsistent with the rest of the output.
203
- - **Failure scenario**: a 4-channel sampler where the minimum of channel 2 occurs at texel 0. The function returns count 1 with `result[0] = 0`, but the element address of that value is 2. A caller converting the returned element index back to a texel (`index / itemSize | 0`) gets the right texel by accident here, but a caller reading `data[result[0]]` gets channel 0's value instead of channel 2's.
204
- - **Fix**: `result[result_offset] = channel;`
205
- - **Confidence**: Confirmed
206
-
207
- ### GFXT-22 `sampler2d_to_texture` hands non-uint8 data to an `UnsignedByteType` texture
208
- - **Severity**: MINOR
209
- - **File**: `engine/graphics/texture/sampler/sampler2d_to_texture.js:20-33`
210
- - **Defect**: When `sampler.itemSize === 4` the function skips conversion and references `sampler.data` directly, then unconditionally sets `result.type = UnsignedByteType` and `result.format = RGBAFormat`. The `else` branch, which allocates through `Sampler2D.uint8`, shows the function assumes 8-bit storage.
211
- - **Failure scenario**: `sampler2d_to_texture(Sampler2D.float32(4, 64, 64))` yields a `DataTexture` whose `image.data` is a `Float32Array` but whose declared type is `UnsignedByteType`. WebGL's `texImage2D` with `UNSIGNED_BYTE` and a `Float32Array` raises `INVALID_OPERATION`; the texture uploads as black.
212
- - **Fix**: take the conversion branch whenever the storage is not `Uint8Array`/`Uint8ClampedArray`, not only when `itemSize !== 4` (`sampler2d_ensure_uint8_RGBA` already implements exactly that test).
213
- - **Confidence**: Confirmed
214
-
215
- ---
216
-
217
- ## texture/atlas
218
-
219
- ### GFXT-23 `TextureAtlas.repack()` clears `Packed` twice and never clears `Painted`, so repacked patches are never redrawn
220
- - **Severity**: CRITICAL
221
- - **File**: `engine/graphics/texture/atlas/TextureAtlas.js:362`
222
- - **Defect**: `patch.clearFlag(AtlasPatchFlag.Packed | AtlasPatchFlag.Packed)` ORs the same flag with itself; `AtlasPatchFlag.Painted` was clearly intended as the second operand. `repack()` then re-places every patch through `pack()` (which reorders by largest-min-side first, so placements do change), but `paint()` at line 497 only repaints a patch when `!getFlag(Painted) || last_painted_version !== sampler.version` — both false — so no pixels move.
223
- - **Failure scenario**: an atlas holding patches A, B, C; remove B, then call `repack()`. A and C get new packing rectangles and new UVs, the pixel buffer is untouched, and no area is erased. A's UV rect now points at whatever pixels happen to occupy its new location (C's old image, or the erased hole left by B). Every repacked sprite renders the wrong texture until something else forces a version bump.
224
- - **Fix**: `patch.clearFlag(AtlasPatchFlag.Packed | AtlasPatchFlag.Painted);` — and erase the vacated areas (or call `erase()`) before the re-pack so leftovers do not bleed into padding.
225
- - **Confidence**: Confirmed
226
-
227
- ### GFXT-24 `AtlasLookupTexture.update()` never allocates storage for the first 32 patches
228
- - **Severity**: CRITICAL
229
- - **File**: `engine/graphics/texture/atlas/AtlasLookupTexture.js:58-65`
230
- - **Defect**: The reallocation is gated solely on `image.height < expected_row_count`. The texture starts at 1×1 with a 4-element `Uint16Array`, and `expected_row_count = Math.ceil(patch_count / 32)` is also 1 for any patch count in `[1, 32]`, so the branch never fires and neither `image.width` (still 1) nor `image.data` (still 4 elements) is ever corrected.
231
- - **Failure scenario**: attach an atlas holding 10 patches and call `update()`. The write loop runs for `i = 0..9` at addresses `0..39`, but the backing array has 4 slots — patches 1 through 9 are silently discarded (typed-array out-of-range writes are no-ops). The texture stays 1×1, so the shader can only ever read patch 0's rect and every other sprite samples the wrong region.
232
- - **Fix**: gate on the full shape, e.g. `if (image.width !== rowSize || image.height < expected_row_count)`, and set `image.width`/`image.height`/`image.data` together.
233
- - **Confidence**: Confirmed
234
-
235
- ### GFXT-25 `WebGLTextureAtlas.add()` never records the patch size, so every patch packs to a zero-area box at (0,0)
236
- - **Severity**: CRITICAL
237
- - **File**: `engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js:93-108` (also `:112`, `:178`)
238
- - **Defect**: `add()` receives `target_width`/`target_height`, uses them only to accumulate `__total_area`, and never writes them into `patch.packing`; nothing ever calls `updatePositionFromPacking`, so `patch.position` also stays `(0,0)`. `layout()` therefore packs default (zero-extent) AABBs, and `build()` copies every source texture to `patch.position` — always the origin. Separately, `power = Math.ceil(Math.log2(this.__total_area))` treats an *area* as if it were a side length.
239
- - **Failure scenario**: `add(tex, 0, 0, 64, 64, 64, 64)` for four textures, then `layout()` and `build(renderer)`. `__total_area = 16384`, so `power = 14` and the atlas is sized 16384×16384 (a ~1 GB `Uint8Array`) for four 64×64 tiles. All four packings remain zero-area at the origin, every `copyTextureToTexture` writes to `(0,0)`, and the four textures overwrite one another. The class cannot produce a usable atlas.
240
- - **Fix**: in `add()`, `patch.size.set(target_width, target_height)` and `patch.packing.set(0, 0, target_width, target_height)`; after `layout()`, call `patch.updatePositionFromPacking(this.__width, this.__height)`; and size the atlas from `Math.ceil(Math.log2(Math.sqrt(this.__total_area)))`. Also index the source array by `patch.id` in `build()` (`this.__sources[atlas_patch.id]`) to match how `add()` stores it.
241
- - **Confidence**: Confirmed
242
-
243
- ### GFXT-26 `AtlasLookupTexture` declares an RGB internal format for RGBA integer data
244
- - **Severity**: MAJOR
245
- - **File**: `engine/graphics/texture/atlas/AtlasLookupTexture.js:27-32`
246
- - **Defect**: `texture.format = RGBAIntegerFormat` with four `Uint16` components per texel, but `texture.internalFormat = 'RGB16UI'`. GLES 3.0 requires format `RGB_INTEGER` for internal format `RGB16UI`; `RGBA_INTEGER` pairs only with `RGBA16UI`.
247
- - **Failure scenario**: the first upload issues `texImage2D(TEXTURE_2D, 0, RGB16UI, w, h, 0, RGBA_INTEGER, UNSIGNED_SHORT, data)` → `GL_INVALID_OPERATION`. No texels are stored, so every `texelFetch` of the lookup table returns zeros and all patch rects resolve to `(0,0,0,0)`.
248
- - **Fix**: `texture.internalFormat = 'RGBA16UI';`
249
- - **Confidence**: Confirmed
250
-
251
- ---
252
-
253
- ## texture/virtual
254
-
255
- ### GFXT-27 Changing `VirtualTexturePage.tile_resolution` leaves the residency capacity and tiles-per-row stale
256
- - **Severity**: MAJOR
257
- - **File**: `engine/graphics/texture/virtual/VirtualTexturePage.js:134-139` (consumed at `:263`, `:349-367`; driven from `VirtualTextureSystem.js:39`)
258
- - **Defect**: The `tile_resolution` setter recomputes `#tile_slot_resolution` and drops residency, but never calls `#update_residency_capacity()`. `#residency_tile_capacity` and `#page_texture_resolution_in_tiles` are only refreshed by the `page_texture_size` setter, so they keep values derived from the previous slot resolution.
259
- - **Failure scenario**: `system.initialize({page_resolution: 4096, …})` fixes capacity at `floor(4096/136)² = 900` slots (default tile 128 + 2×4 margin). A subsequent `system.setTexture({tile_resolution: 256, …})` raises the slot resolution to 264 — true capacity is `floor(4096/264)² = 225` — but capacity stays 900 and tiles-per-row stays 30. `#make_resident` then accepts slot 899 and computes `write_position = (29*264, 29*264) = (7656, 7656)`, far outside the 4096×4096 page, and the shader's `u_page_resolution` of 30 mis-maps every slot index to a page coordinate. The only in-repo call site (`virtual/prototype.js:225-230`) happens to call `setTexture` *before* `initialize`, which masks the bug.
260
- - **Fix**: call `this.#update_residency_capacity()` at the end of the `tile_resolution` setter (and from `#update_tile_slot_resolution`).
261
- - **Confidence**: Confirmed
262
-
263
- ### GFXT-28 `VirtualTextureSystem.setTexture` silently ignores its `tile_margin` argument
264
- - **Severity**: MAJOR
265
- - **File**: `engine/graphics/texture/virtual/VirtualTextureSystem.js:28-43` (`VirtualTexturePage` has a `tile_margin` getter at `:148` but no setter)
266
- - **Defect**: `tile_margin` is destructured out of the options object and never used; `VirtualTexturePage.#tile_margin` is hard-wired to 4 and cannot be configured at all.
267
- - **Failure scenario**: an asset pipeline that bakes 256 px tiles with an 8 px margin, configured as `setTexture({tile_resolution: 256, tile_margin: 8, …})`. The page still uses margin 4, so `#tile_slot_resolution` is 264 while each loaded tile is 272×272. In `#make_resident`, `tile_texture.image.data.set(tile.data.data)` copies 272²·4 values into a 264²·4 buffer and throws `RangeError: offset is out of bounds`; the page loads no tiles at all. The `assert.equal(tile.data.width, this.#tile_slot_resolution)` that would have diagnosed this is compiled out in production.
268
- - **Fix**: add a `tile_margin` setter on `VirtualTexturePage` that updates `#tile_slot_resolution`, refreshes the residency capacity and drops residency, and have `setTexture` forward the argument.
269
- - **Confidence**: Confirmed
270
-
271
- ### GFXT-31 `ResidencyDebugView` maps page slots to a grid using the tile size instead of the slot size
272
- - **Severity**: MINOR
273
- - **File**: `engine/graphics/texture/virtual/debug/ResidencyDebugView.js:27,38-45`
274
- - **Defect**: `tile_columns = t.page_texture_resolution[0] / tile_resolution` divides by the tile resolution, but `VirtualTexturePage` lays slots out on a grid of `tile_resolution + 2 * tile_margin` and exposes the correct count as `page_texture_resolution_in_tiles[0]`. The draw also ignores the per-slot margin offset.
275
- - **Failure scenario**: a 4096 px page with 128 px tiles and the default 4 px margin has 30 slot columns; the view computes 32. Slot 31, which physically lives at grid cell (1,1), is drawn at cell (31,0) — off the right edge of the visualisation. Every slot from 30 upward is drawn in the wrong cell, so the residency picture does not correspond to the page.
276
- - **Fix**: use `t.page_texture_resolution_in_tiles[0]` for the column count and step by `tile_resolution + 2 * t.tile_margin`, offsetting the blit by `t.tile_margin`.
277
- - **Confidence**: Confirmed
278
-
279
- ### GFXT-29 Virtual-texture fragment shader neither floors nor clamps the computed LOD
280
- - **Severity**: MINOR
281
- - **File**: `engine/graphics/texture/virtual/VirtualTextureMaterial.js:139-144` (`:109`)
282
- - **Defect**: `float texture_lod_0 = float(texture_lod);` is a no-op cast where `floor(texture_lod)` was intended (the next line uses `ceil` for the upper level). Neither level is clamped to `[0, u_max_mip_level]` before `uint(...)` conversion and the `u_max_mip_level - texture_lod_0` subtraction.
283
- - **Failure scenario**: a surface close enough to the camera that one screen pixel covers less than one virtual texel gives `texture_lod < 0`. `uint(-0.5)` is undefined in GLSL ES 3.0 and in practice yields a huge value, so `u_max_mip_level - texture_lod_0` underflows the unsigned subtraction and `1u << ui_texture_lod_0` shifts by an out-of-range amount — the mapping fetch lands on an arbitrary texel and the surface renders with a random tile. Symmetrically, a distant surface with `texture_lod > u_max_mip_level` underflows the same subtraction.
284
- - **Fix**: `float texture_lod_0 = floor(texture_lod);` and clamp both levels: `clamp(texture_lod_0, 0.0, float(u_max_mip_level))` before converting to `uint`.
285
- - **Confidence**: Likely (GL-side behaviour not executed here; the `float()`-for-`floor()` substitution itself is confirmed by inspection)
286
-
287
- ---
288
-
289
- ## particles/particular — parameters, simulation, renderers, volumes
290
-
291
- ### GFXT-32 Billboard particle spec declares `color` as 1 component; the shader declares `vec4`
292
- - **Severity**: CRITICAL
293
- - **File**: `engine/graphics/particles/particular/engine/renderers/billboard/BILLBOARD_PARTICLE_SPECIFICATION.js:23` (shader at `renderers/billboard/ParticleBillboardMaterial.js:123`)
294
- - **Defect**: The spec registers `color` with `itemSize: 1`, but the billboard shader it exists to feed declares `attribute vec4 color;`. The parallel spec for the same shader, `emitter/PARTICULAR_PARTICLE_SPECIFICATION.js:68-69`, correctly uses `itemSize: 4`.
295
- - **Failure scenario**: two independent wrong outputs. (a) Rendering — a 1-component buffer bound to a `vec4` attribute has GL supply `(r, 0, 0, 1)`, so every particle renders pure red and fully opaque and per-particle alpha fade is dead (`vColor.a` is always 1). (b) Writing — `ParticlePool.writeAttributeVector4(i, …COLOR, r, g, b, a)` on an `itemSize: 1` attribute writes `array[i*1 + 0..3]`, overwriting the colours of particles `i+1, i+2, i+3`. Writing four particles in index order leaves particle 0 holding whatever particle 3 wrote.
296
- - **Fix**: `itemSize: 4` for the `color` entry, matching `PARTICULAR_PARTICLE_SPECIFICATION`.
297
- - **Confidence**: Confirmed
298
-
299
- ### GFXT-33 `ParticularEngine.advanceEmitter` collapses a layer's simulation steps into a set keyed by type
300
- - **Severity**: MAJOR
301
- - **File**: `engine/graphics/particles/particular/engine/ParticularEngine.js:155-167`
302
- - **Defect**: Each step is dispatched through one shared object per `SimulationStepType` (`const step = steps[type]`) and a layer's parameters go into the single slot `step.layer_parameters[j]`. A second definition of the same type on the same layer overwrites the first, and the execution loop then runs each *type* once in numeric enum order, discarding the authored order.
303
- - **Failure scenario**: `ParticleLayer.steps` is an ordered `List` filled straight from JSON, so expressing wind and gravity as two forces is natural — `[{FixedPhysics}, {ApplyForce, [5,0,0]}, {ApplyForce, [0,-9.8,0]}]`. `layer_parameters[j]` is set to `[5,0,0]` and immediately overwritten with `[0,-9.8,0]`; `ApplyForce.execute()` runs once. After one second a particle has `vx = 0` instead of `5` — the wind silently vanishes with no warning. Separately, an authored `[ApplyForce, FixedPhysics]` (semi-implicit Euler) executes as `[FixedPhysics, ApplyForce]` because `FixedPhysics` is enum value 0, so the first tick's gravity displacement is 0.
304
- - **Fix**: iterate the layer's `steps` list in order and issue one step invocation per definition; at minimum reject duplicate types at build time rather than silently dropping them.
305
- - **Confidence**: Confirmed (mechanism verified by inspection). Scope note: I enumerated every authored `steps` list in `app/data/meep-settings.json` — there are two, `["FixedPhysics","CurlNoiseVelocity"]` and `["FixedPhysics"]`, neither with a duplicate type and both already in enum order. So no shipped effect trips this today; it is a latent hazard for any effect that adds a second force of the same type, with no diagnostic when it does.
306
-
307
- ### GFXT-34 `ParameterLookupTable` writes its item count one byte too narrow at exact powers of two
308
- - **Severity**: MAJOR
309
- - **File**: `core/math/lookup/ParameterLookupTable.js:110` — reached through the in-scope re-export `particles/particular/engine/parameter/ParameterLookupTable.js`
310
- - **Defect**: `const itemCountBitSize = Math.log2(itemCount)` is used as if it were the value's bit width. The width is `floor(log2(n)) + 1`, so at exactly `2^k` the size class picks a field one byte too small.
311
- - **Failure scenario**: `RGB_LUT_TURBO` (in-repo, `parameter/sample/RGB_LUT_TURBO.js`) has exactly 256 entries. `toBinaryBuffer` computes `log2(256) === 8`, takes the `<= 8` branch, and calls `writeUint8(256)`, which truncates to 0 — then still writes 768 data floats plus 256 position floats. `fromBinaryBuffer` reads `itemCount = 0`, produces an empty LUT, and leaves the read cursor 4096 bytes short, so every subsequent field in the same buffer (the rest of the `ParticleParameter` / `ParameterTrack` / emitter payload) decodes as garbage. Same failure at 65536; 255, 257 and 65535 are fine.
312
- - **Fix**: size the field from the value: `itemCountByteSize = itemCount <= 0xFF ? 1 : itemCount <= 0xFFFF ? 2 : 4;`
313
- - **Confidence**: Confirmed
314
-
315
- ### GFXT-35 `ParameterLookupTable.addValue` does not recompute value statistics
316
- - **Severity**: MAJOR
317
- - **File**: `core/math/lookup/ParameterLookupTable.js:305` (end of `addValue`) — reached through the in-scope re-export
318
- - **Defect**: `addValue` splices into `positions` and `data` but never calls `computeStatistics()`. Every other mutator (`write`, `fromJSON`, `fromBinaryBuffer`) does, so a table built purely with `addValue` keeps `valueMin = valueMax = 0`.
319
- - **Failure scenario**: build a scale track programmatically — `lut.addValue(0, [1]); lut.addValue(1, [4]);` — and attach it as the `scale` track of a layer whose `particleSize.max` is 2. `ParticleLayer.computeScaledSpriteHalfSize()` (`emitter/ParticleLayer.js:165`) computes `maxParticleSize * lookupTable.valueMax = 2 * 0 = 0`, so the layer's `baseBoundingBox` gets no sprite padding: the emitter is frustum-culled and its BVH leaf is undersized while 8-unit-wide sprites are still on screen, and particles pop out at the screen edge. Secondarily, `hash()` short-circuits on `valueMin === valueMax`, so every `addValue`-built LUT hashes identically regardless of contents.
320
- - **Fix**: call `this.computeStatistics()` at the end of `addValue`.
321
- - **Confidence**: Confirmed
322
-
323
- ### GFXT-36 Barycentric point picking on a triangle is not uniform
324
- - **Severity**: MAJOR
325
- - **File**: `engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.js:158`; same code at `engine/utils/distributeParticlesOnObject3D.js:215`
326
- - **Defect**: `const r0 = random(); const r1 = random() * (1 - r0);` gives a joint density proportional to `1 / (1 - r0)` over the simplex instead of the constant required for uniformity. The cited reference is the *reflect* method (`if (r0 + r1 > 1) { r0 = 1 - r0; r1 = 1 - r1; }`), which is not what was implemented.
327
- - **Failure scenario**: 2,000,000 samples on one triangle, binned by the barycentric weight on vertex B in deciles, come out flat at `0.100` per decile where the correct distribution is `0.190, 0.170, 0.150, 0.130, 0.110, 0.090, 0.070, 0.050, 0.030, 0.010`. Density is about 10x too high in the decile nearest vertex B and about half the correct density near vertex A. Seeding a 2-triangle 10x10 ground quad with 5000 particles visibly piles them along the shared diagonal and thins the two opposite corners.
328
- - **Fix**: draw two independent uniforms and reflect, or use `su0 = Math.sqrt(random()); r0 = 1 - su0; r1 = random() * su0;`.
329
- - **Confidence**: Confirmed
330
-
331
- ### GFXT-37 `ParticleVolume.fromJSON` logs an unknown attribute and then dereferences it
332
- - **Severity**: MAJOR
333
- - **File**: `engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.js:353-359`
334
- - **Defect**: The `attribute_object === undefined` branch warns "skipping" but has no `continue`; execution falls straight through to `attribute_object.fromJSON(attribute_json)`.
335
- - **Failure scenario**: only `color`, `size` and `blending` are in `EXPOSED_ATTRIBUTES`. Loading `{ "density": 2, "shape": {…}, "attributes": { "velocity": { "value": [0,1,0] } } }` logs `No 'velocity' attribute, skipping` and then throws `TypeError: Cannot read properties of undefined (reading 'fromJSON')`, aborting the entire scene load instead of skipping one attribute.
336
- - **Fix**: add `continue;` inside the `if`.
337
- - **Confidence**: Confirmed
338
-
339
- ### GFXT-38 `lut_to_sampler` clobbers the alpha channel of RGBA lookup tables
340
- - **Severity**: MINOR
341
- - **File**: `engine/graphics/particles/particular/engine/utils/volume/lut_to_sampler.js:18`
342
- - **Defect**: `s[3] = 255` runs unconditionally after `lut.sample(...)`, so an `itemSize === 4` LUT's authored alpha is discarded. The line is only correct for the RGB (`itemSize === 3`) case.
343
- - **Failure scenario**: `lut_to_sampler(RGBA_LUT_HEATMAP_IR, 64, 4)` — the in-repo LUT authors alpha `10` at every key. `sample` writes `s[3] = 10`; the next line overwrites it with `255`. Every texel comes out fully opaque, so the sampler renders as a solid heat ramp instead of a ~4%-opacity overlay.
344
- - **Fix**: `if (lut.itemSize < 4) { s[3] = 255; }`. (Same line, secondary: `width === 1` makes `i / (width - 1)` be `NaN`, and `sample` then falls through its search loop and returns the last key rather than the first.)
345
- - **Confidence**: Confirmed
346
-
347
- ### GFXT-39 `ParticleVolume.build` hard-wires the global unseeded RNG
348
- - **Severity**: MINOR
349
- - **File**: `engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.js:599`
350
- - **Defect**: `__distribute` is explicitly parameterised on a `random` function — and both sibling utilities take an explicit seed — but `build()` passes `Math.random`.
351
- - **Failure scenario**: a scene holding a `ParticleVolume` with `density: 5` over a 10 m³ shape (50 particles) produces different particle positions on every load, and two clients viewing the same serialized scene see different smoke. The volume is not reproducible from its own serialized form.
352
- - **Fix**: accept a seed on `build()`/`fromJSON` and pass `seededRandom(seed)`, matching the mesh distributors.
353
- - **Confidence**: Confirmed
354
-
355
- ---
356
-
357
- ## geometry — buffered attributes, BVH queries, skinning
358
-
359
- ### GFXT-40 `computeMeshSurfaceArea` reads vertex C's Y from the Z slot
360
- - **Severity**: CRITICAL
361
- - **File**: `engine/graphics/geometry/computeMeshSurfaceArea.js:40`
362
- - **Defect**: `const y2 = points[c3 + 2];` — the third vertex is assembled as `(cx, cz, cz)`, so every triangle area in the mesh is computed against a corrupted vertex. (Found independently by two reviewers.)
363
- - **Failure scenario**: any mesh lying in the XY plane, e.g. the single triangle `A=(0,0,0)`, `B=(1,0,0)`, `C=(0,1,0)` whose area is `0.5`. C is read as `(0,0,0) === A`, so `tri3_area` returns 0 and the total is 0. `distributeParticlesOnMesh` then computes `surfaceSpacing = 0` → `invSurfaceSpacing = Infinity` → `placementCount = 0 * Infinity = NaN` → `for (j = 0; j < NaN; …)` never runs, so **zero particles are emitted** and the pool keeps whatever positions it had. For non-planar meshes the per-triangle area weights are simply wrong, so the area-weighted distribution is biased.
364
- - **Fix**: `const y2 = points[c3 + 1];`
365
- - **Confidence**: Confirmed
366
-
367
- ### GFXT-41 `bvh32_geometry_raycast` reports a hit it never wrote
368
- - **Severity**: MAJOR
369
- - **File**: `engine/graphics/geometry/buffered/query/bvh32_geometry_raycast.js:124`
370
- - **Defect**: `hit_found = true` is set as soon as a triangle intersects, before the `best_distance_sqr` test that decides whether `result` is written. If every intersection lies beyond `max_distance`, the function returns `true` with `result` untouched.
371
- - **Failure scenario**: the broadphase `bvh32_query_user_data_ray` prunes on AABB *entry* distance, so a triangle whose box is entered early but whose surface is hit late survives the cull. Triangle `A=(0,0,0)`, `B=(10,0,20)`, `C=(0,10,20)` (AABB `(0,0,0)-(10,10,20)`), ray origin `(1,1,-1)`, direction `(0,0,1)`, `max_distance = 4`: AABB entry `t = 1 < 4` so the triangle is reported, but the surface hit is at `(1,1,4)`, distance 5. `distance_sqr = 25 >= 16`, so `result` is never written — yet the function returns `true` and the caller reads a stale `SurfacePoint3` from the previous query.
372
- - **Fix**: move `hit_found = true;` inside the `if (distance_sqr < best_distance_sqr)` block.
373
- - **Confidence**: Confirmed
374
-
375
- ### GFXT-42 `decode_attribute_value` throws on every 8-bit and signed normalized type
376
- - **Severity**: MAJOR
377
- - **File**: `engine/graphics/geometry/decode_attribute_value.js:17-27`
378
- - **Defect**: The denormalization switch handles only `Float32`, `Float64`, `Uint16` and `Uint32`; `Uint8`, `Int8`, `Int16` and `Int32` fall through to `throw`. The sibling `core/collection/array/typed/typed_array_value_denormalize.js` already carries the full, correct table.
379
- - **Failure scenario**: glTF requires `COLOR_0` as unsigned byte to be `normalized: true`, and `GLTFLoader` produces a `Uint8Array` `BufferAttribute` accordingly. `sh3/path_tracer/texture/sample_material.js:37-42` derives `BinaryDataType.Uint8` from that array and calls `decode_attribute_value(v, Uint8, true)`, which throws `Unsupported data type uint8` and aborts the path-tracer sample for that surface. The same happens for any `KHR_mesh_quantization` asset (`Int8`/`Int16`).
380
- - **Fix**: add the missing arms mirroring `typed_array_value_denormalize` — `Uint8 → /255`, `Int8 → max(v/127, -1)`, `Int16 → max(v/32767, -1)`, `Int32 → max(v/2147483647, -1)`.
381
- - **Confidence**: Confirmed
382
-
383
- ### GFXT-43 `computeSkinnedMeshVertices` reads attributes as raw `.array` with a hard-coded stride
384
- - **Severity**: MAJOR
385
- - **File**: `engine/graphics/geometry/skining/computeSkinnedMeshVertices.js:24-26`
386
- - **Defect**: `attributes.position.array` / `skinIndex.array` / `skinWeight.array` are read directly and then indexed with a fixed stride of 3 or 4, ignoring both interleaving and the `normalized` flag. In three r136 `InterleavedBufferAttribute.array` is a getter returning `this.data.array` — the *whole* interleaved buffer.
387
- - **Failure scenario**: a glTF skinned mesh with an interleaved vertex buffer (POSITION + NORMAL + TEXCOORD_0, stride 8 floats). `positions[index * 3]` walks three floats at a time through position, normal and UV data, so `mesh_apply_skeletal_vertex_skinning` produces completely scrambled vertices. Second case: gltfpack / `KHR_mesh_quantization` output where `WEIGHTS_0` is normalized `Uint8` — a weight is read as `255` instead of `1.0`, scaling the skinned position by 255x. Both feed `computeSkinnedMeshBoundingVolumes` (used by `GLTFAssetLoader.js:307`), `meshFromSkinnedMesh`, and the particle distributors.
388
- - **Fix**: route the three attributes through the existing `buffered/buffer_attribute_deinterleave.js` + `buffered/buffer_attribute_denormalize.js` before reading `.array`, or pass real `offset`/`stride`/`normalized` into `mesh_apply_skeletal_vertex_skinning`.
389
- - **Confidence**: Confirmed
390
-
391
- ### GFXT-44 `computeGeometryBoundingSphereMiniball` builds its point set from the raw attribute array
392
- - **Severity**: MAJOR
393
- - **File**: `engine/graphics/geometry/buffered/computeGeometryBoundingSphereMiniball.js:10`
394
- - **Defect**: `const vertexData = geometry.attributes.position.array;` followed by a point count of `positions.length / 3`. As above, for an interleaved attribute that array is the entire interleaved buffer, and for a quantized attribute the values are un-denormalized.
395
- - **Failure scenario**: interleaved glTF geometry (position + normal + uv, stride 8) makes `PointSet(len/3, 3, wholeBuffer)` fold normal and UV components into the point cloud, so the sphere centre and radius are computed over garbage. Quantized `Int16` normalized positions give a radius roughly 32767x the true size. `ensureGeometryBoundingSphere` → this function is what `GLTFAssetLoader.__processMesh` uses for every non-skinned mesh, so the result is broken frustum culling — meshes popping out of view, or never culling at all.
396
- - **Fix**: deinterleave and denormalize first, and drive the loop from `attribute.count` rather than `array.length`.
397
- - **Confidence**: Confirmed
398
-
399
- ### GFXT-45 Degenerate triangles inject a fabricated up-normal into vertex-normal accumulation
400
- - **Severity**: MAJOR
401
- - **File**: `engine/graphics/geometry/buffered/geometry_compute_vertex_normals_indexed.js:28-37`
402
- - **Defect**: `geometry_construct_triangle_normal` delegates to `tri3_compute_normal`, which writes the sentinel `(0, 1, 0)` when the cross product has zero length. This loop adds that full-magnitude unit vector into all three vertices' accumulated normals instead of contributing nothing.
403
- - **Failure scenario**: a mesh with a collapsed triangle — indices `[0,1,2, 0,1,1]`, where 0/1/2 form a flat face in the XY plane with normal `(0,0,1)`. Vertex 0 accumulates `(0,0,1)` from the good triangle plus `(0,1,0)` from the degenerate one; after `v3_array_normalize_many` its normal is `(0, 0.707, 0.707)` instead of `(0,0,1)`. Shading is visibly wrong along every collapsed edge — routine in LOD/simplified meshes and n-gon triangulation output.
404
- - **Fix**: accumulate the raw unnormalized cross product in this loop (which contributes zero for a degenerate face and also gives three.js-compatible area weighting), or have `geometry_construct_triangle_normal` signal degeneracy and `continue` on it.
405
- - **Confidence**: Confirmed
406
-
407
- ### GFXT-46 `meshFromSkinnedMesh` applies the world transform twice
408
- - **Severity**: MAJOR
409
- - **File**: `engine/graphics/geometry/skining/meshFromSkinnedMesh.js:33-36`
410
- - **Defect**: `computeSkinnedMeshVertices` produces `bindMatrixInverse · Σ(w·boneMatrix) · bindMatrix · p`, which is exactly what three's skinning shader hands to `modelMatrix` — i.e. mesh-local space by construction. The function nonetheless bakes `inverse(matrixWorld)` into those vertices while the new `Mesh` separately copies the source's local TRS, so the transform is applied once forward and once inverted.
411
- - **Failure scenario**: a `SkinnedMesh` at `position = (0,5,0)` with no parent, in rest pose. `V = p`. `mesh.position` is copied as `(0,5,0)` and the geometry is baked to `p - (0,5,0)`, so the result renders at `translate(0,5,0) · (p - (0,5,0)) = p` while the original renders at `p + (0,5,0)`. The exported mesh is displaced by the full world transform; the bug is invisible only when `matrixWorld` is identity. Reachable from `model/game/options/exportSceneAsGLTF.js:166,181`.
412
- - **Fix**: drop the `applyMatrix4(m4)` call (and the `updateMatrix`/`updateMatrixWorld` that exist only to feed it); the copied TRS already reproduces the source placement.
413
- - **Confidence**: Likely
414
-
415
- ### GFXT-47 `compute_buffer_geometry_byte_size` charges every attribute the size of its whole backing buffer
416
- - **Severity**: MINOR
417
- - **File**: `engine/graphics/geometry/buffered/compute_buffer_geometry_byte_size.js:12,16`
418
- - **Defect**: Uses `array.buffer.byteLength` (the whole `ArrayBuffer`) rather than `array.byteLength` (the view). Attributes that are views into one shared buffer are each charged the full buffer.
419
- - **Failure scenario**: a glTF geometry with position, normal and uv interleaved in one 1 MB `ArrayBuffer` reports about 3 MB, plus the index buffer's own full backing buffer. Any cache sized by this number — the pattern `GeometrySpatialQueryAccelerator` uses for its `valueWeigher` — evicts several times too aggressively.
420
- - **Fix**: `result += attribute.array.byteLength;`
421
- - **Confidence**: Confirmed
422
-
423
- ### GFXT-48 `bvh32_geometry_raycast` returns an unset `index` and an unnormalized normal
424
- - **Severity**: MINOR
425
- - **File**: `engine/graphics/geometry/buffered/query/bvh32_geometry_raycast.js:59,126-131`
426
- - **Defect**: `triangle_index` is in hand at line 59 but never assigned to the scratch hit, so `result.copy(scratch_hit)` overwrites `result.index` with the module scratch's permanent `-1`. `result.normal` is the raw cross product produced by `tri3_ray_intersection`, not a unit vector.
427
- - **Failure scenario**: raycasting the triangle `A=(0,0,0)`, `B=(10,0,0)`, `C=(0,10,0)` returns `normal = (0,0,100)` (length `2·area`) and `index = -1`. The sibling `bvh32_geometry_nearest` over the same geometry returns a unit normal and a valid triangle index, so the two query entry points disagree on their output contract. Consumers that route through `SurfacePoint3.applyMatrix4` are masked because `applyDirectionMatrix4` renormalizes.
428
- - **Fix**: set `scratch_hit.index = triangle_index;` before the copy, and normalize the normal (root cause is `core/geom/3d/triangle/tri3_ray_intersection.js:146`).
429
- - **Confidence**: Confirmed
430
-
431
- ### GFXT-49 `meshFromSkinnedMesh` keeps the source geometry's bind-pose bounding volumes
432
- - **Severity**: MINOR
433
- - **File**: `engine/graphics/geometry/skining/meshFromSkinnedMesh.js:13-21`
434
- - **Defect**: `geometry.clone()` carries `boundingBox` and `boundingSphere` across (r136 `BufferGeometry.copy` clones both when non-null), and the position array is then overwritten with skinned vertices without invalidating them.
435
- - **Failure scenario**: a humanoid whose source geometry already has bounds — always the case after `GLTFAssetLoader.__processMesh` — baked while crouched or mid-jump keeps the bind-pose sphere. `WebGLRenderer` frustum-culls the result against bounds that no longer enclose the geometry, so the baked mesh pops out of view at screen edges.
436
- - **Fix**: null out `bakedGeometry.boundingBox` and `bakedGeometry.boundingSphere` after writing the skinned positions, or recompute them.
437
- - **Confidence**: Confirmed
438
-
439
- ### GFXT-50 `aabb3_to_buffer_geometry` winds its top face inward (and the module exports nothing)
440
- - **Severity**: MINOR
441
- - **File**: `engine/graphics/geometry/aabb3_to_buffer_geometry.js:8,19-22`
442
- - **Defect**: The top quad repeats the bottom quad's winding, so its face normal points down instead of up. Separately the function declaration lacks `export`, so the module has no exports at all and no importer can currently reach it — which is why the winding error has gone unnoticed.
443
- - **Failure scenario**: `AABB3.getCorners` emits corners in the order `(x0y0z0, x1y0z0, x0y1z0, x1y1z0, x0y0z1, x1y0z1, x0y1z1, x1y1z1)`. Bottom triangle `0→1→5` yields normal `(0, -dx·dz, 0)` = −Y, correctly outward. Top triangle `2→3→7` yields the same `(0, -dx·dz, 0)` = −Y, i.e. inward. With any default `FrontSide` material the top face of the box is invisible from above.
444
- - **Fix**: add `export` to the declaration and reverse the top winding to `2, 7, 3, 7, 2, 6`.
445
- - **Confidence**: Confirmed
446
-
447
- ---
448
-
449
- ## particles/particular — pool, group, emitter
450
-
451
- ### GFXT-51 `ParticleGroup.deleteIndices` reuses the same swap source for every deletion
452
- - **Severity**: CRITICAL
453
- - **File**: `engine/graphics/particles/particular/group/ParticleGroup.js:478-492`
454
- - **Defect**: The swap-source cursor `swapDestination` is decremented only when it happens to equal the current victim, never after it has actually been consumed as a swap source. Every swap in a multi-delete therefore copies from the *same* tail slot, duplicating one particle and losing the others.
455
- - **Failure scenario**: reproduced under jest against the real class. Four particles with a scalar attribute holding `[10, 11, 12, 13]`; `group.deleteIndices([0, 1])` — equivalently two `group.remove(ref)` calls in one frame, which `groupOperations` merges into a single `Remove` with two operands. Expected survivors `[12, 13]`; the actual buffer after the call is `[13, 13]` — particle `12` is gone and `13` is rendered twice. `indexReferenceLookup`/`referenceIndexLookup` are also rewritten so two live references point at the same data slot. Any batch of two or more removals that produces two or more real swaps corrupts the group. The existing spec cases (`[1]`, `[0,2,4]`, `[1,9,10,11]`) each happen to generate exactly one swap, which is why this is untested.
456
- - **Fix**: consume the source cursor and skip slots that are themselves being deleted — iterate victims ascending, advancing a tail pointer past deleted destinations, and decrement `swapDestination` after each push. A bare `swapDestination--` after the push is *not* sufficient: `indices = [0,1,3]` with `size = 5` would then copy deleted slot 3 into a survivor slot.
457
- - **Confidence**: Confirmed (reproduced against the real `ParticleGroup`)
458
-
459
- ### GFXT-52 `optimizeCommandQueue` sort comparator never returns `+1`, so writes can execute before their `Add`
460
- - **Severity**: CRITICAL
461
- - **File**: `engine/graphics/particles/particular/group/optimizeCommandQueue.js:141`
462
- - **Defect**: `if (operatorA < operatorB) return -1; else if (operatorB > operatorA) return 1;` — the second condition is a restatement of the first, not its inverse, so the operator-ordering branch can never return `+1`. When `a` is a `WriteAttribute` and `b` is an `Add`/`Remove`, control falls through to the attribute-index arithmetic and compares a *reference id* against an *attribute index*, breaking the ordering invariant stated in the comment two lines above.
463
- - **Failure scenario**: reproduced under jest. A group with 4 attributes and one existing particle (ref 0 at index 0, attribute 3 = `200`), then `r1 = create(); r2 = create(); writeAttribute(r1, 3, [11]); writeAttribute(r2, 3, [22]); update();`. `groupOperations` merges the adds into `Add[1,2]`; the comparator evaluates `b.operands[1] - a.operands[1] = 2 - 3 = -1` and sorts the writes first. Observed order: `Write(1,3) | Write(2,3) | Add[1,2]`. Both writes dereference to `undefined`, so `attribute.array.set(value, undefined * itemSize)` is `set(value, NaN)`, which coerces to offset **0** and silently overwrites the existing particle. Attribute 3 ends up `[22, 0, 0]`: the pre-existing `200` is clobbered and both new particles get `0`. This triggers whenever a grouped `Add` has two or more operands and its second reference id is below the write's attribute index — routine, since `IdPool` hands out ids from 0 and `PARTICULAR_PARTICLE_SPECIFICATION` has 12 attributes.
464
- - **Fix**: `} else if (operatorA > operatorB) { return 1; }`
465
- - **Confidence**: Confirmed (reproduced against the real `ParticleGroup`)
466
-
467
- ### GFXT-53 `ParticleGroup.build()` leaves the geometry draw range at `Infinity`
468
- - **Severity**: MAJOR
469
- - **File**: `engine/graphics/particles/particular/group/ParticleGroup.js:101`
470
- - **Defect**: `build()` creates a fresh `BufferGeometry`, whose three.js default `drawRange` is `{start: 0, count: Infinity}`, and never calls `setDrawRange`. Only `updateCapacity()` sets it, and neither the constructor nor `reset()` reaches that path. `build()` additionally copies the *old* attribute data forward unconditionally. The sibling `ParticlePool.build()` does call `setDrawRange` (line 280).
471
- - **Failure scenario**: reproduced under jest. (a) `new ParticleGroup(spec)` gives `size = 0`, `capacity = 100`, `drawRange = {start: 0, count: Infinity}`, so the renderer draws `min(position.count, Infinity) = 100` vertices from zero-filled buffers — exactly the state `view/minimap/gl/MinimapMarkersGL.js:128` renders in (with `frustumCulled = false`) before the first marker is added. (b) Add 5 particles with attribute `a = [7,7]`, `update()` (draw range correctly becomes 5), then `reset()` → the count is `Infinity` again *and* the attribute array still reads `[7,7,7,7,7,7,7,7,7,7,…]`, so the group renders 100 vertices whose first 5 are the supposedly-cleared particles. `MinimapMarkersGL.shutdown()` takes exactly this path.
472
- - **Fix**: end `build()` with `geometry.setDrawRange(0, this.size);`, and skip the old-data copy when `this.size === 0`.
473
- - **Confidence**: Confirmed (reproduced against the real `ParticleGroup`)
474
-
475
- ### GFXT-54 `ParticleGroup.preAllocate` advertises reserved capacity as live particles
476
- - **Severity**: MAJOR
477
- - **File**: `engine/graphics/particles/particular/group/ParticleGroup.js:601`
478
- - **Defect**: `preAllocate` routes a *capacity* request through `updateCapacity`, whose tail unconditionally calls `geometry.setDrawRange(0, newSize)`. Reserving space therefore also tells the renderer that the reserved slots contain particles.
479
- - **Failure scenario**: reproduced under jest. A group with 5 live particles (`size = 5`, `capacity = 5`, `drawRange.count = 5`); `group.preAllocate(500)` gives `capacity = 505` and `size` still `5`, but `drawRange.count = 505`. The renderer draws 500 uninitialised/stale vertices alongside the 5 real particles until the next `create()` + `update()` happens to reset the range.
480
- - **Fix**: have `preAllocate` call `setCapacity(...)` (or a capacity-only helper) and leave the draw range alone — the draw range belongs to `setSize`, not to capacity management.
481
- - **Confidence**: Confirmed (reproduced against the real `ParticleGroup`)
482
-
483
- ### GFXT-55 `SERIALIZABLE_FLAGS` omits `Lit`, so `receiveLight` is dropped by binary serialization
484
- - **Severity**: MAJOR
485
- - **File**: `engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js:46` (consumed at `serde/ParticleEmitterSerializationAdapter.js:23,98-101`)
486
- - **Defect**: `SERIALIZABLE_FLAGS` lists `PreWarm | DepthSorting | DepthReadDisabled | DepthSoftDisabled | AlignOnVelocity` but not `ParticleEmitterFlag.Lit`, even though `Lit` is an authored property (`toJSON`/`fromJSON` persist it as `receiveLight`, `ParticleVolume` writes it, and `ShaderManager` compiles a different shader for it). The adapter masks with `SERIALIZABLE_FLAGS` on both write and read, so the bit is silently discarded.
487
- - **Failure scenario**: author an emitter with `receiveLight: true` (`ParticleVolume.js:296` calls `emitter.writeFlag(ParticleEmitterFlag.Lit, true)`), save the level through `ParticleEmitterSerializationAdapter.serialize`, and reload. `deserialize` neither restores nor clears `Lit`, and a fresh `ParticleEmitter` defaults it to false, so `ShaderManager.buildMaterial` (line 251) builds `make_particle_billboard_shader({ lights: false })` and the particles render unlit. Secondarily, `hash()`/`equals()` also use `SERIALIZABLE_FLAGS`, so a lit and an unlit emitter compare equal and can share a cached material record — note `EMITTER_MATERIAL_FLAGS` in `ShaderManager.js:20` *does* include `Lit`, which shows the intent.
488
- - **Fix**: add `| ParticleEmitterFlag.Lit` to `SERIALIZABLE_FLAGS` and bump the adapter version (the field width is unchanged, so a pass-through upgrader suffices).
489
- - **Confidence**: Confirmed
490
-
491
- ### GFXT-56 Sphere emission shapes get a half-size layer bounding box
492
- - **Severity**: MAJOR
493
- - **File**: `engine/graphics/particles/particular/engine/emitter/ParticleLayer.js:206`
494
- - **Defect**: `computePointBoundingBox` expands the layer bounds by `0.5 * scale` for every non-`Point` emission shape. That factor is correct only for `EmissionShapeType.Box`, whose `random_point_in_box` samples `[-0.5, 0.5]`; `random_point_in_sphere` / `random_point_on_sphere` are unit-**radius** (extent `[-1, 1]`), and the spawn path applies the full scale (`ParticleEmitter.js:1023`, `v3position.multiply(layer.scale)`).
495
- - **Failure scenario**: a layer with `emissionShape = Sphere`, `emissionFrom = Volume`, `scale = (4,4,4)`, `particleSpeed = (0,0)`, `position = (0,0,0)`. Particles occupy a radius-4 ball, but `computePointBoundingBox` returns `[-2,-2,-2 .. 2,2,2]`. `computeEmissionBounds` pads that only by the sprite half-size, so `emissionBounds` misses half the emission volume. Before any particle exists — a freshly built emitter, or one whose particles have all just died — `bvh_leaf.bounds` is `emissionBounds` alone, so the emitter is dropped from `renderLayer.visibleSet`, `ParticularEngine.updateEmitter` flags it `Sleeping`, and it stops emitting while up to 2 world units of its volume are still on screen. The emitter pops in and out at screen edges.
496
- - **Fix**: branch on `emissionShape` — full `scale` for `Sphere`, `0.5 * scale` for `Box`.
497
- - **Confidence**: Confirmed
498
-
499
- ### GFXT-57 Raising `emissionRate` releases a burst of negative-age particles
500
- - **Severity**: MAJOR
501
- - **File**: `engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js:891`
502
- - **Defect**: `spawnLayerParticlesContinuous` seeds its accumulator with `-layer.timeSinceLastEmission` and assigns `initialAge = time`, assuming `timeSinceLastEmission <= emissionPeriod`. That invariant is re-established using the period in force at the *end of the previous call*, so any increase in `emissionRate` (a period shrink) leaves a carry-over larger than the new period. With `emissionRate === 0` it is unbounded: `emissionPeriod` is `Infinity`, the loop never runs, and `timeSinceLastEmission` grows by `timeDelta` every frame forever.
503
- - **Failure scenario**: a torch layer at `emissionRate = 2` (period 0.5) reaches steady state with `timeSinceLastEmission ≈ 0.5`. Gameplay or the editor raises it to 200 — `editor/view/ecs/components/particles/ParticleLayerController.js:110` does exactly `layer.emissionRate = rate`. On the next 60 Hz frame (`timeDelta = 0.0167`, period 0.005) the loop runs from `time = -0.5` to `0.0117`, emitting roughly 102 particles in one frame instead of about 3. About 99 of them get `initialAge` between `-0.495` and `0`, so each is written at `spawnPos + velocity * negativeAge` — up to half a second of travel *behind* the emitter — and `SimulationStepFixedPhysics` will not retire them until their age climbs back through zero, giving each `deathAge + 0.495 s` of life. Holding `emissionRate = 0` for 10 s first (the `graphics/ecs/sprite/SpriteSystemPE.js:23` configuration) makes the carry-over 10 s and the burst about 2000 particles.
504
- - **Fix**: clamp the carry-over when seeding — `let time = -min2(layer.timeSinceLastEmission, emissionPeriod);` — and skip the layer entirely when `emissionRate <= 0` rather than accumulating against an infinite period.
505
- - **Confidence**: Confirmed
506
-
507
- ### GFXT-58 `ParticleLayer` binary round-trip drops the simulation step list
508
- - **Severity**: MINOR
509
- - **File**: `engine/graphics/particles/particular/engine/emitter/ParticleLayer.js:358`
510
- - **Defect**: `toBinaryBuffer`/`fromBinaryBuffer` write and read every layer field except `steps`, and unlike `fromJSON` (line 341) the binary reader has no fallback that installs the default `FixedPhysics` step.
511
- - **Failure scenario**: `srcLayer.toBinaryBuffer(b); b.position = 0; dstLayer.fromBinaryBuffer(b);` leaves `dstLayer.steps.length === 0`. `ParticularEngine.advanceEmitter` then builds an empty `step_mask` for that layer, so `SimulationStepFixedPhysics` never runs: particles never integrate velocity, `PARTICLE_ATTRIBUTE_AGE` never advances, and `particles.remove(i)` is never reached. The layer emits frozen, immortal particles that accumulate in the pool for the emitter's lifetime. The existing round-trip spec (`ParticleLayer.spec.js:48`) asserts every other field individually and never looks at `steps`, and `hash()`/`equals()` also ignore `steps`, so neither a field test nor a comparison test catches it.
512
- - **Fix**: write and read `steps` in this pair the way `ParticleEmitterSerializationAdapter` does (count, then per-step type and parameters), falling back to a single `FixedPhysics` step when the count is zero.
513
- - **Confidence**: Confirmed
514
-
515
- ---
516
-
517
- ## geometry — MikkTSpace tangents and mesh optimization
518
-
519
- ### GFXT-59 MikkT angle weighting clamps the corner cosine to `[0,1]`, flattening every obtuse angle to 90°
520
- - **Severity**: MAJOR
521
- - **File**: `engine/graphics/geometry/MikkT/EvalTspace.js:104`
522
- - **Defect**: `const fCos = clamp01(v3_dot(...));` — the reference `mikktspace.c` clamps to `[-1, 1]` (`fCos = fCos > 1 ? 1 : (fCos < -1 ? -1 : fCos)`). Clamping the low end at 0 means `Math.acos` can never exceed 90°, so every obtuse corner is under-weighted in the per-vertex tangent average.
523
- - **Failure scenario**: a vertex V with three incident triangles whose corner angles at V are 170°, 170° and 20° (a flat fan with neighbours at 0°, 170° and 340°), each with a different affine UV map. Correct weights are 170:170:20; the code produces 90:90:20. Measured against a reference re-implementation, the resulting tangent is rotated by **0.95°** and `fMagS` is off by **2.8%**. A milder 150/105/105 fan gives 0.76° / 0.8%. The error grows without bound as the per-triangle tangents diverge, and it applies to every smoothed vertex with an obtuse incident corner — fan and cap triangulations, decimated meshes, terrain — on the default 180° threshold path.
524
- - **Fix**: use a `[-1, 1]` clamp, e.g. `clamp(v3_dot(...), -1, 1)`, and drop the `clamp01` import.
525
- - **Confidence**: Confirmed (numerically measured against a reference re-implementation)
526
-
527
- ### GFXT-60 `merge_by_material` skips an element after every non-merging splice
528
- - **Severity**: MAJOR
529
- - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:189`
530
- - **Defect**: The loop splices the processed entry out of `undecided` while iterating with an incrementing `i`, but only compensates with `i--` when a merge actually happened (`set_indices.length > 1`). On the singleton path the array shrinks under the cursor, so the element that slides into slot `i` is skipped for the rest of the pass and is later re-scanned alone — it never gets compared against its remaining partners.
531
- - **Failure scenario**: a `Group` with three meshes in order `[A(material m1), B(material m2), C(material m2)]` where B and C are structurally mergeable. Expected output is 2 meshes (A, and B+C); the actual output is **3** — B and C are emitted separately. Reordering the same three children to `[B, C, A]` yields the correct 2. Verified by executing the function.
532
- - **Fix**: always `i--` after the splice loop (the element removed at index `i` is replaced by its successor), or restructure to repeatedly take `undecided[0]`.
533
- - **Confidence**: Confirmed (executed)
534
-
535
- ### GFXT-61 `merge_geometry_hierarchy` calls `getIndex().count` on possibly non-indexed geometry
536
- - **Severity**: MAJOR
537
- - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:168`
538
- - **Defect**: The instancing-size heuristic does `const tri_count = (geo_0.getIndex().count) / 3;` without checking for a non-indexed geometry; `BufferGeometry.getIndex()` returns `null` when `index` is unset.
539
- - **Failure scenario**: a `Group` with two `Mesh` nodes referencing the same non-indexed `BufferGeometry` — or two structurally identical non-indexed geometries, which `dedupe_geometry` collapses into one object earlier in the same function. `merge_geometry_hierarchy` throws `TypeError: Cannot read properties of null (reading 'count')`. Verified by executing the function.
540
- - **Fix**: `const tri_count = (geo_0.index !== null ? geo_0.index.count : geo_0.attributes.position.count) / 3;`
541
- - **Confidence**: Confirmed (executed)
542
-
543
- ### GFXT-62 `merge_contexts` wraps a `null` merge result in a `Mesh`
544
- - **Severity**: MAJOR
545
- - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:132`
546
- - **Defect**: The result of `mergeBufferGeometries` is assigned unconditionally. That function `console.error`s and returns `null` for several mismatches `equal_attribute_structure` does not screen — indexed vs non-indexed, `morphAttributes` key mismatch, `morphTargetsRelative` mismatch, interleaved attributes — and the null propagates into `new Mesh(null, material)`.
547
- - **Failure scenario**: a `Group` with two meshes sharing one material, both carrying only a Float32 `position` attribute of itemSize 3, one indexed and one not. `equal_attribute_structure` returns true (it compares only names, itemSize, normalized and array constructor), so the pair is merged; `mergeBufferGeometries` logs its "make sure index attribute exists among all geometries" error and returns `null`; `context_to_mesh` then throws `TypeError: Cannot read properties of null (reading 'isBufferGeometry')` inside the `Mesh` constructor. Verified by executing the function.
548
- - **Fix**: extend `equal_attribute_structure` to require `(a.index !== null) === (b.index !== null)`, matching `morphAttributes` keys and equal `morphTargetsRelative`; and in `merge_contexts`, fall back to returning the input contexts unmerged when `merged === null` rather than producing a null-geometry context.
549
- - **Confidence**: Confirmed (executed)
550
-
551
- ### GFXT-63 Merging bakes a mirrored transform without reversing winding
552
- - **Severity**: MAJOR
553
- - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:127`
554
- - **Defect**: `merge_contexts` bakes each child's world matrix into vertex positions via `applyMatrix4` but never reverses triangle winding for a negative-determinant (mirrored) transform. Unmerged, three.js compensates at draw time — `WebGLRenderer` sets `frontFaceCW` from `object.matrixWorld.determinant() < 0` — but the merged mesh has an identity, positive-determinant matrix, so that compensation no longer applies.
555
- - **Failure scenario**: a `Group` with two meshes sharing one geometry and material, the second with `scale.set(-1, 1, 1)`. After the merge the single output geometry's two triangles have measured signed areas `[+1, -1]`, i.e. the mirrored triangle is now clockwise. With three.js' default `side: FrontSide` the mirrored half is back-face culled and disappears; before the merge it rendered correctly.
556
- - **Fix**: compute the determinant of `ctx.matrix` in the `contexts.map` callback and, when negative, reverse each triangle's winding on the clone after `applyMatrix4` (swap index elements `[i*3+1]` and `[i*3+2]`, or the corresponding vertex records when non-indexed).
557
- - **Confidence**: Confirmed (measured signed areas)
558
-
559
- ### GFXT-64 `geometry_optimize_uv_tension` produces all-NaN UVs from a single collapsed edge
560
- - **Severity**: MAJOR
561
- - **File**: `engine/graphics/geometry/optimization/geometry_optimize_uv_tension.js:67`
562
- - **Defect**: The neighbour weight is `1 / edge.lengthSqr` with no zero-length handling. A single pair of coincident positions joined by an edge yields `Infinity` weights, and `Infinity / Infinity` gives `NaN` UVs, which then propagate across the whole mesh over the remaining smoothing passes.
563
- - **Failure scenario**: a closed octahedron — so there are no boundary edges and nothing is skipped by the `query_edge_is_boundary` guard — with vertex 0 moved onto pole vertex 4, collapsing edge (0,4). That is one degenerate triangle, a routine artifact of imported or stitched meshes. With `steps = 3`, **all 12 UV components come back `NaN`** (measured), destroying the texture coordinates of the entire mesh rather than just the degenerate corner.
564
- - **Fix**: skip any neighbour whose `edge.lengthSqr` is zero (contributing to neither `sum_*` nor `weight_sum`), and skip the vertex entirely when `weight_sum` ends up zero.
565
- - **Confidence**: Confirmed (executed: 12 of 12 UV values NaN)
566
-
567
- ### GFXT-65 MikkT edge table is a `Float32Array` holding packed integer indices
568
- - **Severity**: MINOR
569
- - **File**: `engine/graphics/geometry/MikkT/InitTriInfo.js:180` (consumed by `BuildNeighborsFast.js:25-26,73-74`)
570
- - **Defect**: `const pEdges = new Float32Array(iNrTrianglesIn * 3 * 3);` but channels 0 and 1 store `MakeIndex`-encoded integers (`(face << 2) | corner`), not floats. Float32 represents integers exactly only up to 2^24. The reference C uses `int` throughout.
571
- - **Failure scenario**: a mesh above 4,194,304 triangles — the point at which `(f << 2) | i` exceeds 2^24, e.g. a 5M-triangle scan. `MakeIndex(4500000, 3) = 18000003` is stored as `18000004`, colliding with `MakeIndex(4500001, 0)`. `GetEdge`'s `index_0 === i0_in` tests then match the wrong corner, `FaceNeighbors` links unrelated triangles, and tangent groups merge across disconnected surface regions — visibly wrong normal mapping on the affected faces.
572
- - **Fix**: allocate `pEdges` as an `Int32Array`, and likewise the `temp_edge_A`/`temp_edge_B` scratch in `BuildNeighborsFast.js:73-74`.
573
- - **Confidence**: Likely (arithmetic verified; not reproduced at that mesh size)
574
-
575
- ### GFXT-66 `m_getTexCoord` copies 3 components out of a 2-component UV buffer
576
- - **Severity**: MINOR
577
- - **File**: `engine/graphics/geometry/MikkT/m_getTexCoord.js:15`
578
- - **Defect**: `array_copy(pContext.geometry_buffer_vertex_uv, vertex_index * 2, fvTexcOut, 0, 3)` reads three elements from a buffer with a stride of two, so the third output component is the next vertex's U — or reads past the end for the last vertex.
579
- - **Failure scenario**: the in-module caller `GetTexCoord` immediately overwrites `res[2] = 1`, so the MikkT pipeline itself is unaffected. But `m_getTexCoord` is an exported public function: calling it directly fills `out[2]` with `uv[vertex_index * 2 + 2]` — the neighbouring vertex's U coordinate — and with `NaN` when `vertex_index` is the last vertex.
580
- - **Fix**: change the copy length from `3` to `2`.
581
- - **Confidence**: Confirmed
582
-
583
- ### GFXT-67 `merge_geometry_hierarchy` reads stale local matrices
584
- - **Severity**: MINOR
585
- - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:60`
586
- - **Defect**: `compute_transform_matrix` reads `el.matrix` directly and `merge_geometry_hierarchy` never calls `updateMatrixWorld`. Under three.js' default `matrixAutoUpdate`, `object.matrix` is synced from `position`/`quaternion`/`scale` only during a render pass, so a hierarchy assembled in code still has identity local matrices at merge time.
587
- - **Failure scenario**: `const m = new Mesh(geo, mat); m.position.set(100, 0, 0); group.add(m); merge_geometry_hierarchy(group)` puts the output mesh at `[0, 0, 0]` — the 100-unit offset is silently discarded (measured). Loader-produced scenes happen to work because `GLTFLoader` writes `matrix` itself.
588
- - **Fix**: call `input.updateMatrixWorld(true)` at the top of `merge_geometry_hierarchy`, before the `traverse`.
589
- - **Confidence**: Confirmed (executed)
590
-
591
- ---
592
-
593
- ## geometry/ribbon, geometry/instancing, texture/reflection, texture/cubemap
594
-
595
- ### GFXT-68 `Ribbon.positionHead` places the two tip vertices on opposite sides in its two branches
596
- - **Severity**: CRITICAL
597
- - **File**: `engine/graphics/geometry/ribbon/Ribbon.js:143`
598
- - **Defect**: The degenerate branch derives `C` from `A` and `D` from `B`, preserving each vertex's side of the ribbon. The normal branch derives the width axis as `vRelativePosition × normal`, which for the ribbon's own geometry normal `(0,0,1)` points toward `B`'s side, so `C` and `D` come out swapped and the head quad is twisted into a bowtie. The two branches contradict each other regardless of the caller's sign convention for `normal`.
599
- - **Failure scenario**: `new Ribbon(2, 1)` gives head `quads[1]` with `A = (0, 0.5, 0)`, `B = (0, -0.5, 0)`, and `PlaneBufferGeometry` normals `(0,0,1)`. `positionHead(new Vector3(2,0,0), new Vector3(0,0,1), 1)` gives `vCross = (2,0,0) × (0,0,1) = (0,-2,0)` → `C = (2,-0.5,0)` (B's side) and `D = (2,0.5,0)` (A's side). `positionHead(new Vector3(0,0,0), new Vector3(0,0,1), 1)` takes the degenerate path and gives `C = (0,0.5,0)` (A's side), `D = (0,-0.5,0)` (B's side) — the same object and the same `normal`, opposite assignment. On the first (common) path the `A→C` and `B→D` edges cross, so triangle `(A,B,C)` stays CCW while `(B,D,C)` flips to CW: with a default `FrontSide` material half the head segment vanishes and the ribbon visibly pinches and flips at the tip every frame.
600
- - **Fix**: negate the cross so it agrees with the degenerate branch (which is unambiguously correct, since it reuses the existing A/B directions): `vCross.copy(normal).cross(vRelativePosition);`, keeping `vC = +vCross`, `vD = -vCross`.
601
- - **Confidence**: Confirmed
602
-
603
- ### GFXT-69 `.exr` environment maps never load — `EXRLoader` cannot produce `UnsignedByteType`
604
- - **Severity**: CRITICAL
605
- - **File**: `engine/graphics/texture/cubemap/load_environment_map.js:57`
606
- - **Defect**: `new EXRLoader().setDataType(UnsignedByteType)` requests an output type three r136's `EXRLoader` does not implement. Its `EXRDecoder` starts with `getter: null, inputSize: null`, both `switch (outputType)` blocks that assign them have only `case FloatType` / `case HalfFloatType`, and the buffer-allocation switch falls through to `default: console.error('THREE.EXRLoader: unsupported type: ', outputType); break;` without returning (verified at `node_modules/three/examples/jsm/loaders/EXRLoader.js:2128,2133,2145,2150,2175,2184,2193`).
607
- - **Failure scenario**: `await load_environment_map({ path: 'data/textures/env/sky.exr', renderer })` — `setupDecoder` leaves `getter = null` and `byteArray = undefined`, then `parse` reaches `EXRDecoder.byteArray[outIndex] = EXRDecoder.getter(viewer, tmpOffset)` and throws `TypeError: EXRDecoder.getter is not a function`. `FileLoader` routes that to `onError`, so the promise rejects and no environment map is installed — for every `.exr` input, on every call.
608
- - **Fix**: `.setDataType(HalfFloatType)` (or `FloatType`), imported from `three` in place of `UnsignedByteType`.
609
- - **Confidence**: Confirmed
610
-
611
- ### GFXT-70 `Ribbon.traverseEdges` seeds from `quads[0]` instead of the tail
612
- - **Severity**: MAJOR
613
- - **File**: `engine/graphics/geometry/ribbon/Ribbon.js:174`
614
- - **Defect**: The traversal seeds from `quads[0]` — the *array* slot — instead of `this.__tail`. `rotate()` deliberately does not reorder `this.quads`; it only relinks `next`/`previous` and moves `__head`/`__tail`. After any rotation the seed edge is an interior edge, which the loop then visits a second time, while the real tail edge is never visited.
615
- - **Failure scenario**: `const r = createRibbon(3); rotateRibbon(r);` leaves `__tail = quads[1]`, `__head = quads[0]`, `quads[0].previous = quads[2]`. The initialisation `createRibbon` itself performs — `r.traverseEdges((a,b) => { off[a] = 1; off[b] = -1; })` — then visits vertex pairs `(3,7)`, `(0,4)`, `(3,7)`, `(2,6)`. Vertices 1 and 5, the true tail edge, are never touched, so their `off` attribute stays 0 and the shader collapses the ribbon to zero width at the tail segment, while `(3,7)` is written twice.
616
- - **Fix**: `let q = this.__tail;`
617
- - **Confidence**: Confirmed
618
-
619
- ### GFXT-71 `Ribbon.traverseEdges` emits index 0 twice, never the last index, and runs the ramp backwards
620
- - **Severity**: MAJOR
621
- - **File**: `engine/graphics/geometry/ribbon/Ribbon.js:181`
622
- - **Defect**: The seed callback passes index `0`, and the loop then starts its own counter at `0` as well, so index 0 is delivered twice and the final index is never delivered. `maxIndex` is passed as `numQuads + 1` while the loop only reaches `numQuads - 1`. The loop also walks head → tail with `i` ascending while the seed call — the tail edge — is also given `0`, so the parameter runs backwards relative to edge order.
623
- - **Failure scenario**: `new Ribbon(3, 1).traverseLerpEdges(0, 1, cb)` computes `value = 0 + (index / maxIndex) * 1` and yields `0, 0, 0.25, 0.5` for the four edges. The intended `0 → 1` ramp never reaches 1, is clipped to `(N-1)/(N+1)`, and is inverted — the head edge receives 0 while the edge nearest the tail receives the largest value. For `N = 64` the head gets 0 and the maximum is 0.969, so a trail fade runs the wrong way and never reaches full opacity.
624
- - **Fix**: use a single monotonic counter over `numQuads + 1` edges in tail → head order with `maxIndex = numQuads`, so `index / maxIndex` spans `[0, 1]`.
625
- - **Confidence**: Confirmed
626
-
627
- ### GFXT-72 `Ribbon.moveToPoint` does not flag the position attribute for re-upload
628
- - **Severity**: MAJOR
629
- - **File**: `engine/graphics/geometry/ribbon/Ribbon.js:97`
630
- - **Defect**: The method writes the raw position array but never sets `position.needsUpdate = true`. Every other position write in the module (`Quad.setAttributePosition`) does set it, so this is an omission rather than a module convention.
631
- - **Failure scenario**: `const r = createRibbon(16); scene.add(new Mesh(r.geometry, mat)); renderer.render(...)` uploads the positions and caches `attribute.version` in `WebGLAttributes`. Respawning the trail at a new location with `r.moveToPoint(new Vector3(50, 0, 0)); renderer.render(...)` then hits `data.version === attribute.version` in `WebGLAttributes.update`, which skips the upload — the frame still draws the full 16-segment trail at the old location instead of a collapsed ribbon at `(50,0,0)`.
632
- - **Fix**: add `position.needsUpdate = true;` after the loop.
633
- - **Confidence**: Confirmed
634
-
635
- ### GFXT-73 First-time colour attribute creation leaves existing instances fully transparent
636
- - **Severity**: MINOR
637
- - **File**: `engine/graphics/geometry/instancing/InstancedMeshGroup.js:575`
638
- - **Defect**: When the colour attribute is created for the first time on a group that already has live instances, only `[count, capacity)` is filled with `255` — slots `[0, count)` stay at the `Uint8Array` default of `0`, because there is no old array to copy from. (Distinct from the known stale-colour-on-regrow issue: here the colours were never written at all.)
639
- - **Failure scenario**: build a group with `use_color = false`, add 50 instances with transforms, then set `use_color = true` and `setCapacity(128)` to trigger the first build with colours. Slots 0..49 hold `(0,0,0,0)`; `rewriteMaterial` defines `USE_COLOR_ALPHA` so `diffuseColor *= vColor` with alpha 0 — all 50 live instances render fully transparent while instances added afterwards render correctly.
640
- - **Fix**: fill the whole array when there was no previous one — `newColorArray.fill(255, this.__attributeColor === null ? 0 : this.count * 4)`.
641
- - **Confidence**: Confirmed
642
-
643
- ### GFXT-74 Reflection convolution takes its channel count from the input, not the output
644
- - **Severity**: MINOR
645
- - **File**: `engine/graphics/texture/reflection/convolve_equirectangular_reflection.js:56` (caller at `:118`)
646
- - **Defect**: `convolve_equirectangular_reflection_direction` decides how many channels to write into `result` from `input.itemSize`, but the caller sizes `result` from `output.itemSize` (`const texel = new Float32Array(output.itemSize);`). When they differ, the trailing channels of every output texel are left untouched.
647
- - **Failure scenario**: `convolve_equirectangular_reflection_level(rgbSky /* itemSize 3 */, rgbaMip /* itemSize 4 */, 0.3, 32)` allocates a fresh `Float32Array(4)` per texel whose element 3 is never assigned, so `output.write` stores alpha 0 for every texel and the prefiltered mip comes out fully transparent.
648
- - **Fix**: take the channel count from the destination — pass an explicit `channel_count`, or use `result.length - result_offset` — and read `_radiance[3]` only when the input actually has a fourth channel.
649
- - **Confidence**: Confirmed
650
-
651
- ### GFXT-75 Cubemap face-array branch compares the file extension case-sensitively
652
- - **Severity**: MINOR
653
- - **File**: `engine/graphics/texture/cubemap/load_environment_map.js:104`
654
- - **Defect**: The six-face array branch does `const ext = computeFileExtension(path[0]); if (ext === 'hdr')`, while the single-path branch at line 116 lowercases first. An uppercase face set silently takes the LDR `CubeTextureLoader` path.
655
- - **Failure scenario**: `load_and_set_cubemap_v0(graphics, 'data/textures/cubemaps/sky/', '.HDR')` has `buildCubeURLs` yield `.../posx.HDR`; `ext === 'HDR'` fails the check, so `CubeTextureLoader` (`ImageLoader`) is used, the browser cannot decode a Radiance file as an image, and the promise rejects instead of loading the HDR cubemap.
656
- - **Fix**: `const ext = computeFileExtension(path[0]).toLowerCase();`
657
- - **Confidence**: Confirmed
658
-
659
- ### GFXT-76 `geometry_copy` drops `drawRange`
660
- - **Severity**: MINOR
661
- - **File**: `engine/graphics/geometry/instancing/geometry_copy.js:6`
662
- - **Defect**: The function copies `index` and `attributes` but not `drawRange`, so the target reverts to the `{start: 0, count: Infinity}` default and renders the source's entire index buffer. (`groups` and `morphAttributes` are dropped too, but with a single non-array material three ignores `groups`, so only `drawRange` produces wrong output.)
663
- - **Failure scenario**: a pooled geometry that keeps a fixed-size buffer and publishes its live element count via `setDrawRange` — the pattern at `particles/particular/engine/emitter/ParticlePool.js:158`, `geometry.setDrawRange(0, occupancy.size())` — passed to `InstancedMeshGroup.setGeometry` or `geometry_copy` with 120 live of 10000 allocated elements renders all 10000, drawing 9880 stale/uninitialised quads.
664
- - **Fix**: also copy `target.setDrawRange(source.drawRange.start, source.drawRange.count)`, plus `morphAttributes` and `morphTargetsRelative`.
665
- - **Confidence**: Confirmed
666
-
667
- ---
668
-
669
- ## Status of the pre-existing "already known" list
670
-
671
- | Item | Status |
672
- | --- | --- |
673
- | `sampler2d_scale_down_lanczos` `rcp_ratio_y = 2/ratio_x` | FIXED — now `2 / ratio_y` (`:93`) |
674
- | `sampler2d_scale_down_linear` non-integer-ratio mis-index | OPEN — `sampleOffsetX = x * sW` is still used as a texel index, and `for (i = sW; i > 0; i--)` still over-counts (`:50-58`) |
675
- | `sampler2d_copy_with_margins` width used for a vertical bound | FIXED — all vertical bounds now use `destination.height`. A different, still-open bug in the same file is filed as GFXT-30 |
676
- | `ParticleGroup.deleteIndices` bare `.sort()` | FIXED — now `indices.sort((a, b) => a - b)`. A different, still-open bug in the same function is filed as GFXT-51 |
677
- | `ParticlePool` `updateRange.count` missing `* itemSize` | FIXED — both sites multiply (`:177`, `:507`) |
678
- | `SimulationStepCurlNoiseVelocity` shared `this.time` | OPEN — still `this.time += this.timeDelta` on the shared step object |
679
- | `ManagedTextureAtlas` serving atlas-evicted patches from a TTL-∞ cache | OPEN — `LoadingCache` is still constructed with no TTL |
680
- | `CachingTextureAtlas.__evict_all` leaving index 0 | FIXED — loop runs `i >= 0` |
681
- | `ParticleEmitter` hash/equals flag disagreement | FIXED — both now mask with `SERIALIZABLE_FLAGS`. A different issue with that mask is filed as GFXT-55 |
682
- | `ShaderManager` graveyard leak | FIXED — `cleanup()` evicts from both maps and releases the material reference |
683
- | `equal_attribute_structure` returning truthy `-1` | FIXED — now `indexOf(...) === -1` guarded. Other still-open bugs in that function are filed as GFXT-62 |
684
- | `InstancedMeshGroup.build` stale per-instance colours on shrink+regrow | FIXED — `newColorArray.fill(255, this.count * 4)`. A different first-build case is filed as GFXT-73 |
685
- | MikkT `BuildNeighborsFast` edge-neighbour pairing | OPEN — `i1_B` and `edgenum_B` both read `temp_edge_B[2]` (`:117-118`) |
686
- | MikkT `GenerateSharedVerticesIndexList` radix precedence | OPEN — `pass * 257 + (_comp_0[i] >> shift) & 0xff` at `:79`, versus the correctly parenthesised `:59` |
687
- | `bvh32_geometry_nearest` duplicated denormalize block | FIXED — one block remains |
688
- | `ensureGeometryBoundingSphere` returning stale null | FIXED — assigns to `geometry.boundingSphere` and returns it |
689
- | `AtlasLookupTexture` writing `size.y` into the width channel | FIXED — writes `position.x, position.y, size.x, size.y` |
690
-
1
+ # Correctness review — engine/graphics/{texture, particles/particular, geometry}
2
+
3
+ Scope: every non-spec, non-`prototype*` source file under
4
+ `engine/graphics/texture/`, `engine/graphics/particles/particular/`, `engine/graphics/geometry/`.
5
+ (The brief named `graphics/particular/`; the actual path is `graphics/particles/particular/`.)
6
+
7
+ Rules applied: correctness only; every finding carries a concrete failure scenario; `assert.*`
8
+ is treated as compiled out; no defensive-guard requests; unreferenced exports treated as public
9
+ surface.
10
+
11
+ **Executed verification.** GFXT-1, -2, -3, -4, -5, -6/-7, -17, -24, -40 and -42 were confirmed by
12
+ running jest against the real classes (11/11 assertions pass). GFXT-51, -52, -53, -54 (ParticleGroup),
13
+ GFXT-60, -61, -62, -63, -64, -67 (merge / uv-tension) and GFXT-36 (barycentric sampling) were
14
+ likewise reproduced by execution during review. The remainder are confirmed by inspection; the two
15
+ findings resting on unexecuted GL or scale-dependent behaviour (GFXT-29, GFXT-65) and the one
16
+ resting on a transform-convention argument (GFXT-46) are marked `Likely`.
17
+
18
+ **Note on stray files.** Two untracked scratch specs —
19
+ `src/engine/graphics/texture/sampler/__advcheck_gfxt.spec.js` and `__advcheck_gfxt2.spec.js` —
20
+ were present in the source tree at the end of this review. They are the verification specs whose
21
+ results are cited above. They were not written by this review pass and were left in place rather
22
+ than deleted, in case a concurrent session still owns them; they are scratch files and should be
23
+ removed before committing. Sibling `__advcheck_*.spec.js` files also exist under `src/core/collection/`
24
+ and `src/engine/`, along with `src/REVIEW_2026_08_06.md`. No source file in the reviewed tree was
25
+ modified by this review.
26
+
27
+ ---
28
+
29
+ ## texture/sampler — pixel arithmetic, filters, conversion
30
+
31
+ ### GFXT-1 `sampler2d_paint` reads the source X coordinate from `sourceY`
32
+ - **Severity**: CRITICAL
33
+ - **File**: `engine/graphics/texture/sampler/sampler2d_paint.js:68`
34
+ - **Defect**: The horizontal source coordinate is built from `sourceY` instead of `sourceX`, so every blend reads from the wrong column of the source image.
35
+ - **Failure scenario**: `sampler2d_paint(dst, brushAtlas, /*sourceX*/ 64, /*sourceY*/ 0, 0, 0, 32, 32)` — intended to stamp the brush at atlas column 64 — computes `s_x = x + 0`, so it stamps atlas columns 0..31 instead. With `sourceX = 0, sourceY = 8` the stamp is shifted 8 px right and reads 8 px past the intended patch on the last columns.
36
+ - **Fix**: `const s_x = Math.round(x + sourceX);`
37
+ - **Confidence**: Confirmed
38
+
39
+ ### GFXT-2 `bitSet2Sampler2D` calls a method `Sampler2D` does not have
40
+ - **Severity**: CRITICAL
41
+ - **File**: `engine/graphics/texture/sampler/util/bitSet2Sampler2D.js:35,37`
42
+ - **Defect**: The function writes texels via `target.set(x, y, value)`. `Sampler2D` exposes `write(x, y, texel)`; there is no `set` method on the class or its prototype.
43
+ - **Failure scenario**: `bitSet2Sampler2D(bits, Sampler2D.uint8(4, 8, 8), [255,0,0,255], [0,0,0,255])` throws `TypeError: target.set is not a function` on the first texel. The function can never have worked.
44
+ - **Fix**: `target.write(x, y, vTrue)` / `target.write(x, y, vFalse)`.
45
+ - **Confidence**: Confirmed
46
+
47
+ ### GFXT-3 Signed chamfer SDF keeps a stale `v`, so it stores the last improving candidate, not the minimum
48
+ - **Severity**: CRITICAL
49
+ - **File**: `engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js:72-96` (forward pass), `:103-127` (backward pass)
50
+ - **Defect**: Both relaxation passes read `const v = getD(x, y)` once and then compare each of the four candidates against that same stale `v`, calling `setD` on every candidate that beats it. The stored value ends up being the *last* candidate below the original value rather than `min(v, v0, v1, v2, v3)`. The sibling `computeUnsignedDistanceField_Chamfer` does this correctly (it reassigns `v = v0` etc. after each improvement).
51
+ - **Failure scenario**: pixel with `D = 255` whose forward-pass neighbours hold `D(x-1,y-1) = 0`, `D(x,y-1) = 100`, `D(x+1,y-1) = 200`, `D(x-1,y) = 50`, with `d1 = 1`, `d2 = 1.414`. Candidates are `1.414, 101, 201.414, 51`; all four are `< 255`, so the cell is written four times and ends at `51`. Correct answer is `1.414`. The error then propagates to every downstream cell in both passes.
52
+ - **Fix**: after each `setD(x, y, vN)` also assign `v = vN` (mirror the unsigned version), or compute `min2` of all five values and write once.
53
+ - **Confidence**: Confirmed
54
+
55
+ ### GFXT-4 `convertTexture2Sampler2D` flips the source DataTexture's own pixel buffer in place
56
+ - **Severity**: CRITICAL
57
+ - **File**: `engine/graphics/texture/sampler/convertTexture2Sampler2D.js:130-132` and `:163-165` (via `data_texture_to_sampler` at `:25-38`, `sampler2d_ensure_uint8_RGBA.js:14`)
58
+ - **Defect**: For a `DataTexture`, `data_texture_to_sampler` wraps `texture.image.data` in a `Sampler2D`; `sampler2d_ensure_uint8_RGBA` returns that same object unchanged when the texture is already 4-channel uint8, and the dimension check `converted.width !== _width` never fires because `_width/_height` default to the image's own dimensions. `data` is therefore the live texture buffer, and `sampler2d_flipY_in_place(data, …)` mutates it.
59
+ - **Failure scenario**: `convertTexture2Sampler2D(someRGBA8DataTexture)` (no width/height override, default `flipY = true`) vertically flips the pixels of the *input* texture. Anything still rendering that texture is now upside-down; calling the function a second time flips it back, so the corruption alternates per call.
60
+ - **Fix**: copy before flipping on the shared path — e.g. have `data_texture_to_sampler` clone when `converted === source`, or flip into a freshly allocated buffer.
61
+ - **Confidence**: Confirmed
62
+
63
+ ### GFXT-5 `sampler2d_combine` reads input1 with input0's channel count
64
+ - **Severity**: MAJOR
65
+ - **File**: `engine/graphics/texture/sampler/sampler2d_combine.js:52`
66
+ - **Defect**: The loop that fills `arg1` from `input1` is bounded by `itemSize0` instead of `itemSize1`, so when the two inputs have different channel counts the second operand is over- or under-read.
67
+ - **Failure scenario**: `input0` RGBA (`itemSize 4`), `input1` a 1-channel mask, 4×4 image. For texel `i = 0` the code reads `data1[0..3]`, i.e. mask texels 0,1,2,3, and hands `operation` an `arg1` of `[m0, m1, m2, m3]` where `[m0]` was intended. Every masked texel gets the wrong mask value, and the stale tail of `arg1` persists for the remaining texels.
68
+ - **Fix**: bound that loop with `itemSize1`, and clear/resize `arg1` per iteration if `itemSize1` can shrink.
69
+ - **Confidence**: Confirmed
70
+
71
+ ### GFXT-6 `sampler2d_compute_texel_value_conversion_scale_to_uint8` accumulates `max` from `min`
72
+ - **Severity**: MAJOR
73
+ - **File**: `engine/graphics/texture/sampler/sampler2d_compute_texel_value_conversion_scale_to_uint8.js:44`
74
+ - **Defect**: `max = max2(min, sampler2d_channel_compute_max(sampler, i).value)` uses `min` as the running accumulator, so the per-channel maxima of all but the last channel are discarded. The final `max` is just the last channel's maximum.
75
+ - **Failure scenario**: a `Float32` 2-channel sampler where channel 0 spans `[0, 1000]` and channel 1 spans `[0, 5]`. Iteration 0 sets `max = 1000`; iteration 1 overwrites it with `max2(0, 5) = 5`. `span = 5`, `scale = 51`. Every channel-0 value above 5 maps past 255 and saturates — the channel comes out uniformly white through `sampler2d_to_data_url`.
76
+ - **Fix**: `max = max2(max, sampler2d_channel_compute_max(sampler, i).value)`.
77
+ - **Confidence**: Confirmed
78
+
79
+ ### GFXT-7 `sampler2d_to_html_canvas` adds the offset that its producer supplies as a value to subtract
80
+ - **Severity**: MAJOR
81
+ - **File**: `engine/graphics/texture/sampler/sampler2d_to_html_canvas.js:61,81` (producer: `sampler2d_compute_texel_value_conversion_scale_to_uint8.js:51`, composed in `sampler2d_to_data_url.js:16-18`)
82
+ - **Defect**: The canvas writer computes `(source_value + offset) * scale`, but the offset it is fed is `offset = min` and the scale is `255 / (max - min)`. Normalising to `[0,255]` requires `(v - min) * scale`. The two disagree in sign, so the mapping is only correct when `min === 0`.
83
+ - **Failure scenario**: `sampler2d_to_data_url(s)` on a `Float32` sampler holding `[10, 11, 12, 13]` (1 channel, 2×2). `min = 10`, `scale = 85`, `offset = 10`. Texel 0 becomes `(10 + 10) * 85 = 1700`, clamped to 255; every other texel likewise. The PNG is uniformly white instead of a four-step gradient. A sampler spanning `[-1, 1]` comes out uniformly black.
84
+ - **Fix**: return `offset: -min` from the producer (keeping the canvas writer's additive convention), or change the canvas writer to `(source_value - offset) * scale`. Pick one convention and apply it in both places.
85
+ - **Confidence**: Confirmed
86
+
87
+ ### GFXT-8 `saturated_value_by_constructor` has no `Uint8ClampedArray` arm, so RGB→RGBA fills alpha with 1
88
+ - **Severity**: MAJOR
89
+ - **File**: `engine/graphics/texture/sampler/saturated_value_by_constructor.js:8-26` (consumer: `sampler2d_transfer_data.js:41,52`)
90
+ - **Defect**: The switch covers `Uint8Array` (255) but not `Uint8ClampedArray`, which is a distinct constructor and is not an `instanceof Uint8Array`. It falls through to `default: return 1`. `Sampler2D.uint8clamped()` is a first-class factory on the class and `ImageData.data` is always a `Uint8ClampedArray`.
91
+ - **Failure scenario**: `sampler2d_transfer_data(rgbSampler, Sampler2D.uint8clamped(4, 64, 64))` fills the alpha channel with `1` instead of `255`. Drawn to a canvas or uploaded as an RGBA texture, the image is ~0.4% opaque — effectively invisible.
92
+ - **Fix**: add `case Uint8ClampedArray: return 255;` (and, for completeness, `Float16Array` alongside the float arms).
93
+ - **Confidence**: Confirmed
94
+
95
+ ### GFXT-9 `sampler2d_to_uint8_RGBA` treats Float16 and plain-array samplers as already-uint8
96
+ - **Severity**: MAJOR
97
+ - **File**: `engine/graphics/texture/sampler/sampler2d_to_uint8_RGBA.js:7-32,60-64`
98
+ - **Defect**: `compute_gradient_and_intercept_uint8` has arms for `Float32Array`/`Float64Array` (gradient 255) but none for `Float16Array` or a plain `Array`, both of which are supported `Sampler2D` storages (`Sampler2D.float16()`, `sampler2d_to_f16.js`, and the array branches in `clone`/`hash`/`computeByteSize`). They fall through to `default` — gradient 1, intercept 0 — which also satisfies the fast-path test on line 60.
99
+ - **Failure scenario**: `sampler2d_to_uint8_RGBA(Sampler2D.uint8(4,w,h), someFloat16RGBASampler)` with values in `[0,1]` takes the `output.data.set(input.data)` shortcut and writes 0 or 1 into each uint8 channel. A white image (1.0) becomes value 1 — effectively black. The same happens on the `VirtualTextureTileLoader.js:242` tile path for any non-uint8 tile source.
100
+ - **Fix**: add `case Float16Array:` next to the float arms (gradient 255), and derive the plain-array case from `sampler2d_compute_texel_value_conversion_scale_to_uint8`'s convention (it already treats a plain numeric array as Float32).
101
+ - **Confidence**: Confirmed
102
+
103
+ ### GFXT-10 `Sampler2DSerializationAdapter` rejects `Uint8ClampedArray`-backed samplers
104
+ - **Severity**: MAJOR
105
+ - **File**: `engine/graphics/texture/sampler/serialization/Sampler2DSerializationAdapter.js:26`
106
+ - **Defect**: `if (value.data instanceof Uint8Array)` is false for a `Uint8ClampedArray`; the adapter then throws `TypeError: Unsupported data type`.
107
+ - **Failure scenario**: serialising any sampler produced by `Sampler2D.uint8clamped(...)`, `sampler2d_to_html_canvas` (line 31), `canvasDataToSampler`, or `html_canvas_to_sampler2d` (all `Uint8ClampedArray`) throws instead of writing the 8-bit path it clearly intends to cover. Deserialisation always produces a `Uint8Array`, so a clamped sampler cannot round-trip at all.
108
+ - **Fix**: test `value.data instanceof Uint8Array || value.data instanceof Uint8ClampedArray` (or use `compute_binary_data_type_from_typed_array`), and record which of the two to restore.
109
+ - **Confidence**: Confirmed
110
+
111
+ ### GFXT-11 Binary/JSON serialisation of a `Sampler2D` ignores the view's `byteOffset`
112
+ - **Severity**: MAJOR
113
+ - **File**: `engine/graphics/texture/sampler/serialization/TextureBinaryBufferSerializer.js:102,114`; same defect in `Sampler2D.js:942` (`toJSON`)
114
+ - **Defect**: `serializeTexture` writes `new Uint8Array(array.buffer)` starting at byte 0 of the *underlying ArrayBuffer*, ignoring `array.byteOffset`; `toJSON` base64-encodes `this.data.buffer` wholesale for the same reason. When the sampler's data is a subarray view, the wrong bytes are serialised.
115
+ - **Failure scenario**: `SplatMapping.js:307` builds per-layer samplers as `new Sampler2D(this.weightData.subarray(startAddress, endAddress), 1, width, height)`. Serialising layer 2 of a 4-layer 256×256 splat map writes `width*height*1` bytes from offset 0 — i.e. layer 0's weights — and deserialising reproduces layer 0's data under layer 2's identity. `toJSON` additionally throws on an `Array`-backed sampler, since a plain array has no `.buffer`.
116
+ - **Fix**: `buffer.writeBytes(new Uint8Array(array.buffer, array.byteOffset, byteSize), 0, byteSize)`; in `toJSON`, encode `this.data.buffer.slice(byteOffset, byteOffset + byteLength)` (and handle plain arrays explicitly).
117
+ - **Confidence**: Confirmed
118
+
119
+ ### GFXT-12 `Sampler2D.resize` crashes on an array-backed sampler
120
+ - **Severity**: MAJOR
121
+ - **File**: `engine/graphics/texture/sampler/Sampler2D.js:801-829`
122
+ - **Defect**: `resize` obtains a constructor via `typedArrayConstructorByInstance` (which returns `Array` for a plain array), allocates `new Array(length)` — a sparse array of holes — and in the width-preserving fast path calls `oldData.subarray(...)`, which plain arrays do not have. Plain `number[]` storage is explicitly supported everywhere else in the class (constructor default `data = []`, the `Array.isArray` branches in `clone`, `computeByteSize`, and `hash`).
123
+ - **Failure scenario**: `new Sampler2D([1,2,3,4], 1, 2, 2).resize(2, 4)` throws `TypeError: oldData.subarray is not a function`. Taking the other branch, `new Sampler2D([1,2,3,4], 1, 2, 2).resize(3, 3)` yields a sparse 9-slot array; `readChannel(2, 0, 0)` returns `undefined` instead of 0, and any arithmetic on it yields `NaN`.
124
+ - **Fix**: branch on `Array.isArray(oldData)` — allocate `new Array(length).fill(0)` and use `slice`/element copy instead of `subarray`.
125
+ - **Confidence**: Confirmed
126
+
127
+ ### GFXT-13 `sampler2d_copy_rectangle` samples at pixel corners, so an identity copy blurs and shifts
128
+ - **Severity**: MAJOR
129
+ - **File**: `engine/graphics/texture/sampler/sampler2d_copy_rectangle.js:28,32`
130
+ - **Defect**: The source UV for output row `y` is `y * (v_span / target_height) + v0`, i.e. the *corner* of the output pixel. `sampleBilinearUV` then converts UV to texel space with `v * height - 0.5`, so the net sample position is half a destination pixel above/left of the pixel centre. The `+ 0.5` destination-pixel-centre term is missing.
131
+ - **Failure scenario**: `sampler2d_copy_rectangle(src, dst, 0, 0, 1, 1)` with `src` and `dst` both 4×4 (a straight full-rect copy) produces rows `[t0, (t0+t1)/2, (t1+t2)/2, (t2+t3)/2]` per column instead of `[t0, t1, t2, t3]`. The copy is blurred and shifted half a texel up-left, and the last row/column of the source is never fully reached.
132
+ - **Fix**: `const source_v = (y + 0.5) * v_multiplier + v0;` and `const source_u = (x + 0.5) * u_multiplier + u0;`.
133
+ - **Confidence**: Confirmed
134
+
135
+ ### GFXT-14 `sampler2d_scale_down_mipmap` builds its mip chain from the width only
136
+ - **Severity**: MAJOR
137
+ - **File**: `engine/graphics/texture/sampler/resize/sampler2d_scale_down_mipmap.js:20`
138
+ - **Defect**: `while (current_mip.width > dest_width && current_mip.width > 1)` never consults the height. Each iteration halves *both* dimensions, so for a non-square source (or an aspect-changing resize) the chain stops based on X alone and Y is either over- or under-reduced.
139
+ - **Failure scenario**: source 256×64, destination 32×32. The loop runs three times (256→128→64→32), producing mips 128×32, 64×16, 32×8; `mip_mix = inverseLerp(64, 32, 32) = 1`, so the output is taken entirely from the 32×8 mip and then stretched back to 32 rows. Four vertical levels of detail are destroyed. In the reverse case — source 256×256, destination 256×8 — the loop body never executes, `inverseLerp(256, 256, 256)` returns 0 (its documented `range === 0` fallback), and the result is a raw bilinear resample of the full-resolution source with no prefiltering at all, so the vertical downscale aliases badly.
140
+ - **Fix**: drive the loop on both axes (`current_mip.width > dest_width || current_mip.height > dest_height`, clamping each halving at 1) and compute `mip_mix` from whichever axis is actually reducing.
141
+ - **Confidence**: Confirmed
142
+
143
+ ### GFXT-15 `sampler2d_scale_down_lanczos` centres its kernel on a texel corner, not a texel centre
144
+ - **Severity**: MAJOR
145
+ - **File**: `engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js:127,134`
146
+ - **Defect**: `center_x` is computed in continuous source space as `(u + 0.5) * ratio_x`, where source texel `i` has its centre at `i + 0.5`. The tap distance must therefore be `(i + 0.5) - center_x`, but the code uses `f_x = i - center_x` (likewise `f_y`). The near-identical `filter/sampler2d_scale_down_generic.js:63,70` gets this right (`(j - center_y) + 0.5`), which confirms the intent.
147
+ - **Failure scenario**: 512×512 → 256×256 (`ratio = 2`). For output pixel `u = 0`, `center_x = 1.0`. The correct tap distances to source texels 0 and 1 are `-0.5` and `+0.5` (symmetric); the code produces `-1.0` and `0.0`, centring the kernel on texel 1. Every output pixel is biased toward the higher-index source texel, so the whole downscaled image is shifted half a source texel up and left relative to a correct Lanczos resample.
148
+ - **Fix**: `const f_y = (j - center_y) + 0.5;` and `const f_x = (i - center_x) + 0.5;`, matching the generic downsampler.
149
+ - **Confidence**: Confirmed
150
+
151
+ ### GFXT-16 Downsamplers discard negative filter lobes, silently substituting a different filter
152
+ - **Severity**: MAJOR
153
+ - **File**: `engine/graphics/texture/sampler/filter/sampler2d_scale_down_generic.js:80-82`; same code at `resize/sampler2d_scale_down_lanczos.js:144-146`
154
+ - **Defect**: `if (weight <= 0) { continue; }` skips the tap entirely and excludes it from the normalising sum `z`. That is harmless for strictly non-negative kernels (`box`, `triangle`, `gaussian`, `kaiser_1`) but silently truncates every windowed-sinc / BC-spline kernel to its positive lobes only.
155
+ - **Failure scenario**: `sampler2d_scale_down_generic(src, dst, filter_lanczos3, 3)` on a 512×512 → 128×128 downscale. `filter_lanczos3(1.5) = -0.135` and every tap with `|x| ∈ (1,2) ∪ (2,3)` is dropped, so the effective kernel is a positive-lobes-only blob normalised over its own positive weights. The result is a soft, Gaussian-like downscale with none of Lanczos-3's edge acutance — the caller asked for Lanczos and received something else. `mitchell` (which dips to about `-0.035` around `x = 1.5`) is affected the same way, as is the built-in `lanczos` with `lobes = 2`.
156
+ - **Fix**: only skip taps that are exactly zero (`if (weight === 0) continue;`) and accumulate negative weights into `z` normally.
157
+ - **Confidence**: Confirmed
158
+
159
+ ### GFXT-17 `cubic2` evaluates the wrong polynomial in both branches
160
+ - **Severity**: MAJOR
161
+ - **File**: `engine/graphics/texture/sampler/filter/cubic2.js:17,21,25,29`
162
+ - **Defect**: Three separate errors. (a) Each term is written `Math.pow(coefficient * Math.abs(x), n)` — the coefficient is raised to the power along with `|x|` instead of multiplying `|x|^n`. (b) The two branches are swapped: the `|x| < 1` arm evaluates the `1 ≤ |x| < 2` polynomial and vice versa. (c) The `/6` normalisation is applied to only one term in one branch and omitted entirely in another. Compare `mitchell.js`, which implements the same BC-spline family correctly.
163
+ - **Failure scenario**: with `b = 0, c = 1`, `cubic2(0)` must be `(6 - 2b)/6 = 1`; it returns `pow(0,3) + pow(0,2) + 0 + 24 = 24`. `cubic2(1.5)` must be `-0.125`; it returns `pow(9,3) + pow(-18,2) + 6 = 1059`. Used as a weight function the filter produces weights three to four orders of magnitude off and of the wrong shape. Separately, `cubic2` never sets a `.support` property, so `sampler2d_scale_down_generic(input, output, cubic2)` (whose `support` parameter defaults to `weight_computer.support`) computes `range2_x = Math.ceil(ratio_x * undefined / 2) = NaN`, iterates zero taps, and divides by `z = 0` — every output texel becomes `NaN`.
164
+ - **Fix**: rewrite as `(A*|x|**3 + B*|x|**2 + C*|x| + D) / 6` with the standard BC-spline coefficients in the correct branches (mirror `mitchell.js`), and add `cubic2.support = 2;`.
165
+ - **Confidence**: Confirmed
166
+
167
+ ### GFXT-30 `sampler2d_copy_with_margins` addresses the *source* with the *destination* channel count
168
+ - **Severity**: MAJOR
169
+ - **File**: `engine/graphics/texture/sampler/sampler2d_copy_with_margins.js:45,66,73,91,110,123,144,151`
170
+ - **Defect**: Every source column offset is computed as `… * dItemSize` (the destination's channel count). `sItemSize` is read on line 29 and then used only to derive `_itemSize` and `sRowSize`, never for source column addressing. The function is explicitly built to tolerate differing channel counts (`_itemSize = Math.min(dItemSize, sItemSize)`), so this is not an implicit precondition.
171
+ - **Failure scenario**: copying a 3-channel RGB patch into a 4-channel RGBA atlas. For the top-margin row, `sOffset = sA + (x + sourceX) * 4` addresses source element `4x` where element `3x` was meant. At `x = 3` it reads element 12 — texel 4's red — instead of element 9, texel 3's red; and because the read then takes 3 consecutive elements it splices `(R4, G4, B4)` where `(R3, G3, B3)` belonged. The margins are colour-shifted garbage sampled a third of a row too far right, and past `x = source.width * 3/4` they run off the end of the row into the next scanline. (The interior patch itself is correct — it goes through `destination.copy`, which uses `sItemSize`.)
172
+ - **Fix**: replace `dItemSize` with `sItemSize` in every source-offset expression (lines 45, 66, 73, 91, 110, 123, 144, 151); leave the destination offsets on `dItemSize`.
173
+ - **Confidence**: Confirmed
174
+
175
+ ### GFXT-18 `TextureBinaryBufferSerializer` can write a Float16 texture it cannot read back
176
+ - **Severity**: MINOR
177
+ - **File**: `engine/graphics/texture/sampler/serialization/TextureBinaryBufferSerializer.js:37-72`
178
+ - **Defect**: `DataTypeIndices` assigns `Float16: 8` and `serializeTexture` happily writes it, but `readTypedArray`'s switch has no `BinaryDataType.Float16` arm, so it returns `null` and throws.
179
+ - **Failure scenario**: `serializeTexture(buf, Sampler2D.float16(1, 8, 8))` succeeds; `deserializeTexture(buf)` throws `TypeError: Unsupported data type float16`. The data is written to disk in a format that can never be loaded.
180
+ - **Fix**: add `case BinaryDataType.Float16: result = new Float16Array(length); buffer.readUint16Array(new Uint16Array(result.buffer), 0, length); break;` (or the matching typed reader), or reject Float16 at serialise time.
181
+ - **Confidence**: Confirmed
182
+
183
+ ### GFXT-19 `computeSignedDistanceField_NaiveFlood` skips the top neighbours of cell (0,1)
184
+ - **Severity**: MINOR
185
+ - **File**: `engine/graphics/texture/sampler/distance/computeSignedDistanceField_NaiveFlood.js:38`
186
+ - **Defect**: `const top = index > width;` should be `index >= width`. Row 0 correctly has no top neighbours (`index` in `[0, width-1]`), but `index === width` — which is `(x=0, y=1)` — also fails the test even though it does have a row above it.
187
+ - **Failure scenario**: an 8×8 input whose only seed pixel is at (0,0). Cell (0,1) never sees its top-row neighbours, so it is not seeded from (0,0) or (1,0) during the initial open-set scan and is reached only later through (1,1). Its distance comes out one step larger than the correct value, and that error propagates down column 0.
188
+ - **Fix**: `const top = index >= width;`
189
+ - **Confidence**: Confirmed
190
+
191
+ ### GFXT-20 `Sampler2D` constructor validates `width` twice and never validates `height`
192
+ - **Severity**: MINOR
193
+ - **File**: `engine/graphics/texture/sampler/Sampler2D.js:45`
194
+ - **Defect**: `if (!Number.isInteger(height) || width < 0)` — the second clause re-tests `width`. A negative integer `height` passes both this guard and the buffer-underflow check below it (the product `width * height * itemSize` goes negative, so `data.length < …` is trivially false).
195
+ - **Failure scenario**: `new Sampler2D(new Uint8Array(0), 1, 4, -1)` constructs successfully with `height = -1` instead of throwing `height must be a non-negative integer`. Downstream, `zeroFill`/`fill` iterate zero rows and `resize` allocates a `new Constructor(negative)`, which throws a `RangeError` far from the actual mistake.
196
+ - **Fix**: `if (!Number.isInteger(height) || height < 0)`.
197
+ - **Confidence**: Confirmed
198
+
199
+ ### GFXT-21 `sampler2d_channel_compute_min_indices` seeds the result with index 0 rather than the channel offset
200
+ - **Severity**: MINOR
201
+ - **File**: `engine/graphics/texture/sampler/sampler2d_channel_compute_min_indices.js:36-40`
202
+ - **Defect**: `bestValue` is initialised from `data[channel]` (element index `channel`) but the corresponding reported index is hard-coded to `0`. Every index written later in the loop is a true element index `i`, so the seed is inconsistent with the rest of the output.
203
+ - **Failure scenario**: a 4-channel sampler where the minimum of channel 2 occurs at texel 0. The function returns count 1 with `result[0] = 0`, but the element address of that value is 2. A caller converting the returned element index back to a texel (`index / itemSize | 0`) gets the right texel by accident here, but a caller reading `data[result[0]]` gets channel 0's value instead of channel 2's.
204
+ - **Fix**: `result[result_offset] = channel;`
205
+ - **Confidence**: Confirmed
206
+
207
+ ### GFXT-22 `sampler2d_to_texture` hands non-uint8 data to an `UnsignedByteType` texture
208
+ - **Severity**: MINOR
209
+ - **File**: `engine/graphics/texture/sampler/sampler2d_to_texture.js:20-33`
210
+ - **Defect**: When `sampler.itemSize === 4` the function skips conversion and references `sampler.data` directly, then unconditionally sets `result.type = UnsignedByteType` and `result.format = RGBAFormat`. The `else` branch, which allocates through `Sampler2D.uint8`, shows the function assumes 8-bit storage.
211
+ - **Failure scenario**: `sampler2d_to_texture(Sampler2D.float32(4, 64, 64))` yields a `DataTexture` whose `image.data` is a `Float32Array` but whose declared type is `UnsignedByteType`. WebGL's `texImage2D` with `UNSIGNED_BYTE` and a `Float32Array` raises `INVALID_OPERATION`; the texture uploads as black.
212
+ - **Fix**: take the conversion branch whenever the storage is not `Uint8Array`/`Uint8ClampedArray`, not only when `itemSize !== 4` (`sampler2d_ensure_uint8_RGBA` already implements exactly that test).
213
+ - **Confidence**: Confirmed
214
+
215
+ ---
216
+
217
+ ## texture/atlas
218
+
219
+ ### GFXT-23 `TextureAtlas.repack()` clears `Packed` twice and never clears `Painted`, so repacked patches are never redrawn
220
+ - **Severity**: CRITICAL
221
+ - **File**: `engine/graphics/texture/atlas/TextureAtlas.js:362`
222
+ - **Defect**: `patch.clearFlag(AtlasPatchFlag.Packed | AtlasPatchFlag.Packed)` ORs the same flag with itself; `AtlasPatchFlag.Painted` was clearly intended as the second operand. `repack()` then re-places every patch through `pack()` (which reorders by largest-min-side first, so placements do change), but `paint()` at line 497 only repaints a patch when `!getFlag(Painted) || last_painted_version !== sampler.version` — both false — so no pixels move.
223
+ - **Failure scenario**: an atlas holding patches A, B, C; remove B, then call `repack()`. A and C get new packing rectangles and new UVs, the pixel buffer is untouched, and no area is erased. A's UV rect now points at whatever pixels happen to occupy its new location (C's old image, or the erased hole left by B). Every repacked sprite renders the wrong texture until something else forces a version bump.
224
+ - **Fix**: `patch.clearFlag(AtlasPatchFlag.Packed | AtlasPatchFlag.Painted);` — and erase the vacated areas (or call `erase()`) before the re-pack so leftovers do not bleed into padding.
225
+ - **Confidence**: Confirmed
226
+
227
+ ### GFXT-24 `AtlasLookupTexture.update()` never allocates storage for the first 32 patches
228
+ - **Severity**: CRITICAL
229
+ - **File**: `engine/graphics/texture/atlas/AtlasLookupTexture.js:58-65`
230
+ - **Defect**: The reallocation is gated solely on `image.height < expected_row_count`. The texture starts at 1×1 with a 4-element `Uint16Array`, and `expected_row_count = Math.ceil(patch_count / 32)` is also 1 for any patch count in `[1, 32]`, so the branch never fires and neither `image.width` (still 1) nor `image.data` (still 4 elements) is ever corrected.
231
+ - **Failure scenario**: attach an atlas holding 10 patches and call `update()`. The write loop runs for `i = 0..9` at addresses `0..39`, but the backing array has 4 slots — patches 1 through 9 are silently discarded (typed-array out-of-range writes are no-ops). The texture stays 1×1, so the shader can only ever read patch 0's rect and every other sprite samples the wrong region.
232
+ - **Fix**: gate on the full shape, e.g. `if (image.width !== rowSize || image.height < expected_row_count)`, and set `image.width`/`image.height`/`image.data` together.
233
+ - **Confidence**: Confirmed
234
+
235
+ ### GFXT-25 `WebGLTextureAtlas.add()` never records the patch size, so every patch packs to a zero-area box at (0,0)
236
+ - **Severity**: CRITICAL
237
+ - **File**: `engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js:93-108` (also `:112`, `:178`)
238
+ - **Defect**: `add()` receives `target_width`/`target_height`, uses them only to accumulate `__total_area`, and never writes them into `patch.packing`; nothing ever calls `updatePositionFromPacking`, so `patch.position` also stays `(0,0)`. `layout()` therefore packs default (zero-extent) AABBs, and `build()` copies every source texture to `patch.position` — always the origin. Separately, `power = Math.ceil(Math.log2(this.__total_area))` treats an *area* as if it were a side length.
239
+ - **Failure scenario**: `add(tex, 0, 0, 64, 64, 64, 64)` for four textures, then `layout()` and `build(renderer)`. `__total_area = 16384`, so `power = 14` and the atlas is sized 16384×16384 (a ~1 GB `Uint8Array`) for four 64×64 tiles. All four packings remain zero-area at the origin, every `copyTextureToTexture` writes to `(0,0)`, and the four textures overwrite one another. The class cannot produce a usable atlas.
240
+ - **Fix**: in `add()`, `patch.size.set(target_width, target_height)` and `patch.packing.set(0, 0, target_width, target_height)`; after `layout()`, call `patch.updatePositionFromPacking(this.__width, this.__height)`; and size the atlas from `Math.ceil(Math.log2(Math.sqrt(this.__total_area)))`. Also index the source array by `patch.id` in `build()` (`this.__sources[atlas_patch.id]`) to match how `add()` stores it.
241
+ - **Confidence**: Confirmed
242
+
243
+ ### GFXT-26 `AtlasLookupTexture` declares an RGB internal format for RGBA integer data
244
+ - **Severity**: MAJOR
245
+ - **File**: `engine/graphics/texture/atlas/AtlasLookupTexture.js:27-32`
246
+ - **Defect**: `texture.format = RGBAIntegerFormat` with four `Uint16` components per texel, but `texture.internalFormat = 'RGB16UI'`. GLES 3.0 requires format `RGB_INTEGER` for internal format `RGB16UI`; `RGBA_INTEGER` pairs only with `RGBA16UI`.
247
+ - **Failure scenario**: the first upload issues `texImage2D(TEXTURE_2D, 0, RGB16UI, w, h, 0, RGBA_INTEGER, UNSIGNED_SHORT, data)` → `GL_INVALID_OPERATION`. No texels are stored, so every `texelFetch` of the lookup table returns zeros and all patch rects resolve to `(0,0,0,0)`.
248
+ - **Fix**: `texture.internalFormat = 'RGBA16UI';`
249
+ - **Confidence**: Confirmed
250
+
251
+ ---
252
+
253
+ ## texture/virtual
254
+
255
+ ### GFXT-27 Changing `VirtualTexturePage.tile_resolution` leaves the residency capacity and tiles-per-row stale
256
+ - **Severity**: MAJOR
257
+ - **File**: `engine/graphics/texture/virtual/VirtualTexturePage.js:134-139` (consumed at `:263`, `:349-367`; driven from `VirtualTextureSystem.js:39`)
258
+ - **Defect**: The `tile_resolution` setter recomputes `#tile_slot_resolution` and drops residency, but never calls `#update_residency_capacity()`. `#residency_tile_capacity` and `#page_texture_resolution_in_tiles` are only refreshed by the `page_texture_size` setter, so they keep values derived from the previous slot resolution.
259
+ - **Failure scenario**: `system.initialize({page_resolution: 4096, …})` fixes capacity at `floor(4096/136)² = 900` slots (default tile 128 + 2×4 margin). A subsequent `system.setTexture({tile_resolution: 256, …})` raises the slot resolution to 264 — true capacity is `floor(4096/264)² = 225` — but capacity stays 900 and tiles-per-row stays 30. `#make_resident` then accepts slot 899 and computes `write_position = (29*264, 29*264) = (7656, 7656)`, far outside the 4096×4096 page, and the shader's `u_page_resolution` of 30 mis-maps every slot index to a page coordinate. The only in-repo call site (`virtual/prototype.js:225-230`) happens to call `setTexture` *before* `initialize`, which masks the bug.
260
+ - **Fix**: call `this.#update_residency_capacity()` at the end of the `tile_resolution` setter (and from `#update_tile_slot_resolution`).
261
+ - **Confidence**: Confirmed
262
+
263
+ ### GFXT-28 `VirtualTextureSystem.setTexture` silently ignores its `tile_margin` argument
264
+ - **Severity**: MAJOR
265
+ - **File**: `engine/graphics/texture/virtual/VirtualTextureSystem.js:28-43` (`VirtualTexturePage` has a `tile_margin` getter at `:148` but no setter)
266
+ - **Defect**: `tile_margin` is destructured out of the options object and never used; `VirtualTexturePage.#tile_margin` is hard-wired to 4 and cannot be configured at all.
267
+ - **Failure scenario**: an asset pipeline that bakes 256 px tiles with an 8 px margin, configured as `setTexture({tile_resolution: 256, tile_margin: 8, …})`. The page still uses margin 4, so `#tile_slot_resolution` is 264 while each loaded tile is 272×272. In `#make_resident`, `tile_texture.image.data.set(tile.data.data)` copies 272²·4 values into a 264²·4 buffer and throws `RangeError: offset is out of bounds`; the page loads no tiles at all. The `assert.equal(tile.data.width, this.#tile_slot_resolution)` that would have diagnosed this is compiled out in production.
268
+ - **Fix**: add a `tile_margin` setter on `VirtualTexturePage` that updates `#tile_slot_resolution`, refreshes the residency capacity and drops residency, and have `setTexture` forward the argument.
269
+ - **Confidence**: Confirmed
270
+
271
+ ### GFXT-31 `ResidencyDebugView` maps page slots to a grid using the tile size instead of the slot size
272
+ - **Severity**: MINOR
273
+ - **File**: `engine/graphics/texture/virtual/debug/ResidencyDebugView.js:27,38-45`
274
+ - **Defect**: `tile_columns = t.page_texture_resolution[0] / tile_resolution` divides by the tile resolution, but `VirtualTexturePage` lays slots out on a grid of `tile_resolution + 2 * tile_margin` and exposes the correct count as `page_texture_resolution_in_tiles[0]`. The draw also ignores the per-slot margin offset.
275
+ - **Failure scenario**: a 4096 px page with 128 px tiles and the default 4 px margin has 30 slot columns; the view computes 32. Slot 31, which physically lives at grid cell (1,1), is drawn at cell (31,0) — off the right edge of the visualisation. Every slot from 30 upward is drawn in the wrong cell, so the residency picture does not correspond to the page.
276
+ - **Fix**: use `t.page_texture_resolution_in_tiles[0]` for the column count and step by `tile_resolution + 2 * t.tile_margin`, offsetting the blit by `t.tile_margin`.
277
+ - **Confidence**: Confirmed
278
+
279
+ ### GFXT-29 Virtual-texture fragment shader neither floors nor clamps the computed LOD
280
+ - **Severity**: MINOR
281
+ - **File**: `engine/graphics/texture/virtual/VirtualTextureMaterial.js:139-144` (`:109`)
282
+ - **Defect**: `float texture_lod_0 = float(texture_lod);` is a no-op cast where `floor(texture_lod)` was intended (the next line uses `ceil` for the upper level). Neither level is clamped to `[0, u_max_mip_level]` before `uint(...)` conversion and the `u_max_mip_level - texture_lod_0` subtraction.
283
+ - **Failure scenario**: a surface close enough to the camera that one screen pixel covers less than one virtual texel gives `texture_lod < 0`. `uint(-0.5)` is undefined in GLSL ES 3.0 and in practice yields a huge value, so `u_max_mip_level - texture_lod_0` underflows the unsigned subtraction and `1u << ui_texture_lod_0` shifts by an out-of-range amount — the mapping fetch lands on an arbitrary texel and the surface renders with a random tile. Symmetrically, a distant surface with `texture_lod > u_max_mip_level` underflows the same subtraction.
284
+ - **Fix**: `float texture_lod_0 = floor(texture_lod);` and clamp both levels: `clamp(texture_lod_0, 0.0, float(u_max_mip_level))` before converting to `uint`.
285
+ - **Confidence**: Likely (GL-side behaviour not executed here; the `float()`-for-`floor()` substitution itself is confirmed by inspection)
286
+
287
+ ---
288
+
289
+ ## particles/particular — parameters, simulation, renderers, volumes
290
+
291
+ ### GFXT-32 Billboard particle spec declares `color` as 1 component; the shader declares `vec4`
292
+ - **Severity**: CRITICAL
293
+ - **File**: `engine/graphics/particles/particular/engine/renderers/billboard/BILLBOARD_PARTICLE_SPECIFICATION.js:23` (shader at `renderers/billboard/ParticleBillboardMaterial.js:123`)
294
+ - **Defect**: The spec registers `color` with `itemSize: 1`, but the billboard shader it exists to feed declares `attribute vec4 color;`. The parallel spec for the same shader, `emitter/PARTICULAR_PARTICLE_SPECIFICATION.js:68-69`, correctly uses `itemSize: 4`.
295
+ - **Failure scenario**: two independent wrong outputs. (a) Rendering — a 1-component buffer bound to a `vec4` attribute has GL supply `(r, 0, 0, 1)`, so every particle renders pure red and fully opaque and per-particle alpha fade is dead (`vColor.a` is always 1). (b) Writing — `ParticlePool.writeAttributeVector4(i, …COLOR, r, g, b, a)` on an `itemSize: 1` attribute writes `array[i*1 + 0..3]`, overwriting the colours of particles `i+1, i+2, i+3`. Writing four particles in index order leaves particle 0 holding whatever particle 3 wrote.
296
+ - **Fix**: `itemSize: 4` for the `color` entry, matching `PARTICULAR_PARTICLE_SPECIFICATION`.
297
+ - **Confidence**: Confirmed
298
+
299
+ ### GFXT-33 `ParticularEngine.advanceEmitter` collapses a layer's simulation steps into a set keyed by type
300
+ - **Severity**: MAJOR
301
+ - **File**: `engine/graphics/particles/particular/engine/ParticularEngine.js:155-167`
302
+ - **Defect**: Each step is dispatched through one shared object per `SimulationStepType` (`const step = steps[type]`) and a layer's parameters go into the single slot `step.layer_parameters[j]`. A second definition of the same type on the same layer overwrites the first, and the execution loop then runs each *type* once in numeric enum order, discarding the authored order.
303
+ - **Failure scenario**: `ParticleLayer.steps` is an ordered `List` filled straight from JSON, so expressing wind and gravity as two forces is natural — `[{FixedPhysics}, {ApplyForce, [5,0,0]}, {ApplyForce, [0,-9.8,0]}]`. `layer_parameters[j]` is set to `[5,0,0]` and immediately overwritten with `[0,-9.8,0]`; `ApplyForce.execute()` runs once. After one second a particle has `vx = 0` instead of `5` — the wind silently vanishes with no warning. Separately, an authored `[ApplyForce, FixedPhysics]` (semi-implicit Euler) executes as `[FixedPhysics, ApplyForce]` because `FixedPhysics` is enum value 0, so the first tick's gravity displacement is 0.
304
+ - **Fix**: iterate the layer's `steps` list in order and issue one step invocation per definition; at minimum reject duplicate types at build time rather than silently dropping them.
305
+ - **Confidence**: Confirmed (mechanism verified by inspection). Scope note: I enumerated every authored `steps` list in `app/data/meep-settings.json` — there are two, `["FixedPhysics","CurlNoiseVelocity"]` and `["FixedPhysics"]`, neither with a duplicate type and both already in enum order. So no shipped effect trips this today; it is a latent hazard for any effect that adds a second force of the same type, with no diagnostic when it does.
306
+
307
+ ### GFXT-34 `ParameterLookupTable` writes its item count one byte too narrow at exact powers of two
308
+ - **Severity**: MAJOR
309
+ - **File**: `core/math/lookup/ParameterLookupTable.js:110` — reached through the in-scope re-export `particles/particular/engine/parameter/ParameterLookupTable.js`
310
+ - **Defect**: `const itemCountBitSize = Math.log2(itemCount)` is used as if it were the value's bit width. The width is `floor(log2(n)) + 1`, so at exactly `2^k` the size class picks a field one byte too small.
311
+ - **Failure scenario**: `RGB_LUT_TURBO` (in-repo, `parameter/sample/RGB_LUT_TURBO.js`) has exactly 256 entries. `toBinaryBuffer` computes `log2(256) === 8`, takes the `<= 8` branch, and calls `writeUint8(256)`, which truncates to 0 — then still writes 768 data floats plus 256 position floats. `fromBinaryBuffer` reads `itemCount = 0`, produces an empty LUT, and leaves the read cursor 4096 bytes short, so every subsequent field in the same buffer (the rest of the `ParticleParameter` / `ParameterTrack` / emitter payload) decodes as garbage. Same failure at 65536; 255, 257 and 65535 are fine.
312
+ - **Fix**: size the field from the value: `itemCountByteSize = itemCount <= 0xFF ? 1 : itemCount <= 0xFFFF ? 2 : 4;`
313
+ - **Confidence**: Confirmed
314
+
315
+ ### GFXT-35 `ParameterLookupTable.addValue` does not recompute value statistics
316
+ - **Severity**: MAJOR
317
+ - **File**: `core/math/lookup/ParameterLookupTable.js:305` (end of `addValue`) — reached through the in-scope re-export
318
+ - **Defect**: `addValue` splices into `positions` and `data` but never calls `computeStatistics()`. Every other mutator (`write`, `fromJSON`, `fromBinaryBuffer`) does, so a table built purely with `addValue` keeps `valueMin = valueMax = 0`.
319
+ - **Failure scenario**: build a scale track programmatically — `lut.addValue(0, [1]); lut.addValue(1, [4]);` — and attach it as the `scale` track of a layer whose `particleSize.max` is 2. `ParticleLayer.computeScaledSpriteHalfSize()` (`emitter/ParticleLayer.js:165`) computes `maxParticleSize * lookupTable.valueMax = 2 * 0 = 0`, so the layer's `baseBoundingBox` gets no sprite padding: the emitter is frustum-culled and its BVH leaf is undersized while 8-unit-wide sprites are still on screen, and particles pop out at the screen edge. Secondarily, `hash()` short-circuits on `valueMin === valueMax`, so every `addValue`-built LUT hashes identically regardless of contents.
320
+ - **Fix**: call `this.computeStatistics()` at the end of `addValue`.
321
+ - **Confidence**: Confirmed
322
+
323
+ ### GFXT-36 Barycentric point picking on a triangle is not uniform
324
+ - **Severity**: MAJOR
325
+ - **File**: `engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.js:158`; same code at `engine/utils/distributeParticlesOnObject3D.js:215`
326
+ - **Defect**: `const r0 = random(); const r1 = random() * (1 - r0);` gives a joint density proportional to `1 / (1 - r0)` over the simplex instead of the constant required for uniformity. The cited reference is the *reflect* method (`if (r0 + r1 > 1) { r0 = 1 - r0; r1 = 1 - r1; }`), which is not what was implemented.
327
+ - **Failure scenario**: 2,000,000 samples on one triangle, binned by the barycentric weight on vertex B in deciles, come out flat at `0.100` per decile where the correct distribution is `0.190, 0.170, 0.150, 0.130, 0.110, 0.090, 0.070, 0.050, 0.030, 0.010`. Density is about 10x too high in the decile nearest vertex B and about half the correct density near vertex A. Seeding a 2-triangle 10x10 ground quad with 5000 particles visibly piles them along the shared diagonal and thins the two opposite corners.
328
+ - **Fix**: draw two independent uniforms and reflect, or use `su0 = Math.sqrt(random()); r0 = 1 - su0; r1 = random() * su0;`.
329
+ - **Confidence**: Confirmed
330
+
331
+ ### GFXT-37 `ParticleVolume.fromJSON` logs an unknown attribute and then dereferences it
332
+ - **Severity**: MAJOR
333
+ - **File**: `engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.js:353-359`
334
+ - **Defect**: The `attribute_object === undefined` branch warns "skipping" but has no `continue`; execution falls straight through to `attribute_object.fromJSON(attribute_json)`.
335
+ - **Failure scenario**: only `color`, `size` and `blending` are in `EXPOSED_ATTRIBUTES`. Loading `{ "density": 2, "shape": {…}, "attributes": { "velocity": { "value": [0,1,0] } } }` logs `No 'velocity' attribute, skipping` and then throws `TypeError: Cannot read properties of undefined (reading 'fromJSON')`, aborting the entire scene load instead of skipping one attribute.
336
+ - **Fix**: add `continue;` inside the `if`.
337
+ - **Confidence**: Confirmed
338
+
339
+ ### GFXT-38 `lut_to_sampler` clobbers the alpha channel of RGBA lookup tables
340
+ - **Severity**: MINOR
341
+ - **File**: `engine/graphics/particles/particular/engine/utils/volume/lut_to_sampler.js:18`
342
+ - **Defect**: `s[3] = 255` runs unconditionally after `lut.sample(...)`, so an `itemSize === 4` LUT's authored alpha is discarded. The line is only correct for the RGB (`itemSize === 3`) case.
343
+ - **Failure scenario**: `lut_to_sampler(RGBA_LUT_HEATMAP_IR, 64, 4)` — the in-repo LUT authors alpha `10` at every key. `sample` writes `s[3] = 10`; the next line overwrites it with `255`. Every texel comes out fully opaque, so the sampler renders as a solid heat ramp instead of a ~4%-opacity overlay.
344
+ - **Fix**: `if (lut.itemSize < 4) { s[3] = 255; }`. (Same line, secondary: `width === 1` makes `i / (width - 1)` be `NaN`, and `sample` then falls through its search loop and returns the last key rather than the first.)
345
+ - **Confidence**: Confirmed
346
+
347
+ ### GFXT-39 `ParticleVolume.build` hard-wires the global unseeded RNG
348
+ - **Severity**: MINOR
349
+ - **File**: `engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.js:599`
350
+ - **Defect**: `__distribute` is explicitly parameterised on a `random` function — and both sibling utilities take an explicit seed — but `build()` passes `Math.random`.
351
+ - **Failure scenario**: a scene holding a `ParticleVolume` with `density: 5` over a 10 m³ shape (50 particles) produces different particle positions on every load, and two clients viewing the same serialized scene see different smoke. The volume is not reproducible from its own serialized form.
352
+ - **Fix**: accept a seed on `build()`/`fromJSON` and pass `seededRandom(seed)`, matching the mesh distributors.
353
+ - **Confidence**: Confirmed
354
+
355
+ ---
356
+
357
+ ## geometry — buffered attributes, BVH queries, skinning
358
+
359
+ ### GFXT-40 `computeMeshSurfaceArea` reads vertex C's Y from the Z slot
360
+ - **Severity**: CRITICAL
361
+ - **File**: `engine/graphics/geometry/computeMeshSurfaceArea.js:40`
362
+ - **Defect**: `const y2 = points[c3 + 2];` — the third vertex is assembled as `(cx, cz, cz)`, so every triangle area in the mesh is computed against a corrupted vertex. (Found independently by two reviewers.)
363
+ - **Failure scenario**: any mesh lying in the XY plane, e.g. the single triangle `A=(0,0,0)`, `B=(1,0,0)`, `C=(0,1,0)` whose area is `0.5`. C is read as `(0,0,0) === A`, so `tri3_area` returns 0 and the total is 0. `distributeParticlesOnMesh` then computes `surfaceSpacing = 0` → `invSurfaceSpacing = Infinity` → `placementCount = 0 * Infinity = NaN` → `for (j = 0; j < NaN; …)` never runs, so **zero particles are emitted** and the pool keeps whatever positions it had. For non-planar meshes the per-triangle area weights are simply wrong, so the area-weighted distribution is biased.
364
+ - **Fix**: `const y2 = points[c3 + 1];`
365
+ - **Confidence**: Confirmed
366
+
367
+ ### GFXT-41 `bvh32_geometry_raycast` reports a hit it never wrote
368
+ - **Severity**: MAJOR
369
+ - **File**: `engine/graphics/geometry/buffered/query/bvh32_geometry_raycast.js:124`
370
+ - **Defect**: `hit_found = true` is set as soon as a triangle intersects, before the `best_distance_sqr` test that decides whether `result` is written. If every intersection lies beyond `max_distance`, the function returns `true` with `result` untouched.
371
+ - **Failure scenario**: the broadphase `bvh32_query_user_data_ray` prunes on AABB *entry* distance, so a triangle whose box is entered early but whose surface is hit late survives the cull. Triangle `A=(0,0,0)`, `B=(10,0,20)`, `C=(0,10,20)` (AABB `(0,0,0)-(10,10,20)`), ray origin `(1,1,-1)`, direction `(0,0,1)`, `max_distance = 4`: AABB entry `t = 1 < 4` so the triangle is reported, but the surface hit is at `(1,1,4)`, distance 5. `distance_sqr = 25 >= 16`, so `result` is never written — yet the function returns `true` and the caller reads a stale `SurfacePoint3` from the previous query.
372
+ - **Fix**: move `hit_found = true;` inside the `if (distance_sqr < best_distance_sqr)` block.
373
+ - **Confidence**: Confirmed
374
+
375
+ ### GFXT-42 `decode_attribute_value` throws on every 8-bit and signed normalized type
376
+ - **Severity**: MAJOR
377
+ - **File**: `engine/graphics/geometry/decode_attribute_value.js:17-27`
378
+ - **Defect**: The denormalization switch handles only `Float32`, `Float64`, `Uint16` and `Uint32`; `Uint8`, `Int8`, `Int16` and `Int32` fall through to `throw`. The sibling `core/collection/array/typed/typed_array_value_denormalize.js` already carries the full, correct table.
379
+ - **Failure scenario**: glTF requires `COLOR_0` as unsigned byte to be `normalized: true`, and `GLTFLoader` produces a `Uint8Array` `BufferAttribute` accordingly. `sh3/path_tracer/texture/sample_material.js:37-42` derives `BinaryDataType.Uint8` from that array and calls `decode_attribute_value(v, Uint8, true)`, which throws `Unsupported data type uint8` and aborts the path-tracer sample for that surface. The same happens for any `KHR_mesh_quantization` asset (`Int8`/`Int16`).
380
+ - **Fix**: add the missing arms mirroring `typed_array_value_denormalize` — `Uint8 → /255`, `Int8 → max(v/127, -1)`, `Int16 → max(v/32767, -1)`, `Int32 → max(v/2147483647, -1)`.
381
+ - **Confidence**: Confirmed
382
+
383
+ ### GFXT-43 `computeSkinnedMeshVertices` reads attributes as raw `.array` with a hard-coded stride
384
+ - **Severity**: MAJOR
385
+ - **File**: `engine/graphics/geometry/skining/computeSkinnedMeshVertices.js:24-26`
386
+ - **Defect**: `attributes.position.array` / `skinIndex.array` / `skinWeight.array` are read directly and then indexed with a fixed stride of 3 or 4, ignoring both interleaving and the `normalized` flag. In three r136 `InterleavedBufferAttribute.array` is a getter returning `this.data.array` — the *whole* interleaved buffer.
387
+ - **Failure scenario**: a glTF skinned mesh with an interleaved vertex buffer (POSITION + NORMAL + TEXCOORD_0, stride 8 floats). `positions[index * 3]` walks three floats at a time through position, normal and UV data, so `mesh_apply_skeletal_vertex_skinning` produces completely scrambled vertices. Second case: gltfpack / `KHR_mesh_quantization` output where `WEIGHTS_0` is normalized `Uint8` — a weight is read as `255` instead of `1.0`, scaling the skinned position by 255x. Both feed `computeSkinnedMeshBoundingVolumes` (used by `GLTFAssetLoader.js:307`), `meshFromSkinnedMesh`, and the particle distributors.
388
+ - **Fix**: route the three attributes through the existing `buffered/buffer_attribute_deinterleave.js` + `buffered/buffer_attribute_denormalize.js` before reading `.array`, or pass real `offset`/`stride`/`normalized` into `mesh_apply_skeletal_vertex_skinning`.
389
+ - **Confidence**: Confirmed
390
+
391
+ ### GFXT-44 `computeGeometryBoundingSphereMiniball` builds its point set from the raw attribute array
392
+ - **Severity**: MAJOR
393
+ - **File**: `engine/graphics/geometry/buffered/computeGeometryBoundingSphereMiniball.js:10`
394
+ - **Defect**: `const vertexData = geometry.attributes.position.array;` followed by a point count of `positions.length / 3`. As above, for an interleaved attribute that array is the entire interleaved buffer, and for a quantized attribute the values are un-denormalized.
395
+ - **Failure scenario**: interleaved glTF geometry (position + normal + uv, stride 8) makes `PointSet(len/3, 3, wholeBuffer)` fold normal and UV components into the point cloud, so the sphere centre and radius are computed over garbage. Quantized `Int16` normalized positions give a radius roughly 32767x the true size. `ensureGeometryBoundingSphere` → this function is what `GLTFAssetLoader.__processMesh` uses for every non-skinned mesh, so the result is broken frustum culling — meshes popping out of view, or never culling at all.
396
+ - **Fix**: deinterleave and denormalize first, and drive the loop from `attribute.count` rather than `array.length`.
397
+ - **Confidence**: Confirmed
398
+
399
+ ### GFXT-45 Degenerate triangles inject a fabricated up-normal into vertex-normal accumulation
400
+ - **Severity**: MAJOR
401
+ - **File**: `engine/graphics/geometry/buffered/geometry_compute_vertex_normals_indexed.js:28-37`
402
+ - **Defect**: `geometry_construct_triangle_normal` delegates to `tri3_compute_normal`, which writes the sentinel `(0, 1, 0)` when the cross product has zero length. This loop adds that full-magnitude unit vector into all three vertices' accumulated normals instead of contributing nothing.
403
+ - **Failure scenario**: a mesh with a collapsed triangle — indices `[0,1,2, 0,1,1]`, where 0/1/2 form a flat face in the XY plane with normal `(0,0,1)`. Vertex 0 accumulates `(0,0,1)` from the good triangle plus `(0,1,0)` from the degenerate one; after `v3_array_normalize_many` its normal is `(0, 0.707, 0.707)` instead of `(0,0,1)`. Shading is visibly wrong along every collapsed edge — routine in LOD/simplified meshes and n-gon triangulation output.
404
+ - **Fix**: accumulate the raw unnormalized cross product in this loop (which contributes zero for a degenerate face and also gives three.js-compatible area weighting), or have `geometry_construct_triangle_normal` signal degeneracy and `continue` on it.
405
+ - **Confidence**: Confirmed
406
+
407
+ ### GFXT-46 `meshFromSkinnedMesh` applies the world transform twice
408
+ - **Severity**: MAJOR
409
+ - **File**: `engine/graphics/geometry/skining/meshFromSkinnedMesh.js:33-36`
410
+ - **Defect**: `computeSkinnedMeshVertices` produces `bindMatrixInverse · Σ(w·boneMatrix) · bindMatrix · p`, which is exactly what three's skinning shader hands to `modelMatrix` — i.e. mesh-local space by construction. The function nonetheless bakes `inverse(matrixWorld)` into those vertices while the new `Mesh` separately copies the source's local TRS, so the transform is applied once forward and once inverted.
411
+ - **Failure scenario**: a `SkinnedMesh` at `position = (0,5,0)` with no parent, in rest pose. `V = p`. `mesh.position` is copied as `(0,5,0)` and the geometry is baked to `p - (0,5,0)`, so the result renders at `translate(0,5,0) · (p - (0,5,0)) = p` while the original renders at `p + (0,5,0)`. The exported mesh is displaced by the full world transform; the bug is invisible only when `matrixWorld` is identity. Reachable from `model/game/options/exportSceneAsGLTF.js:166,181`.
412
+ - **Fix**: drop the `applyMatrix4(m4)` call (and the `updateMatrix`/`updateMatrixWorld` that exist only to feed it); the copied TRS already reproduces the source placement.
413
+ - **Confidence**: Likely
414
+
415
+ ### GFXT-47 `compute_buffer_geometry_byte_size` charges every attribute the size of its whole backing buffer
416
+ - **Severity**: MINOR
417
+ - **File**: `engine/graphics/geometry/buffered/compute_buffer_geometry_byte_size.js:12,16`
418
+ - **Defect**: Uses `array.buffer.byteLength` (the whole `ArrayBuffer`) rather than `array.byteLength` (the view). Attributes that are views into one shared buffer are each charged the full buffer.
419
+ - **Failure scenario**: a glTF geometry with position, normal and uv interleaved in one 1 MB `ArrayBuffer` reports about 3 MB, plus the index buffer's own full backing buffer. Any cache sized by this number — the pattern `GeometrySpatialQueryAccelerator` uses for its `valueWeigher` — evicts several times too aggressively.
420
+ - **Fix**: `result += attribute.array.byteLength;`
421
+ - **Confidence**: Confirmed
422
+
423
+ ### GFXT-48 `bvh32_geometry_raycast` returns an unset `index` and an unnormalized normal
424
+ - **Severity**: MINOR
425
+ - **File**: `engine/graphics/geometry/buffered/query/bvh32_geometry_raycast.js:59,126-131`
426
+ - **Defect**: `triangle_index` is in hand at line 59 but never assigned to the scratch hit, so `result.copy(scratch_hit)` overwrites `result.index` with the module scratch's permanent `-1`. `result.normal` is the raw cross product produced by `tri3_ray_intersection`, not a unit vector.
427
+ - **Failure scenario**: raycasting the triangle `A=(0,0,0)`, `B=(10,0,0)`, `C=(0,10,0)` returns `normal = (0,0,100)` (length `2·area`) and `index = -1`. The sibling `bvh32_geometry_nearest` over the same geometry returns a unit normal and a valid triangle index, so the two query entry points disagree on their output contract. Consumers that route through `SurfacePoint3.applyMatrix4` are masked because `applyDirectionMatrix4` renormalizes.
428
+ - **Fix**: set `scratch_hit.index = triangle_index;` before the copy, and normalize the normal (root cause is `core/geom/3d/triangle/tri3_ray_intersection.js:146`).
429
+ - **Confidence**: Confirmed
430
+
431
+ ### GFXT-49 `meshFromSkinnedMesh` keeps the source geometry's bind-pose bounding volumes
432
+ - **Severity**: MINOR
433
+ - **File**: `engine/graphics/geometry/skining/meshFromSkinnedMesh.js:13-21`
434
+ - **Defect**: `geometry.clone()` carries `boundingBox` and `boundingSphere` across (r136 `BufferGeometry.copy` clones both when non-null), and the position array is then overwritten with skinned vertices without invalidating them.
435
+ - **Failure scenario**: a humanoid whose source geometry already has bounds — always the case after `GLTFAssetLoader.__processMesh` — baked while crouched or mid-jump keeps the bind-pose sphere. `WebGLRenderer` frustum-culls the result against bounds that no longer enclose the geometry, so the baked mesh pops out of view at screen edges.
436
+ - **Fix**: null out `bakedGeometry.boundingBox` and `bakedGeometry.boundingSphere` after writing the skinned positions, or recompute them.
437
+ - **Confidence**: Confirmed
438
+
439
+ ### GFXT-50 `aabb3_to_buffer_geometry` winds its top face inward (and the module exports nothing)
440
+ - **Severity**: MINOR
441
+ - **File**: `engine/graphics/geometry/aabb3_to_buffer_geometry.js:8,19-22`
442
+ - **Defect**: The top quad repeats the bottom quad's winding, so its face normal points down instead of up. Separately the function declaration lacks `export`, so the module has no exports at all and no importer can currently reach it — which is why the winding error has gone unnoticed.
443
+ - **Failure scenario**: `AABB3.getCorners` emits corners in the order `(x0y0z0, x1y0z0, x0y1z0, x1y1z0, x0y0z1, x1y0z1, x0y1z1, x1y1z1)`. Bottom triangle `0→1→5` yields normal `(0, -dx·dz, 0)` = −Y, correctly outward. Top triangle `2→3→7` yields the same `(0, -dx·dz, 0)` = −Y, i.e. inward. With any default `FrontSide` material the top face of the box is invisible from above.
444
+ - **Fix**: add `export` to the declaration and reverse the top winding to `2, 7, 3, 7, 2, 6`.
445
+ - **Confidence**: Confirmed
446
+
447
+ ---
448
+
449
+ ## particles/particular — pool, group, emitter
450
+
451
+ ### GFXT-51 `ParticleGroup.deleteIndices` reuses the same swap source for every deletion
452
+ - **Severity**: CRITICAL
453
+ - **File**: `engine/graphics/particles/particular/group/ParticleGroup.js:478-492`
454
+ - **Defect**: The swap-source cursor `swapDestination` is decremented only when it happens to equal the current victim, never after it has actually been consumed as a swap source. Every swap in a multi-delete therefore copies from the *same* tail slot, duplicating one particle and losing the others.
455
+ - **Failure scenario**: reproduced under jest against the real class. Four particles with a scalar attribute holding `[10, 11, 12, 13]`; `group.deleteIndices([0, 1])` — equivalently two `group.remove(ref)` calls in one frame, which `groupOperations` merges into a single `Remove` with two operands. Expected survivors `[12, 13]`; the actual buffer after the call is `[13, 13]` — particle `12` is gone and `13` is rendered twice. `indexReferenceLookup`/`referenceIndexLookup` are also rewritten so two live references point at the same data slot. Any batch of two or more removals that produces two or more real swaps corrupts the group. The existing spec cases (`[1]`, `[0,2,4]`, `[1,9,10,11]`) each happen to generate exactly one swap, which is why this is untested.
456
+ - **Fix**: consume the source cursor and skip slots that are themselves being deleted — iterate victims ascending, advancing a tail pointer past deleted destinations, and decrement `swapDestination` after each push. A bare `swapDestination--` after the push is *not* sufficient: `indices = [0,1,3]` with `size = 5` would then copy deleted slot 3 into a survivor slot.
457
+ - **Confidence**: Confirmed (reproduced against the real `ParticleGroup`)
458
+
459
+ ### GFXT-52 `optimizeCommandQueue` sort comparator never returns `+1`, so writes can execute before their `Add`
460
+ - **Severity**: CRITICAL
461
+ - **File**: `engine/graphics/particles/particular/group/optimizeCommandQueue.js:141`
462
+ - **Defect**: `if (operatorA < operatorB) return -1; else if (operatorB > operatorA) return 1;` — the second condition is a restatement of the first, not its inverse, so the operator-ordering branch can never return `+1`. When `a` is a `WriteAttribute` and `b` is an `Add`/`Remove`, control falls through to the attribute-index arithmetic and compares a *reference id* against an *attribute index*, breaking the ordering invariant stated in the comment two lines above.
463
+ - **Failure scenario**: reproduced under jest. A group with 4 attributes and one existing particle (ref 0 at index 0, attribute 3 = `200`), then `r1 = create(); r2 = create(); writeAttribute(r1, 3, [11]); writeAttribute(r2, 3, [22]); update();`. `groupOperations` merges the adds into `Add[1,2]`; the comparator evaluates `b.operands[1] - a.operands[1] = 2 - 3 = -1` and sorts the writes first. Observed order: `Write(1,3) | Write(2,3) | Add[1,2]`. Both writes dereference to `undefined`, so `attribute.array.set(value, undefined * itemSize)` is `set(value, NaN)`, which coerces to offset **0** and silently overwrites the existing particle. Attribute 3 ends up `[22, 0, 0]`: the pre-existing `200` is clobbered and both new particles get `0`. This triggers whenever a grouped `Add` has two or more operands and its second reference id is below the write's attribute index — routine, since `IdPool` hands out ids from 0 and `PARTICULAR_PARTICLE_SPECIFICATION` has 12 attributes.
464
+ - **Fix**: `} else if (operatorA > operatorB) { return 1; }`
465
+ - **Confidence**: Confirmed (reproduced against the real `ParticleGroup`)
466
+
467
+ ### GFXT-53 `ParticleGroup.build()` leaves the geometry draw range at `Infinity`
468
+ - **Severity**: MAJOR
469
+ - **File**: `engine/graphics/particles/particular/group/ParticleGroup.js:101`
470
+ - **Defect**: `build()` creates a fresh `BufferGeometry`, whose three.js default `drawRange` is `{start: 0, count: Infinity}`, and never calls `setDrawRange`. Only `updateCapacity()` sets it, and neither the constructor nor `reset()` reaches that path. `build()` additionally copies the *old* attribute data forward unconditionally. The sibling `ParticlePool.build()` does call `setDrawRange` (line 280).
471
+ - **Failure scenario**: reproduced under jest. (a) `new ParticleGroup(spec)` gives `size = 0`, `capacity = 100`, `drawRange = {start: 0, count: Infinity}`, so the renderer draws `min(position.count, Infinity) = 100` vertices from zero-filled buffers — exactly the state `view/minimap/gl/MinimapMarkersGL.js:128` renders in (with `frustumCulled = false`) before the first marker is added. (b) Add 5 particles with attribute `a = [7,7]`, `update()` (draw range correctly becomes 5), then `reset()` → the count is `Infinity` again *and* the attribute array still reads `[7,7,7,7,7,7,7,7,7,7,…]`, so the group renders 100 vertices whose first 5 are the supposedly-cleared particles. `MinimapMarkersGL.shutdown()` takes exactly this path.
472
+ - **Fix**: end `build()` with `geometry.setDrawRange(0, this.size);`, and skip the old-data copy when `this.size === 0`.
473
+ - **Confidence**: Confirmed (reproduced against the real `ParticleGroup`)
474
+
475
+ ### GFXT-54 `ParticleGroup.preAllocate` advertises reserved capacity as live particles
476
+ - **Severity**: MAJOR
477
+ - **File**: `engine/graphics/particles/particular/group/ParticleGroup.js:601`
478
+ - **Defect**: `preAllocate` routes a *capacity* request through `updateCapacity`, whose tail unconditionally calls `geometry.setDrawRange(0, newSize)`. Reserving space therefore also tells the renderer that the reserved slots contain particles.
479
+ - **Failure scenario**: reproduced under jest. A group with 5 live particles (`size = 5`, `capacity = 5`, `drawRange.count = 5`); `group.preAllocate(500)` gives `capacity = 505` and `size` still `5`, but `drawRange.count = 505`. The renderer draws 500 uninitialised/stale vertices alongside the 5 real particles until the next `create()` + `update()` happens to reset the range.
480
+ - **Fix**: have `preAllocate` call `setCapacity(...)` (or a capacity-only helper) and leave the draw range alone — the draw range belongs to `setSize`, not to capacity management.
481
+ - **Confidence**: Confirmed (reproduced against the real `ParticleGroup`)
482
+
483
+ ### GFXT-55 `SERIALIZABLE_FLAGS` omits `Lit`, so `receiveLight` is dropped by binary serialization
484
+ - **Severity**: MAJOR
485
+ - **File**: `engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js:46` (consumed at `serde/ParticleEmitterSerializationAdapter.js:23,98-101`)
486
+ - **Defect**: `SERIALIZABLE_FLAGS` lists `PreWarm | DepthSorting | DepthReadDisabled | DepthSoftDisabled | AlignOnVelocity` but not `ParticleEmitterFlag.Lit`, even though `Lit` is an authored property (`toJSON`/`fromJSON` persist it as `receiveLight`, `ParticleVolume` writes it, and `ShaderManager` compiles a different shader for it). The adapter masks with `SERIALIZABLE_FLAGS` on both write and read, so the bit is silently discarded.
487
+ - **Failure scenario**: author an emitter with `receiveLight: true` (`ParticleVolume.js:296` calls `emitter.writeFlag(ParticleEmitterFlag.Lit, true)`), save the level through `ParticleEmitterSerializationAdapter.serialize`, and reload. `deserialize` neither restores nor clears `Lit`, and a fresh `ParticleEmitter` defaults it to false, so `ShaderManager.buildMaterial` (line 251) builds `make_particle_billboard_shader({ lights: false })` and the particles render unlit. Secondarily, `hash()`/`equals()` also use `SERIALIZABLE_FLAGS`, so a lit and an unlit emitter compare equal and can share a cached material record — note `EMITTER_MATERIAL_FLAGS` in `ShaderManager.js:20` *does* include `Lit`, which shows the intent.
488
+ - **Fix**: add `| ParticleEmitterFlag.Lit` to `SERIALIZABLE_FLAGS` and bump the adapter version (the field width is unchanged, so a pass-through upgrader suffices).
489
+ - **Confidence**: Confirmed
490
+
491
+ ### GFXT-56 Sphere emission shapes get a half-size layer bounding box
492
+ - **Severity**: MAJOR
493
+ - **File**: `engine/graphics/particles/particular/engine/emitter/ParticleLayer.js:206`
494
+ - **Defect**: `computePointBoundingBox` expands the layer bounds by `0.5 * scale` for every non-`Point` emission shape. That factor is correct only for `EmissionShapeType.Box`, whose `random_point_in_box` samples `[-0.5, 0.5]`; `random_point_in_sphere` / `random_point_on_sphere` are unit-**radius** (extent `[-1, 1]`), and the spawn path applies the full scale (`ParticleEmitter.js:1023`, `v3position.multiply(layer.scale)`).
495
+ - **Failure scenario**: a layer with `emissionShape = Sphere`, `emissionFrom = Volume`, `scale = (4,4,4)`, `particleSpeed = (0,0)`, `position = (0,0,0)`. Particles occupy a radius-4 ball, but `computePointBoundingBox` returns `[-2,-2,-2 .. 2,2,2]`. `computeEmissionBounds` pads that only by the sprite half-size, so `emissionBounds` misses half the emission volume. Before any particle exists — a freshly built emitter, or one whose particles have all just died — `bvh_leaf.bounds` is `emissionBounds` alone, so the emitter is dropped from `renderLayer.visibleSet`, `ParticularEngine.updateEmitter` flags it `Sleeping`, and it stops emitting while up to 2 world units of its volume are still on screen. The emitter pops in and out at screen edges.
496
+ - **Fix**: branch on `emissionShape` — full `scale` for `Sphere`, `0.5 * scale` for `Box`.
497
+ - **Confidence**: Confirmed
498
+
499
+ ### GFXT-57 Raising `emissionRate` releases a burst of negative-age particles
500
+ - **Severity**: MAJOR
501
+ - **File**: `engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js:891`
502
+ - **Defect**: `spawnLayerParticlesContinuous` seeds its accumulator with `-layer.timeSinceLastEmission` and assigns `initialAge = time`, assuming `timeSinceLastEmission <= emissionPeriod`. That invariant is re-established using the period in force at the *end of the previous call*, so any increase in `emissionRate` (a period shrink) leaves a carry-over larger than the new period. With `emissionRate === 0` it is unbounded: `emissionPeriod` is `Infinity`, the loop never runs, and `timeSinceLastEmission` grows by `timeDelta` every frame forever.
503
+ - **Failure scenario**: a torch layer at `emissionRate = 2` (period 0.5) reaches steady state with `timeSinceLastEmission ≈ 0.5`. Gameplay or the editor raises it to 200 — `editor/view/ecs/components/particles/ParticleLayerController.js:110` does exactly `layer.emissionRate = rate`. On the next 60 Hz frame (`timeDelta = 0.0167`, period 0.005) the loop runs from `time = -0.5` to `0.0117`, emitting roughly 102 particles in one frame instead of about 3. About 99 of them get `initialAge` between `-0.495` and `0`, so each is written at `spawnPos + velocity * negativeAge` — up to half a second of travel *behind* the emitter — and `SimulationStepFixedPhysics` will not retire them until their age climbs back through zero, giving each `deathAge + 0.495 s` of life. Holding `emissionRate = 0` for 10 s first (the `graphics/ecs/sprite/SpriteSystemPE.js:23` configuration) makes the carry-over 10 s and the burst about 2000 particles.
504
+ - **Fix**: clamp the carry-over when seeding — `let time = -min2(layer.timeSinceLastEmission, emissionPeriod);` — and skip the layer entirely when `emissionRate <= 0` rather than accumulating against an infinite period.
505
+ - **Confidence**: Confirmed
506
+
507
+ ### GFXT-58 `ParticleLayer` binary round-trip drops the simulation step list
508
+ - **Severity**: MINOR
509
+ - **File**: `engine/graphics/particles/particular/engine/emitter/ParticleLayer.js:358`
510
+ - **Defect**: `toBinaryBuffer`/`fromBinaryBuffer` write and read every layer field except `steps`, and unlike `fromJSON` (line 341) the binary reader has no fallback that installs the default `FixedPhysics` step.
511
+ - **Failure scenario**: `srcLayer.toBinaryBuffer(b); b.position = 0; dstLayer.fromBinaryBuffer(b);` leaves `dstLayer.steps.length === 0`. `ParticularEngine.advanceEmitter` then builds an empty `step_mask` for that layer, so `SimulationStepFixedPhysics` never runs: particles never integrate velocity, `PARTICLE_ATTRIBUTE_AGE` never advances, and `particles.remove(i)` is never reached. The layer emits frozen, immortal particles that accumulate in the pool for the emitter's lifetime. The existing round-trip spec (`ParticleLayer.spec.js:48`) asserts every other field individually and never looks at `steps`, and `hash()`/`equals()` also ignore `steps`, so neither a field test nor a comparison test catches it.
512
+ - **Fix**: write and read `steps` in this pair the way `ParticleEmitterSerializationAdapter` does (count, then per-step type and parameters), falling back to a single `FixedPhysics` step when the count is zero.
513
+ - **Confidence**: Confirmed
514
+
515
+ ---
516
+
517
+ ## geometry — MikkTSpace tangents and mesh optimization
518
+
519
+ ### GFXT-59 MikkT angle weighting clamps the corner cosine to `[0,1]`, flattening every obtuse angle to 90°
520
+ - **Severity**: MAJOR
521
+ - **File**: `engine/graphics/geometry/MikkT/EvalTspace.js:104`
522
+ - **Defect**: `const fCos = clamp01(v3_dot(...));` — the reference `mikktspace.c` clamps to `[-1, 1]` (`fCos = fCos > 1 ? 1 : (fCos < -1 ? -1 : fCos)`). Clamping the low end at 0 means `Math.acos` can never exceed 90°, so every obtuse corner is under-weighted in the per-vertex tangent average.
523
+ - **Failure scenario**: a vertex V with three incident triangles whose corner angles at V are 170°, 170° and 20° (a flat fan with neighbours at 0°, 170° and 340°), each with a different affine UV map. Correct weights are 170:170:20; the code produces 90:90:20. Measured against a reference re-implementation, the resulting tangent is rotated by **0.95°** and `fMagS` is off by **2.8%**. A milder 150/105/105 fan gives 0.76° / 0.8%. The error grows without bound as the per-triangle tangents diverge, and it applies to every smoothed vertex with an obtuse incident corner — fan and cap triangulations, decimated meshes, terrain — on the default 180° threshold path.
524
+ - **Fix**: use a `[-1, 1]` clamp, e.g. `clamp(v3_dot(...), -1, 1)`, and drop the `clamp01` import.
525
+ - **Confidence**: Confirmed (numerically measured against a reference re-implementation)
526
+
527
+ ### GFXT-60 `merge_by_material` skips an element after every non-merging splice
528
+ - **Severity**: MAJOR
529
+ - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:189`
530
+ - **Defect**: The loop splices the processed entry out of `undecided` while iterating with an incrementing `i`, but only compensates with `i--` when a merge actually happened (`set_indices.length > 1`). On the singleton path the array shrinks under the cursor, so the element that slides into slot `i` is skipped for the rest of the pass and is later re-scanned alone — it never gets compared against its remaining partners.
531
+ - **Failure scenario**: a `Group` with three meshes in order `[A(material m1), B(material m2), C(material m2)]` where B and C are structurally mergeable. Expected output is 2 meshes (A, and B+C); the actual output is **3** — B and C are emitted separately. Reordering the same three children to `[B, C, A]` yields the correct 2. Verified by executing the function.
532
+ - **Fix**: always `i--` after the splice loop (the element removed at index `i` is replaced by its successor), or restructure to repeatedly take `undecided[0]`.
533
+ - **Confidence**: Confirmed (executed)
534
+
535
+ ### GFXT-61 `merge_geometry_hierarchy` calls `getIndex().count` on possibly non-indexed geometry
536
+ - **Severity**: MAJOR
537
+ - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:168`
538
+ - **Defect**: The instancing-size heuristic does `const tri_count = (geo_0.getIndex().count) / 3;` without checking for a non-indexed geometry; `BufferGeometry.getIndex()` returns `null` when `index` is unset.
539
+ - **Failure scenario**: a `Group` with two `Mesh` nodes referencing the same non-indexed `BufferGeometry` — or two structurally identical non-indexed geometries, which `dedupe_geometry` collapses into one object earlier in the same function. `merge_geometry_hierarchy` throws `TypeError: Cannot read properties of null (reading 'count')`. Verified by executing the function.
540
+ - **Fix**: `const tri_count = (geo_0.index !== null ? geo_0.index.count : geo_0.attributes.position.count) / 3;`
541
+ - **Confidence**: Confirmed (executed)
542
+
543
+ ### GFXT-62 `merge_contexts` wraps a `null` merge result in a `Mesh`
544
+ - **Severity**: MAJOR
545
+ - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:132`
546
+ - **Defect**: The result of `mergeBufferGeometries` is assigned unconditionally. That function `console.error`s and returns `null` for several mismatches `equal_attribute_structure` does not screen — indexed vs non-indexed, `morphAttributes` key mismatch, `morphTargetsRelative` mismatch, interleaved attributes — and the null propagates into `new Mesh(null, material)`.
547
+ - **Failure scenario**: a `Group` with two meshes sharing one material, both carrying only a Float32 `position` attribute of itemSize 3, one indexed and one not. `equal_attribute_structure` returns true (it compares only names, itemSize, normalized and array constructor), so the pair is merged; `mergeBufferGeometries` logs its "make sure index attribute exists among all geometries" error and returns `null`; `context_to_mesh` then throws `TypeError: Cannot read properties of null (reading 'isBufferGeometry')` inside the `Mesh` constructor. Verified by executing the function.
548
+ - **Fix**: extend `equal_attribute_structure` to require `(a.index !== null) === (b.index !== null)`, matching `morphAttributes` keys and equal `morphTargetsRelative`; and in `merge_contexts`, fall back to returning the input contexts unmerged when `merged === null` rather than producing a null-geometry context.
549
+ - **Confidence**: Confirmed (executed)
550
+
551
+ ### GFXT-63 Merging bakes a mirrored transform without reversing winding
552
+ - **Severity**: MAJOR
553
+ - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:127`
554
+ - **Defect**: `merge_contexts` bakes each child's world matrix into vertex positions via `applyMatrix4` but never reverses triangle winding for a negative-determinant (mirrored) transform. Unmerged, three.js compensates at draw time — `WebGLRenderer` sets `frontFaceCW` from `object.matrixWorld.determinant() < 0` — but the merged mesh has an identity, positive-determinant matrix, so that compensation no longer applies.
555
+ - **Failure scenario**: a `Group` with two meshes sharing one geometry and material, the second with `scale.set(-1, 1, 1)`. After the merge the single output geometry's two triangles have measured signed areas `[+1, -1]`, i.e. the mirrored triangle is now clockwise. With three.js' default `side: FrontSide` the mirrored half is back-face culled and disappears; before the merge it rendered correctly.
556
+ - **Fix**: compute the determinant of `ctx.matrix` in the `contexts.map` callback and, when negative, reverse each triangle's winding on the clone after `applyMatrix4` (swap index elements `[i*3+1]` and `[i*3+2]`, or the corresponding vertex records when non-indexed).
557
+ - **Confidence**: Confirmed (measured signed areas)
558
+
559
+ ### GFXT-64 `geometry_optimize_uv_tension` produces all-NaN UVs from a single collapsed edge
560
+ - **Severity**: MAJOR
561
+ - **File**: `engine/graphics/geometry/optimization/geometry_optimize_uv_tension.js:67`
562
+ - **Defect**: The neighbour weight is `1 / edge.lengthSqr` with no zero-length handling. A single pair of coincident positions joined by an edge yields `Infinity` weights, and `Infinity / Infinity` gives `NaN` UVs, which then propagate across the whole mesh over the remaining smoothing passes.
563
+ - **Failure scenario**: a closed octahedron — so there are no boundary edges and nothing is skipped by the `query_edge_is_boundary` guard — with vertex 0 moved onto pole vertex 4, collapsing edge (0,4). That is one degenerate triangle, a routine artifact of imported or stitched meshes. With `steps = 3`, **all 12 UV components come back `NaN`** (measured), destroying the texture coordinates of the entire mesh rather than just the degenerate corner.
564
+ - **Fix**: skip any neighbour whose `edge.lengthSqr` is zero (contributing to neither `sum_*` nor `weight_sum`), and skip the vertex entirely when `weight_sum` ends up zero.
565
+ - **Confidence**: Confirmed (executed: 12 of 12 UV values NaN)
566
+
567
+ ### GFXT-65 MikkT edge table is a `Float32Array` holding packed integer indices
568
+ - **Severity**: MINOR
569
+ - **File**: `engine/graphics/geometry/MikkT/InitTriInfo.js:180` (consumed by `BuildNeighborsFast.js:25-26,73-74`)
570
+ - **Defect**: `const pEdges = new Float32Array(iNrTrianglesIn * 3 * 3);` but channels 0 and 1 store `MakeIndex`-encoded integers (`(face << 2) | corner`), not floats. Float32 represents integers exactly only up to 2^24. The reference C uses `int` throughout.
571
+ - **Failure scenario**: a mesh above 4,194,304 triangles — the point at which `(f << 2) | i` exceeds 2^24, e.g. a 5M-triangle scan. `MakeIndex(4500000, 3) = 18000003` is stored as `18000004`, colliding with `MakeIndex(4500001, 0)`. `GetEdge`'s `index_0 === i0_in` tests then match the wrong corner, `FaceNeighbors` links unrelated triangles, and tangent groups merge across disconnected surface regions — visibly wrong normal mapping on the affected faces.
572
+ - **Fix**: allocate `pEdges` as an `Int32Array`, and likewise the `temp_edge_A`/`temp_edge_B` scratch in `BuildNeighborsFast.js:73-74`.
573
+ - **Confidence**: Likely (arithmetic verified; not reproduced at that mesh size)
574
+
575
+ ### GFXT-66 `m_getTexCoord` copies 3 components out of a 2-component UV buffer
576
+ - **Severity**: MINOR
577
+ - **File**: `engine/graphics/geometry/MikkT/m_getTexCoord.js:15`
578
+ - **Defect**: `array_copy(pContext.geometry_buffer_vertex_uv, vertex_index * 2, fvTexcOut, 0, 3)` reads three elements from a buffer with a stride of two, so the third output component is the next vertex's U — or reads past the end for the last vertex.
579
+ - **Failure scenario**: the in-module caller `GetTexCoord` immediately overwrites `res[2] = 1`, so the MikkT pipeline itself is unaffected. But `m_getTexCoord` is an exported public function: calling it directly fills `out[2]` with `uv[vertex_index * 2 + 2]` — the neighbouring vertex's U coordinate — and with `NaN` when `vertex_index` is the last vertex.
580
+ - **Fix**: change the copy length from `3` to `2`.
581
+ - **Confidence**: Confirmed
582
+
583
+ ### GFXT-67 `merge_geometry_hierarchy` reads stale local matrices
584
+ - **Severity**: MINOR
585
+ - **File**: `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:60`
586
+ - **Defect**: `compute_transform_matrix` reads `el.matrix` directly and `merge_geometry_hierarchy` never calls `updateMatrixWorld`. Under three.js' default `matrixAutoUpdate`, `object.matrix` is synced from `position`/`quaternion`/`scale` only during a render pass, so a hierarchy assembled in code still has identity local matrices at merge time.
587
+ - **Failure scenario**: `const m = new Mesh(geo, mat); m.position.set(100, 0, 0); group.add(m); merge_geometry_hierarchy(group)` puts the output mesh at `[0, 0, 0]` — the 100-unit offset is silently discarded (measured). Loader-produced scenes happen to work because `GLTFLoader` writes `matrix` itself.
588
+ - **Fix**: call `input.updateMatrixWorld(true)` at the top of `merge_geometry_hierarchy`, before the `traverse`.
589
+ - **Confidence**: Confirmed (executed)
590
+
591
+ ---
592
+
593
+ ## geometry/ribbon, geometry/instancing, texture/reflection, texture/cubemap
594
+
595
+ ### GFXT-68 `Ribbon.positionHead` places the two tip vertices on opposite sides in its two branches
596
+ - **Severity**: CRITICAL
597
+ - **File**: `engine/graphics/geometry/ribbon/Ribbon.js:143`
598
+ - **Defect**: The degenerate branch derives `C` from `A` and `D` from `B`, preserving each vertex's side of the ribbon. The normal branch derives the width axis as `vRelativePosition × normal`, which for the ribbon's own geometry normal `(0,0,1)` points toward `B`'s side, so `C` and `D` come out swapped and the head quad is twisted into a bowtie. The two branches contradict each other regardless of the caller's sign convention for `normal`.
599
+ - **Failure scenario**: `new Ribbon(2, 1)` gives head `quads[1]` with `A = (0, 0.5, 0)`, `B = (0, -0.5, 0)`, and `PlaneBufferGeometry` normals `(0,0,1)`. `positionHead(new Vector3(2,0,0), new Vector3(0,0,1), 1)` gives `vCross = (2,0,0) × (0,0,1) = (0,-2,0)` → `C = (2,-0.5,0)` (B's side) and `D = (2,0.5,0)` (A's side). `positionHead(new Vector3(0,0,0), new Vector3(0,0,1), 1)` takes the degenerate path and gives `C = (0,0.5,0)` (A's side), `D = (0,-0.5,0)` (B's side) — the same object and the same `normal`, opposite assignment. On the first (common) path the `A→C` and `B→D` edges cross, so triangle `(A,B,C)` stays CCW while `(B,D,C)` flips to CW: with a default `FrontSide` material half the head segment vanishes and the ribbon visibly pinches and flips at the tip every frame.
600
+ - **Fix**: negate the cross so it agrees with the degenerate branch (which is unambiguously correct, since it reuses the existing A/B directions): `vCross.copy(normal).cross(vRelativePosition);`, keeping `vC = +vCross`, `vD = -vCross`.
601
+ - **Confidence**: Confirmed
602
+
603
+ ### GFXT-69 `.exr` environment maps never load — `EXRLoader` cannot produce `UnsignedByteType`
604
+ - **Severity**: CRITICAL
605
+ - **File**: `engine/graphics/texture/cubemap/load_environment_map.js:57`
606
+ - **Defect**: `new EXRLoader().setDataType(UnsignedByteType)` requests an output type three r136's `EXRLoader` does not implement. Its `EXRDecoder` starts with `getter: null, inputSize: null`, both `switch (outputType)` blocks that assign them have only `case FloatType` / `case HalfFloatType`, and the buffer-allocation switch falls through to `default: console.error('THREE.EXRLoader: unsupported type: ', outputType); break;` without returning (verified at `node_modules/three/examples/jsm/loaders/EXRLoader.js:2128,2133,2145,2150,2175,2184,2193`).
607
+ - **Failure scenario**: `await load_environment_map({ path: 'data/textures/env/sky.exr', renderer })` — `setupDecoder` leaves `getter = null` and `byteArray = undefined`, then `parse` reaches `EXRDecoder.byteArray[outIndex] = EXRDecoder.getter(viewer, tmpOffset)` and throws `TypeError: EXRDecoder.getter is not a function`. `FileLoader` routes that to `onError`, so the promise rejects and no environment map is installed — for every `.exr` input, on every call.
608
+ - **Fix**: `.setDataType(HalfFloatType)` (or `FloatType`), imported from `three` in place of `UnsignedByteType`.
609
+ - **Confidence**: Confirmed
610
+
611
+ ### GFXT-70 `Ribbon.traverseEdges` seeds from `quads[0]` instead of the tail
612
+ - **Severity**: MAJOR
613
+ - **File**: `engine/graphics/geometry/ribbon/Ribbon.js:174`
614
+ - **Defect**: The traversal seeds from `quads[0]` — the *array* slot — instead of `this.__tail`. `rotate()` deliberately does not reorder `this.quads`; it only relinks `next`/`previous` and moves `__head`/`__tail`. After any rotation the seed edge is an interior edge, which the loop then visits a second time, while the real tail edge is never visited.
615
+ - **Failure scenario**: `const r = createRibbon(3); rotateRibbon(r);` leaves `__tail = quads[1]`, `__head = quads[0]`, `quads[0].previous = quads[2]`. The initialisation `createRibbon` itself performs — `r.traverseEdges((a,b) => { off[a] = 1; off[b] = -1; })` — then visits vertex pairs `(3,7)`, `(0,4)`, `(3,7)`, `(2,6)`. Vertices 1 and 5, the true tail edge, are never touched, so their `off` attribute stays 0 and the shader collapses the ribbon to zero width at the tail segment, while `(3,7)` is written twice.
616
+ - **Fix**: `let q = this.__tail;`
617
+ - **Confidence**: Confirmed
618
+
619
+ ### GFXT-71 `Ribbon.traverseEdges` emits index 0 twice, never the last index, and runs the ramp backwards
620
+ - **Severity**: MAJOR
621
+ - **File**: `engine/graphics/geometry/ribbon/Ribbon.js:181`
622
+ - **Defect**: The seed callback passes index `0`, and the loop then starts its own counter at `0` as well, so index 0 is delivered twice and the final index is never delivered. `maxIndex` is passed as `numQuads + 1` while the loop only reaches `numQuads - 1`. The loop also walks head → tail with `i` ascending while the seed call — the tail edge — is also given `0`, so the parameter runs backwards relative to edge order.
623
+ - **Failure scenario**: `new Ribbon(3, 1).traverseLerpEdges(0, 1, cb)` computes `value = 0 + (index / maxIndex) * 1` and yields `0, 0, 0.25, 0.5` for the four edges. The intended `0 → 1` ramp never reaches 1, is clipped to `(N-1)/(N+1)`, and is inverted — the head edge receives 0 while the edge nearest the tail receives the largest value. For `N = 64` the head gets 0 and the maximum is 0.969, so a trail fade runs the wrong way and never reaches full opacity.
624
+ - **Fix**: use a single monotonic counter over `numQuads + 1` edges in tail → head order with `maxIndex = numQuads`, so `index / maxIndex` spans `[0, 1]`.
625
+ - **Confidence**: Confirmed
626
+
627
+ ### GFXT-72 `Ribbon.moveToPoint` does not flag the position attribute for re-upload
628
+ - **Severity**: MAJOR
629
+ - **File**: `engine/graphics/geometry/ribbon/Ribbon.js:97`
630
+ - **Defect**: The method writes the raw position array but never sets `position.needsUpdate = true`. Every other position write in the module (`Quad.setAttributePosition`) does set it, so this is an omission rather than a module convention.
631
+ - **Failure scenario**: `const r = createRibbon(16); scene.add(new Mesh(r.geometry, mat)); renderer.render(...)` uploads the positions and caches `attribute.version` in `WebGLAttributes`. Respawning the trail at a new location with `r.moveToPoint(new Vector3(50, 0, 0)); renderer.render(...)` then hits `data.version === attribute.version` in `WebGLAttributes.update`, which skips the upload — the frame still draws the full 16-segment trail at the old location instead of a collapsed ribbon at `(50,0,0)`.
632
+ - **Fix**: add `position.needsUpdate = true;` after the loop.
633
+ - **Confidence**: Confirmed
634
+
635
+ ### GFXT-73 First-time colour attribute creation leaves existing instances fully transparent
636
+ - **Severity**: MINOR
637
+ - **File**: `engine/graphics/geometry/instancing/InstancedMeshGroup.js:575`
638
+ - **Defect**: When the colour attribute is created for the first time on a group that already has live instances, only `[count, capacity)` is filled with `255` — slots `[0, count)` stay at the `Uint8Array` default of `0`, because there is no old array to copy from. (Distinct from the known stale-colour-on-regrow issue: here the colours were never written at all.)
639
+ - **Failure scenario**: build a group with `use_color = false`, add 50 instances with transforms, then set `use_color = true` and `setCapacity(128)` to trigger the first build with colours. Slots 0..49 hold `(0,0,0,0)`; `rewriteMaterial` defines `USE_COLOR_ALPHA` so `diffuseColor *= vColor` with alpha 0 — all 50 live instances render fully transparent while instances added afterwards render correctly.
640
+ - **Fix**: fill the whole array when there was no previous one — `newColorArray.fill(255, this.__attributeColor === null ? 0 : this.count * 4)`.
641
+ - **Confidence**: Confirmed
642
+
643
+ ### GFXT-74 Reflection convolution takes its channel count from the input, not the output
644
+ - **Severity**: MINOR
645
+ - **File**: `engine/graphics/texture/reflection/convolve_equirectangular_reflection.js:56` (caller at `:118`)
646
+ - **Defect**: `convolve_equirectangular_reflection_direction` decides how many channels to write into `result` from `input.itemSize`, but the caller sizes `result` from `output.itemSize` (`const texel = new Float32Array(output.itemSize);`). When they differ, the trailing channels of every output texel are left untouched.
647
+ - **Failure scenario**: `convolve_equirectangular_reflection_level(rgbSky /* itemSize 3 */, rgbaMip /* itemSize 4 */, 0.3, 32)` allocates a fresh `Float32Array(4)` per texel whose element 3 is never assigned, so `output.write` stores alpha 0 for every texel and the prefiltered mip comes out fully transparent.
648
+ - **Fix**: take the channel count from the destination — pass an explicit `channel_count`, or use `result.length - result_offset` — and read `_radiance[3]` only when the input actually has a fourth channel.
649
+ - **Confidence**: Confirmed
650
+
651
+ ### GFXT-75 Cubemap face-array branch compares the file extension case-sensitively
652
+ - **Severity**: MINOR
653
+ - **File**: `engine/graphics/texture/cubemap/load_environment_map.js:104`
654
+ - **Defect**: The six-face array branch does `const ext = computeFileExtension(path[0]); if (ext === 'hdr')`, while the single-path branch at line 116 lowercases first. An uppercase face set silently takes the LDR `CubeTextureLoader` path.
655
+ - **Failure scenario**: `load_and_set_cubemap_v0(graphics, 'data/textures/cubemaps/sky/', '.HDR')` has `buildCubeURLs` yield `.../posx.HDR`; `ext === 'HDR'` fails the check, so `CubeTextureLoader` (`ImageLoader`) is used, the browser cannot decode a Radiance file as an image, and the promise rejects instead of loading the HDR cubemap.
656
+ - **Fix**: `const ext = computeFileExtension(path[0]).toLowerCase();`
657
+ - **Confidence**: Confirmed
658
+
659
+ ### GFXT-76 `geometry_copy` drops `drawRange`
660
+ - **Severity**: MINOR
661
+ - **File**: `engine/graphics/geometry/instancing/geometry_copy.js:6`
662
+ - **Defect**: The function copies `index` and `attributes` but not `drawRange`, so the target reverts to the `{start: 0, count: Infinity}` default and renders the source's entire index buffer. (`groups` and `morphAttributes` are dropped too, but with a single non-array material three ignores `groups`, so only `drawRange` produces wrong output.)
663
+ - **Failure scenario**: a pooled geometry that keeps a fixed-size buffer and publishes its live element count via `setDrawRange` — the pattern at `particles/particular/engine/emitter/ParticlePool.js:158`, `geometry.setDrawRange(0, occupancy.size())` — passed to `InstancedMeshGroup.setGeometry` or `geometry_copy` with 120 live of 10000 allocated elements renders all 10000, drawing 9880 stale/uninitialised quads.
664
+ - **Fix**: also copy `target.setDrawRange(source.drawRange.start, source.drawRange.count)`, plus `morphAttributes` and `morphTargetsRelative`.
665
+ - **Confidence**: Confirmed
666
+
667
+ ---
668
+
669
+ ## Status of the pre-existing "already known" list
670
+
671
+ | Item | Status |
672
+ | --- | --- |
673
+ | `sampler2d_scale_down_lanczos` `rcp_ratio_y = 2/ratio_x` | FIXED — now `2 / ratio_y` (`:93`) |
674
+ | `sampler2d_scale_down_linear` non-integer-ratio mis-index | OPEN — `sampleOffsetX = x * sW` is still used as a texel index, and `for (i = sW; i > 0; i--)` still over-counts (`:50-58`) |
675
+ | `sampler2d_copy_with_margins` width used for a vertical bound | FIXED — all vertical bounds now use `destination.height`. A different, still-open bug in the same file is filed as GFXT-30 |
676
+ | `ParticleGroup.deleteIndices` bare `.sort()` | FIXED — now `indices.sort((a, b) => a - b)`. A different, still-open bug in the same function is filed as GFXT-51 |
677
+ | `ParticlePool` `updateRange.count` missing `* itemSize` | FIXED — both sites multiply (`:177`, `:507`) |
678
+ | `SimulationStepCurlNoiseVelocity` shared `this.time` | OPEN — still `this.time += this.timeDelta` on the shared step object |
679
+ | `ManagedTextureAtlas` serving atlas-evicted patches from a TTL-∞ cache | OPEN — `LoadingCache` is still constructed with no TTL |
680
+ | `CachingTextureAtlas.__evict_all` leaving index 0 | FIXED — loop runs `i >= 0` |
681
+ | `ParticleEmitter` hash/equals flag disagreement | FIXED — both now mask with `SERIALIZABLE_FLAGS`. A different issue with that mask is filed as GFXT-55 |
682
+ | `ShaderManager` graveyard leak | FIXED — `cleanup()` evicts from both maps and releases the material reference |
683
+ | `equal_attribute_structure` returning truthy `-1` | FIXED — now `indexOf(...) === -1` guarded. Other still-open bugs in that function are filed as GFXT-62 |
684
+ | `InstancedMeshGroup.build` stale per-instance colours on shrink+regrow | FIXED — `newColorArray.fill(255, this.count * 4)`. A different first-build case is filed as GFXT-73 |
685
+ | MikkT `BuildNeighborsFast` edge-neighbour pairing | OPEN — `i1_B` and `edgenum_B` both read `temp_edge_B[2]` (`:117-118`) |
686
+ | MikkT `GenerateSharedVerticesIndexList` radix precedence | OPEN — `pass * 257 + (_comp_0[i] >> shift) & 0xff` at `:79`, versus the correctly parenthesised `:59` |
687
+ | `bvh32_geometry_nearest` duplicated denormalize block | FIXED — one block remains |
688
+ | `ensureGeometryBoundingSphere` returning stale null | FIXED — assigns to `geometry.boundingSphere` and returns it |
689
+ | `AtlasLookupTexture` writing `size.y` into the width channel | FIXED — writes `position.x, position.y, size.x, size.y` |
690
+