@photivra/engine 2.16.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 (107) hide show
  1. package/CHANGELOG.md +104 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +24 -0
  6. package/dist/camera/lens-projection.d.ts +5 -0
  7. package/dist/camera/lens-projection.js +10 -0
  8. package/dist/camera/lens-projection.js.map +1 -1
  9. package/dist/camera/sensor-equivalence.d.ts +156 -0
  10. package/dist/camera/sensor-equivalence.js +208 -0
  11. package/dist/camera/sensor-equivalence.js.map +1 -0
  12. package/dist/capture/native-environment-tabulated.d.ts +5 -0
  13. package/dist/capture/native-environment-tabulated.js +1 -1
  14. package/dist/capture/native-environment-tabulated.js.map +1 -1
  15. package/dist/core/version.d.ts +1 -1
  16. package/dist/core/version.js +1 -1
  17. package/dist/core/version.js.map +1 -1
  18. package/dist/equipment/generic-lens-catalogue.d.ts +227 -0
  19. package/dist/equipment/generic-lens-catalogue.js +243 -0
  20. package/dist/equipment/generic-lens-catalogue.js.map +1 -0
  21. package/dist/equipment/ideal-lens.d.ts +25 -0
  22. package/dist/equipment/ideal-lens.js +21 -0
  23. package/dist/equipment/ideal-lens.js.map +1 -0
  24. package/dist/facade/catalogue-lens-signal.d.ts +94 -0
  25. package/dist/facade/catalogue-lens-signal.js +362 -0
  26. package/dist/facade/catalogue-lens-signal.js.map +1 -0
  27. package/dist/facade/defocus-bokeh-zones.d.ts +33 -0
  28. package/dist/facade/defocus-bokeh-zones.js +53 -0
  29. package/dist/facade/defocus-bokeh-zones.js.map +1 -0
  30. package/dist/facade/defocus-image-space-provider.d.ts +9 -0
  31. package/dist/facade/defocus-image-space-provider.js +101 -9
  32. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  33. package/dist/facade/diffraction-provider.d.ts +29 -0
  34. package/dist/facade/diffraction-provider.js +117 -11
  35. package/dist/facade/diffraction-provider.js.map +1 -1
  36. package/dist/facade/display-view-gpu-session.js +4 -0
  37. package/dist/facade/display-view-gpu-session.js.map +1 -1
  38. package/dist/facade/display-view-gpu.d.ts +10 -0
  39. package/dist/facade/display-view-gpu.js.map +1 -1
  40. package/dist/facade/display-view-lens-signal.d.ts +61 -0
  41. package/dist/facade/display-view-lens-signal.js +502 -0
  42. package/dist/facade/display-view-lens-signal.js.map +1 -0
  43. package/dist/facade/expected-signal-setup.d.ts +15 -0
  44. package/dist/facade/expected-signal-setup.js +19 -2
  45. package/dist/facade/expected-signal-setup.js.map +1 -1
  46. package/dist/facade/expected-signal.d.ts +13 -0
  47. package/dist/facade/expected-signal.js +155 -1
  48. package/dist/facade/expected-signal.js.map +1 -1
  49. package/dist/facade/generic-lens-capture.d.ts +7 -0
  50. package/dist/facade/generic-lens-capture.js +9 -0
  51. package/dist/facade/generic-lens-capture.js.map +1 -0
  52. package/dist/facade/large-defocus-lattice.d.ts +39 -0
  53. package/dist/facade/large-defocus-lattice.js +64 -0
  54. package/dist/facade/large-defocus-lattice.js.map +1 -0
  55. package/dist/facade/scene-radiance.d.ts +5 -0
  56. package/dist/facade/scene-radiance.js +5 -2
  57. package/dist/facade/scene-radiance.js.map +1 -1
  58. package/dist/facade/simulate-capture.d.ts +67 -7
  59. package/dist/facade/simulate-capture.js +400 -64
  60. package/dist/facade/simulate-capture.js.map +1 -1
  61. package/dist/facade/simulate-display-view.js +23 -17
  62. package/dist/facade/simulate-display-view.js.map +1 -1
  63. package/dist/gpu/display-view-quality-gpu.d.ts +0 -9
  64. package/dist/gpu/display-view-quality-gpu.js +6 -0
  65. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  66. package/dist/gpu/display-view-speed-gpu.js +2 -0
  67. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  68. package/dist/index.d.ts +6 -1
  69. package/dist/index.js +5 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/optics/defocus-bokeh-kernel.d.ts +207 -0
  72. package/dist/optics/defocus-bokeh-kernel.js +380 -0
  73. package/dist/optics/defocus-bokeh-kernel.js.map +1 -0
  74. package/dist/optics/defocus-disc-kernel.d.ts +41 -19
  75. package/dist/optics/defocus-disc-kernel.js +44 -23
  76. package/dist/optics/defocus-disc-kernel.js.map +1 -1
  77. package/dist/optics/generic-lens-psf.d.ts +68 -0
  78. package/dist/optics/generic-lens-psf.js +337 -0
  79. package/dist/optics/generic-lens-psf.js.map +1 -0
  80. package/dist/optics/illumination-vignetting.d.ts +6 -0
  81. package/dist/optics/illumination-vignetting.js +24 -7
  82. package/dist/optics/illumination-vignetting.js.map +1 -1
  83. package/dist/optics/sensor-environment-query.d.ts +6 -0
  84. package/dist/optics/sensor-environment-query.js +13 -6
  85. package/dist/optics/sensor-environment-query.js.map +1 -1
  86. package/dist/optics/site-kernel-runs.d.ts +77 -0
  87. package/dist/optics/site-kernel-runs.js +200 -0
  88. package/dist/optics/site-kernel-runs.js.map +1 -0
  89. package/dist/output/generic-lens-corrections.d.ts +24 -0
  90. package/dist/output/generic-lens-corrections.js +51 -0
  91. package/dist/output/generic-lens-corrections.js.map +1 -0
  92. package/docs/AGENT_GUIDE.md +4 -0
  93. package/docs/API_REFERENCE.md +77 -1
  94. package/docs/QUICK_START.md +305 -17
  95. package/docs/USAGE.md +98 -5
  96. package/docs/api/camera-sensor-equivalence.md +209 -0
  97. package/docs/api/core-version.md +1 -1
  98. package/docs/api/equipment-generic-lens-catalogue.md +281 -0
  99. package/docs/api/equipment-ideal-lens.md +26 -0
  100. package/docs/api/exports.json +202 -2
  101. package/docs/api/facade-simulate-capture.md +48 -4
  102. package/docs/api/optics-defocus-bokeh-kernel.md +145 -0
  103. package/docs/api/optics-defocus-disc-kernel.md +22 -15
  104. package/docs/api/optics-sensor-environment-query.md +6 -0
  105. package/docs/api/output-generic-lens-corrections.md +39 -0
  106. package/llms.txt +3 -0
  107. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,109 @@
1
1
  # Changelog
2
2
 
3
- ## 2.16.0 — unreleased (prepared November 7, 2026)
3
+ ## 2.18.0 — unreleased (prepared November 9, 2026)
4
+
5
+ A minor release of four issues: equivalence numbers for teaching (#573), an ideal lens at any physical focal length and f-number (#576), catalogue lenses in the display view (#569) and fast catalogue lens captures (#568). Captures, RAW codes, files and assumptions are the same as 2.17.0 for every request that 2.17.0 renders and that uses none of the new options (every hash lock holds).
6
+ - **New:** `calculateSensorEquivalence` and `calculateBlurShareOfFrame` (#573); `lens.ideal: { focalLengthMm, widestFNumber?, narrowestFNumber? }` and `IDEAL_LENS_LIMITS` (#576; the optional f-number range added at the owner's request, 2026-11-09); `simulateDisplayView` and `prepareDisplayView` render `lens.catalogue` (#569); a fast method for `lens.catalogue` behind `fast` or `lens.catalogue: { kind, fast }` (#568).
7
+ - **Fixes** (requests that 2.17.0 refused): `subjectDistanceM` may equal twice the focal length, so the Macro's 1:1 close focus renders (#569); a lens whose range excludes f/8 (*PV 800 9*, f/9–f/32) takes the reference exposure at the aperture of its range nearest f/8 with the same N²/t, where every request with it failed (#569).
8
+ - **Root exports:** 13 new, 1,837 → 1,850 (runtime exports 623 → 626): 11 from #573 (2 runtime), 1 from #576 (runtime), 1 from #568 (type). #569 adds none.
9
+ - **Why minor:** new root exports and new opt-in options; requests that 2.17.0 refused now render (the display view with `lens.catalogue`, a subject at twice the focal length, *PV 800 9*). Nothing is removed or changed incompatibly, and no default changes. No `src/gpu` change.
10
+ - **Bundle:** the app's capture-worker shape (every chunk of `NATIVE_RAW_LIMITS`, `prepareDisplayView`, `serializeSimulatedCaptureDngFromParts`, `simulateCapture`, `simulateIdealReference`) is 351,859 B gzip (2.17.0: 347,775 B): #568's lazily loaded fast catalogue code +2,907 B, #569's display-view catalogue PSF +550 B, the entry +397 B (the three parts' option handling), the ideal lens's f-number range +147 B. The integration saves 19 B (one closure for the tabulated paths' tables; unused grid axes removed). `test/capture-bundle.test.ts`'s limit is raised from 348,000 to 352,000 B (owner-approved 2026-11-09 for engine 2.18.0, #568's fast catalogue chunk). The display view adds 45,807 B minified and 18,128 B gzip to a capture bundle (2.17.0: 45,791 B and 18,159 B; budgets 46,500 B and 18,550 B, unchanged).
11
+ - **Moved to 2.19.0:** #570, #571 and #566 (the owner's decision of 2026-11-09 to publish these four parts now; #566's breaking orientation fix, decided for 2.18.0 on 2026-11-07, moves with them).
12
+
13
+ ### #573 Equivalence for teaching: equivalent f-number, sensor area ratio and blur as a share of the frame
14
+
15
+ - **New: `calculateSensorEquivalence({ sensor, focalLengthMm, fNumber, reference? })`** (model `sensor-equivalence` 1.0.0): for a lens on one sensor against 36 × 24 mm (or a `reference`), the crop factor, equivalent focal length, equivalent f-number (k·N, for depth of field, background blur and total light at the same framing and output size), the unchanged exposure f-number, entrance-pupil diameter, sensor area ratio, total-light difference in stops at the same f-number and shutter, and horizontal, vertical and diagonal angles of view. A `basis` record states, for every value, `"exact"` or `"approximation"` and its conditions; the equivalent f-number is a thin-lens approximation, exact only when the focus distance is much greater than the focal length (the exact thin-lens factor k·N·(s − f)/(s − k·f) is stated and tested). 50 mm f/2 on 23.5 × 15.6 mm gives ≈ 76.7 mm and ≈ f/3.07. With the default reference the crop factor and equivalent focal length are bit-identical to `calculateEquivalentFocalLength35Mm`.
16
+ - **New: `calculateBlurShareOfFrame({ blurCircleMm, sensor, print? })`** (model `blur-share-of-frame` 1.0.0): a blur-circle diameter as a share of the frame diagonal, width and height, and with a print size, the enlargement (whole frame fitted, long side to long side) and the blur on the print.
17
+ - **Root exports:** 11 new (2 runtime): the two functions and the types `CalculateSensorEquivalenceInput`, `SensorEquivalence`, `SensorEquivalenceFieldOfView`, `SensorEquivalenceQuantity`, `EquivalenceExactness`, `EquivalenceValueBasis`, `CalculateBlurShareOfFrameInput`, `BlurShareOfFrame`, `BlurShareOfPrint`. Pure functions; no capture, RAW or bundle change.
18
+ - **Docs and tests:** docs/PHYSICS_FOUNDATION.md ("Sensor equivalence: f-number, area and blur share"), docs/USAGE.md, docs/AGENT_GUIDE.md, llms.txt, API reference task index; `test/sensor-equivalence.test.ts` (hand-checked values, the thin-lens check of the approximation, validation).
19
+
20
+ ### #576 Ideal lens at any physical focal length and f-number (capture and display view)
21
+
22
+ - **New: `lens.ideal: { focalLengthMm }` (#576, for Photivra/apps#246, #250 and #251).** An ideal lens, an aberration-free thin lens named by its physical focal length, on whatever sensor the simulated frame is (no crop-factor scaling, unlike `lens.prime` and `lens.catalogue`), at any `lens.fNumber` from f/0.5 to f/128 and focal lengths of 1–4000 mm. f/0.5 is the physical limit of an aplanatic lens in air (the sine condition, N = 1/(2·sin θ′)); the other bounds are engine bounds. Values beyond them are refused by name (`lens.ideal.focalLengthMm must be 1–4000 mm (IDEAL_LENS_LIMITS).`, `lens.fNumber 0.45 is outside the ideal-50mm range f/0.5–f/128.`).
23
+ - **Model:** it replaces the tier lens's focal length and f-number range, as `lens.prime` does (the tier lens's transmission is kept); its blur is that of `lens.defocus`, `lens.diffraction` and `lens.vignetting`, the same models. No minimum focus distance beyond the thin-lens limits (subject at least twice the focal length away, with #569). Shutter priority and program may choose any aperture in f/0.5–f/128. Recorded in the new `lens.ideal` assumption (`{ lensId: "ideal-50mm", focalLengthMm: 50 }`).
24
+ - **Optional f-number range (owner, 2026-11-09):** `lens.ideal.widestFNumber` and `narrowestFNumber`, within `IDEAL_LENS_LIMITS` and widest ≤ narrowest (each defaults to its bound), narrow the range like a real lens's: `lens.fNumber` must lie in it and shutter priority chooses apertures only in it (1/4000 s, ISO 100 in 2000 lx: wider than f/1.4 over the whole range, f/1.4 with `widestFNumber: 1.4`). Program follows the engine's program line (f/4–f/16) and refuses a range that does not hold it, as for `lens.prime` and `lens.catalogue`. The assumption records the range only when given, so requests without it are unchanged. Refused: `lens.ideal.widestFNumber and narrowestFNumber must be f-numbers in f/0.5–f/128 (IDEAL_LENS_LIMITS), the widest no larger than the narrowest.`
25
+ - **Where:** `simulateCapture`, `simulateDisplayView` and `prepareDisplayView` (it goes through the shared request path). Not combinable with `lens.prime`, `lens.catalogue` or `lens.focalLengthMm`.
26
+ - **Tests** (`test/ideal-lens.test.ts`): 50 mm f/1.8 focused at 2.5 m with an edge at 3 m on full frame (7200 × 4800) and APS-C 23.5 × 15.6 mm (4700 × 3120) at 5 µm: the blur is `calculateDefocusCircle`'s 94.48 µm on both; the display view of the defocused edge equals the view of the disc's closed-form edge-spread function within 2.2e-14, and the capture equals the capture of that closed form byte for byte. Same pixels as the tier lens (`lens.focalLengthMm: 50`, f/4; with defocus and diffraction) and as the 50 mm-equivalent prime on a 36 × 24 mm frame at f/1.8. Bounds, every exposure mode and refusals.
27
+ - **API:** new root export `IDEAL_LENS_LIMITS` (runtime). `SimulateCaptureOptions.lens` gains `ideal`. The f-number range check of `lens.prime` and `lens.catalogue` now shares one code path (same messages).
28
+ - **Compatibility:** requests without `lens.ideal` give the same captures, RAW codes, files and assumptions. No `src/gpu` change.
29
+ - **Docs:** docs/QUICK_START.md ("Ideal lens at any focal length and f-number"), docs/USAGE.md, docs/AGENT_GUIDE.md; API reference regenerated.
30
+
31
+ ### #569 Display view: render catalogue lenses
32
+
33
+ - **New: the display view renders `lens.catalogue` (#569, for Photivra/apps#251 and #252).** `simulateDisplayView` and `prepareDisplayView` accept the 30 generic catalogue lenses at every f-number of each lens's range and down to its close focus, with `lens.defocus` and `lens.focusScatter`, where 2.17.0 refused `lens.catalogue`. The lens's distortion, vignetting and fisheye projection go through #563's lens signal; its PSF is taken per field zone.
34
+ - **Model (an approximation, the new `view.lensPsf` assumption):** the native raster is cut into square zones of ⌈max(W, H) / 64⌉ sites (94 on 6000 × 4000; one site on frames of at most 64 sites a side), and every site of a zone takes the PSF the capture's own resolver gives at the zone's centre, at each spectral node; every tap reads the scene through the lens with its own field factor, as the capture's environment PSF quadrature does. Catalogue fisheyes see zero radiance beyond 90°, as in the capture. `createDisplayViewLensPsf` in `src/facade/display-view-lens-signal.ts`, loaded on demand; the zones' tables are the expected signal's own (`buildTables`, now exported internally).
35
+ - **Accuracy** (per site, against the capture's own site record and PSF quadrature): ≤ 2e-15 with one-site zones; 2.5e-3 with the box aperture, four-spot filter and defocus (the capture resolves the PSF at each query point); on a whole 6 µm full frame at most 0.66 % per site and 1.2e-4 on average over four hard lenses; all 30 lenses at their widest and narrowest f-number at close focus within 1 %. A slanted edge against the box-downsized capture is within the rectilinear test's 0.6-code RMS and 3-code maximum. Siemens-star boundary ties behave as with `lens.diffraction` (#565).
36
+ - **Cost** on the app's request (24 MP, 2028 × 1352 in 96 × 96 parts, one thread, loaded host): part medians 0.2–0.8 s for most lenses (1.9 s for the Macro at f/22) against 50–80 ms without a lens; 1.9–39× the no-lens view per display pixel (docs/DISPLAY_VIEW.md, "Catalogue lenses"). The CPU renders these requests; `gpu: "auto"` falls back, labelled `unsupported-on-backend`. `scripts/benchmark-display-view.mjs` gains `LENS=catalogue:<id>[:open|:<f-number>]`.
37
+ - **Capture fixes** (only requests that failed before change): `subjectDistanceM` may equal twice the focal length (the Macro's 1:1 close focus was refused); a lens whose range excludes f/8 (*PV 800 9*) takes the reference exposure at the aperture of its range nearest f/8 (its widest for *PV 800 9*) with the same N²/t, where every request with it failed.
38
+ - **Unchanged:** every request without `lens.catalogue` gives the same bytes; no root export is added; no `src/gpu` change. `lensGeometry`'s image-space mapping (#548) now honours `outsideImageCircle: "backward-ray"`, which only catalogue fisheyes set.
39
+ - **Docs and tests:** docs/DISPLAY_VIEW.md ("Catalogue lenses"), docs/LENS_CATALOGUE.md, docs/QUICK_START.md; `test/display-view-lens-catalogue.test.ts`.
40
+
41
+ ### #568 Fast catalogue lens captures for interactive use
42
+
43
+ - **New: a fast method for `lens.catalogue` (#568, for Photivra/apps#251).** `fast: true` on a `simulateCapture` request, or the record form `lens: { catalogue: { kind: "<lens id>", fast }, fNumber }` (whose `fast` overrides the top-level flag), renders a catalogue lens from tabulated expected electrons instead of the environment PSF quadrature's per-tap scene queries. It is the first option with a fast method under the #395 convention: `lens.catalogue` joins `SIMULATE_CAPTURE_FAST_OPTIONS`, and without the flag the exact method runs unchanged.
44
+ - **Approximation (labeled):** the catalogue PSF is resolved exactly on a field grid (at least 4 cells between neighboring profile field nodes, more where the node kernels' centroids move by over 0.05 site per cell) and each site takes the bilinear blend of its four grid points' kernels instead of its own; distortion, vignetting and the fisheye projection are the exact method's. Reported in `diagnostics.fast` (`applied: { "lens.catalogue": true }`, `assurance: "approximation-with-a-priori-bound"`), the new `fast.lens.catalogue` assumption and `raw.expectedSignal` (method `tabulated-expected-electrons-catalogue-psf-field-grid`). One site per CFA phase runs the exact chain at run time and must agree within 0.5 %, or the exact method runs.
45
+ - **Accuracy against the exact method:** without the blend the sum equals the exact chain's within 1e-9; with it, site by site within 2.3e-4 of the frame's peak and 2.0e-3 of the site's own value (tested bounds 1e-3 and 5e-3); on the #549 charts the edge rise within 0.010 px, coma within 0.0001 px, lateral color within 0.003 px, distortion and vignetting unchanged (tested bounds 0.02 px and 0.005 stops).
46
+ - **Cost** (Apple M5 Pro, Node 24.19.0, one thread; Siemens star, whole full frame, f/8, PV 50 1.8 against `lens.prime` 50): 768 × 512 renders in 0.209 s against 0.145 s (1.44×; the issue's target was 2×) and 185 s with the exact method; 384 × 256 in 0.084 s (exact 47.5 s). `node scripts/benchmark-catalogue-capture.mjs` reproduces it (docs/LENS_CATALOGUE.md, "Fast method").
47
+ - **Where:** a still camera, site-center queries (no anti-aliasing filter or box pixel aperture) and a chart or material scene, in whole frames and regions, with or without `lens.defocus`; elsewhere the exact method runs and `applied` is `false`. The display view renders `lens.catalogue` with #569's per-zone PSF and never takes the fast method (`applied` is `false` there).
48
+ - **API:** one new root type export, `SimulateCaptureLensCatalogueRecord`; `SimulateCaptureOptions.lens.catalogue` also takes the record; `SIMULATE_CAPTURE_FAST_OPTIONS` gains `lens.catalogue`. Internal: `renderTabulatedNativeRaw` takes an optional verification tolerance.
49
+ - **Compatibility:** every request without `fast: true` (or the record's `fast`) renders byte-identically to 2.17.0. A request that already passed `fast: true` with `lens.catalogue` now takes the fast method.
50
+
51
+ ## 2.17.0 — published November 8, 2026
52
+
53
+ A minor release of four issues: large defocus discs (#564), bokeh character (#553), lens projection, distortion, vignetting and rotation in the display view (#563), and the generic lens catalogue (#549). Captures, RAW codes, files and assumptions are the same as 2.16.0 for every request that 2.16.0 renders and that uses none of the new options (every hash lock holds).
54
+ - **New:** `lens.defocus` up to a kernel radius of 512 sites (#564); `lens.defocus.blades`, `sphericalAberration` and `catsEye` and `calculateDefocusBokehKernel` (#553); the display view with `lens.projection`, `lens.distortion`, `lens.vignetting` and global-shutter rotation (#563); `lens.catalogue`, `lens.focusScatter` and the 30-lens `GENERIC_LENS_CATALOGUE`, each lens with a short `summary` (#549).
55
+ - **Root exports:** 27 new, 1,810 → 1,837 (runtime exports 614 → 623): 9 from #553 (2 runtime) and 18 from #549 (7 runtime). #564 and #563 add none.
56
+ - **Why minor:** new root exports and new opt-in options; requests that 2.16.0 refused (discs above 64 sites, the display view with lens options) now render. Nothing is removed or changed incompatibly, and no default changes.
57
+ - **GPU:** #564 changes `src/gpu` (the display view's renderers refuse a disc above 64 sites, so it renders on the CPU, labelled); the hardware gates are in docs/RELEASE_2_17_0.md and docs/WEBGPU_DEVICES.md.
58
+ - **Bundle:** the display view adds 45,791 B minified and 18,159 B gzip to a capture bundle (2.16.0: 46,380 B and 18,540 B; budgets 46,500 B and 18,550 B, unchanged).
59
+ - **Follow-ups filed:** #565 (in-focus diffraction quadrature and chart-boundary ties), #566 (a PSF mode mirrors asymmetric PSFs; owner decision 2026-11-07: fixed in 2.18.0 as a breaking fix), #567 (catalogue character into bokeh).
60
+
61
+ - **New: large defocus discs (#564).** `lens.defocus` now renders blur discs up to a kernel radius of 512 sites (1,025 sites across, 6.15 mm at 6 µm) instead of 64, in `simulateCapture` (object-space, image-space and tabulated paths) and in `simulateDisplayView` / `prepareDisplayView`. The owner's case, 50 mm f/1.4 on 6 µm full frame focused at infinity with the subject at 0.83 m (a 2,144 µm blur, 357 sites, radius 179), renders where 2.16.0 refused it.
62
+ - **Method:** above 64 sites each kernel row is split into runs of equal weights, and a run's sum is a difference of two prefix sums of the source row (summed-area tables, Crow 1984; Heckbert 1986). The disc's interior cells share one weight, so it costs about 12·R reads per output instead of (2R + 1)² (2,149 against 101,689 at R = 179). It is the same sum regrouped, not an approximation: within 2.5e-14 of a compensated sum on the Siemens star, slanted edge and dead leaves at R = 65, 179 and 512, where the direct gather was up to 9.9e-12 off. Fixed tiles at absolute positions keep regions, parts and DNG parts bit-identical to the whole frame. The tabulated expected signal applies the disc once per channel to the in-focus signal (defocus commutes with the spectral, offset and diffraction sums); with `lens.diffraction` the providers apply each node's diffraction kernel after the disc.
63
+ - **Cost** (Apple M5 Pro, Node 24.19.0, shared machine): a 1024 × 683 Siemens-star capture at 6 µm took 0.72–0.77 s at R = 65, 1.6–1.7 s at R = 179 and 5.4–5.7 s at R = 512 (peak memory 216–457 MiB), against 5.7–6.8 s at R = 64 (direct). The app's display view (24 MP, 2028 × 1352, Speed) extrapolates to 98 s at R = 179 and 296 s at R = 512 on one thread, against 431 s at R = 64. `node scripts/benchmark-large-defocus.mjs` reproduces these.
64
+ - **Unchanged:** every request with a disc radius of 64 sites or less renders byte-identically to 2.16.0 (the direct convolution, the same assumptions; all hash locks hold).
65
+ - **API:** `DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES` is 512 (was 64). `calculateDefocusDiscKernelRadius` (model 1.1.0) reports the new limit and gains `convolution` (`"direct"`, `"runs"`, or null past the limit). The `lens.defocus` and `view.defocus` assumptions gain `convolution: "runs"` above 64 sites only. No root export is added or removed.
66
+ - **Errors:** a blur past the limit fails with a short message, for example `lens.defocus: The blur circle is 7.81 mm (1,303 sites) across; the engine renders at most 1,025 sites (DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES).`
67
+ - **GPU (`src/gpu`):** the display view's Speed and Quality renderers refuse a disc above 64 sites (a new `DisplayViewGpuRequest.largeDefocus` and a render-time check), so `gpu: "auto"` renders it on the CPU, labelled `unsupported-on-backend`. In-focus and ≤ 64-site requests reach the GPU as before; no kernel or WGSL changed.
68
+ - **Bokeh (#553):** the run convolution takes the shaped kernel when bokeh options are given (blade polygons share the disc's uniform interior). A kernel needing more than 16,641 reads per site (spherical aberration's ray binning, above 64 sites) is refused with `lens.defocus: this blur needs … reads per site; above a kernel radius of 64 sites the limit is 16,641. Reduce the spherical aberration or the defocus.` A cat's eye above 64 sites is refused (this path takes one kernel per request). See "Integration" below.
69
+ - **Docs:** docs/QUICK_START.md ("Large defocus discs"), docs/DISPLAY_VIEW.md, docs/USAGE.md, docs/WEBGPU.md; API reference regenerated. Tests: `test/site-kernel-runs.test.ts`, `test/large-defocus.test.ts`, and updated limit tests.
70
+ - **New: bokeh character, the defocus kernel shaped by the lens (#553, for #549).** `lens.defocus` takes three optional parts, each off by default: `blades` `{ count, firstBladeEdgeAngleDegrees?, curvature?, circularAtOrBelowFNumber? }` (a regular or rounded-blade polygon, scaled to the f/N circle's area), `sphericalAberration` `{ w040Micrometers, atFNumber }` (primary W040, scaled to the working f-number as (N₀/N)⁴) and `catsEye` `{ apertures: [{ radiusMm, shiftPerTanFieldMm }], fieldZonesAcrossLongSide? }` (mechanical vignetting, `calculateMechanicalVignetting`'s model).
71
+ - **Model** (a geometric approximation, labelled in the `lens.defocus` assumption's new `bokeh` record): the open pupil projected onto the sensor by the ray map (σ·c/2 + b·ρ²)·P, with c the thin-lens blur diameter, σ = +1 behind the focus plane and −1 in front, and b = 8·N·W040/pitch the transverse ray aberration (Wyant & Creath 1992, free). Front and back blurs are the same pupil turned through 180°; with undercorrected spherical aberration the blur behind the focus plane is soft-edged and the one in front bright-rimmed (reversed when overcorrected). Without spherical aberration each weight is the exact polygon area in the site's cell; with it, rays are binned per cell. Bit-identical in every JavaScript engine.
72
+ - **Where:** `simulateCapture` (tabulated, object-space and image-space paths) renders all three; the display view (CPU and GPU) renders `blades` and `sphericalAberration`. Above a kernel radius of 64 sites the shaped kernel takes #564's run path (CPU only); a shaped kernel past 512 sites is refused with #564's short message. The cat's eye is applied per square field zone (a piecewise-constant approximation whose largest step is reported), leaves the tabulated fast path, does not apply the light it removes (declare it with `lens.vignetting`), and is refused with `lens.diffraction` and in the display view.
73
+ - **API:** new root exports `calculateDefocusBokehKernel`, `DEFOCUS_BOKEH_MAXIMUM_BLADES` and the types `CalculateDefocusBokehKernelInput`, `DefocusBokehKernel`, `DefocusBokehOptions`, `DefocusBokehBlades`, `DefocusBokehSphericalAberration`, `DefocusBokehCatsEye`, `DefocusBokehClippingAperture` (9 new, 2 runtime). `SimulateCaptureOptions.lens.defocus` gains the three optional fields.
74
+ - **Compatibility:** requests without the new fields, and in-focus requests with them, give the same captures, RAW codes, files and assumptions (`lens.defocus` keeps the exact disc). No `src/gpu` change.
75
+ - **Tests:** `test/defocus-bokeh-kernel.test.ts` (polygon area and shape against a supersampled closed-form polygon; cat's eye against `calculateMechanicalVignetting`'s exact area at 0–25°; the spherical-aberration encircled energy and rim profile against the analytic ray map; front against back) and `test/simulate-capture-defocus-bokeh.test.ts` (visible on the point grid out of focus; cat's eye by field zone; byte-identity; regions; validation; the GPU display view's float32 emulation). Numbers and cost: docs/QUICK_START.md, "Bokeh character".
76
+ - **Display view: lens projection, distortion, vignetting and camera rotation (#563, for Photivra/apps#252; with #548, #549 and #553).** `simulateDisplayView` and `prepareDisplayView` now render `lens.projection`, `lens.distortion`, `lens.vignetting` and camera rotation (`motion.angularVelocityRadPerSec`, global shutter), in focus and with `lens.defocus` out of focus, the combinations `simulateCapture` renders since #548. Until now they refused them in any focus.
77
+ - **Model:** each site's expected electrons are the capture environment chain's sum, μ_c = Σ_o w_o·(1/T)Σ_t Σ_λ A_c(λ) Σ_k K_λ(k)·F(s)·L(D_t(s), λ): the query point's (or, with diffraction or defocus, each lattice source point's) scene ray through the inverse distortion, the projection and the rotation at each temporal midpoint, times its field factor (vignetting × distortion area factor), with the view's own combined kernels and material tables. In focus it is the environment's per-query sum (with `lens.diffraction`, its PSF quadrature); out of focus it is #548's image-space disc, and the view records the capture's `lens.defocus` assumption. The estimators, noise, AHD, development and highlight count are unchanged (`src/facade/display-view-lens-signal.ts`, loaded on demand; docs/DISPLAY_VIEW.md, "Lens projection, distortion, vignetting and rotation").
78
+ - **Accuracy:** every site's expected electrons equal the capture's within 1e-9 relative (the rectilinear view's tolerance) in eight in-focus and two out-of-focus combinations. Exception: with `lens.diffraction` in focus, a lattice source point exactly on a chart boundary (a Siemens star's diagonals through a symmetric frame's centre) is rounded as the lattice rounds it, as the rectilinear view and the out-of-focus capture do; the in-focus capture's PSF quadrature can resolve it to the other material, as it does against its own rectilinear path. Against box-downsized captures (a slanted edge through distortion and vignetting, in and out of focus) within the rectilinear test's 0.6-code RMS and 3-code maximum. On 6000 × 4000 blocks at the frame's corner (4 real pixels per display pixel, a checkerboard, ISO 100), the view's expected value differs from the three-seed capture mean by 0.25–0.29 codes RMS (at most 1.36), no more than the reference's own noise (0.30–0.34), and by at most 0.04 code in each channel's mean, over Speed and Quality with distortion and vignetting, a fisheye, rotation (Speed) and defocus.
79
+ - **Still refused:** rotation with `motion.shutter: "rolling"`, and a mapping or vignetting profile that does not reach the outer site centres plus the kernel radius and the largest query offset (the message names the reach).
80
+ - **GPU:** these requests stay on the CPU reference: `gpu: "auto"` falls back with `unsupported-on-backend` before any device is requested (the limitation names #563), `"require"` fails with it. `src/gpu` is unchanged, so no new hardware evidence is needed.
81
+ - **Cost:** on the app request (2028 × 1352 in 96 × 96 parts, f/8, Apple M5 Pro under a load average of 10–33, so ratios are the measure), µs per display pixel against the rectilinear view: Speed 5.7–6.3 with vignetting (1.2×), 7.2–7.8 with an equisolid fisheye (1.5×), 18.2–19.3 with distortion and vignetting (3.8×, the capture's own inverse-distortion bisection per point), 30.4–32.7 with rotation at eight midpoints (6.5×), against 4.6–5.2; Quality 41.8–44.1 (5.2×), 37.5 (4.5×) and 54.8–56.5 (6.7×) against 8.1–8.4 (every kernel tap carries its own field factor, so the one-material shortcut does not apply). Quality with rotation is refused by the capture's own limit of 100 000 scene queries per site. Prepare times unchanged.
82
+ - **Bundle:** the display view's addition to a capture bundle is 46,341 B minified and 18,527 B gzip, down from 46,380 B and 18,540 B (budget 46,500 B and 18,550 B, unchanged): a simpler Kelvin white-balance branch and one view-region expression in `simulate-display-view.ts` pay for the lens hook. The lens module is a separate chunk (4,380 B, 2,173 B gzip) loaded only for these options; the capture-only entry grows by 642 B (212 B gzip) for the request setup in `runCapture`, and a bundler emits that chunk there too, never loaded.
83
+ - **Internal:** `prepareIlluminationVignettingThroughput` (not exported from the package) evaluates `calculateIlluminationVignetting`'s throughput with the profile validated once; #548's image-space geometry uses it, with the same numbers. `DisplayViewGpuRequest` gains `lens`. `scripts/benchmark-display-view.mjs --app` gains `LENS` variants.
84
+ - **Compatibility:** no new root export; every request that renders today gives the same bytes (hash locks unchanged). Tests: `test/display-view-lens.test.ts`; `test/display-view.test.ts` and `test/simulate-capture-defocus-optics.test.ts` updated for the refusals that are gone.
85
+ - **New: the generic lens catalogue, 30 primes with documented optical character (#549, for the apps "Pro and consumer lens sets" epic).** Two sets, Professional (17: *PV 8 3.5 Fisheye Pro* to *PV 1200 8 Pro*) and Consumer (13: *PV 16 2.8* to *PV 800 9*), named `PV <focal length> <maximum aperture>` with ids such as `pv-50-1.2-pro`, `pv-50-1.8`, `pv-100-2.8-macro-pro`. Every lens is Photivra's own generic design, not modelled on any product; every number is a labelled approximation (docs/LENS_CATALOGUE.md).
86
+ - **Lenses:** the owner's table of focal lengths and widest apertures; the generic smallest aperture (f/16 to 16 mm, f/22 to 135 mm, f/32 beyond) and close focus (0.15×, 1:1 for the macro, at least 10 cm); equisolid fisheyes (8 mm circular, 15 mm full frame). Focal lengths scale by the crop factor, as `lens.prime`.
87
+ - **Character:** a set baseline (the owner's table) times per-lens adjustments, each with its reason: Seidel spherical (balanced by defocus), coma, astigmatism and field curvature at the corner, scaled with the pupil (a⁴, a³, a², a², a = N0/N) and a close-focus factor; axial colour (defocus) and lateral colour (radial shift) with the normalised Cauchy dispersion; radial distortion; vignetting E = (1 − bρ²)² with optical vignetting fading over two stops; flare, focus breathing and focus scatter as numbers (Hopkins 1950; Wyant & Creath 1992, free).
88
+ - **`simulateCapture` `lens.catalogue`:** renders the lens's PSF (scalar Fraunhofer diffraction of the aberrated pupil by FFT, integrated over site cells, on a 5 × 5 field grid × 5 wavelengths, centroid-aligned interpolation, window keeping all but ≤ 3 % of each node's energy) through the environment PSF quadrature, its distortion as the lens field mapping, its vignetting as field throughput and the fisheyes' projection. Directions at or beyond 90° see no scene, so the circular fisheye's image ends at its 180° circle (opt-in `zeroBeyondHorizon` in the facade's provider, and `EnvironmentLensFieldMapping.outsideImageCircle: "backward-ray"` for points outside the equisolid image circle). `lens.defocus` convolves the disc into the PSF (combined radius up to 16 sites); `lens.focusScatter: { seed, shotIndex? }` applies the seeded autofocus error, off by default. Recorded in the assumptions `lens.catalogue` and `lens.catalogue.character`; `diagnostics.options` gains `lens.catalogue` (`SIMULATE_CAPTURE_COST_OPTIONS` lists it). Not combinable with `prime`, `focalLengthMm`, `projection`, `distortion`, `vignetting`, `psf`, `diffraction` or `pupilSampling`; the display view refuses it. The catalogue and its PSF builder are loaded on demand (`src/facade/generic-lens-capture.ts`).
89
+ - **Measured on the charts** (`test/simulate-capture-lens-catalogue.test.ts`, *PV 24 1.4 Pro* against *PV 24 1.8* at their widest apertures, full frame): slanted-edge 10–90 % rise 0.91 / 1.09 px against 1.01 / 1.44 px (centre / corner, 6 µm); corner coma (point, wide open minus f/5.6, 3 µm) 0.02 px against 0.36 px; corner lateral colour 0.31 px against 0.71 px; grid-line bow 0.00 px (expected 0.31) against 0.60 px (expected 0.92) at 120 µm; corner vignetting 1.15 against 1.64 stops, 0.74 at f/5.6, each within 0.02 stops of the profile with the distortion's area factor; the 8 mm's black surround and the 15 mm's filled frame. The PSF builder matches the engine's diffraction-limited site kernels without aberrations (to 0.004 per cell), samples every lens's wavefront within Nyquist (wide open, minimum focus, corner, 380 nm) and points coma and the blue image outward in every quadrant (`test/generic-lens-catalogue.test.ts`).
90
+ - **Focus consistency:** `simulateGenericLensFocusError` (seeded, reproducible; σ = 0.1 / 0.3 of the depth of focus 2·N·c for the sets' baselines, tested over 4000 shots).
91
+ - **Correction profiles:** `createGenericLensCorrectionProfile` builds each lens's generic correction profile (distortion map, per-channel lateral-colour scale, vignetting gain) for `resolveLensCorrectionPlan`; `simulateCapture` does not apply it.
92
+ - **Cost:** like `lens.distortion`, one scene query per kernel tap through the lens field mapping: a 48 × 48 region of a full frame at 6 µm takes 1.2–1.3 s (pro, 3 × 3 kernel) to 5.9–9.7 s (consumer, 7 × 7 to 9 × 9) at the default spectral step, against 0.01 s with `lens.prime` (Apple M5 Pro, Node.js 24.19.0).
93
+ - **Found, left as is:** the sampled-PSF mode `rotationally-symmetric-radial-field-centroid-aligned` rotates kernels as if rows ran towards −Y, while the environment PSF quadrature (`sensorPsfSourcePoint`) applies rows as +Y, so an asymmetric kernel under that mode renders mirrored in y. The catalogue uses the 2-D grid mode; changing either would change existing captures. Owner decision 2026-11-07: fixed in 2.18.0, as a breaking fix with an orientation test (#566); 2.17.0 leaves it as is.
94
+ - **API:** 18 new root exports (7 runtime): `GENERIC_LENS_CATALOGUE`, `GENERIC_LENS_IDS`, `GENERIC_LENS_SET_BASELINES`, `resolveGenericLens`, `simulateGenericLensFocusError`, `createGenericLensCorrectionProfile`, `GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM` and the types `GenericLensCatalogueEntry`, `GenericLensSet`, `GenericLensKind`, `GenericLensCharacterValues`, `GenericLensAdjustments`, `GenericLensCharacter`, `ResolvedGenericLens`, `ResolveGenericLensInput`, `SimulateGenericLensFocusErrorInput`, `GenericLensFocusError`, `CreateGenericLensCorrectionProfileInput`. Each `GenericLensCatalogueEntry` and `ResolvedGenericLens` has `summary`, a short plain-English description of the lens's character (at most 40 characters, US spelling, written from its adjustments and reason; tested against them). `SimulateCaptureOptions.lens` gains `catalogue` and `focusScatter`; `EnvironmentLensFieldMapping` gains the optional `outsideImageCircle`. Root exports 1,810 → 1,828 (runtime 614 → 621).
95
+ - **Compatibility:** requests without `lens.catalogue` give the same captures, RAW codes, files and assumptions; a lens field mapping without `outsideImageCircle` still refuses points outside the image circle. The display view's added bundle size is 46,366 B minified and 18,498 B gzip (2.16.0: 46,380 B and 18,540 B; budgets unchanged). No `src/gpu` change.
96
+ - **Not in 2.17.0:** a catalogue lens's bokeh character. #553's bokeh options are refused with `lens.catalogue` (the lens folds a plain disc into its PSF), and feeding each lens's spherical aberration and character into the bokeh kernel is a follow-up (#567). The display view still refuses `lens.catalogue`. The phone profile's three fixed lenses are #554. Docs: docs/LENS_CATALOGUE.md (new), `docs/QUICK_START.md` ("Generic lens catalogue"), `docs/USAGE.md`; API reference regenerated.
97
+ - **Integration of #564, #553, #563 and #549 (this release).**
98
+ - **The defocus kernel is the shaped kernel or the disc** (`siteKernel ?? disc`) in the capture and the display view, so a blade-shaped kernel above 64 sites takes #564's run path. The shaped kernel's radius cap is 512 sites, refused with #564's short message (`lens.defocus: The blur circle is … across; the engine renders at most 1,025 sites …`). The run path's refusal is now a plain `lens.defocus:` message stating the 16,641-read limit, and a cat's eye above 64 sites is refused by name. The `lens.defocus` assumption gains `convolution: "runs"` whenever the applied kernel is above 64 sites.
99
+ - **The display view's lens signal with a large disc.** #563's lens signal read only the combined kernels, so with #564 a lens view above 64 sites would have rendered without the blur. It now applies the disc by runs to each channel's in-focus signal through the lens, as the rectilinear view does. With radial distortion, vignetting and a disc of radius 179, every site matches the capture's image-space provider within 1e-9 relative, and `gpu: "auto"` falls back to the labelled CPU reference.
100
+ - **Fixed tiles and lens profiles.** #564's fixed 64-site tiles in the image-space provider mapped lattice points up to a tile beyond the reach #548 checks, which threw with a distortion profile sized to that reach. Points beyond the reach (which feed no queried value) are now set to 0 and never mapped, and so are the lens signal's in-focus tiles. Every queried value is the same sum; only the prefix sums' rounding can change, within the run path's stated accuracy. These requests are new in 2.17.0, so no 2.16.0 bytes are involved.
101
+ - **`resolveGenericLens` profiles reach past the corner (app report).** Its distortion and vignetting profiles stopped at the half-diagonal (21.633 mm on full frame), while the display view and the capture read slightly beyond the outer site centres (21.637 mm), so `simulateDisplayView` and `simulateCapture` refused even PV 50 1.8 with its own profiles. They now reach 1.25 of the half-diagonal (the distortion's range is widened for barrel lenses, whose image points come from farther out). The coefficients are unchanged, so every value inside the frame is too; every lens stays valid there (vignetting E > 0, distortion monotonic; tested for all 30). `createGenericLensCorrectionProfile` keeps its own 1.05.
102
+ - **The catalogue and bokeh.** `lens.catalogue` with `blades`, `sphericalAberration` or `catsEye` is refused by name; before, the options were validated and then dropped. The catalogue's combined defocus limit of 16 sites stays as built.
103
+ - **The app's bundle (app report).** Built against this release, the app's total JavaScript grows from 498.6 KiB (2.15.0) to 521,974 B (2.16.0) and 546,366 B gzip (2.17.0), against its 512,000 B budget. An engine bundle of exactly the app's worker imports grows the same: 311,439 → 323,428 → 347,775 B gzip over all chunks. Tree-shaking works: unused root exports such as `calculateDefocusBokehKernel` are absent. The growth is the new options' code, which `simulateCapture` and `prepareDisplayView` reach at run time. The capture worker chunk's +65 KB is mostly about 60 KB of existing modules (the display-view GPU host, text metadata, pixel limits) that the bundler now places in the worker's entry chunk instead of three chunks of their own, so they are not new bytes. `test/capture-bundle.test.ts` now checks that shape against its measured total (348,000 B gzip), so growth shows in the engine first.
104
+ - **Tests:** `test/defocus-integration-2-17.test.ts`; the bokeh limit tests updated for the 512-site cap and the new messages.
105
+
106
+ ## 2.16.0 — published November 7, 2026
4
107
 
5
108
  A minor release of five issues: the display view's highlight count (#533), a development exposure push (#556), sharpening and diffraction correction (#552), `lens.defocus` with projection, distortion, vignetting and camera rotation (#548), and docs (#400). Captures, RAW codes, files and assumptions are the same as 2.15.0 for every request that 2.15.0 renders and that uses none of the new options; the display view's result gains one diagnostic.
6
109
  - **New:** `DisplayViewResult.diagnostics.highlight` (#533); `output.exposureCompensationStops` (#556); `output.sharpening` and `output.diffractionCorrection` (#552); out-of-focus `lens.defocus` with `lens.projection`, `lens.distortion`, `lens.vignetting` or camera rotation under a global shutter, in image space (#548).
package/CITATION.cff CHANGED
@@ -9,7 +9,7 @@ abstract: "An open TypeScript engine for the science and mathematics of photogra
9
9
  repository-code: "https://github.com/photivra/photivra"
10
10
  url: "https://photivra.com"
11
11
  license: "Apache-2.0"
12
- version: "2.16.0"
12
+ version: "2.18.0"
13
13
  keywords:
14
14
  - photography
15
15
  - camera optics
package/README.md CHANGED
@@ -32,10 +32,10 @@ The source is licensed under Apache-2.0.
32
32
 
33
33
  ## Quick start
34
34
 
35
- Install the published ESM package below. The [2.16.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_16_0.md) and [changelog](https://github.com/Photivra/photivra/blob/main/CHANGELOG.md) describe the current release:
35
+ Install the published ESM package below. The [2.18.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_18_0.md) and [changelog](https://github.com/Photivra/photivra/blob/main/CHANGELOG.md) describe the current release:
36
36
 
37
37
  ```sh
38
- npm install @photivra/engine@2.16.0
38
+ npm install @photivra/engine@2.18.0
39
39
  ```
40
40
 
41
41
  Simulate a photograph of a test chart with a generic camera, then read the developed sRGB pixels:
@@ -75,7 +75,7 @@ console.log(horizontal.provenance);
75
75
 
76
76
  Start with the [Developer Guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Then use the [Usage Guide](https://github.com/Photivra/photivra/blob/main/docs/USAGE.md) and the [API reference](https://github.com/Photivra/photivra/blob/main/docs/API_REFERENCE.md), which is generated from the package exports.
77
77
 
78
- Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.16.0` tag.
78
+ Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.18.0` tag.
79
79
 
80
80
  ## Documentation
81
81
 
@@ -363,13 +363,13 @@ The application can then use those results, while the photography calculations t
363
363
 
364
364
  ## Status
365
365
 
366
- - Repository package version: `2.16.0` (release candidate prepared November 7, 2026; publication is separate)
367
- - Engine API contract: `2.16.0`
366
+ - Repository package version: `2.18.0` (release candidate prepared November 9, 2026; publication is separate)
367
+ - Engine API contract: `2.18.0`
368
368
  - Versioned generic tier reference assets: [GENERIC_TIER_PRESETS.md](https://github.com/Photivra/photivra/blob/main/docs/GENERIC_TIER_PRESETS.md).
369
369
  - Composed POC simulation API contract: `1.0.0`
370
370
  - Stability: 1.0 root-package compatibility policy; bounded scientific models and independent POC contract
371
371
 
372
- From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.16.0; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
372
+ From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.18.0; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
373
373
 
374
374
  A breaking change to the root contracts now needs a new package major version. Additive and corrective changes follow [compatibility and migration](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_1_3_0.md). A 1.0 package is not a claim of calibrated physical accuracy.
375
375
 
package/SECURITY.md CHANGED
@@ -4,7 +4,7 @@ Please do not open a public issue that contains vulnerability details.
4
4
 
5
5
  ## Supported versions
6
6
 
7
- The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.15.0**, published on November 7, 2026 (see [CHANGELOG.md](CHANGELOG.md)).
7
+ The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.17.0**, published on November 8, 2026 (see [CHANGELOG.md](CHANGELOG.md)).
8
8
 
9
9
  Security fixes target the current maintained development and release line. Do not assume that older 1.x releases, snapshots or pre-1.0 releases will receive fixes.
10
10
 
package/THIRD_PARTY.md CHANGED
@@ -217,6 +217,30 @@ no code, table or data copied):
217
217
  - Patent status: no public-record patent check has been recorded for these two
218
218
  long-published methods; it is left for the owner's review with the others.
219
219
 
220
+ Defocus and lens-character references (2.17.0: #564, #553, #549; published
221
+ methods and qualitative descriptions only, independently implemented; no code,
222
+ table or data copied):
223
+
224
+ - Run-length convolution by prefix sums (#564, `src/optics/site-kernel-runs.ts`):
225
+ F. C. Crow, "Summed-area tables for texture mapping", SIGGRAPH 1984, and
226
+ P. S. Heckbert, "Filtering by repeated integration", SIGGRAPH 1986.
227
+ - Bokeh kernel (#553, `src/optics/defocus-bokeh-kernel.ts`): the transverse ray
228
+ aberration of primary spherical aberration, Wyant & Creath (1992), *Applied
229
+ Optics and Optical Engineering* XI ch. 1 (free author copy); mechanical
230
+ vignetting, Greivenkamp, OPTI-502 course notes sec. 10; sectioned
231
+ shift-variant blur, Denis, Thiébaut, Soulez, Becker & Mourya (2015), IJCV 115
232
+ (open preprint arXiv:1502.03305); the photographic description of front and
233
+ back bokeh, H. H. Nasse, "Depth of Field and Bokeh", Carl Zeiss (2010).
234
+ - Generic lens catalogue (#549, `src/equipment/generic-lens-catalogue.ts`,
235
+ `src/optics/generic-lens-psf.ts`): Seidel aberrations, Hopkins (1950) and
236
+ Wyant & Creath (1992); scalar Fraunhofer imaging, Goodman, *Introduction to
237
+ Fourier Optics*; the FFT, Cooley & Tukey (1965); qualitative Wikipedia
238
+ articles on vignetting, distortion, chromatic aberration, coma, flare and
239
+ focus breathing (CC BY-SA text read, nothing reproduced). Every catalogue
240
+ number is Photivra's own choice (docs/LENS_CATALOGUE.md).
241
+ - Patent status: no public-record patent check has been recorded for these
242
+ methods; it is left for the owner's review with the others.
243
+
220
244
  The DNG patent-license notice appears in `NOTICE` and exporter source. These
221
245
  references do not authorize copying SDK code or scientific datasets. Human
222
246
  provenance/license review remains required before release.
@@ -114,6 +114,11 @@ export interface LensProjectionFieldOfViewBounds {
114
114
  export declare function parseLensProjectionModel(value: unknown): LensProjectionModel;
115
115
  /** Image radius r(theta) for one radial model. */
116
116
  export declare function radialProjectionImageRadius(model: RadialLensProjectionModel, focalLengthMm: number, fieldAngleRadians: number): number;
117
+ /**
118
+ * @internal Whether an image radius lies inside a radial model's image circle (the domain of {@link radialProjectionFieldAngle}).
119
+ * Equirectangular is not radial and returns true.
120
+ */
121
+ export declare function insideRadialImageCircle(model: LensProjectionModel, focalLengthMm: number, imageRadiusMm: number): boolean;
117
122
  /** Inverse theta(r) for one radial model; throws outside the image circle. */
118
123
  export declare function radialProjectionFieldAngle(model: RadialLensProjectionModel, focalLengthMm: number, imageRadiusMm: number): number;
119
124
  /**
@@ -111,6 +111,16 @@ function radialImageRadiusLimit(model, focalLengthMm) {
111
111
  return { radiusMm: focalLengthMm, inclusive: true };
112
112
  }
113
113
  }
114
+ /**
115
+ * @internal Whether an image radius lies inside a radial model's image circle (the domain of {@link radialProjectionFieldAngle}).
116
+ * Equirectangular is not radial and returns true.
117
+ */
118
+ export function insideRadialImageCircle(model, focalLengthMm, imageRadiusMm) {
119
+ if (model === "equirectangular")
120
+ return true;
121
+ const limit = radialImageRadiusLimit(model, focalLengthMm);
122
+ return imageRadiusMm >= 0 && (imageRadiusMm < limit.radiusMm || (limit.inclusive && imageRadiusMm === limit.radiusMm));
123
+ }
114
124
  /** Inverse theta(r) for one radial model; throws outside the image circle. */
115
125
  export function radialProjectionFieldAngle(model, focalLengthMm, imageRadiusMm) {
116
126
  const limit = radialImageRadiusLimit(model, focalLengthMm);
@@ -1 +1 @@
1
- {"version":3,"file":"lens-projection.js","sourceRoot":"","sources":["../../src/camera/lens-projection.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Ideal lens projection (mapping) functions. They convert between a scene direction and a point on the\n * image plane.\n *\n * A radial projection maps the field angle theta (the angle from the optical axis) to the image\n * radius r:\n * rectilinear r = f tan(theta) (gnomonic / pinhole)\n * equidistant r = f theta (\"f-theta\" fisheye)\n * equisolid-angle r = 2 f sin(theta / 2) (equal-area fisheye)\n * stereographic r = 2 f tan(theta / 2) (conformal fisheye)\n * orthographic r = f sin(theta)\n * The image point keeps the direction's azimuth about the optical axis, so every radial projection is\n * rotationally symmetric. Equirectangular (spherical, or cylindrical equidistant) is not radial:\n * x = f * longitude, y = f * latitude, where f is the image distance per radian.\n *\n * Sources: Kannala & Brandt 2006 (IEEE TPAMI 28:1335, generic camera model listing these\n * projections, eqs. 1-5; free author copy); Miyamoto 1964 (JOSA 54:1060, fish-eye projection\n * functions); Snyder 1987 (Map Projections - A Working Manual, USGS PP 1395, free) for the\n * equirectangular and stereographic projections. Also see: Kingslake 1989 (A History of the\n * Photographic Lens, ch. on fish-eye lenses); Ray 2002 (Applied Photographic Optics, 3rd ed., ch. 6\n * and 47).\n *\n * These are ideal mapping functions. Real fisheye lenses deviate from them. Modelling a real lens\n * would need a calibrated polynomial (for example Kannala-Brandt) with defensible provenance.\n *\n * Camera frame: +X right, +Y up, +Z along the optical axis toward the scene. Image-plane coordinates\n * use the same non-inverted +X right / +Y up convention as the rest of the engine.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { rejectUnknownFields, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/** Ideal projection model discriminator. `rectilinear` is the engine default. */\nexport type LensProjectionModel =\n | \"rectilinear\"\n | \"fisheye-equidistant\"\n | \"fisheye-equisolid-angle\"\n | \"fisheye-stereographic\"\n | \"fisheye-orthographic\"\n | \"equirectangular\";\n\n/** Radial (rotationally symmetric) projection models. */\nexport type RadialLensProjectionModel = Exclude<\n LensProjectionModel,\n \"equirectangular\"\n>;\n\nexport const LENS_PROJECTION_MODELS: readonly LensProjectionModel[] =\n /* @__PURE__ */ Object.freeze([\n \"rectilinear\",\n \"fisheye-equidistant\",\n \"fisheye-equisolid-angle\",\n \"fisheye-stereographic\",\n \"fisheye-orthographic\",\n \"equirectangular\"\n ]);\n\nexport interface LensDirection {\n /** Camera-frame direction component, +X right. Any positive length. */\n x: number;\n /** Camera-frame direction component, +Y up. */\n y: number;\n /** Camera-frame direction component along the optical axis toward the scene. */\n z: number;\n}\n\nexport interface CalculateLensProjectionImagePointInput {\n projectionModel: LensProjectionModel;\n /**\n * Projection scale in millimetres: the focal length f of the mapping function, or for\n * equirectangular the image distance per radian.\n */\n focalLengthMm: number;\n /** Scene direction in the camera frame. Need not be normalized; must be non-zero. */\n direction: LensDirection;\n}\n\nexport interface LensProjectionImagePoint {\n projectionModel: LensProjectionModel;\n imagePointMm: { x: number; y: number };\n /** Angle between the direction and the optical axis, in radians. */\n fieldAngleRadians: number;\n /** Distance of the image point from the optical axis, in millimetres. */\n imageRadiusMm: number;\n}\n\nexport interface CalculateLensProjectionDirectionInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Image-plane point relative to the optical axis, in millimetres. */\n imagePointMm: { x: number; y: number };\n}\n\nexport interface LensProjectionDirection {\n projectionModel: LensProjectionModel;\n /** Unit direction in the camera frame. */\n direction: LensDirection;\n fieldAngleRadians: number;\n imageRadiusMm: number;\n}\n\nexport interface CalculateLensProjectionFieldOfViewInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Centred sensor/capture dimension along one axis through the optical axis, in mm. */\n sensorDimensionMm: number;\n}\n\nexport interface CalculateLensProjectionFieldOfViewBoundsInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Signed minimum sensor coordinate along one axis through the optical axis, in mm. */\n minimumSensorCoordinateMm: number;\n /** Signed maximum sensor coordinate along the same axis, in mm. */\n maximumSensorCoordinateMm: number;\n}\n\nexport interface LensProjectionFieldOfView {\n projectionModel: LensProjectionModel;\n degrees: number;\n radians: number;\n /** Projection scale used by the mapping, in millimetres. */\n projectionDistanceMm: number;\n}\n\nexport interface LensProjectionFieldOfViewBounds {\n projectionModel: LensProjectionModel;\n minimumRadians: number;\n maximumRadians: number;\n radians: number;\n minimumDegrees: number;\n maximumDegrees: number;\n degrees: number;\n projectionDistanceMm: number;\n}\n\nconst HALF_PI = Math.PI / 2;\n\n/** Runtime parser for the projection discriminator. Rejects (fails closed on) unknown values. */\nexport function parseLensProjectionModel(\n value: unknown\n): LensProjectionModel {\n if (\n typeof value !== \"string\" ||\n !(LENS_PROJECTION_MODELS as readonly string[]).includes(value)\n ) {\n throw new InvalidConfigurationError(\n \"projectionModel must be one of \" +\n LENS_PROJECTION_MODELS.join(\", \") +\n \".\"\n );\n }\n return value as LensProjectionModel;\n}\n\nfunction requireRecord(name: string, value: unknown): void {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(`${name} must be an object.`);\n }\n}\n\nfunction requireFinite(name: string, value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be a finite number.`);\n }\n return value;\n}\n\n/**\n * Largest field angle (radians) a radial model maps, and whether the model can reach it.\n * Rectilinear, stereographic, equidistant and equisolid exclude the limit: the radius there is\n * infinite, or the azimuth of the backward direction is undefined. Orthographic reaches 90 degrees.\n */\nfunction radialFieldAngleLimit(model: RadialLensProjectionModel): {\n radians: number;\n inclusive: boolean;\n} {\n switch (model) {\n case \"rectilinear\":\n return { radians: HALF_PI, inclusive: false };\n case \"fisheye-orthographic\":\n return { radians: HALF_PI, inclusive: true };\n default:\n return { radians: Math.PI, inclusive: false };\n }\n}\n\n/** Image radius r(theta) for one radial model. */\nexport function radialProjectionImageRadius(\n model: RadialLensProjectionModel,\n focalLengthMm: number,\n fieldAngleRadians: number\n): number {\n switch (model) {\n case \"rectilinear\":\n return focalLengthMm * Math.tan(fieldAngleRadians);\n case \"fisheye-equidistant\":\n return focalLengthMm * fieldAngleRadians;\n case \"fisheye-equisolid-angle\":\n return 2 * focalLengthMm * Math.sin(fieldAngleRadians / 2);\n case \"fisheye-stereographic\":\n return 2 * focalLengthMm * Math.tan(fieldAngleRadians / 2);\n case \"fisheye-orthographic\":\n return focalLengthMm * Math.sin(fieldAngleRadians);\n }\n}\n\n/**\n * Largest image radius a radial model reaches inside its field-angle domain, in mm\n * (Infinity for rectilinear and stereographic).\n */\nfunction radialImageRadiusLimit(\n model: RadialLensProjectionModel,\n focalLengthMm: number\n): { radiusMm: number; inclusive: boolean } {\n switch (model) {\n case \"rectilinear\":\n case \"fisheye-stereographic\":\n return { radiusMm: Number.POSITIVE_INFINITY, inclusive: false };\n case \"fisheye-equidistant\":\n return { radiusMm: Math.PI * focalLengthMm, inclusive: false };\n case \"fisheye-equisolid-angle\":\n return { radiusMm: 2 * focalLengthMm, inclusive: false };\n case \"fisheye-orthographic\":\n return { radiusMm: focalLengthMm, inclusive: true };\n }\n}\n\n/** Inverse theta(r) for one radial model; throws outside the image circle. */\nexport function radialProjectionFieldAngle(\n model: RadialLensProjectionModel,\n focalLengthMm: number,\n imageRadiusMm: number\n): number {\n const limit = radialImageRadiusLimit(model, focalLengthMm);\n if (\n imageRadiusMm < 0 ||\n imageRadiusMm > limit.radiusMm ||\n (!limit.inclusive && imageRadiusMm === limit.radiusMm)\n ) {\n throw new InvalidScientificInputError(\n `Image radius lies outside the ${model} image circle.`\n );\n }\n switch (model) {\n case \"rectilinear\":\n return Math.atan(imageRadiusMm / focalLengthMm);\n case \"fisheye-equidistant\":\n return imageRadiusMm / focalLengthMm;\n case \"fisheye-equisolid-angle\":\n return 2 * Math.asin(imageRadiusMm / (2 * focalLengthMm));\n case \"fisheye-stereographic\":\n return 2 * Math.atan(imageRadiusMm / (2 * focalLengthMm));\n case \"fisheye-orthographic\":\n return Math.asin(Math.min(1, imageRadiusMm / focalLengthMm));\n }\n}\n\nfunction assumptions(model: LensProjectionModel): readonly string[] {\n const common = [\n \"Ideal mapping function of a perfect projection; real lenses deviate from it and need calibrated distortion data\",\n \"Camera frame +X right, +Y up, +Z toward the scene; image plane +X right, +Y up (non-inverted)\"\n ];\n if (model === \"equirectangular\") {\n return [\n \"Equirectangular (spherical) projection: x = f * longitude, y = f * latitude, longitude measured about +Y from +Z\",\n ...common\n ];\n }\n return [\n `Radial projection ${model}; image azimuth equals scene azimuth about the optical axis`,\n ...common\n ];\n}\n\nfunction validateCommon(\n input: { projectionModel: unknown; focalLengthMm: number },\n name: string\n): LensProjectionModel {\n requireRecord(name, input);\n const model = parseLensProjectionModel(input.projectionModel);\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n return model;\n}\n\n/**\n * Maps one camera-frame scene direction to its ideal image-plane point under the declared\n * projection model.\n */\nexport function calculateLensProjectionImagePoint(\n input: CalculateLensProjectionImagePointInput\n): CalculationResult<LensProjectionImagePoint> {\n requireInputKeys(input, \"calculateLensProjectionImagePoint\", [\"projectionModel\", \"focalLengthMm\", \"direction\"]);\n const model = validateCommon(input, \"input\");\n requireRecord(\"direction\", input.direction);\n rejectUnknownFields(input.direction, [\"x\", \"y\", \"z\"], \"direction\");\n const x = requireFinite(\"direction.x\", input.direction.x);\n const y = requireFinite(\"direction.y\", input.direction.y);\n const z = requireFinite(\"direction.z\", input.direction.z);\n const length = Math.hypot(x, y, z);\n if (!(length > 0) || !Number.isFinite(length)) {\n throw new InvalidScientificInputError(\n \"direction must be a finite non-zero vector.\"\n );\n }\n const { imagePointMm, fieldAngleRadians } = lensProjectionImagePointValue(\n model,\n input.focalLengthMm,\n x,\n y,\n z\n );\n\n return calculatedResult(\n {\n projectionModel: model,\n imagePointMm,\n fieldAngleRadians,\n imageRadiusMm: Math.hypot(imagePointMm.x, imagePointMm.y)\n },\n `${model}-lens-projection`,\n \"1.0.0\",\n assumptions(model)\n );\n}\n\n/**\n * Inverse-maps one image-plane point to its unit camera-frame scene direction under the declared\n * projection model. It rejects points outside the model's image circle. For equirectangular, it\n * rejects points outside +/-pi longitude and +/-pi/2 latitude.\n */\nexport function calculateLensProjectionDirection(\n input: CalculateLensProjectionDirectionInput\n): CalculationResult<LensProjectionDirection> {\n requireInputKeys(input, \"calculateLensProjectionDirection\", [\"projectionModel\", \"focalLengthMm\", \"imagePointMm\"]);\n const model = validateCommon(input, \"input\");\n requireRecord(\"imagePointMm\", input.imagePointMm);\n rejectUnknownFields(input.imagePointMm, [\"x\", \"y\"], \"imagePointMm\");\n const px = requireFinite(\"imagePointMm.x\", input.imagePointMm.x);\n const py = requireFinite(\"imagePointMm.y\", input.imagePointMm.y);\n const imageRadiusMm = Math.hypot(px, py);\n const direction = lensProjectionDirectionValue(\n model,\n input.focalLengthMm,\n px,\n py\n );\n\n return calculatedResult(\n {\n projectionModel: model,\n direction,\n fieldAngleRadians: Math.atan2(\n Math.hypot(direction.x, direction.y),\n direction.z\n ),\n imageRadiusMm\n },\n `inverse-${model}-lens-projection`,\n \"1.0.0\",\n assumptions(model)\n );\n}\n\n/** Signed angle subtended along one axis through the optical axis for a signed coordinate. */\nfunction signedAxisAngle(\n model: LensProjectionModel,\n focalLengthMm: number,\n coordinateMm: number\n): number {\n if (model === \"equirectangular\") {\n // Along a horizontal or vertical line through the centre the angle is linear in position.\n return coordinateMm / focalLengthMm;\n }\n const theta = radialProjectionFieldAngle(\n model,\n focalLengthMm,\n Math.abs(coordinateMm)\n );\n return coordinateMm < 0 ? -theta : theta;\n}\n\n/**\n * Angular field of view across a centred sensor dimension along one axis through the optical\n * axis: 2 * theta(d / 2) for radial models and d / f for equirectangular.\n *\n * For equirectangular, the result is the longitude span for a horizontal dimension and the latitude\n * span for a vertical one. A sensor dimension larger than a fisheye's image circle is rejected, not\n * clipped.\n */\nexport function calculateLensProjectionFieldOfView(\n input: CalculateLensProjectionFieldOfViewInput\n): CalculationResult<LensProjectionFieldOfView> {\n requireInputKeys(input, \"calculateLensProjectionFieldOfView\", [\"projectionModel\", \"focalLengthMm\", \"sensorDimensionMm\"]);\n const model = validateCommon(input, \"input\");\n requirePositiveFinite(\"sensorDimensionMm\", input.sensorDimensionMm);\n const radians =\n 2 *\n signedAxisAngle(model, input.focalLengthMm, input.sensorDimensionMm / 2);\n if (model === \"equirectangular\" && radians > 2 * Math.PI) {\n throw new InvalidScientificInputError(\n \"sensorDimensionMm exceeds the full 360-degree equirectangular span.\"\n );\n }\n return calculatedResult(\n {\n projectionModel: model,\n radians,\n degrees: (radians * 180) / Math.PI,\n projectionDistanceMm: input.focalLengthMm\n },\n `${model}-field-of-view`,\n \"1.0.0\",\n [\n ...assumptions(model),\n \"Field of view is measured along one axis through the optical axis\"\n ]\n );\n}\n\n/**\n * Asymmetric angular span between two signed sensor coordinates on one axis through the optical\n * axis, for off-centre crops.\n */\nexport function calculateLensProjectionFieldOfViewBounds(\n input: CalculateLensProjectionFieldOfViewBoundsInput\n): CalculationResult<LensProjectionFieldOfViewBounds> {\n requireInputKeys(input, \"calculateLensProjectionFieldOfViewBounds\", [\"projectionModel\", \"focalLengthMm\", \"minimumSensorCoordinateMm\", \"maximumSensorCoordinateMm\"]);\n const model = validateCommon(input, \"input\");\n const minimum = requireFinite(\n \"minimumSensorCoordinateMm\",\n input.minimumSensorCoordinateMm\n );\n const maximum = requireFinite(\n \"maximumSensorCoordinateMm\",\n input.maximumSensorCoordinateMm\n );\n if (maximum <= minimum) {\n throw new InvalidScientificInputError(\n \"maximumSensorCoordinateMm must be greater than minimumSensorCoordinateMm.\"\n );\n }\n const minimumRadians = signedAxisAngle(model, input.focalLengthMm, minimum);\n const maximumRadians = signedAxisAngle(model, input.focalLengthMm, maximum);\n const radians = maximumRadians - minimumRadians;\n if (model === \"equirectangular\" && radians > 2 * Math.PI) {\n throw new InvalidScientificInputError(\n \"Sensor bounds exceed the full 360-degree equirectangular span.\"\n );\n }\n return calculatedResult(\n {\n projectionModel: model,\n minimumRadians,\n maximumRadians,\n radians,\n minimumDegrees: (minimumRadians * 180) / Math.PI,\n maximumDegrees: (maximumRadians * 180) / Math.PI,\n degrees: (radians * 180) / Math.PI,\n projectionDistanceMm: input.focalLengthMm\n },\n `asymmetric-${model}-field-of-view`,\n \"1.0.0\",\n [\n ...assumptions(model),\n \"Signed sensor-plane bounds are measured relative to the optical axis along one axis through it\"\n ]\n );\n}\n\n/**\n * @internal Image point of a non-zero camera-frame direction under one model (no envelope).\n * Shared by the public mapping, geometric remaps and the environment ray query.\n */\nexport function lensProjectionImagePointValue(\n model: LensProjectionModel,\n focalLengthMm: number,\n x: number,\n y: number,\n z: number\n): { imagePointMm: { x: number; y: number }; fieldAngleRadians: number } {\n const length = Math.hypot(x, y, z);\n const f = focalLengthMm;\n const transverse = Math.hypot(x, y);\n const fieldAngleRadians = Math.atan2(transverse, z);\n\n let imagePointMm: { x: number; y: number };\n if (model === \"equirectangular\") {\n const latitude = Math.asin(Math.max(-1, Math.min(1, y / length)));\n const horizontal = Math.hypot(x, z);\n if (horizontal === 0) {\n // Poles have an undefined longitude; the projection is singular there.\n throw new InvalidScientificInputError(\n \"Equirectangular projection is undefined at the poles (direction parallel to +/-Y).\"\n );\n }\n const longitude = Math.atan2(x, z);\n imagePointMm = { x: f * longitude, y: f * latitude };\n } else {\n const limit = radialFieldAngleLimit(model);\n if (\n fieldAngleRadians > limit.radians ||\n (!limit.inclusive && fieldAngleRadians === limit.radians)\n ) {\n throw new InvalidScientificInputError(\n `direction lies outside the ${model} projection domain.`\n );\n }\n const radius = radialProjectionImageRadius(model, f, fieldAngleRadians);\n imagePointMm =\n transverse === 0\n ? { x: 0, y: 0 }\n : { x: (radius * x) / transverse, y: (radius * y) / transverse };\n }\n\n return { imagePointMm, fieldAngleRadians };\n}\n\n/** @internal Unit camera-frame direction of one image point under one model (no envelope). */\nexport function lensProjectionDirectionValue(\n model: LensProjectionModel,\n focalLengthMm: number,\n px: number,\n py: number\n): LensDirection {\n const f = focalLengthMm;\n const imageRadiusMm = Math.hypot(px, py);\n\n let direction: LensDirection;\n if (model === \"equirectangular\") {\n const longitude = px / f;\n const latitude = py / f;\n if (Math.abs(longitude) > Math.PI || Math.abs(latitude) > HALF_PI) {\n throw new InvalidScientificInputError(\n \"imagePointMm lies outside the equirectangular longitude/latitude domain.\"\n );\n }\n const cosLatitude = Math.cos(latitude);\n direction = {\n x: cosLatitude * Math.sin(longitude),\n y: Math.sin(latitude),\n z: cosLatitude * Math.cos(longitude)\n };\n } else {\n const theta = radialProjectionFieldAngle(model, f, imageRadiusMm);\n const sinTheta = Math.sin(theta);\n direction =\n imageRadiusMm === 0\n ? { x: 0, y: 0, z: 1 }\n : {\n x: (sinTheta * px) / imageRadiusMm,\n y: (sinTheta * py) / imageRadiusMm,\n z: Math.cos(theta)\n };\n }\n\n return direction;\n}\n\n/** d r / d theta of a radial model. */\nfunction radialDerivative(\n model: RadialLensProjectionModel,\n f: number,\n theta: number\n): number {\n switch (model) {\n case \"rectilinear\":\n return f / Math.cos(theta) ** 2;\n case \"fisheye-equidistant\":\n return f;\n case \"fisheye-equisolid-angle\":\n return f * Math.cos(theta / 2);\n case \"fisheye-stereographic\":\n return f / Math.cos(theta / 2) ** 2;\n case \"fisheye-orthographic\":\n return f * Math.cos(theta);\n }\n}\n\ntype Matrix32 = readonly [readonly [number, number], readonly [number, number], readonly [number, number]];\n\n/** Unit direction of an image point and its derivative d(direction)/d(point) (3 x 2). */\nfunction directionWithDerivative(\n model: LensProjectionModel,\n f: number,\n px: number,\n py: number\n): { direction: LensDirection; derivative: Matrix32 } {\n const direction = lensProjectionDirectionValue(model, f, px, py);\n if (model === \"equirectangular\") {\n const lambda = px / f;\n const phi = py / f;\n const cp = Math.cos(phi);\n const sp = Math.sin(phi);\n const cl = Math.cos(lambda);\n const sl = Math.sin(lambda);\n return {\n direction,\n derivative: [\n [(cp * cl) / f, (-sp * sl) / f],\n [0, cp / f],\n [(-cp * sl) / f, (-sp * cl) / f]\n ]\n };\n }\n const rho = Math.hypot(px, py);\n if (rho === 0) {\n const a = 1 / radialDerivative(model, f, 0);\n return { direction, derivative: [[a, 0], [0, a], [0, 0]] };\n }\n const theta = radialProjectionFieldAngle(model, f, rho);\n const dTheta = 1 / radialDerivative(model, f, theta);\n const a = Math.sin(theta) / rho;\n const aPrime = (Math.cos(theta) * dTheta * rho - Math.sin(theta)) / (rho * rho);\n const ux = px / rho;\n const uy = py / rho;\n const dz = -Math.sin(theta) * dTheta;\n return {\n direction,\n derivative: [\n [a + aPrime * px * ux, aPrime * px * uy],\n [aPrime * py * ux, a + aPrime * py * uy],\n [dz * ux, dz * uy]\n ]\n };\n}\n\n/** Image point of a unit direction and its derivative d(point)/d(direction) (2 x 3). */\nfunction imagePointWithDerivative(\n model: LensProjectionModel,\n f: number,\n v: LensDirection\n): { point: { x: number; y: number }; derivative: readonly [readonly [number, number, number], readonly [number, number, number]] } {\n const point = lensProjectionImagePointValue(model, f, v.x, v.y, v.z).imagePointMm;\n if (model === \"equirectangular\") {\n const h2 = v.x * v.x + v.z * v.z;\n const c = Math.sqrt(Math.max(0, 1 - v.y * v.y));\n return {\n point,\n derivative: [\n [(f * v.z) / h2, 0, (-f * v.x) / h2],\n [0, f / c, 0]\n ]\n };\n }\n const t = Math.hypot(v.x, v.y);\n const theta = Math.atan2(t, v.z);\n if (theta < 1e-9) {\n const g = radialDerivative(model, f, 0);\n return { point, derivative: [[g, 0, 0], [0, g, 0]] };\n }\n const s = Math.sin(theta);\n const g = radialProjectionImageRadius(model, f, theta);\n const b = g / s;\n const bPrime = (radialDerivative(model, f, theta) * s - g * Math.cos(theta)) / (s * s);\n // q = b(theta) (vx, vy); on the unit sphere d theta = -d vz / sin theta.\n const k = -bPrime / s;\n return {\n point,\n derivative: [\n [b, 0, k * v.x],\n [0, b, k * v.y]\n ]\n };\n}\n\n/**\n * @internal Destination-to-source remap between two ideal projections: the destination point is\n * unprojected with the destination model and reprojected with the source model. Returns the\n * source point and the 2 x 2 Jacobian d(source)/d(destination), row-major.\n */\nexport function lensProjectionRemapValue(\n destinationModel: LensProjectionModel,\n destinationFocalLengthMm: number,\n sourceModel: LensProjectionModel,\n sourceFocalLengthMm: number,\n px: number,\n py: number\n): { sourcePointMm: { x: number; y: number }; jacobian: readonly [number, number, number, number] } {\n const inverse = directionWithDerivative(destinationModel, destinationFocalLengthMm, px, py);\n const forward = imagePointWithDerivative(sourceModel, sourceFocalLengthMm, inverse.direction);\n const d = inverse.derivative;\n const q = forward.derivative;\n const j = (row: 0 | 1, column: 0 | 1): number =>\n q[row][0] * d[0][column] + q[row][1] * d[1][column] + q[row][2] * d[2][column];\n return {\n sourcePointMm: forward.point,\n jacobian: [j(0, 0), j(0, 1), j(1, 0), j(1, 1)]\n };\n}\n\n/** @internal Signed angle along one axis through the optical axis (exported for sampling plans). */\nexport function lensProjectionSignedAxisAngle(\n model: LensProjectionModel,\n focalLengthMm: number,\n coordinateMm: number\n): number {\n return signedAxisAngle(model, focalLengthMm, coordinateMm);\n}\n"]}
1
+ {"version":3,"file":"lens-projection.js","sourceRoot":"","sources":["../../src/camera/lens-projection.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Ideal lens projection (mapping) functions. They convert between a scene direction and a point on the\n * image plane.\n *\n * A radial projection maps the field angle theta (the angle from the optical axis) to the image\n * radius r:\n * rectilinear r = f tan(theta) (gnomonic / pinhole)\n * equidistant r = f theta (\"f-theta\" fisheye)\n * equisolid-angle r = 2 f sin(theta / 2) (equal-area fisheye)\n * stereographic r = 2 f tan(theta / 2) (conformal fisheye)\n * orthographic r = f sin(theta)\n * The image point keeps the direction's azimuth about the optical axis, so every radial projection is\n * rotationally symmetric. Equirectangular (spherical, or cylindrical equidistant) is not radial:\n * x = f * longitude, y = f * latitude, where f is the image distance per radian.\n *\n * Sources: Kannala & Brandt 2006 (IEEE TPAMI 28:1335, generic camera model listing these\n * projections, eqs. 1-5; free author copy); Miyamoto 1964 (JOSA 54:1060, fish-eye projection\n * functions); Snyder 1987 (Map Projections - A Working Manual, USGS PP 1395, free) for the\n * equirectangular and stereographic projections. Also see: Kingslake 1989 (A History of the\n * Photographic Lens, ch. on fish-eye lenses); Ray 2002 (Applied Photographic Optics, 3rd ed., ch. 6\n * and 47).\n *\n * These are ideal mapping functions. Real fisheye lenses deviate from them. Modelling a real lens\n * would need a calibrated polynomial (for example Kannala-Brandt) with defensible provenance.\n *\n * Camera frame: +X right, +Y up, +Z along the optical axis toward the scene. Image-plane coordinates\n * use the same non-inverted +X right / +Y up convention as the rest of the engine.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { rejectUnknownFields, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/** Ideal projection model discriminator. `rectilinear` is the engine default. */\nexport type LensProjectionModel =\n | \"rectilinear\"\n | \"fisheye-equidistant\"\n | \"fisheye-equisolid-angle\"\n | \"fisheye-stereographic\"\n | \"fisheye-orthographic\"\n | \"equirectangular\";\n\n/** Radial (rotationally symmetric) projection models. */\nexport type RadialLensProjectionModel = Exclude<\n LensProjectionModel,\n \"equirectangular\"\n>;\n\nexport const LENS_PROJECTION_MODELS: readonly LensProjectionModel[] =\n /* @__PURE__ */ Object.freeze([\n \"rectilinear\",\n \"fisheye-equidistant\",\n \"fisheye-equisolid-angle\",\n \"fisheye-stereographic\",\n \"fisheye-orthographic\",\n \"equirectangular\"\n ]);\n\nexport interface LensDirection {\n /** Camera-frame direction component, +X right. Any positive length. */\n x: number;\n /** Camera-frame direction component, +Y up. */\n y: number;\n /** Camera-frame direction component along the optical axis toward the scene. */\n z: number;\n}\n\nexport interface CalculateLensProjectionImagePointInput {\n projectionModel: LensProjectionModel;\n /**\n * Projection scale in millimetres: the focal length f of the mapping function, or for\n * equirectangular the image distance per radian.\n */\n focalLengthMm: number;\n /** Scene direction in the camera frame. Need not be normalized; must be non-zero. */\n direction: LensDirection;\n}\n\nexport interface LensProjectionImagePoint {\n projectionModel: LensProjectionModel;\n imagePointMm: { x: number; y: number };\n /** Angle between the direction and the optical axis, in radians. */\n fieldAngleRadians: number;\n /** Distance of the image point from the optical axis, in millimetres. */\n imageRadiusMm: number;\n}\n\nexport interface CalculateLensProjectionDirectionInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Image-plane point relative to the optical axis, in millimetres. */\n imagePointMm: { x: number; y: number };\n}\n\nexport interface LensProjectionDirection {\n projectionModel: LensProjectionModel;\n /** Unit direction in the camera frame. */\n direction: LensDirection;\n fieldAngleRadians: number;\n imageRadiusMm: number;\n}\n\nexport interface CalculateLensProjectionFieldOfViewInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Centred sensor/capture dimension along one axis through the optical axis, in mm. */\n sensorDimensionMm: number;\n}\n\nexport interface CalculateLensProjectionFieldOfViewBoundsInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Signed minimum sensor coordinate along one axis through the optical axis, in mm. */\n minimumSensorCoordinateMm: number;\n /** Signed maximum sensor coordinate along the same axis, in mm. */\n maximumSensorCoordinateMm: number;\n}\n\nexport interface LensProjectionFieldOfView {\n projectionModel: LensProjectionModel;\n degrees: number;\n radians: number;\n /** Projection scale used by the mapping, in millimetres. */\n projectionDistanceMm: number;\n}\n\nexport interface LensProjectionFieldOfViewBounds {\n projectionModel: LensProjectionModel;\n minimumRadians: number;\n maximumRadians: number;\n radians: number;\n minimumDegrees: number;\n maximumDegrees: number;\n degrees: number;\n projectionDistanceMm: number;\n}\n\nconst HALF_PI = Math.PI / 2;\n\n/** Runtime parser for the projection discriminator. Rejects (fails closed on) unknown values. */\nexport function parseLensProjectionModel(\n value: unknown\n): LensProjectionModel {\n if (\n typeof value !== \"string\" ||\n !(LENS_PROJECTION_MODELS as readonly string[]).includes(value)\n ) {\n throw new InvalidConfigurationError(\n \"projectionModel must be one of \" +\n LENS_PROJECTION_MODELS.join(\", \") +\n \".\"\n );\n }\n return value as LensProjectionModel;\n}\n\nfunction requireRecord(name: string, value: unknown): void {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(`${name} must be an object.`);\n }\n}\n\nfunction requireFinite(name: string, value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be a finite number.`);\n }\n return value;\n}\n\n/**\n * Largest field angle (radians) a radial model maps, and whether the model can reach it.\n * Rectilinear, stereographic, equidistant and equisolid exclude the limit: the radius there is\n * infinite, or the azimuth of the backward direction is undefined. Orthographic reaches 90 degrees.\n */\nfunction radialFieldAngleLimit(model: RadialLensProjectionModel): {\n radians: number;\n inclusive: boolean;\n} {\n switch (model) {\n case \"rectilinear\":\n return { radians: HALF_PI, inclusive: false };\n case \"fisheye-orthographic\":\n return { radians: HALF_PI, inclusive: true };\n default:\n return { radians: Math.PI, inclusive: false };\n }\n}\n\n/** Image radius r(theta) for one radial model. */\nexport function radialProjectionImageRadius(\n model: RadialLensProjectionModel,\n focalLengthMm: number,\n fieldAngleRadians: number\n): number {\n switch (model) {\n case \"rectilinear\":\n return focalLengthMm * Math.tan(fieldAngleRadians);\n case \"fisheye-equidistant\":\n return focalLengthMm * fieldAngleRadians;\n case \"fisheye-equisolid-angle\":\n return 2 * focalLengthMm * Math.sin(fieldAngleRadians / 2);\n case \"fisheye-stereographic\":\n return 2 * focalLengthMm * Math.tan(fieldAngleRadians / 2);\n case \"fisheye-orthographic\":\n return focalLengthMm * Math.sin(fieldAngleRadians);\n }\n}\n\n/**\n * Largest image radius a radial model reaches inside its field-angle domain, in mm\n * (Infinity for rectilinear and stereographic).\n */\nfunction radialImageRadiusLimit(\n model: RadialLensProjectionModel,\n focalLengthMm: number\n): { radiusMm: number; inclusive: boolean } {\n switch (model) {\n case \"rectilinear\":\n case \"fisheye-stereographic\":\n return { radiusMm: Number.POSITIVE_INFINITY, inclusive: false };\n case \"fisheye-equidistant\":\n return { radiusMm: Math.PI * focalLengthMm, inclusive: false };\n case \"fisheye-equisolid-angle\":\n return { radiusMm: 2 * focalLengthMm, inclusive: false };\n case \"fisheye-orthographic\":\n return { radiusMm: focalLengthMm, inclusive: true };\n }\n}\n\n/**\n * @internal Whether an image radius lies inside a radial model's image circle (the domain of {@link radialProjectionFieldAngle}).\n * Equirectangular is not radial and returns true.\n */\nexport function insideRadialImageCircle(model: LensProjectionModel, focalLengthMm: number, imageRadiusMm: number): boolean {\n if (model === \"equirectangular\") return true;\n const limit = radialImageRadiusLimit(model, focalLengthMm);\n return imageRadiusMm >= 0 && (imageRadiusMm < limit.radiusMm || (limit.inclusive && imageRadiusMm === limit.radiusMm));\n}\n\n/** Inverse theta(r) for one radial model; throws outside the image circle. */\nexport function radialProjectionFieldAngle(\n model: RadialLensProjectionModel,\n focalLengthMm: number,\n imageRadiusMm: number\n): number {\n const limit = radialImageRadiusLimit(model, focalLengthMm);\n if (\n imageRadiusMm < 0 ||\n imageRadiusMm > limit.radiusMm ||\n (!limit.inclusive && imageRadiusMm === limit.radiusMm)\n ) {\n throw new InvalidScientificInputError(\n `Image radius lies outside the ${model} image circle.`\n );\n }\n switch (model) {\n case \"rectilinear\":\n return Math.atan(imageRadiusMm / focalLengthMm);\n case \"fisheye-equidistant\":\n return imageRadiusMm / focalLengthMm;\n case \"fisheye-equisolid-angle\":\n return 2 * Math.asin(imageRadiusMm / (2 * focalLengthMm));\n case \"fisheye-stereographic\":\n return 2 * Math.atan(imageRadiusMm / (2 * focalLengthMm));\n case \"fisheye-orthographic\":\n return Math.asin(Math.min(1, imageRadiusMm / focalLengthMm));\n }\n}\n\nfunction assumptions(model: LensProjectionModel): readonly string[] {\n const common = [\n \"Ideal mapping function of a perfect projection; real lenses deviate from it and need calibrated distortion data\",\n \"Camera frame +X right, +Y up, +Z toward the scene; image plane +X right, +Y up (non-inverted)\"\n ];\n if (model === \"equirectangular\") {\n return [\n \"Equirectangular (spherical) projection: x = f * longitude, y = f * latitude, longitude measured about +Y from +Z\",\n ...common\n ];\n }\n return [\n `Radial projection ${model}; image azimuth equals scene azimuth about the optical axis`,\n ...common\n ];\n}\n\nfunction validateCommon(\n input: { projectionModel: unknown; focalLengthMm: number },\n name: string\n): LensProjectionModel {\n requireRecord(name, input);\n const model = parseLensProjectionModel(input.projectionModel);\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n return model;\n}\n\n/**\n * Maps one camera-frame scene direction to its ideal image-plane point under the declared\n * projection model.\n */\nexport function calculateLensProjectionImagePoint(\n input: CalculateLensProjectionImagePointInput\n): CalculationResult<LensProjectionImagePoint> {\n requireInputKeys(input, \"calculateLensProjectionImagePoint\", [\"projectionModel\", \"focalLengthMm\", \"direction\"]);\n const model = validateCommon(input, \"input\");\n requireRecord(\"direction\", input.direction);\n rejectUnknownFields(input.direction, [\"x\", \"y\", \"z\"], \"direction\");\n const x = requireFinite(\"direction.x\", input.direction.x);\n const y = requireFinite(\"direction.y\", input.direction.y);\n const z = requireFinite(\"direction.z\", input.direction.z);\n const length = Math.hypot(x, y, z);\n if (!(length > 0) || !Number.isFinite(length)) {\n throw new InvalidScientificInputError(\n \"direction must be a finite non-zero vector.\"\n );\n }\n const { imagePointMm, fieldAngleRadians } = lensProjectionImagePointValue(\n model,\n input.focalLengthMm,\n x,\n y,\n z\n );\n\n return calculatedResult(\n {\n projectionModel: model,\n imagePointMm,\n fieldAngleRadians,\n imageRadiusMm: Math.hypot(imagePointMm.x, imagePointMm.y)\n },\n `${model}-lens-projection`,\n \"1.0.0\",\n assumptions(model)\n );\n}\n\n/**\n * Inverse-maps one image-plane point to its unit camera-frame scene direction under the declared\n * projection model. It rejects points outside the model's image circle. For equirectangular, it\n * rejects points outside +/-pi longitude and +/-pi/2 latitude.\n */\nexport function calculateLensProjectionDirection(\n input: CalculateLensProjectionDirectionInput\n): CalculationResult<LensProjectionDirection> {\n requireInputKeys(input, \"calculateLensProjectionDirection\", [\"projectionModel\", \"focalLengthMm\", \"imagePointMm\"]);\n const model = validateCommon(input, \"input\");\n requireRecord(\"imagePointMm\", input.imagePointMm);\n rejectUnknownFields(input.imagePointMm, [\"x\", \"y\"], \"imagePointMm\");\n const px = requireFinite(\"imagePointMm.x\", input.imagePointMm.x);\n const py = requireFinite(\"imagePointMm.y\", input.imagePointMm.y);\n const imageRadiusMm = Math.hypot(px, py);\n const direction = lensProjectionDirectionValue(\n model,\n input.focalLengthMm,\n px,\n py\n );\n\n return calculatedResult(\n {\n projectionModel: model,\n direction,\n fieldAngleRadians: Math.atan2(\n Math.hypot(direction.x, direction.y),\n direction.z\n ),\n imageRadiusMm\n },\n `inverse-${model}-lens-projection`,\n \"1.0.0\",\n assumptions(model)\n );\n}\n\n/** Signed angle subtended along one axis through the optical axis for a signed coordinate. */\nfunction signedAxisAngle(\n model: LensProjectionModel,\n focalLengthMm: number,\n coordinateMm: number\n): number {\n if (model === \"equirectangular\") {\n // Along a horizontal or vertical line through the centre the angle is linear in position.\n return coordinateMm / focalLengthMm;\n }\n const theta = radialProjectionFieldAngle(\n model,\n focalLengthMm,\n Math.abs(coordinateMm)\n );\n return coordinateMm < 0 ? -theta : theta;\n}\n\n/**\n * Angular field of view across a centred sensor dimension along one axis through the optical\n * axis: 2 * theta(d / 2) for radial models and d / f for equirectangular.\n *\n * For equirectangular, the result is the longitude span for a horizontal dimension and the latitude\n * span for a vertical one. A sensor dimension larger than a fisheye's image circle is rejected, not\n * clipped.\n */\nexport function calculateLensProjectionFieldOfView(\n input: CalculateLensProjectionFieldOfViewInput\n): CalculationResult<LensProjectionFieldOfView> {\n requireInputKeys(input, \"calculateLensProjectionFieldOfView\", [\"projectionModel\", \"focalLengthMm\", \"sensorDimensionMm\"]);\n const model = validateCommon(input, \"input\");\n requirePositiveFinite(\"sensorDimensionMm\", input.sensorDimensionMm);\n const radians =\n 2 *\n signedAxisAngle(model, input.focalLengthMm, input.sensorDimensionMm / 2);\n if (model === \"equirectangular\" && radians > 2 * Math.PI) {\n throw new InvalidScientificInputError(\n \"sensorDimensionMm exceeds the full 360-degree equirectangular span.\"\n );\n }\n return calculatedResult(\n {\n projectionModel: model,\n radians,\n degrees: (radians * 180) / Math.PI,\n projectionDistanceMm: input.focalLengthMm\n },\n `${model}-field-of-view`,\n \"1.0.0\",\n [\n ...assumptions(model),\n \"Field of view is measured along one axis through the optical axis\"\n ]\n );\n}\n\n/**\n * Asymmetric angular span between two signed sensor coordinates on one axis through the optical\n * axis, for off-centre crops.\n */\nexport function calculateLensProjectionFieldOfViewBounds(\n input: CalculateLensProjectionFieldOfViewBoundsInput\n): CalculationResult<LensProjectionFieldOfViewBounds> {\n requireInputKeys(input, \"calculateLensProjectionFieldOfViewBounds\", [\"projectionModel\", \"focalLengthMm\", \"minimumSensorCoordinateMm\", \"maximumSensorCoordinateMm\"]);\n const model = validateCommon(input, \"input\");\n const minimum = requireFinite(\n \"minimumSensorCoordinateMm\",\n input.minimumSensorCoordinateMm\n );\n const maximum = requireFinite(\n \"maximumSensorCoordinateMm\",\n input.maximumSensorCoordinateMm\n );\n if (maximum <= minimum) {\n throw new InvalidScientificInputError(\n \"maximumSensorCoordinateMm must be greater than minimumSensorCoordinateMm.\"\n );\n }\n const minimumRadians = signedAxisAngle(model, input.focalLengthMm, minimum);\n const maximumRadians = signedAxisAngle(model, input.focalLengthMm, maximum);\n const radians = maximumRadians - minimumRadians;\n if (model === \"equirectangular\" && radians > 2 * Math.PI) {\n throw new InvalidScientificInputError(\n \"Sensor bounds exceed the full 360-degree equirectangular span.\"\n );\n }\n return calculatedResult(\n {\n projectionModel: model,\n minimumRadians,\n maximumRadians,\n radians,\n minimumDegrees: (minimumRadians * 180) / Math.PI,\n maximumDegrees: (maximumRadians * 180) / Math.PI,\n degrees: (radians * 180) / Math.PI,\n projectionDistanceMm: input.focalLengthMm\n },\n `asymmetric-${model}-field-of-view`,\n \"1.0.0\",\n [\n ...assumptions(model),\n \"Signed sensor-plane bounds are measured relative to the optical axis along one axis through it\"\n ]\n );\n}\n\n/**\n * @internal Image point of a non-zero camera-frame direction under one model (no envelope).\n * Shared by the public mapping, geometric remaps and the environment ray query.\n */\nexport function lensProjectionImagePointValue(\n model: LensProjectionModel,\n focalLengthMm: number,\n x: number,\n y: number,\n z: number\n): { imagePointMm: { x: number; y: number }; fieldAngleRadians: number } {\n const length = Math.hypot(x, y, z);\n const f = focalLengthMm;\n const transverse = Math.hypot(x, y);\n const fieldAngleRadians = Math.atan2(transverse, z);\n\n let imagePointMm: { x: number; y: number };\n if (model === \"equirectangular\") {\n const latitude = Math.asin(Math.max(-1, Math.min(1, y / length)));\n const horizontal = Math.hypot(x, z);\n if (horizontal === 0) {\n // Poles have an undefined longitude; the projection is singular there.\n throw new InvalidScientificInputError(\n \"Equirectangular projection is undefined at the poles (direction parallel to +/-Y).\"\n );\n }\n const longitude = Math.atan2(x, z);\n imagePointMm = { x: f * longitude, y: f * latitude };\n } else {\n const limit = radialFieldAngleLimit(model);\n if (\n fieldAngleRadians > limit.radians ||\n (!limit.inclusive && fieldAngleRadians === limit.radians)\n ) {\n throw new InvalidScientificInputError(\n `direction lies outside the ${model} projection domain.`\n );\n }\n const radius = radialProjectionImageRadius(model, f, fieldAngleRadians);\n imagePointMm =\n transverse === 0\n ? { x: 0, y: 0 }\n : { x: (radius * x) / transverse, y: (radius * y) / transverse };\n }\n\n return { imagePointMm, fieldAngleRadians };\n}\n\n/** @internal Unit camera-frame direction of one image point under one model (no envelope). */\nexport function lensProjectionDirectionValue(\n model: LensProjectionModel,\n focalLengthMm: number,\n px: number,\n py: number\n): LensDirection {\n const f = focalLengthMm;\n const imageRadiusMm = Math.hypot(px, py);\n\n let direction: LensDirection;\n if (model === \"equirectangular\") {\n const longitude = px / f;\n const latitude = py / f;\n if (Math.abs(longitude) > Math.PI || Math.abs(latitude) > HALF_PI) {\n throw new InvalidScientificInputError(\n \"imagePointMm lies outside the equirectangular longitude/latitude domain.\"\n );\n }\n const cosLatitude = Math.cos(latitude);\n direction = {\n x: cosLatitude * Math.sin(longitude),\n y: Math.sin(latitude),\n z: cosLatitude * Math.cos(longitude)\n };\n } else {\n const theta = radialProjectionFieldAngle(model, f, imageRadiusMm);\n const sinTheta = Math.sin(theta);\n direction =\n imageRadiusMm === 0\n ? { x: 0, y: 0, z: 1 }\n : {\n x: (sinTheta * px) / imageRadiusMm,\n y: (sinTheta * py) / imageRadiusMm,\n z: Math.cos(theta)\n };\n }\n\n return direction;\n}\n\n/** d r / d theta of a radial model. */\nfunction radialDerivative(\n model: RadialLensProjectionModel,\n f: number,\n theta: number\n): number {\n switch (model) {\n case \"rectilinear\":\n return f / Math.cos(theta) ** 2;\n case \"fisheye-equidistant\":\n return f;\n case \"fisheye-equisolid-angle\":\n return f * Math.cos(theta / 2);\n case \"fisheye-stereographic\":\n return f / Math.cos(theta / 2) ** 2;\n case \"fisheye-orthographic\":\n return f * Math.cos(theta);\n }\n}\n\ntype Matrix32 = readonly [readonly [number, number], readonly [number, number], readonly [number, number]];\n\n/** Unit direction of an image point and its derivative d(direction)/d(point) (3 x 2). */\nfunction directionWithDerivative(\n model: LensProjectionModel,\n f: number,\n px: number,\n py: number\n): { direction: LensDirection; derivative: Matrix32 } {\n const direction = lensProjectionDirectionValue(model, f, px, py);\n if (model === \"equirectangular\") {\n const lambda = px / f;\n const phi = py / f;\n const cp = Math.cos(phi);\n const sp = Math.sin(phi);\n const cl = Math.cos(lambda);\n const sl = Math.sin(lambda);\n return {\n direction,\n derivative: [\n [(cp * cl) / f, (-sp * sl) / f],\n [0, cp / f],\n [(-cp * sl) / f, (-sp * cl) / f]\n ]\n };\n }\n const rho = Math.hypot(px, py);\n if (rho === 0) {\n const a = 1 / radialDerivative(model, f, 0);\n return { direction, derivative: [[a, 0], [0, a], [0, 0]] };\n }\n const theta = radialProjectionFieldAngle(model, f, rho);\n const dTheta = 1 / radialDerivative(model, f, theta);\n const a = Math.sin(theta) / rho;\n const aPrime = (Math.cos(theta) * dTheta * rho - Math.sin(theta)) / (rho * rho);\n const ux = px / rho;\n const uy = py / rho;\n const dz = -Math.sin(theta) * dTheta;\n return {\n direction,\n derivative: [\n [a + aPrime * px * ux, aPrime * px * uy],\n [aPrime * py * ux, a + aPrime * py * uy],\n [dz * ux, dz * uy]\n ]\n };\n}\n\n/** Image point of a unit direction and its derivative d(point)/d(direction) (2 x 3). */\nfunction imagePointWithDerivative(\n model: LensProjectionModel,\n f: number,\n v: LensDirection\n): { point: { x: number; y: number }; derivative: readonly [readonly [number, number, number], readonly [number, number, number]] } {\n const point = lensProjectionImagePointValue(model, f, v.x, v.y, v.z).imagePointMm;\n if (model === \"equirectangular\") {\n const h2 = v.x * v.x + v.z * v.z;\n const c = Math.sqrt(Math.max(0, 1 - v.y * v.y));\n return {\n point,\n derivative: [\n [(f * v.z) / h2, 0, (-f * v.x) / h2],\n [0, f / c, 0]\n ]\n };\n }\n const t = Math.hypot(v.x, v.y);\n const theta = Math.atan2(t, v.z);\n if (theta < 1e-9) {\n const g = radialDerivative(model, f, 0);\n return { point, derivative: [[g, 0, 0], [0, g, 0]] };\n }\n const s = Math.sin(theta);\n const g = radialProjectionImageRadius(model, f, theta);\n const b = g / s;\n const bPrime = (radialDerivative(model, f, theta) * s - g * Math.cos(theta)) / (s * s);\n // q = b(theta) (vx, vy); on the unit sphere d theta = -d vz / sin theta.\n const k = -bPrime / s;\n return {\n point,\n derivative: [\n [b, 0, k * v.x],\n [0, b, k * v.y]\n ]\n };\n}\n\n/**\n * @internal Destination-to-source remap between two ideal projections: the destination point is\n * unprojected with the destination model and reprojected with the source model. Returns the\n * source point and the 2 x 2 Jacobian d(source)/d(destination), row-major.\n */\nexport function lensProjectionRemapValue(\n destinationModel: LensProjectionModel,\n destinationFocalLengthMm: number,\n sourceModel: LensProjectionModel,\n sourceFocalLengthMm: number,\n px: number,\n py: number\n): { sourcePointMm: { x: number; y: number }; jacobian: readonly [number, number, number, number] } {\n const inverse = directionWithDerivative(destinationModel, destinationFocalLengthMm, px, py);\n const forward = imagePointWithDerivative(sourceModel, sourceFocalLengthMm, inverse.direction);\n const d = inverse.derivative;\n const q = forward.derivative;\n const j = (row: 0 | 1, column: 0 | 1): number =>\n q[row][0] * d[0][column] + q[row][1] * d[1][column] + q[row][2] * d[2][column];\n return {\n sourcePointMm: forward.point,\n jacobian: [j(0, 0), j(0, 1), j(1, 0), j(1, 1)]\n };\n}\n\n/** @internal Signed angle along one axis through the optical axis (exported for sampling plans). */\nexport function lensProjectionSignedAxisAngle(\n model: LensProjectionModel,\n focalLengthMm: number,\n coordinateMm: number\n): number {\n return signedAxisAngle(model, focalLengthMm, coordinateMm);\n}\n"]}