@photivra/engine 2.19.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 (242) hide show
  1. package/CHANGELOG.md +289 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +7 -5
  4. package/README.md +7 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +21 -10
  7. package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
  8. package/dist/capture/dng-lens-correction-opcodes.js +119 -0
  9. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
  10. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  11. package/dist/capture/exposure-build-up-stage.js +4 -0
  12. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  13. package/dist/capture/native-environment-lean.d.ts +2 -1
  14. package/dist/capture/native-environment-lean.js +42 -78
  15. package/dist/capture/native-environment-lean.js.map +1 -1
  16. package/dist/capture/native-environment-tabulated.js +4 -3
  17. package/dist/capture/native-environment-tabulated.js.map +1 -1
  18. package/dist/capture/native-raw-dng.d.ts +6 -0
  19. package/dist/capture/native-raw-dng.js +16 -2
  20. package/dist/capture/native-raw-dng.js.map +1 -1
  21. package/dist/capture/native-raw.d.ts +3 -2
  22. package/dist/capture/native-raw.js +5 -4
  23. package/dist/capture/native-raw.js.map +1 -1
  24. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  25. package/dist/capture/sensor-raw-producer.js +15 -8
  26. package/dist/capture/sensor-raw-producer.js.map +1 -1
  27. package/dist/core/shared-helpers.d.ts +2 -0
  28. package/dist/core/shared-helpers.js +4 -0
  29. package/dist/core/shared-helpers.js.map +1 -1
  30. package/dist/core/version.d.ts +1 -1
  31. package/dist/core/version.js +1 -1
  32. package/dist/core/version.js.map +1 -1
  33. package/dist/equipment/generic-lens-catalogue.d.ts +11 -4
  34. package/dist/equipment/generic-lens-catalogue.js +49 -7
  35. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  36. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  37. package/dist/equipment/generic-tier-assets.js +18 -14
  38. package/dist/equipment/generic-tier-assets.js.map +1 -1
  39. package/dist/equipment/generic-tier-presets.js +2 -3
  40. package/dist/equipment/generic-tier-presets.js.map +1 -1
  41. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  42. package/dist/equipment/sensor-profile-body.js +77 -0
  43. package/dist/equipment/sensor-profile-body.js.map +1 -0
  44. package/dist/equipment/sensor-profiles.d.ts +8 -0
  45. package/dist/equipment/sensor-profiles.js +82 -0
  46. package/dist/equipment/sensor-profiles.js.map +1 -0
  47. package/dist/facade/catalogue-lens-signal.d.ts +2 -2
  48. package/dist/facade/catalogue-lens-signal.js +58 -71
  49. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  50. package/dist/facade/chart-radiance-batch.js +2 -1
  51. package/dist/facade/chart-radiance-batch.js.map +1 -1
  52. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  53. package/dist/facade/defocus-image-space-provider.js +64 -76
  54. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  55. package/dist/facade/display-view-gpu-session.js +2 -0
  56. package/dist/facade/display-view-gpu-session.js.map +1 -1
  57. package/dist/facade/display-view-gpu.d.ts +13 -5
  58. package/dist/facade/display-view-gpu.js +4 -4
  59. package/dist/facade/display-view-gpu.js.map +1 -1
  60. package/dist/facade/display-view-lens-signal.d.ts +1 -1
  61. package/dist/facade/display-view-lens-signal.js +30 -26
  62. package/dist/facade/display-view-lens-signal.js.map +1 -1
  63. package/dist/facade/display-view.d.ts +3 -1
  64. package/dist/facade/display-view.js +6 -24
  65. package/dist/facade/display-view.js.map +1 -1
  66. package/dist/facade/expected-signal-setup.d.ts +3 -1
  67. package/dist/facade/expected-signal-setup.js +1 -1
  68. package/dist/facade/expected-signal-setup.js.map +1 -1
  69. package/dist/facade/expected-signal.d.ts +47 -14
  70. package/dist/facade/expected-signal.js +117 -112
  71. package/dist/facade/expected-signal.js.map +1 -1
  72. package/dist/facade/exposure-build-up.d.ts +70 -0
  73. package/dist/facade/exposure-build-up.js +164 -0
  74. package/dist/facade/exposure-build-up.js.map +1 -0
  75. package/dist/facade/fast-methods.d.ts +50 -0
  76. package/dist/facade/fast-methods.js +58 -0
  77. package/dist/facade/fast-methods.js.map +1 -0
  78. package/dist/facade/fast-resolution.d.ts +12 -0
  79. package/dist/facade/fast-resolution.js +18 -0
  80. package/dist/facade/fast-resolution.js.map +1 -0
  81. package/dist/facade/generic-lens-capture.d.ts +5 -4
  82. package/dist/facade/generic-lens-capture.js +5 -4
  83. package/dist/facade/generic-lens-capture.js.map +1 -1
  84. package/dist/facade/image-plane-enable.d.ts +7 -0
  85. package/dist/facade/image-plane-enable.js +17 -0
  86. package/dist/facade/image-plane-enable.js.map +1 -0
  87. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  88. package/dist/facade/image-plane-lattice-data.js +10 -0
  89. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  90. package/dist/facade/image-plane-support.d.ts +33 -0
  91. package/dist/facade/image-plane-support.js +11 -0
  92. package/dist/facade/image-plane-support.js.map +1 -0
  93. package/dist/facade/image-plane.d.ts +36 -0
  94. package/dist/facade/image-plane.js +353 -0
  95. package/dist/facade/image-plane.js.map +1 -0
  96. package/dist/facade/mixed-illumination.d.ts +108 -0
  97. package/dist/facade/mixed-illumination.js +174 -0
  98. package/dist/facade/mixed-illumination.js.map +1 -0
  99. package/dist/facade/representative-light-sources.d.ts +72 -0
  100. package/dist/facade/representative-light-sources.js +124 -0
  101. package/dist/facade/representative-light-sources.js.map +1 -0
  102. package/dist/facade/scene-radiance.d.ts +129 -4
  103. package/dist/facade/scene-radiance.js +48 -13
  104. package/dist/facade/scene-radiance.js.map +1 -1
  105. package/dist/facade/simulate-capture.d.ts +88 -19
  106. package/dist/facade/simulate-capture.js +374 -145
  107. package/dist/facade/simulate-capture.js.map +1 -1
  108. package/dist/facade/simulate-display-view.d.ts +3 -2
  109. package/dist/facade/simulate-display-view.js +36 -18
  110. package/dist/facade/simulate-display-view.js.map +1 -1
  111. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  112. package/dist/gpu/chart-composite-gpu.js +48 -9
  113. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  114. package/dist/gpu/chart-radiance-gpu.js +4 -30
  115. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  116. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  117. package/dist/gpu/display-view-composite-material-module.js +39 -13
  118. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  119. package/dist/gpu/display-view-image-material-module.d.ts +86 -0
  120. package/dist/gpu/display-view-image-material-module.js +339 -0
  121. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  122. package/dist/gpu/display-view-image-quality.d.ts +45 -0
  123. package/dist/gpu/display-view-image-quality.js +380 -0
  124. package/dist/gpu/display-view-image-quality.js.map +1 -0
  125. package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
  126. package/dist/gpu/display-view-quality-gpu.js +136 -205
  127. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  128. package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
  129. package/dist/gpu/display-view-speed-gpu.js +131 -93
  130. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  131. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
  132. package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
  133. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  134. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
  135. package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
  136. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  137. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  138. package/dist/gpu/wgsl-exact-sum.js +42 -0
  139. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  140. package/dist/index.d.ts +18 -3
  141. package/dist/index.js +12 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/motion/subject-motion.d.ts +66 -0
  144. package/dist/motion/subject-motion.js +52 -0
  145. package/dist/motion/subject-motion.js.map +1 -0
  146. package/dist/optics/aperture-diameter.d.ts +47 -0
  147. package/dist/optics/aperture-diameter.js +50 -0
  148. package/dist/optics/aperture-diameter.js.map +1 -0
  149. package/dist/optics/detail-limit.d.ts +182 -0
  150. package/dist/optics/detail-limit.js +336 -0
  151. package/dist/optics/detail-limit.js.map +1 -0
  152. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  153. package/dist/optics/generic-lens-lateral-color.js +77 -0
  154. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  155. package/dist/optics/generic-lens-psf.d.ts +18 -2
  156. package/dist/optics/generic-lens-psf.js +79 -48
  157. package/dist/optics/generic-lens-psf.js.map +1 -1
  158. package/dist/optics/lens-psf-profile.d.ts +7 -0
  159. package/dist/optics/lens-psf-profile.js +2 -1
  160. package/dist/optics/lens-psf-profile.js.map +1 -1
  161. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  162. package/dist/optics/scene-to-sensor-irradiance.js +7 -0
  163. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  164. package/dist/output/crop-to-match-focal-length.d.ts +62 -0
  165. package/dist/output/crop-to-match-focal-length.js +63 -0
  166. package/dist/output/crop-to-match-focal-length.js.map +1 -0
  167. package/dist/output/developed-lens-corrections.d.ts +19 -5
  168. package/dist/output/developed-lens-corrections.js +30 -11
  169. package/dist/output/developed-lens-corrections.js.map +1 -1
  170. package/dist/output/generic-lens-corrections.d.ts +41 -3
  171. package/dist/output/generic-lens-corrections.js +57 -9
  172. package/dist/output/generic-lens-corrections.js.map +1 -1
  173. package/dist/output/lens-corrections.d.ts +12 -2
  174. package/dist/output/lens-corrections.js +22 -7
  175. package/dist/output/lens-corrections.js.map +1 -1
  176. package/dist/output/print-contrast-reference.js +1 -1
  177. package/dist/output/print-contrast-reference.js.map +1 -1
  178. package/dist/output/print-plan.d.ts +41 -5
  179. package/dist/output/print-plan.js +59 -14
  180. package/dist/output/print-plan.js.map +1 -1
  181. package/dist/output/print-size-blur-noise.d.ts +110 -0
  182. package/dist/output/print-size-blur-noise.js +138 -0
  183. package/dist/output/print-size-blur-noise.js.map +1 -0
  184. package/dist/sensor/binomial-sample.d.ts +3 -0
  185. package/dist/sensor/binomial-sample.js +86 -0
  186. package/dist/sensor/binomial-sample.js.map +1 -0
  187. package/dist/sensor/eqe-electron-rate.js +1 -7
  188. package/dist/sensor/eqe-electron-rate.js.map +1 -1
  189. package/dist/sensor/exposure-window.d.ts +2 -0
  190. package/dist/sensor/exposure-window.js +2 -2
  191. package/dist/sensor/exposure-window.js.map +1 -1
  192. package/dist/sensor/readout-timing.js +2 -23
  193. package/dist/sensor/readout-timing.js.map +1 -1
  194. package/dist/sensor/response-application-compatibility.js +1 -6
  195. package/dist/sensor/response-application-compatibility.js.map +1 -1
  196. package/dist/sensor/responsivity-photocurrent.js +1 -7
  197. package/dist/sensor/responsivity-photocurrent.js.map +1 -1
  198. package/dist/sensor/sensor-characteristics.d.ts +147 -0
  199. package/dist/sensor/sensor-characteristics.js +126 -0
  200. package/dist/sensor/sensor-characteristics.js.map +1 -0
  201. package/dist/sensor/spatial-sample-reduction.js +1 -5
  202. package/dist/sensor/spatial-sample-reduction.js.map +1 -1
  203. package/dist/sensor/spectral-quadrature.js +1 -6
  204. package/dist/sensor/spectral-quadrature.js.map +1 -1
  205. package/dist/sensor/temporal-photo-signal.d.ts +3 -0
  206. package/dist/sensor/temporal-photo-signal.js +5 -2
  207. package/dist/sensor/temporal-photo-signal.js.map +1 -1
  208. package/dist/stabilization/system.d.ts +9 -0
  209. package/dist/stabilization/system.js +5 -6
  210. package/dist/stabilization/system.js.map +1 -1
  211. package/docs/AGENT_GUIDE.md +5 -0
  212. package/docs/API_REFERENCE.md +173 -2
  213. package/docs/QUICK_START.md +213 -6
  214. package/docs/USAGE.md +157 -3
  215. package/docs/api/capture-native-raw-dng.md +6 -0
  216. package/docs/api/core-version.md +1 -1
  217. package/docs/api/equipment-generic-lens-catalogue.md +17 -7
  218. package/docs/api/equipment-sensor-profile-body.md +64 -0
  219. package/docs/api/equipment-sensor-profiles.md +86 -0
  220. package/docs/api/exports.json +357 -2
  221. package/docs/api/facade-display-view-gpu.md +5 -2
  222. package/docs/api/facade-exposure-build-up.md +105 -0
  223. package/docs/api/facade-fast-methods.md +99 -0
  224. package/docs/api/facade-image-plane-enable.md +16 -0
  225. package/docs/api/facade-mixed-illumination.md +67 -0
  226. package/docs/api/facade-representative-light-sources.md +131 -0
  227. package/docs/api/facade-scene-radiance.md +54 -3
  228. package/docs/api/facade-simulate-capture.md +60 -15
  229. package/docs/api/facade-simulate-display-view.md +3 -2
  230. package/docs/api/motion-subject-motion.md +23 -0
  231. package/docs/api/optics-aperture-diameter.md +51 -0
  232. package/docs/api/optics-detail-limit.md +203 -0
  233. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  234. package/docs/api/output-crop-to-match-focal-length.md +67 -0
  235. package/docs/api/output-developed-lens-corrections.md +10 -1
  236. package/docs/api/output-generic-lens-corrections.md +40 -2
  237. package/docs/api/output-lens-corrections.md +11 -1
  238. package/docs/api/output-print-plan.md +35 -1
  239. package/docs/api/output-print-size-blur-noise.md +115 -0
  240. package/docs/api/sensor-sensor-characteristics.md +105 -0
  241. package/llms.txt +6 -0
  242. package/package.json +1 -1
@@ -0,0 +1,124 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Representative light sources (#557): four relative spectral power distributions designed by Photivra for indoor
4
+ * light presets, so that two lights at the same correlated color temperature (CCT) can render colors differently.
5
+ *
6
+ * Every spectrum is an engine-defined analytic function of wavelength, not measured or copied data: a constant plus a
7
+ * sum of Gaussian bands,
8
+ *
9
+ * S(λ) = b + Σₖ aₖ·exp(−½·((λ − μₖ)/σₖ)²),
10
+ *
11
+ * with λ in nanometers on 360–830 nm (the CIE 1931 observer's domain), in relative units (the capture facade scales an
12
+ * illuminant to its lux). Each source's band shapes were chosen by Photivra to represent a kind of lamp; two amplitudes
13
+ * (or one amplitude and one band center) were then solved so the chromaticity lies on the Planckian locus at the stated
14
+ * CCT, and rounded (amplitudes to 0.001, wavelengths to 0.1 nm). Those rounded values are the definition. The CCT and
15
+ * Duv of every rounded spectrum are checked with the engine's own `calculateCorrelatedColorTemperature` and
16
+ * `estimateCorrelatedColorTemperatureMcCamy` (docs/LIGHT_SOURCES.md, test/representative-light-sources.test.ts).
17
+ *
18
+ * These are representative constructions, not named products: no CIE, IES or manufacturer spectral data, names or
19
+ * tables are used. The bands are wider than the emission lines of real fluorescent phosphors so that the 5 nm sensor
20
+ * quadrature recommended for color work integrates them; real lamps of each kind vary widely.
21
+ *
22
+ * The exponentials use the portable `portableExp`, so a spectrum's values have the same bits in every JavaScript
23
+ * engine, in captures and in the display view alike.
24
+ *
25
+ * The module is not reached by `simulateCapture` itself: an application passes a spectrum as
26
+ * `illuminant: { kind: "spectrum", spectrum }` (or {@link createRepresentativeLightSourceIlluminant}), so bundles that
27
+ * do not use the sources do not contain them.
28
+ */
29
+ import { InvalidConfigurationError } from "../core/configuration-error.js";
30
+ import { InvalidScientificInputError } from "../core/validation.js";
31
+ import { requireInputKeys } from "../core/record-validation.js";
32
+ import { portableExp } from "../core/portable-math.js";
33
+ /** Identity of the built-in light-source set. */
34
+ export const REPRESENTATIVE_LIGHT_SOURCE_SET_ID = "photivra-representative-light-sources";
35
+ /** Version of the set's spectra; a change to any spectrum changes it. */
36
+ export const REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION = "1.0.0";
37
+ /** Domain of every spectrum, nm (the CIE 1931 observer's domain; the capture facade uses 380–780 nm of it). */
38
+ export const REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS = /* @__PURE__ */ Object.freeze({ minimum: 360, maximum: 830 });
39
+ /** The source ids, in set order. */
40
+ export const REPRESENTATIVE_LIGHT_SOURCE_IDS = /* @__PURE__ */ Object.freeze([
41
+ "tri-phosphor-fluorescent-4000k", "white-led-low-rendering-4000k", "white-led-high-rendering-4000k", "warm-white-led-2700k"
42
+ ]);
43
+ const LABEL = "representative, not a named product";
44
+ /* Photivra's design (docs/LIGHT_SOURCES.md, "The spectra"); the solved values were rounded and are the definition. */
45
+ const ROWS = [
46
+ ["tri-phosphor-fluorescent-4000k", "Tri-phosphor fluorescent, 4000 K", "fluorescent", 4000,
47
+ "Three narrow emission bands in the blue, green and red on a low continuum, the construction of a tri-phosphor fluorescent tube, which leaves gaps between the bands.",
48
+ 0.02, [["blue phosphor band", 0.285, 450, 6], ["green phosphor line", 1, 544, 3.5], ["red phosphor line", 1.02, 611, 3.5]]],
49
+ ["white-led-low-rendering-4000k", "White LED, low color rendering, 4000 K", "led", 4000,
50
+ "A blue pump peak and one broad yellow phosphor band, the simplest white LED, which is weak in the cyan and the deep red.",
51
+ 0, [["blue pump", 1.123, 450, 9], ["yellow phosphor", 1, 579.1, 50]]],
52
+ ["white-led-high-rendering-4000k", "White LED, high color rendering, 4000 K", "led", 4000,
53
+ "A blue pump with a green and a red phosphor band that fill the cyan and the red, a white LED built for high color rendering at the same CCT as the low-rendering one.",
54
+ 0, [["blue pump", 1.306, 450, 9], ["green phosphor", 1, 530, 35], ["red phosphor", 1.264, 630, 40]]],
55
+ ["warm-white-led-2700k", "Warm white LED, 2700 K", "led", 2700,
56
+ "The same pump and two-phosphor construction with more red phosphor and less pump, a warm white LED for home lighting.",
57
+ 0, [["blue pump", 0.795, 450, 9], ["green phosphor", 1, 540, 38], ["red phosphor", 2.133, 630, 40]]]
58
+ ];
59
+ const SET = /* @__PURE__ */ (() => Object.freeze({
60
+ setId: REPRESENTATIVE_LIGHT_SOURCE_SET_ID,
61
+ setVersion: REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION,
62
+ wavelengthRangeNanometers: REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS,
63
+ sources: Object.freeze(ROWS.map(([sourceId, name, family, cct, reason, base, bands]) => Object.freeze({
64
+ sourceId, name, family, nominalCorrelatedColorTemperatureKelvin: cct, label: LABEL, reason, base,
65
+ bands: Object.freeze(bands.map(([role, amplitude, centerNanometers, standardDeviationNanometers]) => Object.freeze({ kind: "gaussian", role, amplitude, centerNanometers, standardDeviationNanometers })))
66
+ })))
67
+ }))();
68
+ /**
69
+ * The built-in representative light sources: Photivra's own parametric spectra of a tri-phosphor fluorescent tube and
70
+ * three white LEDs (low and high color rendering at 4000 K, warm at 2700 K). A static, deeply frozen engine contract.
71
+ * Each is "representative, not a named product"; docs/LIGHT_SOURCES.md gives every formula and the reasons for it.
72
+ */
73
+ export function getRepresentativeLightSourceSet() {
74
+ return SET;
75
+ }
76
+ function findSource(input, functionName) {
77
+ requireInputKeys(input, functionName, ["sourceId"]);
78
+ const id = input.sourceId;
79
+ const source = typeof id === "string" ? SET.sources.find((s) => s.sourceId === id) : undefined;
80
+ if (!source)
81
+ throw new InvalidConfigurationError("sourceId must be one of the built-in representative light sources (REPRESENTATIVE_LIGHT_SOURCE_IDS).");
82
+ return source;
83
+ }
84
+ /** Most wavelengths whose values one spectrum keeps (a capture uses a few hundred). */
85
+ const CACHE_LIMIT = 4096;
86
+ /**
87
+ * A source's relative spectral power distribution S(λ) = b + Σ aₖ·exp(−½((λ − μₖ)/σₖ)²) as a function spectrum on
88
+ * 360–830 nm (relative units; values memoized per wavelength). Pass it to a capture as
89
+ * `illuminant: { kind: "spectrum", spectrum }`; for color work use `sensor.spectralStepNanometers: 5` or finer
90
+ * (docs/LIGHT_SOURCES.md, "Quadrature"). Throws `InvalidScientificInputError` outside 360–830 nm.
91
+ */
92
+ export function createRepresentativeLightSourceSpectrum(input) {
93
+ const source = findSource(input, "createRepresentativeLightSourceSpectrum");
94
+ const range = REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS, cache = new Map();
95
+ return {
96
+ kind: "function",
97
+ wavelengthRangeNanometers: { minimum: range.minimum, maximum: range.maximum },
98
+ evaluate(nm) {
99
+ const hit = cache.get(nm);
100
+ if (hit !== undefined)
101
+ return hit;
102
+ if (typeof nm !== "number" || !(nm >= range.minimum && nm <= range.maximum)) {
103
+ throw new InvalidScientificInputError("Representative light source " + source.sourceId + " is defined on 360–830 nm.");
104
+ }
105
+ let value = source.base;
106
+ for (const band of source.bands) {
107
+ const z = (nm - band.centerNanometers) / band.standardDeviationNanometers;
108
+ value += band.amplitude * portableExp(-0.5 * z * z);
109
+ }
110
+ if (cache.size < CACHE_LIMIT)
111
+ cache.set(nm, value);
112
+ return value;
113
+ }
114
+ };
115
+ }
116
+ /**
117
+ * A capture illuminant for a source: `{ kind: "spectrum", spectrum, label }` with the spectrum of
118
+ * {@link createRepresentativeLightSourceSpectrum} and the label "<name> (representative, not a named product)".
119
+ */
120
+ export function createRepresentativeLightSourceIlluminant(input) {
121
+ const source = findSource(input, "createRepresentativeLightSourceIlluminant");
122
+ return { kind: "spectrum", spectrum: createRepresentativeLightSourceSpectrum({ sourceId: source.sourceId }), label: source.name + " (" + LABEL + ")" };
123
+ }
124
+ //# sourceMappingURL=representative-light-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"representative-light-sources.js","sourceRoot":"","sources":["../../src/facade/representative-light-sources.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Representative light sources (#557): four relative spectral power distributions designed by Photivra for indoor\n * light presets, so that two lights at the same correlated color temperature (CCT) can render colors differently.\n *\n * Every spectrum is an engine-defined analytic function of wavelength, not measured or copied data: a constant plus a\n * sum of Gaussian bands,\n *\n * S(λ) = b + Σₖ aₖ·exp(−½·((λ − μₖ)/σₖ)²),\n *\n * with λ in nanometers on 360–830 nm (the CIE 1931 observer's domain), in relative units (the capture facade scales an\n * illuminant to its lux). Each source's band shapes were chosen by Photivra to represent a kind of lamp; two amplitudes\n * (or one amplitude and one band center) were then solved so the chromaticity lies on the Planckian locus at the stated\n * CCT, and rounded (amplitudes to 0.001, wavelengths to 0.1 nm). Those rounded values are the definition. The CCT and\n * Duv of every rounded spectrum are checked with the engine's own `calculateCorrelatedColorTemperature` and\n * `estimateCorrelatedColorTemperatureMcCamy` (docs/LIGHT_SOURCES.md, test/representative-light-sources.test.ts).\n *\n * These are representative constructions, not named products: no CIE, IES or manufacturer spectral data, names or\n * tables are used. The bands are wider than the emission lines of real fluorescent phosphors so that the 5 nm sensor\n * quadrature recommended for color work integrates them; real lamps of each kind vary widely.\n *\n * The exponentials use the portable `portableExp`, so a spectrum's values have the same bits in every JavaScript\n * engine, in captures and in the display view alike.\n *\n * The module is not reached by `simulateCapture` itself: an application passes a spectrum as\n * `illuminant: { kind: \"spectrum\", spectrum }` (or {@link createRepresentativeLightSourceIlluminant}), so bundles that\n * do not use the sources do not contain them.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { portableExp } from \"../core/portable-math.js\";\nimport type { ColorimetricSpectrum } from \"../color/color-matching-functions.js\";\nimport type { CaptureIlluminant } from \"./scene-radiance.js\";\n\n/** Identity of the built-in light-source set. */\nexport const REPRESENTATIVE_LIGHT_SOURCE_SET_ID = \"photivra-representative-light-sources\";\n/** Version of the set's spectra; a change to any spectrum changes it. */\nexport const REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION = \"1.0.0\";\n/** Domain of every spectrum, nm (the CIE 1931 observer's domain; the capture facade uses 380–780 nm of it). */\nexport const REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS = /* @__PURE__ */ Object.freeze({ minimum: 360, maximum: 830 });\n\n/** The source ids, in set order. */\nexport const REPRESENTATIVE_LIGHT_SOURCE_IDS = /* @__PURE__ */ Object.freeze([\n \"tri-phosphor-fluorescent-4000k\", \"white-led-low-rendering-4000k\", \"white-led-high-rendering-4000k\", \"warm-white-led-2700k\"\n] as const);\nexport type RepresentativeLightSourceId = (typeof REPRESENTATIVE_LIGHT_SOURCE_IDS)[number];\n\n/** One Gaussian band a·exp(−½((λ − μ)/σ)²) of a source spectrum. */\nexport interface RepresentativeLightSourceBand {\n kind: \"gaussian\";\n /** What the band represents, for example \"blue pump\" or \"green phosphor line\". */\n role: string;\n amplitude: number;\n centerNanometers: number;\n standardDeviationNanometers: number;\n}\n\n/** One built-in source: its id, name, design reason and spectrum definition. */\nexport interface RepresentativeLightSource {\n sourceId: RepresentativeLightSourceId;\n /** Photivra's descriptive name, for example \"White LED, high color rendering, 4000 K\". */\n name: string;\n family: \"fluorescent\" | \"led\";\n /** The CCT the spectrum was designed for, K (its computed CCT is within 1 K of it, Duv within 0.0001; docs/LIGHT_SOURCES.md). */\n nominalCorrelatedColorTemperatureKelvin: number;\n /** Always \"representative, not a named product\". */\n label: \"representative, not a named product\";\n /** One sentence: what the source represents and why it is built this way. */\n reason: string;\n /** The constant b (a low continuum), relative units. */\n base: number;\n bands: readonly RepresentativeLightSourceBand[];\n}\n\nexport interface RepresentativeLightSourceSet {\n setId: typeof REPRESENTATIVE_LIGHT_SOURCE_SET_ID;\n setVersion: typeof REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION;\n wavelengthRangeNanometers: { readonly minimum: number; readonly maximum: number };\n sources: readonly RepresentativeLightSource[];\n}\n\nconst LABEL = \"representative, not a named product\" as const;\ntype BandRow = readonly [role: string, amplitude: number, centerNanometers: number, standardDeviationNanometers: number];\n/* Photivra's design (docs/LIGHT_SOURCES.md, \"The spectra\"); the solved values were rounded and are the definition. */\nconst ROWS: readonly (readonly [RepresentativeLightSourceId, string, \"fluorescent\" | \"led\", number, string, number, readonly BandRow[]])[] = [\n [\"tri-phosphor-fluorescent-4000k\", \"Tri-phosphor fluorescent, 4000 K\", \"fluorescent\", 4000,\n \"Three narrow emission bands in the blue, green and red on a low continuum, the construction of a tri-phosphor fluorescent tube, which leaves gaps between the bands.\",\n 0.02, [[\"blue phosphor band\", 0.285, 450, 6], [\"green phosphor line\", 1, 544, 3.5], [\"red phosphor line\", 1.02, 611, 3.5]]],\n [\"white-led-low-rendering-4000k\", \"White LED, low color rendering, 4000 K\", \"led\", 4000,\n \"A blue pump peak and one broad yellow phosphor band, the simplest white LED, which is weak in the cyan and the deep red.\",\n 0, [[\"blue pump\", 1.123, 450, 9], [\"yellow phosphor\", 1, 579.1, 50]]],\n [\"white-led-high-rendering-4000k\", \"White LED, high color rendering, 4000 K\", \"led\", 4000,\n \"A blue pump with a green and a red phosphor band that fill the cyan and the red, a white LED built for high color rendering at the same CCT as the low-rendering one.\",\n 0, [[\"blue pump\", 1.306, 450, 9], [\"green phosphor\", 1, 530, 35], [\"red phosphor\", 1.264, 630, 40]]],\n [\"warm-white-led-2700k\", \"Warm white LED, 2700 K\", \"led\", 2700,\n \"The same pump and two-phosphor construction with more red phosphor and less pump, a warm white LED for home lighting.\",\n 0, [[\"blue pump\", 0.795, 450, 9], [\"green phosphor\", 1, 540, 38], [\"red phosphor\", 2.133, 630, 40]]]\n];\n\nconst SET: RepresentativeLightSourceSet = /* @__PURE__ */ ((): RepresentativeLightSourceSet => Object.freeze({\n setId: REPRESENTATIVE_LIGHT_SOURCE_SET_ID,\n setVersion: REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION,\n wavelengthRangeNanometers: REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS,\n sources: Object.freeze(ROWS.map(([sourceId, name, family, cct, reason, base, bands]) => Object.freeze({\n sourceId, name, family, nominalCorrelatedColorTemperatureKelvin: cct, label: LABEL, reason, base,\n bands: Object.freeze(bands.map(([role, amplitude, centerNanometers, standardDeviationNanometers]) =>\n Object.freeze({ kind: \"gaussian\" as const, role, amplitude, centerNanometers, standardDeviationNanometers })))\n })))\n}))();\n\n/**\n * The built-in representative light sources: Photivra's own parametric spectra of a tri-phosphor fluorescent tube and\n * three white LEDs (low and high color rendering at 4000 K, warm at 2700 K). A static, deeply frozen engine contract.\n * Each is \"representative, not a named product\"; docs/LIGHT_SOURCES.md gives every formula and the reasons for it.\n */\nexport function getRepresentativeLightSourceSet(): RepresentativeLightSourceSet {\n return SET;\n}\n\nfunction findSource(input: unknown, functionName: string): RepresentativeLightSource {\n requireInputKeys(input, functionName, [\"sourceId\"]);\n const id = (input as { sourceId?: unknown }).sourceId;\n const source = typeof id === \"string\" ? SET.sources.find((s) => s.sourceId === id) : undefined;\n if (!source) throw new InvalidConfigurationError(\"sourceId must be one of the built-in representative light sources (REPRESENTATIVE_LIGHT_SOURCE_IDS).\");\n return source;\n}\n\n/** Most wavelengths whose values one spectrum keeps (a capture uses a few hundred). */\nconst CACHE_LIMIT = 4096;\n\n/**\n * A source's relative spectral power distribution S(λ) = b + Σ aₖ·exp(−½((λ − μₖ)/σₖ)²) as a function spectrum on\n * 360–830 nm (relative units; values memoized per wavelength). Pass it to a capture as\n * `illuminant: { kind: \"spectrum\", spectrum }`; for color work use `sensor.spectralStepNanometers: 5` or finer\n * (docs/LIGHT_SOURCES.md, \"Quadrature\"). Throws `InvalidScientificInputError` outside 360–830 nm.\n */\nexport function createRepresentativeLightSourceSpectrum(input: { sourceId: RepresentativeLightSourceId }): ColorimetricSpectrum {\n const source = findSource(input, \"createRepresentativeLightSourceSpectrum\");\n const range = REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS, cache = new Map<number, number>();\n return {\n kind: \"function\",\n wavelengthRangeNanometers: { minimum: range.minimum, maximum: range.maximum },\n evaluate(nm: number): number {\n const hit = cache.get(nm);\n if (hit !== undefined) return hit;\n if (typeof nm !== \"number\" || !(nm >= range.minimum && nm <= range.maximum)) {\n throw new InvalidScientificInputError(\"Representative light source \" + source.sourceId + \" is defined on 360–830 nm.\");\n }\n let value = source.base;\n for (const band of source.bands) {\n const z = (nm - band.centerNanometers) / band.standardDeviationNanometers;\n value += band.amplitude * portableExp(-0.5 * z * z);\n }\n if (cache.size < CACHE_LIMIT) cache.set(nm, value);\n return value;\n }\n };\n}\n\n/**\n * A capture illuminant for a source: `{ kind: \"spectrum\", spectrum, label }` with the spectrum of\n * {@link createRepresentativeLightSourceSpectrum} and the label \"<name> (representative, not a named product)\".\n */\nexport function createRepresentativeLightSourceIlluminant(input: { sourceId: RepresentativeLightSourceId }): Extract<CaptureIlluminant, { kind: \"spectrum\" }> {\n const source = findSource(input, \"createRepresentativeLightSourceIlluminant\");\n return { kind: \"spectrum\", spectrum: createRepresentativeLightSourceSpectrum({ sourceId: source.sourceId }), label: source.name + \" (\" + LABEL + \")\" };\n}\n"]}
@@ -101,6 +101,115 @@ export interface CallerMaterialScene {
101
101
  }
102
102
  /** Most materials a {@link CallerMaterialScene} may list. */
103
103
  export declare const CALLER_MATERIAL_SCENE_MAX_MATERIALS = 65536;
104
+ /**
105
+ * 2.21 (#581): an image the caller supplies, for an {@link ImagePlaneScene}. `data` holds `width × height` pixels row by row
106
+ * from the top, `channels` (3: RGB, 4: RGBA) samples each, in the encoding's type: `srgb-8` Uint8Array or Uint8ClampedArray
107
+ * (canvas `ImageData.data`), `srgb-16` Uint16Array, `linear-srgb` Float32Array. sRGB primaries and white (IEC 61966-2-1) in
108
+ * every encoding; `srgb-8` and `srgb-16` are decoded with the sRGB transfer function, `linear-srgb` is already linear and is
109
+ * clipped to [0, 1] (the clipped samples are counted). Alpha is linear coverage over the paper. The array is read, never
110
+ * changed or kept beyond the call.
111
+ */
112
+ export interface ImagePlaneImage {
113
+ width: number;
114
+ height: number;
115
+ channels: 3 | 4;
116
+ encoding: "srgb-8" | "srgb-16" | "linear-srgb";
117
+ data: Uint8Array | Uint8ClampedArray | Uint16Array | Float32Array;
118
+ }
119
+ /**
120
+ * 2.21 (#581): a photo as a flat matte print on the scene plane, lit by the request's illuminant like a chart. Each pixel's
121
+ * color becomes a reflectance spectrum through the engine's print reflectance lattice (docs/IMAGE_PLANE.md): an
122
+ * approximation, a print of an image that was already processed, clipped and blurred by the camera that took it. The print's
123
+ * physical size is `chartSize` (its height defaults to the image's aspect); without it the print is fitted inside the frame,
124
+ * centered, on the 18 % surround.
125
+ */
126
+ export interface ImagePlaneScene {
127
+ kind: "image-plane";
128
+ sceneId: string;
129
+ image: ImagePlaneImage;
130
+ /** The paper and the deepest ink: reflectances of sRGB white and black, 0 ≤ black < white ≤ 1. Defaults 0.90 and 0.01. */
131
+ paper?: {
132
+ whiteReflectance?: number;
133
+ blackReflectance?: number;
134
+ };
135
+ /** How the print is reconstructed between pixel centers. Default `bilinear` (the pixels' spectra, bilinearly weighted). */
136
+ interpolation?: "bilinear" | "nearest";
137
+ /** Opt-in raised pixel limit: an integer up to the full-resolution ceiling. Default 24 000 000 pixels. */
138
+ maximumPixels?: number;
139
+ }
140
+ /**
141
+ * Internal (#581): what a pixel's signals are integrated from (`ResolvedImagePlaneScene.pixelSignals`): the lattice's node spectra at
142
+ * the N spectral nodes (node k's reflectance at λ_n at k·N + n), the white radiance and the channel responses per node.
143
+ */
144
+ export interface ImagePlaneSpectralNodes {
145
+ count: number;
146
+ nodeSpectra: Float64Array;
147
+ whiteRadiance: Float64Array;
148
+ response: readonly Float64Array[];
149
+ }
150
+ /**
151
+ * Internal (#581): an image-plane print as the tabulated expected signal's scene (`ResolvedImagePlaneScene.signalScene`): its radiance
152
+ * per node, the direct path's signal (each pixel's Σ_n A_c(λ_n)·L_n integrated from its reconstructed spectrum, `pixelSignals`, then
153
+ * interpolated between pixels), and (for the GPU module) the scene, its spectral nodes and the surround's channel signals.
154
+ */
155
+ export interface ImagePlaneSignalScene {
156
+ kind: "radiance";
157
+ radianceAt(u: number, v: number, nm: number): number;
158
+ signalAt(u: number, v: number, c: number): number;
159
+ image: {
160
+ scene: ResolvedImagePlaneScene;
161
+ nodes: ImagePlaneSpectralNodes;
162
+ surround: readonly [number, number, number];
163
+ whiteRadiance: Float64Array;
164
+ surroundReflectance: number;
165
+ };
166
+ }
167
+ /**
168
+ * Internal (#581): a validated image-plane scene (src/facade/image-plane.ts, a lazily loaded module), placed on a print of
169
+ * aspect `aspect` (height / width). Print-local coordinates are the chart's: cu = ±1 at the print's left and right edges, cv up
170
+ * on the same scale, ±aspect at its top and bottom.
171
+ */
172
+ export interface ResolvedImagePlaneScene {
173
+ kind: "image-plane";
174
+ sceneId: string;
175
+ width: number;
176
+ height: number;
177
+ aspect: number;
178
+ channels: 3 | 4;
179
+ encoding: ImagePlaneImage["encoding"];
180
+ interpolation: "bilinear" | "nearest";
181
+ paperWhiteReflectance: number;
182
+ paperBlackReflectance: number;
183
+ maximumPixels: number;
184
+ /** `linear-srgb` samples (color and alpha) clipped to [0, 1]; 0 for the integer encodings. */
185
+ clippedSampleCount: number;
186
+ /** SHA-256 of the image data's bytes, hex. */
187
+ dataSha256: string;
188
+ /** The print's reflectance at a print-local point and wavelength (nm, 380–780). */
189
+ reflectanceAt(cu: number, cv: number, nm: number): number;
190
+ /**
191
+ * One pixel's three channel signals Σ_n A_c(λ_n)·ρ_n·W_n, integrated from its reconstructed spectrum ρ_n (its cell's node spectra
192
+ * blended, then the paper) at the time of use, into `out[at..at + 2]`. No per-channel values of the lattice nodes are formed.
193
+ */
194
+ pixelSignals(index: number, nodes: ImagePlaneSpectralNodes, out: Float64Array, at: number): void;
195
+ /** The print at a print-local point in node values (`nodeValues[k]` of node k, `neutral` of the flat reflectance 1). */
196
+ sampleAt(cu: number, cv: number, nodeValues: Float64Array, neutral: number): number;
197
+ /** One pixel (row-major index) in node values. */
198
+ pixelValue(index: number, nodeValues: Float64Array, neutral: number): number;
199
+ /** One pixel's lattice cell (its base node r·25 + g·5 + b, returned) and its fractions within the cell per channel (written to `out[0..2]`). */
200
+ pixelLattice(index: number, out: Float64Array): number;
201
+ /** Every lattice node's reflectance at one wavelength (nm, 380–780). */
202
+ nodeReflectances(nm: number): Float64Array;
203
+ /**
204
+ * The print placed on the plane (frame point → print-local cu = u·scale/half, cv = v·scale/half; outside |cu| ≤ 1, |cv| ≤ aspect the
205
+ * surround) as the expected signal's scene at these nodes, responses and white radiance (see `ImagePlaneSignalScene`).
206
+ */
207
+ signalScene(wavelengths: readonly number[], perNode: readonly Float64Array[], whiteRadiance: Float64Array, radianceAt: (u: number, v: number, nm: number) => number, scale: number, half: number, surroundReflectance: number): ImagePlaneSignalScene;
208
+ /** The scene's form in the facade's source digest (the image's SHA-256 and shape, the paper, the interpolation, the lattice version). */
209
+ digestForm: Readonly<Record<string, unknown>>;
210
+ /** The result's `scene.parameters`: the image, paper and interpolation, the clipped samples and the image's SHA-256. */
211
+ parameters: Readonly<Record<string, unknown>>;
212
+ }
104
213
  /** Internal: a validated material scene with its materials prepared (and the caller inputs, for the scene digest). */
105
214
  export interface ResolvedCallerMaterialScene {
106
215
  kind: "material-function";
@@ -110,7 +219,7 @@ export interface ResolvedCallerMaterialScene {
110
219
  materialAt(point: Readonly<SceneMaterialPoint>): number;
111
220
  }
112
221
  /** Internal: a validated caller scene. */
113
- export type ResolvedCallerScene = CallerReflectanceScene | CallerRadianceScene | ResolvedCallerMaterialScene;
222
+ export type ResolvedCallerScene = CallerReflectanceScene | CallerRadianceScene | ResolvedCallerMaterialScene | ResolvedImagePlaneScene;
114
223
  export interface PlanarSceneGeometry {
115
224
  distanceM: number;
116
225
  /** Metres per frame unit on the plane (half the framed width). */
@@ -131,8 +240,11 @@ export interface PlanarSceneRadianceProvider {
131
240
  providerProfileId: string;
132
241
  evaluateRadiance(request: Readonly<SceneRadianceEvaluationRequest>): SceneRadianceEvaluationResult;
133
242
  evaluateApertureRadiance(request: Readonly<SceneRadianceEvaluationRequest>, ray: Readonly<SensorApertureRay>): SceneRadianceEvaluationResult;
134
- /** Spectral radiance at a plane point (frame units), the same value the callbacks return. */
135
- radianceAt(u: number, v: number, wavelengthNanometers: number): number;
243
+ /**
244
+ * Spectral radiance at a plane point (frame units), the same value the callbacks return. #551: a moving subject's provider
245
+ * reads it at time `t` (seconds from the first opening, default 0); a still scene ignores it.
246
+ */
247
+ radianceAt(u: number, v: number, wavelengthNanometers: number, t?: number): number;
136
248
  /**
137
249
  * 2.3, chart scenes only: the chart material at a plane point (frame units), or `materials.length` for the
138
250
  * surround of a fixed-size chart; `materialRadiance` gives that material's spectral radiance, the same value
@@ -140,6 +252,12 @@ export interface PlanarSceneRadianceProvider {
140
252
  */
141
253
  chartMaterialAt?(u: number, v: number): number;
142
254
  materialRadiance?(material: number, wavelengthNanometers: number): number;
255
+ /**
256
+ * 2.21 (#581), image-plane scenes only: the tabulated expected signal's scene at these spectral nodes and channel responses, its
257
+ * radiance per node and, for the direct path, Σ_n A_c(λ_n)·L(λ_n) integrated from each pixel's reconstructed spectrum
258
+ * (`ResolvedImagePlaneScene.pixelSignals`, the surround's reflectance times the neutral signal outside the print).
259
+ */
260
+ imageSignalScene?(wavelengths: readonly number[], perNode: readonly Float64Array[]): ImagePlaneSignalScene;
143
261
  }
144
262
  /** Internal (#394): the material lookup of a provider built by {@link buildPlanarSceneRadianceProvider}, if it has one. */
145
263
  export declare function planarProviderMaterialOfDirection(provider: PlanarSceneRadianceProvider): ((direction: Readonly<{
@@ -171,6 +289,12 @@ export declare function buildPlanarSceneRadianceProvider(input: {
171
289
  * radiance instead of failing; the plane fills the forward hemisphere only. Omitted: such a query fails, as before.
172
290
  */
173
291
  zeroBeyondHorizon?: true;
292
+ /**
293
+ * #550, mixed illumination: the plane's spectral irradiance at a plane point, given in chart units (chart-local with a fixed-size
294
+ * chart, frame units without) and in metres from the optical axis, used instead of `irradianceScale`·S(λ). The provider then has no material access (the radiance at a point is
295
+ * no longer a function of its material alone), so every renderer reads `radianceAt`. A radiance-function scene is refused.
296
+ */
297
+ irradianceAt?: (u: number, v: number, xM: number, yM: number, wavelengthNanometers: number) => number;
174
298
  }): PlanarSceneRadianceProvider;
175
299
  /** 2.3: a chart's physical size on the scene plane (`simulateCapture` `chartSize`, `createPlanarSceneRadianceProvider` `chartSize`). */
176
300
  export interface ChartSizeOption {
@@ -199,7 +323,8 @@ export declare function resolveChartSizeOption(value: unknown, frameAspect: numb
199
323
  * provider `providerProfileId`.
200
324
  */
201
325
  export declare function createPlanarSceneRadianceProvider(input: {
202
- scene: ChartScene | ChartMaterialLayerScene | CallerReflectanceScene | CallerRadianceScene | CallerMaterialScene;
326
+ /** An `image-plane` scene (#581) is refused: it is taken by the facade's capture and display-view functions, which load its module. */
327
+ scene: ChartScene | ChartMaterialLayerScene | CallerReflectanceScene | CallerRadianceScene | CallerMaterialScene | ImagePlaneScene;
203
328
  illuminant: CaptureIlluminant;
204
329
  illuminanceLux: number;
205
330
  distanceM: number;
@@ -164,6 +164,7 @@ export function buildPlanarSceneRadianceProvider(input) {
164
164
  const evidence = input.evidence, limitations = input.limitations;
165
165
  const caller = "kind" in scene ? scene : undefined, chart = caller ? undefined : scene;
166
166
  const materialScene = caller?.kind === "material-function" ? caller : undefined, materials = chart ? chart.materials : materialScene?.materials;
167
+ const imageScene = caller?.kind === "image-plane" ? caller : undefined;
167
168
  const placed = geometry.chart, half = placed ? placed.widthM / 2 : 0, chartAspect = placed ? placed.heightM / placed.widthM : 0;
168
169
  const shapeAt = tabulatedSpectrum(illuminant.shape);
169
170
  /** A material scene's material at a frame point: the caller's index, or `materials.length` for a fixed-size chart's surround. */
@@ -183,6 +184,29 @@ export function buildPlanarSceneRadianceProvider(input) {
183
184
  throw new InvalidConfigurationError("Scene materialAt must return an index into its materials.");
184
185
  return index;
185
186
  }
187
+ const light = input.irradianceAt;
188
+ if (light && caller?.kind === "radiance-function")
189
+ throw new InvalidConfigurationError("illumination needs a lit scene; a radiance-function scene ignores the illuminant.");
190
+ // #581 with #550: a print's signals are integrated per pixel under one light (its spectral nodes); mixed light over a print is not modelled.
191
+ if (light && imageScene)
192
+ throw new InvalidConfigurationError("illumination cannot light an image-plane scene yet: the print's signals are integrated under one illuminant.");
193
+ /** #550: reflectance × the blended irradiance at the point (chart-local units with a fixed-size chart) / π. */
194
+ function litRadianceAt(u, v, nm) {
195
+ let cu = u, cv = v, reflectance;
196
+ if (placed) {
197
+ cu = u * scale / half;
198
+ cv = v * scale / half;
199
+ }
200
+ if (materialScene)
201
+ reflectance = materialReflectanceOf(callerMaterialAt(u, v), nm);
202
+ else if (placed && !(Math.abs(cu) <= 1 && Math.abs(cv) <= chartAspect))
203
+ reflectance = placed.surroundReflectance;
204
+ else if (chart)
205
+ reflectance = chart.reflectanceAt(cu, cv, nm);
206
+ else
207
+ reflectance = callerReflectance(placed ? { xM: u * scale, yM: v * scale, u, v, wavelengthNanometers: nm, chartU: cu, chartV: cv } : { xM: u * scale, yM: v * scale, u, v, wavelengthNanometers: nm });
208
+ return reflectance * light(cu, cv, u * scale, v * scale, nm) / Math.PI;
209
+ }
186
210
  function radianceAt(u, v, nm) {
187
211
  if (materialScene)
188
212
  return materialRadianceOf(callerMaterialAt(u, v), nm);
@@ -191,21 +215,26 @@ export function buildPlanarSceneRadianceProvider(input) {
191
215
  const xM = u * scale, yM = v * scale, cu = xM / half, cv = yM / half;
192
216
  if (!(Math.abs(cu) <= 1 && Math.abs(cv) <= chartAspect))
193
217
  return placed.surroundReflectance * irradianceScale * shapeAt(nm) / Math.PI;
194
- if (chart)
195
- return chart.reflectanceAt(cu, cv, nm) * irradianceScale * shapeAt(nm) / Math.PI;
218
+ // #581: an image-plane print, like a chart, in print-local units.
219
+ const planar = chart ?? imageScene;
220
+ if (planar)
221
+ return planar.reflectanceAt(cu, cv, nm) * irradianceScale * shapeAt(nm) / Math.PI;
196
222
  return callerValue({ xM, yM, u, v, wavelengthNanometers: nm, chartU: cu, chartV: cv }, nm);
197
223
  }
198
224
  if (chart)
199
225
  return chart.reflectanceAt(u, v, nm) * irradianceScale * shapeAt(nm) / Math.PI;
200
226
  return callerValue({ xM: u * scale, yM: v * scale, u, v, wavelengthNanometers: nm }, nm);
201
227
  }
228
+ function callerReflectance(point) {
229
+ const value = caller.evaluate(point);
230
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1)
231
+ throw new InvalidConfigurationError("Scene reflectance function must return a value in [0, 1].");
232
+ return value;
233
+ }
202
234
  function callerValue(point, nm) {
235
+ if (caller.kind === "reflectance-function")
236
+ return callerReflectance(point) * irradianceScale * shapeAt(nm) / Math.PI;
203
237
  const value = caller.evaluate(point);
204
- if (caller.kind === "reflectance-function") {
205
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1)
206
- throw new InvalidConfigurationError("Scene reflectance function must return a value in [0, 1].");
207
- return value * irradianceScale * shapeAt(nm) / Math.PI;
208
- }
209
238
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
210
239
  throw new InvalidConfigurationError("Scene radiance function must return a finite non-negative radiance.");
211
240
  return value;
@@ -217,6 +246,9 @@ export function buildPlanarSceneRadianceProvider(input) {
217
246
  }
218
247
  /** Spectral radiance of material `material` (`materials.length`: a fixed-size chart's surround), in the chart's expression order. */
219
248
  function materialRadianceOf(material, nm) {
249
+ return materialReflectanceOf(material, nm) * irradianceScale * shapeAt(nm) / Math.PI;
250
+ }
251
+ function materialReflectanceOf(material, nm) {
220
252
  const m = materials[material];
221
253
  let value;
222
254
  if (m === undefined)
@@ -230,7 +262,7 @@ export function buildPlanarSceneRadianceProvider(input) {
230
262
  }
231
263
  if (!(value >= 0))
232
264
  throw new InvalidConfigurationError("Unknown chart material.");
233
- return value * irradianceScale * shapeAt(nm) / Math.PI;
265
+ return value;
234
266
  }
235
267
  function chartMaterialAt(u, v) {
236
268
  if (!placed)
@@ -238,10 +270,12 @@ export function buildPlanarSceneRadianceProvider(input) {
238
270
  const cu = u * scale / half, cv = v * scale / half;
239
271
  return Math.abs(cu) <= 1 && Math.abs(cv) <= chartAspect ? chart.materialAt(cu, cv) : chart.materials.length;
240
272
  }
241
- const chartAccess = chart ? { chartMaterialAt, materialRadiance: materialRadianceOf } :
273
+ const chartAccess = light ? {} : chart ? { chartMaterialAt, materialRadiance: materialRadianceOf } :
242
274
  materialScene ? { chartMaterialAt: callerMaterialAt, materialRadiance: materialRadianceOf } : {};
275
+ // #581: an image-plane print in node values at a frame point (the surround outside it), for the tabulated expected signal.
276
+ const imageAccess = imageScene ? { imageSignalScene: (wavelengths, perNode) => imageScene.signalScene(wavelengths, perNode, Float64Array.from(wavelengths, (nm) => irradianceScale * shapeAt(nm) / Math.PI), radianceAt, scale, half, placed.surroundReflectance) } : {};
243
277
  // #394: evaluateRadiance's own plane-point arithmetic; radianceAt is then materialRadianceOf(material, nm) for both scene forms.
244
- const materialAtPoint = chart ? chartMaterialAt : materialScene ? callerMaterialAt : undefined;
278
+ const materialAtPoint = light ? undefined : chart ? chartMaterialAt : materialScene ? callerMaterialAt : undefined;
245
279
  const materialOfDirection = materialAtPoint === undefined ? undefined : (o) => {
246
280
  const lz = -o.z;
247
281
  if (!(lz > 0))
@@ -249,8 +283,9 @@ export function buildPlanarSceneRadianceProvider(input) {
249
283
  const t = distance / lz;
250
284
  return materialAtPoint(-o.x * t / scale, -o.y * t / scale);
251
285
  };
286
+ const radiance = light ? litRadianceAt : radianceAt;
252
287
  const provider = {
253
- sceneId, providerProfileId, radianceAt, ...chartAccess,
288
+ sceneId, providerProfileId, radianceAt: radiance, ...chartAccess, ...imageAccess,
254
289
  evaluateRadiance(request) {
255
290
  const target = request.target;
256
291
  if (target.kind !== "environment-direction")
@@ -262,14 +297,14 @@ export function buildPlanarSceneRadianceProvider(input) {
262
297
  throw new InvalidConfigurationError("Planar scene query does not point toward the scene plane.");
263
298
  }
264
299
  const t = distance / lz;
265
- return result(request, radianceAt(-o.x * t / scale, -o.y * t / scale, request.wavelengthNanometers));
300
+ return result(request, radiance(-o.x * t / scale, -o.y * t / scale, request.wavelengthNanometers));
266
301
  },
267
302
  evaluateApertureRadiance(request, ray) {
268
303
  const d = ray.directionUnitVector, origin = ray.originM;
269
304
  if (!(d.z > 0))
270
305
  throw new InvalidConfigurationError("Planar scene pupil ray does not point toward the scene plane.");
271
306
  const t = (distance - origin.z) / d.z;
272
- return result(request, radianceAt((origin.x + d.x * t) / scale, (origin.y + d.y * t) / scale, request.wavelengthNanometers));
307
+ return result(request, radiance((origin.x + d.x * t) / scale, (origin.y + d.y * t) / scale, request.wavelengthNanometers));
273
308
  }
274
309
  };
275
310
  if (materialOfDirection)