@photivra/engine 2.9.0 → 2.10.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 +85 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +2 -0
- package/dist/capture/native-environment-lean.js +89 -5
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +37 -0
- package/dist/capture/native-environment-tabulated.js +42 -6
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/core/portable-math.d.ts +13 -0
- package/dist/core/portable-math.js +43 -0
- package/dist/core/portable-math.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/facade/channel-response.d.ts +15 -0
- package/dist/facade/channel-response.js +33 -0
- package/dist/facade/channel-response.js.map +1 -0
- package/dist/facade/diffraction-provider.js +50 -15
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +75 -0
- package/dist/facade/display-view.js +471 -109
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.js +2 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +32 -3
- package/dist/facade/expected-signal.js +192 -24
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +51 -4
- package/dist/facade/simulate-capture.js +130 -31
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +25 -2
- package/dist/facade/simulate-display-view.js +55 -22
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +5 -0
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +11 -0
- package/dist/optics/irradiance-convolution.js +64 -24
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +58 -24
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/sensor-aperture-rays.js +2 -1
- package/dist/optics/sensor-aperture-rays.js.map +1 -1
- package/dist/optics/sensor-environment-query.js +4 -3
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +16 -0
- package/dist/optics/site-diffraction-kernel.js +60 -8
- package/dist/optics/site-diffraction-kernel.js.map +1 -1
- package/dist/processing/demosaic.js +114 -37
- package/dist/processing/demosaic.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +8 -0
- package/dist/schema/scene-radiance.js +28 -17
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/sensor/environment-photo-signal.d.ts +21 -0
- package/dist/sensor/environment-photo-signal.js +68 -2
- package/dist/sensor/environment-photo-signal.js.map +1 -1
- package/docs/API_REFERENCE.md +16 -1
- package/docs/QUICK_START.md +18 -8
- package/docs/USAGE.md +30 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +42 -2
- package/docs/api/facade-display-view.md +50 -0
- package/docs/api/facade-simulate-capture.md +60 -8
- package/docs/api/facade-simulate-display-view.md +23 -1
- package/docs/api/gpu-image-kernel-execution.md +5 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
8
8
|
import { siteDiffractionKernel } from "../optics/site-diffraction-kernel.js";
|
|
9
9
|
import { resolveProjectionDistanceMm } from "../motion/camera-rotation.js";
|
|
10
|
+
import { hypot3 } from "../core/portable-math.js";
|
|
10
11
|
/** The query offsets of each site and the expected-signal input (see the module comment). */
|
|
11
12
|
export function buildExpectedSignalInput(input) {
|
|
12
13
|
const { boxSamples, pitch, wavelengths, imagingArea, subjectDistanceM, metresPerFrameUnit } = input;
|
|
@@ -42,7 +43,7 @@ export function buildExpectedSignalInput(input) {
|
|
|
42
43
|
const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;
|
|
43
44
|
const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;
|
|
44
45
|
const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);
|
|
45
|
-
const length =
|
|
46
|
+
const length = hypot3(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);
|
|
46
47
|
out[0] = rx / length * t / metresPerFrameUnit;
|
|
47
48
|
out[1] = ry / length * t / metresPerFrameUnit;
|
|
48
49
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expected-signal-setup.js","sourceRoot":"","sources":["../../src/facade/expected-signal-setup.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#394, not exported from the package): the input of the tabulated expected signal (`createDisplayViewExpectedSignal`)\n * for a resolved `simulateCapture` request, shared by the display view and the capture's tabulated RAW path so both build it\n * the same way: query offsets of each site, the environment's query points, the diffraction kernels and the scene tables.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport { siteDiffractionKernel } from \"../optics/site-diffraction-kernel.js\";\nimport { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport type { DisplayViewExpectedSignalInput, DisplayViewScene } from \"./display-view.js\";\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\nexport interface ExpectedSignalSetupInput {\n nativeWidth: number;\n nativeHeight: number;\n /** Frame output width (plane coordinates are 2 / width frame units per site). */\n width: number;\n wavelengths: readonly number[];\n /** Per CFA channel (red, green, blue): expected electrons per unit spectral radiance at each node. */\n perNode: readonly [Float64Array, Float64Array, Float64Array];\n /** Pixel-aperture nodes in sites from the site centre (`apertureNodeOffsets(boxSamples)`) and the box sample count. */\n apertureNodes: readonly { x: number; y: number }[];\n boxSamples: number;\n antiAliasing?: { components: readonly { offsetMicrometers: { x: number; y: number }; normalizedWeight: number }[] } | undefined;\n pitch: number;\n /** Object-space diffraction: the kernel radius, the PSF and its profile wavelengths. */\n diffraction?: { radius: number; psf: Psf; profileWavelengths: readonly number[] } | undefined;\n /** Material scenes: the materials (neutral reflectances, or radiance per wavelength), the material lookup and the white radiance. */\n materials?: { list: readonly ({ neutral: number } | { radiance: (nm: number) => number })[]; materialAt(u: number, v: number): number; whiteRadiance(nm: number): number } | undefined;\n radianceAt(u: number, v: number, nm: number): number;\n imagingArea: { widthMm: number; heightMm: number };\n focalLengthMm: number;\n focusDistanceM: number;\n subjectDistanceM: number;\n metresPerFrameUnit: number;\n /** Prefix of the error messages (\"simulateDisplayView\", \"simulateCapture\"). */\n caller: string;\n}\n\n/** The query offsets of each site and the expected-signal input (see the module comment). */\nexport function buildExpectedSignalInput(input: ExpectedSignalSetupInput): { offsets: { x: number; y: number; weight: number }[]; signalInput: DisplayViewExpectedSignalInput } {\n const { boxSamples, pitch, wavelengths, imagingArea, subjectDistanceM, metresPerFrameUnit } = input;\n // Query offsets of each site: pixel-aperture nodes minus anti-aliasing split offsets (sites, +x right, +y down), as the environment forms them.\n const spots = input.antiAliasing ? input.antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 }, normalizedWeight: 1 }];\n const offsets = input.apertureNodes.flatMap((node) => spots.map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch,\n weight: c.normalizedWeight / (boxSamples * boxSamples) })));\n // Diffraction: the same per-node site kernels as the object-space convolution (createDiffractedPlanarProvider).\n let kernels: { radius: number; perNode: Float64Array[] } | undefined;\n const diffraction = input.diffraction;\n if (diffraction) {\n const radius = diffraction.radius;\n kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {\n if (!diffraction.profileWavelengths.includes(wavelengthNm)) throw new InvalidConfigurationError(input.caller + \": a spectral node has no diffraction kernel.\");\n return siteDiffractionKernel(diffraction.psf, wavelengthNm, pitch, radius, input.caller + \": diffraction kernels must sample the site pitch exactly.\");\n }) };\n }\n // Chart presets and (2.7) caller material scenes take the material path: each material tabulated once per node.\n const materials = input.materials;\n const scene: DisplayViewScene = materials ? { kind: \"materials\", materialAt: materials.materialAt,\n materials: materials.list.map((m) => (\"neutral\" in m ? { neutral: m.neutral } : { radiance: Float64Array.from(wavelengths, (nm) => m.radiance(nm)) })),\n whiteRadiance: Float64Array.from(wavelengths, (nm) => materials.whiteRadiance(nm)) } :\n { kind: \"radiance\", radianceAt: input.radianceAt };\n const channelIndex = (x: number, y: number): number => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));\n // Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear\n // still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.\n const projectionDistance = resolveProjectionDistanceMm(input.focalLengthMm, input.focusDistanceM).projectionDistanceMm, siteWidthMm = pitch / 1000;\n const nodePhases = input.apertureNodes.flatMap((_, index) => spots.map((c) => ({ xPhase: (index % boxSamples + 0.5) / boxSamples, yPhase: (Math.floor(index / boxSamples) + 0.5) / boxSamples,\n aaX: c.offsetMicrometers.x / 1000, aaY: c.offsetMicrometers.y / 1000 })));\n const queryPoint = (x: number, y: number, o: number, out: Float64Array): void => {\n const node = nodePhases[o]!;\n const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;\n const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;\n const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);\n const length = Math.hypot(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);\n out[0] = rx / length * t / metresPerFrameUnit; out[1] = ry / length * t / metresPerFrameUnit;\n };\n return { offsets, signalInput: { nativeWidth: input.nativeWidth, nativeHeight: input.nativeHeight, outputWidth: input.width, wavelengthsNm: wavelengths,\n electronsPerRadiance: input.perNode, offsets, ...(kernels ? { kernels } : { queryPoint }), scene, channelAt: channelIndex } };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"expected-signal-setup.js","sourceRoot":"","sources":["../../src/facade/expected-signal-setup.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#394, not exported from the package): the input of the tabulated expected signal (`createDisplayViewExpectedSignal`)\n * for a resolved `simulateCapture` request, shared by the display view and the capture's tabulated RAW path so both build it\n * the same way: query offsets of each site, the environment's query points, the diffraction kernels and the scene tables.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport { siteDiffractionKernel } from \"../optics/site-diffraction-kernel.js\";\nimport { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\nimport type { DisplayViewExpectedSignalInput, DisplayViewScene } from \"./display-view.js\";\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\nexport interface ExpectedSignalSetupInput {\n nativeWidth: number;\n nativeHeight: number;\n /** Frame output width (plane coordinates are 2 / width frame units per site). */\n width: number;\n wavelengths: readonly number[];\n /** Per CFA channel (red, green, blue): expected electrons per unit spectral radiance at each node. */\n perNode: readonly [Float64Array, Float64Array, Float64Array];\n /** Pixel-aperture nodes in sites from the site centre (`apertureNodeOffsets(boxSamples)`) and the box sample count. */\n apertureNodes: readonly { x: number; y: number }[];\n boxSamples: number;\n antiAliasing?: { components: readonly { offsetMicrometers: { x: number; y: number }; normalizedWeight: number }[] } | undefined;\n pitch: number;\n /** Object-space diffraction: the kernel radius, the PSF and its profile wavelengths. */\n diffraction?: { radius: number; psf: Psf; profileWavelengths: readonly number[] } | undefined;\n /** Material scenes: the materials (neutral reflectances, or radiance per wavelength), the material lookup and the white radiance. */\n materials?: { list: readonly ({ neutral: number } | { radiance: (nm: number) => number })[]; materialAt(u: number, v: number): number; whiteRadiance(nm: number): number } | undefined;\n radianceAt(u: number, v: number, nm: number): number;\n imagingArea: { widthMm: number; heightMm: number };\n focalLengthMm: number;\n focusDistanceM: number;\n subjectDistanceM: number;\n metresPerFrameUnit: number;\n /** Prefix of the error messages (\"simulateDisplayView\", \"simulateCapture\"). */\n caller: string;\n}\n\n/** The query offsets of each site and the expected-signal input (see the module comment). */\nexport function buildExpectedSignalInput(input: ExpectedSignalSetupInput): { offsets: { x: number; y: number; weight: number }[]; signalInput: DisplayViewExpectedSignalInput } {\n const { boxSamples, pitch, wavelengths, imagingArea, subjectDistanceM, metresPerFrameUnit } = input;\n // Query offsets of each site: pixel-aperture nodes minus anti-aliasing split offsets (sites, +x right, +y down), as the environment forms them.\n const spots = input.antiAliasing ? input.antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 }, normalizedWeight: 1 }];\n const offsets = input.apertureNodes.flatMap((node) => spots.map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch,\n weight: c.normalizedWeight / (boxSamples * boxSamples) })));\n // Diffraction: the same per-node site kernels as the object-space convolution (createDiffractedPlanarProvider).\n let kernels: { radius: number; perNode: Float64Array[] } | undefined;\n const diffraction = input.diffraction;\n if (diffraction) {\n const radius = diffraction.radius;\n kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {\n if (!diffraction.profileWavelengths.includes(wavelengthNm)) throw new InvalidConfigurationError(input.caller + \": a spectral node has no diffraction kernel.\");\n return siteDiffractionKernel(diffraction.psf, wavelengthNm, pitch, radius, input.caller + \": diffraction kernels must sample the site pitch exactly.\");\n }) };\n }\n // Chart presets and (2.7) caller material scenes take the material path: each material tabulated once per node.\n const materials = input.materials;\n const scene: DisplayViewScene = materials ? { kind: \"materials\", materialAt: materials.materialAt,\n materials: materials.list.map((m) => (\"neutral\" in m ? { neutral: m.neutral } : { radiance: Float64Array.from(wavelengths, (nm) => m.radiance(nm)) })),\n whiteRadiance: Float64Array.from(wavelengths, (nm) => materials.whiteRadiance(nm)) } :\n { kind: \"radiance\", radianceAt: input.radianceAt };\n const channelIndex = (x: number, y: number): number => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));\n // Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear\n // still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.\n const projectionDistance = resolveProjectionDistanceMm(input.focalLengthMm, input.focusDistanceM).projectionDistanceMm, siteWidthMm = pitch / 1000;\n const nodePhases = input.apertureNodes.flatMap((_, index) => spots.map((c) => ({ xPhase: (index % boxSamples + 0.5) / boxSamples, yPhase: (Math.floor(index / boxSamples) + 0.5) / boxSamples,\n aaX: c.offsetMicrometers.x / 1000, aaY: c.offsetMicrometers.y / 1000 })));\n const queryPoint = (x: number, y: number, o: number, out: Float64Array): void => {\n const node = nodePhases[o]!;\n const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;\n const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;\n const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);\n const length = hypot3(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);\n out[0] = rx / length * t / metresPerFrameUnit; out[1] = ry / length * t / metresPerFrameUnit;\n };\n return { offsets, signalInput: { nativeWidth: input.nativeWidth, nativeHeight: input.nativeHeight, outputWidth: input.width, wavelengthsNm: wavelengths,\n electronsPerRadiance: input.perNode, offsets, ...(kernels ? { kernels } : { queryPoint }), scene, channelAt: channelIndex } };\n}\n"]}
|
|
@@ -58,18 +58,47 @@ export interface DisplayViewExpectedSignal {
|
|
|
58
58
|
/** Scene lookups performed so far (material or radiance queries), for diagnostics. */
|
|
59
59
|
readonly lookups: number;
|
|
60
60
|
/**
|
|
61
|
-
* The chart material that every kernel tap of the last `site` call read, when
|
|
62
|
-
* then a function of that material and the site's CFA channel only); −1 when
|
|
61
|
+
* The chart material that every kernel tap (without diffraction: every query point) of the last `site` call read, when
|
|
62
|
+
* they all read one material (its value is then a function of that material and the site's CFA channel only); −1 when
|
|
63
|
+
* unknown or mixed.
|
|
63
64
|
*/
|
|
64
65
|
readonly uniformMaterial: number;
|
|
65
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Whether `uniformMaterial` is tracked in the current window (otherwise it is not meaningful): chart materials in a
|
|
68
|
+
* filled window or, without diffraction, in a window the direct ring serves (#444); never for radiance scenes.
|
|
69
|
+
*/
|
|
66
70
|
readonly tracksMaterial: boolean;
|
|
67
71
|
/**
|
|
68
72
|
* Whether the current window caches its sites: a second `site` call for a site inside the window then returns the stored
|
|
69
73
|
* value without any work or lookup (#392).
|
|
70
74
|
*/
|
|
71
75
|
readonly cachesSites: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Whether `markReads` is available in the current window: a filled window (chart materials with diffraction kernels),
|
|
78
|
+
* whose lookup count comes from the sites it computed. False otherwise: for windows without kernels (whose sites the
|
|
79
|
+
* direct ring already serves cheaply) and for the lazy window caches (radiance scenes with diffraction), whose count
|
|
80
|
+
* depends on the lattice points each computed site reads.
|
|
81
|
+
*/
|
|
82
|
+
readonly marksReads: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Records `count` sites as read in the current window without computing them: site n at (xs[n], ys[n]) with value values[n]
|
|
85
|
+
* (as `site` returned it for that site) and uniform material materials[n] (as `uniformMaterial` reported it). Sites outside
|
|
86
|
+
* the window and sites the window already holds are skipped. For the window's lookup count and for later `site` calls this
|
|
87
|
+
* is the same as reading each site with `site`. Only valid while `marksReads` is true; it must do for a site the
|
|
88
|
+
* bookkeeping `site` does for a site the window's cache does not hold yet.
|
|
89
|
+
*/
|
|
90
|
+
markReads(xs: Int32Array, ys: Int32Array, values: Float64Array, materials: Int32Array, count: number): void;
|
|
72
91
|
}
|
|
92
|
+
/** Chart-material tables kept by content across input objects (#449), least recently used first. */
|
|
93
|
+
export declare const EXPECTED_SIGNAL_TABLE_CACHE_SIZE = 4;
|
|
94
|
+
/** Internal test hook: the kept table entries, and the tables built and found by content so far. */
|
|
95
|
+
export declare function expectedSignalTableCacheState(): {
|
|
96
|
+
size: number;
|
|
97
|
+
builds: number;
|
|
98
|
+
hits: number;
|
|
99
|
+
};
|
|
100
|
+
/** Internal test hook: forgets the tables kept by content (inputs already prepared keep theirs). */
|
|
101
|
+
export declare function clearExpectedSignalTableCache(): void;
|
|
73
102
|
/**
|
|
74
103
|
* Expected photo electrons per site: μ_c(i, j) = Σ_o w_o Σ_λ A_c(λ) Σ_t K_λ(t)·L(p_o(i, j) − t, λ), where p_o is the
|
|
75
104
|
* query point of offset o and K the diffraction kernel (a single unit tap without diffraction). This is the sum the
|
|
@@ -16,7 +16,85 @@ const BAND_LIMIT = 1 << 22;
|
|
|
16
16
|
*/
|
|
17
17
|
const LATTICE_RING_ROWS = 128, LATTICE_RING_MAXIMUM_BAND_ROWS = 64, LATTICE_RING_MARGIN = 64;
|
|
18
18
|
const PREPARED = new WeakMap();
|
|
19
|
+
/** Chart-material tables kept by content across input objects (#449), least recently used first. */
|
|
20
|
+
export const EXPECTED_SIGNAL_TABLE_CACHE_SIZE = 4;
|
|
21
|
+
const tableCache = [];
|
|
22
|
+
let tableBuilds = 0, tableHits = 0;
|
|
23
|
+
/** Internal test hook: the kept table entries, and the tables built and found by content so far. */
|
|
24
|
+
export function expectedSignalTableCacheState() { return { size: tableCache.length, builds: tableBuilds, hits: tableHits }; }
|
|
25
|
+
/** Internal test hook: forgets the tables kept by content (inputs already prepared keep theirs). */
|
|
26
|
+
export function clearExpectedSignalTableCache() { tableCache.length = 0; }
|
|
27
|
+
/**
|
|
28
|
+
* Every number the tables read, with the lengths that delimit them, as the bits of doubles (so -0, NaN and every digit are kept
|
|
29
|
+
* apart): the spectral nodes, the three channel responses, the offsets, the kernel radius and taps, each material's reflectance or
|
|
30
|
+
* radiance, and the white radiance. Undefined for radiance scenes, whose tables are not kept by content.
|
|
31
|
+
*/
|
|
32
|
+
function tableSignature(input) {
|
|
33
|
+
const scene = input.scene;
|
|
34
|
+
if (scene.kind !== "materials")
|
|
35
|
+
return undefined;
|
|
36
|
+
const values = [];
|
|
37
|
+
const push = (a) => { values.push(a.length); for (let i = 0; i < a.length; i++)
|
|
38
|
+
values.push(a[i]); };
|
|
39
|
+
push(input.wavelengthsNm);
|
|
40
|
+
values.push(input.electronsPerRadiance.length);
|
|
41
|
+
for (const a of input.electronsPerRadiance)
|
|
42
|
+
push(a);
|
|
43
|
+
values.push(input.offsets.length);
|
|
44
|
+
for (const o of input.offsets)
|
|
45
|
+
values.push(o.x, o.y, o.weight);
|
|
46
|
+
if (input.kernels) {
|
|
47
|
+
values.push(1, input.kernels.radius, input.kernels.perNode.length);
|
|
48
|
+
for (const k of input.kernels.perNode)
|
|
49
|
+
push(k);
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
values.push(0);
|
|
53
|
+
values.push(scene.materials.length);
|
|
54
|
+
for (const m of scene.materials)
|
|
55
|
+
if ("neutral" in m)
|
|
56
|
+
values.push(0, m.neutral);
|
|
57
|
+
else {
|
|
58
|
+
values.push(1);
|
|
59
|
+
push(m.radiance);
|
|
60
|
+
}
|
|
61
|
+
push(scene.whiteRadiance);
|
|
62
|
+
return new Uint32Array(Float64Array.from(values).buffer);
|
|
63
|
+
}
|
|
64
|
+
function sameBits(a, b) {
|
|
65
|
+
if (a.length !== b.length)
|
|
66
|
+
return false;
|
|
67
|
+
for (let i = 0; i < a.length; i++)
|
|
68
|
+
if (a[i] !== b[i])
|
|
69
|
+
return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
19
72
|
function prepareSignal(input) {
|
|
73
|
+
const signature = tableSignature(input);
|
|
74
|
+
let tables;
|
|
75
|
+
if (signature) {
|
|
76
|
+
const at = tableCache.findIndex((entry) => sameBits(entry.signature, signature));
|
|
77
|
+
if (at >= 0) {
|
|
78
|
+
const [entry] = tableCache.splice(at, 1);
|
|
79
|
+
tableCache.push(entry);
|
|
80
|
+
tables = entry.tables;
|
|
81
|
+
tableHits++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!tables) {
|
|
85
|
+
tables = buildTables(input);
|
|
86
|
+
tableBuilds++;
|
|
87
|
+
if (signature) {
|
|
88
|
+
tableCache.push({ signature, tables });
|
|
89
|
+
while (tableCache.length > EXPECTED_SIGNAL_TABLE_CACHE_SIZE)
|
|
90
|
+
tableCache.shift();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const prepared = { tables, rings: tables.groups.map(() => undefined) };
|
|
94
|
+
PREPARED.set(input, prepared);
|
|
95
|
+
return prepared;
|
|
96
|
+
}
|
|
97
|
+
function buildTables(input) {
|
|
20
98
|
const nodes = input.wavelengthsNm.length, R = input.kernels ? input.kernels.radius : 0, size = 2 * R + 1;
|
|
21
99
|
const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets;
|
|
22
100
|
const kernelAt = (node, t) => (input.kernels ? input.kernels.perNode[node][t] : 1);
|
|
@@ -26,11 +104,12 @@ function prepareSignal(input) {
|
|
|
26
104
|
if (scene.kind === "materials") {
|
|
27
105
|
reflectance = new Float64Array(scene.materials.length);
|
|
28
106
|
spectralIndex = new Int32Array(scene.materials.length).fill(-1);
|
|
107
|
+
// A copy of each spectral radiance: the tables may outlive the input and serve other inputs of the same content.
|
|
29
108
|
scene.materials.forEach((m, index) => { if ("neutral" in m)
|
|
30
109
|
reflectance[index] = m.neutral;
|
|
31
110
|
else {
|
|
32
111
|
spectralIndex[index] = spectral.length;
|
|
33
|
-
spectral.push(m.radiance);
|
|
112
|
+
spectral.push(Float64Array.from(m.radiance));
|
|
34
113
|
} });
|
|
35
114
|
}
|
|
36
115
|
// Offsets grouped by fractional shift: offsets that differ by whole sites share one lattice.
|
|
@@ -40,7 +119,7 @@ function prepareSignal(input) {
|
|
|
40
119
|
const fx = frac(o.x), fy = frac(o.y), ix = Math.round(o.x - fx), iy = Math.round(o.y - fy);
|
|
41
120
|
let group = groups.find((q) => Math.abs(q.fx - fx) <= OFFSET_TOLERANCE && Math.abs(q.fy - fy) <= OFFSET_TOLERANCE);
|
|
42
121
|
if (!group) {
|
|
43
|
-
group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: []
|
|
122
|
+
group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: [] };
|
|
44
123
|
groups.push(group);
|
|
45
124
|
}
|
|
46
125
|
group.members.push({ ix, iy, weight: o.weight });
|
|
@@ -93,9 +172,10 @@ function prepareSignal(input) {
|
|
|
93
172
|
}
|
|
94
173
|
return sum;
|
|
95
174
|
})) : [];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
175
|
+
// Frozen records: a render copies each group before it gives it a window (see createDisplayViewExpectedSignal).
|
|
176
|
+
for (const group of groups)
|
|
177
|
+
Object.freeze(group);
|
|
178
|
+
return Object.freeze({ reflectance, spectralIndex, spectral, groups, neutralDirect, spectralDirect, uniformSite });
|
|
99
179
|
}
|
|
100
180
|
/**
|
|
101
181
|
* Expected photo electrons per site: μ_c(i, j) = Σ_o w_o Σ_λ A_c(λ) Σ_t K_λ(t)·L(p_o(i, j) − t, λ), where p_o is the
|
|
@@ -120,9 +200,10 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
120
200
|
const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets, channelAt = input.channelAt;
|
|
121
201
|
// The request's tables (materials, lattice groups, combined kernels, per-material sums) are the same for every render of a
|
|
122
202
|
// prepared view, which passes the same input object: computed once per input. Each render gets its own groups' window caches.
|
|
203
|
+
// Chart-material tables are also shared by inputs of the same content (#449): each render gets its own groups' window state.
|
|
123
204
|
const prepared = PREPARED.get(input) ?? prepareSignal(input);
|
|
124
|
-
const { reflectance, spectralIndex, spectral, neutralDirect, spectralDirect, uniformSite } = prepared;
|
|
125
|
-
const groups = prepared.groups.map((group) => ({ ...group, cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 }));
|
|
205
|
+
const { reflectance, spectralIndex, spectral, neutralDirect, spectralDirect, uniformSite } = prepared.tables;
|
|
206
|
+
const groups = prepared.tables.groups.map((group) => ({ ...group, cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 }));
|
|
126
207
|
let lookups = 0;
|
|
127
208
|
const point = new Float64Array(2);
|
|
128
209
|
const materialSignalDirect = (m, c) => {
|
|
@@ -131,15 +212,21 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
131
212
|
throw new InvalidConfigurationError("Unknown chart material.");
|
|
132
213
|
return si < 0 ? reflectance[m] * neutralDirect[c] : spectralDirect[c][si];
|
|
133
214
|
};
|
|
134
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* Without diffraction: one site from the environment's own query points. Also sets lastMaterial: the chart material every
|
|
217
|
+
* query point read, or −1 when they read more than one (or the scene is a radiance scene). A site whose points all read
|
|
218
|
+
* material m sums the same operands in the same order as every other such site of its channel, so it holds the same value.
|
|
219
|
+
*/
|
|
135
220
|
function directSite(x, y) {
|
|
136
221
|
const c = channelAt(x, y);
|
|
137
|
-
let sum = 0;
|
|
222
|
+
let sum = 0, uniform = -1;
|
|
138
223
|
for (let o = 0; o < offsets.length; o++) {
|
|
139
224
|
input.queryPoint(x, y, o, point);
|
|
140
225
|
let value;
|
|
141
226
|
if (scene.kind === "materials") {
|
|
142
|
-
|
|
227
|
+
const m = scene.materialAt(point[0], point[1]);
|
|
228
|
+
uniform = o === 0 || uniform === m ? m : -2;
|
|
229
|
+
value = materialSignalDirect(m, c);
|
|
143
230
|
lookups++;
|
|
144
231
|
}
|
|
145
232
|
else {
|
|
@@ -150,6 +237,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
150
237
|
}
|
|
151
238
|
sum += offsets[o].weight * value;
|
|
152
239
|
}
|
|
240
|
+
lastMaterial = uniform < 0 ? -1 : uniform;
|
|
153
241
|
return sum;
|
|
154
242
|
}
|
|
155
243
|
// Lattice point (p, q) of a group: the facade's lattice formula (createDiffractedPlanarProvider).
|
|
@@ -226,7 +314,8 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
226
314
|
changeLeft: new Int32Array(0), changeUp: new Int32Array(0), v: new Float64Array(0) }));
|
|
227
315
|
// Lattice rows of material indices per group, kept across windows (filled windows only): rows LATTICE_RING_ROWS apart share a
|
|
228
316
|
// slot; columns from −LATTICE_RING_MARGIN. Int16 when every index fits (about 3 MB for four groups of a 6000-site row). A
|
|
229
|
-
// lattice point's material depends only on the input, so the rings are kept
|
|
317
|
+
// lattice point's material depends only on the input, so the rings are kept per input object, across its renders (not with
|
|
318
|
+
// the tables kept by content: they depend on `materialAt` and the raster).
|
|
230
319
|
const ringWide = scene.kind === "materials" && scene.materials.length <= 32767;
|
|
231
320
|
const rings = prepared.rings;
|
|
232
321
|
function latticeRing(g) {
|
|
@@ -412,8 +501,9 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
412
501
|
return v;
|
|
413
502
|
}
|
|
414
503
|
// Without diffraction (#392): site values persist across windows the same way, in their own ring (a site's value does not
|
|
415
|
-
// depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged.
|
|
416
|
-
|
|
504
|
+
// depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged. The site's
|
|
505
|
+
// material (directSite's lastMaterial) is kept beside its value, so the display view's one-material memo applies (#444).
|
|
506
|
+
let directRows = 0, directOn = false, directRing = new Float64Array(0), directRowId = new Int32Array(0), directRingMaterial = new Int32Array(0);
|
|
417
507
|
const directLookups = offsets.length * (scene.kind === "materials" ? 1 : nodes);
|
|
418
508
|
function directRead(x, y) {
|
|
419
509
|
const slot = y & (directRows - 1), q = slot * W + x;
|
|
@@ -421,6 +511,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
421
511
|
const v = directRing[q];
|
|
422
512
|
if (v === v) {
|
|
423
513
|
lookups += directLookups;
|
|
514
|
+
lastMaterial = directRingMaterial[q];
|
|
424
515
|
return v;
|
|
425
516
|
}
|
|
426
517
|
}
|
|
@@ -430,14 +521,17 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
430
521
|
}
|
|
431
522
|
const v = directSite(x, y);
|
|
432
523
|
directRing[q] = v;
|
|
524
|
+
directRingMaterial[q] = lastMaterial;
|
|
433
525
|
return v;
|
|
434
526
|
}
|
|
435
527
|
/** A site not in the window's site cache: from a ring, a filled window without the ring, the lazy window caches, or uncached. */
|
|
436
528
|
function siteMiss(x, y, i, j, cached) {
|
|
437
529
|
if (directOn) {
|
|
438
530
|
const v = directRead(x, y);
|
|
439
|
-
if (cached)
|
|
531
|
+
if (cached) {
|
|
440
532
|
siteCache[j * sw + i] = v;
|
|
533
|
+
windowMaterial[j * sw + i] = lastMaterial;
|
|
534
|
+
}
|
|
441
535
|
return v;
|
|
442
536
|
}
|
|
443
537
|
if (ringOn) {
|
|
@@ -464,22 +558,66 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
464
558
|
/**
|
|
465
559
|
* Lattice points of the closing window that kernelSite's lazy cache would have looked up: per group, the window cache points
|
|
466
560
|
* covered by the rectangles of the computed sites (the site cache's filled entries and the outside sites), a box dilation.
|
|
561
|
+
* A group's count depends only on the computed sites and its geometry (cw, ch, cpw, cph), so a group shaped like an
|
|
562
|
+
* earlier one takes that group's count (the app's Quality groups all have one shape).
|
|
467
563
|
*/
|
|
564
|
+
const groupTotals = new Float64Array(groups.length);
|
|
565
|
+
function sameGeometry(g) {
|
|
566
|
+
const group = groups[g];
|
|
567
|
+
for (let h = 0; h < g; h++) {
|
|
568
|
+
const o = groups[h];
|
|
569
|
+
if (o.cw === group.cw && o.ch === group.ch && o.cpw === group.cpw && o.cph === group.cph)
|
|
570
|
+
return h;
|
|
571
|
+
}
|
|
572
|
+
return -1;
|
|
573
|
+
}
|
|
468
574
|
function windowLookups() {
|
|
469
575
|
let total = 0;
|
|
470
576
|
if (outsideSites.length === 0 && filledCount === 0)
|
|
471
577
|
return 0;
|
|
578
|
+
if (outsideSites.length === 0 && filledCount === sw * sh) {
|
|
579
|
+
// Every site computed (each once) and none outside: the marks at (i + cw − 1, j + ch − 1) cover cache columns
|
|
580
|
+
// 0 … sw + cw − 2 and rows 0 … sh + ch − 2, the whole cpw × cph cache of every group.
|
|
581
|
+
for (const group of groups)
|
|
582
|
+
total += group.cpw * group.cph;
|
|
583
|
+
return total;
|
|
584
|
+
}
|
|
472
585
|
if (outsideSites.length === 0 && sw <= 31) {
|
|
473
586
|
// Computed sites as a bit mask per window row. Cache point (ci, cj) is covered when a computed site (i, j) has
|
|
474
587
|
// i ≤ ci < i + cw and j ≤ cj < j + ch; a union of dilations is the dilation of the union, so per cache row the
|
|
475
|
-
// covered columns are the dilation of the OR of the masks of window rows cj − ch + 1 … cj.
|
|
476
|
-
|
|
477
|
-
|
|
588
|
+
// covered columns are the dilation of the OR of the masks of window rows cj − ch + 1 … cj. That OR is a prefix OR
|
|
589
|
+
// when the rows start at fy0 and a suffix OR when they end at fy1 (OR is associative and commutative).
|
|
590
|
+
if (prefixOr.length < sh) {
|
|
591
|
+
prefixOr = new Int32Array(sh);
|
|
592
|
+
suffixOr = new Int32Array(sh);
|
|
593
|
+
}
|
|
594
|
+
for (let j = fy0, m = 0; j <= fy1; j++) {
|
|
595
|
+
m |= rowMask[j];
|
|
596
|
+
prefixOr[j] = m;
|
|
597
|
+
}
|
|
598
|
+
for (let j = fy1, m = 0; j >= fy0; j--) {
|
|
599
|
+
m |= rowMask[j];
|
|
600
|
+
suffixOr[j] = m;
|
|
601
|
+
}
|
|
602
|
+
for (let g = 0; g < groups.length; g++) {
|
|
603
|
+
const same = sameGeometry(g);
|
|
604
|
+
if (same >= 0) {
|
|
605
|
+
total += groupTotals[same];
|
|
606
|
+
groupTotals[g] = groupTotals[same];
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const group = groups[g], cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, last = Math.min(cph - 1, fy1 + ch - 1), before = total;
|
|
478
610
|
let previous = -1, width = 0;
|
|
479
611
|
for (let cj = fy0; cj <= last; cj++) {
|
|
480
612
|
let m = 0;
|
|
481
|
-
|
|
482
|
-
|
|
613
|
+
const a = Math.max(fy0, cj - ch + 1), b = Math.min(fy1, cj);
|
|
614
|
+
if (a === fy0)
|
|
615
|
+
m = prefixOr[b];
|
|
616
|
+
else if (b === fy1)
|
|
617
|
+
m = suffixOr[a];
|
|
618
|
+
else
|
|
619
|
+
for (let j = a; j <= b; j++)
|
|
620
|
+
m |= rowMask[j];
|
|
483
621
|
if (m !== previous) {
|
|
484
622
|
previous = m;
|
|
485
623
|
width = 0;
|
|
@@ -498,11 +636,20 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
498
636
|
}
|
|
499
637
|
total += width;
|
|
500
638
|
}
|
|
639
|
+
groupTotals[g] = total - before;
|
|
501
640
|
}
|
|
502
641
|
return total;
|
|
503
642
|
}
|
|
504
|
-
for (
|
|
505
|
-
|
|
643
|
+
for (let g = 0; g < groups.length; g++) {
|
|
644
|
+
// The marks are the computed sites shifted by (cw − 1, ch − 1), so the count depends on the group's geometry only.
|
|
645
|
+
const same = sameGeometry(g);
|
|
646
|
+
if (same >= 0) {
|
|
647
|
+
total += groupTotals[same];
|
|
648
|
+
groupTotals[g] = groupTotals[same];
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const group = groups[g], cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, ew = cpw + cw - 1, eh = cph + ch - 1, before = total;
|
|
652
|
+
groupTotals[g] = 0;
|
|
506
653
|
// Top-left marks in extended coordinates (window coordinates + (cw − 1, ch − 1)), and their bounding box.
|
|
507
654
|
if (marks.length < ew * eh) {
|
|
508
655
|
marks = new Uint8Array(ew * eh);
|
|
@@ -557,6 +704,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
557
704
|
total++;
|
|
558
705
|
}
|
|
559
706
|
}
|
|
707
|
+
groupTotals[g] = total - before;
|
|
560
708
|
}
|
|
561
709
|
return total;
|
|
562
710
|
}
|
|
@@ -564,7 +712,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
564
712
|
// The computed entries of a filled window's site cache (windowLookups): their count, bounding box and a bit mask per row
|
|
565
713
|
// (columns below 31).
|
|
566
714
|
let filledCount = 0, fx0 = 0, fx1 = -1, fy0 = 0, fy1 = -1;
|
|
567
|
-
let rowMask = new Int32Array(0);
|
|
715
|
+
let rowMask = new Int32Array(0), prefixOr = new Int32Array(0), suffixOr = new Int32Array(0);
|
|
568
716
|
function markFilled(i, j) {
|
|
569
717
|
if (i < 31)
|
|
570
718
|
rowMask[j] = rowMask[j] | (1 << i);
|
|
@@ -586,8 +734,25 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
586
734
|
return {
|
|
587
735
|
get lookups() { return lookups + (filled ? windowLookups() : 0); },
|
|
588
736
|
get uniformMaterial() { return lastMaterial; },
|
|
589
|
-
|
|
737
|
+
// Chart materials are tracked on the filled path and, without diffraction, on the direct ring (radiance scenes never).
|
|
738
|
+
get tracksMaterial() { return ringOn || (directOn && scene.kind === "materials"); },
|
|
590
739
|
get cachesSites() { return sw > 0; },
|
|
740
|
+
get marksReads() { return filled; },
|
|
741
|
+
markReads(xs, ys, values, materials, count) {
|
|
742
|
+
for (let n = 0; n < count; n++) {
|
|
743
|
+
const i = xs[n] - sx0, j = ys[n] - sy0;
|
|
744
|
+
if (!(i >= 0 && j >= 0 && i < sw && j < sh))
|
|
745
|
+
continue;
|
|
746
|
+
const w = j * sw + i;
|
|
747
|
+
if (siteCache[w] === siteCache[w])
|
|
748
|
+
continue;
|
|
749
|
+
// As siteMiss for a site inside a filled window: the computed site is marked (windowLookups counts from the marks)
|
|
750
|
+
// and its material kept.
|
|
751
|
+
siteCache[w] = values[n];
|
|
752
|
+
windowMaterial[w] = materials[n];
|
|
753
|
+
markFilled(i, j);
|
|
754
|
+
}
|
|
755
|
+
},
|
|
591
756
|
window(x0, y0, x1, y1) {
|
|
592
757
|
if (filled)
|
|
593
758
|
lookups += windowLookups();
|
|
@@ -619,9 +784,12 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
619
784
|
directRows = rows;
|
|
620
785
|
directRing = new Float64Array(rows * W).fill(NaN);
|
|
621
786
|
directRowId = new Int32Array(rows).fill(-1);
|
|
787
|
+
directRingMaterial = new Int32Array(rows * W);
|
|
622
788
|
}
|
|
623
789
|
}
|
|
624
790
|
directOn = sh > 0 && directRows >= 2 * sh;
|
|
791
|
+
if (directOn && windowMaterial.length < sw * sh)
|
|
792
|
+
windowMaterial = new Int32Array(sw * sh);
|
|
625
793
|
return;
|
|
626
794
|
}
|
|
627
795
|
for (const group of groups) {
|
|
@@ -675,7 +843,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
|
675
843
|
if (cached) {
|
|
676
844
|
const w = j * sw + i, v = siteCache[w];
|
|
677
845
|
if (v === v) {
|
|
678
|
-
if (ringOn)
|
|
846
|
+
if (ringOn || directOn)
|
|
679
847
|
lastMaterial = windowMaterial[w];
|
|
680
848
|
return v;
|
|
681
849
|
}
|