@photivra/engine 2.4.1 → 2.6.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 (85) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +1 -0
  6. package/dist/capture/native-environment-lean.d.ts +1 -1
  7. package/dist/capture/native-environment-lean.js +17 -3
  8. package/dist/capture/native-environment-lean.js.map +1 -1
  9. package/dist/capture/native-environment-raw.d.ts +0 -4
  10. package/dist/capture/native-environment-raw.js +6 -2
  11. package/dist/capture/native-environment-raw.js.map +1 -1
  12. package/dist/color/illuminants.d.ts +15 -0
  13. package/dist/color/illuminants.js +81 -24
  14. package/dist/color/illuminants.js.map +1 -1
  15. package/dist/color/srgb-transfer.d.ts +6 -0
  16. package/dist/color/srgb-transfer.js +9 -0
  17. package/dist/color/srgb-transfer.js.map +1 -1
  18. package/dist/color/white-balance.d.ts +6 -0
  19. package/dist/color/white-balance.js +15 -2
  20. package/dist/color/white-balance.js.map +1 -1
  21. package/dist/core/internal-radiance-evaluator.d.ts +6 -0
  22. package/dist/core/internal-radiance-evaluator.js +33 -0
  23. package/dist/core/internal-radiance-evaluator.js.map +1 -0
  24. package/dist/core/portable-math.d.ts +33 -0
  25. package/dist/core/portable-math.js +342 -0
  26. package/dist/core/portable-math.js.map +1 -1
  27. package/dist/core/version.d.ts +1 -1
  28. package/dist/core/version.js +1 -1
  29. package/dist/core/version.js.map +1 -1
  30. package/dist/facade/chart-scenes.d.ts +9 -0
  31. package/dist/facade/chart-scenes.js +17 -4
  32. package/dist/facade/chart-scenes.js.map +1 -1
  33. package/dist/facade/diffraction-provider.d.ts +10 -0
  34. package/dist/facade/diffraction-provider.js +38 -17
  35. package/dist/facade/diffraction-provider.js.map +1 -1
  36. package/dist/facade/display-view.d.ts +208 -0
  37. package/dist/facade/display-view.js +1461 -0
  38. package/dist/facade/display-view.js.map +1 -0
  39. package/dist/facade/scene-radiance.d.ts +7 -0
  40. package/dist/facade/scene-radiance.js +18 -5
  41. package/dist/facade/scene-radiance.js.map +1 -1
  42. package/dist/facade/simulate-capture.d.ts +229 -5
  43. package/dist/facade/simulate-capture.js +119 -35
  44. package/dist/facade/simulate-capture.js.map +1 -1
  45. package/dist/facade/simulate-display-view.d.ts +21 -0
  46. package/dist/facade/simulate-display-view.js +168 -0
  47. package/dist/facade/simulate-display-view.js.map +1 -0
  48. package/dist/facade/subject-framing.d.ts +78 -0
  49. package/dist/facade/subject-framing.js +69 -20
  50. package/dist/facade/subject-framing.js.map +1 -1
  51. package/dist/index.d.ts +3 -2
  52. package/dist/index.js +3 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/optics/diffraction-psf-profile.d.ts +59 -3
  55. package/dist/optics/diffraction-psf-profile.js +147 -5
  56. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  57. package/dist/optics/irradiance-convolution.js +1 -1
  58. package/dist/optics/irradiance-convolution.js.map +1 -1
  59. package/dist/optics/scene-to-sensor-irradiance.d.ts +7 -0
  60. package/dist/optics/scene-to-sensor-irradiance.js +10 -5
  61. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  62. package/dist/output/print-plan.d.ts +8 -1
  63. package/dist/output/print-plan.js +8 -1
  64. package/dist/output/print-plan.js.map +1 -1
  65. package/dist/schema/scene-radiance.d.ts +16 -0
  66. package/dist/schema/scene-radiance.js +25 -4
  67. package/dist/schema/scene-radiance.js.map +1 -1
  68. package/dist/sensor/raw-readout.d.ts +12 -0
  69. package/dist/sensor/raw-readout.js +131 -9
  70. package/dist/sensor/raw-readout.js.map +1 -1
  71. package/dist/sensor/stochastic-seed.d.ts +13 -0
  72. package/dist/sensor/stochastic-seed.js +42 -8
  73. package/dist/sensor/stochastic-seed.js.map +1 -1
  74. package/docs/AGENT_GUIDE.md +1 -0
  75. package/docs/API_REFERENCE.md +16 -1
  76. package/docs/QUICK_START.md +245 -9
  77. package/docs/USAGE.md +15 -0
  78. package/docs/api/core-version.md +1 -1
  79. package/docs/api/exports.json +37 -2
  80. package/docs/api/facade-simulate-capture.md +70 -2
  81. package/docs/api/facade-simulate-display-view.md +18 -0
  82. package/docs/api/facade-subject-framing.md +83 -0
  83. package/docs/api/optics-diffraction-psf-profile.md +8 -1
  84. package/docs/api/output-print-plan.md +8 -1
  85. package/package.json +3 -2
@@ -1,6 +1,6 @@
1
1
  {
2
- "packageVersion": "2.4.1",
3
- "engineApiVersion": "2.4.1",
2
+ "packageVersion": "2.6.0",
3
+ "engineApiVersion": "2.6.0",
4
4
  "exports": [
5
5
  {
6
6
  "name": "AbsoluteUncertaintyEstimate",
@@ -437,6 +437,16 @@
437
437
  "source": "src/output/crop.ts",
438
438
  "kind": "type"
439
439
  },
440
+ {
441
+ "name": "calculateChartFraming",
442
+ "source": "src/facade/subject-framing.ts",
443
+ "kind": "runtime"
444
+ },
445
+ {
446
+ "name": "CalculateChartFramingInput",
447
+ "source": "src/facade/subject-framing.ts",
448
+ "kind": "type"
449
+ },
440
450
  {
441
451
  "name": "calculateChartRadianceBatch",
442
452
  "source": "src/facade/chart-radiance-batch.ts",
@@ -1842,6 +1852,11 @@
1842
1852
  "source": "src/facade/chart-scenes.ts",
1843
1853
  "kind": "type"
1844
1854
  },
1855
+ {
1856
+ "name": "ChartFraming",
1857
+ "source": "src/facade/subject-framing.ts",
1858
+ "kind": "type"
1859
+ },
1845
1860
  {
1846
1861
  "name": "ChartRadianceBatchInput",
1847
1862
  "source": "src/facade/chart-radiance-batch.ts",
@@ -2567,6 +2582,11 @@
2567
2582
  "source": "src/core/spectral-composition.ts",
2568
2583
  "kind": "type"
2569
2584
  },
2585
+ {
2586
+ "name": "DisplayViewResult",
2587
+ "source": "src/facade/simulate-capture.ts",
2588
+ "kind": "type"
2589
+ },
2570
2590
  {
2571
2591
  "name": "DistributeIntegratedQuantityAcrossDiscreteLinesInput",
2572
2592
  "source": "src/core/spectral-composition.ts",
@@ -7892,6 +7912,11 @@
7892
7912
  "source": "src/facade/simulate-capture.ts",
7893
7913
  "kind": "runtime"
7894
7914
  },
7915
+ {
7916
+ "name": "SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS",
7917
+ "source": "src/facade/simulate-capture.ts",
7918
+ "kind": "runtime"
7919
+ },
7895
7920
  {
7896
7921
  "name": "simulateCapture",
7897
7922
  "source": "src/facade/simulate-capture.ts",
@@ -7987,6 +8012,16 @@
7987
8012
  "source": "src/capture/simulated-capture.ts",
7988
8013
  "kind": "type"
7989
8014
  },
8015
+ {
8016
+ "name": "simulateDisplayView",
8017
+ "source": "src/facade/simulate-display-view.ts",
8018
+ "kind": "runtime"
8019
+ },
8020
+ {
8021
+ "name": "SimulateDisplayViewOptions",
8022
+ "source": "src/facade/simulate-capture.ts",
8023
+ "kind": "type"
8024
+ },
7990
8025
  {
7991
8026
  "name": "simulateEnvironmentSensorRawFrame",
7992
8027
  "source": "src/capture/environment-raw-producer.ts",
@@ -2,6 +2,42 @@
2
2
 
3
3
  The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
4
 
5
+ ## DisplayViewResult
6
+
7
+ Result of {@link simulateDisplayView }: the full-resolution capture resized for display, estimated; not a capture.
8
+
9
+ ```ts
10
+ export interface DisplayViewResult {
11
+ kind: "display-view";
12
+ /** Human-readable label for a user interface. */
13
+ label: string;
14
+ facadeVersion: typeof SIMULATE_CAPTURE_FACADE_VERSION;
15
+ engineApiVersion: string;
16
+ /** The display view (or its region): interleaved sRGB samples, row by row from the top-left. */
17
+ image: { width: number; height: number; format: "srgb-8" | "srgb-16"; samples: Uint8Array | Uint16Array };
18
+ /**
19
+ * The view: display size, the capture frame it shows (output pixels), real pixels per display pixel along each axis,
20
+ * the samples per axis and, with `view.region`, the rendered rectangle.
21
+ */
22
+ view: { width: number; height: number; frameWidth: number; frameHeight: number; scaleX: number; scaleY: number; samplesPerAxis: number;
23
+ region?: { x: number; y: number; width: number; height: number } };
24
+ /** The resolved exposure of the capture this view shows. */
25
+ exposure: { mode: SimulateCaptureExposureMode; settings: { fNumber: number; shutterSeconds: number; iso: number }; ev100: number };
26
+ scene: SimulateCaptureResult["scene"];
27
+ /**
28
+ * Work done: real pixels sampled, real-site expectations evaluated, scene queries, display pixels sampled in full (detail) or with
29
+ * a quarter of the samples per axis (uniform), display pixels whose value includes the AHD low-signal correction, and wall-clock
30
+ * milliseconds (diagnostic only; never affects the samples).
31
+ */
32
+ diagnostics: { sampleCount: number; siteEvaluations: number; sceneLookups: number; detailPixelCount: number; uniformPixelCount: number; ahdCorrectedPixelCount: number;
33
+ timingsMs: { view: number; total: number } };
34
+ provenance: { scientificStatus: "approximation"; cameraPresetId: string; cameraPresetVersion: string; evidence: readonly EvidenceProvenance[];
35
+ composedContracts: readonly string[]; limitations: readonly string[] };
36
+ /** The capture request's assumptions, then `view.definition`, `view.estimation`, `view.noise`, `view.demosaic` and `view.source`. */
37
+ assumptions: readonly SimulateCaptureAssumption[];
38
+ }
39
+ ```
40
+
5
41
  ## IdealReferenceResult
6
42
 
7
43
  Result of {@link simulateIdealReference}: an ideal reference image, not a capture (no RAW, plan or request).
@@ -65,6 +101,14 @@ Version of how the facade builds requests. It changes whenever the same input wo
65
101
  SIMULATE_CAPTURE_FACADE_VERSION = "1.0.0" as const
66
102
  ```
67
103
 
104
+ ## SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS
105
+
106
+ Default stratified samples per axis of a display view (`view.samplesPerAxis`): 16 × 16 sampled real pixels per display pixel with detail, 4 × 4 where it is uniform.
107
+
108
+ ```ts
109
+ SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS = 16
110
+ ```
111
+
68
112
  ## simulateCapture
69
113
 
70
114
  Simulates one still capture of a planar scene and returns the developed image, the packed RAW, the full
@@ -285,14 +329,20 @@ export interface SimulateCaptureOptions {
285
329
  * the whole render: framing, metering, exposure, noise seeds (absolute native site indices), rolling-shutter row
286
330
  * timing, diffraction and demosaic all use the whole frame. Use it to tile a large frame across workers or to
287
331
  * render a 100 % crop. `result.image` is the region; `result.region` reports it. Not combinable with
288
- * `whiteBalance: "auto"`, whose statistics would come from the region only.
332
+ * `whiteBalance: "auto"`, whose statistics would come from the region only. From 2.6 the diffraction convolution
333
+ * covers only the region plus the kernel and filter reach, so a small region of a large frame costs about as much
334
+ * as a capture of the region's size.
289
335
  */
290
336
  region?: { x: number; y: number; width: number; height: number };
291
337
  };
292
338
  /** Camera motion and shutter type. Default: still camera, global electronic shutter. */
293
339
  motion?: SimulateCaptureMotion;
294
340
  sensor?: {
295
- /** Pixel pitch in micrometres. Default 6. */
341
+ /**
342
+ * Pixel pitch in micrometres, 0.5–1000. Default 6. The simulated sensor is output width × pitch wide. 2.5: above 100 µm the
343
+ * capture is a downsized view of a whole frame (for example 36 mm / 48 pixels = 750 µm), recorded in the assumption
344
+ * `sensor.wholeFrameView`: brightness is unchanged and the noise is that of a binned view, not of the real sensor's pixels.
345
+ */
296
346
  pixelPitchMicrometers?: number;
297
347
  /** Maximum spectral quadrature subinterval in nm. Default 20. */
298
348
  spectralStepNanometers?: number;
@@ -499,6 +549,24 @@ export type SimulateCaptureWhiteBalance =
499
549
  | { channelGains: { red: number; green: number; blue: number } };
500
550
  ```
501
551
 
552
+ ## SimulateDisplayViewOptions
553
+
554
+ The display size and sampling of {@link simulateDisplayView }.
555
+
556
+ ```ts
557
+ export interface SimulateDisplayViewOptions {
558
+ /** Display pixels, 1 to the capture's output width and height. */
559
+ width: number;
560
+ height: number;
561
+ /** Stratified Monte Carlo samples per axis for each display pixel's expected value (1–32, default 8: 64 real pixels). */
562
+ samplesPerAxis?: number;
563
+ /** "srgb-8" (default) or "srgb-16". */
564
+ format?: "srgb-8" | "srgb-16";
565
+ /** Render only this rectangle of the display view, in display pixels; byte-identical to the same pixels of the whole view. */
566
+ region?: { x: number; y: number; width: number; height: number };
567
+ }
568
+ ```
569
+
502
570
  ## simulateIdealReference
503
571
 
504
572
  The ideal reference of a capture request (2.3): the same scene, framing, chart size, output size, region and pixel
@@ -0,0 +1,18 @@
1
+ # facade/simulate-display-view.ts public contracts
2
+
3
+ The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
+
5
+ ## simulateDisplayView
6
+
7
+ The display view of a capture request: the image the real full-resolution capture (`simulateCapture(options)`) would
8
+ give after it is resized to `view.width` × `view.height` by area averaging in linear light, estimated at a cost that
9
+ follows the display pixels, not the capture's. Optics (lens diffraction, the anti-aliasing filter, the pixel aperture)
10
+ act at the real pixel pitch; the colour mosaic, demosaic and development are those of the real pixels; the noise is
11
+ that of the average of the real pixels, demosaic correlation included; exposure, white balance and tone are the
12
+ capture's. The result is labelled `kind: "display-view"`; it is not a capture.
13
+
14
+ ```ts
15
+ export async function simulateDisplayView(options: SimulateCaptureOptions, view: SimulateDisplayViewOptions): Promise<DisplayViewResult>;
16
+ ```
17
+
18
+ This call is asynchronous; byte hashing uses the caller runtime's Web Crypto.
@@ -2,6 +2,39 @@
2
2
 
3
3
  The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
4
 
5
+ ## calculateChartFraming
6
+
7
+ How much of the frame a chart of a given physical size covers at any subject distance (2.5): magnification, framed plane size,
8
+ coverage fractions (above 1 when cropped), the chart's image size and the minimum-focus check. The same thin-lens framing as
9
+ `simulateCapture` (`scene.framing`, `result.scene.chart`) and `calculateSubjectDistanceForFraming`.
10
+
11
+ ```ts
12
+ export function calculateChartFraming(input: CalculateChartFramingInput): ChartFraming;
13
+ ```
14
+
15
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
16
+
17
+ ## CalculateChartFramingInput
18
+
19
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
20
+
21
+ ```ts
22
+ export interface CalculateChartFramingInput {
23
+ /** Active sensor (or simulated frame) size, mm: for `simulateCapture`, output width × height × pixel pitch. */
24
+ sensor: { widthMm: number; heightMm: number };
25
+ /** Physical focal length, mm. Give exactly one of this and `equivalentFocalLengthMm`. */
26
+ focalLengthMm?: number;
27
+ /** Full-frame-equivalent focal length (diagonal crop factor); a generic prime's value also reports that lens (as `lens.prime`). */
28
+ equivalentFocalLengthMm?: number;
29
+ /** Chart size as for `simulateCapture` `chartSize`: heightM defaults to widthM × the sensor aspect (height / width). */
30
+ chart: { widthM: number; heightM?: number };
31
+ /** Distance from the lens to the chart plane, metres; more than the focal length. */
32
+ subjectDistanceM: number;
33
+ /** Focus distance, metres (as `lens.focusDistanceM`); default the subject distance. More than the focal length. */
34
+ focusDistanceM?: number;
35
+ }
36
+ ```
37
+
5
38
  ## calculateSubjectDistanceForFraming
6
39
 
7
40
  The subject distance that fits a chart of a given physical size to the frame, with magnification, coverage and the minimum-focus check (2.3).
@@ -30,6 +63,48 @@ export interface CalculateSubjectDistanceForFramingInput {
30
63
  }
31
64
  ```
32
65
 
66
+ ## ChartFraming
67
+
68
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
69
+
70
+ ```ts
71
+ export interface ChartFraming {
72
+ subjectDistanceM: number;
73
+ focusDistanceM: number;
74
+ /** Thin-lens image distance at the focus distance, f·s/(s − f), mm. */
75
+ imageDistanceMm: number;
76
+ /** Image size ÷ object size on the chart plane, v/d: f/(d − f) when focused on the chart. */
77
+ magnification: number;
78
+ focalLengthMm: number;
79
+ equivalentFocalLengthMm: number;
80
+ cropFactor35Mm: number;
81
+ chart: { widthM: number; heightM: number };
82
+ /** Plane area the frame covers at the subject distance, metres: sensor side ÷ magnification. */
83
+ framedWidthM: number;
84
+ framedHeightM: number;
85
+ /** Fraction of the frame width and height the chart covers; above 1 means the chart is larger than the frame and cropped. */
86
+ coverage: { frameWidthFraction: number; frameHeightFraction: number };
87
+ /** The chart's image on the sensor, mm. */
88
+ chartImageMm: { width: number; height: number };
89
+ /**
90
+ * 2.6: the thin-lens close-up light factor at the focus distance, 1/(1 + m)² with m = v/s: the image-plane irradiance
91
+ * relative to the same lens and f-number focused at infinity (working f-number N·(1 + m)). It is the factor
92
+ * `simulateCapture` applies through the scene-to-sensor irradiance (same function, same value).
93
+ */
94
+ closeUpTransmission: number;
95
+ /** 2.6: the same factor in stops, 2·log₂(1 + m): how much darker the image is than at infinity focus. */
96
+ closeUpLossStops: number;
97
+ /** The generic prime of this equivalent focal length, when there is one. */
98
+ prime: GenericPrimeLens | null;
99
+ /** The generic prime rule max(0.10 m, f·(1 + 1/0.15)) at this focal length (the prime's own value when there is one). */
100
+ minimumFocusDistanceM: number;
101
+ /** True when the subject or focus distance is closer than the minimum focus distance (`simulateCapture` with `lens.prime` rejects it). */
102
+ belowMinimumFocus: boolean;
103
+ scientificStatus: "approximation";
104
+ assumptions: readonly SimulateCaptureAssumption[];
105
+ }
106
+ ```
107
+
33
108
  ## SubjectFraming
34
109
 
35
110
  The exact contract is declared below; use the domain guide for assumptions and staged integration.
@@ -52,6 +127,14 @@ export interface SubjectFraming {
52
127
  framedHeightM: number;
53
128
  /** Fraction of the frame width and height the chart covers (1 on the fitted side; above 1 means cropped). */
54
129
  coverage: { frameWidthFraction: number; frameHeightFraction: number };
130
+ /**
131
+ * 2.6: the thin-lens close-up light factor at the focus distance, 1/(1 + m)² with m = v/s: the image-plane irradiance
132
+ * relative to the same lens and f-number focused at infinity (working f-number N·(1 + m)). It is the factor
133
+ * `simulateCapture` applies through the scene-to-sensor irradiance (same function, same value).
134
+ */
135
+ closeUpTransmission: number;
136
+ /** 2.6: the same factor in stops, 2·log₂(1 + m): how much darker the image is than at infinity focus. */
137
+ closeUpLossStops: number;
55
138
  /** The generic prime of this equivalent focal length, when there is one. */
56
139
  prime: GenericPrimeLens | null;
57
140
  /** The generic prime rule max(0.10 m, f·(1 + 1/0.15)) at this focal length (the prime's own value when there is one). */
@@ -71,7 +71,14 @@ export interface DiffractionLimitedPsfProfile {
71
71
  wavelengths: readonly DiffractionLimitedPsfWavelength[];
72
72
  /** Largest truncated energy over the wavelengths. */
73
73
  maximumTruncatedEnergyFraction: number;
74
- quadrature: { method: "polar-frequency-domain-trapezoid-gauss-legendre"; maximumRadialNodes: number; maximumAngularNodes: number };
74
+ /**
75
+ * How the cells were integrated. 2.5: wavelengths with β = p·f_c ≥ 192 (cells far larger than the pattern) use the real-space
76
+ * encircled-energy method; `realSpaceWavelengthsNm` lists them, and `method` is `"frequency-domain-and-real-space"` when both ran.
77
+ * `maximumRadialNodes` counts frequency-domain radial nodes (0 when only the real-space method ran); `maximumAngularNodes` the
78
+ * angular nodes of either method (for the real-space method, all θ nodes of one wavelength).
79
+ */
80
+ quadrature: { method: "polar-frequency-domain-trapezoid-gauss-legendre" | "real-space-encircled-energy-gauss-legendre" | "frequency-domain-and-real-space";
81
+ maximumRadialNodes: number; maximumAngularNodes: number; realSpaceWavelengthsNm?: readonly number[] };
75
82
  }
76
83
  ```
77
84
 
@@ -5,7 +5,11 @@ The package and root API versions are recorded once in [Navigation](../API_REFER
5
5
  ## calculatePrintPlan
6
6
 
7
7
  Finds the smallest file, at the exact native aspect ratio, that meets the declared sampling and lab constraints.
8
- The angular guidance is an approximate rule of thumb; the geometry itself is calculated exactly.
8
+ The angular guidance is an approximate rule of thumb; the geometry itself is calculated exactly. A common criterion,
9
+ one arcminute per pixel, is normal (20/20) visual acuity under optimal conditions: the minimum angle of resolution of an
10
+ ideal eye is about 1 minute of arc, and 20/20 letters are built from elements that each subtend 1 arcmin (Z. Alomar, K. D. Bojikian, "Visual Acuity Testing in Adults", EyeWiki, American Academy of Ophthalmology,
11
+ https://eyewiki.aao.org/Visual_Acuity_Testing_in_Adults, last edited June 16, 2026).
12
+ Some viewers resolve finer detail and many resolve coarser, so it stays a rule of thumb, and the criterion is the caller's.
9
13
  It does not assess source detail, perceived quality, file writing or available resources.
10
14
 
11
15
  ```ts
@@ -18,6 +22,9 @@ This call is synchronous; any supplied provider must follow its explicit synchro
18
22
 
19
23
  Calculates the largest print size under the stated conditions. Lab constraints need individual plans,
20
24
  so this size query reports them as unsupported rather than ignoring them.
25
+ As in `calculatePrintPlan`, an angular criterion is an approximate rule of thumb: one arcminute per pixel is normal
26
+ (20/20) acuity under optimal conditions, which some viewers exceed and many do not (Z. Alomar, K. D. Bojikian, "Visual Acuity Testing in Adults", EyeWiki, American Academy of Ophthalmology,
27
+ https://eyewiki.aao.org/Visual_Acuity_Testing_in_Adults, last edited June 16, 2026).
21
28
 
22
29
  ```ts
23
30
  export function calculatePrintSizeLimit(value: PrintSizeLimitInput): CalculationResult<PrintSizeLimit>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photivra/engine",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "private": false,
5
5
  "description": "Scientific TypeScript engine for camera optics, exposure, focus, motion, sensor sampling, diffraction, and digital imaging calculations.",
6
6
  "type": "module",
@@ -108,7 +108,8 @@
108
108
  "typescript-eslint": "8.70.0",
109
109
  "vitest": "5.0.1",
110
110
  "@types/node": "24.13.6",
111
- "@vitest/coverage-v8": "5.0.1"
111
+ "@vitest/coverage-v8": "5.0.1",
112
+ "rolldown": "1.2.9"
112
113
  },
113
114
  "keywords": [
114
115
  "photography",