@photivra/engine 2.8.1 → 2.10.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 (91) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +8 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +2 -0
  6. package/dist/capture/export-jpeg.d.ts +19 -0
  7. package/dist/capture/export-jpeg.js +190 -108
  8. package/dist/capture/export-jpeg.js.map +1 -1
  9. package/dist/capture/native-environment-lean.js +106 -12
  10. package/dist/capture/native-environment-lean.js.map +1 -1
  11. package/dist/capture/native-environment-tabulated.d.ts +37 -0
  12. package/dist/capture/native-environment-tabulated.js +42 -6
  13. package/dist/capture/native-environment-tabulated.js.map +1 -1
  14. package/dist/core/portable-math.d.ts +13 -0
  15. package/dist/core/portable-math.js +43 -0
  16. package/dist/core/portable-math.js.map +1 -1
  17. package/dist/core/version.d.ts +1 -1
  18. package/dist/core/version.js +1 -1
  19. package/dist/core/version.js.map +1 -1
  20. package/dist/facade/channel-response.d.ts +15 -0
  21. package/dist/facade/channel-response.js +33 -0
  22. package/dist/facade/channel-response.js.map +1 -0
  23. package/dist/facade/diffraction-provider.js +50 -15
  24. package/dist/facade/diffraction-provider.js.map +1 -1
  25. package/dist/facade/display-view.d.ts +75 -0
  26. package/dist/facade/display-view.js +471 -109
  27. package/dist/facade/display-view.js.map +1 -1
  28. package/dist/facade/expected-signal-setup.js +2 -1
  29. package/dist/facade/expected-signal-setup.js.map +1 -1
  30. package/dist/facade/expected-signal.d.ts +36 -4
  31. package/dist/facade/expected-signal.js +331 -43
  32. package/dist/facade/expected-signal.js.map +1 -1
  33. package/dist/facade/scene-radiance.d.ts +4 -1
  34. package/dist/facade/scene-radiance.js +4 -4
  35. package/dist/facade/scene-radiance.js.map +1 -1
  36. package/dist/facade/simulate-capture.d.ts +77 -5
  37. package/dist/facade/simulate-capture.js +232 -40
  38. package/dist/facade/simulate-capture.js.map +1 -1
  39. package/dist/facade/simulate-display-view.d.ts +25 -2
  40. package/dist/facade/simulate-display-view.js +55 -21
  41. package/dist/facade/simulate-display-view.js.map +1 -1
  42. package/dist/gpu/image-kernel-execution.d.ts +5 -0
  43. package/dist/gpu/image-kernel-execution.js +2 -2
  44. package/dist/gpu/image-kernel-execution.js.map +1 -1
  45. package/dist/index.d.ts +5 -4
  46. package/dist/index.js +3 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/optics/diffraction-psf-profile.d.ts +19 -17
  49. package/dist/optics/diffraction-psf-profile.js +49 -18
  50. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  51. package/dist/optics/irradiance-convolution.d.ts +11 -0
  52. package/dist/optics/irradiance-convolution.js +64 -24
  53. package/dist/optics/irradiance-convolution.js.map +1 -1
  54. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  55. package/dist/optics/scene-to-sensor-irradiance.js +58 -24
  56. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  57. package/dist/optics/sensor-aperture-rays.js +2 -1
  58. package/dist/optics/sensor-aperture-rays.js.map +1 -1
  59. package/dist/optics/sensor-environment-query.js +4 -3
  60. package/dist/optics/sensor-environment-query.js.map +1 -1
  61. package/dist/optics/site-diffraction-kernel.d.ts +16 -0
  62. package/dist/optics/site-diffraction-kernel.js +60 -8
  63. package/dist/optics/site-diffraction-kernel.js.map +1 -1
  64. package/dist/output/print-jpeg.d.ts +28 -1
  65. package/dist/output/print-jpeg.js +79 -7
  66. package/dist/output/print-jpeg.js.map +1 -1
  67. package/dist/processing/demosaic.js +114 -37
  68. package/dist/processing/demosaic.js.map +1 -1
  69. package/dist/schema/scene-radiance.d.ts +8 -0
  70. package/dist/schema/scene-radiance.js +28 -17
  71. package/dist/schema/scene-radiance.js.map +1 -1
  72. package/dist/sensor/environment-photo-signal.d.ts +21 -0
  73. package/dist/sensor/environment-photo-signal.js +68 -2
  74. package/dist/sensor/environment-photo-signal.js.map +1 -1
  75. package/dist/sensor/stochastic-seed.d.ts +12 -0
  76. package/dist/sensor/stochastic-seed.js +25 -0
  77. package/dist/sensor/stochastic-seed.js.map +1 -1
  78. package/docs/API_REFERENCE.md +23 -1
  79. package/docs/QUICK_START.md +77 -8
  80. package/docs/USAGE.md +30 -0
  81. package/docs/api/capture-export-jpeg.md +10 -0
  82. package/docs/api/core-version.md +1 -1
  83. package/docs/api/exports.json +77 -2
  84. package/docs/api/facade-display-view.md +50 -0
  85. package/docs/api/facade-scene-radiance.md +2 -1
  86. package/docs/api/facade-simulate-capture.md +88 -8
  87. package/docs/api/facade-simulate-display-view.md +23 -1
  88. package/docs/api/gpu-image-kernel-execution.md +5 -0
  89. package/docs/api/optics-diffraction-psf-profile.md +1 -1
  90. package/docs/api/output-print-jpeg.md +41 -0
  91. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"sensor-environment-query.js","sourceRoot":"","sources":["../../src/optics/sensor-environment-query.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,mBAAmB,EAA0B,MAAM,+BAA+B,CAAC;AAC5F,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,0CAA0C,EACzC,MAAM,8BAA8B,CAAC;AAC5E,OAAO,EAAE,4BAA4B,EAAE,wBAAwB,EAA4B,MAAM,8BAA8B,CAAC;AAChI,OAAO,EACL,sCAAsC,EAAE,+BAA+B,EACxE,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,wBAAwB,EAAE,wBAAwB,EAAE,qCAAqC,EAE1F,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,8BAA8B,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,mCAAmC,EAAuC,MAAM,6BAA6B,CAAC;AACvH,OAAO,EAAE,eAAe,EAAmB,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAwB3E,kGAAkG;AAClG,MAAM,UAAU,kCAAkC,CAAC,KAAc;IAC/D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,yBAAyB,CAAC,qCAAqC,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC,CAAC,iBAAiB,EAAE,YAAY,EAAE,2BAA2B,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAClF,MAAM,IAAI,yBAAyB,CAAC,sCAAsC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpG,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,yBAAyB,KAAK,6BAA6B,EAAE,CAAC;QACvE,MAAM,IAAI,yBAAyB,CAAC,qFAAqF,CAAC,CAAC;IAC7H,CAAC;IACD,MAAM,eAAe,GAAG,wBAAwB,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACzE,MAAM,UAAU,GAAG,MAAM,CAAC,UAAmE,CAAC;IAC9F,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QAC1D,MAAM,IAAI,yBAAyB,CAAC,gDAAgD,CAAC,CAAC;IACxF,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC/B,OAAO,EAAE,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,yBAAyB,EAAE,6BAA6B,EAAE,EAAE,CAAC;IAClI,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,+BAA+B,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACnE,OAAO,EAAE,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE;gBAC9F,yBAAyB,EAAE,6BAA6B,EAAE,EAAE,MAAM,EAAE,CAAC;IACzE,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QACxC,MAAM,YAAY,GAAG,qCAAqC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAC/E,OAAO,EAAE,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE;gBACrG,yBAAyB,EAAE,6BAA6B,EAAE,EAAE,YAAY,EAAE,CAAC;IAC/E,CAAC;IACD,MAAM,IAAI,yBAAyB,CAAC,yEAAyE,CAAC,CAAC;AACjH,CAAC;AA8CD;;;;;;;;GAQG;AACH,MAAM,UAAU,uCAAuC,CACrD,KAAmD;IAEnD,gBAAgB,CAAC,KAAK,EAAE,yCAAyC,EAAE,CAAC,2BAA2B,EAAE,eAAe,EAAE,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,iCAAiC,EAAE,gCAAgC,EAAE,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC7Q,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO,6CAA6C,CAAC,KAAK,EAAE,kCAAkC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC1H,CAAC;IACD,IAAI,KAAK,CAAC,aAAa,KAAK,8BAA8B;QACxD,KAAK,CAAC,8BAA8B,KAAK,iCAAiC,EAAE,CAAC;QAC7E,MAAM,IAAI,2BAA2B,CAAC,kHAAkH,CAAC,CAAC;IAC5J,CAAC;IACD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,uBAAuB,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC;IAChH,MAAM,UAAU,GAAG,0CAA0C,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa;QAChG,YAAY,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;QAC/F,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;QACnE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,qBAAqB,EAAE,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,oBAAoB,CAAC;IAChG,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/C,MAAM,gCAAgC,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,CAAC,GAAC,MAAM,EAAE,CAAC;IAC/F,MAAM,GAAG,GAAG,gCAAgC,CAAC;IAC7C,MAAM,OAAO,GAAG,mCAAmC,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO;QACpE,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;QACnE,MAAM,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACjH,OAAO,mBAAmB,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAE,GAAG,KAAK,CAAC,yBAAyB,EAAE;QACrG,uBAAuB,EAAE,gCAAgC,EAAE,UAAU;QACrE,aAAa,EAAE,qDAAqD,EAAE,mBAAmB,EAAE,iCAAiC;QAC5H,gCAAgC,EAAE,IAAI,EAAE,2BAA2B,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK;KACjI,EAAE,mCAAmC,EAAE,OAAO,EAAE;QAC/C,qIAAqI;QACrI,6HAA6H;QAC7H,oHAAoH;KACrH,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,6CAA6C,CAC3D,KAAmD,EACnD,QAA6C;IAE7C,IAAI,KAAK,CAAC,aAAa,KAAK,8BAA8B;QACxD,KAAK,CAAC,8BAA8B,KAAK,iCAAiC,EAAE,CAAC;QAC7E,MAAM,IAAI,2BAA2B,CAAC,kHAAkH,CAAC,CAAC;IAC5J,CAAC;IACD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,uBAAuB,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC;IAChH,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IACjC,IAAI,KAAK,GAAG,uBAAuB,CAAC;IACpC,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAC7B,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAClC,KAAK,GAAG,sCAAsC,CAAC,uBAAuB,EAAE,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC,kBAAkB,CAAC;QACzI,8EAA8E;QAC9E,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC;QACpD,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,CAAC,CAAC,GAAC,QAAQ,CAAC,MAAM,CAAC,qBAAqB,IAAE,CAAC,CAAC;QACtF,MAAM,KAAK,GAAG,CAAC,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,CAAC;QAC3C,MAAM,gBAAgB,GAAG,CAAC,GAAC,CAAC,GAAC,EAAE,GAAC,EAAE,GAAC,CAAC,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,CAAC,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,CAAC;QAC5D,oBAAoB,GAAG,CAAC,GAAC,CAAC,KAAK,GAAC,gBAAgB,CAAC,CAAC;IACpD,CAAC;SAAM,IAAI,QAAQ,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/C,KAAK,GAAG,wBAAwB,CAAC,QAAQ,CAAC,YAAY,EAAE,uBAAuB,CAAC,CAAC;QACjF,MAAM,CAAC,GAAG,wBAAwB,CAAC,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC;QAC1E,oBAAoB,GAAG,CAAC,GAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,CAAC,CAAC,oBAAoB,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC;QAC1E,MAAM,IAAI,2BAA2B,CAAC,yDAAyD,CAAC,CAAC;IACnG,CAAC;IACD,IAAI,UAAU,GAAyE,IAAI,CAAC;IAC5F,IAAI,IAAyC,CAAC;IAC9C,IAAI,iBAAyB,CAAC;IAC9B,IAAI,OAAO,CAAC,eAAe,KAAK,aAAa,EAAE,CAAC;QAC9C,UAAU,GAAG,0CAA0C,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa;YAC1F,YAAY,EAAE,KAAK,EAAE,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;YAC7E,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;YACnE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7E,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,qBAAqB,EAAE,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,oBAAoB,CAAC;QAChG,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/C,IAAI,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,CAAC,GAAC,MAAM,EAAE,CAAC;QAC7D,iBAAiB,GAAG,CAAC,CAAC;IACxB,CAAC;SAAM,CAAC;QACN,iBAAiB,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ;YACzC,CAAC,CAAC,8BAA8B,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,EAAE,eAAe,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,eAAe;YAChI,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC;QACxB,MAAM,QAAQ,GAAG,KAAK,CAAC,wBAAwB,CAAC;QAChD,MAAM,CAAC,GAAG,KAAK,CAAC,+BAA+B,CAAC;QAChD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,2BAA2B,CAAC,kEAAkE,CAAC,CAAC;QAC5G,CAAC;QACD,MAAM,QAAQ,GAAG,4BAA4B,CAAC,OAAO,CAAC,eAAe,EAAE,iBAAiB,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QAC5G,MAAM,SAAS,GAAG,2BAA2B,CAAC,QAAQ,EACpD,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,GAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAC,CAAC,EAAE,EAAE,qBAAqB,CAAC,CAAC;QAClG,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;QACjE,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,2BAA2B,CAAC,sCAAsC,CAAC,CAAC;QAC7H,IAAI,GAAG,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC;IACjF,CAAC;IACD,MAAM,OAAO,GAAG,mCAAmC,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO;QACpE,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;QACnE,MAAM,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACpH,OAAO,mBAAmB,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAE,GAAG,KAAK,CAAC,yBAAyB,EAAE;QACrG,uBAAuB,EAAE,gCAAgC,EAAE,IAAI,EAAE,UAAU;QAC3E,gBAAgB,EAAE,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,cAAc,EAAE,OAAO,CAAC,UAAU,CAAC,IAAI;YACnG,iBAAiB,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,iBAAiB,EAAE,oBAAoB,EAAE;QAC5E,aAAa,EAAE,qDAAqD,EAAE,mBAAmB,EAAE,iCAAiC;QAC5H,gCAAgC,EAAE,IAAI,EAAE,2BAA2B,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK;KACjI,EAAE,+CAA+C,EAAE,OAAO,EAAE;QAC3D,qLAAqL;QACrL,iLAAiL;QACjL,qLAAqL;QACrL,mGAAmG;KACpG,CAAC,CAAC;AACL,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds an environment radiance request from points on the physical sensor. It uses the existing\n * focus-aware ideal projection and the analytic inverse camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The direction sent to the provider points opposite the\n * look ray at exposure start, because radiance travels toward the camera. This module does not\n * intersect the scene, compute radiance or render anything.\n *\n * Optional lens field mapping (2.0):\n * 1. The physical sensor point is inverse-distorted (radial or Brown-Conrady) to the ideal image\n * point.\n * 2. That point is unprojected with the declared ideal projection (rectilinear, fisheye or\n * equirectangular), at the same focus-aware projection scale.\n * 3. The ray is rotated to the camera axes at exposure start.\n * The local area factor 1 / det(d distorted / d ideal) is reported. The caller applies it to\n * irradiance exactly once. Without the mapping, the existing rectilinear computation is unchanged.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { lensProjectionDirectionValue, parseLensProjectionModel, type LensProjectionModel } from \"../camera/lens-projection.js\";\nimport {\n calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type RadialDistortionProfile, type ValidatedRadialProfile\n} from \"./radial-distortion.js\";\nimport {\n brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile,\n type BrownConradyDistortionProfile, type ValidatedBrownConradyDistortionProfile\n} from \"./brown-conrady-distortion.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseSceneRadianceEvaluationRequest, type SceneRadianceEvaluationRequest } from \"../schema/scene-radiance.js\";\nimport { parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/**\n * Lens field mapping between the physical (distorted) sensor point and the scene ray (2.0).\n * `radiometricAreaCorrection` states who applies the distortion area factor. The environment\n * photo signal multiplies irradiance by 1 / det(d distorted / d ideal) exactly once. A supplied\n * field-throughput profile must therefore not include it as well.\n */\nexport interface EnvironmentLensFieldMapping {\n projectionModel: LensProjectionModel;\n distortion:\n | { kind: \"none\" }\n | { kind: \"radial\"; profile: RadialDistortionProfile }\n | { kind: \"brown-conrady\"; profile: BrownConradyDistortionProfile };\n radiometricAreaCorrection: \"inverse-distortion-jacobian\";\n}\n\n/** @internal Parsed mapping with validated profiles, reused across one plan's queries. */\nexport interface PreparedEnvironmentLensFieldMapping {\n mapping: EnvironmentLensFieldMapping;\n radial?: ValidatedRadialProfile;\n brownConrady?: ValidatedBrownConradyDistortionProfile;\n}\n\n/** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */\nexport function prepareEnvironmentLensFieldMapping(value: unknown): PreparedEnvironmentLensFieldMapping {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(\"lensFieldMapping must be an object.\");\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n if (![\"projectionModel\", \"distortion\", \"radiometricAreaCorrection\"].includes(key)) {\n throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);\n }\n }\n if (record.radiometricAreaCorrection !== \"inverse-distortion-jacobian\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.radiometricAreaCorrection must be \\\"inverse-distortion-jacobian\\\".\");\n }\n const projectionModel = parseLensProjectionModel(record.projectionModel);\n const distortion = record.distortion as EnvironmentLensFieldMapping[\"distortion\"] | undefined;\n if (typeof distortion !== \"object\" || distortion === null) {\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion must be an object.\");\n }\n if (distortion.kind === \"none\") {\n return { mapping: { projectionModel, distortion: { kind: \"none\" }, radiometricAreaCorrection: \"inverse-distortion-jacobian\" } };\n }\n if (distortion.kind === \"radial\") {\n const radial = validateRadialDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"radial\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, radial };\n }\n if (distortion.kind === \"brown-conrady\") {\n const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"brown-conrady\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, brownConrady };\n }\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion.kind must be none, radial or brown-conrady.\");\n}\n\nexport interface CalculateSensorEnvironmentRadianceQueryInput {\n /** Optical sample point before the anti-aliasing filter or PSF. Native +X right, +Y down, in mm from the optical axis. */\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n focus: FocusPlane;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n /** States that the moment the shutter opens is t=0 in the rotation model. */\n timeReference: \"first-opening-boundary-phase\";\n timeSecondsFromOpeningReference: number;\n /** The request uses the direction radiance travels: toward the camera, opposite the look ray. */\n environmentDirectionConvention: \"outgoing-radiance-toward-camera\";\n request: Omit<SceneRadianceEvaluationRequest, \"target\" | \"timeSecondsFromExposureStart\">;\n /** Optional 2.0 lens field mapping. Omit it for the ideal rectilinear, distortion-free query. */\n lensFieldMapping?: EnvironmentLensFieldMapping;\n}\n\nexport interface SensorEnvironmentLensFieldMappingResult {\n projectionModel: LensProjectionModel;\n distortionKind: \"none\" | \"radial\" | \"brown-conrady\";\n /** Ideal (undistorted) image-plane point, +X right / +Y up, mm. */\n idealImagePointMm: { x: number; y: number };\n /** Projection scale: thin-lens image distance at finite focus, focal length at infinity, mm. */\n projectionScaleMm: number;\n /** 1 / det(d distorted / d ideal) at this point; 1 without distortion. */\n distortionAreaFactor: number;\n}\n\nexport interface SensorEnvironmentRadianceQuery {\n request: SceneRadianceEvaluationRequest;\n sourcePointNativeSensorMm: { x: number; y: number };\n sourcePointImagePlaneMm: { x: number; y: number };\n referenceLookDirectionUnitVector: { x: number; y: number; z: number };\n /** Rectilinear rotation mapping. Null when a non-rectilinear lens field mapping is used. */\n projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null;\n /** Present only when the input declared a lens field mapping. */\n lensFieldMapping?: SensorEnvironmentLensFieldMappingResult;\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\";\n directionConvention: \"outgoing-radiance-toward-camera\";\n geometricRayProjectionCalculated: true;\n sceneIntersectionCalculated: false;\n visibilityCalculated: false;\n providerExecuted: false;\n}\n\n/**\n * Builds an environment radiance request from points on the physical sensor,\n * using the existing focus-aware ideal projection and the analytic inverse\n * camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The provider direction points\n * opposite the look ray at exposure start. Nothing intersects the scene,\n * computes radiance or renders.\n */\nexport function calculateSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n requireInputKeys(input, \"calculateSensorEnvironmentRadianceQuery\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"focus\", \"angularVelocityRadPerSec\", \"timeReference\", \"timeSecondsFromOpeningReference\", \"environmentDirectionConvention\", \"request\", \"lensFieldMapping\"]);\n if (input.lensFieldMapping !== undefined) {\n return calculateMappedSensorEnvironmentRadianceQuery(input, prepareEnvironmentLensFieldMapping(input.lensFieldMapping));\n }\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: sourcePointImagePlaneMm, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = Math.hypot(point.x, point.y, z);\n const referenceLookDirectionUnitVector = { x: point.x/length, y: point.y/length, z: z/length };\n const ray = referenceLookDirectionUnitVector;\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -ray.x, y: -ray.y, z: -ray.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector, projection,\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"sensor-environment-radiance-query\", \"0.1.0\", [\n \"Ideal focus-aware rectilinear projection and constant-axis pure camera rotation; reference ray must stay in the forward hemisphere.\",\n \"The environment and rotation reference axes coincide with camera axes at the first opening boundary by explicit convention.\",\n \"Lens distortion, breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n\n/**\n * @internal Query with a lens field mapping that is already validated. The environment photo-signal\n * planner shares it, so profiles are validated once per plan.\n */\nexport function calculateMappedSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput,\n prepared: PreparedEnvironmentLensFieldMapping\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const mapping = prepared.mapping;\n let ideal = sourcePointImagePlaneMm;\n let distortionAreaFactor = 1;\n if (prepared.radial !== undefined) {\n ideal = calculateInverseRadialSourcePointValue(sourcePointImagePlaneMm, prepared.radial, \"sourcePointNativeSensorMm\").sourceImagePointMm;\n // Radial map p -> s(r) p: det = s (s + r ds/dr) with r ds/dr = 2 r^2 s'(r^2).\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n distortionAreaFactor = 1/(scale*radialDerivative);\n } else if (prepared.brownConrady !== undefined) {\n ideal = brownConradyInverseValue(prepared.brownConrady, sourcePointImagePlaneMm);\n const j = brownConradyForwardValue(prepared.brownConrady, ideal).jacobian;\n distortionAreaFactor = 1/(j[0]*j[3]-j[1]*j[2]);\n }\n if (!(distortionAreaFactor > 0) || !Number.isFinite(distortionAreaFactor)) {\n throw new InvalidScientificInputError(\"Distortion area factor must remain finite and positive.\");\n }\n let projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null = null;\n let look: { x: number; y: number; z: number };\n let projectionScaleMm: number;\n if (mapping.projectionModel === \"rectilinear\") {\n projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: ideal, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = Math.hypot(point.x, point.y, z);\n look = { x: point.x/length, y: point.y/length, z: z/length };\n projectionScaleMm = z;\n } else {\n projectionScaleMm = focus.kind === \"finite\"\n ? calculateThinLensImageDistance({ focalLengthMm: input.focalLengthMm, objectDistanceM: focus.distanceM }).value.imageDistanceMm\n : input.focalLengthMm;\n const velocity = input.angularVelocityRadPerSec;\n const t = input.timeSecondsFromOpeningReference;\n if (typeof t !== \"number\" || !Number.isFinite(t) || t < 0) {\n throw new InvalidScientificInputError(\"timeSecondsFromOpeningReference must be finite and non-negative.\");\n }\n const captured = lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);\n const reference = applyPhysicalCameraRotation(captured,\n { pitch: velocity.pitch*t, yaw: velocity.yaw*t, roll: velocity.roll*t }, \"camera-to-reference\");\n const length = Math.hypot(reference.x, reference.y, reference.z);\n if (!(length > 0) || !Number.isFinite(length)) throw new InvalidScientificInputError(\"Mapped reference ray must be finite.\");\n look = { x: reference.x/length, y: reference.y/length, z: reference.z/length };\n }\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -look.x, y: -look.y, z: -look.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector: look, projection,\n lensFieldMapping: { projectionModel: mapping.projectionModel, distortionKind: mapping.distortion.kind,\n idealImagePointMm: { ...ideal }, projectionScaleMm, distortionAreaFactor },\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"lens-mapped-sensor-environment-radiance-query\", \"0.1.0\", [\n \"The physical sensor point is inverse-distorted to the ideal image point, then unprojected with the declared ideal projection and rotated by the constant-axis pure camera rotation.\",\n \"Projection scale is the thin-lens image distance at finite focus (focal length at infinity) for every projection model; a calibrated finite-focus fisheye scale is not modeled.\",\n \"The distortion area factor 1/det(d distorted/d ideal) is reported for exactly-once application to irradiance; field throughput, PSF and pupil models are not changed by this query.\",\n \"Breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n"]}
1
+ {"version":3,"file":"sensor-environment-query.js","sourceRoot":"","sources":["../../src/optics/sensor-environment-query.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,mBAAmB,EAA0B,MAAM,+BAA+B,CAAC;AAC5F,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,0CAA0C,EACzC,MAAM,8BAA8B,CAAC;AAC5E,OAAO,EAAE,4BAA4B,EAAE,wBAAwB,EAA4B,MAAM,8BAA8B,CAAC;AAChI,OAAO,EACL,sCAAsC,EAAE,+BAA+B,EACxE,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,wBAAwB,EAAE,wBAAwB,EAAE,qCAAqC,EAE1F,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,8BAA8B,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,mCAAmC,EAAuC,MAAM,6BAA6B,CAAC;AACvH,OAAO,EAAE,eAAe,EAAmB,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAwBlD,kGAAkG;AAClG,MAAM,UAAU,kCAAkC,CAAC,KAAc;IAC/D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,yBAAyB,CAAC,qCAAqC,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC,CAAC,iBAAiB,EAAE,YAAY,EAAE,2BAA2B,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAClF,MAAM,IAAI,yBAAyB,CAAC,sCAAsC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpG,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,yBAAyB,KAAK,6BAA6B,EAAE,CAAC;QACvE,MAAM,IAAI,yBAAyB,CAAC,qFAAqF,CAAC,CAAC;IAC7H,CAAC;IACD,MAAM,eAAe,GAAG,wBAAwB,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACzE,MAAM,UAAU,GAAG,MAAM,CAAC,UAAmE,CAAC;IAC9F,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QAC1D,MAAM,IAAI,yBAAyB,CAAC,gDAAgD,CAAC,CAAC;IACxF,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC/B,OAAO,EAAE,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,yBAAyB,EAAE,6BAA6B,EAAE,EAAE,CAAC;IAClI,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,+BAA+B,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACnE,OAAO,EAAE,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE;gBAC9F,yBAAyB,EAAE,6BAA6B,EAAE,EAAE,MAAM,EAAE,CAAC;IACzE,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QACxC,MAAM,YAAY,GAAG,qCAAqC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAC/E,OAAO,EAAE,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE;gBACrG,yBAAyB,EAAE,6BAA6B,EAAE,EAAE,YAAY,EAAE,CAAC;IAC/E,CAAC;IACD,MAAM,IAAI,yBAAyB,CAAC,yEAAyE,CAAC,CAAC;AACjH,CAAC;AA8CD;;;;;;;;GAQG;AACH,MAAM,UAAU,uCAAuC,CACrD,KAAmD;IAEnD,gBAAgB,CAAC,KAAK,EAAE,yCAAyC,EAAE,CAAC,2BAA2B,EAAE,eAAe,EAAE,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,iCAAiC,EAAE,gCAAgC,EAAE,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC7Q,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO,6CAA6C,CAAC,KAAK,EAAE,kCAAkC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC1H,CAAC;IACD,IAAI,KAAK,CAAC,aAAa,KAAK,8BAA8B;QACxD,KAAK,CAAC,8BAA8B,KAAK,iCAAiC,EAAE,CAAC;QAC7E,MAAM,IAAI,2BAA2B,CAAC,kHAAkH,CAAC,CAAC;IAC5J,CAAC;IACD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,uBAAuB,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC;IAChH,MAAM,UAAU,GAAG,0CAA0C,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa;QAChG,YAAY,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;QAC/F,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;QACnE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,qBAAqB,EAAE,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,oBAAoB,CAAC;IAChG,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3C,MAAM,gCAAgC,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,CAAC,GAAC,MAAM,EAAE,CAAC;IAC/F,MAAM,GAAG,GAAG,gCAAgC,CAAC;IAC7C,MAAM,OAAO,GAAG,mCAAmC,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO;QACpE,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;QACnE,MAAM,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACjH,OAAO,mBAAmB,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAE,GAAG,KAAK,CAAC,yBAAyB,EAAE;QACrG,uBAAuB,EAAE,gCAAgC,EAAE,UAAU;QACrE,aAAa,EAAE,qDAAqD,EAAE,mBAAmB,EAAE,iCAAiC;QAC5H,gCAAgC,EAAE,IAAI,EAAE,2BAA2B,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK;KACjI,EAAE,mCAAmC,EAAE,OAAO,EAAE;QAC/C,qIAAqI;QACrI,6HAA6H;QAC7H,oHAAoH;KACrH,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,6CAA6C,CAC3D,KAAmD,EACnD,QAA6C;IAE7C,IAAI,KAAK,CAAC,aAAa,KAAK,8BAA8B;QACxD,KAAK,CAAC,8BAA8B,KAAK,iCAAiC,EAAE,CAAC;QAC7E,MAAM,IAAI,2BAA2B,CAAC,kHAAkH,CAAC,CAAC;IAC5J,CAAC;IACD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,uBAAuB,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC;IAChH,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IACjC,IAAI,KAAK,GAAG,uBAAuB,CAAC;IACpC,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAC7B,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAClC,KAAK,GAAG,sCAAsC,CAAC,uBAAuB,EAAE,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC,kBAAkB,CAAC;QACzI,8EAA8E;QAC9E,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC;QACpD,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,CAAC,CAAC,GAAC,QAAQ,CAAC,MAAM,CAAC,qBAAqB,IAAE,CAAC,CAAC;QACtF,MAAM,KAAK,GAAG,CAAC,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,CAAC;QAC3C,MAAM,gBAAgB,GAAG,CAAC,GAAC,CAAC,GAAC,EAAE,GAAC,EAAE,GAAC,CAAC,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,CAAC,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,GAAC,EAAE,CAAC;QAC5D,oBAAoB,GAAG,CAAC,GAAC,CAAC,KAAK,GAAC,gBAAgB,CAAC,CAAC;IACpD,CAAC;SAAM,IAAI,QAAQ,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/C,KAAK,GAAG,wBAAwB,CAAC,QAAQ,CAAC,YAAY,EAAE,uBAAuB,CAAC,CAAC;QACjF,MAAM,CAAC,GAAG,wBAAwB,CAAC,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC;QAC1E,oBAAoB,GAAG,CAAC,GAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,CAAC,CAAC,oBAAoB,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC;QAC1E,MAAM,IAAI,2BAA2B,CAAC,yDAAyD,CAAC,CAAC;IACnG,CAAC;IACD,IAAI,UAAU,GAAyE,IAAI,CAAC;IAC5F,IAAI,IAAyC,CAAC;IAC9C,IAAI,iBAAyB,CAAC;IAC9B,IAAI,OAAO,CAAC,eAAe,KAAK,aAAa,EAAE,CAAC;QAC9C,UAAU,GAAG,0CAA0C,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa;YAC1F,YAAY,EAAE,KAAK,EAAE,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;YAC7E,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;YACnE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7E,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,qBAAqB,EAAE,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,oBAAoB,CAAC;QAChG,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3C,IAAI,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,CAAC,GAAC,MAAM,EAAE,CAAC;QAC7D,iBAAiB,GAAG,CAAC,CAAC;IACxB,CAAC;SAAM,CAAC;QACN,iBAAiB,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ;YACzC,CAAC,CAAC,8BAA8B,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,EAAE,eAAe,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,eAAe;YAChI,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC;QACxB,MAAM,QAAQ,GAAG,KAAK,CAAC,wBAAwB,CAAC;QAChD,MAAM,CAAC,GAAG,KAAK,CAAC,+BAA+B,CAAC;QAChD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,2BAA2B,CAAC,kEAAkE,CAAC,CAAC;QAC5G,CAAC;QACD,MAAM,QAAQ,GAAG,4BAA4B,CAAC,OAAO,CAAC,eAAe,EAAE,iBAAiB,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QAC5G,MAAM,SAAS,GAAG,2BAA2B,CAAC,QAAQ,EACpD,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,GAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAC,CAAC,EAAE,EAAE,qBAAqB,CAAC,CAAC;QAClG,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;QAC7D,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,2BAA2B,CAAC,sCAAsC,CAAC,CAAC;QAC7H,IAAI,GAAG,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC;IACjF,CAAC;IACD,MAAM,OAAO,GAAG,mCAAmC,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO;QACpE,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;QACnE,MAAM,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACpH,OAAO,mBAAmB,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAE,GAAG,KAAK,CAAC,yBAAyB,EAAE;QACrG,uBAAuB,EAAE,gCAAgC,EAAE,IAAI,EAAE,UAAU;QAC3E,gBAAgB,EAAE,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,cAAc,EAAE,OAAO,CAAC,UAAU,CAAC,IAAI;YACnG,iBAAiB,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,iBAAiB,EAAE,oBAAoB,EAAE;QAC5E,aAAa,EAAE,qDAAqD,EAAE,mBAAmB,EAAE,iCAAiC;QAC5H,gCAAgC,EAAE,IAAI,EAAE,2BAA2B,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK;KACjI,EAAE,+CAA+C,EAAE,OAAO,EAAE;QAC3D,qLAAqL;QACrL,iLAAiL;QACjL,qLAAqL;QACrL,mGAAmG;KACpG,CAAC,CAAC;AACL,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds an environment radiance request from points on the physical sensor. It uses the existing\n * focus-aware ideal projection and the analytic inverse camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The direction sent to the provider points opposite the\n * look ray at exposure start, because radiance travels toward the camera. This module does not\n * intersect the scene, compute radiance or render anything.\n *\n * Optional lens field mapping (2.0):\n * 1. The physical sensor point is inverse-distorted (radial or Brown-Conrady) to the ideal image\n * point.\n * 2. That point is unprojected with the declared ideal projection (rectilinear, fisheye or\n * equirectangular), at the same focus-aware projection scale.\n * 3. The ray is rotated to the camera axes at exposure start.\n * The local area factor 1 / det(d distorted / d ideal) is reported. The caller applies it to\n * irradiance exactly once. Without the mapping, the existing rectilinear computation is unchanged.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { lensProjectionDirectionValue, parseLensProjectionModel, type LensProjectionModel } from \"../camera/lens-projection.js\";\nimport {\n calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type RadialDistortionProfile, type ValidatedRadialProfile\n} from \"./radial-distortion.js\";\nimport {\n brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile,\n type BrownConradyDistortionProfile, type ValidatedBrownConradyDistortionProfile\n} from \"./brown-conrady-distortion.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseSceneRadianceEvaluationRequest, type SceneRadianceEvaluationRequest } from \"../schema/scene-radiance.js\";\nimport { parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\n\n/**\n * Lens field mapping between the physical (distorted) sensor point and the scene ray (2.0).\n * `radiometricAreaCorrection` states who applies the distortion area factor. The environment\n * photo signal multiplies irradiance by 1 / det(d distorted / d ideal) exactly once. A supplied\n * field-throughput profile must therefore not include it as well.\n */\nexport interface EnvironmentLensFieldMapping {\n projectionModel: LensProjectionModel;\n distortion:\n | { kind: \"none\" }\n | { kind: \"radial\"; profile: RadialDistortionProfile }\n | { kind: \"brown-conrady\"; profile: BrownConradyDistortionProfile };\n radiometricAreaCorrection: \"inverse-distortion-jacobian\";\n}\n\n/** @internal Parsed mapping with validated profiles, reused across one plan's queries. */\nexport interface PreparedEnvironmentLensFieldMapping {\n mapping: EnvironmentLensFieldMapping;\n radial?: ValidatedRadialProfile;\n brownConrady?: ValidatedBrownConradyDistortionProfile;\n}\n\n/** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */\nexport function prepareEnvironmentLensFieldMapping(value: unknown): PreparedEnvironmentLensFieldMapping {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(\"lensFieldMapping must be an object.\");\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n if (![\"projectionModel\", \"distortion\", \"radiometricAreaCorrection\"].includes(key)) {\n throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);\n }\n }\n if (record.radiometricAreaCorrection !== \"inverse-distortion-jacobian\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.radiometricAreaCorrection must be \\\"inverse-distortion-jacobian\\\".\");\n }\n const projectionModel = parseLensProjectionModel(record.projectionModel);\n const distortion = record.distortion as EnvironmentLensFieldMapping[\"distortion\"] | undefined;\n if (typeof distortion !== \"object\" || distortion === null) {\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion must be an object.\");\n }\n if (distortion.kind === \"none\") {\n return { mapping: { projectionModel, distortion: { kind: \"none\" }, radiometricAreaCorrection: \"inverse-distortion-jacobian\" } };\n }\n if (distortion.kind === \"radial\") {\n const radial = validateRadialDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"radial\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, radial };\n }\n if (distortion.kind === \"brown-conrady\") {\n const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"brown-conrady\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, brownConrady };\n }\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion.kind must be none, radial or brown-conrady.\");\n}\n\nexport interface CalculateSensorEnvironmentRadianceQueryInput {\n /** Optical sample point before the anti-aliasing filter or PSF. Native +X right, +Y down, in mm from the optical axis. */\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n focus: FocusPlane;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n /** States that the moment the shutter opens is t=0 in the rotation model. */\n timeReference: \"first-opening-boundary-phase\";\n timeSecondsFromOpeningReference: number;\n /** The request uses the direction radiance travels: toward the camera, opposite the look ray. */\n environmentDirectionConvention: \"outgoing-radiance-toward-camera\";\n request: Omit<SceneRadianceEvaluationRequest, \"target\" | \"timeSecondsFromExposureStart\">;\n /** Optional 2.0 lens field mapping. Omit it for the ideal rectilinear, distortion-free query. */\n lensFieldMapping?: EnvironmentLensFieldMapping;\n}\n\nexport interface SensorEnvironmentLensFieldMappingResult {\n projectionModel: LensProjectionModel;\n distortionKind: \"none\" | \"radial\" | \"brown-conrady\";\n /** Ideal (undistorted) image-plane point, +X right / +Y up, mm. */\n idealImagePointMm: { x: number; y: number };\n /** Projection scale: thin-lens image distance at finite focus, focal length at infinity, mm. */\n projectionScaleMm: number;\n /** 1 / det(d distorted / d ideal) at this point; 1 without distortion. */\n distortionAreaFactor: number;\n}\n\nexport interface SensorEnvironmentRadianceQuery {\n request: SceneRadianceEvaluationRequest;\n sourcePointNativeSensorMm: { x: number; y: number };\n sourcePointImagePlaneMm: { x: number; y: number };\n referenceLookDirectionUnitVector: { x: number; y: number; z: number };\n /** Rectilinear rotation mapping. Null when a non-rectilinear lens field mapping is used. */\n projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null;\n /** Present only when the input declared a lens field mapping. */\n lensFieldMapping?: SensorEnvironmentLensFieldMappingResult;\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\";\n directionConvention: \"outgoing-radiance-toward-camera\";\n geometricRayProjectionCalculated: true;\n sceneIntersectionCalculated: false;\n visibilityCalculated: false;\n providerExecuted: false;\n}\n\n/**\n * Builds an environment radiance request from points on the physical sensor,\n * using the existing focus-aware ideal projection and the analytic inverse\n * camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The provider direction points\n * opposite the look ray at exposure start. Nothing intersects the scene,\n * computes radiance or renders.\n */\nexport function calculateSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n requireInputKeys(input, \"calculateSensorEnvironmentRadianceQuery\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"focus\", \"angularVelocityRadPerSec\", \"timeReference\", \"timeSecondsFromOpeningReference\", \"environmentDirectionConvention\", \"request\", \"lensFieldMapping\"]);\n if (input.lensFieldMapping !== undefined) {\n return calculateMappedSensorEnvironmentRadianceQuery(input, prepareEnvironmentLensFieldMapping(input.lensFieldMapping));\n }\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: sourcePointImagePlaneMm, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n const referenceLookDirectionUnitVector = { x: point.x/length, y: point.y/length, z: z/length };\n const ray = referenceLookDirectionUnitVector;\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -ray.x, y: -ray.y, z: -ray.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector, projection,\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"sensor-environment-radiance-query\", \"0.1.0\", [\n \"Ideal focus-aware rectilinear projection and constant-axis pure camera rotation; reference ray must stay in the forward hemisphere.\",\n \"The environment and rotation reference axes coincide with camera axes at the first opening boundary by explicit convention.\",\n \"Lens distortion, breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n\n/**\n * @internal Query with a lens field mapping that is already validated. The environment photo-signal\n * planner shares it, so profiles are validated once per plan.\n */\nexport function calculateMappedSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput,\n prepared: PreparedEnvironmentLensFieldMapping\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const mapping = prepared.mapping;\n let ideal = sourcePointImagePlaneMm;\n let distortionAreaFactor = 1;\n if (prepared.radial !== undefined) {\n ideal = calculateInverseRadialSourcePointValue(sourcePointImagePlaneMm, prepared.radial, \"sourcePointNativeSensorMm\").sourceImagePointMm;\n // Radial map p -> s(r) p: det = s (s + r ds/dr) with r ds/dr = 2 r^2 s'(r^2).\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n distortionAreaFactor = 1/(scale*radialDerivative);\n } else if (prepared.brownConrady !== undefined) {\n ideal = brownConradyInverseValue(prepared.brownConrady, sourcePointImagePlaneMm);\n const j = brownConradyForwardValue(prepared.brownConrady, ideal).jacobian;\n distortionAreaFactor = 1/(j[0]*j[3]-j[1]*j[2]);\n }\n if (!(distortionAreaFactor > 0) || !Number.isFinite(distortionAreaFactor)) {\n throw new InvalidScientificInputError(\"Distortion area factor must remain finite and positive.\");\n }\n let projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null = null;\n let look: { x: number; y: number; z: number };\n let projectionScaleMm: number;\n if (mapping.projectionModel === \"rectilinear\") {\n projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: ideal, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n look = { x: point.x/length, y: point.y/length, z: z/length };\n projectionScaleMm = z;\n } else {\n projectionScaleMm = focus.kind === \"finite\"\n ? calculateThinLensImageDistance({ focalLengthMm: input.focalLengthMm, objectDistanceM: focus.distanceM }).value.imageDistanceMm\n : input.focalLengthMm;\n const velocity = input.angularVelocityRadPerSec;\n const t = input.timeSecondsFromOpeningReference;\n if (typeof t !== \"number\" || !Number.isFinite(t) || t < 0) {\n throw new InvalidScientificInputError(\"timeSecondsFromOpeningReference must be finite and non-negative.\");\n }\n const captured = lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);\n const reference = applyPhysicalCameraRotation(captured,\n { pitch: velocity.pitch*t, yaw: velocity.yaw*t, roll: velocity.roll*t }, \"camera-to-reference\");\n const length = hypot3(reference.x, reference.y, reference.z);\n if (!(length > 0) || !Number.isFinite(length)) throw new InvalidScientificInputError(\"Mapped reference ray must be finite.\");\n look = { x: reference.x/length, y: reference.y/length, z: reference.z/length };\n }\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -look.x, y: -look.y, z: -look.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector: look, projection,\n lensFieldMapping: { projectionModel: mapping.projectionModel, distortionKind: mapping.distortion.kind,\n idealImagePointMm: { ...ideal }, projectionScaleMm, distortionAreaFactor },\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"lens-mapped-sensor-environment-radiance-query\", \"0.1.0\", [\n \"The physical sensor point is inverse-distorted to the ideal image point, then unprojected with the declared ideal projection and rotated by the constant-axis pure camera rotation.\",\n \"Projection scale is the thin-lens image distance at finite focus (focal length at infinity) for every projection model; a calibrated finite-focus fisheye scale is not modeled.\",\n \"The distortion area factor 1/det(d distorted/d ideal) is reported for exactly-once application to irradiance; field throughput, PSF and pupil models are not changed by this query.\",\n \"Breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n"]}
@@ -1,5 +1,21 @@
1
1
  import { resolveLensPsfConvolutionKernels } from "./lens-psf-kernels.js";
2
2
  type Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0]["psf"];
3
+ /** PSF records kept by value: at most 20 kernels of (2·32 + 1)² doubles each (676 KB) per record at the default spectral sampling. */
4
+ export declare const SITE_DIFFRACTION_KERNEL_CACHE_SIZE = 4;
5
+ /**
6
+ * Records that `psf.profile` is the profile `createDiffractionLimitedPsfProfile(profileInput)` returned, unmodified, so that the
7
+ * kernels of `psf` are those of any record with the same `profileInput` and the same other fields. Call it right after building the
8
+ * record; neither the record nor `profileInput` may change afterwards.
9
+ */
10
+ export declare function registerDiffractionPsfKey(psf: Psf, profileInput: object): void;
3
11
  /** The (2·radius + 1)² kernel of `psf` at `wavelengthNm` on a square site grid of `pitchMicrometers`; `failure` is the error message when the kernel cannot sample the pitch exactly. */
4
12
  export declare function siteDiffractionKernel(psf: Psf, wavelengthNm: number, pitchMicrometers: number, radius: number, failure: string): Float64Array;
13
+ /** Test hook: the number of PSF records kept by value, the kernels resolved so far, and whether `psf` is registered. */
14
+ export declare function siteDiffractionKernelCacheState(psf?: Psf): {
15
+ valueEntries: number;
16
+ resolves: number;
17
+ registered: boolean;
18
+ };
19
+ /** Test hook: forgets every kernel kept by value. */
20
+ export declare function clearSiteDiffractionKernelCache(): void;
5
21
  export {};
@@ -4,19 +4,62 @@
4
4
  * convolution (facade/diffraction-provider.ts) and the tabulated expected signal (facade/expected-signal-setup.ts) use it.
5
5
  *
6
6
  * `resolveLensPsfConvolutionKernels` parses and samples the PSF profile on every call (about 10 ms per wavelength), and both users
7
- * ask for the same kernels, so they are kept while the PSF record lives. The kernel is a function of its inputs alone; each caller
8
- * gets its own copy.
7
+ * ask for the same kernels. The kernel is a function of its inputs alone; each caller gets its own copy.
8
+ *
9
+ * The facade builds a new PSF record for every request, so kernels are kept by value: `registerDiffractionPsfKey` records, for a
10
+ * record whose profile `createDiffractionLimitedPsfProfile` built, a canonical string of that call's whole input and of the record's
11
+ * own fields (everything the kernel is a function of besides the wavelength, pitch and radius of the query). Records with an equal
12
+ * string share their kernels, in a least-recently-used cache of `SITE_DIFFRACTION_KERNEL_CACHE_SIZE` records. A record that was not
13
+ * registered (a caller's own profile) keeps its kernels while it lives, by identity, as before.
9
14
  */
10
15
  import { InvalidConfigurationError } from "../core/configuration-error.js";
11
16
  import { resolveLensPsfConvolutionKernels } from "./lens-psf-kernels.js";
12
- /** Kernels per PSF record (by identity: the record is large, so it is not serialized), then per wavelength, pitch and radius. */
13
- const cache = new WeakMap();
17
+ /** PSF records kept by value: at most 20 kernels of (2·32 + 1)² doubles each (676 KB) per record at the default spectral sampling. */
18
+ export const SITE_DIFFRACTION_KERNEL_CACHE_SIZE = 4;
19
+ /** Canonical value key of each registered PSF record. */
20
+ const keys = new WeakMap();
21
+ /** Kernels per registered value key (least recently used first), then per wavelength, pitch and radius. */
22
+ const byValue = new Map();
23
+ /** Kernels per unregistered PSF record (by identity: the record is large, so it is not serialized), then per wavelength, pitch and radius. */
24
+ const byIdentity = new WeakMap();
25
+ /** Kernels resolved so far (test hook). */
26
+ let resolves = 0;
27
+ /** JSON with every number spelled exactly: shortest round-trip digits, and -0, NaN and ±Infinity kept apart from 0 and null. */
28
+ function canonical(value) {
29
+ return JSON.stringify(value, (_key, v) => (typeof v === "number" && (Object.is(v, -0) || !Number.isFinite(v)) ? "number:" + (Object.is(v, -0) ? "-0" : String(v)) : v));
30
+ }
31
+ /**
32
+ * Records that `psf.profile` is the profile `createDiffractionLimitedPsfProfile(profileInput)` returned, unmodified, so that the
33
+ * kernels of `psf` are those of any record with the same `profileInput` and the same other fields. Call it right after building the
34
+ * record; neither the record nor `profileInput` may change afterwards.
35
+ */
36
+ export function registerDiffractionPsfKey(psf, profileInput) {
37
+ const fields = Object.entries(psf).filter(([name]) => name !== "profile").sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
38
+ keys.set(psf, canonical(["createDiffractionLimitedPsfProfile", profileInput, fields]));
39
+ }
40
+ /** The kernels kept for `psf`, made the most recently used. */
41
+ function kernelsOf(psf) {
42
+ const key = keys.get(psf);
43
+ if (key === undefined) {
44
+ let kernels = byIdentity.get(psf);
45
+ if (!kernels)
46
+ byIdentity.set(psf, (kernels = new Map()));
47
+ return kernels;
48
+ }
49
+ let kernels = byValue.get(key);
50
+ if (kernels)
51
+ byValue.delete(key);
52
+ else
53
+ kernels = new Map();
54
+ byValue.set(key, kernels);
55
+ while (byValue.size > SITE_DIFFRACTION_KERNEL_CACHE_SIZE)
56
+ byValue.delete(byValue.keys().next().value);
57
+ return kernels;
58
+ }
14
59
  /** The (2·radius + 1)² kernel of `psf` at `wavelengthNm` on a square site grid of `pitchMicrometers`; `failure` is the error message when the kernel cannot sample the pitch exactly. */
15
60
  export function siteDiffractionKernel(psf, wavelengthNm, pitchMicrometers, radius, failure) {
16
- let kernels = cache.get(psf);
17
- if (!kernels)
18
- cache.set(psf, (kernels = new Map()));
19
- const key = wavelengthNm + ":" + pitchMicrometers + ":" + radius;
61
+ const kernels = kernelsOf(psf);
62
+ const key = canonical([wavelengthNm, pitchMicrometers, radius]);
20
63
  let kernel = kernels.get(key);
21
64
  if (!kernel) {
22
65
  const k = resolveLensPsfConvolutionKernels({ schemaVersion: "0.1.0", psf, spectralWeights: [{ wavelengthNm, weight: 1 }],
@@ -25,8 +68,17 @@ export function siteDiffractionKernel(psf, wavelengthNm, pitchMicrometers, radiu
25
68
  if (!k.exactSiteSampling || k.kernelRadius !== radius)
26
69
  throw new InvalidConfigurationError(failure);
27
70
  kernel = Float64Array.from(k.kernels);
71
+ resolves++;
28
72
  kernels.set(key, kernel);
29
73
  }
30
74
  return Float64Array.from(kernel);
31
75
  }
76
+ /** Test hook: the number of PSF records kept by value, the kernels resolved so far, and whether `psf` is registered. */
77
+ export function siteDiffractionKernelCacheState(psf) {
78
+ return { valueEntries: byValue.size, resolves, registered: psf !== undefined && keys.has(psf) };
79
+ }
80
+ /** Test hook: forgets every kernel kept by value. */
81
+ export function clearSiteDiffractionKernelCache() {
82
+ byValue.clear();
83
+ }
32
84
  //# sourceMappingURL=site-diffraction-kernel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"site-diffraction-kernel.js","sourceRoot":"","sources":["../../src/optics/site-diffraction-kernel.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAC;AAIzE,iIAAiI;AACjI,MAAM,KAAK,GAAG,IAAI,OAAO,EAAqC,CAAC;AAE/D,yLAAyL;AACzL,MAAM,UAAU,qBAAqB,CAAC,GAAQ,EAAE,YAAoB,EAAE,gBAAwB,EAAE,MAAc,EAAE,OAAe;IAC7H,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,OAAO;QAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,YAAY,GAAG,GAAG,GAAG,gBAAgB,GAAG,GAAG,GAAG,MAAM,CAAC;IACjE,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,CAAC,GAAG,gCAAgC,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;YACtH,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,oBAAoB,EAAE,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,EAAE,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;YACrK,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC;QAChC,IAAI,CAAC,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC,YAAY,KAAK,MAAM;YAAE,MAAM,IAAI,yBAAyB,CAAC,OAAO,CAAC,CAAC;QACpG,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (not exported from the package): one wavelength's diffraction kernel sampled on the site pitch, as the object-space\n * convolution (facade/diffraction-provider.ts) and the tabulated expected signal (facade/expected-signal-setup.ts) use it.\n *\n * `resolveLensPsfConvolutionKernels` parses and samples the PSF profile on every call (about 10 ms per wavelength), and both users\n * ask for the same kernels, so they are kept while the PSF record lives. The kernel is a function of its inputs alone; each caller\n * gets its own copy.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { resolveLensPsfConvolutionKernels } from \"./lens-psf-kernels.js\";\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\n/** Kernels per PSF record (by identity: the record is large, so it is not serialized), then per wavelength, pitch and radius. */\nconst cache = new WeakMap<object, Map<string, Float64Array>>();\n\n/** The (2·radius + 1)² kernel of `psf` at `wavelengthNm` on a square site grid of `pitchMicrometers`; `failure` is the error message when the kernel cannot sample the pitch exactly. */\nexport function siteDiffractionKernel(psf: Psf, wavelengthNm: number, pitchMicrometers: number, radius: number, failure: string): Float64Array {\n let kernels = cache.get(psf);\n if (!kernels) cache.set(psf, (kernels = new Map()));\n const key = wavelengthNm + \":\" + pitchMicrometers + \":\" + radius;\n let kernel = kernels.get(key);\n if (!kernel) {\n const k = resolveLensPsfConvolutionKernels({ schemaVersion: \"0.1.0\", psf, spectralWeights: [{ wavelengthNm, weight: 1 }],\n grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitchMicrometers, y: pitchMicrometers }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1,\n kernelRadius: radius }).value;\n if (!k.exactSiteSampling || k.kernelRadius !== radius) throw new InvalidConfigurationError(failure);\n kernel = Float64Array.from(k.kernels);\n kernels.set(key, kernel);\n }\n return Float64Array.from(kernel);\n}\n"]}
1
+ {"version":3,"file":"site-diffraction-kernel.js","sourceRoot":"","sources":["../../src/optics/site-diffraction-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (not exported from the package): one wavelength's diffraction kernel sampled on the site pitch, as the object-space\n * convolution (facade/diffraction-provider.ts) and the tabulated expected signal (facade/expected-signal-setup.ts) use it.\n *\n * `resolveLensPsfConvolutionKernels` parses and samples the PSF profile on every call (about 10 ms per wavelength), and both users\n * ask for the same kernels. The kernel is a function of its inputs alone; each caller gets its own copy.\n *\n * The facade builds a new PSF record for every request, so kernels are kept by value: `registerDiffractionPsfKey` records, for a\n * record whose profile `createDiffractionLimitedPsfProfile` built, a canonical string of that call's whole input and of the record's\n * own fields (everything the kernel is a function of besides the wavelength, pitch and radius of the query). Records with an equal\n * string share their kernels, in a least-recently-used cache of `SITE_DIFFRACTION_KERNEL_CACHE_SIZE` records. A record that was not\n * registered (a caller's own profile) keeps its kernels while it lives, by identity, as before.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { resolveLensPsfConvolutionKernels } from \"./lens-psf-kernels.js\";\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\n/** PSF records kept by value: at most 20 kernels of (2·32 + 1)² doubles each (676 KB) per record at the default spectral sampling. */\nexport const SITE_DIFFRACTION_KERNEL_CACHE_SIZE = 4;\n\n/** Canonical value key of each registered PSF record. */\nconst keys = new WeakMap<object, string>();\n/** Kernels per registered value key (least recently used first), then per wavelength, pitch and radius. */\nconst byValue = new Map<string, Map<string, Float64Array>>();\n/** Kernels per unregistered PSF record (by identity: the record is large, so it is not serialized), then per wavelength, pitch and radius. */\nconst byIdentity = new WeakMap<object, Map<string, Float64Array>>();\n/** Kernels resolved so far (test hook). */\nlet resolves = 0;\n\n/** JSON with every number spelled exactly: shortest round-trip digits, and -0, NaN and ±Infinity kept apart from 0 and null. */\nfunction canonical(value: unknown): string {\n return JSON.stringify(value, (_key, v: unknown) => (typeof v === \"number\" && (Object.is(v, -0) || !Number.isFinite(v)) ? \"number:\" + (Object.is(v, -0) ? \"-0\" : String(v)) : v));\n}\n\n/**\n * Records that `psf.profile` is the profile `createDiffractionLimitedPsfProfile(profileInput)` returned, unmodified, so that the\n * kernels of `psf` are those of any record with the same `profileInput` and the same other fields. Call it right after building the\n * record; neither the record nor `profileInput` may change afterwards.\n */\nexport function registerDiffractionPsfKey(psf: Psf, profileInput: object): void {\n const fields = Object.entries(psf as object).filter(([name]) => name !== \"profile\").sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n keys.set(psf as object, canonical([\"createDiffractionLimitedPsfProfile\", profileInput, fields]));\n}\n\n/** The kernels kept for `psf`, made the most recently used. */\nfunction kernelsOf(psf: Psf): Map<string, Float64Array> {\n const key = keys.get(psf as object);\n if (key === undefined) {\n let kernels = byIdentity.get(psf as object);\n if (!kernels) byIdentity.set(psf as object, (kernels = new Map()));\n return kernels;\n }\n let kernels = byValue.get(key);\n if (kernels) byValue.delete(key);\n else kernels = new Map();\n byValue.set(key, kernels);\n while (byValue.size > SITE_DIFFRACTION_KERNEL_CACHE_SIZE) byValue.delete(byValue.keys().next().value!);\n return kernels;\n}\n\n/** The (2·radius + 1)² kernel of `psf` at `wavelengthNm` on a square site grid of `pitchMicrometers`; `failure` is the error message when the kernel cannot sample the pitch exactly. */\nexport function siteDiffractionKernel(psf: Psf, wavelengthNm: number, pitchMicrometers: number, radius: number, failure: string): Float64Array {\n const kernels = kernelsOf(psf);\n const key = canonical([wavelengthNm, pitchMicrometers, radius]);\n let kernel = kernels.get(key);\n if (!kernel) {\n const k = resolveLensPsfConvolutionKernels({ schemaVersion: \"0.1.0\", psf, spectralWeights: [{ wavelengthNm, weight: 1 }],\n grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitchMicrometers, y: pitchMicrometers }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1,\n kernelRadius: radius }).value;\n if (!k.exactSiteSampling || k.kernelRadius !== radius) throw new InvalidConfigurationError(failure);\n kernel = Float64Array.from(k.kernels);\n resolves++;\n kernels.set(key, kernel);\n }\n return Float64Array.from(kernel);\n}\n\n/** Test hook: the number of PSF records kept by value, the kernels resolved so far, and whether `psf` is registered. */\nexport function siteDiffractionKernelCacheState(psf?: Psf): { valueEntries: number; resolves: number; registered: boolean } {\n return { valueEntries: byValue.size, resolves, registered: psf !== undefined && keys.has(psf as object) };\n}\n\n/** Test hook: forgets every kernel kept by value. */\nexport function clearSiteDiffractionKernelCache(): void {\n byValue.clear();\n}\n"]}
@@ -2,7 +2,7 @@
2
2
  * Print JPEG export: downsamples an SDR image by exact area averaging, then writes a baseline JPEG
3
3
  * with an ICC colour profile. Bounded in size; the encoder is written for this project.
4
4
  */
5
- import { EXPORT_JPEG_ENCODER_ID } from "../capture/export-jpeg.js";
5
+ import { EXPORT_JPEG_ENCODER_ID, type JpegStripBits } from "../capture/export-jpeg.js";
6
6
  import { SRGB_ICC_PROFILE_VERSION } from "../color/srgb-icc.js";
7
7
  import type { NativeRawTaskState } from "../capture/native-raw.js";
8
8
  import type { RasterRect } from "./capture-geometry.js";
@@ -98,5 +98,32 @@ export interface PrintJpegTask {
98
98
  }
99
99
  /** Validates the input only. Source detail, perception and printer suitability are checked by separate APIs. */
100
100
  export declare function parsePrintJpegInput(input: PrintJpegInput): PrintJpegInput;
101
+ /** Rows of a 1:1 Print JPEG for `createPrintJpegStrip`: crop rows `y` to `y + height` (relative to the crop), crop-wide RGB. */
102
+ export interface PrintJpegStripInput {
103
+ y: number;
104
+ height: number;
105
+ samples: Uint8Array;
106
+ }
107
+ /** A strip of a 1:1 Print JPEG's coded blocks (`createPrintJpegStrip`), for `composePrintJpegStrips`. */
108
+ export interface PrintJpegStrip {
109
+ readonly y: number;
110
+ readonly height: number;
111
+ readonly width: number;
112
+ readonly quantizationStep: number;
113
+ readonly bits: JpegStripBits;
114
+ }
115
+ /**
116
+ * Codes rows of a 1:1 Print JPEG (output size equal to the crop) apart from the rows before them, so a large file's strips
117
+ * can be written on separate workers. `strip.y` is a multiple of 8 and `strip.height` a multiple of 8 unless the strip ends at
118
+ * the image's bottom; `strip.samples` are those crop rows' 8-bit sRGB RGB, crop-wide. `composePrintJpegStrips` joins strips that
119
+ * cover the image, in order, into the bytes `createPrintJpegTask` writes for the same plan and source pixels.
120
+ */
121
+ export declare function createPrintJpegStrip(input: PrintJpegInput, strip: PrintJpegStripInput): PrintJpegStrip;
122
+ /**
123
+ * Joins the strips of a 1:1 Print JPEG (`createPrintJpegStrip`), covering the image top to bottom in order, into its file:
124
+ * the same bytes `createPrintJpegTask` writes for the plan and the same source pixels. No source is read, so the output's
125
+ * `providerReads` is 0.
126
+ */
127
+ export declare function composePrintJpegStrips(input: PrintJpegInput, strips: readonly PrintJpegStrip[]): PrintJpegOutput;
101
128
  /** Creates the export task. Filtering weights each source pixel by its exact area overlap, after decoding the declared 8-bit SDR source. */
102
129
  export declare function createPrintJpegTask(input: PrintJpegInput, provider: PrintJpegProvider): PrintJpegTask;
@@ -3,7 +3,7 @@
3
3
  * Print JPEG export: downsamples an SDR image by exact area averaging, then writes a baseline JPEG
4
4
  * with an ICC colour profile. Bounded in size; the encoder is written for this project.
5
5
  */
6
- import { createExportJpegEncoder, EXPORT_JPEG_ENCODER_ID } from "../capture/export-jpeg.js";
6
+ import { createExportJpegEncoder, encodeJpegStrip, EXPORT_JPEG_ENCODER_ID } from "../capture/export-jpeg.js";
7
7
  import { packExportJpegExif, exportTiffNumbers, exportTiffAscii, exportTiffRationals } from "../capture/export-tiff.js";
8
8
  import { createSrgbIccProfile, SRGB_ICC_PROFILE_VERSION } from "../color/srgb-icc.js";
9
9
  import { decodeSrgbComponent, encodeSrgbComponent } from "../color/srgb-transfer.js";
@@ -68,6 +68,82 @@ export function parsePrintJpegInput(input) {
68
68
  return freezeOwnedData({ source, crop, outputWidth, outputHeight, quantizationStep, maximumEncodedBytes, maximumProviderReads, ...(ppi === undefined ? {} : { pixelsPerInch: ppi }),
69
69
  ...(r.maximumPixels === undefined ? {} : { maximumPixels }) });
70
70
  }
71
+ /** The JPEG writer of a Print plan: its size, quantization, budget, ICC profile, EXIF, XMP plan record and density. */
72
+ function printEncoder(plan) {
73
+ const exif = printExif(plan);
74
+ const escaped = JSON.stringify(plan).replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
75
+ const xmp = new TextEncoder().encode('<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:photivra="https://photivra.com/ns/simulation/1.0/" photivra:PrintExportPlan="' + escaped + '" photivra:SourceBytesVerified="False"/></rdf:RDF></x:xmpmeta>');
76
+ return createExportJpegEncoder({ width: plan.outputWidth, height: plan.outputHeight, quantizationStep: plan.quantizationStep, maximumOutputBytes: plan.maximumEncodedBytes, icc: createSrgbIccProfile(), exif, xmp,
77
+ ...(plan.pixelsPerInch === undefined ? {} : { densityDotsPerInch: Math.round(plan.pixelsPerInch) }) });
78
+ }
79
+ /** At 1:1, the output code of each source code (its decoded value re-encoded and rounded). */
80
+ function directCodes() {
81
+ return Uint8Array.from(srgbDecodeTable(), (value) => Math.min(255, Math.max(0, Math.round(encodeSrgbComponent(value) * 255))));
82
+ }
83
+ function requireIdentity(plan) {
84
+ if (plan.outputWidth !== plan.crop.width || plan.outputHeight !== plan.crop.height)
85
+ throw new InvalidConfigurationError("Print JPEG strips require a 1:1 plan (output size equal to the crop).");
86
+ }
87
+ /**
88
+ * Codes rows of a 1:1 Print JPEG (output size equal to the crop) apart from the rows before them, so a large file's strips
89
+ * can be written on separate workers. `strip.y` is a multiple of 8 and `strip.height` a multiple of 8 unless the strip ends at
90
+ * the image's bottom; `strip.samples` are those crop rows' 8-bit sRGB RGB, crop-wide. `composePrintJpegStrips` joins strips that
91
+ * cover the image, in order, into the bytes `createPrintJpegTask` writes for the same plan and source pixels.
92
+ */
93
+ export function createPrintJpegStrip(input, strip) {
94
+ const plan = parsePrintJpegInput(input);
95
+ requireIdentity(plan);
96
+ const r = requireAllowlistedRecord(strip, ["y", "height", "samples"], "Invalid Print JPEG strip.");
97
+ const y = integer(r.y, 0), height = integer(r.height), width = plan.outputWidth, samples = r.samples;
98
+ if (y % 8 !== 0 || y + height > plan.outputHeight || (height % 8 !== 0 && y + height !== plan.outputHeight))
99
+ throw new InvalidConfigurationError("Print JPEG strips start on a block row and cover whole block rows.");
100
+ if (!(samples instanceof Uint8Array) || samples.length !== width * height * 3)
101
+ throw new InvalidConfigurationError("Print JPEG strip requires the crop rows' RGB samples.");
102
+ const direct = directCodes(), blocksX = Math.ceil(width / 8), rowBytes = width * 3;
103
+ // The same block samples as createPrintJpegTask's 1:1 path: each block's edge pixels repeated into a partial block.
104
+ const bits = encodeJpegStrip(plan.quantizationStep, blocksX * Math.ceil(height / 8), (index, out) => {
105
+ const bx = (index % blocksX) * 8, by = Math.floor(index / blocksX) * 8, bw = Math.min(8, width - bx), bh = Math.min(8, plan.outputHeight - y - by);
106
+ for (let yy = 0, o = 0; yy < 8; yy++) {
107
+ const row = (by + Math.min(yy, bh - 1)) * rowBytes + bx * 3;
108
+ for (let x = 0; x < 8; x++, o += 3) {
109
+ const i = row + Math.min(x, bw - 1) * 3;
110
+ out[o] = direct[samples[i]];
111
+ out[o + 1] = direct[samples[i + 1]];
112
+ out[o + 2] = direct[samples[i + 2]];
113
+ }
114
+ }
115
+ });
116
+ return Object.freeze({ y, height, width, quantizationStep: plan.quantizationStep, bits });
117
+ }
118
+ /**
119
+ * Joins the strips of a 1:1 Print JPEG (`createPrintJpegStrip`), covering the image top to bottom in order, into its file:
120
+ * the same bytes `createPrintJpegTask` writes for the plan and the same source pixels. No source is read, so the output's
121
+ * `providerReads` is 0.
122
+ */
123
+ export function composePrintJpegStrips(input, strips) {
124
+ const plan = parsePrintJpegInput(input);
125
+ requireIdentity(plan);
126
+ if (!Array.isArray(strips) || strips.length < 1)
127
+ throw new InvalidConfigurationError("Print JPEG assembly requires strips.");
128
+ const encoder = printEncoder(plan);
129
+ try {
130
+ let next = 0;
131
+ for (const strip of strips) {
132
+ if (strip.y !== next || strip.width !== plan.outputWidth || strip.quantizationStep !== plan.quantizationStep)
133
+ throw new InvalidConfigurationError("Print JPEG strips must cover the plan's image in order.");
134
+ encoder.appendStrip(strip.bits);
135
+ next += strip.height;
136
+ }
137
+ if (next !== plan.outputHeight)
138
+ throw new InvalidConfigurationError("Print JPEG strips must cover the plan's image in order.");
139
+ const bytes = encoder.finish();
140
+ return { schemaVersion: PRINT_JPEG_SCHEMA_VERSION, input: plan, mediaType: "image/jpeg", bytes, iccProfileVersion: SRGB_ICC_PROFILE_VERSION, width: plan.outputWidth, height: plan.outputHeight, resampling: "exact-area-average-in-decoded-linear-light", sourceStage: "post-SDR-8-bit-quantization", digitalUpscalingApplied: false, sourceBytesVerified: false, providerReads: 0, encoderId: EXPORT_JPEG_ENCODER_ID };
141
+ }
142
+ catch (error) {
143
+ encoder.dispose();
144
+ throw error;
145
+ }
146
+ }
71
147
  /** Creates the export task. Filtering weights each source pixel by its exact area overlap, after decoding the declared 8-bit SDR source. */
72
148
  export function createPrintJpegTask(input, provider) {
73
149
  const plan = parsePrintJpegInput(input);
@@ -84,11 +160,7 @@ export function createPrintJpegTask(input, provider) {
84
160
  throw new InvalidConfigurationError("Print export is single-use.");
85
161
  state = "running";
86
162
  try {
87
- const exif = printExif(plan);
88
- const escaped = JSON.stringify(plan).replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
89
- const xmp = new TextEncoder().encode('<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:photivra="https://photivra.com/ns/simulation/1.0/" photivra:PrintExportPlan="' + escaped + '" photivra:SourceBytesVerified="False"/></rdf:RDF></x:xmpmeta>');
90
- encoder = createExportJpegEncoder({ width: plan.outputWidth, height: plan.outputHeight, quantizationStep: plan.quantizationStep, maximumOutputBytes: plan.maximumEncodedBytes, icc: createSrgbIccProfile(), exif, xmp,
91
- ...(plan.pixelsPerInch === undefined ? {} : { densityDotsPerInch: Math.round(plan.pixelsPerInch) }) });
163
+ encoder = printEncoder(plan);
92
164
  const scaleX = plan.crop.width / plan.outputWidth, scaleY = plan.crop.height / plan.outputHeight, area = scaleX * scaleY;
93
165
  const cropRight = plan.crop.x + plan.crop.width, cropBottom = plan.crop.y + plan.crop.height, rowBytes = plan.crop.width * 3;
94
166
  // Every crop pixel is read exactly once, in tiles of at most 256 x 32 aligned to the crop origin.
@@ -140,7 +212,7 @@ export function createPrintJpegTask(input, provider) {
140
212
  // At 1:1 every output pixel is one source pixel with weight exactly 1, so its sum is exactly lut[code] and its
141
213
  // sample is a function of the code alone: the same bytes from a 256-entry table, without the accumulation.
142
214
  const identity = scaleX === 1 && scaleY === 1;
143
- const direct = identity ? Uint8Array.from(lut, (value) => Math.min(255, Math.max(0, Math.round(encodeSrgbComponent(value) * 255)))) : undefined;
215
+ const direct = identity ? directCodes() : undefined;
144
216
  if (direct)
145
217
  for (let by = 0; by < plan.outputHeight; by += 8) {
146
218
  check();