@photivra/engine 2.17.0 → 2.18.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 (52) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/dist/camera/sensor-equivalence.d.ts +156 -0
  6. package/dist/camera/sensor-equivalence.js +208 -0
  7. package/dist/camera/sensor-equivalence.js.map +1 -0
  8. package/dist/capture/native-environment-tabulated.d.ts +5 -0
  9. package/dist/capture/native-environment-tabulated.js +1 -1
  10. package/dist/capture/native-environment-tabulated.js.map +1 -1
  11. package/dist/core/version.d.ts +1 -1
  12. package/dist/core/version.js +1 -1
  13. package/dist/core/version.js.map +1 -1
  14. package/dist/equipment/ideal-lens.d.ts +25 -0
  15. package/dist/equipment/ideal-lens.js +21 -0
  16. package/dist/equipment/ideal-lens.js.map +1 -0
  17. package/dist/facade/catalogue-lens-signal.d.ts +94 -0
  18. package/dist/facade/catalogue-lens-signal.js +362 -0
  19. package/dist/facade/catalogue-lens-signal.js.map +1 -0
  20. package/dist/facade/defocus-image-space-provider.js +5 -2
  21. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  22. package/dist/facade/display-view-lens-signal.d.ts +32 -1
  23. package/dist/facade/display-view-lens-signal.js +85 -10
  24. package/dist/facade/display-view-lens-signal.js.map +1 -1
  25. package/dist/facade/expected-signal-setup.d.ts +8 -0
  26. package/dist/facade/expected-signal-setup.js +7 -0
  27. package/dist/facade/expected-signal-setup.js.map +1 -1
  28. package/dist/facade/expected-signal.d.ts +5 -0
  29. package/dist/facade/expected-signal.js +5 -1
  30. package/dist/facade/expected-signal.js.map +1 -1
  31. package/dist/facade/generic-lens-capture.d.ts +1 -0
  32. package/dist/facade/generic-lens-capture.js +1 -0
  33. package/dist/facade/generic-lens-capture.js.map +1 -1
  34. package/dist/facade/simulate-capture.d.ts +31 -4
  35. package/dist/facade/simulate-capture.js +117 -38
  36. package/dist/facade/simulate-capture.js.map +1 -1
  37. package/dist/facade/simulate-display-view.js +1 -1
  38. package/dist/facade/simulate-display-view.js.map +1 -1
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.js +2 -0
  41. package/dist/index.js.map +1 -1
  42. package/docs/AGENT_GUIDE.md +3 -0
  43. package/docs/API_REFERENCE.md +29 -1
  44. package/docs/QUICK_START.md +49 -1
  45. package/docs/USAGE.md +43 -2
  46. package/docs/api/camera-sensor-equivalence.md +209 -0
  47. package/docs/api/core-version.md +1 -1
  48. package/docs/api/equipment-ideal-lens.md +26 -0
  49. package/docs/api/exports.json +67 -2
  50. package/docs/api/facade-simulate-capture.md +27 -4
  51. package/llms.txt +2 -0
  52. package/package.json +1 -1
package/docs/USAGE.md CHANGED
@@ -374,6 +374,8 @@ The actual focal length stays the physical optical quantity used by projection a
374
374
  - Focus distance is not part of conventional 35 mm-equivalent focal length. A later digital/output crop and the output resolution are also excluded.
375
375
  - If a product wants to describe the final digital framing, it should label it as output framing. It should not silently replace the optical focal length or the capture-equivalent focal length.
376
376
 
377
+ To render a lens by its physical focal length on any sensor, give `simulateCapture()` or the display view `lens.ideal: { focalLengthMm }` with any `lens.fNumber` within `IDEAL_LENS_LIMITS` (f/0.5–f/128, 1–4000 mm; optional `widestFNumber` and `narrowestFNumber` narrow the f-number range for the automatic exposure modes): an aberration-free thin lens that is not scaled by the crop factor, unlike `lens.prime` (see docs/QUICK_START.md, "Ideal lens at any focal length and f-number").
378
+
377
379
  `calculateImagingAreaMetrics()` provides the same canonical diagonal crop-factor calculation, independent of raster density, so other engine modules do not need to repeat that formula.
378
380
 
379
381
  ## Focus-breathing projection
@@ -7479,8 +7481,9 @@ own designs, not real products). `resolveGenericLens` resolves one for a sensor,
7479
7481
  colour, distortion and vignetting profiles, flare, breathing and focus scatter, each a labelled approximation.
7480
7482
  `simulateGenericLensFocusError` draws a seeded autofocus error; `createGenericLensCorrectionProfile` builds the lens's generic
7481
7483
  correction profile for `resolveLensCorrectionPlan`. `simulateCapture` renders a catalogue lens with `lens.catalogue`
7482
- ([quick start](QUICK_START.md#generic-lens-catalogue-549)). Equations, tables and the chart measurements:
7483
- [LENS_CATALOGUE.md](https://github.com/Photivra/photivra/blob/main/docs/LENS_CATALOGUE.md).
7484
+ ([quick start](QUICK_START.md#generic-lens-catalogue-549)); `fast: true` (or `lens: { catalogue: { kind, fast: true } }`) takes its
7485
+ fast method (#568), a labeled approximation that blends the PSF over a field grid, for interactive captures. Equations, tables and the
7486
+ chart measurements: [LENS_CATALOGUE.md](https://github.com/Photivra/photivra/blob/main/docs/LENS_CATALOGUE.md).
7484
7487
 
7485
7488
  ```ts
7486
7489
  import { GENERIC_LENS_CATALOGUE, resolveGenericLens, simulateGenericLensFocusError } from "@photivra/engine";
@@ -7999,3 +8002,41 @@ import { simulateCapture } from "@photivra/engine";
7999
8002
  const rolling = await simulateCapture({ scene: { preset: "checkerboard" }, motion: { angularVelocityRadPerSec: { pitch: 0.02 }, shutter: "rolling" }, output: { width: 6, height: 6 } });
8000
8003
  console.log(rolling.request.siteInput(7 * 8).charge.completenessProfile.startOffsetSecondsFromOpeningReference);
8001
8004
  ```
8005
+
8006
+ ## Equivalent f-number, sensor area and blur as a share of the frame
8007
+
8008
+ `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:
8009
+
8010
+ ```ts
8011
+ import { calculateBlurShareOfFrame, calculateSensorEquivalence } from "@photivra/engine";
8012
+
8013
+ const apsC = calculateSensorEquivalence({
8014
+ sensor: { widthMm: 23.5, heightMm: 15.6 },
8015
+ focalLengthMm: 50,
8016
+ fNumber: 2
8017
+ }).value;
8018
+
8019
+ console.log(apsC.cropFactor); // ≈ 1.53
8020
+ console.log(apsC.equivalentFocalLengthMm); // ≈ 76.7 (framing)
8021
+ console.log(apsC.equivalentFNumber); // ≈ 3.07 (depth of field, blur and total light; not exposure)
8022
+ console.log(apsC.exposureFNumber); // 2 (exposure does not change with sensor size)
8023
+ console.log(apsC.areaRatio); // ≈ 0.42 of the full-frame area
8024
+ console.log(apsC.totalLightDifferenceStops); // ≈ -1.24 stops at the same f-number and shutter
8025
+ console.log(apsC.basis.equivalentFNumber.kind); // "approximation"
8026
+
8027
+ const share = calculateBlurShareOfFrame({
8028
+ blurCircleMm: 0.03,
8029
+ sensor: { widthMm: 36, heightMm: 24 },
8030
+ print: { widthMm: 300, heightMm: 200 }
8031
+ }).value;
8032
+
8033
+ console.log(share.shareOfDiagonal); // ≈ 0.00069 of the frame diagonal
8034
+ console.log(share.print?.blurOnPrintMm); // 0.25 mm on the print
8035
+ ```
8036
+
8037
+ - `basis` gives, for every number, `kind: "exact"` or `"approximation"` and the conditions. Crop factor, equivalent focal length, area ratio, total-light difference, entrance pupil and angles of view are exact under their stated conditions.
8038
+ - The equivalent f-number (crop factor × f-number) assumes the same framing, subject distance and output size. It is a thin-lens approximation that is exact only when the focus distance is much greater than the focal length; at close focus the blur shares match at `k·N·(s − f)/(s − k·f)`.
8039
+ - Exposure uses the f-number on the lens. Equivalence never changes shutter, ISO or exposure.
8040
+ - Equal shares of the frame diagonal look the same when images are shown at the same size. `calculateBlurShareOfFrame()` does not judge whether a blur is visible.
8041
+
8042
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#sensor-equivalence-f-number-area-and-blur-share).
@@ -0,0 +1,209 @@
1
+ # camera/sensor-equivalence.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
+ ## BlurShareOfFrame
6
+
7
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
8
+
9
+ ```ts
10
+ export interface BlurShareOfFrame {
11
+ /** Blur-circle diameter on the sensor in millimetres. */
12
+ blurCircleMm: number;
13
+ /** Sensor diagonal in millimetres. */
14
+ frameDiagonalMm: number;
15
+ /** Blur diameter / frame diagonal. Equal shares look the same at the same output size. */
16
+ shareOfDiagonal: number;
17
+ /** Blur diameter / frame width. */
18
+ shareOfWidth: number;
19
+ /** Blur diameter / frame height. */
20
+ shareOfHeight: number;
21
+ /** Blur on the print; present only when a print size was given. */
22
+ print?: BlurShareOfPrint;
23
+ }
24
+ ```
25
+
26
+ ## BlurShareOfPrint
27
+
28
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
29
+
30
+ ```ts
31
+ export interface BlurShareOfPrint {
32
+ /** Linear enlargement from sensor to print: the smaller of the long-side and short-side ratios. */
33
+ enlargement: number;
34
+ /** Blur-circle diameter on the print in millimetres: blurCircleMm × enlargement. */
35
+ blurOnPrintMm: number;
36
+ /** Size of the printed image in millimetres (equal to the print when the aspect ratios match). */
37
+ imageLongSideMm: number;
38
+ imageShortSideMm: number;
39
+ }
40
+ ```
41
+
42
+ ## calculateBlurShareOfFrame
43
+
44
+ A blur circle on the sensor as a share of the frame (diagonal, width and height) and, when a
45
+ print size is given, its diameter on that print.
46
+
47
+ Equal shares of the diagonal look the same when the images are shown at the same size, which
48
+ is how "same framing" comparisons across sensor sizes are stated. Every value is an exact ratio
49
+ or scaling; whether a blur is visible depends on viewing distance and acuity, which this
50
+ function does not judge.
51
+
52
+ ```ts
53
+ export function calculateBlurShareOfFrame(
54
+ input: CalculateBlurShareOfFrameInput
55
+ ): CalculationResult<BlurShareOfFrame>;
56
+ ```
57
+
58
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
59
+
60
+ ## CalculateBlurShareOfFrameInput
61
+
62
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
63
+
64
+ ```ts
65
+ export interface CalculateBlurShareOfFrameInput {
66
+ /** Blur-circle diameter on the sensor in millimetres (for example from calculateDefocusCircle). Zero is allowed. */
67
+ blurCircleMm: number;
68
+ /** Physical active imaging area of the sensor. */
69
+ sensor: SensorImagingArea;
70
+ /**
71
+ * Optional print size. The whole frame is enlarged to fit inside it without cropping, long side
72
+ * to long side.
73
+ */
74
+ print?: { widthMm: number; heightMm: number };
75
+ }
76
+ ```
77
+
78
+ ## calculateSensorEquivalence
79
+
80
+ Equivalence of a lens (focal length, f-number) on one sensor against a reference format,
81
+ 36 × 24 mm by default: crop factor, equivalent focal length, equivalent f-number for depth of
82
+ field, blur and total light, the unchanged f-number for exposure, sensor area ratio, total-light
83
+ difference in stops and the angles of view.
84
+
85
+ Call it once per sensor for a lens table. Every quantity carries a `basis` entry saying whether it
86
+ is exact under its stated conditions or an approximation. The equivalent f-number is an
87
+ approximation (exact when the focus distance is much greater than the focal length) and never an
88
+ exposure setting: exposure uses `exposureFNumber`, the f-number on the lens.
89
+
90
+ Example: 50 mm f/2 on a 23.5 × 15.6 mm sensor gives crop factor ≈ 1.53, ≈ 77 mm and ≈ f/3.1.
91
+
92
+ ```ts
93
+ export function calculateSensorEquivalence(
94
+ input: CalculateSensorEquivalenceInput
95
+ ): CalculationResult<SensorEquivalence>;
96
+ ```
97
+
98
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
99
+
100
+ ## CalculateSensorEquivalenceInput
101
+
102
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
103
+
104
+ ```ts
105
+ export interface CalculateSensorEquivalenceInput {
106
+ /** Physical active imaging area of the sensor the lens is used on. */
107
+ sensor: SensorImagingArea;
108
+ /** Physical focal length of the lens in millimetres. */
109
+ focalLengthMm: number;
110
+ /** f-number set on the lens (N = focal length / entrance-pupil diameter). */
111
+ fNumber: number;
112
+ /** Format to compare against. Defaults to the 36 × 24 mm frame. */
113
+ reference?: SensorImagingArea;
114
+ }
115
+ ```
116
+
117
+ ## EquivalenceExactness
118
+
119
+ Whether a value is exact under its stated conditions or an approximation.
120
+
121
+ ```ts
122
+ export type EquivalenceExactness = "exact" | "approximation";
123
+ ```
124
+
125
+ ## EquivalenceValueBasis
126
+
127
+ How one equivalence quantity was obtained, and under which conditions it holds.
128
+
129
+ ```ts
130
+ export interface EquivalenceValueBasis {
131
+ /** `"exact"` under the stated conditions, or `"approximation"`. */
132
+ kind: EquivalenceExactness;
133
+ /** Plain statement of the definition and its conditions. */
134
+ statement: string;
135
+ }
136
+ ```
137
+
138
+ ## SensorEquivalence
139
+
140
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
141
+
142
+ ```ts
143
+ export interface SensorEquivalence {
144
+ /** Physical focal length supplied by the caller, in millimetres. */
145
+ focalLengthMm: number;
146
+ /** f-number supplied by the caller. */
147
+ fNumber: number;
148
+ /** Sensor active imaging area used. */
149
+ sensor: SensorImagingArea;
150
+ /** Reference format used (36 × 24 mm unless the caller passed one). */
151
+ reference: SensorImagingArea;
152
+ /** Sensor diagonal in millimetres. */
153
+ sensorDiagonalMm: number;
154
+ /** Reference diagonal in millimetres. */
155
+ referenceDiagonalMm: number;
156
+ /** Diagonal crop factor: reference diagonal / sensor diagonal. Greater than 1 for a smaller sensor. */
157
+ cropFactor: number;
158
+ /** Focal length on the reference format with the same diagonal angle of view: cropFactor × focalLengthMm. */
159
+ equivalentFocalLengthMm: number;
160
+ /**
161
+ * f-number on the reference format that gives the same depth of field, background blur and total
162
+ * light for the same framing and output size: cropFactor × fNumber. Not an exposure setting.
163
+ */
164
+ equivalentFNumber: number;
165
+ /** f-number that sets exposure (image brightness): the f-number on the lens, unchanged by sensor size. */
166
+ exposureFNumber: number;
167
+ /** Entrance-pupil diameter f/N in millimetres; the equivalent lens has the same diameter. */
168
+ entrancePupilDiameterMm: number;
169
+ /** Sensor area / reference area. Less than 1 for a smaller sensor. */
170
+ areaRatio: number;
171
+ /**
172
+ * log2(areaRatio): total light collected by the sensor relative to the reference at the same
173
+ * f-number, shutter time and scene, in stops. Negative for a smaller sensor.
174
+ */
175
+ totalLightDifferenceStops: number;
176
+ /** Angles of view of this lens on the sensor (the equivalent lens gives the same diagonal angle). */
177
+ fieldOfView: SensorEquivalenceFieldOfView;
178
+ /** Exact or approximated, with the conditions, for every quantity above. */
179
+ basis: Readonly<Record<SensorEquivalenceQuantity, EquivalenceValueBasis>>;
180
+ }
181
+ ```
182
+
183
+ ## SensorEquivalenceFieldOfView
184
+
185
+ Angles of view of an ideal rectilinear lens focused at infinity, in degrees.
186
+
187
+ ```ts
188
+ export interface SensorEquivalenceFieldOfView {
189
+ horizontalDegrees: number;
190
+ verticalDegrees: number;
191
+ diagonalDegrees: number;
192
+ }
193
+ ```
194
+
195
+ ## SensorEquivalenceQuantity
196
+
197
+ Quantities of a sensor equivalence result that carry an exactness statement.
198
+
199
+ ```ts
200
+ export type SensorEquivalenceQuantity =
201
+ | "cropFactor"
202
+ | "equivalentFocalLengthMm"
203
+ | "equivalentFNumber"
204
+ | "exposureFNumber"
205
+ | "entrancePupilDiameterMm"
206
+ | "areaRatio"
207
+ | "totalLightDifferenceStops"
208
+ | "fieldOfView";
209
+ ```
@@ -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.17.0" as const
14
+ ENGINE_API_VERSION = "2.18.0" as const
15
15
  ```
@@ -0,0 +1,26 @@
1
+ # equipment/ideal-lens.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
+ ## IDEAL_LENS_LIMITS
6
+
7
+ Ideal lens (#576): the bounds of `simulateCapture`'s and the display view's `lens.ideal`, a plain aberration-free thin lens
8
+ named by its physical focal length, on any sensor. Its blur is that of the options the request gives, the same models as with
9
+ `lens.prime`: `lens.defocus` (the thin-lens blur circle, `calculateDefocusCircle`), `lens.diffraction` (the Airy pattern of a
10
+ circular pupil) and `lens.vignetting` (a caller profile). Without them it renders sharp and evenly lit.
11
+
12
+ - `fNumber.minimum` 0.5 is a physical limit: a lens in air that satisfies the Abbe sine condition (aplanatic) has
13
+ N = 1/(2·n·sin θ′) with n = 1 and sin θ′ ≤ 1, so N ≥ 0.5 (the sine condition: Born & Wolf, *Principles of Optics*, 7th ed.;
14
+ Kingslake & Johnson, *Lens Design Fundamentals*, 2nd ed.). The engine's thin-lens defocus and scalar Airy diffraction
15
+ are paraxial models, so near f/0.5 they are approximations further from a real lens than at f/2.
16
+ - `fNumber.maximum` 128 and both focal-length bounds are engine bounds, not physical limits: 1 mm is shorter than phone
17
+ camera lenses, 4000 mm longer than photographic lenses, and f/128 beyond large-format lenses' smallest stops.
18
+ - The ideal lens's f-number range is the whole of `fNumber`: manual and aperture priority take any `lens.fNumber` in it, and
19
+ shutter priority and program may choose any aperture in it (in dim light, wider than real lenses open).
20
+
21
+ ```ts
22
+ IDEAL_LENS_LIMITS = Object.freeze({
23
+ focalLengthMm: Object.freeze({ minimum: 1, maximum: 4000 }),
24
+ fNumber: Object.freeze({ minimum: 0.5, maximum: 128 })
25
+ })
26
+ ```
@@ -1,6 +1,6 @@
1
1
  {
2
- "packageVersion": "2.17.0",
3
- "engineApiVersion": "2.17.0",
2
+ "packageVersion": "2.18.0",
3
+ "engineApiVersion": "2.18.0",
4
4
  "exports": [
5
5
  {
6
6
  "name": "AbsoluteUncertaintyEstimate",
@@ -282,6 +282,16 @@
282
282
  "source": "src/sensor/local-exposure-binding.ts",
283
283
  "kind": "type"
284
284
  },
285
+ {
286
+ "name": "BlurShareOfFrame",
287
+ "source": "src/camera/sensor-equivalence.ts",
288
+ "kind": "type"
289
+ },
290
+ {
291
+ "name": "BlurShareOfPrint",
292
+ "source": "src/camera/sensor-equivalence.ts",
293
+ "kind": "type"
294
+ },
285
295
  {
286
296
  "name": "BOLTZMANN_CONSTANT_JOULES_PER_KELVIN",
287
297
  "source": "src/color/illuminants.ts",
@@ -367,6 +377,16 @@
367
377
  "source": "src/facade/chart-scenes.ts",
368
378
  "kind": "runtime"
369
379
  },
380
+ {
381
+ "name": "calculateBlurShareOfFrame",
382
+ "source": "src/camera/sensor-equivalence.ts",
383
+ "kind": "runtime"
384
+ },
385
+ {
386
+ "name": "CalculateBlurShareOfFrameInput",
387
+ "source": "src/camera/sensor-equivalence.ts",
388
+ "kind": "type"
389
+ },
370
390
  {
371
391
  "name": "calculateBrownConradyDistortionMapping",
372
392
  "source": "src/optics/brown-conrady-distortion.ts",
@@ -1287,6 +1307,16 @@
1287
1307
  "source": "src/sensor/eqe-temporal-exposure.ts",
1288
1308
  "kind": "type"
1289
1309
  },
1310
+ {
1311
+ "name": "calculateSensorEquivalence",
1312
+ "source": "src/camera/sensor-equivalence.ts",
1313
+ "kind": "runtime"
1314
+ },
1315
+ {
1316
+ "name": "CalculateSensorEquivalenceInput",
1317
+ "source": "src/camera/sensor-equivalence.ts",
1318
+ "kind": "type"
1319
+ },
1290
1320
  {
1291
1321
  "name": "calculateSensorFixedPatternSite",
1292
1322
  "source": "src/sensor/fixed-pattern-noise.ts",
@@ -2957,6 +2987,16 @@
2957
2987
  "source": "src/capture/environment-raw-producer.ts",
2958
2988
  "kind": "type"
2959
2989
  },
2990
+ {
2991
+ "name": "EquivalenceExactness",
2992
+ "source": "src/camera/sensor-equivalence.ts",
2993
+ "kind": "type"
2994
+ },
2995
+ {
2996
+ "name": "EquivalenceValueBasis",
2997
+ "source": "src/camera/sensor-equivalence.ts",
2998
+ "kind": "type"
2999
+ },
2960
3000
  {
2961
3001
  "name": "EquivalentFocalLength35Mm",
2962
3002
  "source": "src/camera/equivalent-focal-length.ts",
@@ -3892,6 +3932,11 @@
3892
3932
  "source": "src/facade/chart-scenes.ts",
3893
3933
  "kind": "type"
3894
3934
  },
3935
+ {
3936
+ "name": "IDEAL_LENS_LIMITS",
3937
+ "source": "src/equipment/ideal-lens.ts",
3938
+ "kind": "runtime"
3939
+ },
3895
3940
  {
3896
3941
  "name": "IdealApertureGeometry",
3897
3942
  "source": "src/optics/aperture.ts",
@@ -7827,6 +7872,21 @@
7827
7872
  "source": "src/sensor/eqe-electron-rate.ts",
7828
7873
  "kind": "type"
7829
7874
  },
7875
+ {
7876
+ "name": "SensorEquivalence",
7877
+ "source": "src/camera/sensor-equivalence.ts",
7878
+ "kind": "type"
7879
+ },
7880
+ {
7881
+ "name": "SensorEquivalenceFieldOfView",
7882
+ "source": "src/camera/sensor-equivalence.ts",
7883
+ "kind": "type"
7884
+ },
7885
+ {
7886
+ "name": "SensorEquivalenceQuantity",
7887
+ "source": "src/camera/sensor-equivalence.ts",
7888
+ "kind": "type"
7889
+ },
7830
7890
  {
7831
7891
  "name": "SensorExpectedReadoutSignal",
7832
7892
  "source": "src/sensor/raw-readout.ts",
@@ -8512,6 +8572,11 @@
8512
8572
  "source": "src/facade/simulate-capture.ts",
8513
8573
  "kind": "type"
8514
8574
  },
8575
+ {
8576
+ "name": "SimulateCaptureLensCatalogueRecord",
8577
+ "source": "src/facade/simulate-capture.ts",
8578
+ "kind": "type"
8579
+ },
8515
8580
  {
8516
8581
  "name": "SimulateCaptureLensDistortion",
8517
8582
  "source": "src/facade/simulate-capture.ts",
@@ -173,7 +173,7 @@ the record form with `fast` and is reported in `diagnostics.fast.applied` when i
173
173
  whether or not it has a fast method yet; one without runs exact and reports `false`.
174
174
 
175
175
  ```ts
176
- SIMULATE_CAPTURE_FAST_OPTIONS = Object.freeze(["lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"] as const)
176
+ SIMULATE_CAPTURE_FAST_OPTIONS = Object.freeze(["lens.diffraction", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"] as const)
177
177
  ```
178
178
 
179
179
  ## SIMULATE_CAPTURE_METERING_MODES
@@ -459,6 +459,14 @@ export interface SimulateCaptureFastReport {
459
459
  }
460
460
  ```
461
461
 
462
+ ## SimulateCaptureLensCatalogueRecord
463
+
464
+ `lens.catalogue` as a record (#568): the lens id as `kind` and the `fast` override (see {@link SimulateCaptureOptions.fast}).
465
+
466
+ ```ts
467
+ export interface SimulateCaptureLensCatalogueRecord { kind: string; fast?: boolean }
468
+ ```
469
+
462
470
  ## SimulateCaptureLensDistortion
463
471
 
464
472
  The exact contract is declared below; use the domain guide for assumptions and staged integration.
@@ -514,6 +522,18 @@ export interface SimulateCaptureOptions {
514
522
  * focus distance below its minimum focus distance, is rejected. Not combinable with `focalLengthMm`.
515
523
  */
516
524
  prime?: number;
525
+ /**
526
+ * Opt-in (#576): an ideal lens, an aberration-free thin lens of this physical focal length (1–4000 mm) on whatever sensor the
527
+ * frame is (output width × height × pixel pitch; no crop-factor scaling), at any `fNumber` from f/0.5 to f/128
528
+ * (`IDEAL_LENS_LIMITS`; beyond them the request is refused). It replaces the tier lens's focal length and f-number range (the
529
+ * transmission is kept); shutter priority and program may choose any aperture in that range. Recorded in the `lens.ideal`
530
+ * assumption. Its blur is that of `defocus`, `diffraction` and `vignetting`, the same models as with `prime`. It focuses at any
531
+ * distance beyond the focal length (no minimum focus distance; `subjectDistanceM` must be at least twice the focal length, as for
532
+ * every lens). Works in `simulateCapture` and the display view. Not combinable with `prime`, `catalogue` or `focalLengthMm`.
533
+ * Optional `widestFNumber` and `narrowestFNumber` (within `IDEAL_LENS_LIMITS`, widest ≤ narrowest; each defaults to its bound)
534
+ * narrow the range: `fNumber` must lie in it, and shutter priority and program choose apertures only in it. Default off.
535
+ */
536
+ ideal?: { focalLengthMm: number; widestFNumber?: number; narrowestFNumber?: number };
517
537
  /**
518
538
  * Opt-in (#549): a lens of the generic lens catalogue by id (`GENERIC_LENS_IDS`, for example "pv-50-1.2-pro" or "pv-50-1.8";
519
539
  * docs/LENS_CATALOGUE.md). Like `prime`, its focal length is the full-frame equivalent ÷ the simulated frame's diagonal crop
@@ -522,9 +542,12 @@ export interface SimulateCaptureOptions {
522
542
  * colour, at the resolved f-number and focus) through the environment PSF quadrature, its radial distortion, its vignetting
523
543
  * and, for the fisheyes, the equisolid projection. Flare, focus breathing and bokeh shape are not rendered. Not combinable
524
544
  * with `prime`, `focalLengthMm`, `projection`, `distortion`, `vignetting`, `psf`, `diffraction` or `pupilSampling` (the
525
- * lens sets them); `defocus` convolves the disc into the lens's PSF. Not offered by the display view. Default off.
545
+ * lens sets them); `defocus` convolves the disc into the lens's PSF. The display view renders it with the PSF taken per field zone (#569; docs/DISPLAY_VIEW.md). Default off.
546
+ * #568: the record form `{ kind: <lens id>, fast }` overrides the top-level `fast` flag for this option; the fast method
547
+ * blends the PSF between exactly resolved kernels on a field grid and tabulates each site's expected signal
548
+ * (docs/LENS_CATALOGUE.md, "Fast method").
526
549
  */
527
- catalogue?: string;
550
+ catalogue?: string | SimulateCaptureLensCatalogueRecord;
528
551
  /**
529
552
  * Opt-in (#549), with `catalogue` only: a seeded autofocus error (`simulateGenericLensFocusError`). The focus distance becomes
530
553
  * the sampled one for (seed, shotIndex), reported in the `lens.focusScatter` assumption; it is visible with `defocus`. Default off.
@@ -748,7 +771,7 @@ export interface SimulateCaptureOptions {
748
771
  * Exact or fast (#395). `false` or omitted: every option runs its exact method, the engine reference. `true`: each option
749
772
  * listed in {@link SIMULATE_CAPTURE_FAST_OPTIONS} runs its fast method where it has one, a measured, bounded approximation of
750
773
  * the exact one; an option without a fast method runs exact. An option's own record `fast` overrides this for that option
751
- * only. `diagnostics.fast` reports what was requested and what ran. No option has a fast method yet, so every result is exact.
774
+ * only. `diagnostics.fast` reports what was requested and what ran. `lens.catalogue` has a fast method (#568); the other options run exact.
752
775
  */
753
776
  fast?: boolean;
754
777
  /** Aborting cancels the running task; the promise rejects with a domain error and returns no partial image. */
package/llms.txt CHANGED
@@ -24,6 +24,8 @@
24
24
  - [resolveGenericLens()](https://github.com/Photivra/photivra/blob/main/docs/api/equipment-generic-lens-catalogue.md#resolvegenericlens): one of the 30 generic catalogue lenses ("PV 50 1.2 Pro") on a sensor, with its approximated optical character; `simulateCapture` renders it with `lens.catalogue`
25
25
  - [calculateFieldOfView()](https://github.com/Photivra/photivra/blob/main/docs/api/camera-field-of-view.md#calculatefieldofview): angle of view from focal length and sensor size
26
26
  - [calculateEquivalentFocalLength35Mm()](https://github.com/Photivra/photivra/blob/main/docs/api/camera-equivalent-focal-length.md#calculateequivalentfocallength35mm): crop factor and 35 mm-equivalent focal length
27
+ - [calculateSensorEquivalence()](https://github.com/Photivra/photivra/blob/main/docs/api/camera-sensor-equivalence.md#calculatesensorequivalence): equivalent focal length and f-number, sensor area ratio and angles of view of a lens on one sensor, each marked exact or approximated
28
+ - [calculateBlurShareOfFrame()](https://github.com/Photivra/photivra/blob/main/docs/api/camera-sensor-equivalence.md#calculateblurshareofframe): a blur circle as a share of the frame diagonal, or its size on a print
27
29
  - [calculateCenteredCrop()](https://github.com/Photivra/photivra/blob/main/docs/api/output-crop.md#calculatecenteredcrop): pixels left after a centred crop
28
30
  - [calculateProjectedMotionBlur()](https://github.com/Photivra/photivra/blob/main/docs/api/motion-projected-motion.md#calculateprojectedmotionblur): how far a moving subject smears on the sensor during the shutter time
29
31
  - [calculateAiryDisk()](https://github.com/Photivra/photivra/blob/main/docs/api/optics-diffraction.md#calculateairydisk): diffraction spot size from f-number and wavelength
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photivra/engine",
3
- "version": "2.17.0",
3
+ "version": "2.18.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",