supervision 0.2.7 → 0.2.9

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, decodeCompressedRleCounts, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, createDetectionPickKey, rebaseDetectionPickToFrame, pickDetectionAtPoint, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, MAX_ID_MASK_STROKE_WIDTH, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, resolveIdMaskStrokeTexels, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
1
+ import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, decodeCompressedRleCounts, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, rebaseDetectionPickToFrame, pickDetectionAtPoint, createDetectionPickKey, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, MAX_ID_MASK_STROKE_WIDTH, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, resolveIdMaskStrokeTexels, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
2
2
  export { BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BaseOrientedBoxStyle, BasePercentageBarStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DepthColormap, DepthMapKind, DepthPreviewLevels, DepthQuantity, DepthSampling, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, PercentageBarPlacement, PlaybackGateReach, RegionRendererComposeMode, RegionRendererCoverageKind, RegionRendererMediaEffectKind, RegionRendererRegionKind, RegionRendererSizeSpace, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, TrackingGeometry, annotationRendererKinds, annotationRenderers, computeDepthPercentileRange, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createWritableDetectionFrameSource, depthColormapColors, detectionPostProcessors, normalizeDetectionClassName, parseDepthManifest, pickDetectionAtPoint, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, readDepthAt, resolveDetectionClassColorStyle } from 'supervision-js-core';
3
3
  import { c as createMediaFrameClock, M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-DOFsuGS4.js';
4
4
  export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-DOFsuGS4.js';
@@ -10132,6 +10132,9 @@ async function createMediaRendererCore(options, providers) {
10132
10132
  polygonStyle: currentPresentation.polygonStyle,
10133
10133
  polylineStyle: currentPresentation.polylineStyle,
10134
10134
  presentedFrames: protectedPresentedFrames?.source,
10135
+ // A source with no duration, such as a still image, has one frame to
10136
+ // show and nothing to play.
10137
+ stillFrame: metadata.duration === 0,
10135
10138
  regionRenderers: resolveRegionRenderers(currentPresentation),
10136
10139
  heatmapRenderers: resolveHeatmapRenderers(currentPresentation),
10137
10140
  depthRenderers: resolveDepthRenderers(currentPresentation),
@@ -10647,6 +10650,260 @@ function matches(left, right) {
10647
10650
  left.every((value, index) => Object.is(value, right[index])));
10648
10651
  }
10649
10652
 
10653
+ const BINS = 256;
10654
+ const TILE_COUNT = 8;
10655
+ const CLIP_LIMIT = 2;
10656
+ function computeMediaClahe(rgba, width, height) {
10657
+ const tilesX = Math.min(TILE_COUNT, width);
10658
+ const tilesY = Math.min(TILE_COUNT, height);
10659
+ const luts = new Uint8Array(tilesX * tilesY * BINS);
10660
+ const histogram = new Uint32Array(BINS);
10661
+ for (let ty = 0; ty < tilesY; ty++) {
10662
+ const y0 = Math.floor((ty * height) / tilesY);
10663
+ const y1 = Math.floor(((ty + 1) * height) / tilesY);
10664
+ for (let tx = 0; tx < tilesX; tx++) {
10665
+ const x0 = Math.floor((tx * width) / tilesX);
10666
+ const x1 = Math.floor(((tx + 1) * width) / tilesX);
10667
+ histogram.fill(0);
10668
+ for (let y = y0; y < y1; y++) {
10669
+ for (let x = x0; x < x1; x++) {
10670
+ const pixel = (y * width + x) * 4;
10671
+ const luma = Math.round(0.299 * rgba[pixel] +
10672
+ 0.587 * rgba[pixel + 1] +
10673
+ 0.114 * rgba[pixel + 2]);
10674
+ histogram[luma]++;
10675
+ }
10676
+ }
10677
+ const area = (x1 - x0) * (y1 - y0);
10678
+ const limit = Math.max(1, Math.floor((CLIP_LIMIT * area) / BINS));
10679
+ let excess = 0;
10680
+ for (let i = 0; i < BINS; i++) {
10681
+ if (histogram[i] > limit) {
10682
+ excess += histogram[i] - limit;
10683
+ histogram[i] = limit;
10684
+ }
10685
+ }
10686
+ const perBin = Math.floor(excess / BINS);
10687
+ let residual = excess - perBin * BINS;
10688
+ for (let i = 0; i < BINS; i++)
10689
+ histogram[i] += perBin;
10690
+ const residualStep = Math.max(Math.floor(BINS / Math.max(residual, 1)), 1);
10691
+ for (let i = 0; i < BINS && residual > 0; i += residualStep, residual--) {
10692
+ histogram[i]++;
10693
+ }
10694
+ const offset = (ty * tilesX + tx) * BINS;
10695
+ let cumulative = 0;
10696
+ for (let i = 0; i < BINS; i++) {
10697
+ cumulative += histogram[i];
10698
+ luts[offset + i] = Math.round((cumulative * (BINS - 1)) / area);
10699
+ }
10700
+ }
10701
+ }
10702
+ return { luts, tilesX, tilesY };
10703
+ }
10704
+
10705
+ /**
10706
+ * Pixi keys its program cache by source, so one program is shared by every
10707
+ * shader built from it. `destroy(true)` takes that program with it, leaving
10708
+ * both the peers still rendering through it and the next shader built from the
10709
+ * same source bound to nothing.
10710
+ */
10711
+ function destroyShaderKeepingProgram(shader) {
10712
+ try {
10713
+ shader.destroy();
10714
+ }
10715
+ catch {
10716
+ // Pixi has already invalidated this shader's resource group.
10717
+ }
10718
+ }
10719
+ /**
10720
+ * Pixi uploads a placeholder while it builds a shader's first bind group, and
10721
+ * WebGPU rejects a canvas that was never given a rendering context.
10722
+ */
10723
+ function createShaderPlaceholderCanvas() {
10724
+ if (typeof document === "undefined") {
10725
+ return { height: 1, width: 1 };
10726
+ }
10727
+ const canvas = document.createElement("canvas");
10728
+ canvas.height = 1;
10729
+ canvas.width = 1;
10730
+ canvas.getContext("2d");
10731
+ return canvas;
10732
+ }
10733
+
10734
+ const HISTOGRAM_MAX_DIMENSION = 1024;
10735
+ const vertex = `
10736
+ in vec2 aPosition;
10737
+ out vec2 vTextureCoord;
10738
+ out vec2 vImageCoord;
10739
+ uniform vec4 uInputSize;
10740
+ uniform vec4 uOutputFrame;
10741
+ uniform vec4 uOutputTexture;
10742
+ uniform mat3 uImageMatrix;
10743
+ void main() {
10744
+ vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
10745
+ position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
10746
+ position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
10747
+ gl_Position = vec4(position, 0.0, 1.0);
10748
+ vTextureCoord = aPosition * (uOutputFrame.zw * uInputSize.zw);
10749
+ vImageCoord = (uImageMatrix * vec3(vTextureCoord, 1.0)).xy;
10750
+ }
10751
+ `;
10752
+ const fragment = `
10753
+ in vec2 vTextureCoord;
10754
+ in vec2 vImageCoord;
10755
+ out vec4 finalColor;
10756
+ uniform sampler2D uTexture;
10757
+ uniform sampler2D uLut;
10758
+ uniform vec2 uTiles;
10759
+ uniform float uGamma;
10760
+ uniform float uAuto;
10761
+ float lookup(vec2 tile, float luma) {
10762
+ float row = tile.y * uTiles.x + tile.x;
10763
+ return texture(uLut, vec2((luma * 255.0 + 0.5) / 256.0,
10764
+ (row + 0.5) / (uTiles.x * uTiles.y))).r;
10765
+ }
10766
+ void main() {
10767
+ vec4 color = texture(uTexture, vTextureCoord);
10768
+ if (color.a == 0.0) { finalColor = color; return; }
10769
+ vec3 rgb = color.rgb / color.a;
10770
+ if (uAuto > 0.5) {
10771
+ float luma = dot(rgb, vec3(0.299, 0.587, 0.114));
10772
+ vec2 tilePosition = clamp(vImageCoord, 0.0, 1.0) * uTiles - 0.5;
10773
+ vec2 t0 = floor(tilePosition);
10774
+ vec2 f = tilePosition - t0;
10775
+ vec2 t1 = clamp(t0 + 1.0, vec2(0.0), uTiles - 1.0);
10776
+ t0 = clamp(t0, vec2(0.0), uTiles - 1.0);
10777
+ float top = mix(lookup(t0, luma), lookup(vec2(t1.x, t0.y), luma), f.x);
10778
+ float bottom = mix(lookup(vec2(t0.x, t1.y), luma), lookup(t1, luma), f.x);
10779
+ rgb = clamp(rgb + (mix(top, bottom, f.y) - luma), 0.0, 1.0);
10780
+ }
10781
+ finalColor = vec4(pow(rgb, vec3(1.0 / uGamma)) * color.a, color.a);
10782
+ }
10783
+ `;
10784
+ const gpuSource = `
10785
+ struct GlobalFilterUniforms {
10786
+ uInputSize: vec4<f32>, uInputPixel: vec4<f32>, uInputClamp: vec4<f32>,
10787
+ uOutputFrame: vec4<f32>, uGlobalFrame: vec4<f32>, uOutputTexture: vec4<f32>,
10788
+ };
10789
+ struct InspectionUniforms {
10790
+ uImageMatrix: mat3x3<f32>, uTiles: vec2<f32>, uGamma: f32, uAuto: f32,
10791
+ };
10792
+ @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
10793
+ @group(0) @binding(1) var uTexture: texture_2d<f32>;
10794
+ @group(0) @binding(2) var uSampler: sampler;
10795
+ @group(1) @binding(0) var<uniform> inspectionUniforms: InspectionUniforms;
10796
+ @group(1) @binding(1) var uLut: texture_2d<f32>;
10797
+ @group(1) @binding(2) var uLutSampler: sampler;
10798
+ struct VertexOutput {
10799
+ @builtin(position) position: vec4<f32>,
10800
+ @location(0) uv: vec2<f32>, @location(1) image: vec2<f32>,
10801
+ };
10802
+ @vertex fn mainVertex(@location(0) aPosition: vec2<f32>) -> VertexOutput {
10803
+ var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
10804
+ position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
10805
+ position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
10806
+ let uv = aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
10807
+ return VertexOutput(vec4(position, 0.0, 1.0), uv,
10808
+ (inspectionUniforms.uImageMatrix * vec3(uv, 1.0)).xy);
10809
+ }
10810
+ fn lookup(tile: vec2<f32>, luma: f32) -> f32 {
10811
+ let tiles = inspectionUniforms.uTiles;
10812
+ let row = tile.y * tiles.x + tile.x;
10813
+ return textureSampleLevel(uLut, uLutSampler,
10814
+ vec2((luma * 255.0 + 0.5) / 256.0, (row + 0.5) / (tiles.x * tiles.y)), 0.0).r;
10815
+ }
10816
+ @fragment fn mainFragment(input: VertexOutput) -> @location(0) vec4<f32> {
10817
+ let color = textureSample(uTexture, uSampler, input.uv);
10818
+ if (color.a == 0.0) { return color; }
10819
+ var rgb = color.rgb / color.a;
10820
+ if (inspectionUniforms.uAuto > 0.5) {
10821
+ let luma = dot(rgb, vec3(0.299, 0.587, 0.114));
10822
+ let tiles = inspectionUniforms.uTiles;
10823
+ let tilePosition = clamp(input.image, vec2(0.0), vec2(1.0)) * tiles - 0.5;
10824
+ let unbounded = floor(tilePosition);
10825
+ let f = tilePosition - unbounded;
10826
+ let t0 = clamp(unbounded, vec2(0.0), tiles - 1.0);
10827
+ let t1 = clamp(unbounded + 1.0, vec2(0.0), tiles - 1.0);
10828
+ let top = mix(lookup(t0, luma), lookup(vec2(t1.x, t0.y), luma), f.x);
10829
+ let bottom = mix(lookup(vec2(t0.x, t1.y), luma), lookup(t1, luma), f.x);
10830
+ rgb = clamp(rgb + (mix(top, bottom, f.y) - luma), vec3(0.0), vec3(1.0));
10831
+ }
10832
+ return vec4(pow(rgb, vec3(1.0 / inspectionUniforms.uGamma)) * color.a, color.a);
10833
+ }
10834
+ `;
10835
+ function createPixiMediaInspectionFilter(sprite, pixi) {
10836
+ const { BufferImageSource, Filter, Matrix } = pixi;
10837
+ const pixels = new Uint8Array(256 * 64 * 4);
10838
+ const lutSource = new BufferImageSource({
10839
+ resource: pixels,
10840
+ width: 256,
10841
+ height: 64,
10842
+ scaleMode: "linear",
10843
+ alphaMode: "no-premultiply-alpha",
10844
+ });
10845
+ const filter = Filter.from({
10846
+ gl: { vertex: vertex, fragment: fragment },
10847
+ gpu: {
10848
+ vertex: { source: gpuSource, entryPoint: "mainVertex" },
10849
+ fragment: { source: gpuSource, entryPoint: "mainFragment" },
10850
+ },
10851
+ resolution: "inherit",
10852
+ resources: {
10853
+ inspectionUniforms: {
10854
+ uImageMatrix: { value: new Matrix(), type: "mat3x3<f32>" },
10855
+ uTiles: { value: new Float32Array([8, 8]), type: "vec2<f32>" },
10856
+ uGamma: { value: 1, type: "f32" },
10857
+ uAuto: { value: 0, type: "f32" },
10858
+ },
10859
+ uLut: lutSource,
10860
+ uLutSampler: lutSource.style,
10861
+ },
10862
+ });
10863
+ const uniforms = filter.resources.inspectionUniforms.uniforms;
10864
+ let hasHistogram = false;
10865
+ filter.apply = (manager, input, output, clear) => {
10866
+ // The filter input is clipped by the viewport; lookup tiles stay anchored
10867
+ // to the full image when the user zooms or pans.
10868
+ manager.calculateSpriteMatrix(uniforms.uImageMatrix, sprite);
10869
+ manager.applyFilter(filter, input, output, clear);
10870
+ };
10871
+ return {
10872
+ filter,
10873
+ invalidateImage() {
10874
+ hasHistogram = false;
10875
+ },
10876
+ update(gamma, autoContrast, image) {
10877
+ uniforms.uGamma = Math.max(gamma, 0.0001);
10878
+ if (autoContrast && image && !hasHistogram) {
10879
+ const scale = Math.min(1, HISTOGRAM_MAX_DIMENSION / Math.max(image.width, image.height));
10880
+ const sample = document.createElement("canvas");
10881
+ sample.width = Math.max(1, Math.round(image.width * scale));
10882
+ sample.height = Math.max(1, Math.round(image.height * scale));
10883
+ const context = sample.getContext("2d", { willReadFrequently: true });
10884
+ context.drawImage(image, 0, 0, sample.width, sample.height);
10885
+ const { luts, tilesX, tilesY } = computeMediaClahe(context.getImageData(0, 0, sample.width, sample.height).data, sample.width, sample.height);
10886
+ for (let i = 0; i < luts.length; i++) {
10887
+ pixels[i * 4] = luts[i];
10888
+ pixels[i * 4 + 1] = luts[i];
10889
+ pixels[i * 4 + 2] = luts[i];
10890
+ pixels[i * 4 + 3] = 255;
10891
+ }
10892
+ uniforms.uTiles.set([tilesX, tilesY]);
10893
+ lutSource.resource = pixels.subarray(0, luts.length * 4);
10894
+ lutSource.resize(256, tilesX * tilesY);
10895
+ lutSource.update();
10896
+ hasHistogram = true;
10897
+ }
10898
+ uniforms.uAuto = autoContrast && hasHistogram ? 1 : 0;
10899
+ },
10900
+ destroy() {
10901
+ destroyShaderKeepingProgram(filter);
10902
+ lutSource.destroy();
10903
+ },
10904
+ };
10905
+ }
10906
+
10650
10907
  /**
10651
10908
  * Which frame a media time draws and how much of it is cooked, as two separate
10652
10909
  * facts. Reporting readiness is what the snapshot and the token are for; a
@@ -10786,35 +11043,6 @@ void main(void) {
10786
11043
  }
10787
11044
  `;
10788
11045
 
10789
- /**
10790
- * Pixi keys its program cache by source, so one program is shared by every
10791
- * shader built from it. `destroy(true)` takes that program with it, leaving
10792
- * both the peers still rendering through it and the next shader built from the
10793
- * same source bound to nothing.
10794
- */
10795
- function destroyShaderKeepingProgram(shader) {
10796
- try {
10797
- shader.destroy();
10798
- }
10799
- catch {
10800
- // Pixi has already invalidated this shader's resource group.
10801
- }
10802
- }
10803
- /**
10804
- * Pixi uploads a placeholder while it builds a shader's first bind group, and
10805
- * WebGPU rejects a canvas that was never given a rendering context.
10806
- */
10807
- function createShaderPlaceholderCanvas() {
10808
- if (typeof document === "undefined") {
10809
- return { height: 1, width: 1 };
10810
- }
10811
- const canvas = document.createElement("canvas");
10812
- canvas.height = 1;
10813
- canvas.width = 1;
10814
- canvas.getContext("2d");
10815
- return canvas;
10816
- }
10817
-
10818
11046
  /**
10819
11047
  * The halo a detection actually paints, or `undefined`. A style may answer an
10820
11048
  * instruction that draws nothing, and both the coverage the halo prepares and
@@ -13562,7 +13790,6 @@ function createPixiInteractionLayer(options) {
13562
13790
  let hoveredPick = null;
13563
13791
  let hoveredPickKey = null;
13564
13792
  let selectedPick = null;
13565
- let selectedPickKey = null;
13566
13793
  let selectedPicks = [];
13567
13794
  let marqueeStart = null;
13568
13795
  let marqueeRect = null;
@@ -13615,9 +13842,19 @@ function createPixiInteractionLayer(options) {
13615
13842
  setSelectedPick(null);
13616
13843
  return null;
13617
13844
  }
13618
- const nextPick = createPickFromSelection(selection);
13619
- setSelectedPick(nextPick);
13620
- return nextPick;
13845
+ const nextPicks = selection.detectionIds
13846
+ ? [...new Set(selection.detectionIds)].flatMap((detectionId) => {
13847
+ const pick = createPickFromSelection({
13848
+ detectionId,
13849
+ mediaTime: selection.mediaTime,
13850
+ point: selection.point,
13851
+ target: selection.target,
13852
+ });
13853
+ return pick ? [pick] : [];
13854
+ })
13855
+ : [createPickFromSelection(selection)].filter((pick) => pick !== null);
13856
+ setSelectedPicks(nextPicks);
13857
+ return nextPicks.at(-1) ?? null;
13621
13858
  },
13622
13859
  getState() {
13623
13860
  return createState();
@@ -13744,6 +13981,18 @@ function createPixiInteractionLayer(options) {
13744
13981
  if (event.button !== undefined && event.button !== 0) {
13745
13982
  return;
13746
13983
  }
13984
+ if (options.interaction.multiSelect &&
13985
+ event.shiftKey &&
13986
+ pick &&
13987
+ !options.editingEngine.hasCreationTool() &&
13988
+ options.editingEngine.getState().kind ===
13989
+ AnnotationGestureStateKind.Idle) {
13990
+ // Selection modifiers must run before the editing engine captures a
13991
+ // move. The following pointertap belongs to this same toggle.
13992
+ toggleSelectedPick(pick);
13993
+ suppressNextTap = true;
13994
+ return;
13995
+ }
13747
13996
  // Keep selection owned by this layer. The editing engine deliberately
13748
13997
  // treats a click as a cancelled move, so it cannot establish selection
13749
13998
  // itself; selecting here also makes resize handles available on the next
@@ -13783,7 +14032,7 @@ function createPixiInteractionLayer(options) {
13783
14032
  if (!marqueeStart)
13784
14033
  return;
13785
14034
  if (didDragMarquee && marqueeRect && activeFrame) {
13786
- selectedPicks = activeFrame.detections.flatMap((detection, detectionIndex) => {
14035
+ const nextPicks = activeFrame.detections.flatMap((detection, detectionIndex) => {
13787
14036
  if (!canPickDetection(detection))
13788
14037
  return [];
13789
14038
  const rect = getDetectionRect(detection);
@@ -13804,10 +14053,7 @@ function createPixiInteractionLayer(options) {
13804
14053
  },
13805
14054
  ];
13806
14055
  });
13807
- selectedPick = selectedPicks.at(-1) ?? null;
13808
- selectedPickKey = createDetectionPickKey(selectedPick);
13809
- options.interaction.onSelect?.(selectedPick);
13810
- options.interaction.onSelectionChange?.(selectedPicks);
14056
+ setSelectedPicks(nextPicks);
13811
14057
  }
13812
14058
  marqueeStart = null;
13813
14059
  marqueeRect = null;
@@ -13903,7 +14149,6 @@ function createPixiInteractionLayer(options) {
13903
14149
  if (nextKeys === currentKeys) {
13904
14150
  return;
13905
14151
  }
13906
- selectedPickKey = createDetectionPickKey(selectedPick);
13907
14152
  // A followed pick re-bases onto every new frame, so its full pick key
13908
14153
  // changes at playback rate. The scene's normal frame path consumes this
13909
14154
  // refreshed state immediately; emitting onStateChange here would redraw
@@ -13935,15 +14180,17 @@ function createPixiInteractionLayer(options) {
13935
14180
  return true;
13936
14181
  }
13937
14182
  function setSelectedPick(nextPick) {
13938
- const nextKey = createDetectionPickKey(nextPick);
13939
- if (nextKey === selectedPickKey) {
13940
- selectedPick = nextPick;
14183
+ setSelectedPicks(nextPick ? [nextPick] : []);
14184
+ }
14185
+ function setSelectedPicks(nextPicks) {
14186
+ const unchanged = selectedPicks.length === nextPicks.length &&
14187
+ selectedPicks.every((pick, index) => createDetectionPickKey(pick) ===
14188
+ createDetectionPickKey(nextPicks[index]));
14189
+ selectedPicks = nextPicks;
14190
+ selectedPick = nextPicks.at(-1) ?? null;
14191
+ if (unchanged)
13941
14192
  return;
13942
- }
13943
- selectedPick = nextPick;
13944
- selectedPicks = nextPick ? [nextPick] : [];
13945
- selectedPickKey = nextKey;
13946
- options.interaction.onSelect?.(nextPick);
14193
+ options.interaction.onSelect?.(selectedPick);
13947
14194
  options.interaction.onSelectionChange?.(selectedPicks);
13948
14195
  notifyStateChange();
13949
14196
  }
@@ -13962,17 +14209,20 @@ function createPixiInteractionLayer(options) {
13962
14209
  function toggleSelectedPick(pick) {
13963
14210
  if (!pick)
13964
14211
  return;
13965
- const key = createDetectionPickKey(pick);
13966
- const index = selectedPicks.findIndex((candidate) => createDetectionPickKey(candidate) === key);
13967
- selectedPicks =
13968
- index >= 0
13969
- ? selectedPicks.filter((_, candidateIndex) => candidateIndex !== index)
13970
- : [...selectedPicks, pick];
13971
- selectedPick = selectedPicks.at(-1) ?? null;
13972
- selectedPickKey = createDetectionPickKey(selectedPick);
13973
- options.interaction.onSelect?.(selectedPick);
13974
- options.interaction.onSelectionChange?.(selectedPicks);
13975
- notifyStateChange();
14212
+ const key = createSelectionKey(pick);
14213
+ const index = selectedPicks.findIndex((candidate) => createSelectionKey(candidate) === key);
14214
+ setSelectedPicks(index >= 0
14215
+ ? selectedPicks.filter((_, candidateIndex) => candidateIndex !== index)
14216
+ : [...selectedPicks, pick]);
14217
+ }
14218
+ function createSelectionKey(pick) {
14219
+ // Selection toggles the detection, regardless of whether its body, label,
14220
+ // or keypoint was hit. Keep the picker's frame/index fallback for duplicate ids.
14221
+ return createDetectionPickKey({
14222
+ ...pick,
14223
+ geometryIndex: undefined,
14224
+ target: DetectionPickTarget.Box,
14225
+ });
13976
14226
  }
13977
14227
  function getPoint(event) {
13978
14228
  if (!container)
@@ -17438,6 +17688,13 @@ function createPixiRegionLayer(options) {
17438
17688
  }
17439
17689
  return { activeDetectionIndexes: [...activeDetectionIndexes] };
17440
17690
  },
17691
+ hasAnimatedDisplay() {
17692
+ for (const entry of entries.values()) {
17693
+ if (isAnimatedDisplay(entry.display))
17694
+ return true;
17695
+ }
17696
+ return false;
17697
+ },
17441
17698
  releaseMaskTexture(texture) {
17442
17699
  if (destroyed)
17443
17700
  return;
@@ -17760,12 +18017,15 @@ function destroyEntry(entry) {
17760
18017
  // lightweight crop texture; the scene destroys the shared source.
17761
18018
  entry.ownedTexture?.destroy(false);
17762
18019
  }
18020
+ function isAnimatedDisplay(display) {
18021
+ return "play" in display && typeof display.play === "function";
18022
+ }
17763
18023
  function pauseAnimatedDisplay(display) {
17764
18024
  if ("stop" in display && typeof display.stop === "function")
17765
18025
  display.stop();
17766
18026
  }
17767
18027
  function resumeAnimatedDisplay(display) {
17768
- if ("play" in display && typeof display.play === "function")
18028
+ if (isAnimatedDisplay(display))
17769
18029
  display.play();
17770
18030
  }
17771
18031
  function matchesTarget(renderer, detection, context) {
@@ -18466,6 +18726,11 @@ function observePixiContainerResize(container, resize) {
18466
18726
  * sample it wants, hands it to `presentSample`, and Pixi's ticker paints the
18467
18727
  * scene every frame.
18468
18728
  *
18729
+ * A still frame is pulled once and then renders on change the way push
18730
+ * presentation does, with the sample it presents as one more change. The
18731
+ * ticker runs only while a looping GIF region or a loading spinner is on the
18732
+ * frame.
18733
+ *
18469
18734
  * Push presentation engages when the opened source carries a presented-frame
18470
18735
  * channel. The producer then decides which frame is on screen and announces it,
18471
18736
  * so the ticker paints nothing and every render is an explicit answer to a
@@ -18496,6 +18761,18 @@ async function createPixiMediaScene(options) {
18496
18761
  let maxTextureSide;
18497
18762
  const maxTextureSize = () => (maxTextureSide ??= queryMaxTextureSize(app.renderer));
18498
18763
  const frameChannel = options.presentedFrames;
18764
+ /**
18765
+ * A still frame has nothing to play, so it renders on change exactly as push
18766
+ * presentation does. The ticker runs only while something on the frame
18767
+ * animates by the clock.
18768
+ */
18769
+ const stillFrame = frameChannel === undefined && options.stillFrame === true;
18770
+ const rendersOnChange = frameChannel !== undefined || stillFrame;
18771
+ let stillFrameTickerRunning = false;
18772
+ /** The content of the `previewOverlay` answer the overlay on screen drew. */
18773
+ let drawnPreviewOverlayContent = null;
18774
+ /** The editing state the overlay on screen was drawn from. */
18775
+ let drawnEditingState;
18499
18776
  let currentFocusStyle = options.focusStyle ?? null;
18500
18777
  let currentLabelStyle = options.labelStyle ?? null;
18501
18778
  let currentMaskStyle = options.maskStyle ?? null;
@@ -18520,6 +18797,9 @@ async function createPixiMediaScene(options) {
18520
18797
  let isScrubbing = false;
18521
18798
  let displayBrightness = 1;
18522
18799
  let displayContrast = 1;
18800
+ let displayGamma = 1;
18801
+ let displayAutoContrast = false;
18802
+ let inspectionFilter;
18523
18803
  let viewportScale = 1;
18524
18804
  let hasPresentedSample = false;
18525
18805
  let mediaHeight = 0;
@@ -18736,7 +19016,7 @@ async function createPixiMediaScene(options) {
18736
19016
  : undefined;
18737
19017
  await app.init({
18738
19018
  autoDensity: true,
18739
- autoStart: frameChannel === undefined,
19019
+ autoStart: !rendersOnChange,
18740
19020
  backgroundColor: options.backgroundColor ?? 0x111111,
18741
19021
  /* Only the video-engine media path supplies a presented-frame channel, so
18742
19022
  * which reader opens the clip is also what decides which backend draws. */
@@ -18755,7 +19035,10 @@ async function createPixiMediaScene(options) {
18755
19035
  if (!stagingContext) {
18756
19036
  throw new Error("Unable to create staging canvas context.");
18757
19037
  }
18758
- const renderScheduler = createSceneRenderScheduler(() => app.render());
19038
+ const renderScheduler = createSceneRenderScheduler(() => {
19039
+ app.render();
19040
+ syncStillFrameTicker();
19041
+ });
18759
19042
  const maskBrushPreview = options.maskBrush
18760
19043
  ? createPixiMaskBrushPreview({
18761
19044
  CanvasSource,
@@ -18764,7 +19047,7 @@ async function createPixiMediaScene(options) {
18764
19047
  Sprite,
18765
19048
  Texture,
18766
19049
  preview: options.maskBrush,
18767
- onInvalidate: frameChannel ? renderNow : undefined,
19050
+ onInvalidate: rendersOnChange ? renderNow : undefined,
18768
19051
  })
18769
19052
  : undefined;
18770
19053
  let appliedPresentation;
@@ -18933,6 +19216,17 @@ async function createPixiMediaScene(options) {
18933
19216
  labelLayer?.drawFrame(currentMediaTime, viewportScale);
18934
19217
  renderNow();
18935
19218
  }
19219
+ // With no ticker to pick it up, an edit the host drives itself, such as a
19220
+ // cancel or a commit, reaches the screen through this notification. A
19221
+ // pointer gesture draws and renders its own state before the microtask
19222
+ // runs, which then finds nothing left to draw or render.
19223
+ if (rendersOnChange) {
19224
+ queueMicrotask(() => {
19225
+ if (isDestroyed || isPresenting)
19226
+ return;
19227
+ renderOnChange();
19228
+ });
19229
+ }
18936
19230
  });
18937
19231
  const handleKeyDown = (event) => {
18938
19232
  if (event.key === "Tab" && interactionLayer) {
@@ -19122,15 +19416,17 @@ async function createPixiMediaScene(options) {
19122
19416
  renderScene();
19123
19417
  };
19124
19418
  let unsubscribeDetectionTimeline;
19125
- if (frameChannel) {
19126
- frameChannel.onPresentedFrame(handlePresentedFrame);
19419
+ if (rendersOnChange) {
19127
19420
  document.addEventListener?.("visibilitychange", handleVisibilityChange);
19128
19421
  // A detection load landing moves what the window covers exactly as a cook
19129
19422
  // landing does. Nothing else brings that to a resting playhead: the layers
19130
19423
  // read the timeline only while drawing, and this is what makes the draw.
19131
19424
  unsubscribeDetectionTimeline = options.detectionTimeline.subscribe?.(handlePreparedWindowChange);
19132
19425
  }
19133
- else {
19426
+ if (frameChannel) {
19427
+ frameChannel.onPresentedFrame(handlePresentedFrame);
19428
+ }
19429
+ else if (!stillFrame) {
19134
19430
  app.ticker.add(updateMediaSceneFit);
19135
19431
  app.ticker.add(drawAnnotationOverlayNow);
19136
19432
  app.ticker.add(tickFocusLayer);
@@ -19200,7 +19496,9 @@ async function createPixiMediaScene(options) {
19200
19496
  advanceFocus: () => undefined,
19201
19497
  drawAnnotationOverlay: frameChannel
19202
19498
  ? presentLayers.drawAnnotationOverlay
19203
- : () => undefined,
19499
+ : stillFrame
19500
+ ? (mediaTime) => drawAnnotationOverlay(mediaTime, overlayNow())
19501
+ : () => undefined,
19204
19502
  };
19205
19503
  // A viewport-style redraw covers the layers whose geometry is a function of
19206
19504
  // the viewport scale. Mask outlines reuse the artifact already on screen;
@@ -19250,7 +19548,7 @@ async function createPixiMediaScene(options) {
19250
19548
  };
19251
19549
  return {
19252
19550
  getRenderCount() {
19253
- return frameChannel ? renderScheduler.getRenderCount() : null;
19551
+ return rendersOnChange ? renderScheduler.getRenderCount() : null;
19254
19552
  },
19255
19553
  getPreparedAnnotationWindow() {
19256
19554
  return frameChannel ? annotationWindow.getSnapshot() : null;
@@ -19359,10 +19657,12 @@ async function createPixiMediaScene(options) {
19359
19657
  if (!collectFrameTimings) {
19360
19658
  sample.draw(stagingContext, 0, 0, mediaWidth, mediaHeight);
19361
19659
  presentedSampleTimestamp = sample.timestamp;
19660
+ refreshInspectionImage();
19362
19661
  stagingTextureSource?.update();
19363
19662
  stagingTexture?.update();
19364
19663
  const { boxState, regionState } = drawFramePresentLayers(annotationDrawLayers, sample.timestamp);
19365
19664
  updateMediaSceneFit();
19665
+ renderOnChange();
19366
19666
  return {
19367
19667
  ...createPresentedSampleState(sample.timestamp, boxState, regionState),
19368
19668
  duration: sample.duration,
@@ -19373,11 +19673,13 @@ async function createPixiMediaScene(options) {
19373
19673
  const mediaUploadMs = measure(() => {
19374
19674
  sample.draw(stagingContext, 0, 0, mediaWidth, mediaHeight);
19375
19675
  presentedSampleTimestamp = sample.timestamp;
19676
+ refreshInspectionImage();
19376
19677
  stagingTextureSource?.update();
19377
19678
  stagingTexture?.update();
19378
19679
  });
19379
19680
  const { boxState, regionState } = drawFramePresentLayers(measuredAnnotationDrawLayers, sample.timestamp);
19380
19681
  const fitMs = measure(updateMediaSceneFit);
19682
+ renderOnChange();
19381
19683
  return {
19382
19684
  ...createPresentedSampleState(sample.timestamp, boxState, regionState),
19383
19685
  duration: sample.duration,
@@ -19435,15 +19737,27 @@ async function createPixiMediaScene(options) {
19435
19737
  },
19436
19738
  setDisplayAdjustments(adjustments) {
19437
19739
  void import('pixi.js').then(({ ColorMatrixFilter }) => {
19740
+ if (isDestroyed)
19741
+ return;
19438
19742
  const brightness = adjustments.brightness ?? 1;
19439
19743
  const contrast = adjustments.contrast ?? 1;
19440
19744
  const filter = new ColorMatrixFilter();
19441
19745
  // Preserve the renderer's pixel grid through the adjustment pass.
19442
19746
  filter.resolution = "inherit";
19443
19747
  filter.brightness(brightness, false);
19444
- filter.contrast(contrast - 1, true);
19445
- if (mediaSprite)
19446
- mediaSprite.filters = [filter];
19748
+ displayGamma = adjustments.gamma ?? 1;
19749
+ displayAutoContrast = stillFrame && adjustments.autoContrast === true;
19750
+ filter.contrast(displayAutoContrast ? 0 : contrast - 1, true);
19751
+ if (mediaSprite) {
19752
+ if (displayGamma !== 1 || displayAutoContrast) {
19753
+ inspectionFilter ??= createPixiMediaInspectionFilter(mediaSprite, pixi);
19754
+ inspectionFilter.update(displayGamma, displayAutoContrast, hasPresentedSample ? stagingCanvas : null);
19755
+ mediaSprite.filters = [inspectionFilter.filter, filter];
19756
+ }
19757
+ else {
19758
+ mediaSprite.filters = [filter];
19759
+ }
19760
+ }
19447
19761
  displayBrightness = brightness;
19448
19762
  displayContrast = contrast;
19449
19763
  renderOnChange();
@@ -19750,6 +20064,7 @@ async function createPixiMediaScene(options) {
19750
20064
  // a decoder buffer in a producer that outlives the scene.
19751
20065
  frameChannel?.onPresentedFrame((presented) => presented.frame.close());
19752
20066
  destroyBatchTextureBindings();
20067
+ inspectionFilter?.destroy();
19753
20068
  mediaCompositor?.destroy();
19754
20069
  interactionLayer?.destroy();
19755
20070
  interactionPresentationLayer?.destroy();
@@ -20256,7 +20571,7 @@ async function createPixiMediaScene(options) {
20256
20571
  return;
20257
20572
  }
20258
20573
  const { boxState, regionState } = drawAnnotationFrame(currentMediaTime);
20259
- if (frameChannel) {
20574
+ if (rendersOnChange) {
20260
20575
  const signature = describeSceneRender();
20261
20576
  if (forceRender)
20262
20577
  renderScheduler.render(signature);
@@ -20313,6 +20628,13 @@ async function createPixiMediaScene(options) {
20313
20628
  labelLayer?.drawCreationPreview(editingState?.kind === AnnotationGestureStateKind.Creating
20314
20629
  ? editingState.preview
20315
20630
  : null, mediaTime, viewportScale);
20631
+ const previewOverlay = options.previewOverlay?.() ?? null;
20632
+ drawnEditingState = editingState;
20633
+ // Only a scene that renders on change compares signatures.
20634
+ if (rendersOnChange) {
20635
+ drawnPreviewOverlayContent =
20636
+ describePreviewOverlayContent(previewOverlay);
20637
+ }
20316
20638
  annotationOverlayLayer.draw({
20317
20639
  frame,
20318
20640
  marquee: interactionState?.marqueeRect ?? null,
@@ -20320,7 +20642,7 @@ async function createPixiMediaScene(options) {
20320
20642
  mediaWidth,
20321
20643
  now: overlayNowMs,
20322
20644
  pointer: interactionState?.pointerPoint ?? null,
20323
- previewOverlay: options.previewOverlay?.() ?? null,
20645
+ previewOverlay,
20324
20646
  selectedDetectionIds: (interactionState?.selectedPicks ?? []).flatMap(({ detection }) => (detection.id === undefined ? [] : [detection.id])),
20325
20647
  viewportScale,
20326
20648
  visibility: currentVisibility,
@@ -20328,8 +20650,9 @@ async function createPixiMediaScene(options) {
20328
20650
  }
20329
20651
  /**
20330
20652
  * Milliseconds for animations that are not annotation geometry. A pull scene
20331
- * has a ticker and reads the wall clock; a push scene has the presented media
20332
- * time and nothing else, so its animations move only when the media does.
20653
+ * has a ticker and reads the wall clock, as does a still frame while its
20654
+ * ticker runs; a push scene has the presented media time and nothing else,
20655
+ * so its animations move only when the media does.
20333
20656
  */
20334
20657
  function overlayNow() {
20335
20658
  return frameChannel ? currentMediaTime * MILLISECONDS_PER_SECOND : now();
@@ -20378,8 +20701,21 @@ async function createPixiMediaScene(options) {
20378
20701
  focusLayer.tick(focusAnimationTimeMs);
20379
20702
  }
20380
20703
  function renderScene() {
20704
+ drawOverlayForEditingState();
20381
20705
  renderScheduler.render(describeSceneRender());
20382
20706
  }
20707
+ /**
20708
+ * The render signature names the editing state, so a render must not record
20709
+ * a state the overlay has not drawn: the redraw that follows would match the
20710
+ * recorded signature and be skipped, leaving the old preview on screen.
20711
+ */
20712
+ function drawOverlayForEditingState() {
20713
+ const editingState = options.editingEngine?.getState();
20714
+ if (editingState === undefined || editingState === drawnEditingState) {
20715
+ return;
20716
+ }
20717
+ drawAnnotationOverlay(currentMediaTime, overlayNow());
20718
+ }
20383
20719
  /**
20384
20720
  * Renders a present at most once per display refresh. Frames arrive as
20385
20721
  * messages from the producer's own thread, so a main thread that falls
@@ -20405,15 +20741,52 @@ async function createPixiMediaScene(options) {
20405
20741
  presentFrameNow(deferred);
20406
20742
  }
20407
20743
  function renderNow() {
20408
- if (frameChannel)
20744
+ if (rendersOnChange)
20409
20745
  renderScene();
20410
20746
  }
20411
20747
  function renderOnChange() {
20412
- if (!frameChannel)
20748
+ if (!rendersOnChange)
20413
20749
  return;
20414
20750
  settleFocusAnimation();
20751
+ drawOverlayForEditingState();
20415
20752
  renderScheduler.renderOnChange(describeSceneRender());
20416
20753
  }
20754
+ /**
20755
+ * Runs the ticker on a still frame for as long as something on it moves by
20756
+ * the clock: a looping GIF region or a loading spinner. Pixi renders on
20757
+ * every tick it runs, and the overlay redraws so the spinner turns. Each
20758
+ * render checks again, so the ticker stops with the render that removes the
20759
+ * last of them.
20760
+ */
20761
+ function syncStillFrameTicker() {
20762
+ if (!stillFrame || isDestroyed)
20763
+ return;
20764
+ const animating = hasPresentedSample &&
20765
+ (regionLayer.hasAnimatedDisplay() || hasLoadingDetections());
20766
+ if (animating === stillFrameTickerRunning)
20767
+ return;
20768
+ stillFrameTickerRunning = animating;
20769
+ if (animating) {
20770
+ app.ticker.add(drawAnnotationOverlayNow);
20771
+ app.ticker.start();
20772
+ }
20773
+ else {
20774
+ app.ticker.stop();
20775
+ app.ticker.remove(drawAnnotationOverlayNow);
20776
+ }
20777
+ }
20778
+ function refreshInspectionImage() {
20779
+ if (!stillFrame || !inspectionFilter)
20780
+ return;
20781
+ inspectionFilter.invalidateImage();
20782
+ inspectionFilter.update(displayGamma, displayAutoContrast, stagingCanvas);
20783
+ }
20784
+ function hasLoadingDetections() {
20785
+ const loading = currentVisibility?.loadingDetectionIds;
20786
+ if (!loading)
20787
+ return false;
20788
+ return "size" in loading ? loading.size > 0 : loading.length > 0;
20789
+ }
20417
20790
  /**
20418
20791
  * Everything a render would put on screen, as values that can be compared.
20419
20792
  * Styles are opaque resolvers, so they are compared by identity: handing the
@@ -20439,6 +20812,8 @@ async function createPixiMediaScene(options) {
20439
20812
  app.renderer.resolution,
20440
20813
  displayBrightness,
20441
20814
  displayContrast,
20815
+ displayGamma,
20816
+ displayAutoContrast,
20442
20817
  ...RENDERED_PRESENTATION_FIELDS.map((field) => appliedPresentation?.[field]),
20443
20818
  // Styles above compare by identity; mask opacity is applied after
20444
20819
  // preparation, so a host can move it inside a style object it keeps.
@@ -20451,6 +20826,9 @@ async function createPixiMediaScene(options) {
20451
20826
  interactionState?.marqueeRect,
20452
20827
  options.editingEngine?.getState(),
20453
20828
  options.editingEngine ? interactionState?.pointerPoint : null,
20829
+ // The host's preview is read when the overlay draws, and a host may edit
20830
+ // its arrays in place, so its content is what tells a redraw it changed.
20831
+ drawnPreviewOverlayContent,
20454
20832
  ];
20455
20833
  }
20456
20834
  /**
@@ -20789,6 +21167,37 @@ function elapsedSince(start) {
20789
21167
  function now() {
20790
21168
  return globalThis.performance?.now?.() ?? Date.now();
20791
21169
  }
21170
+ /**
21171
+ * A key for what a preview overlay draws, hashed from its coordinates rather
21172
+ * than its arrays, so a preview the host edits in place still changes it.
21173
+ * Coordinates count to a 256th of a media pixel, finer than anything drawn.
21174
+ */
21175
+ function describePreviewOverlayContent(overlay) {
21176
+ if (!overlay)
21177
+ return null;
21178
+ let hash = 0x811c9dc5;
21179
+ const mix = (value) => {
21180
+ hash = Math.imul(hash ^ value, 0x01000193);
21181
+ };
21182
+ const mixPoint = (point) => {
21183
+ mix(Math.round(point.x * 256));
21184
+ mix(Math.round(point.y * 256));
21185
+ };
21186
+ for (const polygons of [overlay.hoverPolygons, overlay.draftPolygons]) {
21187
+ mix(polygons?.length ?? -1);
21188
+ for (const polygon of polygons ?? []) {
21189
+ mix(polygon.length);
21190
+ for (const point of polygon)
21191
+ mixPoint(point);
21192
+ }
21193
+ }
21194
+ mix(overlay.points?.length ?? -1);
21195
+ for (const point of overlay.points ?? []) {
21196
+ mixPoint(point);
21197
+ mix(point.positive ? 1 : 0);
21198
+ }
21199
+ return hash;
21200
+ }
20792
21201
  /**
20793
21202
  * Fills the container and paints the margin around the canvas in the colour the
20794
21203
  * renderer clears to. The scene owns this element so the canvas is positioned