@woosh/meep-engine 3.29.0 → 3.30.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 (226) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/build/bundle-worker-terrain.js +1 -1
  3. package/package.json +1 -1
  4. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  5. package/src/core/binary/BinaryBuffer.js +14 -2
  6. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  7. package/src/core/bvh2/bvh3/BVH.js +14 -1
  8. package/src/core/cache/FrequencySketch.d.ts +3 -1
  9. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  10. package/src/core/cache/FrequencySketch.js +10 -4
  11. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -1
  12. package/src/core/cache/wtinylfu/CacheWTinylfu.js +24 -2
  13. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts +20 -0
  14. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts.map +1 -1
  15. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +69 -4
  16. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts +48 -18
  17. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  18. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +262 -70
  19. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts +25 -0
  20. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts.map +1 -0
  21. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.js +228 -0
  22. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts +13 -0
  23. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts.map +1 -1
  24. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.js +10 -128
  25. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.d.ts.map +1 -1
  26. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.js +11 -1
  27. package/src/core/graph/mn_graph_collapse_weighted_edge.js +6 -6
  28. package/src/core/math/lookup/ParameterLookupTable.d.ts.map +1 -1
  29. package/src/core/math/lookup/ParameterLookupTable.js +10 -5
  30. package/src/core/math/noise/sdnoise.d.ts.map +1 -1
  31. package/src/core/math/noise/sdnoise.js +16 -10
  32. package/src/core/model/object/ImmutableObjectPool.d.ts.map +1 -1
  33. package/src/core/model/object/ImmutableObjectPool.js +17 -7
  34. package/src/core/process/executor/ConcurrentExecutor.d.ts +1 -5
  35. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  36. package/src/core/process/executor/ConcurrentExecutor.js +95 -1
  37. package/src/core/process/task/task_find_dependency_cycle.d.ts +17 -0
  38. package/src/core/process/task/task_find_dependency_cycle.d.ts.map +1 -0
  39. package/src/core/process/task/task_find_dependency_cycle.js +81 -0
  40. package/src/core/wfc/WFCCellChoice.d.ts +17 -0
  41. package/src/core/wfc/WFCCellChoice.d.ts.map +1 -0
  42. package/src/core/wfc/WFCCellChoice.js +36 -0
  43. package/src/core/wfc/WFCLattice.d.ts +105 -0
  44. package/src/core/wfc/WFCLattice.d.ts.map +1 -0
  45. package/src/core/wfc/WFCLattice.js +235 -0
  46. package/src/core/wfc/WFCRuleTable.d.ts +172 -0
  47. package/src/core/wfc/WFCRuleTable.d.ts.map +1 -0
  48. package/src/core/wfc/WFCRuleTable.js +334 -0
  49. package/src/core/wfc/WFCState.d.ts +273 -0
  50. package/src/core/wfc/WFCState.d.ts.map +1 -0
  51. package/src/core/wfc/WFCState.js +552 -0
  52. package/src/core/wfc/WFCStatus.d.ts +11 -0
  53. package/src/core/wfc/WFCStatus.d.ts.map +1 -0
  54. package/src/core/wfc/WFCStatus.js +28 -0
  55. package/src/core/wfc/WFCTileWeights.d.ts +48 -0
  56. package/src/core/wfc/WFCTileWeights.d.ts.map +1 -0
  57. package/src/core/wfc/WFCTileWeights.js +86 -0
  58. package/src/core/wfc/WFC_UNRESOLVED.d.ts +12 -0
  59. package/src/core/wfc/WFC_UNRESOLVED.d.ts.map +1 -0
  60. package/src/core/wfc/WFC_UNRESOLVED.js +11 -0
  61. package/src/core/wfc/wfc_estimate_bytes.d.ts +24 -0
  62. package/src/core/wfc/wfc_estimate_bytes.d.ts.map +1 -0
  63. package/src/core/wfc/wfc_estimate_bytes.js +47 -0
  64. package/src/core/wfc/wfc_rule_table_tile_components.d.ts +27 -0
  65. package/src/core/wfc/wfc_rule_table_tile_components.d.ts.map +1 -0
  66. package/src/core/wfc/wfc_rule_table_tile_components.js +80 -0
  67. package/src/core/wfc/wfc_solve.d.ts +21 -0
  68. package/src/core/wfc/wfc_solve.d.ts.map +1 -0
  69. package/src/core/wfc/wfc_solve.js +154 -0
  70. package/src/core/wfc/wfc_state_apply_boundary.d.ts +18 -0
  71. package/src/core/wfc/wfc_state_apply_boundary.d.ts.map +1 -0
  72. package/src/core/wfc/wfc_state_apply_boundary.js +61 -0
  73. package/src/core/wfc/wfc_state_assign.d.ts +11 -0
  74. package/src/core/wfc/wfc_state_assign.d.ts.map +1 -0
  75. package/src/core/wfc/wfc_state_assign.js +36 -0
  76. package/src/core/wfc/wfc_state_ban_bits.d.ts +17 -0
  77. package/src/core/wfc/wfc_state_ban_bits.d.ts.map +1 -0
  78. package/src/core/wfc/wfc_state_ban_bits.js +81 -0
  79. package/src/core/wfc/wfc_state_collapse.d.ts +12 -0
  80. package/src/core/wfc/wfc_state_collapse.d.ts.map +1 -0
  81. package/src/core/wfc/wfc_state_collapse.js +73 -0
  82. package/src/core/wfc/wfc_state_flush_dirty.d.ts +12 -0
  83. package/src/core/wfc/wfc_state_flush_dirty.d.ts.map +1 -0
  84. package/src/core/wfc/wfc_state_flush_dirty.js +39 -0
  85. package/src/core/wfc/wfc_state_propagate.d.ts +14 -0
  86. package/src/core/wfc/wfc_state_propagate.d.ts.map +1 -0
  87. package/src/core/wfc/wfc_state_propagate.js +155 -0
  88. package/src/core/wfc/wfc_state_read.d.ts +11 -0
  89. package/src/core/wfc/wfc_state_read.d.ts.map +1 -0
  90. package/src/core/wfc/wfc_state_read.js +38 -0
  91. package/src/core/wfc/wfc_state_restrict.d.ts +18 -0
  92. package/src/core/wfc/wfc_state_restrict.d.ts.map +1 -0
  93. package/src/core/wfc/wfc_state_restrict.js +39 -0
  94. package/src/core/wfc/wfc_state_set_eligible.d.ts +24 -0
  95. package/src/core/wfc/wfc_state_set_eligible.d.ts.map +1 -0
  96. package/src/core/wfc/wfc_state_set_eligible.js +49 -0
  97. package/src/core/wfc/wfc_state_undo_to.d.ts +13 -0
  98. package/src/core/wfc/wfc_state_undo_to.d.ts.map +1 -0
  99. package/src/core/wfc/wfc_state_undo_to.js +84 -0
  100. package/src/core/wfc/wfc_words_per_cell.d.ts +11 -0
  101. package/src/core/wfc/wfc_words_per_cell.d.ts.map +1 -0
  102. package/src/core/wfc/wfc_words_per_cell.js +16 -0
  103. package/src/engine/ecs/EntityComponentDataset.d.ts +8 -0
  104. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  105. package/src/engine/ecs/EntityComponentDataset.js +37 -14
  106. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts +8 -1
  107. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts.map +1 -1
  108. package/src/engine/ecs/fow/FogOfWarRevealerSystem.js +37 -2
  109. package/src/engine/ecs/guid/UUID.d.ts.map +1 -1
  110. package/src/engine/ecs/guid/UUID.js +45 -17
  111. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts.map +1 -1
  112. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +3 -1
  113. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
  114. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +366 -360
  115. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.d.ts.map +1 -1
  116. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.js +24 -10
  117. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts.map +1 -1
  118. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.js +24 -5
  119. package/src/engine/interpolation/Interpoland.d.ts +7 -1
  120. package/src/engine/interpolation/Interpoland.d.ts.map +1 -1
  121. package/src/engine/interpolation/Interpoland.js +8 -1
  122. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -1
  123. package/src/engine/interpolation/InterpolationSystem.js +3 -0
  124. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +5 -3
  125. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -1
  126. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +14 -3
  127. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -1
  128. package/src/engine/interpolation/pose_interpoland.js +2 -0
  129. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  130. package/src/engine/network/orchestrator/NetworkPeer.js +15 -10
  131. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  132. package/src/engine/physics/cloth/ecs/ClothWorld.js +17 -0
  133. package/src/engine/physics/mls-mpm/MLS_MPM.d.ts.map +1 -1
  134. package/src/engine/physics/mls-mpm/MLS_MPM.js +14 -0
  135. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts +2 -1
  136. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts.map +1 -1
  137. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +1 -1
  138. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts +1 -0
  139. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
  140. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +1 -0
  141. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -1
  142. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts +2 -1
  143. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -1
  144. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +12 -3
  145. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts +1 -0
  146. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -1
  147. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +1 -0
  148. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
  149. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  150. package/src/engine/sound/sopra/runtime/EventInstance.js +4 -1
  151. package/src/format/image/avif/av1/decode/decode_coefficients.d.ts.map +1 -1
  152. package/src/format/image/avif/av1/decode/decode_coefficients.js +248 -259
  153. package/src/format/image/avif/av1/decode/read_golomb.d.ts +21 -0
  154. package/src/format/image/avif/av1/decode/read_golomb.d.ts.map +1 -0
  155. package/src/format/image/avif/av1/decode/read_golomb.js +49 -0
  156. package/src/format/image/avif/av1/filter/loop_restoration.js +461 -451
  157. package/src/format/image/avif/av1/util/round2.d.ts +19 -2
  158. package/src/format/image/avif/av1/util/round2.d.ts.map +1 -1
  159. package/src/format/image/avif/av1/util/round2.js +51 -27
  160. package/src/format/image/avif/heif/parse_avif_file.js +24 -0
  161. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
  162. package/src/shade/renderer/buffer/table/GPUTypedTable.js +14 -0
  163. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +46 -3
  164. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +91 -1
  165. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +0 -13
  166. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  167. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +0 -15
  168. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +5 -5
  169. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts +11 -1
  170. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts.map +1 -1
  171. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.js +11 -1
  172. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +18 -9
  173. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -1
  174. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.js +20 -26
  175. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +1 -3
  176. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -1
  177. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.js +2 -5
  178. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +1 -1
  179. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
  180. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +14 -1
  181. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.d.ts.map +1 -1
  182. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.js +12 -3
  183. package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.d.ts.map +1 -1
  184. package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.js +7 -1
  185. package/src/shade/renderer/lightmap/GPULightMap.d.ts.map +1 -1
  186. package/src/shade/renderer/lightmap/GPULightMap.js +349 -336
  187. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  188. package/src/shade/renderer/loader/gltf/load_gltf.js +1137 -1128
  189. package/src/shade/renderer/particles/DESIGN.md +14 -5
  190. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +25 -10
  191. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  192. package/src/shade/renderer/particles/GPUParticleSystem.js +182 -38
  193. package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts.map +1 -1
  194. package/src/shade/renderer/particles/graph/groups/simplex_reference.js +16 -10
  195. package/src/shade/renderer/particles/graph_particles.d.ts +4 -2
  196. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  197. package/src/shade/renderer/particles/graph_particles.js +8 -2
  198. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  199. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +8 -2
  200. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
  201. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +10 -4
  202. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts +31 -0
  203. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts.map +1 -0
  204. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.js +62 -0
  205. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +13 -0
  206. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  207. package/src/shade/renderer/particles/vm/ParticleVMReference.js +95 -17
  208. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts +21 -0
  209. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts.map +1 -0
  210. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js +28 -0
  211. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  212. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +19 -2
  213. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
  214. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +19 -2
  215. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  216. package/src/shade/renderer/shader/type/WebGPUStruct.js +12 -0
  217. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts +8 -2
  218. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts.map +1 -1
  219. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +41 -6
  220. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2383 -2356
  221. package/src/view/tooltip/DomTooltipManager.d.ts +7 -0
  222. package/src/view/tooltip/DomTooltipManager.d.ts.map +1 -1
  223. package/src/view/tooltip/DomTooltipManager.js +22 -0
  224. package/src/view/tooltip/DomTooltipObserver.d.ts.map +1 -1
  225. package/src/view/tooltip/DomTooltipObserver.js +13 -3
  226. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.js +0 -45
@@ -9,6 +9,7 @@ import { quadric_attribute_reduce } from "../../../../quadric/quadric_attribute_
9
9
  import { quadric_attribute_size } from "../../../../quadric/quadric_attribute_size.js";
10
10
  import { quadric_attribute_solve } from "../../../../quadric/quadric_attribute_solve.js";
11
11
  import { NULL_POINTER } from "../BinaryTopology.js";
12
+ import { bt_edge_collapse_fold_limit } from "../query/bt_edge_collapse_fold_limit.js";
12
13
  import { bt_edge_collapse_would_fold } from "../query/bt_edge_collapse_would_fold.js";
13
14
  import { bt_loop_kill } from "./bt_loop_kill.js";
14
15
  import { bt_mesh_compute_vertex_quadrics } from "./bt_mesh_compute_vertex_quadrics.js";
@@ -24,6 +25,28 @@ const scratch_coord_a = new Float32Array(3);
24
25
  const scratch_coord_b = new Float32Array(3);
25
26
  const scratch_target = new Float32Array(3);
26
27
  const scratch_combined_q = new Quadric3();
28
+ const scratch_anchor = new Float32Array(3);
29
+ const scratch_candidate = new Float32Array(3);
30
+ const scratch_placement = new Float32Array(3);
31
+
32
+ /**
33
+ * How much of the way to the fold limit a constrained placement is allowed to travel.
34
+ *
35
+ * The limit itself is where a surviving triangle reaches *zero* area, which is refused along with an
36
+ * outright flip and for the same reason: a zero-area triangle has no normal, so the quadrics built
37
+ * from it afterwards are meaningless and every later collapse around it is priced off nothing. So
38
+ * stopping short of the limit is a statement about the mesh rather than about arithmetic.
39
+ *
40
+ * Half, because the feasible interval along the segment is `[0, limit)` and its midpoint is the
41
+ * placement furthest from both ends of it — from the anchor, which is the placement this was trying
42
+ * to improve on, and from the degeneracy. Nothing is given up by stopping there: swept against 0.9
43
+ * and 0.99 over Zorah's 479k-triangle `mesh0[0]`, all three build the *same* 34 levels to the *same*
44
+ * root error of `1.0445e+3`, and differ only in cluster count, by 0.3% and in no particular
45
+ * direction (11,511 / 11,498 / 11,532). The choice is free on everything that was measured, so it
46
+ * goes to the end that cannot produce a sliver.
47
+ * @type {number}
48
+ */
49
+ const FOLD_LIMIT_TRAVEL = 0.5;
27
50
 
28
51
  /**
29
52
  * The pair's summed attribute quadric, and its reduction. Grown to whatever channel count the
@@ -104,6 +127,143 @@ function count_edge_faces(mesh, edge_id) {
104
127
  return count;
105
128
  }
106
129
 
130
+ /**
131
+ * What a collapse of this pair costs with its surviving vertex at `(x, y, z)`.
132
+ *
133
+ * The two geometric quadrics say how far the placement is from the surface the pair was cut out of;
134
+ * the reduced attribute quadric, when there is one, says what the channels still cost once each has
135
+ * been given its best value there — in units of length, because the weights put them there.
136
+ *
137
+ * It is exactly the function the placement minimizes — quadric evaluation is linear in the quadric,
138
+ * so the sum `optimize` was handed evaluates to the sum of the two terms below — which is why
139
+ * constraining the placement can only ever raise this, never lower it. Read at the placement
140
+ * actually chosen, so the queue's order is a statement about the collapses that will happen rather
141
+ * than about ones that will not.
142
+ *
143
+ * @param {Quadric3} q1
144
+ * @param {Quadric3} q2
145
+ * @param {boolean} has_attributes whether {@link scratch_attribute_reduced} holds this pair's
146
+ * @param {number} x
147
+ * @param {number} y
148
+ * @param {number} z
149
+ * @returns {number}
150
+ */
151
+ function placement_cost(q1, q2, has_attributes, x, y, z) {
152
+ // Cost can drift slightly negative for near-zero values due to fp noise.
153
+ const geometric = Math.abs(q1.evaluate(x, y, z) + q2.evaluate(x, y, z));
154
+
155
+ if (!has_attributes) {
156
+ return geometric;
157
+ }
158
+
159
+ return geometric + Math.abs(quadric3_evaluate(scratch_attribute_reduced, 0, x, y, z));
160
+ }
161
+
162
+ /**
163
+ * Move `out_target` to the cheapest placement near it that turns no surviving triangle over, or
164
+ * report that the pair has none.
165
+ *
166
+ * **A placement that folds is not an impossible collapse, and this is where the two stop being
167
+ * conflated.** What the quadric hands back is the unconstrained minimum of a convex cost. Keeping
168
+ * every surviving triangle facing the way it did is a *constraint* on that minimum — a linear one,
169
+ * as {@link bt_edge_collapse_fold_limit} derives, so the placements that satisfy it are a convex set
170
+ * and the constrained minimum is the thing this should be choosing. Testing the unconstrained
171
+ * answer and abandoning the collapse when it fails is the classic way of getting that wrong, and it
172
+ * is expensive: measured over one 479k-triangle Zorah primitive, 12% to 15% of every candidate
173
+ * collapse scored was discarded that way — and it is not a tail, it is the whole build, which is how
174
+ * a level of the DAG that could still have been halved came to be reported as having no coarser
175
+ * form at all.
176
+ *
177
+ * The constrained minimum lies on the boundary whenever the unconstrained one is outside, and the
178
+ * boundary is reached along a segment: the cost is convex with its minimum at `out_target`, so along
179
+ * a segment ending there it falls monotonically, and the best placement on that segment is simply
180
+ * the furthest one the fold limit allows. That makes this exact in one dimension rather than a
181
+ * search — what is heuristic is only the choice of where to measure *from*.
182
+ *
183
+ * Three anchors, and they are the three points the surface already passes through: either endpoint,
184
+ * or the middle of the edge. An anchor that folds on its own is no anchor and is skipped; when all
185
+ * three do, the pair really has nothing this can offer and the collapse is refused.
186
+ *
187
+ * @param {BinaryTopology} mesh
188
+ * @param {number} v1
189
+ * @param {number} v2
190
+ * @param {Quadric3} q1
191
+ * @param {Quadric3} q2
192
+ * @param {boolean} has_attributes
193
+ * @param {Float32Array|number[]} out_target the wanted placement, replaced by the chosen one
194
+ * @returns {boolean} `false` when every anchor folds, and the collapse has no placement at all
195
+ */
196
+ function constrain_placement(mesh, v1, v2, q1, q2, has_attributes, out_target) {
197
+ if (!bt_edge_collapse_would_fold(mesh, v1, v2, out_target)) {
198
+ return true;
199
+ }
200
+
201
+ mesh.vertex_read_coordinate(scratch_coord_a, 0, v1);
202
+ mesh.vertex_read_coordinate(scratch_coord_b, 0, v2);
203
+
204
+ // Tracked separately rather than read off `best`, which cannot say the difference between "no
205
+ // anchor held" and "one did, at a price that overflowed" — a quadric carrying the planes of a
206
+ // few hundred thousand source triangles is not far off it.
207
+ let placed = false;
208
+
209
+ let best = Number.POSITIVE_INFINITY;
210
+
211
+ for (let anchor = 0; anchor < 3; anchor++) {
212
+ if (anchor === 0) {
213
+ scratch_anchor.set(scratch_coord_a);
214
+ } else if (anchor === 1) {
215
+ scratch_anchor.set(scratch_coord_b);
216
+ } else {
217
+ scratch_anchor[0] = (scratch_coord_a[0] + scratch_coord_b[0]) * 0.5;
218
+ scratch_anchor[1] = (scratch_coord_a[1] + scratch_coord_b[1]) * 0.5;
219
+ scratch_anchor[2] = (scratch_coord_a[2] + scratch_coord_b[2]) * 0.5;
220
+ }
221
+
222
+ const limit = bt_edge_collapse_fold_limit(mesh, v1, v2, scratch_anchor, out_target);
223
+
224
+ if (limit < 0) {
225
+ continue;
226
+ }
227
+
228
+ const travel = limit * FOLD_LIMIT_TRAVEL;
229
+
230
+ scratch_candidate[0] = scratch_anchor[0] + (out_target[0] - scratch_anchor[0]) * travel;
231
+ scratch_candidate[1] = scratch_anchor[1] + (out_target[1] - scratch_anchor[1]) * travel;
232
+ scratch_candidate[2] = scratch_anchor[2] + (out_target[2] - scratch_anchor[2]) * travel;
233
+
234
+ if (bt_edge_collapse_would_fold(mesh, v1, v2, scratch_candidate)) {
235
+ /*
236
+ The limit is exact in real arithmetic and this is not real arithmetic: a fan whose
237
+ triangles are already slivers can put the crossing and the anchor within a few ulps of
238
+ each other, and half of nothing is still nothing. The anchor is feasible by
239
+ construction, so fall back to it rather than to a search for how much less to travel.
240
+ */
241
+ scratch_candidate.set(scratch_anchor);
242
+ }
243
+
244
+ const cost = placement_cost(
245
+ q1, q2, has_attributes,
246
+ scratch_candidate[0], scratch_candidate[1], scratch_candidate[2]
247
+ );
248
+
249
+ if (!placed || cost < best) {
250
+ placed = true;
251
+ best = cost;
252
+ scratch_placement.set(scratch_candidate);
253
+ }
254
+ }
255
+
256
+ if (!placed) {
257
+ return false;
258
+ }
259
+
260
+ out_target[0] = scratch_placement[0];
261
+ out_target[1] = scratch_placement[1];
262
+ out_target[2] = scratch_placement[2];
263
+
264
+ return true;
265
+ }
266
+
107
267
  /**
108
268
  * Evaluate a potential edge collapse: choose the optimal surviving vertex position
109
269
  * and return the quadric-based cost. Returns Infinity if the edge cannot be collapsed.
@@ -116,7 +276,9 @@ function count_edge_faces(mesh, edge_id) {
116
276
  * attributes the channels the collapse carries, or `null` to carry none
117
277
  * @param {BtWedgeSet|null} wedges which corners of each vertex shade as one
118
278
  * @param {Float32Array|number[]} out_target Target position is written here.
119
- * @returns {number}
279
+ * @returns {number} the price at the placement written, which is the placement the collapse would
280
+ * actually use — so the queue this feeds is ordered on what the collapses cost rather than on
281
+ * what they would have cost somewhere they are not allowed to go
120
282
  */
121
283
  function evaluate_edge_collapse(
122
284
  mesh, edge_id, quadratics, restricted_vertices, attributes, wedges, out_target
@@ -146,9 +308,9 @@ function evaluate_edge_collapse(
146
308
  return Number.POSITIVE_INFINITY;
147
309
  }
148
310
 
149
- let attribute_cost = 0;
311
+ const has_attributes = attributes !== null;
150
312
 
151
- if (attributes !== null) {
313
+ if (has_attributes) {
152
314
  reduce_pair_attributes(mesh, v1, v2, attributes, wedges, scratch_attribute_reduced);
153
315
  }
154
316
 
@@ -160,7 +322,7 @@ function evaluate_edge_collapse(
160
322
  scratch_combined_q.copy(q1);
161
323
  scratch_combined_q.add(q2);
162
324
 
163
- if (attributes !== null) {
325
+ if (has_attributes) {
164
326
  /*
165
327
  The channels decide where the vertex goes, and this is where they do it. `A` and `b` of
166
328
  the reduced attribute quadric are what is left of them after the exact solve for their
@@ -187,24 +349,34 @@ function evaluate_edge_collapse(
187
349
  }
188
350
  }
189
351
 
190
- const tx = out_target[0];
191
- const ty = out_target[1];
192
- const tz = out_target[2];
193
-
194
- if (attributes !== null) {
195
- // What the channels still cost once each has been given its best value at this position --
196
- // in units of length, because the weights put them there.
197
- attribute_cost = Math.abs(quadric3_evaluate(scratch_attribute_reduced, 0, tx, ty, tz));
352
+ if (v1_restricted || v2_restricted) {
353
+ /*
354
+ A pin takes the placement with it, so there is no segment to slide along and a fold here is
355
+ final. Refused with the price rather than by the caller, so the queue never holds an edge
356
+ whose cost describes a collapse that will not be performed.
357
+ */
358
+ if (bt_edge_collapse_would_fold(mesh, v1, v2, out_target)) {
359
+ return Number.POSITIVE_INFINITY;
360
+ }
361
+ } else if (!constrain_placement(mesh, v1, v2, q1, q2, has_attributes, out_target)) {
362
+ return Number.POSITIVE_INFINITY;
198
363
  }
199
364
 
200
- // Cost can drift slightly negative for near-zero values due to fp noise.
201
- return Math.abs(q1.evaluate(tx, ty, tz) + q2.evaluate(tx, ty, tz)) + attribute_cost;
365
+ return placement_cost(
366
+ q1, q2, has_attributes, out_target[0], out_target[1], out_target[2]
367
+ );
202
368
  }
203
369
 
204
370
  /**
205
371
  * Re-score every edge in the disk cycle of `survivor` that is currently in the heap.
206
372
  * Edges that were never inserted (e.g. both endpoints restricted) are left untouched.
207
373
  *
374
+ * Left untouched including the ones that were left out because they had no placement at the time,
375
+ * which is a real limit and a deliberate one: an edge is excluded when *both* its fans leave it
376
+ * nowhere to go, and a collapse one edge away is unlikely to change that. Re-admitting them here was
377
+ * built and measured — it moved a 479k-triangle Zorah primitive from 34 levels to 33, and cost 60%
378
+ * of the build time to do it.
379
+ *
208
380
  * @param {BinaryTopology} mesh
209
381
  * @param {number} survivor
210
382
  * @param {Quadric3[]} quadratics
@@ -818,9 +990,24 @@ function collapse_kills_protected_face(mesh, edge_id, protected_faces) {
818
990
  * removed. If the target cannot be reached (e.g. too many restricted vertices),
819
991
  * simplification stops early without failing.
820
992
  *
821
- * A collapse that would turn a surviving triangle inside out is refused however cheap the quadric
822
- * says it is ({@link bt_edge_collapse_would_fold}) -- the quadric cannot see a fold, because a flat
823
- * region scores zero for every placement in its plane.
993
+ * No collapse ever turns a surviving triangle inside out, and the quadric cannot help with that: it
994
+ * cannot see a fold at all, because a flat region scores zero for every placement in its plane. So
995
+ * the placement is *constrained* rather than merely checked. Where the quadric's own answer folds,
996
+ * the collapse is re-placed at the best point that does not — see {@link constrain_placement}, and
997
+ * {@link bt_edge_collapse_fold_limit} for why that point can be found exactly instead of hunted for.
998
+ * A collapse is refused only when the pair has no such placement anywhere, which is a statement
999
+ * about the geometry rather than about the quadric's preference.
1000
+ *
1001
+ * Every edge is priced at the placement it would actually use, including the constrained ones, and
1002
+ * the queue is ordered on that. It is the expensive way round — the constraint costs a walk of both
1003
+ * the pair's fans, and pricing happens once per edge at the start and again for every neighbour of
1004
+ * every collapse, which measures at 1.4x to 2.5x the build time of the alternative. The alternative
1005
+ * is to order on the unconstrained price, which is a genuine lower bound ({@link placement_cost}),
1006
+ * and settle only on the way out of the queue. That was built and measured, and it is not here: the
1007
+ * greedy order it produces is approximate, and on a pinwheel whose whole rim is pinned it stopped at
1008
+ * 28 faces against a target of 21 — which is precisely the shape a vgeo group step hands this, since
1009
+ * such a step pins its entire outer boundary. A cheaper order that cannot finish a pinned
1010
+ * simplification is no bargain.
824
1011
  *
825
1012
  * **Topology is not preserved**, and cannot be here. The other standard guard, the *link condition*
826
1013
  * ({@link bt_edge_collapse_breaks_link_condition}), refuses collapses that weld together two parts
@@ -836,21 +1023,36 @@ function collapse_kills_protected_face(mesh, edge_id, protected_faces) {
836
1023
  * At the end of the call the mesh is cleaned up so that no dangling loops, edges,
837
1024
  * or vertices remain in the topology.
838
1025
  *
839
- * The return value is the **object-space distance** the surface moved: the largest, over every
840
- * vertex, of that vertex's accumulated quadric evaluated where the vertex ended up, normalized by
841
- * the quadric's weight. Because a survivor inherits its victim's quadric, this measures deviation
842
- * from the mesh as it stood when the call began — not merely from the step before.
1026
+ * The return value is the **object-space distance** the surface moved: the largest reading taken at
1027
+ * any collapse the call performed, a collapse being read by evaluating *each* of its two endpoints'
1028
+ * accumulated quadrics at the placement the survivor is about to take, each normalized by its own
1029
+ * weight. Because a survivor inherits its victim's quadric, every such reading is a deviation from
1030
+ * the mesh as it stood when the call began — not merely from the step before.
1031
+ *
1032
+ * **At the collapses, and per endpoint, and both of those are what keep it from diluting.** The
1033
+ * weight is what makes the value a length ({@link Quadric3#distance}) and it is also a divisor that
1034
+ * grows every time a vertex absorbs another. A vertex that flattens a spike and then spends the rest
1035
+ * of the call swallowing the plane around it ends up standing mostly for planes it sits exactly on,
1036
+ * and a single reading taken at the end divides the spike it destroyed by all of them — measured on
1037
+ * a unit spike at the centre of a pinned grid, the same spike came back at 0.64 on a 7-vertex grid
1038
+ * and at 0.040 on a 33-vertex one, which is the reported error falling as the surface grows.
1039
+ * Reading at every collapse catches the spike while the quadric is still mostly about it. Reading
1040
+ * the two endpoints apart rather than summed stops a small victim's residual being averaged into a
1041
+ * large survivor's untouched history, which is that same dilution one collapse at a time.
843
1042
  *
844
- * It is taken *before* faceless vertices are cleaned away, and that is not an implementation detail.
845
- * A patch that is consumed entirely leaves a vertex behind that belongs to no face; that vertex is
846
- * the only thing still holding the quadric of everything that used to be there, and reading the mesh
847
- * after cleanup would find no trace of it. A soup of separate triangles simplifies by *deleting*
848
- * them, and measuring survivors alone reports the deletion of ninety per cent of a model as costing
849
- * nothing at all.
1043
+ * Nothing is given up by no longer scanning the survivors, and it is a domination rather than a
1044
+ * coincidence. What the scan would read off a survivor is its *merged* quadric at the placement its
1045
+ * last collapse chose — and a merged quadric's reading is the weight-weighted mean of its two parts'
1046
+ * readings at that same point, so it can never exceed the larger of the two this already took there.
1047
+ * Every value the scan could report is therefore one this maximum has seen or beaten, and the
1048
+ * maximum holds the earlier and sharper readings besides. A patch consumed entirely, which leaves
1049
+ * behind a faceless vertex that the cleanup then sweeps, is read at the collapse that consumed it
1050
+ * instead of in the window between the last collapse and the sweep.
850
1051
  *
851
- * A caller that needs deviation from something *older* than this call's input — a mesh several
852
- * simplification passes back — supplies its own quadrics through
853
- * {@link bt_mesh_simplify_with_quadrics} and keeps them across passes.
1052
+ * So what comes back is what *this call's* collapses cost. A caller carrying quadrics across calls
1053
+ * through {@link bt_mesh_simplify_with_quadrics} carries the maximum across them too — and the
1054
+ * deviation a vertex arrives already carrying is, by construction, a reading that the call which
1055
+ * placed it there has already returned.
854
1056
  *
855
1057
  * `protected_faces` names faces that must still be there when the call returns. Pinning vertices
856
1058
  * does not by itself protect the *edges* between them: a collapse elsewhere in the fan can take away
@@ -974,6 +1176,14 @@ export function bt_mesh_simplify_with_quadrics(
974
1176
  // Tracked from here on rather than re-counted, so progress costs nothing per iteration.
975
1177
  let current_face_count = face_pool.count();
976
1178
 
1179
+ /**
1180
+ * The largest object-space distance any collapse so far has put the surface from the one the
1181
+ * quadrics remember. See {@link bt_mesh_simplify} for why it is a running maximum over the
1182
+ * collapses rather than one sweep of the survivors at the end.
1183
+ * @type {number}
1184
+ */
1185
+ let deviation = 0;
1186
+
977
1187
  if (current_face_count <= target_face_count) {
978
1188
  return 0;
979
1189
  }
@@ -1066,14 +1276,16 @@ export function bt_mesh_simplify_with_quadrics(
1066
1276
  continue;
1067
1277
  }
1068
1278
 
1069
- // `evaluate_edge_collapse` has just written the placement into `scratch_target`, which is
1070
- // what this needs and why it is tested here rather than beside the link condition above.
1071
- // Quadric cost is blind to the fold: a flat region scores zero for every placement in its
1072
- // plane, so nothing in the price stops a one-ring that is not star-shaped about the target
1073
- // from turning inside out.
1074
- if (bt_edge_collapse_would_fold(mesh, v1, v2, scratch_target)) {
1075
- continue;
1076
- }
1279
+ /*
1280
+ An invariant now, not a filter. `evaluate_edge_collapse` has just written the placement
1281
+ into `scratch_target`, and it returns a finite price only for a placement it has itself
1282
+ confirmed folds nothing -- so a fold reaching this point is this file being wrong, which is
1283
+ what an assertion is for and what a `continue` would have hidden.
1284
+ */
1285
+ assert.notOk(
1286
+ bt_edge_collapse_would_fold(mesh, v1, v2, scratch_target),
1287
+ 'the placement a finite collapse price was quoted for must not fold'
1288
+ );
1077
1289
 
1078
1290
  const face_deaths = count_edge_faces(mesh, edge_id);
1079
1291
 
@@ -1086,6 +1298,18 @@ export function bt_mesh_simplify_with_quadrics(
1086
1298
  const pre_survivor = mesh.edge_read_vertex1(edge_id);
1087
1299
  const pre_victim = mesh.edge_read_vertex2(edge_id);
1088
1300
 
1301
+ const target_x = scratch_target[0];
1302
+ const target_y = scratch_target[1];
1303
+ const target_z = scratch_target[2];
1304
+
1305
+ // Priced before the accumulation below, which is the merge that would dilute it, and each
1306
+ // endpoint against its own weight for the same reason.
1307
+ deviation = Math.max(
1308
+ deviation,
1309
+ quadratics[pre_survivor].distance(target_x, target_y, target_z),
1310
+ quadratics[pre_victim].distance(target_x, target_y, target_z)
1311
+ );
1312
+
1089
1313
  // Before the accumulation below, which is what makes the blend fallback weigh the two
1090
1314
  // endpoints' own histories rather than the survivor's already carrying the victim's. And
1091
1315
  // before the topology mutates, which is what lets the faces about to die say which corners
@@ -1125,11 +1349,6 @@ export function bt_mesh_simplify_with_quadrics(
1125
1349
  );
1126
1350
  }
1127
1351
 
1128
- // Measured here, before the cleanup below, because a vertex left behind by a patch that was
1129
- // consumed whole is faceless and about to be removed — and it is the only thing still carrying
1130
- // the quadric of the geometry that used to be there.
1131
- const deviation = measure_deviation(mesh, quadratics);
1132
-
1133
1352
  // Drop any loose loops/edges/verts produced during collapses, preserving
1134
1353
  // restricted vertices even if they ended up isolated.
1135
1354
  cleanup_faceless_preserving_restricted(mesh, restricted_vertices);
@@ -1137,33 +1356,6 @@ export function bt_mesh_simplify_with_quadrics(
1137
1356
  return deviation;
1138
1357
  }
1139
1358
 
1140
- /**
1141
- * The largest distance any vertex now sits from the surface its quadric remembers.
1142
- *
1143
- * @param {BinaryTopology} mesh
1144
- * @param {Quadric3[]} quadratics
1145
- * @returns {number}
1146
- */
1147
- function measure_deviation(mesh, quadratics) {
1148
- const vertex_pool = mesh.vertices;
1149
-
1150
- let result = 0;
1151
-
1152
- for (let v = 0; v < vertex_pool.size; v++) {
1153
- if (!vertex_pool.is_allocated(v) || quadratics[v] === null || quadratics[v] === undefined) {
1154
- continue;
1155
- }
1156
-
1157
- mesh.vertex_read_coordinate(scratch_coord_a, 0, v);
1158
-
1159
- result = Math.max(
1160
- result, quadratics[v].distance(scratch_coord_a[0], scratch_coord_a[1], scratch_coord_a[2])
1161
- );
1162
- }
1163
-
1164
- return result;
1165
- }
1166
-
1167
1359
  /**
1168
1360
  * Variant of bt_mesh_cleanup_faceless_references that never releases a restricted
1169
1361
  * vertex even when it has no remaining edges.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * How far from `from` toward `to` may a collapse of `survivor`-`victim` place the surviving vertex
3
+ * and still turn no surviving triangle over?
4
+ *
5
+ * Both fans are walked, because both endpoints move: the victim onto the placement by definition,
6
+ * and the survivor too wherever the caller puts it somewhere other than where it already sits. The
7
+ * union of the two is the set of faces the collapse reshapes, which is what makes the answer
8
+ * symmetric in the two endpoints -- a caller that has not yet settled which of them survives can
9
+ * rely on that.
10
+ *
11
+ * @param {BinaryTopology} mesh
12
+ * @param {number} survivor the endpoint that stays allocated
13
+ * @param {number} victim the endpoint that is removed
14
+ * @param {Float32Array|number[]} from the placement to measure from, at `t = 0`
15
+ * @param {Float32Array|number[]} to the placement wanted, at `t = 1`
16
+ * @param {number} [from_offset]
17
+ * @param {number} [to_offset]
18
+ * @returns {number} where the first surviving triangle reaches **zero** area, so the placements that
19
+ * fold nothing are `t` in `[0, limit)` -- open at the top, since zero area is refused along with
20
+ * a flip, and a caller therefore takes a fraction of this rather than this. `1` when nothing on
21
+ * the segment folds, and `-1` when `from` itself does, which is the one answer no amount of
22
+ * sliding recovers from.
23
+ */
24
+ export function bt_edge_collapse_fold_limit(mesh: BinaryTopology, survivor: number, victim: number, from: Float32Array | number[], to: Float32Array | number[], from_offset?: number, to_offset?: number): number;
25
+ //# sourceMappingURL=bt_edge_collapse_fold_limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bt_edge_collapse_fold_limit.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.js"],"names":[],"mappings":"AA4LA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,4EAZW,MAAM,UACN,MAAM,QACN,YAAY,GAAC,MAAM,EAAE,MACrB,YAAY,GAAC,MAAM,EAAE,gBACrB,MAAM,cACN,MAAM,GACJ,MAAM,CAsBlB"}