@woosh/meep-engine 3.16.0 → 3.17.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 (410) hide show
  1. package/editor/particles/effect/ParticleEffectDocument.d.ts +21 -6
  2. package/editor/particles/effect/ParticleEffectDocument.d.ts.map +1 -1
  3. package/editor/particles/effect/ParticleEffectDocument.js +38 -17
  4. package/editor/view/node-graph/NodeGraphClipboard.d.ts +186 -0
  5. package/editor/view/node-graph/NodeGraphClipboard.d.ts.map +1 -0
  6. package/editor/view/node-graph/NodeGraphClipboard.js +231 -0
  7. package/editor/view/node-graph/NodeGraphEditorView.d.ts +59 -1
  8. package/editor/view/node-graph/NodeGraphEditorView.d.ts.map +1 -1
  9. package/editor/view/node-graph/NodeGraphEditorView.js +248 -5
  10. package/editor/view/node-graph/NodeGraphView.d.ts.map +1 -1
  11. package/editor/view/node-graph/NodeGraphView.js +84 -8
  12. package/editor/view/node-graph/actions/connection_bind_actions.d.ts +23 -0
  13. package/editor/view/node-graph/actions/connection_bind_actions.d.ts.map +1 -0
  14. package/editor/view/node-graph/actions/connection_bind_actions.js +39 -0
  15. package/editor/view/particles/effect/ParticleCodeEditorView.d.ts.map +1 -1
  16. package/editor/view/particles/effect/ParticleCodeEditorView.js +4 -3
  17. package/editor/view/particles/effect/ParticleCurveEditorView.d.ts +20 -0
  18. package/editor/view/particles/effect/ParticleCurveEditorView.d.ts.map +1 -0
  19. package/editor/view/particles/effect/ParticleCurveEditorView.js +574 -0
  20. package/editor/view/particles/effect/ParticleEmitterInspectorView.d.ts.map +1 -1
  21. package/editor/view/particles/effect/ParticleEmitterInspectorView.js +27 -1
  22. package/editor/view/particles/effect/ParticleGradientEditorView.d.ts +24 -0
  23. package/editor/view/particles/effect/ParticleGradientEditorView.d.ts.map +1 -0
  24. package/editor/view/particles/effect/ParticleGradientEditorView.js +472 -0
  25. package/editor/view/particles/effect/ParticleGraphEditorView.d.ts +5 -0
  26. package/editor/view/particles/effect/ParticleGraphEditorView.d.ts.map +1 -1
  27. package/editor/view/particles/effect/ParticleGraphEditorView.js +61 -31
  28. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -1
  29. package/editor/view/particles/effect/ParticleNodeParametersView.js +33 -0
  30. package/editor/view/particles/effect/particle-editor.css +123 -35
  31. package/package.json +1 -1
  32. package/src/core/binary/allocator/OffsetAllocator.d.ts +25 -16
  33. package/src/core/binary/allocator/OffsetAllocator.d.ts.map +1 -1
  34. package/src/core/binary/allocator/OffsetAllocator.js +10 -1
  35. package/src/core/collection/array/typed/float32_array_hash.d.ts +21 -0
  36. package/src/core/collection/array/typed/float32_array_hash.d.ts.map +1 -0
  37. package/src/core/collection/array/typed/float32_array_hash.js +38 -0
  38. package/src/core/graph/csr/CSRGraph.d.ts +8 -1
  39. package/src/core/graph/csr/CSRGraph.d.ts.map +1 -1
  40. package/src/core/graph/csr/CSRGraph.js +14 -6
  41. package/src/core/model/node-graph/visual/layout/LayoutProblem.d.ts +24 -0
  42. package/src/core/model/node-graph/visual/layout/LayoutProblem.d.ts.map +1 -1
  43. package/src/core/model/node-graph/visual/layout/LayoutProblem.js +27 -0
  44. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts +26 -0
  45. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts.map +1 -1
  46. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.js +28 -0
  47. package/src/core/model/node-graph/visual/layout/RouteGrid.d.ts +92 -3
  48. package/src/core/model/node-graph/visual/layout/RouteGrid.d.ts.map +1 -1
  49. package/src/core/model/node-graph/visual/layout/RouteGrid.js +405 -242
  50. package/src/core/model/node-graph/visual/layout/RouteSearch.d.ts +50 -5
  51. package/src/core/model/node-graph/visual/layout/RouteSearch.d.ts.map +1 -1
  52. package/src/core/model/node-graph/visual/layout/RouteSearch.js +160 -24
  53. package/src/core/model/node-graph/visual/layout/RouteSet.d.ts +7 -0
  54. package/src/core/model/node-graph/visual/layout/RouteSet.d.ts.map +1 -1
  55. package/src/core/model/node-graph/visual/layout/RouteSet.js +8 -0
  56. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.js +9 -3
  57. package/src/core/model/node-graph/visual/layout/layout_build_problem.d.ts.map +1 -1
  58. package/src/core/model/node-graph/visual/layout/layout_build_problem.js +7 -0
  59. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts +9 -3
  60. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts.map +1 -1
  61. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.js +17 -3
  62. package/src/core/model/node-graph/visual/layout/layout_node_graph.d.ts +9 -6
  63. package/src/core/model/node-graph/visual/layout/layout_node_graph.d.ts.map +1 -1
  64. package/src/core/model/node-graph/visual/layout/layout_node_graph.js +49 -7
  65. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts +11 -1
  66. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts.map +1 -1
  67. package/src/core/model/node-graph/visual/layout/layout_order_layers.js +135 -16
  68. package/src/core/model/node-graph/visual/layout/route_connections.d.ts +29 -0
  69. package/src/core/model/node-graph/visual/layout/route_connections.d.ts.map +1 -1
  70. package/src/core/model/node-graph/visual/layout/route_connections.js +716 -309
  71. package/src/core/model/node-graph/visual/layout/route_grid_build.d.ts +3 -2
  72. package/src/core/model/node-graph/visual/layout/route_grid_build.d.ts.map +1 -1
  73. package/src/core/model/node-graph/visual/layout/route_grid_build.js +7 -15
  74. package/src/core/model/node-graph/visual/layout/route_nudge.d.ts +6 -0
  75. package/src/core/model/node-graph/visual/layout/route_nudge.d.ts.map +1 -1
  76. package/src/core/model/node-graph/visual/layout/route_nudge.js +42 -13
  77. package/src/engine/animation/AnimationUtils.js +246 -246
  78. package/src/engine/animation/Animations.js +74 -74
  79. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +2078 -2078
  80. package/src/engine/ecs/EventType.d.ts +0 -1
  81. package/src/engine/ecs/EventType.js +1 -26
  82. package/src/engine/ecs/ik/ik_write_local_rotation.d.ts +1 -1
  83. package/src/engine/ecs/ik/ik_write_local_rotation.d.ts.map +1 -1
  84. package/src/engine/ecs/ik/ik_write_local_rotation.js +3 -7
  85. package/src/engine/ecs/parent/EntityNode.d.ts.map +1 -1
  86. package/src/engine/ecs/parent/EntityNode.js +4 -9
  87. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
  88. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +360 -364
  89. package/src/engine/ecs/transform/TRANSFORM64_EVENT_CHANGE.d.ts +41 -0
  90. package/src/engine/ecs/transform/TRANSFORM64_EVENT_CHANGE.d.ts.map +1 -0
  91. package/src/engine/ecs/transform/TRANSFORM64_EVENT_CHANGE.js +40 -0
  92. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +8 -0
  93. package/src/engine/ecs/transform/t64_announce_change.d.ts +10 -7
  94. package/src/engine/ecs/transform/t64_announce_change.d.ts.map +1 -1
  95. package/src/engine/ecs/transform/t64_announce_change.js +12 -12
  96. package/src/engine/ecs/transform-attachment/TRANSFORM_ATTACHMENT_EVENT_CHANGE.d.ts +30 -0
  97. package/src/engine/ecs/transform-attachment/TRANSFORM_ATTACHMENT_EVENT_CHANGE.d.ts.map +1 -0
  98. package/src/engine/ecs/transform-attachment/TRANSFORM_ATTACHMENT_EVENT_CHANGE.js +29 -0
  99. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.d.ts.map +1 -1
  100. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js +13 -21
  101. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts.map +1 -1
  102. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.js +4 -8
  103. package/src/engine/graphics3/LightSystem.d.ts.map +1 -1
  104. package/src/engine/graphics3/LightSystem.js +4 -8
  105. package/src/engine/graphics3/MeshSystem.d.ts.map +1 -1
  106. package/src/engine/graphics3/MeshSystem.js +9 -19
  107. package/src/engine/graphics3/ParticleEmitterSystem.d.ts.map +1 -1
  108. package/src/engine/graphics3/ParticleEmitterSystem.js +4 -8
  109. package/src/engine/graphics3/ShadedGeometrySystem.d.ts.map +1 -1
  110. package/src/engine/graphics3/ShadedGeometrySystem.js +4 -10
  111. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.d.ts +1 -1
  112. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.d.ts.map +1 -1
  113. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.js +4 -8
  114. package/src/engine/intelligence/behavior/ecs/OrbitingBehavior.js +218 -218
  115. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  116. package/src/engine/physics/ecs/PhysicsSystem.js +2 -5
  117. package/src/engine/sound/ecs/SoundListenerSystem.d.ts.map +1 -1
  118. package/src/engine/sound/ecs/SoundListenerSystem.js +6 -8
  119. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.d.ts +0 -4
  120. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.d.ts.map +1 -1
  121. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.js +5 -11
  122. package/src/shade/playground/particle_editor/README.md +43 -2
  123. package/src/shade/playground/particle_system/particle_prototype.d.ts.map +1 -1
  124. package/src/shade/playground/particle_system/particle_prototype.js +4 -5
  125. package/src/shade/playground/particle_system/particle_scene.js +4 -4
  126. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  127. package/src/shade/renderer/Renderer.js +13 -1
  128. package/src/shade/renderer/particles/DESIGN.md +104 -19
  129. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +64 -50
  130. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  131. package/src/shade/renderer/particles/GPUParticleSystem.js +176 -78
  132. package/src/shade/renderer/particles/ParticleConstants.d.ts +59 -23
  133. package/src/shade/renderer/particles/ParticleConstants.d.ts.map +1 -1
  134. package/src/shade/renderer/particles/ParticleConstants.js +66 -23
  135. package/src/shade/renderer/particles/ParticleRenderMode.d.ts +19 -0
  136. package/src/shade/renderer/particles/ParticleRenderMode.d.ts.map +1 -0
  137. package/src/shade/renderer/particles/ParticleRenderMode.js +33 -0
  138. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.d.ts.map +1 -1
  139. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.js +167 -166
  140. package/src/shade/renderer/particles/data/particle_emitter_record.d.ts +37 -0
  141. package/src/shade/renderer/particles/data/particle_emitter_record.d.ts.map +1 -0
  142. package/src/shade/renderer/particles/data/particle_emitter_record.js +109 -0
  143. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts +3 -3
  144. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.js +303 -303
  145. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +18 -0
  146. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -1
  147. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +152 -133
  148. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +0 -11
  149. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  150. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +720 -621
  151. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  152. package/src/shade/renderer/particles/graph/compile_particle_graph.js +8 -3
  153. package/src/shade/renderer/particles/graph/flatten_particle_graph.d.ts.map +1 -1
  154. package/src/shade/renderer/particles/graph/flatten_particle_graph.js +244 -213
  155. package/src/shade/renderer/particles/graph/groups/particle_builtin_groups.d.ts.map +1 -1
  156. package/src/shade/renderer/particles/graph/groups/particle_builtin_groups.js +6 -0
  157. package/src/shade/renderer/particles/graph/groups/particle_groups_geometry.d.ts.map +1 -1
  158. package/src/shade/renderer/particles/graph/groups/particle_groups_geometry.js +294 -319
  159. package/src/shade/renderer/particles/graph/groups/particle_groups_noise.d.ts.map +1 -1
  160. package/src/shade/renderer/particles/graph/groups/particle_groups_noise.js +524 -517
  161. package/src/shade/renderer/particles/graph/groups/particle_groups_psrdnoise.d.ts +22 -0
  162. package/src/shade/renderer/particles/graph/groups/particle_groups_psrdnoise.d.ts.map +1 -0
  163. package/src/shade/renderer/particles/graph/groups/particle_groups_psrdnoise.js +517 -0
  164. package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts.map +1 -1
  165. package/src/shade/renderer/particles/graph/groups/simplex_reference.js +185 -159
  166. package/src/shade/renderer/particles/graph/make_particle_group.d.ts.map +1 -1
  167. package/src/shade/renderer/particles/graph/make_particle_group.js +2 -20
  168. package/src/shade/renderer/particles/graph/particle_compare_ops.d.ts +17 -0
  169. package/src/shade/renderer/particles/graph/particle_compare_ops.d.ts.map +1 -0
  170. package/src/shade/renderer/particles/graph/particle_compare_ops.js +22 -0
  171. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.d.ts +28 -8
  172. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.d.ts.map +1 -1
  173. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.js +49 -25
  174. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts +40 -0
  175. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts.map +1 -1
  176. package/src/shade/renderer/particles/graph/particle_node_parameters.js +312 -234
  177. package/src/shade/renderer/particles/graph/particle_node_presentation.d.ts.map +1 -1
  178. package/src/shade/renderer/particles/graph/particle_node_presentation.js +59 -3
  179. package/src/shade/renderer/particles/graph/particle_ramp.d.ts +230 -0
  180. package/src/shade/renderer/particles/graph/particle_ramp.d.ts.map +1 -0
  181. package/src/shade/renderer/particles/graph/particle_ramp.js +403 -0
  182. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts +7 -1
  183. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts.map +1 -1
  184. package/src/shade/renderer/particles/graph/validate_particle_graph.js +66 -10
  185. package/src/shade/renderer/particles/graph_particles.d.ts +23 -69
  186. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  187. package/src/shade/renderer/particles/graph_particles.js +105 -262
  188. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  189. package/src/shade/renderer/particles/graph_particles_avboit.js +3 -2
  190. package/src/shade/renderer/particles/graph_particles_billboard.d.ts +37 -0
  191. package/src/shade/renderer/particles/graph_particles_billboard.d.ts.map +1 -0
  192. package/src/shade/renderer/particles/graph_particles_billboard.js +93 -0
  193. package/src/shade/renderer/particles/isa/InstructionStream.d.ts +51 -22
  194. package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -1
  195. package/src/shade/renderer/particles/isa/InstructionStream.js +98 -38
  196. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts +49 -8
  197. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts.map +1 -1
  198. package/src/shade/renderer/particles/isa/ParticleProgram.js +95 -150
  199. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts +306 -64
  200. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
  201. package/src/shade/renderer/particles/isa/ParticleVMISA.js +505 -136
  202. package/src/shade/renderer/particles/isa/particle_assembly.d.ts +36 -12
  203. package/src/shade/renderer/particles/isa/particle_assembly.d.ts.map +1 -1
  204. package/src/shade/renderer/particles/isa/particle_assembly.js +354 -139
  205. package/src/shade/renderer/particles/isa/validate_particle_program.d.ts +9 -0
  206. package/src/shade/renderer/particles/isa/validate_particle_program.d.ts.map +1 -0
  207. package/src/shade/renderer/particles/isa/validate_particle_program.js +128 -0
  208. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts +23 -0
  209. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts.map +1 -1
  210. package/src/shade/renderer/particles/layout/ParticleLayout.js +68 -0
  211. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.d.ts +77 -0
  212. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.d.ts.map +1 -0
  213. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.js +219 -0
  214. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.d.ts +83 -0
  215. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.d.ts.map +1 -0
  216. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.js +283 -0
  217. package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.d.ts +133 -0
  218. package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.d.ts.map +1 -0
  219. package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.js +334 -0
  220. package/src/shade/renderer/particles/optimizer/dag/particle_vector_groups.d.ts +117 -0
  221. package/src/shade/renderer/particles/optimizer/dag/particle_vector_groups.d.ts.map +1 -0
  222. package/src/shade/renderer/particles/optimizer/dag/particle_vector_groups.js +70 -0
  223. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.d.ts +97 -0
  224. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.d.ts.map +1 -0
  225. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.js +462 -0
  226. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lower.d.ts +116 -0
  227. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lower.d.ts.map +1 -0
  228. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lower.js +687 -0
  229. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.d.ts +57 -0
  230. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.d.ts.map +1 -0
  231. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.js +860 -0
  232. package/src/shade/renderer/particles/optimizer/dag/particle_vector_rules.d.ts +115 -0
  233. package/src/shade/renderer/particles/optimizer/dag/particle_vector_rules.d.ts.map +1 -0
  234. package/src/shade/renderer/particles/optimizer/dag/particle_vector_rules.js +264 -0
  235. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts +81 -42
  236. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts.map +1 -1
  237. package/src/shade/renderer/particles/optimizer/optimize_particle_program.js +477 -478
  238. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.d.ts +100 -0
  239. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.d.ts.map +1 -0
  240. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.js +236 -0
  241. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts +139 -59
  242. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts.map +1 -1
  243. package/src/shade/renderer/particles/optimizer/particle_program_analysis.js +764 -647
  244. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts +40 -7
  245. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts.map +1 -1
  246. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.js +376 -213
  247. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.d.ts +30 -3
  248. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.d.ts.map +1 -1
  249. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.js +337 -223
  250. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts +24 -38
  251. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  252. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +166 -206
  253. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.d.ts +99 -0
  254. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.d.ts.map +1 -0
  255. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.js +313 -0
  256. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.d.ts +3 -2
  257. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.d.ts.map +1 -1
  258. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.js +198 -197
  259. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.d.ts.map +1 -1
  260. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts +19 -0
  261. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
  262. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +678 -561
  263. package/src/shade/renderer/particles/optimizer/pass_inline_constants.d.ts +22 -0
  264. package/src/shade/renderer/particles/optimizer/pass_inline_constants.d.ts.map +1 -0
  265. package/src/shade/renderer/particles/optimizer/pass_inline_constants.js +221 -0
  266. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.d.ts.map +1 -1
  267. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.js +63 -72
  268. package/src/shade/renderer/particles/optimizer/pass_pack_registers.d.ts.map +1 -1
  269. package/src/shade/renderer/particles/optimizer/pass_pack_registers.js +87 -86
  270. package/src/shade/renderer/particles/optimizer/pass_peephole.d.ts.map +1 -1
  271. package/src/shade/renderer/particles/optimizer/pass_peephole.js +392 -271
  272. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.d.ts.map +1 -1
  273. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.js +361 -268
  274. package/src/shade/renderer/particles/optimizer/pass_schedule.d.ts +4 -0
  275. package/src/shade/renderer/particles/optimizer/pass_schedule.d.ts.map +1 -0
  276. package/src/shade/renderer/particles/optimizer/pass_schedule.js +244 -0
  277. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.d.ts.map +1 -1
  278. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.js +154 -151
  279. package/src/shade/renderer/particles/particle_test_fixtures.d.ts +20 -0
  280. package/src/shade/renderer/particles/particle_test_fixtures.d.ts.map +1 -1
  281. package/src/shade/renderer/particles/particle_test_fixtures.js +229 -203
  282. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts +99 -25
  283. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts.map +1 -1
  284. package/src/shade/renderer/particles/runtime/EmitterRegistry.js +438 -286
  285. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts +109 -0
  286. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts.map +1 -0
  287. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.js +127 -0
  288. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +87 -102
  289. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
  290. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +184 -163
  291. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts +26 -0
  292. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts.map +1 -1
  293. package/src/shade/renderer/particles/runtime/ProgramHeap.d.ts +130 -0
  294. package/src/shade/renderer/particles/runtime/ProgramHeap.d.ts.map +1 -0
  295. package/src/shade/renderer/particles/runtime/ProgramHeap.js +595 -0
  296. package/src/shade/renderer/particles/runtime/ProgramPlacement.d.ts +91 -0
  297. package/src/shade/renderer/particles/runtime/ProgramPlacement.d.ts.map +1 -0
  298. package/src/shade/renderer/particles/runtime/ProgramPlacement.js +106 -0
  299. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +5 -2
  300. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  301. package/src/shade/renderer/particles/runtime/create_particle_effect.js +22 -19
  302. package/src/shade/renderer/particles/shaders/chunk_particle_emit_step.d.ts +28 -0
  303. package/src/shade/renderer/particles/shaders/chunk_particle_emit_step.d.ts.map +1 -0
  304. package/src/shade/renderer/particles/shaders/chunk_particle_emit_step.js +136 -0
  305. package/src/shade/renderer/particles/shaders/chunk_particle_simulate_step.d.ts +35 -0
  306. package/src/shade/renderer/particles/shaders/chunk_particle_simulate_step.d.ts.map +1 -0
  307. package/src/shade/renderer/particles/shaders/chunk_particle_simulate_step.js +133 -0
  308. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts +18 -9
  309. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  310. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +20 -80
  311. package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.d.ts +33 -0
  312. package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.d.ts.map +1 -0
  313. package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.js +114 -0
  314. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts +8 -0
  315. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  316. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +33 -101
  317. package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.d.ts +45 -0
  318. package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.d.ts.map +1 -0
  319. package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.js +123 -0
  320. package/src/shade/renderer/particles/sort/graph_particle_sort.d.ts +51 -0
  321. package/src/shade/renderer/particles/sort/graph_particle_sort.d.ts.map +1 -0
  322. package/src/shade/renderer/particles/sort/graph_particle_sort.js +120 -0
  323. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.d.ts +6 -0
  324. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.d.ts.map +1 -0
  325. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.js +16 -0
  326. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +4 -1
  327. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  328. package/src/shade/renderer/particles/vm/ParticleVMReference.js +261 -140
  329. package/src/shade/renderer/particles/vm/chunk_particle_vm.d.ts.map +1 -1
  330. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +217 -255
  331. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_fast.d.ts +43 -0
  332. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_fast.d.ts.map +1 -0
  333. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_fast.js +55 -0
  334. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_wide.d.ts +62 -0
  335. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_wide.d.ts.map +1 -0
  336. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_wide.js +86 -0
  337. package/src/shade/renderer/shader/chunk/math/chunk_mod_v3f.d.ts +22 -0
  338. package/src/shade/renderer/shader/chunk/math/chunk_mod_v3f.d.ts.map +1 -0
  339. package/src/shade/renderer/shader/chunk/math/chunk_mod_v3f.js +32 -0
  340. package/src/shade/renderer/shader/chunk/math/chunk_mod_v4f.d.ts +9 -0
  341. package/src/shade/renderer/shader/chunk/math/chunk_mod_v4f.d.ts.map +1 -0
  342. package/src/shade/renderer/shader/chunk/math/chunk_mod_v4f.js +19 -0
  343. package/src/shade/renderer/shader/chunk/noise/psrdnoise/README.md +65 -0
  344. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2.d.ts +24 -0
  345. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2.d.ts.map +1 -0
  346. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2.js +83 -0
  347. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_hash.d.ts +25 -0
  348. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_hash.d.ts.map +1 -0
  349. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_hash.js +42 -0
  350. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_kernel.d.ts +31 -0
  351. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_kernel.d.ts.map +1 -0
  352. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_kernel.js +80 -0
  353. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3.d.ts +26 -0
  354. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3.d.ts.map +1 -0
  355. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3.js +109 -0
  356. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_kernel.d.ts +38 -0
  357. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_kernel.d.ts.map +1 -0
  358. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_kernel.js +129 -0
  359. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_permute.d.ts +21 -0
  360. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_permute.d.ts.map +1 -0
  361. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_permute.js +35 -0
  362. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_skew.d.ts +19 -0
  363. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_skew.d.ts.map +1 -0
  364. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_skew.js +28 -0
  365. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_unskew.d.ts +13 -0
  366. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_unskew.d.ts.map +1 -0
  367. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_unskew.js +22 -0
  368. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise2.d.ts +22 -0
  369. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise2.d.ts.map +1 -0
  370. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise2.js +59 -0
  371. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise3.d.ts +23 -0
  372. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise3.d.ts.map +1 -0
  373. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise3.js +74 -0
  374. package/src/shade/wgsl/emulator/ComputeShaderEmulator.d.ts +34 -1
  375. package/src/shade/wgsl/emulator/ComputeShaderEmulator.d.ts.map +1 -1
  376. package/src/shade/wgsl/emulator/ComputeShaderEmulator.js +41 -1
  377. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts +3 -2
  378. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  379. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +49 -10
  380. package/src/view/minimap/dom/MinimapCameraView.d.ts.map +1 -1
  381. package/src/view/minimap/dom/MinimapCameraView.js +4 -9
  382. package/src/core/model/node-graph/visual/layout/BoxLayoutSpec.d.ts +0 -15
  383. package/src/core/model/node-graph/visual/layout/BoxLayoutSpec.d.ts.map +0 -1
  384. package/src/core/model/node-graph/visual/layout/ConnectedBoxLayouter.d.ts +0 -45
  385. package/src/core/model/node-graph/visual/layout/ConnectedBoxLayouter.d.ts.map +0 -1
  386. package/src/core/model/node-graph/visual/layout/ConnectionEndpointLayoutSpec.d.ts +0 -24
  387. package/src/core/model/node-graph/visual/layout/ConnectionEndpointLayoutSpec.d.ts.map +0 -1
  388. package/src/core/model/node-graph/visual/layout/ConnectionLayoutSpec.d.ts +0 -19
  389. package/src/core/model/node-graph/visual/layout/ConnectionLayoutSpec.d.ts.map +0 -1
  390. package/src/engine/ecs/transform/__probe.d.ts +0 -4
  391. package/src/engine/ecs/transform/__probe.d.ts.map +0 -1
  392. package/src/engine/ecs/util/hideEntityGracefully.d.ts +0 -9
  393. package/src/engine/ecs/util/hideEntityGracefully.d.ts.map +0 -1
  394. package/src/shade/renderer/buffer/table/gpu_database_emulator_image.d.ts +0 -27
  395. package/src/shade/renderer/buffer/table/gpu_database_emulator_image.d.ts.map +0 -1
  396. package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts +0 -14
  397. package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +0 -1
  398. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_LAYOUT.d.ts +0 -156
  399. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_LAYOUT.d.ts.map +0 -1
  400. package/src/shade/renderer/particles/runtime/Emitter.d.ts +0 -219
  401. package/src/shade/renderer/particles/runtime/Emitter.d.ts.map +0 -1
  402. package/src/shade/renderer/particles/runtime/ParticleSpawnScheduler.d.ts +0 -77
  403. package/src/shade/renderer/particles/runtime/ParticleSpawnScheduler.d.ts.map +0 -1
  404. package/src/shade/renderer/particles/runtime/ProgramArena.js +0 -155
  405. package/src/shade/renderer/particles/runtime/emitter_database_image.d.ts +0 -15
  406. package/src/shade/renderer/particles/runtime/emitter_database_image.d.ts.map +0 -1
  407. package/src/shade/renderer/particles/runtime/spawn_request_encoding.d.ts +0 -34
  408. package/src/shade/renderer/particles/runtime/spawn_request_encoding.d.ts.map +0 -1
  409. package/src/shade/renderer/particles/shaders/chunk_particle_context.d.ts +0 -32
  410. package/src/shade/renderer/particles/shaders/chunk_particle_context.d.ts.map +0 -1
@@ -1,621 +1,720 @@
1
- import { NodeRegistry } from "../../../../core/model/node-graph/node/NodeRegistry.js";
2
- import { PortDirection } from "../../../../core/model/node-graph/node/PortDirection.js";
3
- import { DataType } from "../../../../core/model/node-graph/type/DataType.js";
4
- import { pack_swizzle, VM_BUILTIN_COMPONENTS, VM_OP } from "../isa/ParticleVMISA.js";
5
- import { particle_node_presentation } from "./particle_node_presentation.js";
6
- import { ParticleNodeDescription } from "./ParticleNodeDescription.js";
7
- import {
8
- PARTICLE_ALL_VALUE_TYPES,
9
- PARTICLE_TYPE_COLOR,
10
- PARTICLE_TYPE_F32,
11
- PARTICLE_TYPE_VEC3,
12
- particle_semantic_type,
13
- particle_value_type,
14
- particle_value_type_or_null,
15
- particle_widest_type,
16
- } from "./ParticleValueType.js";
17
-
18
- /**
19
- * Standard particle-node library, expressed as {@link ParticleNodeDescription}s in a
20
- * {@link NodeRegistry}. Nodes are value-producing operations (constants, builtins, attribute reads,
21
- * math, curves, random) or sinks (attribute writes, kill).
22
- *
23
- * These lower to the same opcodes the JS reference + WGSL interpreter agree on, so any effect
24
- * authored here is executable and testable end-to-end on the CPU before it ever hits the GPU.
25
- *
26
- * ## Types
27
- *
28
- * A value is a float vector of one to four lanes, and which it is is a property of the node
29
- * instance rather than of its type: a `builtin` is a scalar or a `vec3` by which builtin it reads,
30
- * a `const` is as wide as the literal it holds, an `add` is as wide as the wider of what it adds.
31
- * So every port is declared with the one {@link PARTICLE_VALUE_TYPE} — any output may be wired to
32
- * any input — and each node type says how it is typed through
33
- * {@link ParticleNodeDescription#infer_types}, which {@link ./infer_particle_graph_types.js}
34
- * resolves over a whole graph. The types themselves are in {@link ./ParticleValueType.js}, and
35
- * registered here so a view can colour by them.
36
- *
37
- * A type may also say what a value *means* — {@link PARTICLE_TYPE_COLOR} is four lanes that are a
38
- * colour — and the rules below never ask. They compare `components` and take the widest, so a
39
- * colour is accepted wherever a `vec4` is, produces whatever a `vec4` would, and survives the
40
- * arithmetic in between. The two places a colour enters a graph are the `color` node and an
41
- * attribute the effect's layout has tagged as one.
42
- */
43
-
44
- /**
45
- * The declared type of every port in the library: a value goes here.
46
- *
47
- * Which value — one lane or four — is resolved per node instance, see the note on types above.
48
- * Declared once for all ports because the graph model connects ports whose declared types are the
49
- * same object, and any particle value may drive any particle input; where the widths differ, the
50
- * compiler converts and the validator says so.
51
- *
52
- * @type {DataType}
53
- */
54
- export const PARTICLE_VALUE_TYPE = DataType.from(0, "value");
55
-
56
- /**
57
- * Name of the single output port carried by every value-producing node.
58
- * @type {string}
59
- */
60
- export const PARTICLE_NODE_OUTPUT_PORT = "out";
61
-
62
- /**
63
- * @type {NodeRegistry}
64
- */
65
- export const PARTICLE_NODE_REGISTRY = new NodeRegistry();
66
-
67
- // The resolved types are the registry's too: a view colours a wire by a type's id, and looks the
68
- // id up here.
69
- for (const type of PARTICLE_ALL_VALUE_TYPES) {
70
- PARTICLE_NODE_REGISTRY.addType(type);
71
- }
72
-
73
- /**
74
- * Node descriptions are addressed by name while authoring; the registry itself is keyed by the
75
- * numeric IDs assigned below.
76
- *
77
- * IDs are handed out in declaration order rather than by {@link NodeDescription}'s global counter,
78
- * so they do not drift with unrelated node usage elsewhere in the engine. Appending new node types
79
- * at the end of the library is therefore safe; reordering or removing existing ones renumbers every
80
- * node that follows, which would invalidate already-serialized graphs.
81
- *
82
- * @type {Map<string, ParticleNodeDescription>}
83
- */
84
- const descriptions_by_name = new Map();
85
-
86
- /**
87
- * @param {string} name
88
- * @param {string[]} inputs input port names, in operand order
89
- * @param {function} emit see {@link ParticleNodeDescription#emit}
90
- * @param {object} [options]
91
- * @param {boolean} [options.produces_value] when false the node is a sink and gets no output port
92
- * @param {function} [options.infer_types] see {@link ParticleNodeDescription#infer_types}
93
- * @param {function} [options.operation_width] see {@link ParticleNodeDescription#operation_width}
94
- * @param {function} [options.input_widths] see {@link ParticleNodeDescription#input_widths}
95
- * @param {function} [options.result_width] see {@link ParticleNodeDescription#result_width}
96
- * @returns {ParticleNodeDescription}
97
- */
98
- function define(name, inputs, emit, {
99
- produces_value = true,
100
- infer_types,
101
- operation_width,
102
- input_widths,
103
- result_width,
104
- } = {}) {
105
- if (descriptions_by_name.has(name)) {
106
- throw new Error(`Duplicate particle node type '${name}'`);
107
- }
108
-
109
- const description = new ParticleNodeDescription();
110
-
111
- description.name = name;
112
- description.id = descriptions_by_name.size;
113
-
114
- // what an author reads is decided in one table, away from the lowering; see
115
- // ./particle_node_presentation.js
116
- const presentation = particle_node_presentation(name);
117
-
118
- description.display_label = presentation.label;
119
- description.icon = presentation.icon;
120
- description.help = presentation.help;
121
-
122
- for (const input of inputs) {
123
- description.createPort(PARTICLE_VALUE_TYPE, input, PortDirection.In);
124
- }
125
-
126
- if (produces_value) {
127
- description.createPort(PARTICLE_VALUE_TYPE, PARTICLE_NODE_OUTPUT_PORT, PortDirection.Out);
128
- }
129
-
130
- description.emit = emit;
131
-
132
- if (infer_types !== undefined) {
133
- description.infer_types = infer_types;
134
- }
135
-
136
- if (operation_width !== undefined) {
137
- description.operation_width = operation_width;
138
- }
139
-
140
- if (input_widths !== undefined) {
141
- description.input_widths = input_widths;
142
- }
143
-
144
- if (result_width !== undefined) {
145
- description.result_width = result_width;
146
- }
147
-
148
- PARTICLE_NODE_REGISTRY.addNode(description);
149
- descriptions_by_name.set(name, description);
150
-
151
- return description;
152
- }
153
-
154
- /** side-effecting sink (no output) */
155
- function sink(name, inputs, emit, options = {}) {
156
- return define(name, inputs, emit, { ...options, produces_value: false });
157
- }
158
-
159
- /**
160
- * A source: no inputs, and a type of its own that its parameters (and the layout) decide.
161
- *
162
- * @param {string} name
163
- * @param {function(object, ParticleLayout|undefined):(ParticleValueType|null)} type_of
164
- * @param {function} emit
165
- * @param {object} [options]
166
- * @returns {ParticleNodeDescription}
167
- */
168
- function source(name, type_of, emit, options = {}) {
169
- return define(name, [], emit, {
170
- ...options,
171
- infer_types: ({ parameters, layout }) => ({ output: type_of(parameters, layout), inputs: {} }),
172
- });
173
- }
174
-
175
- function unary(name, opcode, options = {}) {
176
- return define(name, ["x"], (s, c) => s.emit(opcode, c.out, c.in("x"), 0, 0, c.width), options);
177
- }
178
-
179
- function binary(name, opcode, options = {}) {
180
- return define(name, ["a", "b"], (s, c) => s.emit(opcode, c.out, c.in("a"), c.in("b"), 0, c.width), options);
181
- }
182
-
183
- /**
184
- * Every input read at one type.
185
- *
186
- * @param {Object<string, *>} inputs
187
- * @param {ParticleValueType|null} type
188
- * @returns {Object<string, ParticleValueType|null>}
189
- */
190
- function read_all_as(inputs, type) {
191
- const read = {};
192
-
193
- for (const port of Object.keys(inputs)) {
194
- read[port] = type;
195
- }
196
-
197
- return read;
198
- }
199
-
200
- /**
201
- * @param {Object<string, ParticleValueType>} input_types
202
- * @returns {number} lanes of the first input, which for the reductions is the lanes of every input
203
- */
204
- function first_input_width(input_types) {
205
- return Object.values(input_types)[0].components;
206
- }
207
-
208
- /**
209
- * The same lanes, stripped of what they were for.
210
- *
211
- * A meaning travels through the operations that stay in the value's own space — a colour scaled,
212
- * clamped or mixed is still a colour — and stops at the ones that leave it. A comparison of two
213
- * colours is a mask, and the lane that chooses between two colours is a mask; calling either of
214
- * those a colour would be the type saying something untrue, which is worse than it saying nothing.
215
- *
216
- * @param {ParticleValueType|null} type
217
- * @returns {ParticleValueType|null}
218
- */
219
- function plain_shape(type) {
220
- return type === null ? null : particle_value_type(type.components);
221
- }
222
-
223
- /**
224
- * An op whose result is a mask or a ramp rather than another value of what it read: it compares in
225
- * the operands' own space, and answers in none.
226
- */
227
- const MASK_RESULT = {
228
- infer_types: ({ inputs }) => {
229
- const wide = particle_widest_type(Object.values(inputs));
230
-
231
- return { output: plain_shape(wide), inputs: read_all_as(inputs, wide) };
232
- },
233
- };
234
-
235
- /**
236
- * An op that returns one of the values it was given, or something between them, under the control
237
- * of a third: `mix`'s `t`, `select`'s `condition`.
238
- *
239
- * Two questions with different answers. *How wide* is what it has always been — every operand,
240
- * widest wins, so a pair of scalars chosen between by a three-lane mask is still three lanes of
241
- * result, which is how a vector was assembled before there were constructors for it. *What it
242
- * means* comes only from the ends: the control says which side, not what kind, and a result is only
243
- * a colour if the things being blended were.
244
- *
245
- * @param {string} control the port that steers rather than contributes
246
- * @returns {object} description options
247
- */
248
- function blend_controlled_by(control) {
249
- return {
250
- infer_types: ({ inputs }) => {
251
- const width_from = particle_widest_type(Object.values(inputs));
252
-
253
- if (width_from === null) {
254
- return { output: null, inputs: read_all_as(inputs, null) };
255
- }
256
-
257
- const ends = particle_widest_type([inputs.a, inputs.b]);
258
-
259
- const output = ends !== null && ends.semantic && ends.components === width_from.components
260
- ? ends
261
- : plain_shape(width_from);
262
-
263
- const read = read_all_as(inputs, output);
264
-
265
- read[control] = plain_shape(output);
266
-
267
- return { output, inputs: read };
268
- },
269
- };
270
- }
271
-
272
- /**
273
- * A reduction: reads every lane of its inputs — which are read at one width, the widest among
274
- * them — and writes one, whatever is asked of it. The result is a scalar.
275
- */
276
- const REDUCTION = {
277
- infer_types: ({ inputs }) => {
278
- const wide = particle_widest_type(Object.values(inputs));
279
-
280
- return { output: wide === null ? null : PARTICLE_TYPE_F32, inputs: read_all_as(inputs, wide) };
281
- },
282
- operation_width: ({ input_types }) => first_input_width(input_types),
283
- result_width: () => 1,
284
- };
285
-
286
- /**
287
- * An op that has to see the whole vector to compute any lane of it: a `normalize` feeding a
288
- * one-component attribute still divides by the length of the whole vector rather than by the
289
- * length of its first lane. Its type is its input's; only the width it runs at differs from the
290
- * element-wise default.
291
- */
292
- const WHOLE_VECTOR = {
293
- operation_width: ({ type }) => type.components,
294
- };
295
-
296
- /**
297
- * Comparison operators the single `compare` node type covers, and the opcode each lowers to.
298
- *
299
- * Exported so the authoring metadata in {@link ./particle_node_parameters.js} can offer exactly the
300
- * set this lowers, rather than a hand-copied list that goes stale the moment one is added.
301
- *
302
- * @type {Object<string, number>}
303
- */
304
- export const PARTICLE_COMPARE_OPCODES = Object.freeze({
305
- lt: VM_OP.CMP_LT,
306
- le: VM_OP.CMP_LE,
307
- gt: VM_OP.CMP_GT,
308
- ge: VM_OP.CMP_GE,
309
- eq: VM_OP.CMP_EQ,
310
- });
311
-
312
- /**
313
- * Lane names, in lane order: the input ports of the vector constructors, and how a swizzle is
314
- * spelled.
315
- * @type {string[]}
316
- */
317
- const LANE_NAMES = ["x", "y", "z", "w"];
318
-
319
- /**
320
- * @param {object} parameters
321
- * @returns {number[]} a literal node's value
322
- */
323
- function literal_of(parameters) {
324
- return Array.isArray(parameters.value) ? parameters.value : [];
325
- }
326
-
327
- /**
328
- * Lower a literal into the constant pool, at the lanes it is being read at.
329
- *
330
- * Only those lanes: a scalar literal costs one word of the pool — or, read wider, that word
331
- * repeated, which is the broadcast for free — and a wide literal read narrow interns only its
332
- * leading words. A lane the value does not have reads zero, which is what a `color` written with
333
- * too few numbers gets; the validator reports the parameter, and the compiler does not also throw.
334
- *
335
- * @param {InstructionStream} s
336
- * @param {object} c the emit context
337
- */
338
- function emit_literal(s, c) {
339
- const v = literal_of(c.parameters);
340
-
341
- const components = [];
342
-
343
- for (let i = 0; i < c.width; i++) {
344
- components.push((c.type.is_scalar ? v[0] : v[i]) ?? 0);
345
- }
346
-
347
- s.loadConst(c.out, c.asm.constant(...components), c.width);
348
- }
349
-
350
- /**
351
- * @param {object} parameters
352
- * @returns {number[]} a swizzle's lane selectors, one per output lane
353
- */
354
- function pattern_of(parameters) {
355
- const pattern = parameters.pattern;
356
-
357
- return Array.isArray(pattern) && pattern.length > 0 ? pattern : [0, 1, 2, 3];
358
- }
359
-
360
- /**
361
- * @param {number[]} pattern
362
- * @returns {number} how many lanes of a source the selectors reach into
363
- */
364
- function pattern_span(pattern) {
365
- return 1 + Math.max(...pattern);
366
- }
367
-
368
- /**
369
- * The attribute a node names, as a type; `null` where there is no layout to look in or the name is
370
- * not in it — which the validator reports in its own words.
371
- *
372
- * An attribute the layout has tagged (`color`, say) is that type rather than the plain shape, which
373
- * is how a colour reaches a graph without a node saying so. The tag has to be the shape the
374
- * attribute actually is: three words tagged `color` is a mistake, and reading them as a four-lane
375
- * colour would be a second one, so the shape wins and the tag is ignored.
376
- *
377
- * @param {object} parameters
378
- * @param {ParticleLayout|undefined} layout
379
- * @returns {ParticleValueType|null}
380
- */
381
- function attribute_type(parameters, layout) {
382
- if (layout === undefined || !layout.has(parameters.name)) {
383
- return null;
384
- }
385
-
386
- const attribute = layout.get(parameters.name);
387
-
388
- const semantic = particle_semantic_type(attribute.type);
389
-
390
- if (semantic !== null && semantic.components === attribute.components) {
391
- return semantic;
392
- }
393
-
394
- return particle_value_type(attribute.components);
395
- }
396
-
397
- /**
398
- * Which of a binary op's operands is a scalar meeting a vector — the case `SCALE` covers with no
399
- * broadcast — or `null` where neither or both are.
400
- *
401
- * @param {Object<string, ParticleValueType>} source_types
402
- * @returns {string|null} `"a"`, `"b"` or `null`
403
- */
404
- function scalar_operand(source_types) {
405
- const a = source_types.a.is_scalar;
406
- const b = source_types.b.is_scalar;
407
-
408
- if (a === b) {
409
- return null;
410
- }
411
-
412
- return a ? "a" : "b";
413
- }
414
-
415
- // --- sources ---
416
- // A source has a type of its own, and it is as wide as that: a literal is as wide as it was
417
- // written, a builtin as wide as the host fills it, an attribute as wide as the layout says. A
418
- // consumer wanting more lanes than that is a conversion the compiler makes after the value is
419
- // produced — except where a source can fill lanes for free, which it then does.
420
- source("const", parameters => particle_value_type_or_null(literal_of(parameters).length), emit_literal, {
421
- operation_width: ({ type, demand }) => type.is_scalar ? demand : Math.min(type.components, demand),
422
- });
423
- source(
424
- "builtin",
425
- parameters => particle_value_type_or_null(VM_BUILTIN_COMPONENTS[parameters.id]),
426
- (s, c) => s.loadBuiltin(c.out, c.parameters.id, c.width)
427
- );
428
- source("attribute", attribute_type, (s, c) => {
429
- const a = c.layout.get(c.parameters.name);
430
-
431
- s.loadAttr(c.out, a.offset, a.components, c.width);
432
- });
433
- // One uniform draw in `[0, 1)` per lane, as many lanes as the node says. Shaped distributions are
434
- // compositions over it rather than modes of it — see ./particle_graph_distributions.js.
435
- source("random", parameters => particle_value_type_or_null(parameters.components), (s, c) => s.random(c.out, c.width));
436
-
437
- // --- sinks ---
438
- // A store is exactly as wide as the attribute it writes, and that is how it reads its input.
439
- sink("setAttribute", ["value"], (s, c) => {
440
- const a = c.layout.get(c.parameters.name);
441
-
442
- s.storeAttr(c.in("value"), a.offset, a.components);
443
- }, {
444
- infer_types: ({ parameters, layout }) => ({ output: null, inputs: { value: attribute_type(parameters, layout) } }),
445
- operation_width: ({ parameters, layout }) => layout.get(parameters.name).components,
446
- });
447
- sink("kill", ["condition"], (s, c) => s.kill(c.in("condition")), {
448
- infer_types: () => ({ output: null, inputs: { condition: PARTICLE_TYPE_F32 } }),
449
- operation_width: () => 1,
450
- });
451
-
452
- // --- arithmetic (component-wise) ---
453
- binary("add", VM_OP.ADD);
454
- binary("sub", VM_OP.SUB);
455
- // A vector times a scalar is what `SCALE` is for, and lowering to it here is what keeps `velocity *
456
- // dt` one instruction rather than a broadcast and a multiply.
457
- define("mul", ["a", "b"], (s, c) => {
458
- const scalar = scalar_operand(c.source_types);
459
-
460
- if (scalar === null) {
461
- s.emit(VM_OP.MUL, c.out, c.in("a"), c.in("b"), 0, c.width);
462
- } else {
463
- s.emit(VM_OP.SCALE, c.out, c.in(scalar === "a" ? "b" : "a"), c.in(scalar), 0, c.width);
464
- }
465
- }, {
466
- input_widths: ({ source_types }) => {
467
- const scalar = scalar_operand(source_types);
468
-
469
- return scalar === null ? {} : { [scalar]: 1 };
470
- },
471
- });
472
- binary("div", VM_OP.DIV);
473
- binary("min", VM_OP.MIN);
474
- binary("max", VM_OP.MAX);
475
- binary("pow", VM_OP.POW);
476
- binary("step", VM_OP.STEP, MASK_RESULT);
477
- define("mad", ["a", "b", "c"], (s, c) => s.emit(VM_OP.MAD, c.out, c.in("a"), c.in("b"), c.in("c"), c.width));
478
- define("mix", ["a", "b", "t"], (s, c) => s.emit(VM_OP.MIX, c.out, c.in("a"), c.in("b"), c.in("t"), c.width), blend_controlled_by("t"));
479
- define("clamp", ["x", "lo", "hi"], (s, c) => s.emit(VM_OP.CLAMP, c.out, c.in("x"), c.in("lo"), c.in("hi"), c.width));
480
- define("smoothstep", ["e0", "e1", "x"], (s, c) => s.emit(VM_OP.SMOOTHSTEP, c.out, c.in("e0"), c.in("e1"), c.in("x"), c.width), MASK_RESULT);
481
- // The scalar is one lane; the vector is however wide the result is.
482
- define("scale", ["v", "s"], (s, c) => s.emit(VM_OP.SCALE, c.out, c.in("v"), c.in("s"), 0, c.width), {
483
- infer_types: ({ inputs }) => ({ output: inputs.v, inputs: { v: inputs.v, s: PARTICLE_TYPE_F32 } }),
484
- input_widths: () => ({ s: 1 }),
485
- });
486
-
487
- // --- unary math ---
488
- unary("neg", VM_OP.NEG);
489
- unary("abs", VM_OP.ABS);
490
- unary("floor", VM_OP.FLOOR);
491
- unary("fract", VM_OP.FRACT);
492
- unary("sqrt", VM_OP.SQRT);
493
- unary("sin", VM_OP.SIN);
494
- unary("cos", VM_OP.COS);
495
- unary("exp", VM_OP.EXP);
496
- unary("log", VM_OP.LOG);
497
- unary("normalize", VM_OP.NORMALIZE, WHOLE_VECTOR);
498
- unary("length", VM_OP.LENGTH, REDUCTION);
499
-
500
- // --- vector ---
501
- binary("dot", VM_OP.DOT, REDUCTION);
502
- // A cross product is three lanes in and three out, whatever arrives.
503
- binary("cross", VM_OP.CROSS, {
504
- infer_types: ({ inputs }) => ({ output: PARTICLE_TYPE_VEC3, inputs: read_all_as(inputs, PARTICLE_TYPE_VEC3) }),
505
- operation_width: () => 3,
506
- });
507
- binary("distance", VM_OP.DISTANCE, REDUCTION);
508
- // A swizzle is as wide as its pattern, and its source has to cover the lanes the pattern names —
509
- // which is unrelated to how wide the result is, and is the one place a source's extent is read out
510
- // of a parameter rather than a width.
511
- define("swizzle", ["x"], (s, c) => {
512
- const p = pattern_of(c.parameters);
513
-
514
- s.emit(VM_OP.SWIZZLE, c.out, c.in("x"), pack_swizzle(p[0] ?? 0, p[1] ?? 0, p[2] ?? 0, p[3] ?? 0), 0, c.width);
515
- }, {
516
- infer_types: ({ inputs, parameters }) => {
517
- const p = pattern_of(parameters);
518
- const output = particle_value_type_or_null(p.length);
519
-
520
- if (output === null) {
521
- return { output: null, inputs: { x: null } };
522
- }
523
-
524
- const span = pattern_span(p);
525
- const source = inputs.x;
526
-
527
- // A source that covers the lanes named is read as itself: picking `.x` out of a vec4 is not
528
- // a narrowing. One that does not is read as wide as the pattern reaches, and the lanes it
529
- // has not got read zero — a warning, since `.z` of a vec2 is more likely a slip than a plan.
530
- const x = source !== null && source.components >= span ? source : particle_value_type_or_null(span);
531
-
532
- return { output, inputs: { x } };
533
- },
534
- input_widths: ({ operation_width, parameters }) => ({
535
- x: pattern_span(pattern_of(parameters).slice(0, operation_width)),
536
- }),
537
- });
538
-
539
- // The vector constructors: a value per lane, from scalars. Lowered as one `MOV` per lane into the
540
- // result's run, which is the only way a lane gets written on its own — every other opcode writes a
541
- // run from its start.
542
- for (const components of [2, 3, 4]) {
543
- const inputs = LANE_NAMES.slice(0, components);
544
- const output = particle_value_type(components);
545
-
546
- define(`vec${components}`, inputs, (s, c) => {
547
- const moves = inputs.slice(0, c.width).map((port, lane) => ({ from: c.in(port), to: c.out + lane }));
548
-
549
- // The allocator lets one source sit where the result starts, and nowhere else inside it. A
550
- // move into lane 0 would overwrite that source before a later move could read it into its
551
- // own lane — so it goes first, and the rest are then free to land in any order.
552
- moves.sort((p, q) => Number(q.from === c.out) - Number(p.from === c.out));
553
-
554
- for (const { from, to } of moves) {
555
- if (from !== to) {
556
- s.mov(to, from, 1);
557
- }
558
- }
559
- }, {
560
- infer_types: () => ({ output, inputs: read_all_as(Object.fromEntries(inputs.map(port => [port, null])), PARTICLE_TYPE_F32) }),
561
- input_widths: () => Object.fromEntries(inputs.map(port => [port, 1])),
562
- });
563
- }
564
-
565
- // --- logic ---
566
- define("compare", ["a", "b"], (s, c) => {
567
- const opcode = PARTICLE_COMPARE_OPCODES[c.parameters.op];
568
- if (opcode === undefined) {
569
- throw new Error(`Unknown compare op '${c.parameters.op}'`);
570
- }
571
- s.emit(opcode, c.out, c.in("a"), c.in("b"), 0, c.width);
572
- }, MASK_RESULT);
573
- define("select", ["a", "b", "condition"], (s, c) => s.emit(VM_OP.SELECT, c.out, c.in("a"), c.in("b"), c.in("condition"), c.width), blend_controlled_by("condition"));
574
-
575
- // --- curves ---
576
- // One lane of `t` in, one value out — which the opcode writes across every lane it is asked for, so
577
- // a curve read wider than a scalar is the broadcast for free.
578
- define("curve", ["t"], (s, c) => s.curve(c.out, c.parameters.handle, c.in("t"), c.width), {
579
- infer_types: () => ({ output: PARTICLE_TYPE_F32, inputs: { t: PARTICLE_TYPE_F32 } }),
580
- operation_width: ({ demand }) => demand,
581
- input_widths: () => ({ t: 1 }),
582
- });
583
-
584
- // --- semantic sources ---
585
- // Appended rather than filed with the other sources, because a node type's id is its position in
586
- // this file and the ids are a wire format: inserting one up there renumbers every type below it.
587
- // Where an author finds it is the palette's business, and the palette lists it among the sources.
588
-
589
- // A literal colour. A `const` of four numbers in every respect the compiler sees — it interns the
590
- // same pool words and loads them with the same instruction — and a colour to everything above:
591
- // a swatch to edit rather than four fields, and its own colour along the wire to wherever it is
592
- // written.
593
- source("color", () => PARTICLE_TYPE_COLOR, emit_literal);
594
-
595
- /**
596
- * Every node type in the library, in declaration order.
597
- *
598
- * The registry is keyed by the numeric ids assigned above, so this is the only way to walk the
599
- * library by the names authoring uses — which is what a node palette lists.
600
- *
601
- * @returns {ParticleNodeDescription[]}
602
- */
603
- export function particle_node_descriptions() {
604
- return Array.from(descriptions_by_name.values());
605
- }
606
-
607
- /**
608
- * Look up a node type by name.
609
- * @param {string} name
610
- * @returns {ParticleNodeDescription}
611
- * @throws if no such node type exists
612
- */
613
- export function get_particle_node_description(name) {
614
- const description = descriptions_by_name.get(name);
615
-
616
- if (description === undefined) {
617
- throw new Error(`Unknown particle node type '${name}'`);
618
- }
619
-
620
- return description;
621
- }
1
+ import { NodeRegistry } from "../../../../core/model/node-graph/node/NodeRegistry.js";
2
+ import { PortDirection } from "../../../../core/model/node-graph/node/PortDirection.js";
3
+ import { DataType } from "../../../../core/model/node-graph/type/DataType.js";
4
+ import {
5
+ pack_swizzle,
6
+ VM_BUILTIN_COMPONENTS,
7
+ VM_OP,
8
+ VM_SWIZZLE_BROADCAST,
9
+ vm_immediate,
10
+ vm_register,
11
+ } from "../isa/ParticleVMISA.js";
12
+ import { PARTICLE_COMPARE_OPCODES } from "./particle_compare_ops.js";
13
+ import { ParticleGroupBuilder } from "./ParticleGroupBuilder.js";
14
+ import { particle_node_presentation } from "./particle_node_presentation.js";
15
+ import { ParticleNodeDescription } from "./ParticleNodeDescription.js";
16
+ import {
17
+ PARTICLE_GRADIENT_COLOR_COMPONENTS,
18
+ roll_out_particle_curve,
19
+ roll_out_particle_gradient,
20
+ } from "./particle_ramp.js";
21
+ import {
22
+ PARTICLE_ALL_VALUE_TYPES,
23
+ PARTICLE_TYPE_COLOR,
24
+ PARTICLE_TYPE_F32,
25
+ PARTICLE_TYPE_VEC3,
26
+ particle_semantic_type,
27
+ particle_value_type,
28
+ particle_value_type_or_null,
29
+ particle_widest_type,
30
+ } from "./ParticleValueType.js";
31
+
32
+ /**
33
+ * Standard particle-node library, expressed as {@link ParticleNodeDescription}s in a
34
+ * {@link NodeRegistry}. Nodes are value-producing operations (constants, builtins, attribute reads,
35
+ * math, curves, random) or sinks (attribute writes, kill).
36
+ *
37
+ * These lower to the same opcodes the JS reference + WGSL interpreter agree on, so any effect
38
+ * authored here is executable and testable end-to-end on the CPU before it ever hits the GPU.
39
+ *
40
+ * ## Types
41
+ *
42
+ * A value is a float vector of one to four lanes, and which it is is a property of the node
43
+ * instance rather than of its type: a `builtin` is a scalar or a `vec3` by which builtin it reads,
44
+ * a `const` is as wide as the literal it holds, an `add` is as wide as the wider of what it adds.
45
+ * So every port is declared with the one {@link PARTICLE_VALUE_TYPE} — any output may be wired to
46
+ * any input — and each node type says how it is typed through
47
+ * {@link ParticleNodeDescription#infer_types}, which {@link ./infer_particle_graph_types.js}
48
+ * resolves over a whole graph. The types themselves are in {@link ./ParticleValueType.js}, and
49
+ * registered here so a view can colour by them.
50
+ *
51
+ * A type may also say what a value *means* — {@link PARTICLE_TYPE_COLOR} is four lanes that are a
52
+ * colour — and the rules below never ask. They compare `components` and take the widest, so a
53
+ * colour is accepted wherever a `vec4` is, produces whatever a `vec4` would, and survives the
54
+ * arithmetic in between. The two places a colour enters a graph are the `color` node and an
55
+ * attribute the effect's layout has tagged as one.
56
+ */
57
+
58
+ /**
59
+ * The declared type of every port in the library: a value goes here.
60
+ *
61
+ * Which value — one lane or four — is resolved per node instance, see the note on types above.
62
+ * Declared once for all ports because the graph model connects ports whose declared types are the
63
+ * same object, and any particle value may drive any particle input; where the widths differ, the
64
+ * compiler converts and the validator says so.
65
+ *
66
+ * @type {DataType}
67
+ */
68
+ export const PARTICLE_VALUE_TYPE = DataType.from(0, "value");
69
+
70
+ /**
71
+ * Name of the single output port carried by every value-producing node.
72
+ * @type {string}
73
+ */
74
+ export const PARTICLE_NODE_OUTPUT_PORT = "out";
75
+
76
+ /**
77
+ * @type {NodeRegistry}
78
+ */
79
+ export const PARTICLE_NODE_REGISTRY = new NodeRegistry();
80
+
81
+ // The resolved types are the registry's too: a view colours a wire by a type's id, and looks the
82
+ // id up here.
83
+ for (const type of PARTICLE_ALL_VALUE_TYPES) {
84
+ PARTICLE_NODE_REGISTRY.addType(type);
85
+ }
86
+
87
+ /**
88
+ * Node descriptions are addressed by name while authoring; the registry itself is keyed by the
89
+ * numeric IDs assigned below.
90
+ *
91
+ * IDs are handed out in declaration order rather than by {@link NodeDescription}'s global counter,
92
+ * so they do not drift with unrelated node usage elsewhere in the engine. Appending new node types
93
+ * at the end of the library is therefore safe; reordering or removing existing ones renumbers every
94
+ * node that follows, which would invalidate already-serialized graphs.
95
+ *
96
+ * @type {Map<string, ParticleNodeDescription>}
97
+ */
98
+ const descriptions_by_name = new Map();
99
+
100
+ /**
101
+ * @param {string} name
102
+ * @param {string[]} inputs input port names, in operand order
103
+ * @param {function} emit see {@link ParticleNodeDescription#emit}
104
+ * @param {object} [options]
105
+ * @param {boolean} [options.produces_value] when false the node is a sink and gets no output port
106
+ * @param {function} [options.infer_types] see {@link ParticleNodeDescription#infer_types}
107
+ * @param {function} [options.operation_width] see {@link ParticleNodeDescription#operation_width}
108
+ * @param {function} [options.input_widths] see {@link ParticleNodeDescription#input_widths}
109
+ * @param {function} [options.result_width] see {@link ParticleNodeDescription#result_width}
110
+ * @returns {ParticleNodeDescription}
111
+ */
112
+ function define(name, inputs, emit, {
113
+ produces_value = true,
114
+ infer_types,
115
+ operation_width,
116
+ input_widths,
117
+ result_width,
118
+ } = {}) {
119
+ if (descriptions_by_name.has(name)) {
120
+ throw new Error(`Duplicate particle node type '${name}'`);
121
+ }
122
+
123
+ const description = new ParticleNodeDescription();
124
+
125
+ description.name = name;
126
+ description.id = descriptions_by_name.size;
127
+
128
+ // what an author reads is decided in one table, away from the lowering; see
129
+ // ./particle_node_presentation.js
130
+ const presentation = particle_node_presentation(name);
131
+
132
+ description.display_label = presentation.label;
133
+ description.icon = presentation.icon;
134
+ description.help = presentation.help;
135
+
136
+ for (const input of inputs) {
137
+ description.createPort(PARTICLE_VALUE_TYPE, input, PortDirection.In);
138
+ }
139
+
140
+ if (produces_value) {
141
+ description.createPort(PARTICLE_VALUE_TYPE, PARTICLE_NODE_OUTPUT_PORT, PortDirection.Out);
142
+ }
143
+
144
+ description.emit = emit;
145
+
146
+ if (infer_types !== undefined) {
147
+ description.infer_types = infer_types;
148
+ }
149
+
150
+ if (operation_width !== undefined) {
151
+ description.operation_width = operation_width;
152
+ }
153
+
154
+ if (input_widths !== undefined) {
155
+ description.input_widths = input_widths;
156
+ }
157
+
158
+ if (result_width !== undefined) {
159
+ description.result_width = result_width;
160
+ }
161
+
162
+ PARTICLE_NODE_REGISTRY.addNode(description);
163
+ descriptions_by_name.set(name, description);
164
+
165
+ return description;
166
+ }
167
+
168
+ /** side-effecting sink (no output) */
169
+ function sink(name, inputs, emit, options = {}) {
170
+ return define(name, inputs, emit, { ...options, produces_value: false });
171
+ }
172
+
173
+ /**
174
+ * A source: no inputs, and a type of its own that its parameters (and the layout) decide.
175
+ *
176
+ * @param {string} name
177
+ * @param {function(object, ParticleLayout|undefined):(ParticleValueType|null)} type_of
178
+ * @param {function} emit
179
+ * @param {object} [options]
180
+ * @returns {ParticleNodeDescription}
181
+ */
182
+ function source(name, type_of, emit, options = {}) {
183
+ return define(name, [], emit, {
184
+ ...options,
185
+ infer_types: ({ parameters, layout }) => ({ output: type_of(parameters, layout), inputs: {} }),
186
+ });
187
+ }
188
+
189
+ function unary(name, opcode, options = {}) {
190
+ return define(name, ["x"], (s, c) => s.emit(opcode, c.out, c.in("x"), 0, 0, c.width), options);
191
+ }
192
+
193
+ function binary(name, opcode, options = {}) {
194
+ return define(name, ["a", "b"], (s, c) => s.emit(opcode, c.out, c.in("a"), c.in("b"), 0, c.width), options);
195
+ }
196
+
197
+ /**
198
+ * Every input read at one type.
199
+ *
200
+ * @param {Object<string, *>} inputs
201
+ * @param {ParticleValueType|null} type
202
+ * @returns {Object<string, ParticleValueType|null>}
203
+ */
204
+ function read_all_as(inputs, type) {
205
+ const read = {};
206
+
207
+ for (const port of Object.keys(inputs)) {
208
+ read[port] = type;
209
+ }
210
+
211
+ return read;
212
+ }
213
+
214
+ /**
215
+ * @param {Object<string, ParticleValueType>} input_types
216
+ * @returns {number} lanes of the first input, which for the reductions is the lanes of every input
217
+ */
218
+ function first_input_width(input_types) {
219
+ return Object.values(input_types)[0].components;
220
+ }
221
+
222
+ /**
223
+ * The same lanes, stripped of what they were for.
224
+ *
225
+ * A meaning travels through the operations that stay in the value's own space — a colour scaled,
226
+ * clamped or mixed is still a colour — and stops at the ones that leave it. A comparison of two
227
+ * colours is a mask, and the lane that chooses between two colours is a mask; calling either of
228
+ * those a colour would be the type saying something untrue, which is worse than it saying nothing.
229
+ *
230
+ * @param {ParticleValueType|null} type
231
+ * @returns {ParticleValueType|null}
232
+ */
233
+ function plain_shape(type) {
234
+ return type === null ? null : particle_value_type(type.components);
235
+ }
236
+
237
+ /**
238
+ * An op whose result is a mask or a ramp rather than another value of what it read: it compares in
239
+ * the operands' own space, and answers in none.
240
+ */
241
+ const MASK_RESULT = {
242
+ infer_types: ({ inputs }) => {
243
+ const wide = particle_widest_type(Object.values(inputs));
244
+
245
+ return { output: plain_shape(wide), inputs: read_all_as(inputs, wide) };
246
+ },
247
+ };
248
+
249
+ /**
250
+ * An op that returns one of the values it was given, or something between them, under the control
251
+ * of a third: `mix`'s `t`, `select`'s `condition`.
252
+ *
253
+ * Two questions with different answers. *How wide* is what it has always been — every operand,
254
+ * widest wins, so a pair of scalars chosen between by a three-lane mask is still three lanes of
255
+ * result, which is how a vector was assembled before there were constructors for it. *What it
256
+ * means* comes only from the ends: the control says which side, not what kind, and a result is only
257
+ * a colour if the things being blended were.
258
+ *
259
+ * @param {string} control the port that steers rather than contributes
260
+ * @returns {object} description options
261
+ */
262
+ function blend_controlled_by(control) {
263
+ return {
264
+ infer_types: ({ inputs }) => {
265
+ const width_from = particle_widest_type(Object.values(inputs));
266
+
267
+ if (width_from === null) {
268
+ return { output: null, inputs: read_all_as(inputs, null) };
269
+ }
270
+
271
+ const ends = particle_widest_type([inputs.a, inputs.b]);
272
+
273
+ const output = ends !== null && ends.semantic && ends.components === width_from.components
274
+ ? ends
275
+ : plain_shape(width_from);
276
+
277
+ const read = read_all_as(inputs, output);
278
+
279
+ read[control] = plain_shape(output);
280
+
281
+ return { output, inputs: read };
282
+ },
283
+ };
284
+ }
285
+
286
+ /**
287
+ * A reduction: reads every lane of its inputs — which are read at one width, the widest among
288
+ * them — and writes one, whatever is asked of it. The result is a scalar.
289
+ */
290
+ const REDUCTION = {
291
+ infer_types: ({ inputs }) => {
292
+ const wide = particle_widest_type(Object.values(inputs));
293
+
294
+ return { output: wide === null ? null : PARTICLE_TYPE_F32, inputs: read_all_as(inputs, wide) };
295
+ },
296
+ operation_width: ({ input_types }) => first_input_width(input_types),
297
+ result_width: () => 1,
298
+ };
299
+
300
+ /**
301
+ * An op that has to see the whole vector to compute any lane of it: a `normalize` feeding a
302
+ * one-component attribute still divides by the length of the whole vector rather than by the
303
+ * length of its first lane. Its type is its input's; only the width it runs at differs from the
304
+ * element-wise default.
305
+ */
306
+ const WHOLE_VECTOR = {
307
+ operation_width: ({ type }) => type.components,
308
+ };
309
+
310
+
311
+
312
+ /**
313
+ * Lane names, in lane order: the input ports of the vector constructors, and how a swizzle is
314
+ * spelled.
315
+ * @type {string[]}
316
+ */
317
+ const LANE_NAMES = ["x", "y", "z", "w"];
318
+
319
+ /**
320
+ * @param {object} parameters
321
+ * @returns {number[]} a literal node's value
322
+ */
323
+ function literal_of(parameters) {
324
+ return Array.isArray(parameters.value) ? parameters.value : [];
325
+ }
326
+
327
+ /**
328
+ * Lower a literal into the constant pool, at the lanes it is being read at.
329
+ *
330
+ * Only those lanes: a scalar literal costs one word of the pool — or, read wider, that word
331
+ * repeated, which is the broadcast for free — and a wide literal read narrow interns only its
332
+ * leading words. A lane the value does not have reads zero, which is what a `color` written with
333
+ * too few numbers gets; the validator reports the parameter, and the compiler does not also throw.
334
+ *
335
+ * @param {InstructionStream} s
336
+ * @param {object} c the emit context
337
+ */
338
+ function emit_literal(s, c) {
339
+ const v = literal_of(c.parameters);
340
+
341
+ const components = [];
342
+
343
+ for (let i = 0; i < c.width; i++) {
344
+ components.push((c.type.is_scalar ? v[0] : v[i]) ?? 0);
345
+ }
346
+
347
+ s.loadConst(c.out, c.asm.constant(...components), c.width);
348
+ }
349
+
350
+ /**
351
+ * @param {object} parameters
352
+ * @returns {number[]} a swizzle's lane selectors, one per output lane
353
+ */
354
+ function pattern_of(parameters) {
355
+ const pattern = parameters.pattern;
356
+
357
+ return Array.isArray(pattern) && pattern.length > 0 ? pattern : [0, 1, 2, 3];
358
+ }
359
+
360
+ /**
361
+ * @param {number[]} pattern
362
+ * @returns {number} how many lanes of a source the selectors reach into
363
+ */
364
+ function pattern_span(pattern) {
365
+ return 1 + Math.max(...pattern);
366
+ }
367
+
368
+ /**
369
+ * The attribute a node names, as a type; `null` where there is no layout to look in or the name is
370
+ * not in it — which the validator reports in its own words.
371
+ *
372
+ * An attribute the layout has tagged (`color`, say) is that type rather than the plain shape, which
373
+ * is how a colour reaches a graph without a node saying so. The tag has to be the shape the
374
+ * attribute actually is: three words tagged `color` is a mistake, and reading them as a four-lane
375
+ * colour would be a second one, so the shape wins and the tag is ignored.
376
+ *
377
+ * @param {object} parameters
378
+ * @param {ParticleLayout|undefined} layout
379
+ * @returns {ParticleValueType|null}
380
+ */
381
+ function attribute_type(parameters, layout) {
382
+ if (layout === undefined || !layout.has(parameters.name)) {
383
+ return null;
384
+ }
385
+
386
+ const attribute = layout.get(parameters.name);
387
+
388
+ const semantic = particle_semantic_type(attribute.type);
389
+
390
+ if (semantic !== null && semantic.components === attribute.components) {
391
+ return semantic;
392
+ }
393
+
394
+ return particle_value_type(attribute.components);
395
+ }
396
+
397
+ /**
398
+ * Which of a binary op's operands is a scalar meeting a vector — read through `.xxxx` with no
399
+ * broadcast instruction — or `null` where neither or both are.
400
+ *
401
+ * @param {Object<string, ParticleValueType>} source_types
402
+ * @returns {string|null} `"a"`, `"b"` or `null`
403
+ */
404
+ function scalar_operand(source_types) {
405
+ const a = source_types.a.is_scalar;
406
+ const b = source_types.b.is_scalar;
407
+
408
+ if (a === b) {
409
+ return null;
410
+ }
411
+
412
+ return a ? "a" : "b";
413
+ }
414
+
415
+ // --- sources ---
416
+ // A source has a type of its own, and it is as wide as that: a literal is as wide as it was
417
+ // written, a builtin as wide as the host fills it, an attribute as wide as the layout says. A
418
+ // consumer wanting more lanes than that is a conversion the compiler makes after the value is
419
+ // produced — except where a source can fill lanes for free, which it then does.
420
+ source("const", parameters => particle_value_type_or_null(literal_of(parameters).length), emit_literal, {
421
+ operation_width: ({ type, demand }) => type.is_scalar ? demand : Math.min(type.components, demand),
422
+ });
423
+ source(
424
+ "builtin",
425
+ parameters => particle_value_type_or_null(VM_BUILTIN_COMPONENTS[parameters.id]),
426
+ (s, c) => s.loadBuiltin(c.out, c.parameters.id, c.width)
427
+ );
428
+ source("attribute", attribute_type, (s, c) => {
429
+ const a = c.layout.get(c.parameters.name);
430
+
431
+ s.loadAttr(c.out, a.offset, a.components, c.width);
432
+ });
433
+ // One uniform draw in `[0, 1)` per lane, as many lanes as the node says. Shaped distributions are
434
+ // compositions over it rather than modes of it — see ./particle_graph_distributions.js.
435
+ source("random", parameters => particle_value_type_or_null(parameters.components), (s, c) => s.random(c.out, c.width));
436
+
437
+ // --- sinks ---
438
+ // A store is exactly as wide as the attribute it writes, and that is how it reads its input.
439
+ sink("setAttribute", ["value"], (s, c) => {
440
+ const a = c.layout.get(c.parameters.name);
441
+
442
+ s.storeAttr(c.in("value"), a.offset, a.components);
443
+ }, {
444
+ infer_types: ({ parameters, layout }) => ({ output: null, inputs: { value: attribute_type(parameters, layout) } }),
445
+ operation_width: ({ parameters, layout }) => layout.get(parameters.name).components,
446
+ });
447
+ sink("kill", ["condition"], (s, c) => s.kill(c.in("condition")), {
448
+ infer_types: () => ({ output: null, inputs: { condition: PARTICLE_TYPE_F32 } }),
449
+ operation_width: () => 1,
450
+ });
451
+
452
+ // --- arithmetic (component-wise) ---
453
+ binary("add", VM_OP.ADD);
454
+ binary("sub", VM_OP.SUB);
455
+ // A vector times a scalar reads the scalar through `.xxxx`, which is what keeps `velocity * dt` one
456
+ // instruction rather than a broadcast and a multiply.
457
+ define("mul", ["a", "b"], (s, c) => {
458
+ const scalar = scalar_operand(c.source_types);
459
+
460
+ if (scalar === null) {
461
+ s.emit(VM_OP.MUL, c.out, c.in("a"), c.in("b"), 0, c.width);
462
+ } else {
463
+ s.emit(VM_OP.MUL, c.out, c.in(scalar === "a" ? "b" : "a"), vm_register(c.in(scalar), VM_SWIZZLE_BROADCAST), 0, c.width);
464
+ }
465
+ }, {
466
+ input_widths: ({ source_types }) => {
467
+ const scalar = scalar_operand(source_types);
468
+
469
+ return scalar === null ? {} : { [scalar]: 1 };
470
+ },
471
+ });
472
+ binary("div", VM_OP.DIV);
473
+ binary("min", VM_OP.MIN);
474
+ binary("max", VM_OP.MAX);
475
+ binary("pow", VM_OP.POW);
476
+ binary("step", VM_OP.STEP, MASK_RESULT);
477
+ define("mad", ["a", "b", "c"], (s, c) => s.emit(VM_OP.MAD, c.out, c.in("a"), c.in("b"), c.in("c"), c.width));
478
+ define("mix", ["a", "b", "t"], (s, c) => s.emit(VM_OP.MIX, c.out, c.in("a"), c.in("b"), c.in("t"), c.width), blend_controlled_by("t"));
479
+ define("clamp", ["x", "lo", "hi"], (s, c) => s.emit(VM_OP.CLAMP, c.out, c.in("x"), c.in("lo"), c.in("hi"), c.width));
480
+ define("smoothstep", ["e0", "e1", "x"], (s, c) => s.emit(VM_OP.SMOOTHSTEP, c.out, c.in("e0"), c.in("e1"), c.in("x"), c.width), MASK_RESULT);
481
+ // The scalar is one lane; the vector is however wide the result is.
482
+ define("scale", ["v", "s"], (s, c) => s.emit(VM_OP.MUL, c.out, c.in("v"), vm_register(c.in("s"), VM_SWIZZLE_BROADCAST), 0, c.width), {
483
+ infer_types: ({ inputs }) => ({ output: inputs.v, inputs: { v: inputs.v, s: PARTICLE_TYPE_F32 } }),
484
+ input_widths: () => ({ s: 1 }),
485
+ });
486
+
487
+ // --- unary math ---
488
+ unary("neg", VM_OP.NEG);
489
+ unary("abs", VM_OP.ABS);
490
+ unary("floor", VM_OP.FLOOR);
491
+ unary("fract", VM_OP.FRACT);
492
+ unary("sqrt", VM_OP.SQRT);
493
+ unary("sin", VM_OP.SIN);
494
+ unary("cos", VM_OP.COS);
495
+ unary("exp", VM_OP.EXP);
496
+ unary("log", VM_OP.LOG);
497
+ unary("normalize", VM_OP.NORMALIZE, WHOLE_VECTOR);
498
+ unary("length", VM_OP.LENGTH, REDUCTION);
499
+
500
+ // --- vector ---
501
+ binary("dot", VM_OP.DOT, REDUCTION);
502
+ // A cross product is three lanes in and three out, whatever arrives.
503
+ binary("cross", VM_OP.CROSS, {
504
+ infer_types: ({ inputs }) => ({ output: PARTICLE_TYPE_VEC3, inputs: read_all_as(inputs, PARTICLE_TYPE_VEC3) }),
505
+ operation_width: () => 3,
506
+ });
507
+ binary("distance", VM_OP.DISTANCE, REDUCTION);
508
+ // A swizzle is as wide as its pattern, and its source has to cover the lanes the pattern names —
509
+ // which is unrelated to how wide the result is, and is the one place a source's extent is read out
510
+ // of a parameter rather than a width.
511
+ define("swizzle", ["x"], (s, c) => {
512
+ const p = pattern_of(c.parameters);
513
+
514
+ s.emit(VM_OP.MOV, c.out, vm_register(c.in("x"), pack_swizzle(p[0] ?? 0, p[1] ?? 0, p[2] ?? 0, p[3] ?? 0)), 0, 0, c.width);
515
+ }, {
516
+ infer_types: ({ inputs, parameters }) => {
517
+ const p = pattern_of(parameters);
518
+ const output = particle_value_type_or_null(p.length);
519
+
520
+ if (output === null) {
521
+ return { output: null, inputs: { x: null } };
522
+ }
523
+
524
+ const span = pattern_span(p);
525
+ const source = inputs.x;
526
+
527
+ // A source that covers the lanes named is read as itself: picking `.x` out of a vec4 is not
528
+ // a narrowing. One that does not is read as wide as the pattern reaches, and the lanes it
529
+ // has not got read zero — a warning, since `.z` of a vec2 is more likely a slip than a plan.
530
+ const x = source !== null && source.components >= span ? source : particle_value_type_or_null(span);
531
+
532
+ return { output, inputs: { x } };
533
+ },
534
+ input_widths: ({ operation_width, parameters }) => ({
535
+ x: pattern_span(pattern_of(parameters).slice(0, operation_width)),
536
+ }),
537
+ });
538
+
539
+ // The vector constructors: a value per lane, from scalars. Lowered as one `MOV` per lane into the
540
+ // result's run, which is the only way a lane gets written on its own — every other opcode writes a
541
+ // run from its start.
542
+ for (const components of [2, 3, 4]) {
543
+ const inputs = LANE_NAMES.slice(0, components);
544
+ const output = particle_value_type(components);
545
+
546
+ define(`vec${components}`, inputs, (s, c) => {
547
+ const moves = inputs.slice(0, c.width).map((port, lane) => ({ from: c.in(port), to: c.out + lane }));
548
+
549
+ // The allocator lets one source sit where the result starts, and nowhere else inside it. A
550
+ // move into lane 0 would overwrite that source before a later move could read it into its
551
+ // own lane — so it goes first, and the rest are then free to land in any order.
552
+ moves.sort((p, q) => Number(q.from === c.out) - Number(p.from === c.out));
553
+
554
+ for (const { from, to } of moves) {
555
+ if (from !== to) {
556
+ s.mov(to, from, 1);
557
+ }
558
+ }
559
+ }, {
560
+ infer_types: () => ({ output, inputs: read_all_as(Object.fromEntries(inputs.map(port => [port, null])), PARTICLE_TYPE_F32) }),
561
+ input_widths: () => Object.fromEntries(inputs.map(port => [port, 1])),
562
+ });
563
+ }
564
+
565
+ // --- logic ---
566
+ define("compare", ["a", "b"], (s, c) => {
567
+ const opcode = PARTICLE_COMPARE_OPCODES[c.parameters.op];
568
+ if (opcode === undefined) {
569
+ throw new Error(`Unknown compare op '${c.parameters.op}'`);
570
+ }
571
+ s.emit(opcode, c.out, c.in("a"), c.in("b"), 0, c.width);
572
+ }, MASK_RESULT);
573
+ define("select", ["a", "b", "condition"], (s, c) => s.emit(VM_OP.SELECT, c.out, c.in("a"), c.in("b"), c.in("condition"), c.width), blend_controlled_by("condition"));
574
+
575
+ // --- curves ---
576
+ // One lane of `t` in, one value out — which the opcode writes across every lane it is asked for, so
577
+ // a curve read wider than a scalar is the broadcast for free.
578
+ define("curve", ["t"], (s, c) => s.curve(c.out, c.parameters.handle, c.in("t"), c.width), {
579
+ infer_types: () => ({ output: PARTICLE_TYPE_F32, inputs: { t: PARTICLE_TYPE_F32 } }),
580
+ operation_width: ({ demand }) => demand,
581
+ input_widths: () => ({ t: 1 }),
582
+ });
583
+
584
+ // --- semantic sources ---
585
+ // Appended rather than filed with the other sources, because a node type's id is its position in
586
+ // this file and the ids are a wire format: inserting one up there renumbers every type below it.
587
+ // Where an author finds it is the palette's business, and the palette lists it among the sources.
588
+
589
+ // A literal colour. A `const` of four numbers in every respect the compiler sees — it interns the
590
+ // same pool words and loads them with the same instruction — and a colour to everything above:
591
+ // a swatch to edit rather than four fields, and its own colour along the wire to wherever it is
592
+ // written.
593
+ source("color", () => PARTICLE_TYPE_COLOR, emit_literal);
594
+
595
+ // --- more math ---
596
+ // Appended for the same reason `color` is: these belong with the unary math above and cannot be
597
+ // written there without renumbering every type that follows. The palette files them where they
598
+ // read.
599
+
600
+ unary("rsqrt", VM_OP.RSQRT);
601
+ unary("tan", VM_OP.TAN);
602
+ unary("asin", VM_OP.ASIN);
603
+ unary("acos", VM_OP.ACOS);
604
+ // The one binary among them, and the one whose ports are not interchangeable: `atan2(y, x)` is the
605
+ // angle of the point, so the ports are named for the axes rather than `a` and `b`.
606
+ define("atan2", ["y", "x"], (s, c) => s.emit(VM_OP.ATAN2, c.out, c.in("y"), c.in("x"), 0, c.width));
607
+
608
+ // The `clamp` everybody writes, with the bounds written in rather than wired up. Both of them are
609
+ // literals of one lane, so they are immediates in the instruction word: the node costs one
610
+ // instruction and not one register, where a `clamp` between two `const` nodes costs three and as
611
+ // many registers as it is wide. Nothing else in the library takes an immediate this way, because
612
+ // nothing else in the library knows what its operands are before the graph is drawn.
613
+ define("saturate", ["x"], (s, c) => s.emit(VM_OP.CLAMP, c.out, c.in("x"), vm_immediate(0), vm_immediate(1), c.width));
614
+
615
+ // --- ramps ---
616
+ // A curve and a gradient: authored data, lowered by being written out.
617
+ //
618
+ // These are the library's expanding types. They are made of other nodes, like a
619
+ // {@link ./ParticleNodeGroup.js group} is, but the nodes they are made of come from their own
620
+ // parameters rather than from a body fixed when the library loaded — a gradient with five stops is
621
+ // different arithmetic from a gradient with two, and no one body could be both. So instead of an
622
+ // `emit` they carry an {@link ./ParticleNodeDescription.js#expand}, which
623
+ // {@link ./flatten_particle_graph.js} calls and splices in exactly as it splices a group. What each
624
+ // one expands *to* is in {@link ./particle_ramp.js}, which is also where the reasoning about the
625
+ // arithmetic lives; this is only the declaration.
626
+
627
+ /**
628
+ * Declare a type whose body is built from its own parameters.
629
+ *
630
+ * @param {string} name
631
+ * @param {Array<{name: string, type: ParticleValueType|null}>} inputs
632
+ * @param {ParticleValueType} output what the node produces, and what the expansion's `out` is
633
+ * @param {function(ParticleGroupBuilder, object):*} body given a builder and the instance's
634
+ * parameters, returns the value to expose as `out`
635
+ * @returns {ParticleNodeDescription}
636
+ */
637
+ function expanding(name, inputs, output, body) {
638
+ const description = define(name, inputs.map(port => port.name), () => {
639
+ throw new Error(
640
+ `'${name}' reached the compiler. An expanding type is written out before lowering; `
641
+ + "see flatten_particle_graph.js."
642
+ );
643
+ }, {
644
+ infer_types: ({ inputs: wired }) => ({
645
+ output,
646
+ inputs: Object.fromEntries(inputs.map(port => [port.name, port.type ?? wired[port.name]])),
647
+ }),
648
+ });
649
+
650
+ description.expand = parameters => {
651
+ const g = new ParticleGroupBuilder({
652
+ name,
653
+ label: description.display_label,
654
+ icon: description.icon,
655
+ help: description.help,
656
+ inputs,
657
+ outputs: [{ name: PARTICLE_NODE_OUTPUT_PORT, type: output }],
658
+ });
659
+
660
+ g.out(PARTICLE_NODE_OUTPUT_PORT, body(g, parameters));
661
+
662
+ return g.build();
663
+ };
664
+
665
+ return description;
666
+ }
667
+
668
+ // One curve, written into the program. Not the `curve` node above: that one samples a row of the
669
+ // host's animation-curve table and this one *is* the curve, the way `color` is a colour rather than
670
+ // the name of one.
671
+ expanding(
672
+ "curveLiteral",
673
+ [{ name: "t", type: PARTICLE_TYPE_F32 }],
674
+ PARTICLE_TYPE_F32,
675
+ (g, parameters) => roll_out_particle_curve(g, g.in.t, parameters.curve)
676
+ );
677
+
678
+ // The two gradients. One body, read three lanes wide or four — an RGB gradient still *stores* four
679
+ // channels per stop, so that a gradient copied from one node to the other keeps the alpha it was
680
+ // authored with rather than losing it on the way through.
681
+ expanding(
682
+ "gradientRGBA",
683
+ [{ name: "t", type: PARTICLE_TYPE_F32 }],
684
+ PARTICLE_TYPE_COLOR,
685
+ (g, parameters) => roll_out_particle_gradient(g, g.in.t, parameters.stops, PARTICLE_GRADIENT_COLOR_COMPONENTS)
686
+ );
687
+ expanding(
688
+ "gradientRGB",
689
+ [{ name: "t", type: PARTICLE_TYPE_F32 }],
690
+ PARTICLE_TYPE_VEC3,
691
+ (g, parameters) => roll_out_particle_gradient(g, g.in.t, parameters.stops, PARTICLE_TYPE_VEC3.components)
692
+ );
693
+
694
+ /**
695
+ * Every node type in the library, in declaration order.
696
+ *
697
+ * The registry is keyed by the numeric ids assigned above, so this is the only way to walk the
698
+ * library by the names authoring uses — which is what a node palette lists.
699
+ *
700
+ * @returns {ParticleNodeDescription[]}
701
+ */
702
+ export function particle_node_descriptions() {
703
+ return Array.from(descriptions_by_name.values());
704
+ }
705
+
706
+ /**
707
+ * Look up a node type by name.
708
+ * @param {string} name
709
+ * @returns {ParticleNodeDescription}
710
+ * @throws if no such node type exists
711
+ */
712
+ export function get_particle_node_description(name) {
713
+ const description = descriptions_by_name.get(name);
714
+
715
+ if (description === undefined) {
716
+ throw new Error(`Unknown particle node type '${name}'`);
717
+ }
718
+
719
+ return description;
720
+ }