@photivra/engine 2.20.0 → 2.21.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.
- package/CHANGELOG.md +178 -1
- package/CITATION.cff +1 -1
- package/README.md +7 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +7 -0
- package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
- package/dist/capture/dng-lens-correction-opcodes.js +2 -2
- package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
- package/dist/capture/exposure-build-up-stage.d.ts +16 -0
- package/dist/capture/exposure-build-up-stage.js +4 -0
- package/dist/capture/exposure-build-up-stage.js.map +1 -0
- package/dist/capture/native-environment-lean.d.ts +2 -1
- package/dist/capture/native-environment-lean.js +41 -73
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.js +4 -3
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/capture/native-raw.d.ts +3 -2
- package/dist/capture/native-raw.js +5 -4
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.d.ts +4 -1
- package/dist/capture/sensor-raw-producer.js +12 -3
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/equipment/generic-lens-catalogue.d.ts +3 -2
- package/dist/equipment/generic-lens-catalogue.js +18 -5
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +32 -0
- package/dist/equipment/generic-tier-assets.js +16 -11
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/sensor-profile-body.d.ts +78 -0
- package/dist/equipment/sensor-profile-body.js +77 -0
- package/dist/equipment/sensor-profile-body.js.map +1 -0
- package/dist/equipment/sensor-profiles.d.ts +8 -0
- package/dist/equipment/sensor-profiles.js +82 -0
- package/dist/equipment/sensor-profiles.js.map +1 -0
- package/dist/facade/catalogue-lens-signal.d.ts +2 -2
- package/dist/facade/catalogue-lens-signal.js +58 -71
- package/dist/facade/catalogue-lens-signal.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +2 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/defocus-image-space-provider.d.ts +7 -0
- package/dist/facade/defocus-image-space-provider.js +64 -76
- package/dist/facade/defocus-image-space-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +2 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +6 -1
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.js +25 -23
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/display-view.js +5 -23
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +3 -1
- package/dist/facade/expected-signal-setup.js +1 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +47 -14
- package/dist/facade/expected-signal.js +117 -112
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/exposure-build-up.d.ts +70 -0
- package/dist/facade/exposure-build-up.js +164 -0
- package/dist/facade/exposure-build-up.js.map +1 -0
- package/dist/facade/fast-methods.js +9 -1
- package/dist/facade/fast-methods.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +2 -2
- package/dist/facade/generic-lens-capture.js +2 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/image-plane-enable.d.ts +7 -0
- package/dist/facade/image-plane-enable.js +17 -0
- package/dist/facade/image-plane-enable.js.map +1 -0
- package/dist/facade/image-plane-lattice-data.d.ts +6 -0
- package/dist/facade/image-plane-lattice-data.js +10 -0
- package/dist/facade/image-plane-lattice-data.js.map +1 -0
- package/dist/facade/image-plane-support.d.ts +33 -0
- package/dist/facade/image-plane-support.js +11 -0
- package/dist/facade/image-plane-support.js.map +1 -0
- package/dist/facade/image-plane.d.ts +36 -0
- package/dist/facade/image-plane.js +353 -0
- package/dist/facade/image-plane.js.map +1 -0
- package/dist/facade/mixed-illumination.d.ts +108 -0
- package/dist/facade/mixed-illumination.js +174 -0
- package/dist/facade/mixed-illumination.js.map +1 -0
- package/dist/facade/representative-light-sources.d.ts +72 -0
- package/dist/facade/representative-light-sources.js +124 -0
- package/dist/facade/representative-light-sources.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +129 -4
- package/dist/facade/scene-radiance.js +48 -13
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +39 -10
- package/dist/facade/simulate-capture.js +252 -92
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +14 -6
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +13 -1
- package/dist/gpu/chart-composite-gpu.js +48 -9
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/chart-radiance-gpu.js +4 -30
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
- package/dist/gpu/display-view-composite-material-module.js +39 -13
- package/dist/gpu/display-view-composite-material-module.js.map +1 -1
- package/dist/gpu/display-view-image-material-module.d.ts +86 -0
- package/dist/gpu/display-view-image-material-module.js +339 -0
- package/dist/gpu/display-view-image-material-module.js.map +1 -0
- package/dist/gpu/display-view-image-quality.d.ts +45 -0
- package/dist/gpu/display-view-image-quality.js +380 -0
- package/dist/gpu/display-view-image-quality.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
- package/dist/gpu/display-view-quality-gpu.js +136 -205
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
- package/dist/gpu/display-view-speed-gpu.js +131 -93
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
- package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
- package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
- package/dist/gpu/wgsl-exact-sum.js +42 -0
- package/dist/gpu/wgsl-exact-sum.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/motion/subject-motion.d.ts +66 -0
- package/dist/motion/subject-motion.js +52 -0
- package/dist/motion/subject-motion.js.map +1 -0
- package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
- package/dist/optics/generic-lens-lateral-color.js +77 -0
- package/dist/optics/generic-lens-lateral-color.js.map +1 -0
- package/dist/optics/generic-lens-psf.js +54 -46
- package/dist/optics/generic-lens-psf.js.map +1 -1
- package/dist/optics/lens-psf-profile.d.ts +7 -0
- package/dist/optics/lens-psf-profile.js +2 -1
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/output/developed-lens-corrections.d.ts +19 -5
- package/dist/output/developed-lens-corrections.js +28 -9
- package/dist/output/developed-lens-corrections.js.map +1 -1
- package/dist/output/generic-lens-corrections.d.ts +41 -5
- package/dist/output/generic-lens-corrections.js +52 -8
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/print-plan.d.ts +41 -5
- package/dist/output/print-plan.js +59 -14
- package/dist/output/print-plan.js.map +1 -1
- package/dist/sensor/binomial-sample.d.ts +3 -0
- package/dist/sensor/binomial-sample.js +86 -0
- package/dist/sensor/binomial-sample.js.map +1 -0
- package/dist/sensor/sensor-characteristics.d.ts +10 -0
- package/dist/sensor/sensor-characteristics.js +20 -9
- package/dist/sensor/sensor-characteristics.js.map +1 -1
- package/docs/AGENT_GUIDE.md +1 -0
- package/docs/API_REFERENCE.md +98 -1
- package/docs/QUICK_START.md +125 -0
- package/docs/USAGE.md +92 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +10 -5
- package/docs/api/equipment-sensor-profile-body.md +64 -0
- package/docs/api/equipment-sensor-profiles.md +86 -0
- package/docs/api/exports.json +207 -2
- package/docs/api/facade-exposure-build-up.md +105 -0
- package/docs/api/facade-fast-methods.md +9 -1
- package/docs/api/facade-image-plane-enable.md +16 -0
- package/docs/api/facade-mixed-illumination.md +67 -0
- package/docs/api/facade-representative-light-sources.md +131 -0
- package/docs/api/facade-scene-radiance.md +54 -3
- package/docs/api/facade-simulate-capture.md +26 -6
- package/docs/api/motion-subject-motion.md +23 -0
- package/docs/api/optics-generic-lens-lateral-color.md +79 -0
- package/docs/api/output-developed-lens-corrections.md +10 -1
- package/docs/api/output-generic-lens-corrections.md +40 -4
- package/docs/api/output-print-plan.md +35 -1
- package/llms.txt +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type CaptureIlluminant, type ResolvedCaptureIlluminant } from "./scene-radiance.js";
|
|
2
|
+
/** Most sources an `illumination` may list. */
|
|
3
|
+
export declare const MIXED_ILLUMINATION_MAX_SOURCES = 8;
|
|
4
|
+
/** Most regions a `regions` blend may list. */
|
|
5
|
+
export declare const MIXED_ILLUMINATION_MAX_REGIONS = 64;
|
|
6
|
+
/** One light source of a mixed illumination: an illuminant (as `simulateCapture` `illuminant` takes it) at an illuminance. */
|
|
7
|
+
export interface MixedIlluminationSource {
|
|
8
|
+
illuminant: CaptureIlluminant;
|
|
9
|
+
/** Illuminance this source alone gives where its weight is 1, lux, (0, 1e7]. */
|
|
10
|
+
illuminanceLux: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 2.22 (#550): a point light on the scene plane's front side: an illuminant spectrum, a position in metres (`xM`, `yM` on the plane
|
|
14
|
+
* from the optical axis, +x right and +y up as seen from the camera; `heightM` > 0 in front of the plane, toward the camera) and an
|
|
15
|
+
* intensity, either in candela or as the illuminance it gives at normal incidence at a reference distance (I = E·r²).
|
|
16
|
+
*/
|
|
17
|
+
export interface MixedIlluminationPointSource {
|
|
18
|
+
illuminant: CaptureIlluminant;
|
|
19
|
+
position: {
|
|
20
|
+
xM: number;
|
|
21
|
+
yM: number;
|
|
22
|
+
heightM: number;
|
|
23
|
+
};
|
|
24
|
+
intensity: {
|
|
25
|
+
candela: number;
|
|
26
|
+
} | {
|
|
27
|
+
illuminanceLux: number;
|
|
28
|
+
distanceM: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** A point in chart coordinates (see {@link MixedIllumination}). */
|
|
32
|
+
export interface MixedIlluminationPoint {
|
|
33
|
+
u: number;
|
|
34
|
+
v: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* How the sources blend across the chart. `linear-gradient` and `radial` map a point to a position t in [0, 1] (its projection
|
|
38
|
+
* onto `from` → `to`, clamped; or its distance from `center` over `radius`, clamped) and crossfade linearly between consecutive
|
|
39
|
+
* sources at their `stops` (one per source, non-decreasing in [0, 1]; default evenly spaced from 0 to 1): source 0 alone up to
|
|
40
|
+
* stops[0], the last source alone from its stop on, and two equal stops a hard edge (the later source from the stop on).
|
|
41
|
+
* `regions` gives each point the source of the first rectangle that contains it (bounds inclusive) and `outsideSource` elsewhere.
|
|
42
|
+
*/
|
|
43
|
+
export type MixedIlluminationBlend = {
|
|
44
|
+
kind: "linear-gradient";
|
|
45
|
+
from: MixedIlluminationPoint;
|
|
46
|
+
to: MixedIlluminationPoint;
|
|
47
|
+
stops?: readonly number[];
|
|
48
|
+
} | {
|
|
49
|
+
kind: "radial";
|
|
50
|
+
center?: MixedIlluminationPoint;
|
|
51
|
+
radius: number;
|
|
52
|
+
stops?: readonly number[];
|
|
53
|
+
} | {
|
|
54
|
+
kind: "regions";
|
|
55
|
+
regions: readonly {
|
|
56
|
+
source: number;
|
|
57
|
+
uMin: number;
|
|
58
|
+
uMax: number;
|
|
59
|
+
vMin: number;
|
|
60
|
+
vMax: number;
|
|
61
|
+
}[];
|
|
62
|
+
outsideSource: number;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* 2.22 (#550), `simulateCapture` `illumination`: sources blended across the chart and point sources with inverse-square falloff
|
|
66
|
+
* (docs/MIXED_ILLUMINATION.md). It replaces `illuminant` and `illuminanceLux`. `sources` lists 1 to 8 sources (with two or more, a
|
|
67
|
+
* `blend` is required; one lights the whole plane); `points` lists 1 to 8 point sources; a request needs two or more sources or at least one point. Blend
|
|
68
|
+
* coordinates are chart-local with `chartSize` and frame units without it; (0, 0) is the frame center.
|
|
69
|
+
*/
|
|
70
|
+
export interface MixedIllumination {
|
|
71
|
+
sources?: readonly MixedIlluminationSource[];
|
|
72
|
+
blend?: MixedIlluminationBlend;
|
|
73
|
+
points?: readonly MixedIlluminationPointSource[];
|
|
74
|
+
}
|
|
75
|
+
/** Internal: a validated mixed illumination with its lights resolved and scaled. */
|
|
76
|
+
export interface ResolvedMixedIllumination {
|
|
77
|
+
/**
|
|
78
|
+
* The lights, the blend's sources first and then the point sources: each with the illuminance it gives at the frame center when
|
|
79
|
+
* it lights it alone (a point source's I·cos θ/d²) and the scale k with k·S(λ) that light's irradiance there; `report` is its
|
|
80
|
+
* public form (the assumption's and the digest's).
|
|
81
|
+
*/
|
|
82
|
+
sources: readonly {
|
|
83
|
+
illuminant: ResolvedCaptureIlluminant;
|
|
84
|
+
option: CaptureIlluminant;
|
|
85
|
+
illuminanceLux: number;
|
|
86
|
+
irradianceScale: number;
|
|
87
|
+
report: Readonly<Record<string, unknown>>;
|
|
88
|
+
}[];
|
|
89
|
+
/** The blend with its defaults filled, or null (one source or none). */
|
|
90
|
+
blend: Readonly<Record<string, unknown>> | null;
|
|
91
|
+
/** Spectral irradiance at a plane point, W·m⁻²·nm⁻¹: (u, v) chart coordinates for the blend, (xM, yM) metres for the point sources. */
|
|
92
|
+
irradianceAt(u: number, v: number, xM: number, yM: number, wavelengthNanometers: number): number;
|
|
93
|
+
/**
|
|
94
|
+
* At the frame center: the illuminance, the blend's source weights, each light's illuminance there (w_i·E_i; a point's
|
|
95
|
+
* I·cos θ/d²) and the strongest light (the largest, the first on a tie; an index into `sources`).
|
|
96
|
+
*/
|
|
97
|
+
center: {
|
|
98
|
+
illuminanceLux: number;
|
|
99
|
+
weights: number[];
|
|
100
|
+
illuminancesLux: number[];
|
|
101
|
+
strongestSource: number;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Internal: validates `illumination` and resolves its sources with `resolveIlluminant` (the capture's `resolveCaptureIlluminant`, or
|
|
106
|
+
* the display view's portable one).
|
|
107
|
+
*/
|
|
108
|
+
export declare function resolveMixedIllumination(value: unknown, resolveIlluminant: (value: CaptureIlluminant) => ResolvedCaptureIlluminant): ResolvedMixedIllumination;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* 2.22 (#550): spatially varying illumination of the scene plane (mixed lighting), loaded on demand by `simulateCapture` and the
|
|
4
|
+
* display view only when a request has `illumination`, so requests without it neither load nor run it.
|
|
5
|
+
*
|
|
6
|
+
* Model: N sources (1 to {@link MIXED_ILLUMINATION_MAX_SOURCES}; one lights the whole plane), source i an illuminant spectral shape S_i(λ) scaled to
|
|
7
|
+
* `illuminanceLux` E_i exactly as the single illuminant is (`calculateIlluminantIrradianceScale`: k_i with 683 ∫ k_i S_i ȳ dλ = E_i).
|
|
8
|
+
* The plane's spectral irradiance at a point p is the declared blend E(p, λ) = Σ_i w_i(p)·k_i·S_i(λ) with weights w_i(p) ≥ 0 that
|
|
9
|
+
* sum to 1, so the illuminance at p is Σ_i w_i(p)·E_i and, where one weight is 1, the light is exactly that source's. A Lambertian
|
|
10
|
+
* reflector then has radiance ρ(λ)·E(p, λ)/π, as under one illuminant. The blend is a declared crossfade of irradiance, not a
|
|
11
|
+
* model of how light falls off from a lamp or a window (no inverse-square law, no cosine falloff, no shadows); it is exact for
|
|
12
|
+
* what it declares.
|
|
13
|
+
*
|
|
14
|
+
* Point sources (owner decision 2026-11-10, a step toward flash): source j is a point at height h_j in front of the plane above
|
|
15
|
+
* plane point (x_j, y_j) m, with luminous intensity I_j cd (the same in every direction toward the plane) and spectral shape S_j(λ),
|
|
16
|
+
* scaled so that 683 ∫ c_j S_j ȳ dλ = I_j. A point p of the plane at distance d from it receives E_j(p, λ) = c_j·S_j(λ)·cos θ/d²,
|
|
17
|
+
* with cos θ = h_j/d: the inverse-square law with Lambert's cosine law, which follows from the definitions of luminous intensity
|
|
18
|
+
* (flux per solid angle) and illuminance (flux per area) with the solid angle dΩ = dA·cos θ/d² that a plane element subtends (CIE S
|
|
19
|
+
* 017:2020 ILV, International Lighting Vocabulary, entries "luminous intensity" and "illuminance", free at e-ilv.cie.co.at; BIPM SI
|
|
20
|
+
* Brochure, 9th edition 2019, Table 4, the steradian). Exact for an ideal isotropic point; no occlusion (the camera casts no shadow),
|
|
21
|
+
* no source size and no inter-reflection. The point sources add to the blend: E(p, λ) = Σ_i w_i(p)·k_i·S_i(λ) + Σ_j E_j(p, λ).
|
|
22
|
+
*
|
|
23
|
+
* Coordinates are the chart's: a fixed-size chart's chart-local units (±1 at its left and right edges, v up on the same scale,
|
|
24
|
+
* extended linearly over the surround), or, without `chartSize`, frame units (±1 at the frame's left and right edges), which are
|
|
25
|
+
* the chart-local units of a chart that fills the frame. Both put (0, 0) at the frame center.
|
|
26
|
+
*/
|
|
27
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
28
|
+
import { requireInputObject } from "../core/record-validation.js";
|
|
29
|
+
import { calculateIlluminantIrradianceScale, tabulatedSpectrum } from "./scene-radiance.js";
|
|
30
|
+
/** Most sources an `illumination` may list. */
|
|
31
|
+
export const MIXED_ILLUMINATION_MAX_SOURCES = 8;
|
|
32
|
+
/** Most regions a `regions` blend may list. */
|
|
33
|
+
export const MIXED_ILLUMINATION_MAX_REGIONS = 64;
|
|
34
|
+
const fail = (message) => { throw new InvalidConfigurationError(message); };
|
|
35
|
+
/** A record with no other fields than these (`name` relative to `illumination`). */
|
|
36
|
+
const fields = (value, name, keys) => {
|
|
37
|
+
requireInputObject(value, "illumination" + name);
|
|
38
|
+
for (const key of Object.keys(value))
|
|
39
|
+
if (!keys.includes(key))
|
|
40
|
+
fail("Unknown illumination" + name + " field: " + key + ".");
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const finite = (x, name) => (typeof x === "number" && Number.isFinite(x) && Math.abs(x) <= 1e6 ? x : fail("illumination" + name + " must be a finite number."));
|
|
44
|
+
const positive = (x, name, maximum) => (typeof x === "number" && x > 0 && x <= maximum ? x : fail("illumination" + name + " must be positive, up to " + maximum + "."));
|
|
45
|
+
const list = (value, name, what) => (value === undefined ? [] : Array.isArray(value) && value.length >= 1 && value.length <= MIXED_ILLUMINATION_MAX_SOURCES ? value :
|
|
46
|
+
fail("illumination." + name + " must list 1 to " + MIXED_ILLUMINATION_MAX_SOURCES + " " + what + "."));
|
|
47
|
+
const point = (value, name) => {
|
|
48
|
+
const r = fields(value, ".blend." + name, ["u", "v"]);
|
|
49
|
+
return { u: finite(r.u, ".blend." + name + ".u"), v: finite(r.v, ".blend." + name + ".v") };
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Internal: validates `illumination` and resolves its sources with `resolveIlluminant` (the capture's `resolveCaptureIlluminant`, or
|
|
53
|
+
* the display view's portable one).
|
|
54
|
+
*/
|
|
55
|
+
export function resolveMixedIllumination(value, resolveIlluminant) {
|
|
56
|
+
const record = fields(value, "", ["sources", "blend", "points"]), given = list(record.sources, "sources", "sources"), pointList = list(record.points, "points", "point sources");
|
|
57
|
+
if (pointList.length === 0 && given.length < 2)
|
|
58
|
+
fail("illumination needs two or more sources, or a point source; one source alone is illuminant and illuminanceLux.");
|
|
59
|
+
const sources = given.map((entry, i) => {
|
|
60
|
+
const s = fields(entry, ".sources[" + i + "]", ["illuminant", "illuminanceLux"]), option = s.illuminant, illuminant = resolveIlluminant(option);
|
|
61
|
+
return Object.freeze({ illuminant, option, illuminanceLux: s.illuminanceLux, irradianceScale: calculateIlluminantIrradianceScale(illuminant.shape, s.illuminanceLux),
|
|
62
|
+
report: Object.freeze({ illuminant: illuminant.id, illuminanceLux: s.illuminanceLux }) });
|
|
63
|
+
});
|
|
64
|
+
// Point sources: c_j with 683 ∫ c_j S_j ȳ dλ = I_j (the irradiance scale of 1 lux, times I_j), and E_j = c_j S_j(λ)·h/d³.
|
|
65
|
+
const points = pointList.map((entry, j) => {
|
|
66
|
+
const at = ".points[" + j + "]", p = fields(entry, at, ["illuminant", "position", "intensity"]), option = p.illuminant, illuminant = resolveIlluminant(option);
|
|
67
|
+
const q = fields(p.position, at + ".position", ["xM", "yM", "heightM"]), i = fields(p.intensity, at + ".intensity", ["candela", "illuminanceLux", "distanceM"]);
|
|
68
|
+
const position = { xM: finite(q.xM, at + ".position.xM"), yM: finite(q.yM, at + ".position.yM"), heightM: positive(q.heightM, at + ".position.heightM", 1e4) };
|
|
69
|
+
const candela = i.candela !== undefined ? (i.illuminanceLux === undefined && i.distanceM === undefined ? positive(i.candela, at + ".intensity.candela", 1e9) :
|
|
70
|
+
fail("illumination" + at + ".intensity takes candela or illuminanceLux and distanceM, not both.")) :
|
|
71
|
+
positive(i.illuminanceLux, at + ".intensity.illuminanceLux", 1e7) * positive(i.distanceM, at + ".intensity.distanceM", 1e4) ** 2;
|
|
72
|
+
return { illuminant, option, position, candela, scale: calculateIlluminantIrradianceScale(illuminant.shape, 1) * candela, shape: tabulatedSpectrum(illuminant.shape) };
|
|
73
|
+
});
|
|
74
|
+
/** cos θ/d² of point source j at plane point (x, y) m. */
|
|
75
|
+
const falloff = (p, x, y) => {
|
|
76
|
+
const dx = x - p.position.xM, dy = y - p.position.yM, h = p.position.heightM, d2 = dx * dx + dy * dy + h * h;
|
|
77
|
+
return h / (d2 * Math.sqrt(d2));
|
|
78
|
+
};
|
|
79
|
+
const n = sources.length, shapes = sources.map((s) => tabulatedSpectrum(s.illuminant.shape));
|
|
80
|
+
if (n < 2 && record.blend !== undefined)
|
|
81
|
+
fail("illumination.blend needs two or more sources.");
|
|
82
|
+
const b = n < 2 ? { kind: "none" } : fields(record.blend, ".blend", ["kind", "from", "to", "center", "radius", "stops", "regions", "outsideSource"]);
|
|
83
|
+
const index = (x, name) => (Number.isInteger(x) && x >= 0 && x < n ? x : fail("illumination.blend." + name + " must be a source index from 0 to " + (n - 1) + "."));
|
|
84
|
+
const unused = (keys) => { for (const key of keys)
|
|
85
|
+
if (b[key] !== undefined)
|
|
86
|
+
fail("illumination.blend." + key + " does not apply to a " + String(b.kind) + " blend."); };
|
|
87
|
+
// A point's position t on a gradient or radial blend, or its source on a regions blend.
|
|
88
|
+
let position, blend = null, stops;
|
|
89
|
+
if (b.kind === "none")
|
|
90
|
+
position = () => 0;
|
|
91
|
+
else if (b.kind === "regions") {
|
|
92
|
+
unused(["from", "to", "center", "radius", "stops"]);
|
|
93
|
+
const rects = b.regions;
|
|
94
|
+
if (!Array.isArray(rects) || rects.length < 1 || rects.length > MIXED_ILLUMINATION_MAX_REGIONS)
|
|
95
|
+
fail("illumination.blend.regions must list 1 to " + MIXED_ILLUMINATION_MAX_REGIONS + " rectangles.");
|
|
96
|
+
const regions = rects.map((entry, i) => {
|
|
97
|
+
const at = ".blend.regions[" + i + "]", r = fields(entry, at, ["source", "uMin", "uMax", "vMin", "vMax"]);
|
|
98
|
+
const region = { source: index(r.source, "regions[" + i + "].source"), uMin: finite(r.uMin, at + ".uMin"), uMax: finite(r.uMax, at + ".uMax"), vMin: finite(r.vMin, at + ".vMin"), vMax: finite(r.vMax, at + ".vMax") };
|
|
99
|
+
if (!(region.uMin <= region.uMax && region.vMin <= region.vMax))
|
|
100
|
+
fail("illumination" + at + " uMin and vMin must not exceed uMax and vMax.");
|
|
101
|
+
return region;
|
|
102
|
+
});
|
|
103
|
+
const outside = index(b.outsideSource, "outsideSource");
|
|
104
|
+
position = (u, v) => regions.find((r) => u >= r.uMin && u <= r.uMax && v >= r.vMin && v <= r.vMax)?.source ?? outside;
|
|
105
|
+
blend = { kind: "regions", regions, outsideSource: outside };
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (b.kind === "linear-gradient") {
|
|
109
|
+
unused(["center", "radius", "regions", "outsideSource"]);
|
|
110
|
+
const from = point(b.from, "from"), to = point(b.to, "to"), du = to.u - from.u, dv = to.v - from.v, length2 = du * du + dv * dv;
|
|
111
|
+
if (!(length2 > 0))
|
|
112
|
+
fail("illumination.blend from and to must differ.");
|
|
113
|
+
position = (u, v) => Math.min(1, Math.max(0, ((u - from.u) * du + (v - from.v) * dv) / length2));
|
|
114
|
+
blend = { kind: b.kind, from, to };
|
|
115
|
+
}
|
|
116
|
+
else if (b.kind === "radial") {
|
|
117
|
+
unused(["from", "to", "regions", "outsideSource"]);
|
|
118
|
+
const center = b.center === undefined ? { u: 0, v: 0 } : point(b.center, "center"), radius = finite(b.radius, ".blend.radius");
|
|
119
|
+
if (!(radius > 0))
|
|
120
|
+
fail("illumination.blend.radius must be positive.");
|
|
121
|
+
position = (u, v) => Math.min(1, Math.hypot(u - center.u, v - center.v) / radius);
|
|
122
|
+
blend = { kind: b.kind, center, radius };
|
|
123
|
+
}
|
|
124
|
+
else
|
|
125
|
+
return fail('illumination.blend.kind must be "linear-gradient", "radial" or "regions".');
|
|
126
|
+
const declared = b.stops;
|
|
127
|
+
if (declared !== undefined && (!Array.isArray(declared) || declared.length !== n))
|
|
128
|
+
fail("illumination.blend.stops must list one stop per source.");
|
|
129
|
+
blend.stops = stops = declared ? declared.map((x, i) => {
|
|
130
|
+
const stop = finite(x, ".blend.stops[" + i + "]");
|
|
131
|
+
return stop >= 0 && stop <= 1 && (i === 0 || stop >= declared[i - 1]) ? stop : fail("illumination.blend.stops must be non-decreasing in [0, 1].");
|
|
132
|
+
}) : sources.map((_, i) => i / (n - 1));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The blend at a point: the lower source i and the fraction f of source i + 1 (f = 0: source i alone). On a gradient, the segment
|
|
136
|
+
* [stops[i], stops[i + 1]) that holds t; equal stops are passed over, so the later source takes the stop.
|
|
137
|
+
*/
|
|
138
|
+
const at = (u, v) => {
|
|
139
|
+
const t = position(u, v);
|
|
140
|
+
if (!stops)
|
|
141
|
+
return [t, 0];
|
|
142
|
+
let i = 0;
|
|
143
|
+
while (i < n - 1 && t >= stops[i + 1])
|
|
144
|
+
i++;
|
|
145
|
+
return i === n - 1 || t <= stops[i] ? [i, 0] : [i, (t - stops[i]) / (stops[i + 1] - stops[i])];
|
|
146
|
+
};
|
|
147
|
+
const [i0, f0] = at(0, 0), weights = sources.map((_, i) => (i === i0 ? 1 - f0 : i === i0 + 1 ? f0 : 0));
|
|
148
|
+
// Every light at the frame center, the blend's sources first: a point source's irradiance scale there is c_j·cos θ/d².
|
|
149
|
+
const lights = [...sources, ...points.map((p) => {
|
|
150
|
+
const atCenter = falloff(p, 0, 0);
|
|
151
|
+
return Object.freeze({ illuminant: p.illuminant, option: p.option, illuminanceLux: p.candela * atCenter, irradianceScale: p.scale * atCenter,
|
|
152
|
+
report: Object.freeze({ illuminant: p.illuminant.id, position: p.position, intensityCandela: p.candela }) });
|
|
153
|
+
})];
|
|
154
|
+
const illuminancesLux = lights.map((l, i) => (i < n ? weights[i] * l.illuminanceLux : l.illuminanceLux));
|
|
155
|
+
let strongest = 0, illuminanceLux = 0;
|
|
156
|
+
illuminancesLux.forEach((e, i) => { illuminanceLux += e; if (e > illuminancesLux[strongest])
|
|
157
|
+
strongest = i; });
|
|
158
|
+
/** The blend's irradiance (k·S(λ) itself where one source has weight 1). */
|
|
159
|
+
const ambient = (u, v, nm) => {
|
|
160
|
+
if (n === 0)
|
|
161
|
+
return 0;
|
|
162
|
+
const [i, f] = at(u, v), a = sources[i].irradianceScale * shapes[i](nm);
|
|
163
|
+
return f === 0 ? a : (1 - f) * a + f * (sources[i + 1].irradianceScale * shapes[i + 1](nm));
|
|
164
|
+
};
|
|
165
|
+
return Object.freeze({ sources: Object.freeze(lights), blend: blend && Object.freeze(blend),
|
|
166
|
+
irradianceAt: points.length === 0 ? (u, v, _x, _y, nm) => ambient(u, v, nm) : (u, v, x, y, nm) => {
|
|
167
|
+
let e = ambient(u, v, nm);
|
|
168
|
+
for (const p of points)
|
|
169
|
+
e += p.scale * p.shape(nm) * falloff(p, x, y);
|
|
170
|
+
return e;
|
|
171
|
+
},
|
|
172
|
+
center: Object.freeze({ illuminanceLux, weights, illuminancesLux, strongestSource: strongest }) });
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=mixed-illumination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mixed-illumination.js","sourceRoot":"","sources":["../../src/facade/mixed-illumination.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * 2.22 (#550): spatially varying illumination of the scene plane (mixed lighting), loaded on demand by `simulateCapture` and the\n * display view only when a request has `illumination`, so requests without it neither load nor run it.\n *\n * Model: N sources (1 to {@link MIXED_ILLUMINATION_MAX_SOURCES}; one lights the whole plane), source i an illuminant spectral shape S_i(λ) scaled to\n * `illuminanceLux` E_i exactly as the single illuminant is (`calculateIlluminantIrradianceScale`: k_i with 683 ∫ k_i S_i ȳ dλ = E_i).\n * The plane's spectral irradiance at a point p is the declared blend E(p, λ) = Σ_i w_i(p)·k_i·S_i(λ) with weights w_i(p) ≥ 0 that\n * sum to 1, so the illuminance at p is Σ_i w_i(p)·E_i and, where one weight is 1, the light is exactly that source's. A Lambertian\n * reflector then has radiance ρ(λ)·E(p, λ)/π, as under one illuminant. The blend is a declared crossfade of irradiance, not a\n * model of how light falls off from a lamp or a window (no inverse-square law, no cosine falloff, no shadows); it is exact for\n * what it declares.\n *\n * Point sources (owner decision 2026-11-10, a step toward flash): source j is a point at height h_j in front of the plane above\n * plane point (x_j, y_j) m, with luminous intensity I_j cd (the same in every direction toward the plane) and spectral shape S_j(λ),\n * scaled so that 683 ∫ c_j S_j ȳ dλ = I_j. A point p of the plane at distance d from it receives E_j(p, λ) = c_j·S_j(λ)·cos θ/d²,\n * with cos θ = h_j/d: the inverse-square law with Lambert's cosine law, which follows from the definitions of luminous intensity\n * (flux per solid angle) and illuminance (flux per area) with the solid angle dΩ = dA·cos θ/d² that a plane element subtends (CIE S\n * 017:2020 ILV, International Lighting Vocabulary, entries \"luminous intensity\" and \"illuminance\", free at e-ilv.cie.co.at; BIPM SI\n * Brochure, 9th edition 2019, Table 4, the steradian). Exact for an ideal isotropic point; no occlusion (the camera casts no shadow),\n * no source size and no inter-reflection. The point sources add to the blend: E(p, λ) = Σ_i w_i(p)·k_i·S_i(λ) + Σ_j E_j(p, λ).\n *\n * Coordinates are the chart's: a fixed-size chart's chart-local units (±1 at its left and right edges, v up on the same scale,\n * extended linearly over the surround), or, without `chartSize`, frame units (±1 at the frame's left and right edges), which are\n * the chart-local units of a chart that fills the frame. Both put (0, 0) at the frame center.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { calculateIlluminantIrradianceScale, tabulatedSpectrum, type CaptureIlluminant, type ResolvedCaptureIlluminant } from \"./scene-radiance.js\";\n\n/** Most sources an `illumination` may list. */\nexport const MIXED_ILLUMINATION_MAX_SOURCES = 8;\n/** Most regions a `regions` blend may list. */\nexport const MIXED_ILLUMINATION_MAX_REGIONS = 64;\n\n/** One light source of a mixed illumination: an illuminant (as `simulateCapture` `illuminant` takes it) at an illuminance. */\nexport interface MixedIlluminationSource {\n illuminant: CaptureIlluminant;\n /** Illuminance this source alone gives where its weight is 1, lux, (0, 1e7]. */\n illuminanceLux: number;\n}\n/**\n * 2.22 (#550): a point light on the scene plane's front side: an illuminant spectrum, a position in metres (`xM`, `yM` on the plane\n * from the optical axis, +x right and +y up as seen from the camera; `heightM` > 0 in front of the plane, toward the camera) and an\n * intensity, either in candela or as the illuminance it gives at normal incidence at a reference distance (I = E·r²).\n */\nexport interface MixedIlluminationPointSource {\n illuminant: CaptureIlluminant;\n position: { xM: number; yM: number; heightM: number };\n intensity: { candela: number } | { illuminanceLux: number; distanceM: number };\n}\n/** A point in chart coordinates (see {@link MixedIllumination}). */\nexport interface MixedIlluminationPoint { u: number; v: number }\n/**\n * How the sources blend across the chart. `linear-gradient` and `radial` map a point to a position t in [0, 1] (its projection\n * onto `from` → `to`, clamped; or its distance from `center` over `radius`, clamped) and crossfade linearly between consecutive\n * sources at their `stops` (one per source, non-decreasing in [0, 1]; default evenly spaced from 0 to 1): source 0 alone up to\n * stops[0], the last source alone from its stop on, and two equal stops a hard edge (the later source from the stop on).\n * `regions` gives each point the source of the first rectangle that contains it (bounds inclusive) and `outsideSource` elsewhere.\n */\nexport type MixedIlluminationBlend =\n | { kind: \"linear-gradient\"; from: MixedIlluminationPoint; to: MixedIlluminationPoint; stops?: readonly number[] }\n | { kind: \"radial\"; center?: MixedIlluminationPoint; radius: number; stops?: readonly number[] }\n | { kind: \"regions\"; regions: readonly { source: number; uMin: number; uMax: number; vMin: number; vMax: number }[]; outsideSource: number };\n/**\n * 2.22 (#550), `simulateCapture` `illumination`: sources blended across the chart and point sources with inverse-square falloff\n * (docs/MIXED_ILLUMINATION.md). It replaces `illuminant` and `illuminanceLux`. `sources` lists 1 to 8 sources (with two or more, a\n * `blend` is required; one lights the whole plane); `points` lists 1 to 8 point sources; a request needs two or more sources or at least one point. Blend\n * coordinates are chart-local with `chartSize` and frame units without it; (0, 0) is the frame center.\n */\nexport interface MixedIllumination {\n sources?: readonly MixedIlluminationSource[];\n blend?: MixedIlluminationBlend;\n points?: readonly MixedIlluminationPointSource[];\n}\n\n/** Internal: a validated mixed illumination with its lights resolved and scaled. */\nexport interface ResolvedMixedIllumination {\n /**\n * The lights, the blend's sources first and then the point sources: each with the illuminance it gives at the frame center when\n * it lights it alone (a point source's I·cos θ/d²) and the scale k with k·S(λ) that light's irradiance there; `report` is its\n * public form (the assumption's and the digest's).\n */\n sources: readonly { illuminant: ResolvedCaptureIlluminant; option: CaptureIlluminant; illuminanceLux: number; irradianceScale: number; report: Readonly<Record<string, unknown>> }[];\n /** The blend with its defaults filled, or null (one source or none). */\n blend: Readonly<Record<string, unknown>> | null;\n /** Spectral irradiance at a plane point, W·m⁻²·nm⁻¹: (u, v) chart coordinates for the blend, (xM, yM) metres for the point sources. */\n irradianceAt(u: number, v: number, xM: number, yM: number, wavelengthNanometers: number): number;\n /**\n * At the frame center: the illuminance, the blend's source weights, each light's illuminance there (w_i·E_i; a point's\n * I·cos θ/d²) and the strongest light (the largest, the first on a tie; an index into `sources`).\n */\n center: { illuminanceLux: number; weights: number[]; illuminancesLux: number[]; strongestSource: number };\n}\n\ntype Fields = Record<string, unknown>;\nconst fail = (message: string): never => { throw new InvalidConfigurationError(message); };\n/** A record with no other fields than these (`name` relative to `illumination`). */\nconst fields = (value: unknown, name: string, keys: string[]): Fields => {\n requireInputObject(value, \"illumination\" + name);\n for (const key of Object.keys(value as object)) if (!keys.includes(key)) fail(\"Unknown illumination\" + name + \" field: \" + key + \".\");\n return value as Fields;\n};\nconst finite = (x: unknown, name: string): number => (typeof x === \"number\" && Number.isFinite(x) && Math.abs(x) <= 1e6 ? x : fail(\"illumination\" + name + \" must be a finite number.\"));\nconst positive = (x: unknown, name: string, maximum: number): number => (typeof x === \"number\" && x > 0 && x <= maximum ? x : fail(\"illumination\" + name + \" must be positive, up to \" + maximum + \".\"));\nconst list = (value: unknown, name: string, what: string): unknown[] => (value === undefined ? [] : Array.isArray(value) && value.length >= 1 && value.length <= MIXED_ILLUMINATION_MAX_SOURCES ? value :\n fail(\"illumination.\" + name + \" must list 1 to \" + MIXED_ILLUMINATION_MAX_SOURCES + \" \" + what + \".\"));\nconst point = (value: unknown, name: string): MixedIlluminationPoint => {\n const r = fields(value, \".blend.\" + name, [\"u\", \"v\"]);\n return { u: finite(r.u, \".blend.\" + name + \".u\"), v: finite(r.v, \".blend.\" + name + \".v\") };\n};\n\n/**\n * Internal: validates `illumination` and resolves its sources with `resolveIlluminant` (the capture's `resolveCaptureIlluminant`, or\n * the display view's portable one).\n */\nexport function resolveMixedIllumination(value: unknown, resolveIlluminant: (value: CaptureIlluminant) => ResolvedCaptureIlluminant): ResolvedMixedIllumination {\n const record = fields(value, \"\", [\"sources\", \"blend\", \"points\"]), given = list(record.sources, \"sources\", \"sources\"), pointList = list(record.points, \"points\", \"point sources\");\n if (pointList.length === 0 && given.length < 2) fail(\"illumination needs two or more sources, or a point source; one source alone is illuminant and illuminanceLux.\");\n const sources = given.map((entry, i) => {\n const s = fields(entry, \".sources[\" + i + \"]\", [\"illuminant\", \"illuminanceLux\"]), option = s.illuminant as CaptureIlluminant, illuminant = resolveIlluminant(option);\n return Object.freeze({ illuminant, option, illuminanceLux: s.illuminanceLux as number, irradianceScale: calculateIlluminantIrradianceScale(illuminant.shape, s.illuminanceLux as number),\n report: Object.freeze({ illuminant: illuminant.id, illuminanceLux: s.illuminanceLux }) });\n });\n // Point sources: c_j with 683 ∫ c_j S_j ȳ dλ = I_j (the irradiance scale of 1 lux, times I_j), and E_j = c_j S_j(λ)·h/d³.\n const points = pointList.map((entry, j) => {\n const at = \".points[\" + j + \"]\", p = fields(entry, at, [\"illuminant\", \"position\", \"intensity\"]), option = p.illuminant as CaptureIlluminant, illuminant = resolveIlluminant(option);\n const q = fields(p.position, at + \".position\", [\"xM\", \"yM\", \"heightM\"]), i = fields(p.intensity, at + \".intensity\", [\"candela\", \"illuminanceLux\", \"distanceM\"]);\n const position = { xM: finite(q.xM, at + \".position.xM\"), yM: finite(q.yM, at + \".position.yM\"), heightM: positive(q.heightM, at + \".position.heightM\", 1e4) };\n const candela = i.candela !== undefined ? (i.illuminanceLux === undefined && i.distanceM === undefined ? positive(i.candela, at + \".intensity.candela\", 1e9) :\n fail(\"illumination\" + at + \".intensity takes candela or illuminanceLux and distanceM, not both.\")) :\n positive(i.illuminanceLux, at + \".intensity.illuminanceLux\", 1e7) * positive(i.distanceM, at + \".intensity.distanceM\", 1e4) ** 2;\n return { illuminant, option, position, candela, scale: calculateIlluminantIrradianceScale(illuminant.shape, 1) * candela, shape: tabulatedSpectrum(illuminant.shape) };\n });\n /** cos θ/d² of point source j at plane point (x, y) m. */\n const falloff = (p: typeof points[number], x: number, y: number): number => {\n const dx = x - p.position.xM, dy = y - p.position.yM, h = p.position.heightM, d2 = dx * dx + dy * dy + h * h;\n return h / (d2 * Math.sqrt(d2));\n };\n const n = sources.length, shapes = sources.map((s) => tabulatedSpectrum(s.illuminant.shape));\n if (n < 2 && record.blend !== undefined) fail(\"illumination.blend needs two or more sources.\");\n const b = n < 2 ? { kind: \"none\" } : fields(record.blend, \".blend\", [\"kind\", \"from\", \"to\", \"center\", \"radius\", \"stops\", \"regions\", \"outsideSource\"]);\n const index = (x: unknown, name: string): number => (Number.isInteger(x) && (x as number) >= 0 && (x as number) < n ? x as number : fail(\"illumination.blend.\" + name + \" must be a source index from 0 to \" + (n - 1) + \".\"));\n const unused = (keys: string[]): void => { for (const key of keys) if (b[key] !== undefined) fail(\"illumination.blend.\" + key + \" does not apply to a \" + String(b.kind) + \" blend.\"); };\n // A point's position t on a gradient or radial blend, or its source on a regions blend.\n let position: (u: number, v: number) => number, blend: Fields | null = null, stops: number[] | undefined;\n if (b.kind === \"none\") position = (): number => 0;\n else if (b.kind === \"regions\") {\n unused([\"from\", \"to\", \"center\", \"radius\", \"stops\"]);\n const rects = b.regions as unknown[];\n if (!Array.isArray(rects) || rects.length < 1 || rects.length > MIXED_ILLUMINATION_MAX_REGIONS) fail(\"illumination.blend.regions must list 1 to \" + MIXED_ILLUMINATION_MAX_REGIONS + \" rectangles.\");\n const regions = rects.map((entry, i) => {\n const at = \".blend.regions[\" + i + \"]\", r = fields(entry, at, [\"source\", \"uMin\", \"uMax\", \"vMin\", \"vMax\"]);\n const region = { source: index(r.source, \"regions[\" + i + \"].source\"), uMin: finite(r.uMin, at + \".uMin\"), uMax: finite(r.uMax, at + \".uMax\"), vMin: finite(r.vMin, at + \".vMin\"), vMax: finite(r.vMax, at + \".vMax\") };\n if (!(region.uMin <= region.uMax && region.vMin <= region.vMax)) fail(\"illumination\" + at + \" uMin and vMin must not exceed uMax and vMax.\");\n return region;\n });\n const outside = index(b.outsideSource, \"outsideSource\");\n position = (u: number, v: number): number => regions.find((r) => u >= r.uMin && u <= r.uMax && v >= r.vMin && v <= r.vMax)?.source ?? outside;\n blend = { kind: \"regions\", regions, outsideSource: outside };\n } else {\n if (b.kind === \"linear-gradient\") {\n unused([\"center\", \"radius\", \"regions\", \"outsideSource\"]);\n const from = point(b.from, \"from\"), to = point(b.to, \"to\"), du = to.u - from.u, dv = to.v - from.v, length2 = du * du + dv * dv;\n if (!(length2 > 0)) fail(\"illumination.blend from and to must differ.\");\n position = (u: number, v: number): number => Math.min(1, Math.max(0, ((u - from.u) * du + (v - from.v) * dv) / length2));\n blend = { kind: b.kind, from, to };\n } else if (b.kind === \"radial\") {\n unused([\"from\", \"to\", \"regions\", \"outsideSource\"]);\n const center = b.center === undefined ? { u: 0, v: 0 } : point(b.center, \"center\"), radius = finite(b.radius, \".blend.radius\");\n if (!(radius > 0)) fail(\"illumination.blend.radius must be positive.\");\n position = (u: number, v: number): number => Math.min(1, Math.hypot(u - center.u, v - center.v) / radius);\n blend = { kind: b.kind, center, radius };\n } else return fail('illumination.blend.kind must be \"linear-gradient\", \"radial\" or \"regions\".');\n const declared = b.stops as unknown[] | undefined;\n if (declared !== undefined && (!Array.isArray(declared) || declared.length !== n)) fail(\"illumination.blend.stops must list one stop per source.\");\n blend!.stops = stops = declared ? declared.map((x, i) => {\n const stop = finite(x, \".blend.stops[\" + i + \"]\");\n return stop >= 0 && stop <= 1 && (i === 0 || stop >= (declared[i - 1] as number)) ? stop : fail(\"illumination.blend.stops must be non-decreasing in [0, 1].\");\n }) : sources.map((_, i) => i / (n - 1));\n }\n /**\n * The blend at a point: the lower source i and the fraction f of source i + 1 (f = 0: source i alone). On a gradient, the segment\n * [stops[i], stops[i + 1]) that holds t; equal stops are passed over, so the later source takes the stop.\n */\n const at = (u: number, v: number): [number, number] => {\n const t = position(u, v);\n if (!stops) return [t, 0];\n let i = 0;\n while (i < n - 1 && t >= stops[i + 1]!) i++;\n return i === n - 1 || t <= stops[i]! ? [i, 0] : [i, (t - stops[i]!) / (stops[i + 1]! - stops[i]!)];\n };\n const [i0, f0] = at(0, 0), weights = sources.map((_, i) => (i === i0 ? 1 - f0 : i === i0 + 1 ? f0 : 0));\n // Every light at the frame center, the blend's sources first: a point source's irradiance scale there is c_j·cos θ/d².\n const lights = [...sources, ...points.map((p) => {\n const atCenter = falloff(p, 0, 0);\n return Object.freeze({ illuminant: p.illuminant, option: p.option, illuminanceLux: p.candela * atCenter, irradianceScale: p.scale * atCenter,\n report: Object.freeze({ illuminant: p.illuminant.id, position: p.position, intensityCandela: p.candela }) });\n })];\n const illuminancesLux = lights.map((l, i) => (i < n ? weights[i]! * l.illuminanceLux : l.illuminanceLux));\n let strongest = 0, illuminanceLux = 0;\n illuminancesLux.forEach((e, i) => { illuminanceLux += e; if (e > illuminancesLux[strongest]!) strongest = i; });\n /** The blend's irradiance (k·S(λ) itself where one source has weight 1). */\n const ambient = (u: number, v: number, nm: number): number => {\n if (n === 0) return 0;\n const [i, f] = at(u, v), a = sources[i]!.irradianceScale * shapes[i]!(nm);\n return f === 0 ? a : (1 - f) * a + f * (sources[i + 1]!.irradianceScale * shapes[i + 1]!(nm));\n };\n return Object.freeze({ sources: Object.freeze(lights), blend: blend && Object.freeze(blend),\n irradianceAt: points.length === 0 ? (u: number, v: number, _x: number, _y: number, nm: number): number => ambient(u, v, nm) : (u: number, v: number, x: number, y: number, nm: number): number => {\n let e = ambient(u, v, nm);\n for (const p of points) e += p.scale * p.shape(nm) * falloff(p, x, y);\n return e;\n },\n center: Object.freeze({ illuminanceLux, weights, illuminancesLux, strongestSource: strongest }) });\n}\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ColorimetricSpectrum } from "../color/color-matching-functions.js";
|
|
2
|
+
import type { CaptureIlluminant } from "./scene-radiance.js";
|
|
3
|
+
/** Identity of the built-in light-source set. */
|
|
4
|
+
export declare const REPRESENTATIVE_LIGHT_SOURCE_SET_ID = "photivra-representative-light-sources";
|
|
5
|
+
/** Version of the set's spectra; a change to any spectrum changes it. */
|
|
6
|
+
export declare const REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION = "1.0.0";
|
|
7
|
+
/** Domain of every spectrum, nm (the CIE 1931 observer's domain; the capture facade uses 380–780 nm of it). */
|
|
8
|
+
export declare const REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS: Readonly<{
|
|
9
|
+
minimum: 360;
|
|
10
|
+
maximum: 830;
|
|
11
|
+
}>;
|
|
12
|
+
/** The source ids, in set order. */
|
|
13
|
+
export declare const REPRESENTATIVE_LIGHT_SOURCE_IDS: readonly ["tri-phosphor-fluorescent-4000k", "white-led-low-rendering-4000k", "white-led-high-rendering-4000k", "warm-white-led-2700k"];
|
|
14
|
+
export type RepresentativeLightSourceId = (typeof REPRESENTATIVE_LIGHT_SOURCE_IDS)[number];
|
|
15
|
+
/** One Gaussian band a·exp(−½((λ − μ)/σ)²) of a source spectrum. */
|
|
16
|
+
export interface RepresentativeLightSourceBand {
|
|
17
|
+
kind: "gaussian";
|
|
18
|
+
/** What the band represents, for example "blue pump" or "green phosphor line". */
|
|
19
|
+
role: string;
|
|
20
|
+
amplitude: number;
|
|
21
|
+
centerNanometers: number;
|
|
22
|
+
standardDeviationNanometers: number;
|
|
23
|
+
}
|
|
24
|
+
/** One built-in source: its id, name, design reason and spectrum definition. */
|
|
25
|
+
export interface RepresentativeLightSource {
|
|
26
|
+
sourceId: RepresentativeLightSourceId;
|
|
27
|
+
/** Photivra's descriptive name, for example "White LED, high color rendering, 4000 K". */
|
|
28
|
+
name: string;
|
|
29
|
+
family: "fluorescent" | "led";
|
|
30
|
+
/** The CCT the spectrum was designed for, K (its computed CCT is within 1 K of it, Duv within 0.0001; docs/LIGHT_SOURCES.md). */
|
|
31
|
+
nominalCorrelatedColorTemperatureKelvin: number;
|
|
32
|
+
/** Always "representative, not a named product". */
|
|
33
|
+
label: "representative, not a named product";
|
|
34
|
+
/** One sentence: what the source represents and why it is built this way. */
|
|
35
|
+
reason: string;
|
|
36
|
+
/** The constant b (a low continuum), relative units. */
|
|
37
|
+
base: number;
|
|
38
|
+
bands: readonly RepresentativeLightSourceBand[];
|
|
39
|
+
}
|
|
40
|
+
export interface RepresentativeLightSourceSet {
|
|
41
|
+
setId: typeof REPRESENTATIVE_LIGHT_SOURCE_SET_ID;
|
|
42
|
+
setVersion: typeof REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION;
|
|
43
|
+
wavelengthRangeNanometers: {
|
|
44
|
+
readonly minimum: number;
|
|
45
|
+
readonly maximum: number;
|
|
46
|
+
};
|
|
47
|
+
sources: readonly RepresentativeLightSource[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The built-in representative light sources: Photivra's own parametric spectra of a tri-phosphor fluorescent tube and
|
|
51
|
+
* three white LEDs (low and high color rendering at 4000 K, warm at 2700 K). A static, deeply frozen engine contract.
|
|
52
|
+
* Each is "representative, not a named product"; docs/LIGHT_SOURCES.md gives every formula and the reasons for it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getRepresentativeLightSourceSet(): RepresentativeLightSourceSet;
|
|
55
|
+
/**
|
|
56
|
+
* A source's relative spectral power distribution S(λ) = b + Σ aₖ·exp(−½((λ − μₖ)/σₖ)²) as a function spectrum on
|
|
57
|
+
* 360–830 nm (relative units; values memoized per wavelength). Pass it to a capture as
|
|
58
|
+
* `illuminant: { kind: "spectrum", spectrum }`; for color work use `sensor.spectralStepNanometers: 5` or finer
|
|
59
|
+
* (docs/LIGHT_SOURCES.md, "Quadrature"). Throws `InvalidScientificInputError` outside 360–830 nm.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createRepresentativeLightSourceSpectrum(input: {
|
|
62
|
+
sourceId: RepresentativeLightSourceId;
|
|
63
|
+
}): ColorimetricSpectrum;
|
|
64
|
+
/**
|
|
65
|
+
* A capture illuminant for a source: `{ kind: "spectrum", spectrum, label }` with the spectrum of
|
|
66
|
+
* {@link createRepresentativeLightSourceSpectrum} and the label "<name> (representative, not a named product)".
|
|
67
|
+
*/
|
|
68
|
+
export declare function createRepresentativeLightSourceIlluminant(input: {
|
|
69
|
+
sourceId: RepresentativeLightSourceId;
|
|
70
|
+
}): Extract<CaptureIlluminant, {
|
|
71
|
+
kind: "spectrum";
|
|
72
|
+
}>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Representative light sources (#557): four relative spectral power distributions designed by Photivra for indoor
|
|
4
|
+
* light presets, so that two lights at the same correlated color temperature (CCT) can render colors differently.
|
|
5
|
+
*
|
|
6
|
+
* Every spectrum is an engine-defined analytic function of wavelength, not measured or copied data: a constant plus a
|
|
7
|
+
* sum of Gaussian bands,
|
|
8
|
+
*
|
|
9
|
+
* S(λ) = b + Σₖ aₖ·exp(−½·((λ − μₖ)/σₖ)²),
|
|
10
|
+
*
|
|
11
|
+
* with λ in nanometers on 360–830 nm (the CIE 1931 observer's domain), in relative units (the capture facade scales an
|
|
12
|
+
* illuminant to its lux). Each source's band shapes were chosen by Photivra to represent a kind of lamp; two amplitudes
|
|
13
|
+
* (or one amplitude and one band center) were then solved so the chromaticity lies on the Planckian locus at the stated
|
|
14
|
+
* CCT, and rounded (amplitudes to 0.001, wavelengths to 0.1 nm). Those rounded values are the definition. The CCT and
|
|
15
|
+
* Duv of every rounded spectrum are checked with the engine's own `calculateCorrelatedColorTemperature` and
|
|
16
|
+
* `estimateCorrelatedColorTemperatureMcCamy` (docs/LIGHT_SOURCES.md, test/representative-light-sources.test.ts).
|
|
17
|
+
*
|
|
18
|
+
* These are representative constructions, not named products: no CIE, IES or manufacturer spectral data, names or
|
|
19
|
+
* tables are used. The bands are wider than the emission lines of real fluorescent phosphors so that the 5 nm sensor
|
|
20
|
+
* quadrature recommended for color work integrates them; real lamps of each kind vary widely.
|
|
21
|
+
*
|
|
22
|
+
* The exponentials use the portable `portableExp`, so a spectrum's values have the same bits in every JavaScript
|
|
23
|
+
* engine, in captures and in the display view alike.
|
|
24
|
+
*
|
|
25
|
+
* The module is not reached by `simulateCapture` itself: an application passes a spectrum as
|
|
26
|
+
* `illuminant: { kind: "spectrum", spectrum }` (or {@link createRepresentativeLightSourceIlluminant}), so bundles that
|
|
27
|
+
* do not use the sources do not contain them.
|
|
28
|
+
*/
|
|
29
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
30
|
+
import { InvalidScientificInputError } from "../core/validation.js";
|
|
31
|
+
import { requireInputKeys } from "../core/record-validation.js";
|
|
32
|
+
import { portableExp } from "../core/portable-math.js";
|
|
33
|
+
/** Identity of the built-in light-source set. */
|
|
34
|
+
export const REPRESENTATIVE_LIGHT_SOURCE_SET_ID = "photivra-representative-light-sources";
|
|
35
|
+
/** Version of the set's spectra; a change to any spectrum changes it. */
|
|
36
|
+
export const REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION = "1.0.0";
|
|
37
|
+
/** Domain of every spectrum, nm (the CIE 1931 observer's domain; the capture facade uses 380–780 nm of it). */
|
|
38
|
+
export const REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS = /* @__PURE__ */ Object.freeze({ minimum: 360, maximum: 830 });
|
|
39
|
+
/** The source ids, in set order. */
|
|
40
|
+
export const REPRESENTATIVE_LIGHT_SOURCE_IDS = /* @__PURE__ */ Object.freeze([
|
|
41
|
+
"tri-phosphor-fluorescent-4000k", "white-led-low-rendering-4000k", "white-led-high-rendering-4000k", "warm-white-led-2700k"
|
|
42
|
+
]);
|
|
43
|
+
const LABEL = "representative, not a named product";
|
|
44
|
+
/* Photivra's design (docs/LIGHT_SOURCES.md, "The spectra"); the solved values were rounded and are the definition. */
|
|
45
|
+
const ROWS = [
|
|
46
|
+
["tri-phosphor-fluorescent-4000k", "Tri-phosphor fluorescent, 4000 K", "fluorescent", 4000,
|
|
47
|
+
"Three narrow emission bands in the blue, green and red on a low continuum, the construction of a tri-phosphor fluorescent tube, which leaves gaps between the bands.",
|
|
48
|
+
0.02, [["blue phosphor band", 0.285, 450, 6], ["green phosphor line", 1, 544, 3.5], ["red phosphor line", 1.02, 611, 3.5]]],
|
|
49
|
+
["white-led-low-rendering-4000k", "White LED, low color rendering, 4000 K", "led", 4000,
|
|
50
|
+
"A blue pump peak and one broad yellow phosphor band, the simplest white LED, which is weak in the cyan and the deep red.",
|
|
51
|
+
0, [["blue pump", 1.123, 450, 9], ["yellow phosphor", 1, 579.1, 50]]],
|
|
52
|
+
["white-led-high-rendering-4000k", "White LED, high color rendering, 4000 K", "led", 4000,
|
|
53
|
+
"A blue pump with a green and a red phosphor band that fill the cyan and the red, a white LED built for high color rendering at the same CCT as the low-rendering one.",
|
|
54
|
+
0, [["blue pump", 1.306, 450, 9], ["green phosphor", 1, 530, 35], ["red phosphor", 1.264, 630, 40]]],
|
|
55
|
+
["warm-white-led-2700k", "Warm white LED, 2700 K", "led", 2700,
|
|
56
|
+
"The same pump and two-phosphor construction with more red phosphor and less pump, a warm white LED for home lighting.",
|
|
57
|
+
0, [["blue pump", 0.795, 450, 9], ["green phosphor", 1, 540, 38], ["red phosphor", 2.133, 630, 40]]]
|
|
58
|
+
];
|
|
59
|
+
const SET = /* @__PURE__ */ (() => Object.freeze({
|
|
60
|
+
setId: REPRESENTATIVE_LIGHT_SOURCE_SET_ID,
|
|
61
|
+
setVersion: REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION,
|
|
62
|
+
wavelengthRangeNanometers: REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS,
|
|
63
|
+
sources: Object.freeze(ROWS.map(([sourceId, name, family, cct, reason, base, bands]) => Object.freeze({
|
|
64
|
+
sourceId, name, family, nominalCorrelatedColorTemperatureKelvin: cct, label: LABEL, reason, base,
|
|
65
|
+
bands: Object.freeze(bands.map(([role, amplitude, centerNanometers, standardDeviationNanometers]) => Object.freeze({ kind: "gaussian", role, amplitude, centerNanometers, standardDeviationNanometers })))
|
|
66
|
+
})))
|
|
67
|
+
}))();
|
|
68
|
+
/**
|
|
69
|
+
* The built-in representative light sources: Photivra's own parametric spectra of a tri-phosphor fluorescent tube and
|
|
70
|
+
* three white LEDs (low and high color rendering at 4000 K, warm at 2700 K). A static, deeply frozen engine contract.
|
|
71
|
+
* Each is "representative, not a named product"; docs/LIGHT_SOURCES.md gives every formula and the reasons for it.
|
|
72
|
+
*/
|
|
73
|
+
export function getRepresentativeLightSourceSet() {
|
|
74
|
+
return SET;
|
|
75
|
+
}
|
|
76
|
+
function findSource(input, functionName) {
|
|
77
|
+
requireInputKeys(input, functionName, ["sourceId"]);
|
|
78
|
+
const id = input.sourceId;
|
|
79
|
+
const source = typeof id === "string" ? SET.sources.find((s) => s.sourceId === id) : undefined;
|
|
80
|
+
if (!source)
|
|
81
|
+
throw new InvalidConfigurationError("sourceId must be one of the built-in representative light sources (REPRESENTATIVE_LIGHT_SOURCE_IDS).");
|
|
82
|
+
return source;
|
|
83
|
+
}
|
|
84
|
+
/** Most wavelengths whose values one spectrum keeps (a capture uses a few hundred). */
|
|
85
|
+
const CACHE_LIMIT = 4096;
|
|
86
|
+
/**
|
|
87
|
+
* A source's relative spectral power distribution S(λ) = b + Σ aₖ·exp(−½((λ − μₖ)/σₖ)²) as a function spectrum on
|
|
88
|
+
* 360–830 nm (relative units; values memoized per wavelength). Pass it to a capture as
|
|
89
|
+
* `illuminant: { kind: "spectrum", spectrum }`; for color work use `sensor.spectralStepNanometers: 5` or finer
|
|
90
|
+
* (docs/LIGHT_SOURCES.md, "Quadrature"). Throws `InvalidScientificInputError` outside 360–830 nm.
|
|
91
|
+
*/
|
|
92
|
+
export function createRepresentativeLightSourceSpectrum(input) {
|
|
93
|
+
const source = findSource(input, "createRepresentativeLightSourceSpectrum");
|
|
94
|
+
const range = REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS, cache = new Map();
|
|
95
|
+
return {
|
|
96
|
+
kind: "function",
|
|
97
|
+
wavelengthRangeNanometers: { minimum: range.minimum, maximum: range.maximum },
|
|
98
|
+
evaluate(nm) {
|
|
99
|
+
const hit = cache.get(nm);
|
|
100
|
+
if (hit !== undefined)
|
|
101
|
+
return hit;
|
|
102
|
+
if (typeof nm !== "number" || !(nm >= range.minimum && nm <= range.maximum)) {
|
|
103
|
+
throw new InvalidScientificInputError("Representative light source " + source.sourceId + " is defined on 360–830 nm.");
|
|
104
|
+
}
|
|
105
|
+
let value = source.base;
|
|
106
|
+
for (const band of source.bands) {
|
|
107
|
+
const z = (nm - band.centerNanometers) / band.standardDeviationNanometers;
|
|
108
|
+
value += band.amplitude * portableExp(-0.5 * z * z);
|
|
109
|
+
}
|
|
110
|
+
if (cache.size < CACHE_LIMIT)
|
|
111
|
+
cache.set(nm, value);
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A capture illuminant for a source: `{ kind: "spectrum", spectrum, label }` with the spectrum of
|
|
118
|
+
* {@link createRepresentativeLightSourceSpectrum} and the label "<name> (representative, not a named product)".
|
|
119
|
+
*/
|
|
120
|
+
export function createRepresentativeLightSourceIlluminant(input) {
|
|
121
|
+
const source = findSource(input, "createRepresentativeLightSourceIlluminant");
|
|
122
|
+
return { kind: "spectrum", spectrum: createRepresentativeLightSourceSpectrum({ sourceId: source.sourceId }), label: source.name + " (" + LABEL + ")" };
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=representative-light-sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"representative-light-sources.js","sourceRoot":"","sources":["../../src/facade/representative-light-sources.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIvD,iDAAiD;AACjD,MAAM,CAAC,MAAM,kCAAkC,GAAG,uCAAuC,CAAC;AAC1F,yEAAyE;AACzE,MAAM,CAAC,MAAM,uCAAuC,GAAG,OAAO,CAAC;AAC/D,+GAA+G;AAC/G,MAAM,CAAC,MAAM,uDAAuD,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;AAErI,oCAAoC;AACpC,MAAM,CAAC,MAAM,+BAA+B,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC;IAC3E,gCAAgC,EAAE,+BAA+B,EAAE,gCAAgC,EAAE,sBAAsB;CACnH,CAAC,CAAC;AAqCZ,MAAM,KAAK,GAAG,qCAA8C,CAAC;AAE7D,sHAAsH;AACtH,MAAM,IAAI,GAAmI;IAC3I,CAAC,gCAAgC,EAAE,kCAAkC,EAAE,aAAa,EAAE,IAAI;QACxF,sKAAsK;QACtK,IAAI,EAAE,CAAC,CAAC,oBAAoB,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,qBAAqB,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC7H,CAAC,+BAA+B,EAAE,wCAAwC,EAAE,KAAK,EAAE,IAAI;QACrF,0HAA0H;QAC1H,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC,gCAAgC,EAAE,yCAAyC,EAAE,KAAK,EAAE,IAAI;QACvF,uKAAuK;QACvK,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,gBAAgB,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,cAAc,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACtG,CAAC,sBAAsB,EAAE,wBAAwB,EAAE,KAAK,EAAE,IAAI;QAC5D,uHAAuH;QACvH,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,gBAAgB,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,cAAc,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;CACvG,CAAC;AAEF,MAAM,GAAG,GAAiC,eAAe,CAAC,CAAC,GAAiC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;IAC3G,KAAK,EAAE,kCAAkC;IACzC,UAAU,EAAE,uCAAuC;IACnD,yBAAyB,EAAE,uDAAuD;IAClF,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QACpG,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,uCAAuC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI;QAChG,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,gBAAgB,EAAE,2BAA2B,CAAC,EAAE,EAAE,CAClG,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,UAAmB,EAAE,IAAI,EAAE,SAAS,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,CAAC,CAAC,CAAC;KACjH,CAAC,CAAC,CAAC;CACL,CAAC,CAAC,EAAE,CAAC;AAEN;;;;GAIG;AACH,MAAM,UAAU,+BAA+B;IAC7C,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,UAAU,CAAC,KAAc,EAAE,YAAoB;IACtD,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IACpD,MAAM,EAAE,GAAI,KAAgC,CAAC,QAAQ,CAAC;IACtD,MAAM,MAAM,GAAG,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/F,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,yBAAyB,CAAC,sGAAsG,CAAC,CAAC;IACzJ,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,uFAAuF;AACvF,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB;;;;;GAKG;AACH,MAAM,UAAU,uCAAuC,CAAC,KAAgD;IACtG,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,yCAAyC,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,uDAAuD,EAAE,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzG,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,yBAAyB,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE;QAC7E,QAAQ,CAAC,EAAU;YACjB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC1B,IAAI,GAAG,KAAK,SAAS;gBAAE,OAAO,GAAG,CAAC;YAClC,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,CAAC,EAAE,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC5E,MAAM,IAAI,2BAA2B,CAAC,8BAA8B,GAAG,MAAM,CAAC,QAAQ,GAAG,4BAA4B,CAAC,CAAC;YACzH,CAAC;YACD,IAAI,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;YACxB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBAChC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC,2BAA2B,CAAC;gBAC1E,KAAK,IAAI,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YACtD,CAAC;YACD,IAAI,KAAK,CAAC,IAAI,GAAG,WAAW;gBAAE,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YACnD,OAAO,KAAK,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yCAAyC,CAAC,KAAgD;IACxG,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,2CAA2C,CAAC,CAAC;IAC9E,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,uCAAuC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,GAAG,EAAE,CAAC;AACzJ,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Representative light sources (#557): four relative spectral power distributions designed by Photivra for indoor\n * light presets, so that two lights at the same correlated color temperature (CCT) can render colors differently.\n *\n * Every spectrum is an engine-defined analytic function of wavelength, not measured or copied data: a constant plus a\n * sum of Gaussian bands,\n *\n * S(λ) = b + Σₖ aₖ·exp(−½·((λ − μₖ)/σₖ)²),\n *\n * with λ in nanometers on 360–830 nm (the CIE 1931 observer's domain), in relative units (the capture facade scales an\n * illuminant to its lux). Each source's band shapes were chosen by Photivra to represent a kind of lamp; two amplitudes\n * (or one amplitude and one band center) were then solved so the chromaticity lies on the Planckian locus at the stated\n * CCT, and rounded (amplitudes to 0.001, wavelengths to 0.1 nm). Those rounded values are the definition. The CCT and\n * Duv of every rounded spectrum are checked with the engine's own `calculateCorrelatedColorTemperature` and\n * `estimateCorrelatedColorTemperatureMcCamy` (docs/LIGHT_SOURCES.md, test/representative-light-sources.test.ts).\n *\n * These are representative constructions, not named products: no CIE, IES or manufacturer spectral data, names or\n * tables are used. The bands are wider than the emission lines of real fluorescent phosphors so that the 5 nm sensor\n * quadrature recommended for color work integrates them; real lamps of each kind vary widely.\n *\n * The exponentials use the portable `portableExp`, so a spectrum's values have the same bits in every JavaScript\n * engine, in captures and in the display view alike.\n *\n * The module is not reached by `simulateCapture` itself: an application passes a spectrum as\n * `illuminant: { kind: \"spectrum\", spectrum }` (or {@link createRepresentativeLightSourceIlluminant}), so bundles that\n * do not use the sources do not contain them.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { portableExp } from \"../core/portable-math.js\";\nimport type { ColorimetricSpectrum } from \"../color/color-matching-functions.js\";\nimport type { CaptureIlluminant } from \"./scene-radiance.js\";\n\n/** Identity of the built-in light-source set. */\nexport const REPRESENTATIVE_LIGHT_SOURCE_SET_ID = \"photivra-representative-light-sources\";\n/** Version of the set's spectra; a change to any spectrum changes it. */\nexport const REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION = \"1.0.0\";\n/** Domain of every spectrum, nm (the CIE 1931 observer's domain; the capture facade uses 380–780 nm of it). */\nexport const REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS = /* @__PURE__ */ Object.freeze({ minimum: 360, maximum: 830 });\n\n/** The source ids, in set order. */\nexport const REPRESENTATIVE_LIGHT_SOURCE_IDS = /* @__PURE__ */ Object.freeze([\n \"tri-phosphor-fluorescent-4000k\", \"white-led-low-rendering-4000k\", \"white-led-high-rendering-4000k\", \"warm-white-led-2700k\"\n] as const);\nexport type RepresentativeLightSourceId = (typeof REPRESENTATIVE_LIGHT_SOURCE_IDS)[number];\n\n/** One Gaussian band a·exp(−½((λ − μ)/σ)²) of a source spectrum. */\nexport interface RepresentativeLightSourceBand {\n kind: \"gaussian\";\n /** What the band represents, for example \"blue pump\" or \"green phosphor line\". */\n role: string;\n amplitude: number;\n centerNanometers: number;\n standardDeviationNanometers: number;\n}\n\n/** One built-in source: its id, name, design reason and spectrum definition. */\nexport interface RepresentativeLightSource {\n sourceId: RepresentativeLightSourceId;\n /** Photivra's descriptive name, for example \"White LED, high color rendering, 4000 K\". */\n name: string;\n family: \"fluorescent\" | \"led\";\n /** The CCT the spectrum was designed for, K (its computed CCT is within 1 K of it, Duv within 0.0001; docs/LIGHT_SOURCES.md). */\n nominalCorrelatedColorTemperatureKelvin: number;\n /** Always \"representative, not a named product\". */\n label: \"representative, not a named product\";\n /** One sentence: what the source represents and why it is built this way. */\n reason: string;\n /** The constant b (a low continuum), relative units. */\n base: number;\n bands: readonly RepresentativeLightSourceBand[];\n}\n\nexport interface RepresentativeLightSourceSet {\n setId: typeof REPRESENTATIVE_LIGHT_SOURCE_SET_ID;\n setVersion: typeof REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION;\n wavelengthRangeNanometers: { readonly minimum: number; readonly maximum: number };\n sources: readonly RepresentativeLightSource[];\n}\n\nconst LABEL = \"representative, not a named product\" as const;\ntype BandRow = readonly [role: string, amplitude: number, centerNanometers: number, standardDeviationNanometers: number];\n/* Photivra's design (docs/LIGHT_SOURCES.md, \"The spectra\"); the solved values were rounded and are the definition. */\nconst ROWS: readonly (readonly [RepresentativeLightSourceId, string, \"fluorescent\" | \"led\", number, string, number, readonly BandRow[]])[] = [\n [\"tri-phosphor-fluorescent-4000k\", \"Tri-phosphor fluorescent, 4000 K\", \"fluorescent\", 4000,\n \"Three narrow emission bands in the blue, green and red on a low continuum, the construction of a tri-phosphor fluorescent tube, which leaves gaps between the bands.\",\n 0.02, [[\"blue phosphor band\", 0.285, 450, 6], [\"green phosphor line\", 1, 544, 3.5], [\"red phosphor line\", 1.02, 611, 3.5]]],\n [\"white-led-low-rendering-4000k\", \"White LED, low color rendering, 4000 K\", \"led\", 4000,\n \"A blue pump peak and one broad yellow phosphor band, the simplest white LED, which is weak in the cyan and the deep red.\",\n 0, [[\"blue pump\", 1.123, 450, 9], [\"yellow phosphor\", 1, 579.1, 50]]],\n [\"white-led-high-rendering-4000k\", \"White LED, high color rendering, 4000 K\", \"led\", 4000,\n \"A blue pump with a green and a red phosphor band that fill the cyan and the red, a white LED built for high color rendering at the same CCT as the low-rendering one.\",\n 0, [[\"blue pump\", 1.306, 450, 9], [\"green phosphor\", 1, 530, 35], [\"red phosphor\", 1.264, 630, 40]]],\n [\"warm-white-led-2700k\", \"Warm white LED, 2700 K\", \"led\", 2700,\n \"The same pump and two-phosphor construction with more red phosphor and less pump, a warm white LED for home lighting.\",\n 0, [[\"blue pump\", 0.795, 450, 9], [\"green phosphor\", 1, 540, 38], [\"red phosphor\", 2.133, 630, 40]]]\n];\n\nconst SET: RepresentativeLightSourceSet = /* @__PURE__ */ ((): RepresentativeLightSourceSet => Object.freeze({\n setId: REPRESENTATIVE_LIGHT_SOURCE_SET_ID,\n setVersion: REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION,\n wavelengthRangeNanometers: REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS,\n sources: Object.freeze(ROWS.map(([sourceId, name, family, cct, reason, base, bands]) => Object.freeze({\n sourceId, name, family, nominalCorrelatedColorTemperatureKelvin: cct, label: LABEL, reason, base,\n bands: Object.freeze(bands.map(([role, amplitude, centerNanometers, standardDeviationNanometers]) =>\n Object.freeze({ kind: \"gaussian\" as const, role, amplitude, centerNanometers, standardDeviationNanometers })))\n })))\n}))();\n\n/**\n * The built-in representative light sources: Photivra's own parametric spectra of a tri-phosphor fluorescent tube and\n * three white LEDs (low and high color rendering at 4000 K, warm at 2700 K). A static, deeply frozen engine contract.\n * Each is \"representative, not a named product\"; docs/LIGHT_SOURCES.md gives every formula and the reasons for it.\n */\nexport function getRepresentativeLightSourceSet(): RepresentativeLightSourceSet {\n return SET;\n}\n\nfunction findSource(input: unknown, functionName: string): RepresentativeLightSource {\n requireInputKeys(input, functionName, [\"sourceId\"]);\n const id = (input as { sourceId?: unknown }).sourceId;\n const source = typeof id === \"string\" ? SET.sources.find((s) => s.sourceId === id) : undefined;\n if (!source) throw new InvalidConfigurationError(\"sourceId must be one of the built-in representative light sources (REPRESENTATIVE_LIGHT_SOURCE_IDS).\");\n return source;\n}\n\n/** Most wavelengths whose values one spectrum keeps (a capture uses a few hundred). */\nconst CACHE_LIMIT = 4096;\n\n/**\n * A source's relative spectral power distribution S(λ) = b + Σ aₖ·exp(−½((λ − μₖ)/σₖ)²) as a function spectrum on\n * 360–830 nm (relative units; values memoized per wavelength). Pass it to a capture as\n * `illuminant: { kind: \"spectrum\", spectrum }`; for color work use `sensor.spectralStepNanometers: 5` or finer\n * (docs/LIGHT_SOURCES.md, \"Quadrature\"). Throws `InvalidScientificInputError` outside 360–830 nm.\n */\nexport function createRepresentativeLightSourceSpectrum(input: { sourceId: RepresentativeLightSourceId }): ColorimetricSpectrum {\n const source = findSource(input, \"createRepresentativeLightSourceSpectrum\");\n const range = REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS, cache = new Map<number, number>();\n return {\n kind: \"function\",\n wavelengthRangeNanometers: { minimum: range.minimum, maximum: range.maximum },\n evaluate(nm: number): number {\n const hit = cache.get(nm);\n if (hit !== undefined) return hit;\n if (typeof nm !== \"number\" || !(nm >= range.minimum && nm <= range.maximum)) {\n throw new InvalidScientificInputError(\"Representative light source \" + source.sourceId + \" is defined on 360–830 nm.\");\n }\n let value = source.base;\n for (const band of source.bands) {\n const z = (nm - band.centerNanometers) / band.standardDeviationNanometers;\n value += band.amplitude * portableExp(-0.5 * z * z);\n }\n if (cache.size < CACHE_LIMIT) cache.set(nm, value);\n return value;\n }\n };\n}\n\n/**\n * A capture illuminant for a source: `{ kind: \"spectrum\", spectrum, label }` with the spectrum of\n * {@link createRepresentativeLightSourceSpectrum} and the label \"<name> (representative, not a named product)\".\n */\nexport function createRepresentativeLightSourceIlluminant(input: { sourceId: RepresentativeLightSourceId }): Extract<CaptureIlluminant, { kind: \"spectrum\" }> {\n const source = findSource(input, \"createRepresentativeLightSourceIlluminant\");\n return { kind: \"spectrum\", spectrum: createRepresentativeLightSourceSpectrum({ sourceId: source.sourceId }), label: source.name + \" (\" + LABEL + \")\" };\n}\n"]}
|