@photivra/engine 2.12.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -7
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +3 -0
  6. package/dist/camera/lens-projection.js +2 -1
  7. package/dist/camera/lens-projection.js.map +1 -1
  8. package/dist/capture/dng-import.js +1 -1
  9. package/dist/capture/dng-import.js.map +1 -1
  10. package/dist/capture/export-jpeg.d.ts +1 -1
  11. package/dist/capture/export-jpeg.js +6 -4
  12. package/dist/capture/export-jpeg.js.map +1 -1
  13. package/dist/capture/host-yield-cadence.js +1 -1
  14. package/dist/capture/host-yield-cadence.js.map +1 -1
  15. package/dist/capture/native-environment-optical-stage.js +1 -1
  16. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  17. package/dist/capture/native-raw.js +1 -1
  18. package/dist/capture/native-raw.js.map +1 -1
  19. package/dist/capture/packed-sensor-raw.js +3 -3
  20. package/dist/capture/packed-sensor-raw.js.map +1 -1
  21. package/dist/capture/sensor-raw-producer.js +1 -1
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/color/capture-color.js +3 -3
  24. package/dist/color/capture-color.js.map +1 -1
  25. package/dist/color/chromatic-adaptation.js +2 -2
  26. package/dist/color/chromatic-adaptation.js.map +1 -1
  27. package/dist/color/color-matching-functions.js +1 -1
  28. package/dist/color/color-matching-functions.js.map +1 -1
  29. package/dist/color/colorimetry-math.js +2 -2
  30. package/dist/color/colorimetry-math.js.map +1 -1
  31. package/dist/color/illuminants.js +3 -3
  32. package/dist/color/illuminants.js.map +1 -1
  33. package/dist/color/rgb-color-spaces.js +1 -1
  34. package/dist/color/rgb-color-spaces.js.map +1 -1
  35. package/dist/color/sensor-color-development.js +1 -1
  36. package/dist/color/sensor-color-development.js.map +1 -1
  37. package/dist/core/record-validation.js +3 -3
  38. package/dist/core/record-validation.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/dist/core/version.js.map +1 -1
  42. package/dist/equipment/generic-prime-lenses.js +3 -3
  43. package/dist/equipment/generic-prime-lenses.js.map +1 -1
  44. package/dist/equipment/generic-tier-assets.d.ts +23 -1
  45. package/dist/equipment/generic-tier-assets.js +58 -30
  46. package/dist/equipment/generic-tier-assets.js.map +1 -1
  47. package/dist/equipment/generic-tier-presets.d.ts +6 -3
  48. package/dist/equipment/generic-tier-presets.js +26 -13
  49. package/dist/equipment/generic-tier-presets.js.map +1 -1
  50. package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
  51. package/dist/exposure/auto-iso-slowest-shutter.js +217 -0
  52. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
  53. package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
  54. package/dist/exposure/exposure-mode-resolver.js +238 -33
  55. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  56. package/dist/exposure/flash-ttl.js +1 -1
  57. package/dist/exposure/flash-ttl.js.map +1 -1
  58. package/dist/exposure/flash.js +1 -1
  59. package/dist/exposure/flash.js.map +1 -1
  60. package/dist/facade/chart-material-layers.js +2 -2
  61. package/dist/facade/chart-material-layers.js.map +1 -1
  62. package/dist/facade/chart-radiance-batch.js +1 -1
  63. package/dist/facade/chart-radiance-batch.js.map +1 -1
  64. package/dist/facade/chart-scenes.d.ts +45 -8
  65. package/dist/facade/chart-scenes.js +101 -44
  66. package/dist/facade/chart-scenes.js.map +1 -1
  67. package/dist/facade/color-patches.d.ts +129 -0
  68. package/dist/facade/color-patches.js +209 -0
  69. package/dist/facade/color-patches.js.map +1 -0
  70. package/dist/facade/diffraction-provider.js +12 -13
  71. package/dist/facade/diffraction-provider.js.map +1 -1
  72. package/dist/facade/display-view-gpu.js +1 -1
  73. package/dist/facade/display-view-gpu.js.map +1 -1
  74. package/dist/facade/display-view.d.ts +27 -0
  75. package/dist/facade/display-view.js +234 -65
  76. package/dist/facade/display-view.js.map +1 -1
  77. package/dist/facade/scene-radiance.js +2 -2
  78. package/dist/facade/scene-radiance.js.map +1 -1
  79. package/dist/facade/simulate-capture.d.ts +139 -12
  80. package/dist/facade/simulate-capture.js +209 -36
  81. package/dist/facade/simulate-capture.js.map +1 -1
  82. package/dist/facade/simulate-display-view.js +2 -2
  83. package/dist/facade/simulate-display-view.js.map +1 -1
  84. package/dist/gpu/chart-composite-gpu.js +2 -2
  85. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  86. package/dist/gpu/display-view-composite-material-module.js +1 -1
  87. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  88. package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
  89. package/dist/gpu/display-view-quality-gpu.js +24 -14
  90. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  91. package/dist/gpu/image-kernel-execution.js +2 -2
  92. package/dist/gpu/image-kernel-execution.js.map +1 -1
  93. package/dist/gpu/temporal-sum-gpu.js +23 -23
  94. package/dist/gpu/webgpu-runtime.js +1 -1
  95. package/dist/gpu/webgpu-runtime.js.map +1 -1
  96. package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
  97. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
  98. package/dist/gpu/wgsl-chart-kernel.js +117 -117
  99. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  100. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
  101. package/dist/gpu/wgsl-display-view-quality-kernel.js +522 -488
  102. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  103. package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
  104. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  105. package/dist/gpu/wgsl-kernels.js +203 -203
  106. package/dist/gpu/wgsl-kernels.js.map +1 -1
  107. package/dist/gpu/wgsl-sensor-kernel.js +302 -302
  108. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  109. package/dist/index.d.ts +8 -5
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/motion/temporal-exposure-sum.js +1 -1
  113. package/dist/motion/temporal-exposure-sum.js.map +1 -1
  114. package/dist/optics/diffraction-psf-profile.js +4 -4
  115. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  116. package/dist/optics/front-of-lens-filter.js +1 -1
  117. package/dist/optics/front-of-lens-filter.js.map +1 -1
  118. package/dist/optics/irradiance-convolution.d.ts +10 -0
  119. package/dist/optics/irradiance-convolution.js +49 -14
  120. package/dist/optics/irradiance-convolution.js.map +1 -1
  121. package/dist/output/native-capture-sdr.js +1 -1
  122. package/dist/output/native-capture-sdr.js.map +1 -1
  123. package/dist/output/png-encoder.js +2 -2
  124. package/dist/output/png-encoder.js.map +1 -1
  125. package/dist/output/print-jpeg.js +13 -10
  126. package/dist/output/print-jpeg.js.map +1 -1
  127. package/dist/output/text-metadata.d.ts +22 -2
  128. package/dist/output/text-metadata.js +58 -9
  129. package/dist/output/text-metadata.js.map +1 -1
  130. package/dist/sensor/fixed-pattern-noise.js +1 -1
  131. package/dist/sensor/fixed-pattern-noise.js.map +1 -1
  132. package/dist/sensor/readout-exposure-linkage.js +1 -1
  133. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  134. package/docs/API_REFERENCE.md +49 -1
  135. package/docs/QUICK_START.md +104 -9
  136. package/docs/USAGE.md +241 -2
  137. package/docs/api/core-version.md +1 -1
  138. package/docs/api/equipment-generic-tier-assets.md +41 -1
  139. package/docs/api/equipment-generic-tier-presets.md +11 -3
  140. package/docs/api/exports.json +172 -2
  141. package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
  142. package/docs/api/exposure-exposure-mode-resolver.md +68 -0
  143. package/docs/api/facade-chart-scenes.md +36 -5
  144. package/docs/api/facade-color-patches.md +150 -0
  145. package/docs/api/facade-simulate-capture.md +140 -9
  146. package/docs/api/output-text-metadata.md +34 -0
  147. package/llms.txt +1 -0
  148. package/package.json +2 -2
@@ -2,8 +2,9 @@
2
2
  /**
3
3
  * Procedural test charts for the capture facade, written for this engine. Every chart is a flat,
4
4
  * Lambertian (matte) reflectance pattern, defined by formulas in normalized frame coordinates. No
5
- * third-party chart data (for example ColorChecker reflectances) is bundled. Coloured patches use
6
- * spectra that the caller supplies.
5
+ * third-party chart data (for example ColorChecker reflectances) is bundled. Colored patches use
6
+ * spectra that the caller supplies or, from 2.14, Photivra's own analytic color patches
7
+ * (`color-patches.ts`, docs/COLOR_PATCHES.md).
7
8
  *
8
9
  * Frame coordinates: `u` runs from -1 (left edge of the framed chart) to +1 (right edge); `v` points up
9
10
  * and has the same scale, so it runs from -aspect to +aspect where aspect = height/width of the frame.
@@ -23,35 +24,37 @@
23
24
  * levels, like the grey ramp: the step (below 0.1 % of the range) is far below sensor noise.
24
25
  * @see docs/QUICK_START.md for the chart catalogue and their analytic definitions.
25
26
  */
26
- import { portablePow } from "../core/portable-math.js";
27
+ import { portableExp, portablePow } from "../core/portable-math.js";
27
28
  import { InvalidConfigurationError } from "../core/configuration-error.js";
28
29
  import { prepareColorimetricSpectrum } from "../color/color-matching-functions.js";
29
30
  import { requireInputObject, requireInputKeys } from "../core/record-validation.js";
30
31
  import { createDeterministicUniformStream } from "../sensor/stochastic-seed.js";
31
- export const CHART_SCENE_PRESETS = Object.freeze(["uniform", "slanted-edge", "siemens-star",
32
+ import { COLOR_PATCH_IDS, COLOR_PATCH_SET_ID, COLOR_PATCH_SET_VERSION, findColorPatch, prepareColorPatchSpectrum } from "./color-patches.js";
33
+ export const CHART_SCENE_PRESETS = /* @__PURE__ */ Object.freeze(["uniform", "slanted-edge", "siemens-star",
32
34
  "step-wedge", "grey-ramp", "checkerboard", "point-grid", "patches", "dead-leaves", "bar-groups", "hyperbolic-wedge", "zone-plate",
33
- "slanted-squares", "line-grid", "fiducial", "composite"]);
35
+ "slanted-squares", "line-grid", "fiducial", "color-patches", "composite"]);
34
36
  /** Neutral levels that represent a continuous pattern (grey ramp; 2.3 sine star, zone plate and dead-leaves greys). */
35
37
  export const CHART_CONTINUOUS_LEVELS = 1024;
36
38
  /** Engine-chosen chart parameters, each reported when the caller omitted it. */
37
- export const CHART_SCENE_DEFAULTS = Object.freeze({
38
- uniform: Object.freeze({ reflectance: 0.18 }),
39
- "slanted-edge": Object.freeze({ angleDegrees: 5, darkReflectance: 0.15, lightReflectance: 0.6 }),
40
- "siemens-star": Object.freeze({ cycles: 36, radiusFraction: 0.9, darkReflectance: 0.05, lightReflectance: 0.8, backgroundReflectance: 0.18 }),
41
- "step-wedge": Object.freeze({ steps: 11, minimumReflectance: 0.03, maximumReflectance: 0.9, spacing: "equal-density" }),
42
- "grey-ramp": Object.freeze({ minimumReflectance: 0.03, maximumReflectance: 0.9 }),
43
- checkerboard: Object.freeze({ squaresAcross: 8, darkReflectance: 0.05, lightReflectance: 0.8 }),
44
- "point-grid": Object.freeze({ columns: 5, rows: 3, radius: 0.01, pointReflectance: 0.9, backgroundReflectance: 0.02 }),
45
- patches: Object.freeze({ gapFraction: 0.1, backgroundReflectance: 0.18 }),
46
- "dead-leaves": Object.freeze({ radiusMinimum: 0.01, radiusMaximum: 0.5, minimumReflectance: 0.2, maximumReflectance: 0.6, seed: 1 }),
47
- "bar-groups": Object.freeze({ lineWidthsPerPictureHeight: Object.freeze([100, 200, 400, 800]), numerals: true, darkReflectance: 0.05, lightReflectance: 0.8 }),
48
- "hyperbolic-wedge": Object.freeze({ orientation: "horizontal", lineCount: 5, minimumLineWidthsPerPictureHeight: 100,
39
+ export const CHART_SCENE_DEFAULTS = /* @__PURE__ */ Object.freeze({
40
+ uniform: /* @__PURE__ */ Object.freeze({ reflectance: 0.18 }),
41
+ "slanted-edge": /* @__PURE__ */ Object.freeze({ angleDegrees: 5, darkReflectance: 0.15, lightReflectance: 0.6 }),
42
+ "siemens-star": /* @__PURE__ */ Object.freeze({ cycles: 36, radiusFraction: 0.9, darkReflectance: 0.05, lightReflectance: 0.8, backgroundReflectance: 0.18 }),
43
+ "step-wedge": /* @__PURE__ */ Object.freeze({ steps: 11, minimumReflectance: 0.03, maximumReflectance: 0.9, spacing: "equal-density" }),
44
+ "grey-ramp": /* @__PURE__ */ Object.freeze({ minimumReflectance: 0.03, maximumReflectance: 0.9 }),
45
+ checkerboard: /* @__PURE__ */ Object.freeze({ squaresAcross: 8, darkReflectance: 0.05, lightReflectance: 0.8 }),
46
+ "point-grid": /* @__PURE__ */ Object.freeze({ columns: 5, rows: 3, radius: 0.01, pointReflectance: 0.9, backgroundReflectance: 0.02 }),
47
+ patches: /* @__PURE__ */ Object.freeze({ gapFraction: 0.1, backgroundReflectance: 0.18 }),
48
+ "dead-leaves": /* @__PURE__ */ Object.freeze({ radiusMinimum: 0.01, radiusMaximum: 0.5, minimumReflectance: 0.2, maximumReflectance: 0.6, seed: 1 }),
49
+ "bar-groups": /* @__PURE__ */ Object.freeze({ lineWidthsPerPictureHeight: /* @__PURE__ */ Object.freeze([100, 200, 400, 800]), numerals: true, darkReflectance: 0.05, lightReflectance: 0.8 }),
50
+ "hyperbolic-wedge": /* @__PURE__ */ Object.freeze({ orientation: "horizontal", lineCount: 5, minimumLineWidthsPerPictureHeight: 100,
49
51
  maximumLineWidthsPerPictureHeight: 1000, darkReflectance: 0.05, lightReflectance: 0.8 }),
50
- "zone-plate": Object.freeze({ radiusFraction: 0.9, maximumCyclesPerPictureHeight: 200, darkReflectance: 0.1, lightReflectance: 0.7, backgroundReflectance: 0.18 }),
51
- "slanted-squares": Object.freeze({ columns: 3, rows: 2, sizeFraction: 0.5, angleDegrees: 5, darkReflectance: 0.15, lightReflectance: 0.6 }),
52
- "line-grid": Object.freeze({ spacing: 1 / 3, lineWidth: 0.01, lineReflectance: 0.42, backgroundReflectance: 0.6 }),
53
- fiducial: Object.freeze({ radiusFraction: 0.8, ringFraction: 0.18, inverted: false, darkReflectance: 0.05, lightReflectance: 0.8, backgroundReflectance: 0.18 }),
54
- composite: Object.freeze({ background: Object.freeze({ preset: "uniform", reflectance: 0.18 }) })
52
+ "zone-plate": /* @__PURE__ */ Object.freeze({ radiusFraction: 0.9, maximumCyclesPerPictureHeight: 200, darkReflectance: 0.1, lightReflectance: 0.7, backgroundReflectance: 0.18 }),
53
+ "slanted-squares": /* @__PURE__ */ Object.freeze({ columns: 3, rows: 2, sizeFraction: 0.5, angleDegrees: 5, darkReflectance: 0.15, lightReflectance: 0.6 }),
54
+ "line-grid": /* @__PURE__ */ Object.freeze({ spacing: 1 / 3, lineWidth: 0.01, lineReflectance: 0.42, backgroundReflectance: 0.6 }),
55
+ fiducial: /* @__PURE__ */ Object.freeze({ radiusFraction: 0.8, ringFraction: 0.18, inverted: false, darkReflectance: 0.05, lightReflectance: 0.8, backgroundReflectance: 0.18 }),
56
+ "color-patches": /* @__PURE__ */ Object.freeze({ patchIds: COLOR_PATCH_IDS, gapFraction: 0.1, backgroundReflectance: 0.18 }),
57
+ composite: /* @__PURE__ */ Object.freeze({ background: /* @__PURE__ */ Object.freeze({ preset: "uniform", reflectance: 0.18 }) })
55
58
  });
56
59
  function reflectance(value, name) {
57
60
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1)
@@ -69,11 +72,23 @@ function finiteIn(value, name, minimum, maximum, open = false) {
69
72
  return value;
70
73
  }
71
74
  /** Wavelength range every chart spectrum must cover (the facade's sensor quadrature range). */
72
- export const CHART_SPECTRAL_RANGE_NANOMETERS = Object.freeze({ minimum: 380, maximum: 780 });
73
- /** Internal: validates a chart reflectance input (a neutral number or a reflectance spectrum covering 380–780 nm) as a material. */
74
- export function resolveChartMaterial(value, name) {
75
+ export const CHART_SPECTRAL_RANGE_NANOMETERS = /* @__PURE__ */ Object.freeze({ minimum: 380, maximum: 780 });
76
+ /**
77
+ * Internal: validates a chart reflectance input (a neutral number, a reflectance spectrum covering 380–780 nm or a
78
+ * built-in color patch) as a material.
79
+ */
80
+ export function resolveChartMaterial(value, name, exp) {
75
81
  if (typeof value === "number")
76
82
  return { kind: "neutral", reflectance: reflectance(value, name) };
83
+ if (typeof value === "object" && value !== null && value.kind === "color-patch") {
84
+ for (const key of Object.keys(value))
85
+ if (key !== "kind" && key !== "patchId")
86
+ throw new InvalidConfigurationError(name + " has unknown field \"" + key + "\".");
87
+ const patch = findColorPatch(value.patchId);
88
+ if (!patch)
89
+ throw new InvalidConfigurationError(name + ".patchId must be one of the built-in color patches (COLOR_PATCH_IDS).");
90
+ return { kind: "spectrum", spectrum: prepareColorPatchSpectrum(patch, exp) };
91
+ }
77
92
  let spectrum;
78
93
  try {
79
94
  spectrum = prepareColorimetricSpectrum(value, name);
@@ -88,6 +103,26 @@ export function resolveChartMaterial(value, name) {
88
103
  reflectance(spectrum.evaluate(nm), name + " values");
89
104
  return { kind: "spectrum", spectrum };
90
105
  }
106
+ /** How a uniform chart reports its reflectance: the number, the color-patch record, or `"caller-spectrum"`. */
107
+ function reflectanceReport(raw) {
108
+ if (typeof raw === "number")
109
+ return raw;
110
+ const record = raw;
111
+ return record.kind === "color-patch" ? { kind: "color-patch", patchId: record.patchId } : "caller-spectrum";
112
+ }
113
+ /** Material at a point of a row-major patch grid (`patches`, 2.14 `color-patches`): a patch index or `backgroundIndex`. */
114
+ function patchGridMaterialAt(columns, rows, count, gap, aspect, backgroundIndex) {
115
+ const cellW = 2 / columns, cellH = 2 * aspect / rows;
116
+ return (u, v) => {
117
+ const fx = (u + 1) / cellW, fy = (aspect - v) / cellH, i = Math.floor(fx), j = Math.floor(fy);
118
+ if (i < 0 || j < 0 || i >= columns || j >= rows)
119
+ return backgroundIndex;
120
+ const index = j * columns + i, x = fx - i, y = fy - j;
121
+ if (index >= count || x < gap / 2 || x > 1 - gap / 2 || y < gap / 2 || y > 1 - gap / 2)
122
+ return backgroundIndex;
123
+ return index;
124
+ };
125
+ }
91
126
  /** Raw caller spectrum inputs per material (undefined for neutral materials), for the facade's scene digest. */
92
127
  const MATERIAL_INPUTS = new WeakMap();
93
128
  /**
@@ -136,12 +171,13 @@ export function resolveChartScene(input) {
136
171
  }
137
172
  /**
138
173
  * Internal (not exported from the package): {@link resolveChartScene} with the step wedge's geometric levels computed
139
- * with the portable power (`portablePow`, within 1 ulp) instead of `**`, whose last bit differs between JavaScript
140
- * engines. Every other value, the geometry and the material order are the same. The display view uses its materials.
174
+ * with the portable power (`portablePow`, within 1 ulp) instead of `**`, and (2.14) the built-in color patches with the
175
+ * portable exponential (`portableExp`) instead of `Math.exp`, whose last bits differ between JavaScript engines. Every
176
+ * other value, the geometry and the material order are the same. The display view uses its materials.
141
177
  */
142
178
  export function resolvePortableChartScene(input) {
143
179
  requireInputObject(input, "resolveChartScene input");
144
- return resolveScene(input.scene, input.aspect, { pictureHeight: 2 * input.aspect, nested: false, pow: portablePow });
180
+ return resolveScene(input.scene, input.aspect, { pictureHeight: 2 * input.aspect, nested: false, pow: portablePow, exp: portableExp });
145
181
  }
146
182
  function resolveScene(scene, aspect, context) {
147
183
  requireInputObject(scene, "chart scene");
@@ -152,7 +188,7 @@ function resolveScene(scene, aspect, context) {
152
188
  throw new InvalidConfigurationError("A composite chart cannot contain another composite.");
153
189
  const record = scene;
154
190
  const defaults = CHART_SCENE_DEFAULTS[preset];
155
- const extra = preset === "patches" ? ["patches", "columns"] : preset === "composite" ? ["elements"] : preset === "siemens-star" ? ["profile"] : [];
191
+ const extra = preset === "patches" ? ["patches", "columns"] : preset === "color-patches" ? ["columns"] : preset === "composite" ? ["elements"] : preset === "siemens-star" ? ["profile"] : [];
156
192
  const allowed = ["preset", ...Object.keys(defaults), ...extra];
157
193
  for (const key of Object.keys(record))
158
194
  if (!allowed.includes(key))
@@ -183,8 +219,8 @@ function resolveScene(scene, aspect, context) {
183
219
  };
184
220
  switch (preset) {
185
221
  case "uniform": {
186
- const raw = get("reflectance"), m = resolveChartMaterial(raw, "reflectance");
187
- return done({ reflectance: typeof raw === "number" ? raw : "caller-spectrum" }, [m], () => 0, [typeof raw === "number" ? undefined : raw], { kind: "uniform" });
222
+ const raw = get("reflectance"), m = resolveChartMaterial(raw, "reflectance", context.exp);
223
+ return done({ reflectance: reflectanceReport(raw) }, [m], () => 0, [typeof raw === "number" ? undefined : raw], { kind: "uniform" });
188
224
  }
189
225
  case "slanted-edge": {
190
226
  const angle = finiteIn(get("angleDegrees"), "angleDegrees", -45, 45, true), dark = neutral("darkReflectance"), light = neutral("lightReflectance");
@@ -270,7 +306,7 @@ function resolveScene(scene, aspect, context) {
270
306
  if (typeof p.patchId !== "string" || p.patchId.trim() === "" || ids.has(p.patchId))
271
307
  throw new InvalidConfigurationError("Each patch needs a unique non-empty patchId.");
272
308
  ids.add(p.patchId);
273
- materials.push(resolveChartMaterial(p.reflectance, "patches[" + index + "].reflectance"));
309
+ materials.push(resolveChartMaterial(p.reflectance, "patches[" + index + "].reflectance", context.exp));
274
310
  inputs.push(typeof p.reflectance === "number" ? undefined : p.reflectance);
275
311
  }
276
312
  const columnsRaw = record.columns === undefined ? Math.ceil(Math.sqrt(list.length)) : record.columns;
@@ -284,15 +320,35 @@ function resolveScene(scene, aspect, context) {
284
320
  materials.push({ kind: "neutral", reflectance: background });
285
321
  inputs.push(undefined);
286
322
  const cellW = 2 / columns, cellH = 2 * aspect / rows;
287
- return done({ patchIds: list.map((p) => p.patchId), columns, rows, gapFraction: gap, backgroundReflectance: background }, materials, (u, v) => {
288
- const fx = (u + 1) / cellW, fy = (aspect - v) / cellH, i = Math.floor(fx), j = Math.floor(fy);
289
- if (i < 0 || j < 0 || i >= columns || j >= rows)
290
- return backgroundIndex;
291
- const index = j * columns + i, x = fx - i, y = fy - j;
292
- if (index >= list.length || x < gap / 2 || x > 1 - gap / 2 || y < gap / 2 || y > 1 - gap / 2)
293
- return backgroundIndex;
294
- return index;
295
- }, inputs, { kind: "patches", columns, rows, count: list.length, gap, cellW, cellH, backgroundIndex });
323
+ return done({ patchIds: list.map((p) => p.patchId), columns, rows, gapFraction: gap, backgroundReflectance: background }, materials, patchGridMaterialAt(columns, rows, list.length, gap, aspect, backgroundIndex), inputs, { kind: "patches", columns, rows, count: list.length, gap, cellW, cellH, backgroundIndex });
324
+ }
325
+ case "color-patches": {
326
+ const ids = get("patchIds");
327
+ if (!Array.isArray(ids) || ids.length < 1 || ids.length > COLOR_PATCH_IDS.length)
328
+ throw new InvalidConfigurationError("patchIds must list 1 to " + COLOR_PATCH_IDS.length + " built-in color patch ids.");
329
+ const seen = new Set(), materials = [], inputs = [];
330
+ for (const [index, id] of ids.entries()) {
331
+ if (!findColorPatch(id))
332
+ throw new InvalidConfigurationError("patchIds[" + index + "] must be one of the built-in color patches (COLOR_PATCH_IDS).");
333
+ if (seen.has(id))
334
+ throw new InvalidConfigurationError("patchIds[" + index + "] repeats " + JSON.stringify(id) + ".");
335
+ seen.add(id);
336
+ const input = Object.freeze({ kind: "color-patch", patchId: id });
337
+ materials.push(resolveChartMaterial(input, "patchIds[" + index + "]", context.exp));
338
+ inputs.push(input);
339
+ }
340
+ const columnsRaw = record.columns === undefined ? Math.min(6, ids.length) : record.columns;
341
+ if (record.columns === undefined)
342
+ defaulted.push("columns");
343
+ const columns = integerIn(columnsRaw, "columns", 1, ids.length), rows = Math.ceil(ids.length / columns);
344
+ const gap = finiteIn(get("gapFraction"), "gapFraction", 0, 0.5), background = neutral("backgroundReflectance");
345
+ if (gap >= 0.5)
346
+ throw new InvalidConfigurationError("gapFraction must be below 0.5.");
347
+ const backgroundIndex = materials.length;
348
+ materials.push({ kind: "neutral", reflectance: background });
349
+ inputs.push(undefined);
350
+ const cellW = 2 / columns, cellH = 2 * aspect / rows;
351
+ return done({ patchSet: COLOR_PATCH_SET_ID, patchSetVersion: COLOR_PATCH_SET_VERSION, patchIds: [...ids], columns, rows, gapFraction: gap, backgroundReflectance: background }, materials, patchGridMaterialAt(columns, rows, ids.length, gap, aspect, backgroundIndex), inputs, { kind: "patches", columns, rows, count: ids.length, gap, cellW, cellH, backgroundIndex });
296
352
  }
297
353
  case "dead-leaves": {
298
354
  const rMin = finiteIn(get("radiusMinimum"), "radiusMinimum", DEAD_LEAVES_MINIMUM_RADIUS, 2), rMax = finiteIn(get("radiusMaximum"), "radiusMaximum", 0, 2, true);
@@ -410,7 +466,8 @@ function resolveScene(scene, aspect, context) {
410
466
  const list = record.elements;
411
467
  if (!Array.isArray(list) || list.length < 1 || list.length > 256)
412
468
  throw new InvalidConfigurationError("elements must list 1 to 256 chart elements.");
413
- const nested = { pictureHeight: context.pictureHeight, nested: true, ...(context.pow ? { pow: context.pow } : {}) };
469
+ const portable = { ...(context.pow ? { pow: context.pow } : {}), ...(context.exp ? { exp: context.exp } : {}) };
470
+ const nested = { pictureHeight: context.pictureHeight, nested: true, ...portable };
414
471
  const background = resolveScene(get("background"), aspect, nested);
415
472
  const materials = [...background.materials], inputs = [...chartMaterialInputs(background)];
416
473
  for (const key of background.defaultedParameters)
@@ -430,7 +487,7 @@ function resolveScene(scene, aspect, context) {
430
487
  if (e.mask === undefined)
431
488
  defaulted.push("elements." + index + ".placement.mask");
432
489
  // The element's own frame: u = ±1 across its width; frequencies keep the composite's picture height.
433
- const chart = resolveScene(e.scene, height / width, { pictureHeight: context.pictureHeight * 2 / width, nested: true, ...(context.pow ? { pow: context.pow } : {}) });
490
+ const chart = resolveScene(e.scene, height / width, { pictureHeight: context.pictureHeight * 2 / width, nested: true, ...portable });
434
491
  for (const key of chart.defaultedParameters)
435
492
  defaulted.push("elements." + index + ".parameters." + key);
436
493
  const offset = materials.length;
@@ -574,7 +631,7 @@ function barGroupLayout(list, numerals, aspect, pictureHeight) {
574
631
  /** Engine-drawn seven-segment numerals: a 0.6 × 1 cell per digit (baseline at 0, y up), stroke 0.16, advance 0.82. */
575
632
  export const NUMERAL_WIDTH = 0.6, NUMERAL_ADVANCE = 0.82;
576
633
  /** Internal: segment rectangles [x0, y0, x1, y1] of the seven-segment numerals and the segments each digit lights. */
577
- export const SEGMENTS = Object.freeze({
634
+ export const SEGMENTS = /* @__PURE__ */ Object.freeze({
578
635
  a: [0.08, 0.84, 0.52, 1], b: [0.44, 0.5, 0.6, 1], c: [0.44, 0, 0.6, 0.5], d: [0.08, 0, 0.52, 0.16], e: [0, 0, 0.16, 0.5], f: [0, 0.5, 0.16, 1], g: [0.08, 0.42, 0.52, 0.58]
579
636
  });
580
637
  export const DIGIT_SEGMENTS = ["abcdef", "bc", "abged", "abgcd", "fgbc", "afgcd", "afgedc", "abc", "abcdefg", "abcdfg"];