@photivra/engine 2.12.0 → 2.14.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 (148) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -7
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +3 -0
  6. package/dist/camera/lens-projection.js +2 -1
  7. package/dist/camera/lens-projection.js.map +1 -1
  8. package/dist/capture/dng-import.js +1 -1
  9. package/dist/capture/dng-import.js.map +1 -1
  10. package/dist/capture/export-jpeg.d.ts +1 -1
  11. package/dist/capture/export-jpeg.js +6 -4
  12. package/dist/capture/export-jpeg.js.map +1 -1
  13. package/dist/capture/host-yield-cadence.js +1 -1
  14. package/dist/capture/host-yield-cadence.js.map +1 -1
  15. package/dist/capture/native-environment-optical-stage.js +1 -1
  16. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  17. package/dist/capture/native-raw.js +1 -1
  18. package/dist/capture/native-raw.js.map +1 -1
  19. package/dist/capture/packed-sensor-raw.js +3 -3
  20. package/dist/capture/packed-sensor-raw.js.map +1 -1
  21. package/dist/capture/sensor-raw-producer.js +1 -1
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/color/capture-color.js +3 -3
  24. package/dist/color/capture-color.js.map +1 -1
  25. package/dist/color/chromatic-adaptation.js +2 -2
  26. package/dist/color/chromatic-adaptation.js.map +1 -1
  27. package/dist/color/color-matching-functions.js +1 -1
  28. package/dist/color/color-matching-functions.js.map +1 -1
  29. package/dist/color/colorimetry-math.js +2 -2
  30. package/dist/color/colorimetry-math.js.map +1 -1
  31. package/dist/color/illuminants.js +3 -3
  32. package/dist/color/illuminants.js.map +1 -1
  33. package/dist/color/rgb-color-spaces.js +1 -1
  34. package/dist/color/rgb-color-spaces.js.map +1 -1
  35. package/dist/color/sensor-color-development.js +1 -1
  36. package/dist/color/sensor-color-development.js.map +1 -1
  37. package/dist/core/record-validation.js +3 -3
  38. package/dist/core/record-validation.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/dist/core/version.js.map +1 -1
  42. package/dist/equipment/generic-prime-lenses.js +3 -3
  43. package/dist/equipment/generic-prime-lenses.js.map +1 -1
  44. package/dist/equipment/generic-tier-assets.d.ts +23 -1
  45. package/dist/equipment/generic-tier-assets.js +58 -30
  46. package/dist/equipment/generic-tier-assets.js.map +1 -1
  47. package/dist/equipment/generic-tier-presets.d.ts +6 -3
  48. package/dist/equipment/generic-tier-presets.js +26 -13
  49. package/dist/equipment/generic-tier-presets.js.map +1 -1
  50. package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
  51. package/dist/exposure/auto-iso-slowest-shutter.js +217 -0
  52. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
  53. package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
  54. package/dist/exposure/exposure-mode-resolver.js +238 -33
  55. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  56. package/dist/exposure/flash-ttl.js +1 -1
  57. package/dist/exposure/flash-ttl.js.map +1 -1
  58. package/dist/exposure/flash.js +1 -1
  59. package/dist/exposure/flash.js.map +1 -1
  60. package/dist/facade/chart-material-layers.js +2 -2
  61. package/dist/facade/chart-material-layers.js.map +1 -1
  62. package/dist/facade/chart-radiance-batch.js +1 -1
  63. package/dist/facade/chart-radiance-batch.js.map +1 -1
  64. package/dist/facade/chart-scenes.d.ts +45 -8
  65. package/dist/facade/chart-scenes.js +101 -44
  66. package/dist/facade/chart-scenes.js.map +1 -1
  67. package/dist/facade/color-patches.d.ts +129 -0
  68. package/dist/facade/color-patches.js +209 -0
  69. package/dist/facade/color-patches.js.map +1 -0
  70. package/dist/facade/diffraction-provider.js +12 -13
  71. package/dist/facade/diffraction-provider.js.map +1 -1
  72. package/dist/facade/display-view-gpu.js +1 -1
  73. package/dist/facade/display-view-gpu.js.map +1 -1
  74. package/dist/facade/display-view.d.ts +27 -0
  75. package/dist/facade/display-view.js +234 -65
  76. package/dist/facade/display-view.js.map +1 -1
  77. package/dist/facade/scene-radiance.js +2 -2
  78. package/dist/facade/scene-radiance.js.map +1 -1
  79. package/dist/facade/simulate-capture.d.ts +139 -12
  80. package/dist/facade/simulate-capture.js +209 -36
  81. package/dist/facade/simulate-capture.js.map +1 -1
  82. package/dist/facade/simulate-display-view.js +2 -2
  83. package/dist/facade/simulate-display-view.js.map +1 -1
  84. package/dist/gpu/chart-composite-gpu.js +2 -2
  85. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  86. package/dist/gpu/display-view-composite-material-module.js +1 -1
  87. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  88. package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
  89. package/dist/gpu/display-view-quality-gpu.js +24 -14
  90. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  91. package/dist/gpu/image-kernel-execution.js +2 -2
  92. package/dist/gpu/image-kernel-execution.js.map +1 -1
  93. package/dist/gpu/temporal-sum-gpu.js +23 -23
  94. package/dist/gpu/webgpu-runtime.js +1 -1
  95. package/dist/gpu/webgpu-runtime.js.map +1 -1
  96. package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
  97. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
  98. package/dist/gpu/wgsl-chart-kernel.js +117 -117
  99. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  100. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
  101. package/dist/gpu/wgsl-display-view-quality-kernel.js +522 -488
  102. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  103. package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
  104. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  105. package/dist/gpu/wgsl-kernels.js +203 -203
  106. package/dist/gpu/wgsl-kernels.js.map +1 -1
  107. package/dist/gpu/wgsl-sensor-kernel.js +302 -302
  108. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  109. package/dist/index.d.ts +8 -5
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/motion/temporal-exposure-sum.js +1 -1
  113. package/dist/motion/temporal-exposure-sum.js.map +1 -1
  114. package/dist/optics/diffraction-psf-profile.js +4 -4
  115. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  116. package/dist/optics/front-of-lens-filter.js +1 -1
  117. package/dist/optics/front-of-lens-filter.js.map +1 -1
  118. package/dist/optics/irradiance-convolution.d.ts +10 -0
  119. package/dist/optics/irradiance-convolution.js +49 -14
  120. package/dist/optics/irradiance-convolution.js.map +1 -1
  121. package/dist/output/native-capture-sdr.js +1 -1
  122. package/dist/output/native-capture-sdr.js.map +1 -1
  123. package/dist/output/png-encoder.js +2 -2
  124. package/dist/output/png-encoder.js.map +1 -1
  125. package/dist/output/print-jpeg.js +13 -10
  126. package/dist/output/print-jpeg.js.map +1 -1
  127. package/dist/output/text-metadata.d.ts +22 -2
  128. package/dist/output/text-metadata.js +58 -9
  129. package/dist/output/text-metadata.js.map +1 -1
  130. package/dist/sensor/fixed-pattern-noise.js +1 -1
  131. package/dist/sensor/fixed-pattern-noise.js.map +1 -1
  132. package/dist/sensor/readout-exposure-linkage.js +1 -1
  133. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  134. package/docs/API_REFERENCE.md +49 -1
  135. package/docs/QUICK_START.md +104 -9
  136. package/docs/USAGE.md +241 -2
  137. package/docs/api/core-version.md +1 -1
  138. package/docs/api/equipment-generic-tier-assets.md +41 -1
  139. package/docs/api/equipment-generic-tier-presets.md +11 -3
  140. package/docs/api/exports.json +172 -2
  141. package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
  142. package/docs/api/exposure-exposure-mode-resolver.md +68 -0
  143. package/docs/api/facade-chart-scenes.md +36 -5
  144. package/docs/api/facade-color-patches.md +150 -0
  145. package/docs/api/facade-simulate-capture.md +140 -9
  146. package/docs/api/output-text-metadata.md +34 -0
  147. package/llms.txt +1 -0
  148. package/package.json +2 -2
@@ -0,0 +1,150 @@
1
+ # facade/color-patches.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
+ ## COLOR_PATCH_IDS
6
+
7
+ The ids of the built-in patches, in the order of the `color-patches` chart (row-major, six per row).
8
+
9
+ ```ts
10
+ COLOR_PATCH_IDS = Object.freeze([
11
+ "neutral-1", "neutral-2", "neutral-3", "neutral-4", "neutral-5", "neutral-6",
12
+ "red", "yellow", "green", "cyan", "blue", "magenta",
13
+ "red-soft", "yellow-soft", "green-soft", "cyan-soft", "blue-soft", "magenta-soft",
14
+ "skin-pale", "skin-medium", "skin-tan", "skin-deep", "sky-blue", "foliage-green",
15
+ "brick-red", "sand", "teal", "lavender", "deep-orange", "deep-violet"
16
+ ] as const)
17
+ ```
18
+
19
+ ## COLOR_PATCH_SET_ID
20
+
21
+ Identity of the built-in patch set.
22
+
23
+ ```ts
24
+ COLOR_PATCH_SET_ID = "photivra-color-patches"
25
+ ```
26
+
27
+ ## COLOR_PATCH_SET_VERSION
28
+
29
+ Version of the patch set's spectra and reference values; a change to any spectrum changes it.
30
+
31
+ ```ts
32
+ COLOR_PATCH_SET_VERSION = "1.0.0"
33
+ ```
34
+
35
+ ## COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS
36
+
37
+ Wavelengths on which every patch spectrum is defined, nanometers, inclusive.
38
+
39
+ ```ts
40
+ COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS = Object.freeze({ minimum: 360, maximum: 830 })
41
+ ```
42
+
43
+ ## ColorPatch
44
+
45
+ One built-in patch: its id, short code, name, reason for being on the chart, spectrum definition and reference
46
+ colorimetry.
47
+
48
+ ```ts
49
+ export interface ColorPatch {
50
+ /** Stable API id, e.g. `"skin-medium"`. */
51
+ patchId: ColorPatchId;
52
+ /**
53
+ * Stable short code for labels and printed charts: `PV-` and a group letter and number, e.g. `"PV-N4"`. The letters are
54
+ * N neutral, S strong, T soft (tint), P skin (portrait), M memory and G gamut-edge, numbered in chart order.
55
+ */
56
+ code: string;
57
+ /** Photivra's own descriptive name, in US English, e.g. `"Studio Gray 4"`; not a third-party color name. */
58
+ name: string;
59
+ group: ColorPatchGroup;
60
+ /** One sentence: why the patch is on the chart. */
61
+ reason: string;
62
+ /** The constant b of the formula. */
63
+ base: number;
64
+ terms: readonly ColorPatchSpectralTerm[];
65
+ /**
66
+ * Reference colorimetry of the reflectance (not of a rendered capture): CIELAB under CIE illuminant D65 with the CIE
67
+ * 1931 2° observer, the CIE tables at 1 nm over 360–830 nm, white = the perfect diffuser under D65 (Y = 100); and the
68
+ * sRGB color of that XYZ (CSS Color 4 matrix, after XYZ scaling of the D65 table white to the sRGB white),
69
+ * 8-bit hex. Rounded for publication: L*, a*, b* to 0.01.
70
+ */
71
+ reference: { cielabD65: readonly [number, number, number]; srgbHex: string; inSrgbGamut: boolean };
72
+ }
73
+ ```
74
+
75
+ ## ColorPatchGroup
76
+
77
+ Groups of the patch set, in chart order.
78
+
79
+ ```ts
80
+ export type ColorPatchGroup = "neutral" | "strong" | "soft" | "skin" | "memory" | "gamut-edge";
81
+ ```
82
+
83
+ ## ColorPatchId
84
+
85
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
86
+
87
+ ```ts
88
+ export type ColorPatchId = (typeof COLOR_PATCH_IDS)[number];
89
+ ```
90
+
91
+ ## ColorPatchSet
92
+
93
+ The built-in patch set (see {@link getColorPatchSet}).
94
+
95
+ ```ts
96
+ export interface ColorPatchSet {
97
+ setId: typeof COLOR_PATCH_SET_ID;
98
+ version: typeof COLOR_PATCH_SET_VERSION;
99
+ wavelengthRangeNanometers: { minimum: number; maximum: number };
100
+ /** Patches per row of the `color-patches` chart's default layout. */
101
+ columns: number;
102
+ patches: readonly ColorPatch[];
103
+ }
104
+ ```
105
+
106
+ ## ColorPatchSpectralTerm
107
+
108
+ One term of a patch spectrum (see the module formula).
109
+
110
+ ```ts
111
+ export type ColorPatchSpectralTerm =
112
+ | { kind: "gaussian"; amplitude: number; centerNanometers: number; standardDeviationNanometers: number }
113
+ | { kind: "logistic-rise" | "logistic-fall"; amplitude: number; midpointNanometers: number; scaleNanometers: number };
114
+ ```
115
+
116
+ ## evaluateColorPatchReflectance
117
+
118
+ The reflectance (dimensionless, in [0, 1]) of a built-in color patch at one wavelength, from its analytic formula.
119
+ Labeled `approximation`: the spectra are Photivra's own smooth designs, not measurements of a physical sample.
120
+
121
+ ```ts
122
+ export function evaluateColorPatchReflectance(input: EvaluateColorPatchReflectanceInput): CalculationResult<{ reflectance: number }>;
123
+ ```
124
+
125
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
126
+
127
+ ## EvaluateColorPatchReflectanceInput
128
+
129
+ Input of {@link evaluateColorPatchReflectance}.
130
+
131
+ ```ts
132
+ export interface EvaluateColorPatchReflectanceInput {
133
+ patchId: ColorPatchId;
134
+ /** Vacuum wavelength, nanometers, in 360–830. */
135
+ wavelengthNanometers: number;
136
+ }
137
+ ```
138
+
139
+ ## getColorPatchSet
140
+
141
+ The built-in Photivra color-patch set: every patch's id, code, name, group, reason, spectrum parameters and reference CIELAB (D65,
142
+ CIE 1931 2°) and sRGB values. A static, deeply frozen engine contract. Pass a patch to a chart as
143
+ `{ kind: "color-patch", patchId }` wherever a chart reflectance is accepted, or draw the whole set with the
144
+ `color-patches` chart preset.
145
+
146
+ ```ts
147
+ export function getColorPatchSet(): ColorPatchSet;
148
+ ```
149
+
150
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
@@ -21,8 +21,17 @@ export interface DisplayViewResult {
21
21
  */
22
22
  view: { width: number; height: number; frameWidth: number; frameHeight: number; scaleX: number; scaleY: number; samplesPerAxis: number;
23
23
  region?: { x: number; y: number; width: number; height: number } };
24
- /** The resolved exposure of the capture this view shows; `standardExposureDeviationStops` as in {@link SimulateCaptureResult}. */
24
+ /**
25
+ * The resolved exposure of the capture this view shows; `standardExposureDeviationStops`, `standardExposureIlluminanceLux`
26
+ * and `greyCardDeviationStops` as in {@link SimulateCaptureResult}.
27
+ */
25
28
  exposure: { mode: SimulateCaptureExposureMode; settings: { fNumber: number; shutterSeconds: number; iso: number }; ev100: number; standardExposureDeviationStops: number | null;
29
+ /** As in the capture result. */
30
+ standardExposureIlluminanceLux: number | null;
31
+ /** 2.14: as in the capture result. */
32
+ greyCardDeviationStops: number | null;
33
+ /** 2.14: as in the capture result. */
34
+ metering: SimulateCaptureMetering;
26
35
  /** #516: as in the capture result. */
27
36
  isoControl: "manual" | "automatic"; autoIso?: SimulateCaptureAutoIsoReport };
28
37
  scene: SimulateCaptureResult["scene"];
@@ -134,6 +143,14 @@ whether or not it has a fast method yet; one without runs exact and reports `fal
134
143
  SIMULATE_CAPTURE_FAST_OPTIONS = Object.freeze(["lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"] as const)
135
144
  ```
136
145
 
146
+ ## SIMULATE_CAPTURE_METERING_MODES
147
+
148
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
149
+
150
+ ```ts
151
+ SIMULATE_CAPTURE_METERING_MODES: readonly SimulateCaptureMetering[] = Object.freeze(["scene-average", "grey-card"] as const)
152
+ ```
153
+
137
154
  ## SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS
138
155
 
139
156
  Default stratified samples per axis of a display view (`view.samplesPerAxis`): 16 × 16 sampled real pixels per display pixel with detail, 4 × 4 where it is uniform.
@@ -186,23 +203,84 @@ resolver output stays in the capture's `exposure.resolution`.
186
203
  export interface SimulateCaptureAutoIsoReport {
187
204
  /** The ISO the meter target asked for at the resolved aperture and shutter, before the Auto ISO range and the ISO grid. */
188
205
  idealIsoBeforeConstraints: number;
189
- /** The Auto ISO range the ISO was chosen in: the body's Auto ISO bounds, or its ISO range when it declares none (the tiers: ISO 100 to their maximum). */
206
+ /**
207
+ * The body's Auto ISO range: its Auto ISO bounds, or its ISO range when it declares none (the tiers: ISO 100 to their
208
+ * maximum). Without `limits` the ISO was chosen in this range.
209
+ */
190
210
  autoIsoMinimum: number;
191
211
  autoIsoMaximum: number;
212
+ /**
213
+ * #505: present only with `exposure.autoIso.limits`: the photographer's Auto ISO range the ISO was chosen in, inside the
214
+ * body's (`minimum` is the body's Auto ISO minimum when the option omits it).
215
+ */
216
+ limits?: { minimum: number; maximum: number };
192
217
  /** Whether the ISO was held at an end of the Auto ISO range. */
193
218
  clamped: false | "minimum" | "maximum";
194
219
  /**
195
220
  * The constraint that limited the resolved exposure: the resolver's `targetResidual.limitingConstraint`, for example
196
- * `"iso-maximum"` (the exposure stays under target at the Auto ISO maximum), `"iso-grid-quantization"` (the nearest
197
- * selectable ISO) or `"none"` (on target).
221
+ * `"iso-maximum"` (the exposure stays under target at the body's Auto ISO maximum), `"user-iso-maximum"` (#505: at the
222
+ * photographer's `limits.maximum`, below the body's), `"iso-grid-quantization"` (the nearest selectable ISO) or `"none"`
223
+ * (on target).
198
224
  */
199
225
  limitingConstraint: ExposureResolutionConstraint;
200
226
  /** The resolver's residual, log2(target / achieved exposure), in stops: positive when the exposure is under the meter target, negative when over it. */
201
227
  residualStops: number;
202
- /** Aperture priority only: the policy and slowest preferred shutter used, and whether the slower-shutter fallback ran. */
228
+ /**
229
+ * Aperture priority, and program with a slowest preferred shutter (#504): the policy and slowest preferred shutter used,
230
+ * and whether the slower-shutter fallback ran.
231
+ */
203
232
  afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum;
204
233
  slowestPreferredShutterSeconds?: number;
205
234
  afterMaximumIsoFallbackUsed?: boolean;
235
+ /** #504, program only: whether the program line's shutter was longer than the slowest preferred shutter and was held. */
236
+ slowestPreferredShutterApplied?: boolean;
237
+ /** #504: present only with `slowestPreferredShutterSeconds: "auto"`: how the engine derived it (`estimateAutoIsoSlowestShutter`). */
238
+ automaticSlowestShutter?: SimulateCaptureAutomaticSlowestShutterReport;
239
+ /** 2.14: present when `exposure.autoIso.isoSteps` is given: the ISO steps Auto ISO chose from. */
240
+ isoSteps?: "whole-stop" | "third-stop";
241
+ }
242
+ ```
243
+
244
+ ## SimulateCaptureAutomaticSlowestShutterOptions
245
+
246
+ #504: the settings of `exposure.autoIso.slowestPreferredShutterSeconds: "auto"`. Every field is optional; the defaults are
247
+ labelled generic values, recorded as `default` assumptions.
248
+
249
+ ```ts
250
+ export interface SimulateCaptureAutomaticSlowestShutterOptions {
251
+ /** The bias in whole stops, as cameras' Auto minimum-shutter settings offer: +1 one stop faster, −1 one stop slower; −2 to +2. Default 0. */
252
+ fasterByStops?: number;
253
+ /** The shake blur allowed, in sensor pixels of `sensor.pixelPitchMicrometers`. Default 1 (a generic pixel-level budget). */
254
+ blurBudgetPixels?: number;
255
+ /**
256
+ * The hand's constant angular velocity, rad/s (signs as in `motion.angularVelocityRadPerSec`; only the magnitude matters).
257
+ * Default `GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC`: 4.39 °/s on yaw, a cited generic value for a 100 g phone held in
258
+ * two hands (Xiao et al., Proc. SPIE 7250, 2009), not a measurement of you or of this camera.
259
+ */
260
+ angularVelocityRadPerSec?: { yaw: number; pitch: number };
261
+ /** Stabilization in equivalent stops (≥ 0, at most 10), scaling the motion by 2^−stops. Default 0: the tiers declare no stabilization. */
262
+ stabilizationStopsEquivalent?: number;
263
+ }
264
+ ```
265
+
266
+ ## SimulateCaptureAutomaticSlowestShutterReport
267
+
268
+ #504: the automatic slowest preferred shutter in the Auto ISO report.
269
+
270
+ ```ts
271
+ export interface SimulateCaptureAutomaticSlowestShutterReport {
272
+ /** The exposure time whose camera-shake blur equals the budget (`estimateCameraShakeBlur`), before the bias and the grid. */
273
+ derivedShutterSeconds: number;
274
+ fasterByStops: number;
275
+ /** derivedShutterSeconds · 2^−fasterByStops. */
276
+ biasedShutterSeconds: number;
277
+ /** The slowest preferred shutter used: the biased time within the body's shutter range. */
278
+ shutterSeconds: number;
279
+ blurBudgetPixels: number;
280
+ /** The shake blur at `shutterSeconds`, in pixels. */
281
+ blurAtShutterPixels: number;
282
+ angularVelocityRadPerSec: { yaw: number; pitch: number };
283
+ stabilizationStopsEquivalent: number;
206
284
  }
207
285
  ```
208
286
 
@@ -308,6 +386,17 @@ export type SimulateCaptureLensDistortion =
308
386
  | { kind: "brown-conrady"; profile: BrownConradyDistortionProfile };
309
387
  ```
310
388
 
389
+ ## SimulateCaptureMetering
390
+
391
+ 2.14: what the meter reads. `scene-average` (the default): the body's 16 × 16-zone meter reads the scene's luminance.
392
+ `grey-card`: incident metering; every zone reads the luminance 0.18·E/π of an 18 % Lambertian grey card under the
393
+ request's illuminance E, so the automatic modes choose settings whose `greyCardDeviationStops` is 0 (up to their steps
394
+ and limits), whatever the scene's reflectances.
395
+
396
+ ```ts
397
+ export type SimulateCaptureMetering = "scene-average" | "grey-card";
398
+ ```
399
+
311
400
  ## SimulateCaptureMotion
312
401
 
313
402
  The exact contract is declared below; use the domain guide for assumptions and staged integration.
@@ -391,15 +480,37 @@ export interface SimulateCaptureOptions {
391
480
  */
392
481
  iso?: number | "auto";
393
482
  /**
394
- * #516: aperture priority with `iso: "auto"` only (required there). The shutter gets as long as
395
- * `slowestPreferredShutterSeconds` (seconds, within the body's shutter range) at the lowest Auto ISO; beyond it the ISO
396
- * rises; at the Auto ISO maximum `afterMaximumIso` decides (default `"allow-slower-shutter"`).
483
+ * Auto ISO settings, with `iso: "auto"` only.
484
+ *
485
+ * - `slowestPreferredShutterSeconds` and `afterMaximumIso` (#516): aperture priority, where the slowest shutter is
486
+ * required, and (#504) program, where it is optional. The shutter gets as long as `slowestPreferredShutterSeconds`
487
+ * (seconds, within the body's shutter range) at the lowest Auto ISO; beyond it the ISO rises; at the Auto ISO maximum
488
+ * `afterMaximumIso` decides (default `"allow-slower-shutter"`; in program the shutter then never gets longer than the
489
+ * program line's own). Without it, program follows its line as before.
490
+ * - `slowestPreferredShutterSeconds: "auto"` (#504): the engine derives it from the focal length, the pixel pitch and a
491
+ * hand-shake profile with `estimateAutoIsoSlowestShutter` (the camera-shake model, not the 1/focal-length rule of
492
+ * thumb); `automaticSlowestShutter` sets the bias, the blur budget, the shake and the stabilization.
493
+ * - `limits` (#505): every mode. The photographer's Auto ISO range `{ maximum, minimum? }`, a choice kept apart from
494
+ * the body's Auto ISO capability: both are selectable ISOs inside the body's Auto ISO range, `minimum` at most
495
+ * `maximum` (default: the body's minimum). Outside the body's range or off its ISO grid the request fails; the
496
+ * engine never clips a limit. When the ISO stops at a limit strictly inside the body's range, the report's
497
+ * `limitingConstraint` is `"user-iso-maximum"` or `"user-iso-minimum"`.
498
+ * - `isoSteps` (2.14): `"whole-stop"` (default) lets Auto ISO choose only the whole-stop ISOs of the body's grid
499
+ * (100, 200, 400 …), as before the generic bodies gained third-stop settings (preset 2.1.0); `"third-stop"` lets it
500
+ * choose every setting (100, 125, 160 …). Manual ISO always takes any setting of the grid. Limits must lie on the
501
+ * grid Auto ISO uses.
397
502
  */
398
- autoIso?: { slowestPreferredShutterSeconds: number; afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum };
503
+ autoIso?: { slowestPreferredShutterSeconds?: number | "auto"; afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum; limits?: AutoIsoLimits;
504
+ automaticSlowestShutter?: SimulateCaptureAutomaticSlowestShutterOptions; isoSteps?: "whole-stop" | "third-stop" };
399
505
  /** Shutter for manual and shutter-priority modes. Default 1/125 s. */
400
506
  shutterSeconds?: number;
401
507
  /** Exposure compensation applied to the meter target, in stops. Default 0. */
402
508
  compensationStops?: number;
509
+ /**
510
+ * 2.14: `"scene-average"` (default) or `"grey-card"` (incident metering, see {@link SimulateCaptureMetering}). Manual
511
+ * exposure does not use the meter's reading, so the setting changes only the automatic modes and Auto ISO.
512
+ */
513
+ metering?: SimulateCaptureMetering;
403
514
  };
404
515
  /**
405
516
  * Required: an engine chart (for example `{ preset: "uniform" }`, an 18 % grey card), a declared `material-layers` scene (#491:
@@ -608,6 +719,9 @@ export interface SimulateCaptureResult {
608
719
  isoControl: "manual" | "automatic";
609
720
  /** #516: present with `iso: "auto"`: how Auto ISO chose `settings.iso` and what limited it. */
610
721
  autoIso?: SimulateCaptureAutoIsoReport;
722
+ /** 2.14: the metering the exposure used (`exposure.metering`; `"scene-average"` when omitted). */
723
+ metering: SimulateCaptureMetering;
724
+ /** The scene's average luminance on the meter's zones, cd/m², with either metering. */
611
725
  meteredSceneLuminanceCdPerSquareMeter: number;
612
726
  /**
613
727
  * #515: deviation, in stops, of the exposure used from the CIPA DC-004-2020 standard exposure for the metered luminance L:
@@ -616,6 +730,23 @@ export interface SimulateCaptureResult {
616
730
  * the metered luminance is 0 (a black scene has no standard exposure).
617
731
  */
618
732
  standardExposureDeviationStops: number | null;
733
+ /**
734
+ * The illuminance, lux, at which the settings used give the standard exposure: E·L_ref(N, t, S)/L = E·2^Δ, with E the
735
+ * request's `illuminanceLux`. Exact for a reflective scene (a chart, a reflectance function or a material scene), whose
736
+ * metered luminance is proportional to E. Null for a black scene (L = 0) and for a radiance-function scene, whose
737
+ * radiance does not depend on the illuminance. It may exceed the 1e7 lux that `illuminanceLux` accepts.
738
+ */
739
+ standardExposureIlluminanceLux: number | null;
740
+ /**
741
+ * 2.14: deviation, in stops, of the exposure used from the CIPA DC-004-2020 standard exposure of an 18 % Lambertian grey
742
+ * card under the request's illuminance E (incident metering: the light falling on the scene, as a grey card or a
743
+ * hand-held incident meter reads it): log2(L_ref(N, t, S) / (0.18·E/π)), with L_ref = 10·N²/(0.65·t·S). Same sign as
744
+ * `standardExposureDeviationStops` (positive is darker, under). It does not depend on the scene's reflectances; with the
745
+ * default settings and illuminance it is exactly 0. The exposure modes still meter the scene (`meteredSceneLuminanceCdPerSquareMeter`);
746
+ * this only reports where the settings used sit against the grey card. Null for a radiance-function scene, whose radiance
747
+ * does not depend on the illuminance.
748
+ */
749
+ greyCardDeviationStops: number | null;
619
750
  /** Full output of the existing exposure-mode resolver. */
620
751
  resolution: unknown;
621
752
  };
@@ -2,6 +2,40 @@
2
2
 
3
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
4
 
5
+ ## addJpegTextMetadata
6
+
7
+ Returns a copy of a baseline JPEG with the text fields added: merged into its XMP APP1 packet (or a new packet
8
+ after the last leading APPn segment when it has none), plus a COM segment for `comment`. Every other byte,
9
+ including the entropy-coded data, is unchanged. Reads marker segments only up to SOS. The engine's own
10
+ `output.metadata` path calls this function, so for the same JPEG and text the bytes are the same.
11
+
12
+ Root export (#388). `metadata` is checked as `output.metadata` is (see the module notes); a JPEG without SOI,
13
+ a malformed or truncated segment before SOS, an XMP APP1 segment after a non-APPn segment, an XMP packet that is
14
+ not UTF-8 or has no `rdf:RDF`, or text that makes the packet larger than one APP1 segment fails with
15
+ `InvalidConfigurationError`. The input is not changed.
16
+
17
+ ```ts
18
+ export function addJpegTextMetadata(jpeg: Uint8Array, metadata: Readonly<OutputTextMetadata>): Uint8Array;
19
+ ```
20
+
21
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
22
+
23
+ ## addPngTextMetadata
24
+
25
+ Returns a copy of a PNG with one text chunk per field inserted before the first IDAT chunk. Other chunks are
26
+ unchanged. The engine's own `output.metadata` path calls this function, so for the same PNG and text the bytes
27
+ are the same.
28
+
29
+ Root export (#388). `metadata` is checked as `output.metadata` is (see the module notes); a file without the PNG
30
+ signature, or without an IDAT chunk inside its bytes, fails with `InvalidConfigurationError`. Chunk CRCs before
31
+ IDAT are not checked. The input is not changed.
32
+
33
+ ```ts
34
+ export function addPngTextMetadata(png: Uint8Array, metadata: Readonly<OutputTextMetadata>): Uint8Array;
35
+ ```
36
+
37
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
38
+
5
39
  ## OutputTextMetadata
6
40
 
7
41
  Caller-supplied text written into JPEG, PNG and DNG outputs. Every field is optional; at least one must be set.
package/llms.txt CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  - [Agent guide](https://github.com/Photivra/photivra/blob/main/docs/AGENT_GUIDE.md): which function to call, units, reading results, errors and limits
8
8
  - [Quick start](https://github.com/Photivra/photivra/blob/main/docs/QUICK_START.md): `simulateCapture()` inputs, defaults, chart presets and output formats
9
+ - [Color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md): the built-in color chart patches (`color-patches` preset, `{ kind: "color-patch", patchId }`), their spectra and CIELAB
9
10
  - [Usage guide](https://github.com/Photivra/photivra/blob/main/docs/USAGE.md): worked examples for every public area
10
11
  - [API reference](https://github.com/Photivra/photivra/blob/main/docs/API_REFERENCE.md): every root export, with a task-to-function index
11
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photivra/engine",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "private": false,
5
5
  "description": "Scientific TypeScript engine for camera optics, exposure, focus, motion, sensor sampling, diffraction, and digital imaging calculations.",
6
6
  "type": "module",
@@ -73,7 +73,7 @@
73
73
  "node": ">=22.13.0"
74
74
  },
75
75
  "scripts": {
76
- "build": "tsc -p tsconfig.build.json",
76
+ "build": "tsc -p tsconfig.build.json && node scripts/minify-wgsl-dist.mjs",
77
77
  "hooks:install": "node scripts/install-git-hooks.mjs",
78
78
  "prepush": "node scripts/prepush.mjs",
79
79
  "check:static": "eslint . && tsc --noEmit && npm run check:licenses && npm run check:license-headers && npm run check:identity && npm run check:browser-surface",