@woosh/meep-engine 3.60.4 → 3.60.5

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 (379) hide show
  1. package/CHANGELOG.md +2602 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  63. package/src/shade/renderer/Renderer.js +7 -5
  64. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  67. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  68. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  69. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  70. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  71. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  72. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +2 -2
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +2 -2
  74. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
  75. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
  77. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  78. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  80. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  81. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  82. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  83. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  85. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  86. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  87. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
  88. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  89. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
  90. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  91. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  92. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  93. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  94. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  96. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  98. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  99. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  100. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  101. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  102. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  103. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  104. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  105. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  106. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  107. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  108. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  109. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  110. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  111. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  112. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  113. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  114. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  115. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  116. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  117. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  118. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  119. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  120. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  121. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  122. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  123. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  124. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  125. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  126. package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
  127. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  128. package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
  129. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  130. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  131. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  132. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  133. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  134. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  135. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  136. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  137. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  138. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  139. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  140. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  141. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  142. package/src/shade/renderer/water/shader_water.js +22 -13
  143. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  144. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  145. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  146. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  147. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  148. package/editor/REWORK_PLAN_2026_08.md +0 -692
  149. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  150. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  151. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  152. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  153. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  154. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  155. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  156. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  157. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  158. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  159. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  160. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  161. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  162. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  163. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  164. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  165. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  166. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  167. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  168. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  169. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  170. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  171. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  172. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  173. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  174. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  175. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  176. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  177. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  178. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  179. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  180. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  181. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  182. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  183. package/src/REVIEW_2026_08_06/generation.md +0 -176
  184. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  185. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  186. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  187. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  188. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  189. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  190. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  191. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  192. package/src/REVIEW_2026_08_06/view.md +0 -667
  193. package/src/REVIEW_2026_08_06.md +0 -610
  194. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  195. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  196. package/src/core/bvh8/NOTES.md +0 -63
  197. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  198. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  199. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  200. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  201. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  202. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  203. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  204. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  205. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  206. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  207. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  208. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  209. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  210. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  211. package/src/engine/REVIEW_2026_07_03.md +0 -211
  212. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  213. package/src/engine/control/first-person/DESIGN.md +0 -658
  214. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  215. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  216. package/src/engine/control/first-person/TODO.md +0 -160
  217. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  218. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  219. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  220. package/src/engine/ecs/ik/PLAN.md +0 -179
  221. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  222. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  223. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  224. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  225. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  226. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  227. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  228. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  229. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  230. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  231. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  232. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  233. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  234. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  235. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  236. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  237. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  238. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  239. package/src/engine/physics/cloth/PLAN.md +0 -3056
  240. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  241. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  242. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  243. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  244. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  245. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  246. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  247. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  248. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  249. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  250. package/src/format/image/avif/DECISIONS.md +0 -545
  251. package/src/format/image/jpeg/NOTES.md +0 -3
  252. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  253. package/src/generation/COORDINATES.md +0 -54
  254. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  255. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  256. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  257. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  258. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  259. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  260. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  261. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  262. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  263. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  264. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  265. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  266. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  267. package/src/shade/RENDERER_CONTRACT.md +0 -697
  268. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  269. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  270. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  271. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  272. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  273. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  274. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  275. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  276. package/src/shade/renderer/NOTES.md +0 -78
  277. package/src/shade/renderer/PLAN.md +0 -8
  278. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  279. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  280. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  281. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  282. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  283. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  284. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  285. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  286. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  287. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  288. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  289. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  290. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  291. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  292. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  293. package/src/shade/renderer/camera/orbital/README.md +0 -81
  294. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  295. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  296. package/src/shade/renderer/camera/orbital/index.js +0 -9
  297. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  298. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  299. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  300. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  301. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  302. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  303. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  304. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  305. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  306. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  307. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  308. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  309. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  310. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  311. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  312. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  313. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  314. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  315. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  316. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  317. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  318. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  319. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  320. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  321. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  322. package/src/shade/renderer/hiz/NOTES.md +0 -3
  323. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  324. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  325. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  326. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  327. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  328. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  329. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  330. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  331. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  332. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  333. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  334. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  335. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  336. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  337. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  338. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  339. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  340. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  341. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  342. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  343. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  344. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  345. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  346. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  347. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  348. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  349. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  350. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  351. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  352. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  353. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  354. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  355. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  356. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  357. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  358. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  359. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  360. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  361. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  362. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  363. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  364. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  365. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  366. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  367. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  368. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  369. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  370. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  371. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  372. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  373. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  374. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  375. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  376. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  377. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  378. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  379. package/src/shade/wgsl/NOTES.md +0 -4
@@ -1,243 +0,0 @@
1
- # W-TinyLFU benchmarks
2
-
3
- Benchmarks comparing `CacheWTinylfu` against the LRU `Cache` sibling. They split into **two axes**
4
- that answer different questions:
5
-
6
- | Axis | Question | Answered by |
7
- |------|----------|-------------|
8
- | **Effectiveness** (hit rate) | What fraction of accesses hit? Determined by the **policy**. | `hitRate.spec.js` |
9
- | **Performance** (throughput / latency / space) | What does each operation **cost**? Determined by the **implementation**. | everything else |
10
-
11
- You need both to judge the trade: the policy benchmark tells you what W-TinyLFU *saves you in misses*;
12
- the performance benchmarks tell you what it *costs per op and in RAM*. The exchange rate between them is
13
- the cost of a miss in your system (a disk/network miss dwarfs a few hundred ns; a cheap recompute does not).
14
-
15
- > ⚠️ **About the numbers below.** They're from one dev machine (Node v20.14.0, Windows 11) and microbenchmarks
16
- > are noisy — absolute nanoseconds wander run-to-run by tens of percent. **Trust the ratios and the shape, not
17
- > the exact ns.** The ratios are measured *interleaved* (LRU and W-TinyLFU timed back-to-back each round, then
18
- > the per-round ratios medianed), so they stay stable even as the absolutes drift. Re-run on your own hardware
19
- > for absolutes.
20
-
21
- ---
22
-
23
- ## Running
24
-
25
- Each benchmark is a self-contained `<name>.spec.js` with a skipped test entry and an explicit manual Node
26
- entry. The measurement **only ever runs under plain `node`** — never in-process under
27
- Jest, because Jest's babel transform badly distorts the timings (see *Why not measure under Jest* below).
28
- The files are stripped from the published npm package by the `*.spec.js` rule.
29
-
30
- **To get numbers — run it directly with `node`, from `packages/meep-engine`:**
31
-
32
- ```bash
33
- node src/core/cache/wtinylfu/benchmark/<name>.spec.js --benchmark
34
- ```
35
-
36
- where `<name>` is one of: `hitRate`, `fillDrain`, `readAll`, `continuousPut`, `mixed`, `latency`, `scaling`,
37
- `memory`.
38
-
39
- The files remain `describe.skip` under the test runner. Vitest's `--mode all` includes them in collection;
40
- un-skipping one makes its test spawn a native Node child and check that the report contains finite
41
- measurements. Importing a file or invoking it without `--benchmark` performs no workload setup.
42
-
43
- Options for the manual `node` invocation:
44
-
45
- - **`--expose-gc`** (a Node flag, goes *before* the script path): lets a benchmark force GC between trials for
46
- cleaner numbers. Optional for most; **required** for `memory` (it refuses to run without it). *(When run via
47
- Jest, the child is always spawned with `--expose-gc`.)*
48
- ```bash
49
- node --expose-gc src/core/cache/wtinylfu/benchmark/fillDrain.spec.js --benchmark
50
- ```
51
- - **`HASH=int`** (an env var): the **isolated-hash toggle** — use integer keys + an identity hash instead of
52
- string keys + `computeStringHash`, to strip string-hashing cost and see pure-policy overhead. Works on
53
- **every** benchmark.
54
- ```bash
55
- HASH=int node --expose-gc src/core/cache/wtinylfu/benchmark/readAll.spec.js --benchmark
56
- ```
57
-
58
- All workloads are **seeded**, so runs are reproducible. Both caches are always built at **matched capacity**
59
- (N weight-1 entries) so the comparison is fair.
60
-
61
- ### Layout & file structure
62
-
63
- Each benchmark lives in its own self-contained file in this `benchmark/` folder as `<name>.spec.js`.
64
- There is **no shared harness** — each file inlines the scaffolding it needs, so it stands alone (copy it,
65
- run it, read it without chasing imports). Every file follows the same skeleton:
66
-
67
- ```
68
- 1. HEADER what it measures · how to run · how to read it
69
- 2. CONFIG the knobs: CAPACITY / WARMUP / TRIALS / SEED
70
- 3. SCAFFOLDING inlined: clock, rng, matched-capacity factories, measureInterleaved, any workload
71
- (top-level — pure definitions, safe when Jest imports the file)
72
- if (under tests) → skipped native Node child + finite report checks
73
- if (manual entry with --benchmark) → runBenchmark():
74
- 4. WORKLOAD the setup + the timed body (the "arrange")
75
- 5. MEASURE measureInterleaved([LRU, W-TinyLFU], {...}) (the "act")
76
- 6. REPORT formatting + console.log (the "assert" analog — reports, no pass/fail)
77
- ```
78
-
79
- The `if (typeof describe === "function")` split keeps measurements in the native child. Workload setup is
80
- inside `runBenchmark()`, called only by the explicit main-file `--benchmark` entry.
81
-
82
- A benchmark **reports** numbers; it does not **assert** pass/fail. That's why the measurement always runs
83
- under plain `node` — the Jest side only launches it and checks the output isn't empty.
84
-
85
- ### Why not measure under Jest
86
-
87
- Jest runs every file through **babel-jest** before V8 sees it (transpiled to CommonJS, wrapped in Jest's
88
- module registry). That transformed code JITs differently from the native ES module `node` runs — and the
89
- overhead is **not uniform**: a cheap op (LRU) takes a bigger proportional hit than a heavier one (W-TinyLFU).
90
- Measured in-process under Jest, absolutes inflated ~2–12× **and several ratios inverted** — `fill` read
91
- 1.76x → 0.91x, `mixed` 1.48x → 0.90x, `latency p50` 3.0x → 0.86x — i.e. Jest would tell you W-TinyLFU is
92
- *faster* than LRU on inserts, which is false. It was also ~4× slower wall-clock. So the benchmark body never
93
- runs under Jest; the spec spawns a clean `node` child and only checks the output is well-formed.
94
-
95
- ---
96
-
97
- ## Effectiveness — `hitRate.spec.js`
98
-
99
- Replays seeded access traces as a read-through cache and reports **hit ratio** (capacity N = 500). This is
100
- where W-TinyLFU earns its keep.
101
-
102
- ```
103
- LRU W-TinyLFU Δ
104
- uniform random (no skew) 10.0% 10.0% 0.0 ← control: no harm
105
- zipfian skew 0.7 / 0.9 / 1.1 22.7/45.4/70.7 ~equal ±1 ← realistic skew: roughly even
106
- scan (hot set + one-shot scans) 31.6% 47.0% +15.4 ← scan resistance: the big win
107
- cyclic loop (size > capacity) LRU pathological case win ← incumbent-favoring ties
108
- ```
109
-
110
- **Takeaway:** roughly even with LRU on plain skew, *no worse* on uniform, and a large win on **scan-heavy**
111
- and **cyclic** workloads (where LRU is catastrophic). That scan resistance is the reason to pay the costs below.
112
-
113
- ---
114
-
115
- ## Performance
116
-
117
- Realistic (string-key) mode, `--expose-gc`, interleaved, median of 100 trials. **Capacities:** the read / fill /
118
- space and `continuousPut` benchmarks use **N = 65,536**; `mixed` uses a quarter of that (**N = 16,384**,
119
- keyspace 10×N) so its keyspace can dwarf the cache for realistic misses. All are powers of two — safe under
120
- churn because the shared HashMap grows headroom at its load factor.
121
-
122
- ### `fillDrain` — insert + teardown
123
-
124
- ```
125
- fill (put) | LRU ~197 ns/op | W-TinyLFU ~378 ns/op | ~2.1x
126
- drain (clear) | LRU ~21 ms/call | W-TinyLFU ~11 ms/call | ~0.52x
127
- drain (remove) | LRU ~259 ns/op | W-TinyLFU ~317 ns/op | ~1.2x
128
- ```
129
- `fill` is the policy's insert cost (sketch + region routing). The `drain` rows reflect each class's *teardown
130
- implementation*, not the policy — LRU's `clear()` removes element-by-element while W-TinyLFU bulk-resets, so the
131
- drain difference is an implementation artifact.
132
-
133
- ### `readAll` — read path (cold vs warm)
134
-
135
- ```
136
- read cold | LRU ~370 ns/op | W-TinyLFU ~710 ns/op | ~1.9x
137
- read warm | LRU ~290 ns/op | W-TinyLFU ~490 ns/op | ~1.7x
138
- ```
139
- Every get is a hit. `cold` = first read-pass (W-TinyLFU's promotion activity); `warm` = settled steady state.
140
- The premium is modest and one-time, not a recurring spike.
141
-
142
- ### `continuousPut` — sustained load (churn vs update)
143
-
144
- ```
145
- put churn | LRU ~660 ns/op | W-TinyLFU ~656 ns/op | ~1.0x (new keys → evict + admission duel)
146
- put update | LRU ~220 ns/op | W-TinyLFU ~447 ns/op | ~2.0x (re-put existing keys → in-place)
147
- ```
148
- **The key reframe.** W-TinyLFU's extra policy work is roughly **additive (~150–200 ns/op)**, not a multiplier —
149
- on the cheap `update` baseline it shows as ~2.1×. On `churn` it doesn't show at all: **element pooling** makes
150
- W-TinyLFU's inserts allocation-free, while LRU still allocates an element per insert, so that saving *offsets*
151
- the policy overhead and W-TinyLFU draws level with LRU (~1.0×).
152
-
153
- ### `mixed` — realistic blended throughput
154
-
155
- ```
156
- mixed get/put | LRU ~157 ns/op | W-TinyLFU ~224 ns/op | ~1.4x
157
- ```
158
- Zipfian read-through (get; on miss, put) over a keyspace 10× the cache — the most representative single number.
159
-
160
- ### `latency` — the tail (percentiles)
161
-
162
- ```
163
- p50 p99 p999 max
164
- LRU 200 ~700 ~2400 multi-ms (GC)
165
- W-TinyLFU 300 ~1300 ~7400 multi-ms (GC)
166
- ```
167
- Per-op timing during a fill. W-TinyLFU's whole distribution shifts up ~2× — its heavier insert, paid broadly
168
- rather than in a rare spike. The **max is a multi-ms GC pause on both**, not a policy event — read the policy
169
- tail in p99/p999, not max.
170
-
171
- ### `memory` — bytes per entry (space) · requires `--expose-gc`
172
-
173
- ```
174
- fresh fill after churn (steady state)
175
- LRU | ~144 (heap ~136 + ext ~8) | ~224 (heap ~208 + ext ~16) bytes/entry
176
- W-TinyLFU | ~164 (heap ~152 + ext ~12) | ~245 (heap ~225 + ext ~20) bytes/entry
177
- → fresh, W-TinyLFU vs LRU: +~20 B/entry (+~16 heap = per-element `region` + `keyHash`; +~4 sketch)
178
- → churn cost (both caches): +~80 B/entry (the shared HashMap doubling its slab at the load factor)
179
- ```
180
- ~14% more RAM per entry for the **policy** (fresh) — the **W-vs-LRU difference of ~4 B ≈ the sketch**. The
181
- **churn cost** is the same +~80 for both because it's the *shared HashMap*, not the policy: once the cache is
182
- full and evicting, the entries slab (and its bins) hits the 0.75 load factor and **doubles** to keep amortised
183
- O(1) inserts, so a churning cache carries ~2× the entry headroom of a fresh fill.
184
-
185
- ### `scaling` — O(1) check + locality across N
186
-
187
- ```
188
- N LRU W-TinyLFU ratio
189
- 2,048 62.3 175.6 2.88x
190
- 16,384 102.8 230.0 2.25x
191
- 131,072 379.2 556.8 1.47x
192
- 1,048,576 667.7 911.8 1.37x (warm random read, ns/op)
193
- ```
194
- Per-op cost rises only ~11× from 2k→1M (not ~500×) → **O(1) confirmed**; the rise is **CPU-cache locality**.
195
- The ratio **compresses as N grows** — both caches increasingly bottlenecked on RAM latency at large N.
196
-
197
- ### `HASH=int` — pure-policy overhead (cross-cutting)
198
-
199
- Run any benchmark with `HASH=int` to strip string hashing. Example (read warm):
200
-
201
- ```
202
- realistic isolated (HASH=int)
203
- LRU ~290 ns/op ~151 ns/op
204
- W-TinyLFU ~490 ns/op ~338 ns/op
205
- ratio ~1.7x ~2.3x
206
- ```
207
- Reveals that **string hashing is a large *shared* cost** (~140 ns of LRU's warm read was just `Map` string
208
- hashing, paid by both caches). Removing it shrinks the baseline, so the *same* fixed policy overhead becomes a
209
- bigger multiple.
210
-
211
- ---
212
-
213
- ## Headline findings
214
-
215
- 1. **Cost is a roughly fixed additive overhead, not a multiplier** — ~100–300 ns/op of extra work (sketch
216
- increment + region bookkeeping), so the "Nx" you see depends mostly on how cheap the underlying op already was.
217
- 2. **O(1) and predictable** — confirmed across 3 orders of magnitude; the only growth is shared CPU-cache locality.
218
- 3. **No tail surprises** — the overhead is spread across the whole distribution (median through p999), not a rare
219
- spike; the worst-case `max` is a GC pause both caches share.
220
- 4. **~14% more memory per entry** — the per-element `region` + `keyHash` fields plus the O(N) frequency sketch
221
- (plus a shared ~+80 B/entry once any cache is full and churning — the HashMap doubling its slab, not policy-specific).
222
- 5. **Bought against scan resistance** — the effectiveness axis shows large hit-rate wins on scan/cyclic workloads,
223
- while staying roughly even with LRU on plain skew and doing no harm on uniform.
224
-
225
- **The trade in one line:** ~150–200 ns/op and ~14% RAM, predictable and O(1), in exchange for scan-resistant,
226
- frequency-aware retention — worth it exactly when your misses are expensive and your traffic isn't purely uniform.
227
-
228
- ## Methodology notes (why the numbers are trustworthy)
229
-
230
- - **Pre-generated keys** outside the timed region (else you measure string allocation, not the cache).
231
- - **Warmup** discarded so the JIT has compiled the hot path (we measure optimized machine code, not the cold
232
- interpreter).
233
- - **Interleaved trials** — every round times LRU and W-TinyLFU back-to-back, and the ratio is the **median of
234
- the per-round ratios** (paired). So machine drift within a round hits both equally and cancels in the ratio.
235
- - **Median of trials** (not mean) to shrug off the occasional GC pause.
236
- - **Selective `forceGc`** — forced for low-allocation bodies (reads, teardown) where a stray pause is pure noise;
237
- *not* for allocation-heavy bodies (fill, churn) where GC is part of the real cost.
238
- - **Result sinks** so pure-read loops aren't deleted by dead-code elimination.
239
- - **Matched capacity & shared seeded workloads** so LRU and W-TinyLFU face identical conditions.
240
- - **Measured under `node`, never in-process under a test runner** — a transform (babel/esbuild) distorts both
241
- the absolute ns *and* the ratio (a cheap op suffers proportionally more overhead). Measured under Jest
242
- in-process, several ratios *inverted* (e.g. `fill` read 1.76x → 0.91x). That's why the self-executing spec
243
- spawns a clean `node` child instead of measuring inside Jest — see *Why not measure under Jest*.
@@ -1,417 +0,0 @@
1
- # COLOR_FIX_PLAN — `src/core/color`
2
-
3
- Date: 2026-08-28 · Companion to [COLOR_REVIEW_2026_08_28.md](COLOR_REVIEW_2026_08_28.md) ·
4
- Findings are referenced by their review IDs (`C1`…`C16`, `§n`).
5
-
6
- Each step is independently shippable and ordered so that nothing later depends on a decision not yet
7
- made. Every step names the files it touches and the test that proves it.
8
-
9
- ---
10
-
11
- ## Scope corrections applied
12
-
13
- Four corrections to the review were supplied and are binding on this plan:
14
-
15
- | # | Correction | Effect on the plan |
16
- |---|---|---|
17
- | **R1** | Don't overload the `Color` class | No new methods on `Color`. All new behaviour ships as free functions (`color_*`). §4.2's "missing members" become **Phase 6 files**, not class methods. Existing methods stay where they are — this is an additive rule, not a migration mandate. |
18
- | **R2** | Colour-space tag refused; `Color` is a low-level class | The review's §3.4 recommendation (a) and (b) are both **dropped**. `Color` stays four floats plus alpha with no space metadata. The space contract moves to *function* names and docs instead — **Phase 3**. |
19
- | **R3** | Unused/unimported code is not suspect; meep is a library with downstream consumers | **No deletions anywhere.** Review §1.4, §5.5, §5.6 and §5.8 are struck as *deletion* items. Their real consequence inverts: unimported code is public API that no in-repo caller exercises, so defects in it are unguarded and its **missing specs move up in priority** (Phase 7). |
20
- | **R4** | `prototype*.js` are playgrounds, stripped at ship | `kelvin/prototype_kelvin_to_rgb.js` is **out of scope**. Its `document.body` side effect and `engine/`/`view/` imports are expected for that file class. |
21
-
22
- ### Explicitly not doing
23
-
24
- - Deleting `int2rgb.js`, `sRGB_cmf.js`, `xyz_cmf_wyman.js`, `D65_spd_analytical.js`,
25
- `planckian_radiance.js`, `find_gamut_intersection.js`, `xyz_to_hpe.js`, `MAGMA_LUT.js`, or any of
26
- `operations/`. (R3)
27
- - Touching `prototype_kelvin_to_rgb.js`. (R4)
28
- - Adding a colour-space field, enum, or brand to `Color`. (R2)
29
- - Adding `add`/`sub`/`lerp`/`clamp`/`fromHex` &c. as `Color` methods. (R1)
30
-
31
- ---
32
-
33
- ## Conventions this plan introduces
34
-
35
- Two placement rules, so Phase 6 doesn't turn into a debate per file:
36
-
37
- - **`operations/`** — free functions whose *input* is a `Color`. Existing precedent:
38
- `color_lerp`, `color_darken`, `color_scale_okhsv_channel`.
39
- - **`construct/`** — new directory; free functions that *produce* a `Color` from non-`Color` input
40
- (`color_from_hex`, `color_from_uint24`, …). Keeps `operations/` meaning "transform", and keeps
41
- factories out of the class per R1.
42
-
43
- Naming follows the newer Layer-A style throughout: `snake_case`, `color_*` prefix, verb after the
44
- noun (`color_from_hex`, not `hex_to_color`, so the whole family sorts together).
45
-
46
- ---
47
-
48
- ## Phase 1 — Major correctness
49
-
50
- > Every step here: **write the failing spec first**, then fix. All four are currently invisible to
51
- > the suite (33 files / 158 tests, all green).
52
-
53
- ### Step 1 — Stop dropping alpha in the two `Color` sRGB statics · `C5` · major
54
-
55
- **Files:** [Color.js](Color.js) (`from_linear_to_sRGB` ~L788, `from_sRGB_to_linear` ~L801)
56
-
57
- **Do:**
58
- 1. Convert into a local scratch triple, then write the result through `output.set(r, g, b, input.a)`.
59
- This fixes both halves of C5 at once: alpha is carried, and the write goes through `set()` so
60
- `onChanged` fires and the no-change short-circuit applies.
61
- 2. Do **not** keep the positional `output[0] = …` path — that is the mechanism of the silent
62
- mutation.
63
-
64
- **Test:** `Color.spec.js` — (a) alpha survives a round trip through both statics; (b) passing an
65
- existing `Color` as `output` fires `onChanged` exactly once with the correct old/new payload.
66
-
67
- **Why first:** `Color.onChanged` has live consumers — `engine/graphics/render/forward_plus/model/PointLight.js`,
68
- `SpotLight.js`, and `view/elements/ColorPickerView.js` all subscribe. Today a colour converted in
69
- place updates without notifying any of them.
70
-
71
- ---
72
-
73
- ### Step 2 — Stop dropping alpha in the four OkHSV operations · `C6` · major
74
-
75
- **Files:** [operations/color_scale_okhsv_channel.js](operations/color_scale_okhsv_channel.js)
76
-
77
- **Do:** add `result.a = input.a;` before the return. One line fixes `color_lighten`,
78
- `color_darken`, `color_saturate` and `color_desaturate` together, and brings them in line with
79
- `color_lerp`, which already interpolates alpha.
80
-
81
- **Test:** new `operations/color_scale_okhsv_channel.spec.js` asserting alpha passthrough; extend
82
- `color_darken.spec.js` with an alpha case.
83
-
84
- ---
85
-
86
- ### Step 3 — Make `hex2rgb` reject or expand short-form hex · `C3` · major
87
-
88
- **Files:** [hex/hex2rgb.js](hex/hex2rgb.js)
89
-
90
- **Do:**
91
- 1. Branch on `hex.length`: accept `#RGB` (4) and `#RGBA` (5) by nibble expansion (`f` → `ff`),
92
- plus the existing `#RRGGBB` (7) and `#RRGGBBAA` (9).
93
- 2. Anything else → `throw new Error()` naming the offending string. Do not rely on the caller's
94
- `assert.notNaN` — that guard is downstream, mislabels the cause, and disappears if asserts are
95
- stripped from a build.
96
-
97
- **Test:** extend `hex/hex2rgb.spec.js` — `#f00` → `{255,0,0,255}`, `#f00a` → alpha `0xAA`,
98
- `#ff00` (4 digits *without* `#`-relative alpha intent), `#gg0000`, and `''` all throw.
99
-
100
- ---
101
-
102
- ### Step 4 — Mask and validate `parse_color`'s numeric branch · `C4` · major
103
-
104
- **Files:** [parse_color.js](parse_color.js) (~L66-70)
105
-
106
- **Do:**
107
- 1. Mask all three channels (`(c >> 16) & 0xFF`), not two of three.
108
- 2. Reject `c < 0 || c > 0xFFFFFF` with a thrown error. Decide and document explicitly whether
109
- `0xAARRGGBB` is supported — today it is neither supported nor rejected, which is how
110
- `0xFF8000FF` becomes `r: -0.502`.
111
- 3. `Color`'s HDR tolerance means no assert downstream will ever catch this, so the check has to be
112
- here.
113
-
114
- **Test:** `parse_color.spec.js` — `0xFF8000` decodes; `0xFF8000FF`, `-1`, `0x1000000` throw.
115
-
116
- ---
117
-
118
- ### Step 5 — Close the 2000 K seam in `kelvin_to_rgb` · `C1` · major
119
-
120
- **Files:** [kelvin/kelvin_to_rgb.js](kelvin/kelvin_to_rgb.js) (`approximate_regime_red`, ~L23)
121
-
122
- **Do:** the blue gate `t <= 20 ? 0 : clamp01(fit)` steps 0 → 0.0779 in one 0.01 K increment because
123
- the fit does not reach zero at its own gate. Pick one:
124
- - **(a) preferred** — shift/rescale the blue term so it is continuous at `t = 20`; or
125
- - **(b)** blend across a short window (`t ∈ [20, 21]`) using `inverseLerp` + `lerp`, mirroring the
126
- existing `BLEND_MIN`/`BLEND_MAX` treatment of the 6600 K seam.
127
-
128
- (b) matches the file's established house style and is the lower-risk change; (a) is correct rather
129
- than papered over. The 1000 K seam already does the right thing (hue locked to the red regime's
130
- `t = 10` value, matching to 6 dp) and needs no change.
131
-
132
- **Test:** new `kelvin_seam.spec.js` — sweep 500 K…40000 K in fine steps, assert a bounded
133
- first difference per channel. This test shape catches all three seams and is the reason C1 survived
134
- 9 existing tests.
135
-
136
- ---
137
-
138
- ### Step 6 — Restore injectivity to `rgb_to_kelvin` below 2000 K · `C2` · major
139
-
140
- **Files:** [kelvin/rgb_to_kelvin.js](kelvin/rgb_to_kelvin.js)
141
-
142
- **Do:** depends on Step 5 — the collapse is downstream of the flat `b/r === 0` region. After Step 5,
143
- re-measure. Then either:
144
- - **(a) minimal** — keep the bisection, verify it now resolves below 2000 K, and correct the JSDoc
145
- (it promises "approx 1000 to 40000" but returns `0` for black); or
146
- - **(b) better** — replace with McCamy's approximation over CIE 1960 `uv`, or Robertson. No solver,
147
- uses all three channels (the current version never reads green, so any colour with a matching
148
- `b/r` maps to the same temperature regardless of distance from the Planckian locus), and has a
149
- defined Duv validity domain.
150
-
151
- **Test:** extend `rgb_to_kelvin.spec.js` — round-trip `kelvin_to_rgb → rgb_to_kelvin` at 800, 1200,
152
- 1500, 1800, 1999, 2100, 2700, 5000, 6500, 20000 K within tolerance.
153
-
154
- ---
155
-
156
- ## Phase 2 — Minor correctness
157
-
158
- Small, independent, one commit each.
159
-
160
- | Step | Finding | File | Do | Test |
161
- |---|---|---|---|---|
162
- | **7** | `C7` | [rgb2uint32.js](rgb2uint32.js) | Append `>>> 0` to the return. Behaviour-preserving: both existing consumers (`Color.spec.js`, `deserialize_scene.js`) already normalise. Removes a method whose name promises `uint32` and returns `-1` for white. | Drop the `>>> 0` from `Color.spec.js`'s assertions; they must still pass. |
163
- | **8** | `C12` | [oklab/linear_srgb_to_okhsv.js](oklab/linear_srgb_to_okhsv.js) | Early-return `h = 0, s = 0, v = toe(L)` when `C < epsilon`, not only when `L === 0`. Today every neutral gets the arbitrary hue `0.2496` with `s ≈ 1e-7`. | Neutrals at several lightnesses return `s === 0`; `v` still round-trips. |
164
- | **9** | `C12` | [operations/color_scale_okhsv_channel.js](operations/color_scale_okhsv_channel.js) | `clamp01` the three output channels. OkHSV round-trips overshoot by ~1e-7 (`v = 1.0000000270` for white), so the operations can return out-of-`[0,1]` colours. | White in → all channels exactly ≤ 1. |
165
- | **10** | `C13` | [operations/color_lighten.js](operations/color_lighten.js) | Multiplicative `v * (1 + amount)` has a fixed point at 0, so black cannot be lightened. Switch to `v + amount * (1 - v)` (no fixed point at either end) or document the limitation. Mirror the decision in `color_saturate`. | `color_lighten(black, 1)` ≠ black. |
166
- | **11** | `C8` | [Color.js](Color.js) `setHCL` ~L332, `setHSI` ~L393 | Decide per method: clamped-to-gamut, or unbounded-caller-clamps. Document the choice. Add a `default:` to both switches (unreachable today, one refactor from `undefined`). If `setHSI` cannot be validated against a reference, its own `NOTE: I'm not sure if the implementation is correct` should be resolved or the method marked `@deprecated` — **not deleted** (R3). | New `Color` spec cases for both, at the primaries and at neutral. |
167
- | **12** | `C9` | [Color.js](Color.js) ~L702 | `multiplyScalar` and `multiply` scale alpha. Keep the behaviour, document it explicitly, and add `color_multiply_rgb` in Phase 6 for the exposure/tint case. | Doc-only; spec pins current behaviour so it can't drift. |
168
- | **13** | `C10` | [Color.js](Color.js) ~L501 | `fromUint`: mask red with `& 0xFF`. Superseded in substance by Step 20 (single unpacker) but harmless and immediate. | `fromUint(toUint())` round-trip across the full byte range. |
169
- | **14** | `C11` | [parse_color.js](parse_color.js) L6-8 | Anchor the three regexes (`^…$` after `trim()`), **or** document the substring-scan behaviour. Today `'background: rgb(1,2,3) !important'` parses, which contradicts the docstring. | Malformed-wrapper strings throw (or are documented as accepted). |
170
- | **15** | `C15` | [Color.js](Color.js) ~L563 | `hash()` → `toUint32()` (after Step 7) so alpha participates. Currently colours differing only in alpha collide. | Two colours differing only in alpha hash differently. |
171
- | **16** | `C16` | [illuminant/D65_spd_tabulated.js](illuminant/D65_spd_tabulated.js), [xyz/xyz_cmf_tabulated.js](xyz/xyz_cmf_tabulated.js) | Same problem, two shapes: `<=`/`>=` with an inner equality re-test vs `<`/`>` with a clamped `i1`; one asserts its input, the other doesn't. Converge on one. | Both return the tabulated endpoint exactly at both bounds and `0` outside. |
172
- | **17** | `C14` | [rgb_to_luminance.js](rgb_to_luminance.js) | Doc-only. Replace *"linear or gamma-corrected"* with the GPU twin's wording: input must be linear for the result to be luminance; on encoded input this is luma (`Y′`). Cross-reference `chunk_rgb_to_luminance`. | — |
173
-
174
- ---
175
-
176
- ## Phase 3 — Space contract without a space tag
177
-
178
- > Replaces review §3.4. R2 forbids putting the contract *in* `Color`; it goes into function names and
179
- > docs instead, which is the right layer given `Color` is deliberately low-level.
180
-
181
- ### Step 18 — State the contract on `Color` and on every consumer
182
-
183
- **Files:** [Color.js](Color.js) class JSDoc; `operations/*`; `rgb_to_luminance.js` (done in Step 17)
184
-
185
- **Do:**
186
- 1. `Color` class docstring: state plainly that `Color` is a **space-agnostic** 4-float container —
187
- it holds whatever the caller put in it, values may fall outside `[0,1]` for HDR, and *the
188
- functions operating on it* declare which space they expect. Also correct the false
189
- `@extends {Array.<number>}` (`Array.isArray(color) === false`) and document that the numeric
190
- index setters (`set 0(v)`) bypass `set()` and therefore do not notify `onChanged`.
191
- 2. Every function in `operations/` gets an explicit `@param {Color} input **linear** sRGB` line.
192
- They all call `linear_srgb_to_okhsv` today, so linear is the existing de facto contract —
193
- write it down.
194
- 3. `Color.parse` / `toHex` / `toCssRGBAString` get the converse note: these produce/consume
195
- **encoded** sRGB, so a `Color` built by `parse` must be converted before reaching `operations/`.
196
-
197
- **Test:** doc-only, but add one spec to `color_darken.spec.js` that builds its input via
198
- `Color.from_sRGB_to_linear(Color.parse('#808080'))` — the correct pipeline — so the intended usage is
199
- executable and visible.
200
-
201
- ### Step 19 — Add encoded-input variants so callers aren't required to remember
202
-
203
- **Files:** new `operations/color_srgb_scale_okhsv_channel.js` (or a `{ encoded: true }` option — pick one)
204
-
205
- **Do:** the UI path (`ColorPickerView`, `ColorEditor`, anything built from `Color.parse`) holds
206
- encoded values, and the operations assume linear. Provide the decode→operate→encode wrapper once,
207
- here, rather than in every caller. This is the practical substitute for the refused space tag.
208
-
209
- **Test:** encoded variant on `#808080` produces a visibly different (and correct) result from
210
- feeding the same encoded values to the linear variant.
211
-
212
- ---
213
-
214
- ## Phase 4 — Deduplication (no deletions — R3)
215
-
216
- Consolidate by **delegation**, keeping every existing export in place so downstream imports keep
217
- working.
218
-
219
- ### Step 20 — One uint24 unpacker, three call sites · `§5.1`
220
-
221
- **Files:** [int2rgb.js](int2rgb.js) (keep, becomes the implementation), [Color.js](Color.js)
222
- `fromUint`, [parse_color.js](parse_color.js) numeric branch
223
-
224
- **Do:** three copies exist with three masking behaviours. Make `int2rgb` the masked, validating
225
- implementation and have `Color.fromUint` and `parse_color` call it. Subsumes Steps 4 and 13.
226
- `int2rgb` keeps its export and signature.
227
-
228
- **Test:** all three paths agree across the byte range and reject out-of-range identically.
229
-
230
- ### Step 21 — Reuse the existing hue-wrap helper · `§5.2`
231
-
232
- **Files:** [Color.js](Color.js) `setHSL`/`setHCL`/`setHSI`, [hsv/hsv2rgb_float.js](hsv/hsv2rgb_float.js)
233
-
234
- **Do:** the same wrap is inlined four times in two different orders (`% 1` then fix-up, vs fix-up
235
- then `% 1`). `core/math/euclidean_modulo.js` **already exists** and is exactly this:
236
- `euclidean_modulo(h, 1)`. Replace all four. No new helper needed.
237
-
238
- **Test:** existing specs must stay green; add negative and >1 hue cases (`-3.5`, `2.25`) to each of
239
- the four.
240
-
241
- ### Step 22 — One sextant dispatch · `§5.3`
242
-
243
- **Files:** [Color.js](Color.js) `setHCL`/`setHSI`, [hsv/hsv2rgb_float.js](hsv/hsv2rgb_float.js),
244
- new `hsv/hue_to_rgb_sextant.js`
245
-
246
- **Do:** `setHCL` and `setHSI` contain byte-identical 30-line switches; `hsv2rgb_float` has a third
247
- variant. Extract `hue_to_rgb_sextant(out, h_face, c, x)`. This is also what makes Step 11's clamp a
248
- one-place change instead of three.
249
-
250
- **Test:** existing HSV/HSL specs green; new direct spec for the helper at all six sextant boundaries.
251
-
252
- ### Step 23 — Cross-reference the two lerps · `§5.4`
253
-
254
- **Files:** [Color.js](Color.js) `lerpColors`, [operations/color_lerp.js](operations/color_lerp.js)
255
-
256
- **Do:** doc-only. `lerpColors` is per-channel linear; `color_lerp` is OkHSV shortest-arc. Neither
257
- mentions the other, and the results differ substantially at mid-`t`. Add `@see` both ways and one
258
- sentence on when to pick which.
259
-
260
- ### Step 24 — Disambiguate spec filenames and suite names · `§5.9`
261
-
262
- **Files:** [xyz/XYZ.spec.js](xyz/XYZ.spec.js) → `rgb_xyz_roundtrip.spec.js`;
263
- [oklab/XYZ.spec.js](oklab/XYZ.spec.js) → `oklab_xyz_roundtrip.spec.js`;
264
- [sRGB/sRGB.spec.js](sRGB/sRGB.spec.js) → `srgb_transfer_roundtrip.spec.js`
265
-
266
- **Do:** two files share the name `XYZ.spec.js` and three share the test name
267
- `"to/from consistency"`, so a failure report is ambiguous. Rename files and give each test a
268
- distinct name.
269
-
270
- ---
271
-
272
- ## Phase 5 — Cross-boundary consistency
273
-
274
- ### Step 25 — Reconcile the PQ/ST 2084 reference white · `§3.5` · **highest cross-cutting risk**
275
-
276
- **Files:** [PQ/PQ_constants.js](PQ/PQ_constants.js) (203 nits, BT.2408) vs
277
- `src/shade/renderer/shader/chunk/color/iCtCp/chunk_eotf_st2084.js` and
278
- `chunk_inverse_eotf_st2084.js` (100 nits)
279
-
280
- **Do:** the same engine holds two SDR-white conventions — content encoded on the CPU path and decoded
281
- on the GPU path is off by 2.03×. Decide which is normative (BT.2408's 203 is the modern answer and is
282
- what `avif/encoder_worker.js` already ships), then make the other match. `PQ_constants.js` explicitly
283
- documents why folding a white point into the transfer function is dangerous; the GPU chunk folds a
284
- *different* one in.
285
-
286
- **Test:** a spec that runs the JS `linear_to_PQ`/`PQ_to_linear` pair against the WGSL chunks' expected
287
- values at several nit levels — per the project's WGSL-validate / JS-emulator tiering.
288
-
289
- ### Step 26 — Single source of truth for shared constants · `§5.7`
290
-
291
- **Files:** [rgb_to_luminance.js](rgb_to_luminance.js), [xyz/rgb_to_xyz.js](xyz/rgb_to_xyz.js),
292
- the corresponding WGSL chunks
293
-
294
- **Do:** the Rec.709 luma weights appear at 4, 15 and 20 significant digits in three places; the sRGB
295
- OETF exponent and the XYZ matrix each have two spellings. Generate the shared matrices and
296
- transfer-function coefficients into both JS and WGSL from one definition — the AVIF work already does
297
- exactly this (`feat(avif): generate the normative AV1 constant tables`), so the mechanism exists.
298
- This is what makes Step 25 stay fixed.
299
-
300
- **Test:** generated output is committed; a spec asserts JS and WGSL constants agree bit-for-bit.
301
-
302
- ---
303
-
304
- ## Phase 6 — Missing members, as free functions (R1)
305
-
306
- New files only. **No `Color` methods are added.** Order within the phase is by value; each is
307
- independent.
308
-
309
- ### Step 27 — Gamut clipping · `§4.3` · highest-value missing feature
310
-
311
- **Files:** new `oklab/gamut_clip_preserve_chroma.js`, `oklab/gamut_clip_project_to_lcusp.js`,
312
- `oklab/gamut_clip_adaptive_l0.js`
313
-
314
- **Do:** [find_gamut_intersection.js](oklab/find_gamut_intersection.js) is present and correct and is
315
- the primitive these are built from — the hard part is already written, only the thin wrappers are
316
- missing. Ships the package's only route to "make this out-of-gamut colour displayable".
317
-
318
- **Test:** new `gamut_clip.spec.js` — clipped colours are in-gamut; in-gamut colours are unchanged;
319
- hue is preserved by the preserve-chroma variant.
320
-
321
- ### Step 28 — Missing inverses · `§4.1`
322
-
323
- | New file | Note |
324
- |---|---|
325
- | `lab/lab_to_xyz.js` | Lab is currently write-only, which defeats the main reason to enter it. |
326
- | `hunt/hpe_to_xyz.js` | Pairs with the existing `xyz_to_hpe`. |
327
- | `hex/rgba2hex.js` | `hex2rgb` already **reads** `#RRGGBBAA`; nothing can write it. |
328
- | `oklab/oklab_to_oklch.js`, `oklch_to_oklab.js` | The space CSS Color 4 exposes and the natural one for hue rotation. |
329
- | `oklab/okhsl_*.js` | Ottosson defines OkHSL beside OkHSV; `toe`/`toe_inv` are already here. Better basis for lighten/darken than OkHSV (Step 10). |
330
-
331
- **Test:** absolute reference values, not only mutual round-trips — see Step 32.
332
-
333
- ### Step 29 — `Color` free functions replacing the "missing methods" · `§4.2` under R1
334
-
335
- **Files:** new, in `operations/` (Color in) and `construct/` (Color out)
336
-
337
- | File | Replaces the rejected method |
338
- |---|---|
339
- | `operations/color_add.js`, `color_sub.js`, `color_add_scaled.js` | `Color.prototype.add` &c. |
340
- | `operations/color_multiply_rgb.js` | the alpha-preserving multiply (Step 12) |
341
- | `operations/color_clamp.js`, `color_is_in_gamut.js` | pairs with Steps 9 & 11 |
342
- | `construct/color_from_hex.js` | `Color.fromHex` |
343
- | `construct/color_from_uint24.js`, `color_from_uint32.js` | `Color.fromUint` as a static |
344
- | `construct/color_from_temperature.js` | wraps the existing `kelvin_to_rgb` |
345
- | `construct/color_gray.js` | — |
346
- | `operations/color_get_hsl.js` | `getHSV` exists, `getHSL` does not |
347
-
348
- **Do:** each is a thin function over machinery that already exists. Existing `Color` methods are left
349
- untouched — this is additive.
350
-
351
- ### Step 30 — Lazy `onChanged` · `§4.2` · **defer; highest blast radius**
352
-
353
- **Files:** [Color.js](Color.js) constructor + `set()`
354
-
355
- **Do:** every `Color` eagerly allocates a `Signal`; measured **71.7 ms vs 17.9 ms per 500k
356
- constructions** — a 4× cost on a low-level type used in per-particle and per-vertex paths, which sits
357
- badly with R2's "Color is a low-level class". Make the signal lazy (allocate on first `add`, guard
358
- the `send8`). `Color.onChanged` has three known consumer sites (`PointLight`, `SpotLight`,
359
- `ColorPickerView`) but is accessed as a plain property, so a getter must preserve that shape.
360
-
361
- **Test:** existing `onChanged` specs green; a new allocation benchmark spec pinning the improvement;
362
- manual check of the three consumers.
363
-
364
- **Sequencing:** do this **after** Step 1, which is the step that makes `onChanged` correct in the
365
- first place.
366
-
367
- ---
368
-
369
- ## Phase 7 — Test coverage (raised in priority by R3)
370
-
371
- > Because nothing downstream is visible from this repo, the specs *are* the contract for the 33
372
- > unspecced source files. Absolute reference values, not just mutual inverses.
373
-
374
- | Step | Target | What to add |
375
- |---|---|---|
376
- | **31** | `oklab/` primitives | `linear_srgb_to_oklab`, `oklab_to_linear_srgb`, `find_cusp`, `toe`/`toe_inv`, `find_gamut_intersection` — currently 6 assertions across two whole-pipeline round-trips, and `find_gamut_intersection` has none. Add per-function specs with published reference values. |
377
- | **32** | Matrix pairs | `rgb_to_xyz`/`xyz_to_rgb`, `ycxcz`, `YCoCg`, `PQ` are covered only by mutual round-trips — a forward matrix wrong in both directions passes. Add absolute expected values (D65 white, the three primaries, mid-grey). |
378
- | **33** | Untested leaves | `rgb_to_luminance` (3 non-spec importers), `rgb2uint24`/`rgb2uint32`, `hsv2rgb_float`, `xyz_to_hpe`, `rgb_to_xyz`, `PQ_constants`. |
379
- | **34** | Seams | The sweep spec from Step 5, generalised: assert bounded first differences across every piecewise function in the package (`kelvin_to_rgb`, `convert_channel_*`, `xyz_to_lab`'s `compress`). |
380
- | **35** | Alpha | An alpha case on every function in `operations/` and on both `Color` sRGB statics. Steps 1 and 2 survived because none existed. |
381
- | **36** | Allocation | A guard spec on `Color` construction cost, so Step 30 can't regress. |
382
-
383
- ---
384
-
385
- ## Phase 8 — Convention convergence (last, one sweep)
386
-
387
- > Large, mechanical, mostly rename. Deliberately last so churn lands once, after behaviour is
388
- > correct. Nothing here is a deletion (R3) — old names stay as re-exports for one release if needed.
389
-
390
- | Step | Finding | Do |
391
- |---|---|---|
392
- | **37** | `§3.1` | Give `rgb_to_xyz` / `xyz_to_rgb` the `(out, out_off, in, in_off)` signature. Highest-value single change in the phase — 4 non-spec importers each, and they're the two most-imported conversions in the package. Keep the current 2-arg form as a delegating overload. |
393
- | **38** | `§3.1` | Same treatment for the remaining `(out, in)` member, `xyz_to_hpe`. Leave `(out, …scalars)` alone where inputs genuinely aren't a contiguous triple (`kelvin_to_rgb`, the CMFs). |
394
- | **39** | `§3.2` | Rename `hsv2rgb` → an explicitly byte-scaled name so the unsuffixed name isn't the non-default unit. Fix `parse_color`'s mixed `[0-255, 0-255, 0-255, 0-1]` return, moving the `/255` knowledge out of `Color.parse`. |
395
- | **40** | `§3.3` | `name2name` → `name_to_name` across Layer B (`hsv2rgb`, `rgb2hsv`, `rgb2hex`, `hex2rgb`, `int2rgb`, `rgb2uint24`, `rgb2uint32`). Splits cleanly by age, so it's mechanical. |
396
- | **41** | `§3.3` | One casing for the `sRGB` token (`sRGB_to_linear` vs `linear_srgb_to_oklab`), and one for directory names (`PQ`/`YCbCr`/`YCoCg`/`sRGB` vs the twelve lowercase ones). |
397
- | **42** | `§1.3` | `hunt/` holds `xyz_to_hpe`, the Hunt–Pointer–Estévez *matrix*, while the Hunt-*effect* function lives in `lab/` and carries a disclaimer saying so. Move `xyz_to_hpe` to `xyz/` and retire the `hunt/` directory (move, not delete). |
398
- | **43** | `§3.6` | One assert policy. Today `planckian_radiance` and `D65_spd_tabulated` validate scalars while `xyz_cmf_tabulated`, `xyz_cmf_wyman`, `linear_to_PQ` and `D65_spd_analytical` don't; `kelvin_to_rgb` asserts, `rgb_to_ycocg` doesn't. Also add the missing `@author`/`@copyright` to `linear_srgb_to_okhsv`, `linear_srgb_to_oklab`, `toe`. |
399
-
400
- ---
401
-
402
- ## Suggested batching
403
-
404
- | Batch | Steps | Character |
405
- |---|---|---|
406
- | **A** | 1–4 | Alpha + ingest. Small edits, immediate risk reduction, no design decisions. |
407
- | **B** | 5–6 | Kelvin. One decision (a vs b), one new test shape that pays off in Step 34. |
408
- | **C** | 7–17 | Minor correctness sweep. Mostly one-liners plus docs. |
409
- | **D** | 18–19 | Space contract. Doc + one new wrapper; unblocks correct downstream usage. |
410
- | **E** | 20–24 | Deduplication by delegation. No exports removed. |
411
- | **F** | 25–26 | CPU/GPU reconciliation. Do before more HDR work lands. |
412
- | **G** | 27–29 | New surface area. Purely additive, no risk to existing callers. |
413
- | **H** | 31–36, 30 | Coverage, then the lazy signal last (Step 30 needs 36 as its guard). |
414
- | **I** | 37–43 | Convention sweep. |
415
-
416
- **Regression baseline for every batch:** `npx vitest run src/core/color` — currently 33 files, 158
417
- tests, all passing. No step in this plan may reduce that count.