@photivra/engine 2.20.0 → 2.21.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 (174) hide show
  1. package/CHANGELOG.md +178 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +7 -0
  6. package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
  7. package/dist/capture/dng-lens-correction-opcodes.js +2 -2
  8. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
  9. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  10. package/dist/capture/exposure-build-up-stage.js +4 -0
  11. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  12. package/dist/capture/native-environment-lean.d.ts +2 -1
  13. package/dist/capture/native-environment-lean.js +41 -73
  14. package/dist/capture/native-environment-lean.js.map +1 -1
  15. package/dist/capture/native-environment-tabulated.js +4 -3
  16. package/dist/capture/native-environment-tabulated.js.map +1 -1
  17. package/dist/capture/native-raw.d.ts +3 -2
  18. package/dist/capture/native-raw.js +5 -4
  19. package/dist/capture/native-raw.js.map +1 -1
  20. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  21. package/dist/capture/sensor-raw-producer.js +12 -3
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/core/version.d.ts +1 -1
  24. package/dist/core/version.js +1 -1
  25. package/dist/core/version.js.map +1 -1
  26. package/dist/equipment/generic-lens-catalogue.d.ts +3 -2
  27. package/dist/equipment/generic-lens-catalogue.js +18 -5
  28. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  29. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  30. package/dist/equipment/generic-tier-assets.js +16 -11
  31. package/dist/equipment/generic-tier-assets.js.map +1 -1
  32. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  33. package/dist/equipment/sensor-profile-body.js +77 -0
  34. package/dist/equipment/sensor-profile-body.js.map +1 -0
  35. package/dist/equipment/sensor-profiles.d.ts +8 -0
  36. package/dist/equipment/sensor-profiles.js +82 -0
  37. package/dist/equipment/sensor-profiles.js.map +1 -0
  38. package/dist/facade/catalogue-lens-signal.d.ts +2 -2
  39. package/dist/facade/catalogue-lens-signal.js +58 -71
  40. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  41. package/dist/facade/chart-radiance-batch.js +2 -1
  42. package/dist/facade/chart-radiance-batch.js.map +1 -1
  43. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  44. package/dist/facade/defocus-image-space-provider.js +64 -76
  45. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  46. package/dist/facade/display-view-gpu-session.js +2 -0
  47. package/dist/facade/display-view-gpu-session.js.map +1 -1
  48. package/dist/facade/display-view-gpu.d.ts +6 -1
  49. package/dist/facade/display-view-gpu.js.map +1 -1
  50. package/dist/facade/display-view-lens-signal.js +25 -23
  51. package/dist/facade/display-view-lens-signal.js.map +1 -1
  52. package/dist/facade/display-view.js +5 -23
  53. package/dist/facade/display-view.js.map +1 -1
  54. package/dist/facade/expected-signal-setup.d.ts +3 -1
  55. package/dist/facade/expected-signal-setup.js +1 -1
  56. package/dist/facade/expected-signal-setup.js.map +1 -1
  57. package/dist/facade/expected-signal.d.ts +47 -14
  58. package/dist/facade/expected-signal.js +117 -112
  59. package/dist/facade/expected-signal.js.map +1 -1
  60. package/dist/facade/exposure-build-up.d.ts +70 -0
  61. package/dist/facade/exposure-build-up.js +164 -0
  62. package/dist/facade/exposure-build-up.js.map +1 -0
  63. package/dist/facade/fast-methods.js +9 -1
  64. package/dist/facade/fast-methods.js.map +1 -1
  65. package/dist/facade/generic-lens-capture.d.ts +2 -2
  66. package/dist/facade/generic-lens-capture.js +2 -2
  67. package/dist/facade/generic-lens-capture.js.map +1 -1
  68. package/dist/facade/image-plane-enable.d.ts +7 -0
  69. package/dist/facade/image-plane-enable.js +17 -0
  70. package/dist/facade/image-plane-enable.js.map +1 -0
  71. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  72. package/dist/facade/image-plane-lattice-data.js +10 -0
  73. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  74. package/dist/facade/image-plane-support.d.ts +33 -0
  75. package/dist/facade/image-plane-support.js +11 -0
  76. package/dist/facade/image-plane-support.js.map +1 -0
  77. package/dist/facade/image-plane.d.ts +36 -0
  78. package/dist/facade/image-plane.js +353 -0
  79. package/dist/facade/image-plane.js.map +1 -0
  80. package/dist/facade/mixed-illumination.d.ts +108 -0
  81. package/dist/facade/mixed-illumination.js +174 -0
  82. package/dist/facade/mixed-illumination.js.map +1 -0
  83. package/dist/facade/representative-light-sources.d.ts +72 -0
  84. package/dist/facade/representative-light-sources.js +124 -0
  85. package/dist/facade/representative-light-sources.js.map +1 -0
  86. package/dist/facade/scene-radiance.d.ts +129 -4
  87. package/dist/facade/scene-radiance.js +48 -13
  88. package/dist/facade/scene-radiance.js.map +1 -1
  89. package/dist/facade/simulate-capture.d.ts +39 -10
  90. package/dist/facade/simulate-capture.js +252 -92
  91. package/dist/facade/simulate-capture.js.map +1 -1
  92. package/dist/facade/simulate-display-view.js +14 -6
  93. package/dist/facade/simulate-display-view.js.map +1 -1
  94. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  95. package/dist/gpu/chart-composite-gpu.js +48 -9
  96. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  97. package/dist/gpu/chart-radiance-gpu.js +4 -30
  98. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  99. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  100. package/dist/gpu/display-view-composite-material-module.js +39 -13
  101. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  102. package/dist/gpu/display-view-image-material-module.d.ts +86 -0
  103. package/dist/gpu/display-view-image-material-module.js +339 -0
  104. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  105. package/dist/gpu/display-view-image-quality.d.ts +45 -0
  106. package/dist/gpu/display-view-image-quality.js +380 -0
  107. package/dist/gpu/display-view-image-quality.js.map +1 -0
  108. package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
  109. package/dist/gpu/display-view-quality-gpu.js +136 -205
  110. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  111. package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
  112. package/dist/gpu/display-view-speed-gpu.js +131 -93
  113. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  114. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
  115. package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
  116. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  117. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
  118. package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
  119. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  120. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  121. package/dist/gpu/wgsl-exact-sum.js +42 -0
  122. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  123. package/dist/index.d.ts +11 -3
  124. package/dist/index.js +7 -1
  125. package/dist/index.js.map +1 -1
  126. package/dist/motion/subject-motion.d.ts +66 -0
  127. package/dist/motion/subject-motion.js +52 -0
  128. package/dist/motion/subject-motion.js.map +1 -0
  129. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  130. package/dist/optics/generic-lens-lateral-color.js +77 -0
  131. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  132. package/dist/optics/generic-lens-psf.js +54 -46
  133. package/dist/optics/generic-lens-psf.js.map +1 -1
  134. package/dist/optics/lens-psf-profile.d.ts +7 -0
  135. package/dist/optics/lens-psf-profile.js +2 -1
  136. package/dist/optics/lens-psf-profile.js.map +1 -1
  137. package/dist/output/developed-lens-corrections.d.ts +19 -5
  138. package/dist/output/developed-lens-corrections.js +28 -9
  139. package/dist/output/developed-lens-corrections.js.map +1 -1
  140. package/dist/output/generic-lens-corrections.d.ts +41 -5
  141. package/dist/output/generic-lens-corrections.js +52 -8
  142. package/dist/output/generic-lens-corrections.js.map +1 -1
  143. package/dist/output/print-plan.d.ts +41 -5
  144. package/dist/output/print-plan.js +59 -14
  145. package/dist/output/print-plan.js.map +1 -1
  146. package/dist/sensor/binomial-sample.d.ts +3 -0
  147. package/dist/sensor/binomial-sample.js +86 -0
  148. package/dist/sensor/binomial-sample.js.map +1 -0
  149. package/dist/sensor/sensor-characteristics.d.ts +10 -0
  150. package/dist/sensor/sensor-characteristics.js +20 -9
  151. package/dist/sensor/sensor-characteristics.js.map +1 -1
  152. package/docs/AGENT_GUIDE.md +1 -0
  153. package/docs/API_REFERENCE.md +98 -1
  154. package/docs/QUICK_START.md +125 -0
  155. package/docs/USAGE.md +92 -1
  156. package/docs/api/core-version.md +1 -1
  157. package/docs/api/equipment-generic-lens-catalogue.md +10 -5
  158. package/docs/api/equipment-sensor-profile-body.md +64 -0
  159. package/docs/api/equipment-sensor-profiles.md +86 -0
  160. package/docs/api/exports.json +207 -2
  161. package/docs/api/facade-exposure-build-up.md +105 -0
  162. package/docs/api/facade-fast-methods.md +9 -1
  163. package/docs/api/facade-image-plane-enable.md +16 -0
  164. package/docs/api/facade-mixed-illumination.md +67 -0
  165. package/docs/api/facade-representative-light-sources.md +131 -0
  166. package/docs/api/facade-scene-radiance.md +54 -3
  167. package/docs/api/facade-simulate-capture.md +26 -6
  168. package/docs/api/motion-subject-motion.md +23 -0
  169. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  170. package/docs/api/output-developed-lens-corrections.md +10 -1
  171. package/docs/api/output-generic-lens-corrections.md +40 -4
  172. package/docs/api/output-print-plan.md +35 -1
  173. package/llms.txt +2 -0
  174. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  # Public API reference
2
2
 
3
- Generated from the exact root exports for **@photivra/engine 2.20.0 / root contract 2.20.0** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
3
+ Generated from the exact root exports for **@photivra/engine 2.21.0 / root contract 2.21.0** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
4
4
 
5
5
  Use [the developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md) for executable paths and scientific boundaries. Types are contracts, not runtime validation: parse untrusted data, preserve units/reference frames, read assessment blockers, and retain evidence/limitations. Optional properties do not imply a universal default. Model/schema IDs remain independent of the distribution.
6
6
 
@@ -976,6 +976,26 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
976
976
  | [`GenericReleaseDriveMode`](api/equipment-release-capabilities.md#genericreleasedrivemode) | Type |
977
977
  | [`parseGenericReleaseCapabilityProfile`](api/equipment-release-capabilities.md#parsegenericreleasecapabilityprofile) | Runtime |
978
978
 
979
+ ## equipment/sensor-profile-body.ts
980
+
981
+ [Detailed contracts](api/equipment-sensor-profile-body.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/equipment/sensor-profile-body.ts)
982
+
983
+ | Export | Kind |
984
+ | --- | --- |
985
+ | [`SENSOR_PROFILE_ISO_ANCHOR`](api/equipment-sensor-profile-body.md#sensor_profile_iso_anchor) | Runtime |
986
+ | [`SensorProfile`](api/equipment-sensor-profile-body.md#sensorprofile) | Type |
987
+ | [`SensorProfileValueKind`](api/equipment-sensor-profile-body.md#sensorprofilevaluekind) | Type |
988
+
989
+ ## equipment/sensor-profiles.ts
990
+
991
+ [Detailed contracts](api/equipment-sensor-profiles.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/equipment/sensor-profiles.ts)
992
+
993
+ | Export | Kind |
994
+ | --- | --- |
995
+ | [`resolveSensorProfile`](api/equipment-sensor-profiles.md#resolvesensorprofile) | Runtime |
996
+ | [`SENSOR_PROFILE_IDS`](api/equipment-sensor-profiles.md#sensor_profile_ids) | Runtime |
997
+ | [`SENSOR_PROFILES`](api/equipment-sensor-profiles.md#sensor_profiles) | Runtime |
998
+
979
999
  ## exposure/auto-iso-slowest-shutter.ts
980
1000
 
981
1001
  [Detailed contracts](api/exposure-auto-iso-slowest-shutter.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/exposure/auto-iso-slowest-shutter.ts)
@@ -1314,6 +1334,19 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1314
1334
  | [`DisplayViewCorrectionStateSet`](api/facade-display-view.md#displayviewcorrectionstateset) | Type |
1315
1335
  | [`serializeDisplayViewCorrectionStates`](api/facade-display-view.md#serializedisplayviewcorrectionstates) | Runtime |
1316
1336
 
1337
+ ## facade/exposure-build-up.ts
1338
+
1339
+ [Detailed contracts](api/facade-exposure-build-up.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/exposure-build-up.ts)
1340
+
1341
+ | Export | Kind |
1342
+ | --- | --- |
1343
+ | [`EXPOSURE_BUILD_UP_MAXIMUM_FRACTIONS`](api/facade-exposure-build-up.md#exposure_build_up_maximum_fractions) | Runtime |
1344
+ | [`EXPOSURE_BUILD_UP_STREAM_BASE`](api/facade-exposure-build-up.md#exposure_build_up_stream_base) | Runtime |
1345
+ | [`simulateExposureBuildUp`](api/facade-exposure-build-up.md#simulateexposurebuildup) | Runtime |
1346
+ | [`SimulateExposureBuildUpFrame`](api/facade-exposure-build-up.md#simulateexposurebuildupframe) | Type |
1347
+ | [`SimulateExposureBuildUpOptions`](api/facade-exposure-build-up.md#simulateexposurebuildupoptions) | Type |
1348
+ | [`SimulateExposureBuildUpResult`](api/facade-exposure-build-up.md#simulateexposurebuildupresult) | Type |
1349
+
1317
1350
  ## facade/fast-methods.ts
1318
1351
 
1319
1352
  [Detailed contracts](api/facade-fast-methods.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/fast-methods.ts)
@@ -1323,6 +1356,44 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1323
1356
  | [`SIMULATE_CAPTURE_FAST_METHODS`](api/facade-fast-methods.md#simulate_capture_fast_methods) | Runtime |
1324
1357
  | [`SimulateCaptureFastMethod`](api/facade-fast-methods.md#simulatecapturefastmethod) | Type |
1325
1358
 
1359
+ ## facade/image-plane-enable.ts
1360
+
1361
+ [Detailed contracts](api/facade-image-plane-enable.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/image-plane-enable.ts)
1362
+
1363
+ | Export | Kind |
1364
+ | --- | --- |
1365
+ | [`prepareImagePlaneScenes`](api/facade-image-plane-enable.md#prepareimageplanescenes) | Runtime |
1366
+
1367
+ ## facade/mixed-illumination.ts
1368
+
1369
+ [Detailed contracts](api/facade-mixed-illumination.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/mixed-illumination.ts)
1370
+
1371
+ | Export | Kind |
1372
+ | --- | --- |
1373
+ | [`MixedIllumination`](api/facade-mixed-illumination.md#mixedillumination) | Type |
1374
+ | [`MixedIlluminationBlend`](api/facade-mixed-illumination.md#mixedilluminationblend) | Type |
1375
+ | [`MixedIlluminationPoint`](api/facade-mixed-illumination.md#mixedilluminationpoint) | Type |
1376
+ | [`MixedIlluminationPointSource`](api/facade-mixed-illumination.md#mixedilluminationpointsource) | Type |
1377
+ | [`MixedIlluminationSource`](api/facade-mixed-illumination.md#mixedilluminationsource) | Type |
1378
+
1379
+ ## facade/representative-light-sources.ts
1380
+
1381
+ [Detailed contracts](api/facade-representative-light-sources.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/representative-light-sources.ts)
1382
+
1383
+ | Export | Kind |
1384
+ | --- | --- |
1385
+ | [`createRepresentativeLightSourceIlluminant`](api/facade-representative-light-sources.md#createrepresentativelightsourceilluminant) | Runtime |
1386
+ | [`createRepresentativeLightSourceSpectrum`](api/facade-representative-light-sources.md#createrepresentativelightsourcespectrum) | Runtime |
1387
+ | [`getRepresentativeLightSourceSet`](api/facade-representative-light-sources.md#getrepresentativelightsourceset) | Runtime |
1388
+ | [`REPRESENTATIVE_LIGHT_SOURCE_IDS`](api/facade-representative-light-sources.md#representative_light_source_ids) | Runtime |
1389
+ | [`REPRESENTATIVE_LIGHT_SOURCE_SET_ID`](api/facade-representative-light-sources.md#representative_light_source_set_id) | Runtime |
1390
+ | [`REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION`](api/facade-representative-light-sources.md#representative_light_source_set_version) | Runtime |
1391
+ | [`REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS`](api/facade-representative-light-sources.md#representative_light_source_wavelength_range_nanometers) | Runtime |
1392
+ | [`RepresentativeLightSource`](api/facade-representative-light-sources.md#representativelightsource) | Type |
1393
+ | [`RepresentativeLightSourceBand`](api/facade-representative-light-sources.md#representativelightsourceband) | Type |
1394
+ | [`RepresentativeLightSourceId`](api/facade-representative-light-sources.md#representativelightsourceid) | Type |
1395
+ | [`RepresentativeLightSourceSet`](api/facade-representative-light-sources.md#representativelightsourceset) | Type |
1396
+
1326
1397
  ## facade/scene-radiance.ts
1327
1398
 
1328
1399
  [Detailed contracts](api/facade-scene-radiance.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/scene-radiance.ts)
@@ -1337,6 +1408,8 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1337
1408
  | [`CHART_SIZE_DEFAULT_SURROUND_REFLECTANCE`](api/facade-scene-radiance.md#chart_size_default_surround_reflectance) | Runtime |
1338
1409
  | [`ChartSizeOption`](api/facade-scene-radiance.md#chartsizeoption) | Type |
1339
1410
  | [`createPlanarSceneRadianceProvider`](api/facade-scene-radiance.md#createplanarsceneradianceprovider) | Runtime |
1411
+ | [`ImagePlaneImage`](api/facade-scene-radiance.md#imageplaneimage) | Type |
1412
+ | [`ImagePlaneScene`](api/facade-scene-radiance.md#imageplanescene) | Type |
1340
1413
  | [`PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W`](api/facade-scene-radiance.md#photopic_maximum_luminous_efficacy_lm_per_w) | Runtime |
1341
1414
  | [`PlanarSceneRadianceProvider`](api/facade-scene-radiance.md#planarsceneradianceprovider) | Type |
1342
1415
  | [`resolveCaptureIlluminant`](api/facade-scene-radiance.md#resolvecaptureilluminant) | Runtime |
@@ -1563,6 +1636,14 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1563
1636
  | [`CalculateProjectedMotionBlurInput`](api/motion-projected-motion.md#calculateprojectedmotionblurinput) | Type |
1564
1637
  | [`ProjectedMotionBlur`](api/motion-projected-motion.md#projectedmotionblur) | Type |
1565
1638
 
1639
+ ## motion/subject-motion.ts
1640
+
1641
+ [Detailed contracts](api/motion-subject-motion.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/motion/subject-motion.ts)
1642
+
1643
+ | Export | Kind |
1644
+ | --- | --- |
1645
+ | [`SimulateCaptureSubjectMotion`](api/motion-subject-motion.md#simulatecapturesubjectmotion) | Type |
1646
+
1566
1647
  ## motion/temporal-exposure-sum.ts
1567
1648
 
1568
1649
  [Detailed contracts](api/motion-temporal-exposure-sum.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/motion/temporal-exposure-sum.ts)
@@ -1801,6 +1882,17 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1801
1882
  | [`resolveFrontOfLensFilterTransmission`](api/optics-front-of-lens-filter.md#resolvefrontoflensfiltertransmission) | Runtime |
1802
1883
  | [`ResolveFrontOfLensFilterTransmissionInput`](api/optics-front-of-lens-filter.md#resolvefrontoflensfiltertransmissioninput) | Type |
1803
1884
 
1885
+ ## optics/generic-lens-lateral-color.ts
1886
+
1887
+ [Detailed contracts](api/optics-generic-lens-lateral-color.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/optics/generic-lens-lateral-color.ts)
1888
+
1889
+ | Export | Kind |
1890
+ | --- | --- |
1891
+ | [`calculateLateralColorFringe`](api/optics-generic-lens-lateral-color.md#calculatelateralcolorfringe) | Runtime |
1892
+ | [`CalculateLateralColorFringeInput`](api/optics-generic-lens-lateral-color.md#calculatelateralcolorfringeinput) | Type |
1893
+ | [`LateralColorDisplacement`](api/optics-generic-lens-lateral-color.md#lateralcolordisplacement) | Type |
1894
+ | [`LateralColorFringe`](api/optics-generic-lens-lateral-color.md#lateralcolorfringe) | Type |
1895
+
1804
1896
  ## optics/generic-nd-filter-presets.ts
1805
1897
 
1806
1898
  [Detailed contracts](api/optics-generic-nd-filter-presets.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/optics/generic-nd-filter-presets.ts)
@@ -2183,6 +2275,7 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
2183
2275
 
2184
2276
  | Export | Kind |
2185
2277
  | --- | --- |
2278
+ | [`ChannelDispersion`](api/output-developed-lens-corrections.md#channeldispersion) | Type |
2186
2279
  | [`GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM`](api/output-developed-lens-corrections.md#generic_lens_correction_channel_wavelengths_nm) | Runtime |
2187
2280
 
2188
2281
  ## output/generic-lens-corrections.ts
@@ -2191,6 +2284,8 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
2191
2284
 
2192
2285
  | Export | Kind |
2193
2286
  | --- | --- |
2287
+ | [`calculateChannelEffectiveDispersion`](api/output-generic-lens-corrections.md#calculatechanneleffectivedispersion) | Runtime |
2288
+ | [`CalculateChannelEffectiveDispersionInput`](api/output-generic-lens-corrections.md#calculatechanneleffectivedispersioninput) | Type |
2194
2289
  | [`createGenericLensCorrectionProfile`](api/output-generic-lens-corrections.md#creategenericlenscorrectionprofile) | Runtime |
2195
2290
  | [`CreateGenericLensCorrectionProfileInput`](api/output-generic-lens-corrections.md#creategenericlenscorrectionprofileinput) | Type |
2196
2291
 
@@ -2336,7 +2431,9 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
2336
2431
  | [`calculatePrintPlan`](api/output-print-plan.md#calculateprintplan) | Runtime |
2337
2432
  | [`calculatePrintSizeLimit`](api/output-print-plan.md#calculateprintsizelimit) | Runtime |
2338
2433
  | [`parsePrintPlanInput`](api/output-print-plan.md#parseprintplaninput) | Runtime |
2434
+ | [`PrintContainLayout`](api/output-print-plan.md#printcontainlayout) | Type |
2339
2435
  | [`PrintedImageSize`](api/output-print-plan.md#printedimagesize) | Type |
2436
+ | [`PrintFit`](api/output-print-plan.md#printfit) | Type |
2340
2437
  | [`PrintLength`](api/output-print-plan.md#printlength) | Type |
2341
2438
  | [`PrintNativeSource`](api/output-print-plan.md#printnativesource) | Type |
2342
2439
  | [`PrintPlan`](api/output-print-plan.md#printplan) | Type |
@@ -88,6 +88,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
88
88
  | `chartSize` | fill the frame | 2.3: `{ widthM, heightM?, surroundReflectance? }` places the chart at its physical size (see [Fixed-size chart](#fixed-size-chart-23)). |
89
89
  | `illuminant` | Planckian 6500 K | Also `"cie-a"`, `{ kind: "planckian", temperatureKelvin }`, `{ kind: "daylight", correlatedColorTemperatureKelvin, basis }` or `{ kind: "spectrum", spectrum }`. |
90
90
  | `illuminanceLux` | 21 481 (π·80 000/11.7) | See [Exposure](#exposure). |
91
+ | `illumination` | off | 2.22 (#550): mixed lighting, two to eight sources (`{ illuminant, illuminanceLux }`) blended across the chart by a linear gradient, a radial falloff or regions, and point sources (`points`) with inverse-square falloff, instead of `illuminant` and `illuminanceLux`. The frame center's light sets the grey card and its strongest source the `"scene-illuminant"` white balance (see [Mixed lighting](https://github.com/Photivra/photivra/blob/main/docs/MIXED_ILLUMINATION.md)). |
91
92
  | `output.width`, `output.height` | 64 × 64 | Output pixels, 1–4094 each. The sensor has one extra site on each side for demosaicing (see `output.borderHandling`). |
92
93
  | `output.maximumPixels` | off | Opt-in large images (2.2): the most native sensor sites the capture may allocate, up to `NATIVE_RAW_LIMITS.ceilingPixels` (2^27). Sides may then reach 16 384 sites including the margin (see [Large images](#large-images)). |
93
94
  | `output.region` | whole frame | 2.3: `{ x, y, width, height }` renders that rectangle of the frame, byte-identical to the whole render (see [Region rendering](#region-rendering-23)). |
@@ -118,6 +119,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
118
119
  | `motion.angularVelocityRadPerSec` | still | `{ pitch?, yaw?, roll? }` in rad/s (see [Motion](#motion)). |
119
120
  | `motion.shutter` | `"global"` | Or `"rolling"`. |
120
121
  | `motion.rollingShutterSkewSeconds` | the body's readout skew | Rolling only. |
122
+ | `motion.subject` | still subject | 2.22.0: `{ velocityMPerSec?: { x?, y? }, angularVelocityRadPerSec?, region?, fast? }`, a moving subject (see [Subject motion](#subject-motion)). |
121
123
  | `signal`, `onProgress` | none | An `AbortSignal` cancels the running task; `onProgress` reports `raw`, `white-balance`, `development`, `diffraction-correction` and `sharpening` (#571: output rows, with those options) and `jpeg` tiles. The last event of each stage has `completedTileCount === totalTileCount`. The capture yields to the host about every 8 ms with a `MessageChannel` message (else `setImmediate`, else `setTimeout`), so other queued tasks run in between: an abort sent from another task, such as a Web Worker `message` event, takes effect at the next yield. |
122
124
 
123
125
  Unknown fields are rejected, so a typo never silently falls back to a default.
@@ -530,6 +532,29 @@ SNR is 88.5 (38.9 dB); at ISO 6400 with 1/64 of the light, S ≈ 123 e⁻ and th
530
532
  the photon shot noise grows as √S, so less light means more noise whatever the ISO. The display view
531
533
  reports the same `sensor` record as its capture.
532
534
 
535
+ ### Sensor profiles (#554)
536
+
537
+ `sensor.profile` takes one of eight representative sensor profile records whose values come from
538
+ published specifications (`SENSOR_PROFILES`, or `resolveSensorProfile(id)`; unnamed, by format,
539
+ resolution, architecture and era), checked strictly, so a modified copy also works. It
540
+ replaces the body's ISO range, readout regimes (full well, read noise of each conversion gain, the
541
+ dual-gain switch and the high gain's storage, ADC bit depth), readout time and fixed-pattern noise,
542
+ and sets the default pitch and peak QE; the characteristics above still override it one at a time.
543
+
544
+ ```ts
545
+ import { resolveSensorProfile, simulateCapture } from "@photivra/engine";
546
+ const phone = await simulateCapture({ scene: { preset: "uniform" }, sensor: { profile: resolveSensorProfile("phone-50mp-stacked") } });
547
+ phone.sensor.fullWellElectrons; // 8 600 e⁻ at 1.197 µm
548
+ ```
549
+
550
+ Each profile's base ISO is re-rated from its full well, QE and pixel area, so the same ISO reaches
551
+ the RAW white at the same exposure on every profile (the assumption `sensor.profile` records the
552
+ values and the rounding). The phone profile takes the catalogue's three phone lenses
553
+ (`lens: { catalogue: "pv-24-1.8-phone" }`), and they need it. With a profile, a catalogue lens is framed on
554
+ the profile's real sensor (the largest centred region of the output's aspect; assumption `sensor.profileView`), so it
555
+ shows its real field of view. Values, sources, the framing and the ISO rating:
556
+ [SENSOR_PROFILES.md](https://github.com/Photivra/photivra/blob/main/docs/SENSOR_PROFILES.md).
557
+
533
558
  ## Exposure
534
559
 
535
560
  The facade meters the scene like a reflected-light meter:
@@ -1689,6 +1714,48 @@ const pan = await simulateCapture({
1689
1714
  console.log(pan.assumptions.find((a) => a.id === "sensor.shutter")?.value, pan.exposure.settings.shutterSeconds);
1690
1715
  ```
1691
1716
 
1717
+ ## Subject motion
1718
+
1719
+ `motion.subject` (2.22.0, #551) moves the subject on the scene plane during the exposure: the whole scene, or one
1720
+ rectangle of it (`region: { centerXM, centerYM, widthM, heightM }` in metres at rest) over a still background.
1721
+
1722
+ - `velocityMPerSec: { x, y }` is a translation in m/s on the plane (+x right, +y up as seen from the camera).
1723
+ `angularVelocityRadPerSec` adds a rotation about an axis parallel to the optical axis, counter-clockwise as seen from
1724
+ the camera: the optical axis for the whole scene, the region's center for a region.
1725
+ - The model is rigid motion of a planar layer. At time t from the first opening (camera rotation's time reference), a
1726
+ subject point at rest q is at R(ωt)(q − c) + c + v·t. A plane point shows the subject where one is there, else the
1727
+ region's rest rectangle shows a neutral 18 % background (what the subject revealed), else the still scene. The background
1728
+ is lit by the plane's irradiance at that point: the illuminant, or with `illumination` (#550) the blend and point sources there.
1729
+ - Every scene query reads the subject at its own time. The blur is therefore the same temporal midpoint quadrature as
1730
+ camera rotation (`sensor.temporalSampleCount`, default 8 with a moving subject), and an edge streaks over
1731
+ v·T·m on the sensor (m = v′/D, the thin-lens image distance over the subject distance). With few samples a long streak
1732
+ shows separate copies; raise the count. A test checks the length within sampling error.
1733
+ - A rolling shutter skews a moving subject: each row's exposure window opens later, so it sees the subject later. A test
1734
+ checks the skew against the site windows' start offsets.
1735
+ - Panning: a camera yaw of −v/D rad/s (the engine's rotation convention) follows a subject moving at v m/s in +x. The
1736
+ subject stays sharp and the background streaks.
1737
+ - It combines with `lens.diffraction`, `lens.defocus` (global shutter; their site lattices are formed at each midpoint), the
1738
+ lens projection, distortion and vignetting, and camera rotation. `lens.pupilSampling` is refused.
1739
+ - Display view: sideways motion under a global shutter only. It refuses a rotating subject and a rolling shutter with a
1740
+ reason; `simulateCapture` renders both.
1741
+ - Fast method (2.20.0 convention): with the default count and a still camera, `fast` takes one midpoint per site of the
1742
+ streak plus one when that is fewer than 8 (taps less than one site apart). Exact keeps 8, and an explicit
1743
+ `sensor.temporalSampleCount` always wins. Fast is the default for sRGB samples and the display view, exact for DNG,
1744
+ JPEG and PNG.
1745
+ - Off by default: a request without `motion.subject` is byte-identical to 2.20.0.
1746
+
1747
+ ```ts
1748
+ import { simulateCapture } from "@photivra/engine";
1749
+
1750
+ // A subject 4 mm wide moving right at 0.2 m/s, with the camera panning to follow it.
1751
+ const pan = await simulateCapture({
1752
+ scene: { preset: "bar-groups" }, subjectDistanceM: 2, lens: { focalLengthMm: 50 },
1753
+ motion: { angularVelocityRadPerSec: { yaw: -0.1 }, subject: { velocityMPerSec: { x: 0.2 }, region: { centerXM: 0.002, centerYM: 0, widthM: 0.004, heightM: 0.004 } } },
1754
+ output: { width: 32, height: 16 }
1755
+ });
1756
+ console.log(pan.assumptions.find((a) => a.id === "motion.subject")?.value);
1757
+ ```
1758
+
1692
1759
  ## Performance
1693
1760
 
1694
1761
  Almost all of the cost is engine time per sensor site; the facade itself adds little. Since 2.0, the
@@ -2029,6 +2096,64 @@ developed image in 64-bit floats (24 bytes per pixel, 549 MiB at 24 MP), held as
2029
2096
  sampled every 50 ms). The extra pass therefore costs about 5 s on one worker after the parts, against about 10 s to render
2030
2097
  the whole frame again on one worker.
2031
2098
 
2099
+ ### Exposure build-up (#534)
2100
+
2101
+ `simulateExposureBuildUp(options, { fractions })` renders fractions of one exposure as one accumulating process, the way a
2102
+ camera's live readout of a long exposure shows the light building up. Each fraction is read out and developed as its own
2103
+ photo, and the fraction at 1 is `simulateCapture(options)`, byte for byte (owner decision, 2026-11-06).
2104
+
2105
+ - **Signal.** Each fraction fⱼ below 1 is a capture of the same request in manual mode at the whole exposure's resolved
2106
+ f-number and ISO and the shutter fⱼ·t. So its expected signal at every site is integrated from the spectrum over the window
2107
+ [0, fⱼ·t] by the capture itself. That includes camera rotation, `motion.subject` (#551), mixed lighting (#550, which does not
2108
+ change over time) and, with a rolling shutter, each row's own window. No per-channel signal is precomputed (owner rule).
2109
+ - **Charge.** Photon and dark-charge arrivals are Poisson processes with any rate over time. Given a count n over [0, t], the
2110
+ count over [0, s] is Binomial(n, Λ(s)/Λ(t)), with Λ the expected count (R. G. Gallager, *Discrete Stochastic Processes*,
2111
+ chapter 2, sections 2.4 and 2.5, MIT OpenCourseWare 6.262, free). The whole exposure keeps its ordinary draws. Going down
2112
+ the fractions, each site's photo and dark counts are split: N(fⱼ) ~ Binomial(N(fⱼ₊₁), λ(fⱼ)/λ(fⱼ₊₁)), with λ the site's
2113
+ expected mean in each render (after PRNU and DSNU). As a result:
2114
+ - each fraction's counts are Poisson with that fraction's own mean, which is the mean and variance of a capture at fⱼ·t;
2115
+ - the counts never decrease from one fraction to the next;
2116
+ - two fractions' counts covary by the earlier one's mean.
2117
+
2118
+ The binomial draws are exact: inversion, and Hörmann's BTRS rejection (`src/sensor/binomial-sample.ts`).
2119
+ - **Quadrature.** The windows are nested, so the true means never decrease. The temporal midpoint quadrature can still give an
2120
+ earlier fraction a larger estimate than a later one at a site under motion. There the count is kept (probability 1), and the
2121
+ `exposure.buildUp` assumption counts those sites (`nonMonotoneSites`). Raise `sensor.temporalSampleCount` to remove them.
2122
+ - **Sensor.** Fixed-pattern noise (PRNU, DSNU, hot pixels, offsets) belongs to the sensor and is the same in every fraction.
2123
+ Each fraction's charge is clipped at the full well (`sensor.fullWell`), then read out once: its own read noise (the regime's
2124
+ components; dual gain as the ISO selects), pre-ADC and ADC saturation, and quantization at the ADC bit depth (#555; a #554
2125
+ sensor profile's readout feeds the same stage).
2126
+ - **Cost and settings.** One capture per fraction, plus about 64 bytes per native site for the chain. The request's `fast`
2127
+ and the format default apply to every fraction, and the build-up adds no fast method: with `fast: false` (the calculator's
2128
+ setting) every fraction is an exact capture. `"auto"` white balance is measured on each fraction, as its own photo.
2129
+ - **Limits.** On screen only: formats `"srgb-8"` and `"srgb-16"` (a fraction's JPEG, PNG or DNG is refused). 1 to 64
2130
+ fractions, strictly increasing, above 0 and at most 1. Each fraction's shutter fⱼ·t must be one the body can set. A
2131
+ fraction's realization depends on the whole list, because each step of the chain has its own seeded streams
2132
+ (`EXPOSURE_BUILD_UP_STREAM_BASE`). The same request and list always give the same frames.
2133
+ - **Result.** One frame per fraction: `{ fraction, elapsedSeconds, result }`. Below 1, `result` is the fraction capture's
2134
+ result (`image`, `raw`, `plan` and `request` at fⱼ·t), with the build-up's counts and the assumption `exposure.buildUp`.
2135
+ Its `exposure` and `sensor` are the whole exposure's reports: the camera's settings and noise figure, not the elapsed part's.
2136
+
2137
+ ```ts
2138
+ import { simulateExposureBuildUp } from "@photivra/engine";
2139
+
2140
+ const build = await simulateExposureBuildUp({ scene: { preset: "slanted-edge" }, exposure: { mode: "manual", shutterSeconds: 1 / 15, iso: 400 },
2141
+ motion: { angularVelocityRadPerSec: { yaw: 0.2 } }, output: { width: 64, height: 48 } }, { fractions: [0.2, 0.4, 0.6, 0.8, 1] });
2142
+ for (const frame of build.frames) console.log(frame.fraction, frame.elapsedSeconds, frame.result.image.samples.length);
2143
+ ```
2144
+
2145
+ The tests are in `test/exposure-build-up.test.ts`:
2146
+
2147
+ - The last fraction equals `simulateCapture` byte for byte: tabulated and lean paths, fixed pattern, dual gain, auto white
2148
+ balance and Auto ISO, camera rotation, rolling shutter, a moving subject and mixed lighting.
2149
+ - Each fraction's mean and variance match a single capture at f·t. Under motion this is checked column by column, and the
2150
+ fraction is shown to differ from the whole exposure scaled by f.
2151
+ - Without read noise the codes never decrease, and Cov(c₁, c₂) = Var(c₁).
2152
+ - Dark frames' read noise is independent between readouts.
2153
+ - A fraction past the full well clips.
2154
+
2155
+ `test/binomial-sample.test.ts` checks the binomial draws with chi-square tests of the probability mass function.
2156
+
2032
2157
  ### A Print JPEG written in strips
2033
2158
 
2034
2159
  At 1:1 (output size equal to the crop), a Print JPEG's rows can be coded on separate workers.
package/docs/USAGE.md CHANGED
@@ -7146,6 +7146,7 @@ console.log(png.length, same);
7146
7146
  See [Print planning](https://github.com/Photivra/photivra/blob/main/docs/PRINT_PLANNING.md) for a complete, strict packed example and the exact-ratio integer policy.
7147
7147
 
7148
7148
  - `calculatePrintPlan()` says whether the native sampling is enough, and gives a compatible minimum file raster.
7149
+ - `fit: { kind: "contain", paperOrientation }` (2.22.0) fits the whole image inside paper of another shape and reports the borders ([Contain fit](https://github.com/Photivra/photivra/blob/main/docs/PRINT_PLANNING.md#integer-policy-fit-and-sufficiency)).
7149
7150
  - `calculatePrintSizeLimit()` distinguishes finite, unbounded and unavailable sampling limits.
7150
7151
  - `calculatePrintSizeLimitWithBlurAndNoise()` (2.20.0) gives the largest print at a viewing distance that still looks sharp and clean, counting blur and noise as well as pixels, and names the limiting factor ([Print size limit with blur and noise](https://github.com/Photivra/photivra/blob/main/docs/PRINT_PLANNING.md#print-size-limit-with-blur-and-noise)). An approximation.
7151
7152
  - Print quality and delivery are not assessed.
@@ -7512,6 +7513,26 @@ console.log(calculateGeometricFocusBreathing({
7512
7513
  }).value.deltaDegrees);
7513
7514
  ```
7514
7515
 
7516
+ ## Sensor profiles (#554)
7517
+
7518
+ `SENSOR_PROFILES` lists eight representative sensors (full frame, APS-C, medium format and a large phone sensor; BSI,
7519
+ stacked and front-illuminated; 2010s and 2020s) whose values come from published specifications, values derived from them
7520
+ or labelled design values, with no maker or product named. `simulateCapture({ sensor: { profile: record } })` simulates one
7521
+ (the record is checked strictly; `resolveSensorProfile(id)` finds one by id):
7522
+ it replaces the body's ISO range, readout regimes, readout time and fixed-pattern noise, and the #555 characteristics
7523
+ override it one at a time. `resolveSensorProfile(id)` returns one profile; `SENSOR_PROFILE_ISO_ANCHOR` is the ISO anchor its base ISO is re-rated from. Values, sources and the ISO
7524
+ rating: [SENSOR_PROFILES.md](https://github.com/Photivra/photivra/blob/main/docs/SENSOR_PROFILES.md).
7525
+
7526
+ ```ts
7527
+ import { SENSOR_PROFILES, simulateCapture } from "@photivra/engine";
7528
+
7529
+ for (const p of SENSOR_PROFILES) {
7530
+ // fast: false (full quality, as the calculator asks) computes the mid-gray noise figure.
7531
+ const r = await simulateCapture({ scene: { preset: "uniform" }, fast: false, sensor: { profile: p }, exposure: { mode: "manual", iso: 3200, shutterSeconds: 1 / 4000 } });
7532
+ console.log(p.name, r.sensor.dynamicRangeStops.toFixed(1), r.sensor.midGrey?.snr.toFixed(1));
7533
+ }
7534
+ ```
7535
+
7515
7536
  ## Generic lens catalogue (#549)
7516
7537
 
7517
7538
  `GENERIC_LENS_CATALOGUE` lists the engine's 30 generic primes (17 professional, 13 consumer, "PV 50 1.2 Pro", "PV 50 1.8"; Photivra's
@@ -7524,7 +7545,9 @@ correction profile for `resolveLensCorrectionPlan`, which `simulateCapture` appl
7524
7545
  `output.lensCorrections` (#570; never to the RAW or the DNG), and which a DNG can carry for RAW editors as optional DNG opcodes with
7525
7546
  `output.dng.lensCorrectionOpcodes` (#587; the RAW codes unchanged). `simulateCapture` renders a catalogue lens with `lens.catalogue`
7526
7547
  ([quick start](QUICK_START.md#generic-lens-catalogue-549)); `fast: true` (or `lens: { catalogue: { kind, fast: true } }`) takes its
7527
- fast method (#568), a labeled approximation that blends the PSF over a field grid, for interactive captures. Equations, tables and the
7548
+ fast method (#568), a labeled approximation that blends the PSF over a field grid, for interactive captures.
7549
+ `calculateLateralColorFringe({ lensId, activeImagingArea, pixelPitchMicrometers, fieldPosition? })` gives the lens's lateral color as a
7550
+ fringe width: the blue-minus-red image displacement in µm and pixels at a field position (default the corner; #580). Equations, tables and the
7528
7551
  chart measurements: [LENS_CATALOGUE.md](https://github.com/Photivra/photivra/blob/main/docs/LENS_CATALOGUE.md).
7529
7552
 
7530
7553
  ```ts
@@ -7790,6 +7813,13 @@ including the whole-image stages `output.diffractionCorrection` and `output.shar
7790
7813
  bytes as a whole-frame `simulateCapture()` and per-row progress (`diffraction-correction`, `sharpening`). See
7791
7814
  [Quick start, Developing a capture rendered in parts](QUICK_START.md#developing-a-capture-rendered-in-parts-571).
7792
7815
 
7816
+ `simulateExposureBuildUp(options, { fractions })` (#534) renders fractions of one exposure as one accumulating process. Each
7817
+ fraction's expected signal is its own capture at the elapsed time, so camera rotation, a moving subject and mixed lighting are
7818
+ included. Its photo and dark counts are binomial splits of the next fraction's, so they build up in place. Each fraction is
7819
+ clipped at the full well, read out once with its own read noise and developed as its own photo. The fraction at 1 is
7820
+ `simulateCapture(options)` byte for byte. Only the sRGB sample formats are accepted (on screen). See
7821
+ [Quick start, Exposure build-up](QUICK_START.md#exposure-build-up-534).
7822
+
7793
7823
  ### Color charts (2.14)
7794
7824
 
7795
7825
  The engine's own [color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md) are reflectance spectra lit by the capture's illuminant. Draw the
@@ -7810,6 +7840,33 @@ const sky = getColorPatchSet().patches.find((p) => p.patchId === "sky-blue");
7810
7840
  console.log(tungsten.scene.parameters.patchSet, sky?.code, sky?.name, sky?.reference.cielabD65); // "PV-M1", "Midday Sky"
7811
7841
  ```
7812
7842
 
7843
+ ### A photo as a print (2.21)
7844
+
7845
+ An [image-plane scene](https://github.com/Photivra/photivra/blob/main/docs/IMAGE_PLANE.md) (#581) puts an sRGB (8- or 16-bit) or linear image in front of the camera as a flat matte
7846
+ print. Each pixel becomes a smooth reflectance spectrum in [0, 1] that gives back its sRGB under CIE D65. Under other lights the print's colors are an approximation.
7847
+ `chartSize` sets the print's size (its height follows the image). Without it the print is fitted inside the frame. `prepareDisplayView` renders it on WebGPU in the Speed path.
7848
+ `prepareImagePlaneScenes()` loads the scene's module first, so bundles that never print a photo do not carry it.
7849
+
7850
+ ```ts
7851
+ import { prepareImagePlaneScenes, simulateCapture } from "@photivra/engine";
7852
+
7853
+ await prepareImagePlaneScenes(); // loads the scene's module (a lazy chunk) once
7854
+ const width = 2, height = 1, data = Uint8Array.of(255, 255, 255, 30, 60, 200);
7855
+ const print = await simulateCapture({
7856
+ scene: { kind: "image-plane", sceneId: "snapshot", image: { width, height, channels: 3, encoding: "srgb-8", data }, paper: { whiteReflectance: 0.9 } },
7857
+ chartSize: { widthM: 0.1 },
7858
+ subjectDistanceM: 0.4,
7859
+ output: { width: 32, height: 16 }
7860
+ });
7861
+ console.log(print.scene.parameters.encoding, print.assumptions.some((a) => a.id === "scene.imagePlane")); // "srgb-8", true
7862
+ ```
7863
+
7864
+ **Representative light sources (2.22).** The engine's own [representative light sources](https://github.com/Photivra/photivra/blob/main/docs/LIGHT_SOURCES.md) are four relative spectra
7865
+ designed by Photivra: a tri-phosphor fluorescent tube and white LEDs of low and high color rendering at 4000 K and a warm one at
7866
+ 2700 K, each "representative, not a named product". `createRepresentativeLightSourceIlluminant({ sourceId })` returns an
7867
+ `illuminant` (`{ kind: "spectrum", spectrum, label }`); under the two 4000 K LEDs the color patches differ by up to ΔE00 5.9 while
7868
+ the neutrals stay neutral. Use `sensor.spectralStepNanometers: 5`. The page has a runnable example.
7869
+
7813
7870
  In `aperture-priority` mode, the shutter comes from the meter (CIPA DC-004 relations S = 10/H and H = 0.65·L·t/N²) through
7814
7871
  `resolveAperturePriorityExposureMode()`.
7815
7872
  `exposure.standardExposureDeviationStops` is the exposure used minus the standard exposure for the metered luminance L, in stops,
@@ -7866,6 +7923,28 @@ The chart presets are also available on their own:
7866
7923
  - `createPlanarSceneRadianceProvider()` turns a chart into an `evaluateRadiance` callback for the native
7867
7924
  environment RAW contract.
7868
7925
 
7926
+ ### Mixed lighting (2.22, #550)
7927
+
7928
+ `illumination` lights the chart with two or more sources blended across it (a linear gradient, a radial falloff or regions),
7929
+ for example window daylight on one side and a lamp on the other. The frame center's light sets the grey-card meter and its
7930
+ strongest source the `"scene-illuminant"` white balance; both are reported in `assumptions`. See
7931
+ [Mixed lighting](https://github.com/Photivra/photivra/blob/main/docs/MIXED_ILLUMINATION.md) for the model and every field.
7932
+
7933
+ ```ts
7934
+ import { simulateCapture } from "@photivra/engine";
7935
+
7936
+ const lampPool = await simulateCapture({
7937
+ scene: { preset: "step-wedge" },
7938
+ exposure: { mode: "aperture-priority", metering: "grey-card" },
7939
+ illumination: {
7940
+ sources: [{ illuminant: "cie-a", illuminanceLux: 500 }, { illuminant: { kind: "planckian", temperatureKelvin: 6500 }, illuminanceLux: 20000 }],
7941
+ blend: { kind: "radial", radius: 0.8, stops: [0.25, 1] }
7942
+ },
7943
+ output: { width: 32, height: 16 }
7944
+ });
7945
+ console.log(lampPool.assumptions.find((a) => a.id === "output.whiteBalanceSource")?.value); // { source: 0, illuminant: "cie-a" }
7946
+ ```
7947
+
7869
7948
  ### Display view of a capture
7870
7949
 
7871
7950
  `simulateDisplayView(options, view)` takes a capture request and a display size and returns the real
@@ -8051,6 +8130,18 @@ const rolling = await simulateCapture({ scene: { preset: "checkerboard" }, motio
8051
8130
  console.log(rolling.request.siteInput(7 * 8).charge.completenessProfile.startOffsetSecondsFromOpeningReference);
8052
8131
  ```
8053
8132
 
8133
+ `motion.subject` (2.22.0) moves the subject instead: the whole scene, or one `region` of it over a still background,
8134
+ at `velocityMPerSec` on the scene plane, optionally rotating at `angularVelocityRadPerSec` about the optical axis. It
8135
+ uses the same temporal midpoints, so a rolling shutter skews the moving subject. The display view renders sideways
8136
+ motion under a global shutter and refuses the rest. See [Subject motion](QUICK_START.md#subject-motion).
8137
+
8138
+ ```ts
8139
+ import { simulateCapture } from "@photivra/engine";
8140
+
8141
+ const moving = await simulateCapture({ scene: { preset: "slanted-edge" }, motion: { subject: { velocityMPerSec: { x: 0.5 } } }, output: { width: 8, height: 8 } });
8142
+ console.log(moving.assumptions.find((a) => a.id === "motion.subject")?.value);
8143
+ ```
8144
+
8054
8145
  ## Equivalent f-number, sensor area and blur as a share of the frame
8055
8146
 
8056
8147
  `calculateSensorEquivalence()` says what a lens on one sensor corresponds to on a 36 × 24 mm frame (or on a `reference` you pass). Call it once per sensor to fill a lens table:
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
11
11
  own versions. Release checks confirm it matches package.json and the packed package.
12
12
 
13
13
  ```ts
14
- ENGINE_API_VERSION = "2.20.0" as const
14
+ ENGINE_API_VERSION = "2.21.0" as const
15
15
  ```
@@ -4,11 +4,11 @@ The package and root API versions are recorded once in [Navigation](../API_REFER
4
4
 
5
5
  ## GENERIC_LENS_CATALOGUE
6
6
 
7
- The 30 lenses: 17 professional, then 13 consumer, each set by focal length.
7
+ The 33 lenses: 17 professional, 13 consumer and (#554) 3 phone lenses, each set by focal length.
8
8
 
9
9
  ```ts
10
10
  GENERIC_LENS_CATALOGUE: readonly GenericLensCatalogueEntry[] = Object.freeze([...PRO.map((row) => entry("professional", row)),
11
- ...CONSUMER.map((row) => entry("consumer", row))])
11
+ ...CONSUMER.map((row) => entry("consumer", row)), ...PHONE.map((row) => entry("phone", row))])
12
12
  ```
13
13
 
14
14
  ## GENERIC_LENS_IDS
@@ -30,7 +30,12 @@ GENERIC_LENS_SET_BASELINES: Readonly<Record<GenericLensSet, Readonly<GenericLens
30
30
  breathingFraction: -0.03, focusScatterDepthOfFocusFraction: 0.1, closeFocusFactor: 0.15 }),
31
31
  consumer: Object.freeze({ sphericalWaves: 0.5, comaWaves: 1.0, astigmatismWaves: 0.8, fieldCurvatureWaves: 0.45, axialColorWaves: 0.4,
32
32
  lateralColorFraction: 0.0003, distortionMagnitude: 0.03, naturalVignettingStops: 0.7, opticalVignettingStops: 1.1, veilingGlareFraction: 0.02, ghostFraction: 0.008,
33
- breathingFraction: -0.08, focusScatterDepthOfFocusFraction: 0.3, closeFocusFactor: 0.5 })
33
+ breathingFraction: -0.08, focusScatterDepthOfFocusFraction: 0.3, closeFocusFactor: 0.5 }),
34
+ // #554: design values (docs/LENS_CATALOGUE.md, "Phone lenses"): the consumer baseline with the stronger fall-off, flare and distortion
35
+ // (corrected in software on phones) of small fixed-aperture phone lenses, and no optical-vignetting change (the aperture is fixed).
36
+ phone: Object.freeze({ sphericalWaves: 0.5, comaWaves: 1.0, astigmatismWaves: 0.8, fieldCurvatureWaves: 0.5, axialColorWaves: 0.3,
37
+ lateralColorFraction: 0.0004, distortionMagnitude: 0.03, naturalVignettingStops: 1.2, opticalVignettingStops: 0.5, veilingGlareFraction: 0.03, ghostFraction: 0.01,
38
+ breathingFraction: -0.05, focusScatterDepthOfFocusFraction: 0.3, closeFocusFactor: 0.5 })
34
39
  })
35
40
  ```
36
41
 
@@ -179,10 +184,10 @@ export type GenericLensKind = "standard" | "macro" | "fisheye";
179
184
 
180
185
  ## GenericLensSet
181
186
 
182
- The exact contract is declared below; use the domain guide for assumptions and staged integration.
187
+ #554: "phone", the three fixed lenses of a phone sensor profile (`sensor.profile` with `fixedLenses: "phone"`).
183
188
 
184
189
  ```ts
185
- export type GenericLensSet = "professional" | "consumer";
190
+ export type GenericLensSet = "professional" | "consumer" | "phone";
186
191
  ```
187
192
 
188
193
  ## ResolvedGenericLens
@@ -0,0 +1,64 @@
1
+ # equipment/sensor-profile-body.ts public contracts
2
+
3
+ The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
+
5
+ ## SENSOR_PROFILE_ISO_ANCHOR
6
+
7
+ The engine's ISO anchor (see the module comment): white charge 60 000 e⁻ at ISO 100, 36 µm², QE 0.5.
8
+
9
+ ```ts
10
+ SENSOR_PROFILE_ISO_ANCHOR = Object.freeze({ iso: 100, fullWellElectrons: 60000, pixelAreaSquareMicrometers: 36, peakQuantumEfficiency: .5 })
11
+ ```
12
+
13
+ ## SensorProfile
14
+
15
+ One sensor profile. Every number is per photosite unless stated; see `provenance` for each value's kind.
16
+
17
+ ```ts
18
+ export interface SensorProfile {
19
+ id: string;
20
+ /** Format, resolution, architecture and era; no maker or product name. */
21
+ name: string;
22
+ /** A market category, not a specification: it is a label and changes nothing. */
23
+ marketTag: "consumer" | "professional";
24
+ architecture: "front-illuminated" | "back-illuminated" | "stacked";
25
+ sensorWidthMm: number; sensorHeightMm: number;
26
+ pixelsWide: number; pixelsHigh: number;
27
+ pixelPitchMicrometers: number;
28
+ /** The green channel's peak EQE (#555 `peakQuantumEfficiency`). */
29
+ peakQuantumEfficiency: number;
30
+ /** Full-well capacity, e⁻: the charge at the RAW white level at base ISO. */
31
+ fullWellElectrons: number;
32
+ /** Read noise, RMS e⁻; `highGainFromIso` null for one conversion gain throughout. */
33
+ readNoise: { lowGainElectrons: number; highGainElectrons: number; highGainFromIso: number | null };
34
+ /** Charge storage limit of the high conversion gain, e⁻ (null without one): full well × base ISO / switch ISO. */
35
+ highGainFullWellElectrons: number | null;
36
+ adcBitDepth: 12 | 14 | 16;
37
+ /** Re-rated base ISO (see the module comment) and the highest ISO setting. */
38
+ baseIso: number; maximumIso: number;
39
+ /** Largest analog gain relative to base ISO; above it the gain is digital. */
40
+ maximumAnalogGain: number;
41
+ /** Rolling readout, s: the top-to-bottom skew with `motion.shutter: "rolling"`. */
42
+ readoutSeconds: number;
43
+ /** Fixed-pattern noise used with `sensor.fixedPatternNoise` (EMVA 1288 Release 4.0 non-uniformities). */
44
+ fixedPattern: { prnu: number; dsnu: number; hotPixelFraction: number; hotPixelMultiplier: number; rowElectrons: number; columnElectrons: number };
45
+ /** What the profile is good for (text only). */
46
+ goodFor: readonly string[];
47
+ description: string;
48
+ /** "phone": the profile takes only the catalogue's phone lenses (`lens.catalogue`), which need it. */
49
+ fixedLenses: "phone" | null;
50
+ /**
51
+ * Each value's kind and source key (S1–S10, docs/SENSOR_PROFILES.md "Sources"); a value not listed is a design value.
52
+ * For example `fullWellElectrons: "published S1"` or `baseIso: "derived"`.
53
+ */
54
+ provenance: Readonly<Partial<Record<keyof SensorProfile | "sensorSize" | "pixels" | "highGainFromIso", string>>>;
55
+ }
56
+ ```
57
+
58
+ ## SensorProfileValueKind
59
+
60
+ How a profile value was obtained: copied from a source, calculated from sourced values, or Photivra's design value.
61
+
62
+ ```ts
63
+ export type SensorProfileValueKind = "published" | "derived" | "design";
64
+ ```