supervision 0.2.9 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/{depth-preview-probe-DG7McQEN.js → depth-preview-probe-BG0k-33U.js} +3 -3
  2. package/dist/{depth-preview-probe-DG7McQEN.js.map → depth-preview-probe-BG0k-33U.js.map} +1 -1
  3. package/dist/{depth-preview-track-BvgwNEE9.js → depth-preview-track-BEag1ZWF.js} +2 -2
  4. package/dist/{depth-preview-track-BvgwNEE9.js.map → depth-preview-track-BEag1ZWF.js.map} +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2041 -478
  8. package/dist/index.js.map +1 -1
  9. package/dist/mask-preparation.worker.js +166 -45
  10. package/dist/mask-preparation.worker.js.map +1 -1
  11. package/dist/media/display-pixel-ratio.d.ts +3 -8
  12. package/dist/media/display-pixel-ratio.d.ts.map +1 -1
  13. package/dist/media/video-engine-media-source.js +1 -1
  14. package/dist/render-preparation/mask-frame-artifact.d.ts +7 -1
  15. package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
  16. package/dist/render-preparation/mask-frame-compositor.d.ts +1 -1
  17. package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
  18. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +3 -0
  19. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
  20. package/dist/render-preparation/prepared-render-window.d.ts +2 -0
  21. package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
  22. package/dist/renderers/annotation-fxaa.d.ts +37 -0
  23. package/dist/renderers/annotation-fxaa.d.ts.map +1 -0
  24. package/dist/renderers/id-mask-stroke-coverage.d.ts +2 -2
  25. package/dist/renderers/id-mask-stroke-coverage.d.ts.map +1 -1
  26. package/dist/renderers/mask-palette.d.ts +4 -8
  27. package/dist/renderers/mask-palette.d.ts.map +1 -1
  28. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  29. package/dist/renderers/media-renderer-scene.d.ts +1 -0
  30. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  31. package/dist/renderers/pixi-annotation-antialias-layer.d.ts +15 -0
  32. package/dist/renderers/pixi-annotation-antialias-layer.d.ts.map +1 -0
  33. package/dist/renderers/pixi-annotation-antialias.d.ts +14 -0
  34. package/dist/renderers/pixi-annotation-antialias.d.ts.map +1 -0
  35. package/dist/renderers/pixi-annotation-overlay-layer.d.ts +6 -1
  36. package/dist/renderers/pixi-annotation-overlay-layer.d.ts.map +1 -1
  37. package/dist/renderers/pixi-box-layer.d.ts +6 -1
  38. package/dist/renderers/pixi-box-layer.d.ts.map +1 -1
  39. package/dist/renderers/pixi-filter-bindings.d.ts +6 -0
  40. package/dist/renderers/pixi-filter-bindings.d.ts.map +1 -0
  41. package/dist/renderers/pixi-focus-heatmap-cutout.d.ts +17 -0
  42. package/dist/renderers/pixi-focus-heatmap-cutout.d.ts.map +1 -0
  43. package/dist/renderers/pixi-focus-layer.d.ts +18 -1
  44. package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
  45. package/dist/renderers/pixi-heatmap-layer.d.ts +4 -1
  46. package/dist/renderers/pixi-heatmap-layer.d.ts.map +1 -1
  47. package/dist/renderers/pixi-id-mask-shader.d.ts +2 -1
  48. package/dist/renderers/pixi-id-mask-shader.d.ts.map +1 -1
  49. package/dist/renderers/pixi-interaction-presentation-layer.d.ts +5 -1
  50. package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
  51. package/dist/renderers/pixi-label-layer.d.ts +8 -4
  52. package/dist/renderers/pixi-label-layer.d.ts.map +1 -1
  53. package/dist/renderers/pixi-mask-layer.d.ts +2 -1
  54. package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
  55. package/dist/renderers/pixi-mask-screen-stroke.d.ts +1 -1
  56. package/dist/renderers/pixi-mask-screen-stroke.d.ts.map +1 -1
  57. package/dist/renderers/pixi-mask-viewport.d.ts +21 -0
  58. package/dist/renderers/pixi-mask-viewport.d.ts.map +1 -0
  59. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  60. package/dist/renderers/pixi-polygon-layer.d.ts +3 -0
  61. package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
  62. package/dist/renderers/pixi-region-coverage-mask.d.ts +14 -3
  63. package/dist/renderers/pixi-region-coverage-mask.d.ts.map +1 -1
  64. package/dist/renderers/pixi-region-layer.d.ts +7 -1
  65. package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
  66. package/dist/sessions/media-session.d.ts.map +1 -1
  67. package/dist/tracking.worker.js.map +1 -1
  68. package/dist/types/media-renderer.d.ts +1 -1
  69. package/dist/types/media-renderer.d.ts.map +1 -1
  70. package/dist/{video-engine-media-source-DOFsuGS4.js → video-engine-media-source-MFthAkbe.js} +4 -9
  71. package/dist/video-engine-media-source-MFthAkbe.js.map +1 -0
  72. package/node_modules/supervision-js-core/dist/index.d.ts +2 -2
  73. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  74. package/node_modules/supervision-js-core/dist/index.js +57 -25
  75. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  76. package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts.map +1 -1
  77. package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts +4 -3
  78. package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts.map +1 -1
  79. package/node_modules/supervision-js-core/dist/styles/mask-style.d.ts.map +1 -1
  80. package/node_modules/supervision-js-core/dist/types/focus-style.d.ts +2 -1
  81. package/node_modules/supervision-js-core/dist/types/focus-style.d.ts.map +1 -1
  82. package/node_modules/supervision-js-core/dist/types/label-style.d.ts +4 -1
  83. package/node_modules/supervision-js-core/dist/types/label-style.d.ts.map +1 -1
  84. package/node_modules/supervision-js-core/dist/types/mask-style.d.ts +8 -1
  85. package/node_modules/supervision-js-core/dist/types/mask-style.d.ts.map +1 -1
  86. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +14 -0
  87. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  88. package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts +1 -0
  89. package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts.map +1 -1
  90. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +21 -7
  91. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -1
  92. package/package.json +1 -1
  93. package/dist/video-engine-media-source-DOFsuGS4.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
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
- 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
- import { c as createMediaFrameClock, M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-DOFsuGS4.js';
4
- export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-DOFsuGS4.js';
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, resolveIdMaskStrokeTexels, resolveIdMaskPaletteId, StrokeAlignment, MAX_ID_MASK_STROKE_WIDTH, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, rebaseDetectionPickToFrame, pickDetectionAtPoint, createDetectionPickKey, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, 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
+ 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, StrokeAlignment, 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
+ import { c as createMediaFrameClock, M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-MFthAkbe.js';
4
+ export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-MFthAkbe.js';
5
5
 
6
6
  const DEFAULT_DATABASE_NAME = "supervision-js-detection-frames";
7
7
  const DEFAULT_CHUNK_DURATION_SECONDS = 1;
@@ -3113,7 +3113,7 @@ function assertMediaAspect(map, media) {
3113
3113
  }
3114
3114
  }
3115
3115
 
3116
- const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** Trace closed pixel boundaries, retaining holes and disconnected components. */\n function traceMaskOutlinePaths(raster, width, height, ids) {\n const edgesById = new Map();\n const stride = width + 1;\n const steps = [1, stride, -1, -stride];\n const add = (id, vertex, direction) => {\n let edges = edgesById.get(id);\n if (!edges)\n edgesById.set(id, (edges = new Map()));\n const outgoing = edges.get(vertex);\n if (outgoing)\n outgoing.push(direction);\n else\n edges.set(vertex, [direction]);\n };\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const offset = y * width + x;\n const id = raster[offset];\n if (!ids.has(id))\n continue;\n const vertex = y * stride + x;\n if (y === 0 || raster[offset - width] !== id)\n add(id, vertex, 0);\n if (x === width - 1 || raster[offset + 1] !== id)\n add(id, vertex + 1, 1);\n if (y === height - 1 || raster[offset + width] !== id)\n add(id, vertex + stride + 1, 2);\n if (x === 0 || raster[offset - 1] !== id)\n add(id, vertex + stride, 3);\n }\n }\n const result = new Map();\n for (const [id, edges] of edgesById) {\n const paths = [];\n while (edges.size > 0) {\n const start = edges.keys().next().value;\n let vertex = start;\n let previousDirection = -1;\n let firstDirection = -1;\n const points = [];\n do {\n const outgoing = edges.get(vertex);\n // Turn right at diagonal contacts so separate islands stay separate.\n const direction = previousDirection < 0\n ? outgoing[0]\n : [1, 0, 3, 2]\n .map((turn) => (previousDirection + turn) % 4)\n .find((candidate) => outgoing.includes(candidate));\n if (firstDirection < 0)\n firstDirection = direction;\n if (direction !== previousDirection)\n points.push(vertex % stride, Math.floor(vertex / stride));\n outgoing.splice(outgoing.indexOf(direction), 1);\n if (outgoing.length === 0)\n edges.delete(vertex);\n vertex += steps[direction];\n previousDirection = direction;\n } while (vertex !== start);\n if (previousDirection === firstDirection)\n points.splice(0, 2);\n paths.push(new Float32Array(points));\n }\n result.set(id, paths);\n }\n return result;\n }\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n function decodeCompressedRleMask(mask) {\n if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {\n throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);\n }\n const data = new Uint8Array(mask.width * mask.height);\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n const isForeground = index % 2 === 1;\n if (isForeground) {\n for (let runOffset = 0; runOffset < runLength; runOffset += 1) {\n const maskOffset = offset + runOffset;\n const x = Math.floor(maskOffset / mask.height);\n const y = maskOffset % mask.height;\n const rowMajorOffset = y * mask.width + x;\n if (rowMajorOffset < data.length) {\n data[rowMajorOffset] = 1;\n }\n }\n }\n offset += runLength;\n }\n return {\n data,\n height: mask.height,\n width: mask.width,\n };\n }\n function decodeCompressedRleCounts(counts) {\n const decoded = [];\n let index = 0;\n while (index < counts.length) {\n let value = 0;\n let shift = 0;\n let charCode;\n do {\n charCode = counts.charCodeAt(index) - 48;\n index += 1;\n value |= (charCode & 0x1f) << shift;\n shift += 5;\n } while (charCode & 0x20);\n if (charCode & 0x10) {\n value |= -1 << shift;\n }\n if (decoded.length > 2) {\n value += decoded[decoded.length - 2] ?? 0;\n }\n decoded.push(value);\n }\n return decoded;\n }\n function encodeCompressedRleCounts(counts) {\n return counts\n .map((count, index) => {\n let value = index > 2 ? count - counts[index - 2] : count;\n let encoded = \"\";\n let more = true;\n while (more) {\n let charCode = value & 0x1f;\n value >>= 5;\n more = !((value === 0 && (charCode & 0x10) === 0) ||\n (value === -1 && (charCode & 0x10) !== 0));\n if (more) {\n charCode |= 0x20;\n }\n encoded += String.fromCharCode(charCode + 48);\n }\n return encoded;\n })\n .join(\"\");\n }\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n function encodeBinaryMask(data, width, height) {\n assertMaskDimensions(data, width, height);\n const runs = [];\n let currentValue = 0;\n let runLength = 0;\n for (let x = 0; x < width; x += 1) {\n for (let y = 0; y < height; y += 1) {\n const value = data[y * width + x] ? 1 : 0;\n if (value === currentValue) {\n runLength += 1;\n }\n else {\n runs.push(runLength);\n currentValue = value;\n runLength = 1;\n }\n }\n }\n runs.push(runLength);\n return {\n counts: encodeCompressedRleCounts(runs),\n encoding: DetectionMaskEncoding.CompressedRle,\n height,\n width,\n };\n }\n function assertMaskSize(width, height) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(\"Mask width must be a positive integer.\");\n }\n if (!Number.isInteger(height) || height <= 0) {\n throw new Error(\"Mask height must be a positive integer.\");\n }\n }\n function assertMaskDimensions(data, width, height) {\n assertMaskSize(width, height);\n if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\n\n function centerRectToTopLeftRect(rect) {\n return {\n height: rect.height,\n width: rect.width,\n x: rect.x - rect.width / 2,\n y: rect.y - rect.height / 2,\n };\n }\n function getPointsRect(points) {\n if (points.length === 0) {\n return undefined;\n }\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n minY = Math.min(minY, point.y);\n maxX = Math.max(maxX, point.x);\n maxY = Math.max(maxY, point.y);\n }\n return {\n height: maxY - minY,\n width: maxX - minX,\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n };\n }\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"OrientedBox\"] = \"orientedBox\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var PercentageBarPlacement;\n (function (PercentageBarPlacement) {\n PercentageBarPlacement[\"Top\"] = \"top\";\n PercentageBarPlacement[\"Bottom\"] = \"bottom\";\n PercentageBarPlacement[\"InsideTop\"] = \"insideTop\";\n PercentageBarPlacement[\"InsideBottom\"] = \"insideBottom\";\n })(PercentageBarPlacement || (PercentageBarPlacement = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n\n /**\n * What the stored values of a depth map measure.\n *\n * Every kind stores 0 where there is no depth.\n */\n const DepthMapKind = {\n /** Stereo disparity in pixels of the map. Larger is nearer. */\n DisparityPx: \"disparity_px\",\n /** Metric depth along the optical axis in metres. Smaller is nearer. */\n DepthM: \"depth_m\",\n /**\n * Unitless inverse depth up to an unknown scale and shift, as monocular\n * models produce it. Larger is nearer; it has no metric depth.\n */\n RelativeInverse: \"relative_inverse\",\n };\n /**\n * The quantity a depth renderer colours.\n *\n * Disparity is inverse depth, so it spends colour on near detail the way\n * stereo measures it. Either way the nearest end of the range is the warm or\n * bright end of the colour table.\n */\n const DepthQuantity = {\n Disparity: \"disparity\",\n Depth: \"depth\",\n };\n const DepthColormap = {\n Turbo: \"turbo\",\n Viridis: \"viridis\",\n Cividis: \"cividis\",\n Inferno: \"inferno\",\n Magma: \"magma\",\n Grayscale: \"grayscale\",\n };\n /** How a depth map is sampled where one map pixel does not cover one media pixel. */\n const DepthSampling = {\n /** Nearest when the map is at least media size, edge-aware otherwise. */\n Auto: \"auto\",\n Nearest: \"nearest\",\n /**\n * Bilinear over valid neighbours only, falling back to the nearest one\n * across a depth edge, so objects never bleed into the background.\n */\n EdgeAware: \"edge-aware\",\n };\n /**\n * The luma range a depth preview video's codes are written in.\n *\n * TV range is what browsers' hardware decoders return as written; a decoder\n * may squeeze full range into TV range or move codes converting it to RGB.\n */\n const DepthPreviewLevels = {\n /** Every code from 0 to 255; the top code is 255. */\n Full: \"full\",\n /** Limited range: black is 16 and the top code is 235. */\n Tv: \"tv\",\n };\n new Set(Object.values(DepthColormap));\n new Set(Object.values(DepthQuantity));\n new Set(Object.values(DepthSampling));\n function rasterizePolygonToMask(points, dimensions) {\n const data = createEmptyMask(dimensions);\n if (points.length < 3) {\n return data;\n }\n const bounds = centerRectToTopLeftRect(getPointsRect(points));\n const startY = Math.max(0, Math.floor(bounds.y));\n const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));\n for (let y = startY; y <= endY; y += 1) {\n const scanY = y + 0.5;\n const intersections = [];\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index];\n const next = points[(index + 1) % points.length];\n if ((current.y <= scanY && next.y > scanY) ||\n (next.y <= scanY && current.y > scanY)) {\n const ratio = (scanY - current.y) / (next.y - current.y);\n intersections.push(current.x + ratio * (next.x - current.x));\n }\n }\n intersections.sort((left, right) => left - right);\n for (let index = 0; index < intersections.length - 1; index += 2) {\n const left = Math.max(0, Math.round(intersections[index]));\n const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));\n for (let x = left; x < right; x += 1) {\n data[y * dimensions.width + x] = 1;\n }\n }\n }\n return data;\n }\n function createEmptyMask(dimensions) {\n if (!Number.isInteger(dimensions.width) ||\n dimensions.width <= 0 ||\n !Number.isInteger(dimensions.height) ||\n dimensions.height <= 0) {\n throw new Error(\"Media dimensions must be positive integers.\");\n }\n return new Uint8Array(dimensions.width * dimensions.height);\n }\n\n /**\n * Palette slots an id raster can name, one of them the background. A GLSL\n * fragment stage is guaranteed only 224 uniform vectors, and a palette entry\n * costs 2.25 of them, so this stays a multiple of four and well inside that\n * budget; past it a frame draws from the RGBA composite instead.\n */\n const MAX_ID_MASK_PALETTE_ENTRIES = 80;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions, options = {}) {\n if (instructions.length === 0) {\n return undefined;\n }\n /* Pixel-grid dimensions use integer parts throughout allocation and RLE\n traversal. */\n const maskWidth = Math.floor(Math.max(...instructions.map(({ mask }) => mask.width)));\n const maskHeight = Math.floor(Math.max(...instructions.map(({ mask }) => mask.height)));\n const width = resolveRasterWidth(maskWidth, options.maxWidth);\n const height = width === maskWidth\n ? maskHeight\n : Math.max(1, Math.round((width * maskHeight) / maskWidth));\n const scaledAxes = width === maskWidth\n ? undefined\n : createScaledMaskAxes({ height, maskHeight, maskWidth, width });\n const data = new Uint8Array(new ArrayBuffer(width * height));\n const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n let hasStroke = false;\n let maxStrokeWidth = 0;\n for (const instruction of instructions) {\n const detectionMaskId = resolveIdMaskPaletteId(instruction.detectionIndex);\n if (detectionMaskId === undefined) {\n return undefined;\n }\n writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = resolveIdMaskStrokeTexels(instruction.stroke.width, maskWidth, width);\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);\n writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n if (scaledAxes) {\n writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);\n }\n else {\n writeMaskRuns(data, width, instruction.mask, detectionMaskId);\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n sourceWidth: maskWidth,\n strokePalette,\n strokeWidths,\n width,\n };\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let frameOffset = y * frameWidth + x;\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[frameOffset] = detectionMaskId;\n y += 1;\n frameOffset += frameWidth;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n frameOffset = x;\n }\n }\n maskOffset += runLength;\n }\n }\n /**\n * The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands\n * in, so a mask keeps every part of itself the smaller raster can hold and\n * gains up to a texel at its edges.\n */\n function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n const stride = axes.stride;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let remaining = runLength;\n /* A run walks down one column and wraps into the next, so it splits into\n column-sized segments. Within a segment the destination offset only ever\n steps by the row pitch or stays put: createMaskAxisMap builds\n `rows[i] = min(height - 1, floor(i * scale)) * width` with scale <= 1, so\n consecutive entries differ by exactly 0 or exactly `width`. Walking the\n destination directly therefore touches the same bytes the per-pixel loop\n touched, and drops the bounds test, the wrap test and one typed-array\n load from every source pixel. Where the scale collapses several source\n rows onto one destination row, this writes that byte once instead of\n once per source row. */\n while (remaining > 0 && x < maskWidth) {\n const span = Math.min(remaining, maskHeight - y);\n const column = axes.columns[x];\n const first = axes.rows[y];\n const last = axes.rows[y + span - 1];\n for (let offset = first; offset <= last; offset += stride) {\n data[offset + column] = detectionMaskId;\n }\n remaining -= span;\n y += span;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n }\n }\n maskOffset += runLength;\n }\n }\n function resolveRasterWidth(maskWidth, maxWidth) {\n return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth\n ? Math.max(1, Math.floor(maxWidth))\n : maskWidth;\n }\n /**\n * Converts a source-grid stroke width into raster texels. Fractional widths\n * let coverage shaders draw a border within a texel. The ceiling bounds the\n * neighbourhood the shaders scan.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {\n const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;\n return Math.min(strokeWidth * scale, MAX_ID_MASK_STROKE_WIDTH);\n }\n function createScaledMaskAxes(frame) {\n return {\n columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),\n rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),\n stride: frame.width,\n };\n }\n /** Rows hold their destination offset, so writing a run is two lookups. */\n function createMaskAxisMap(sourceLength, targetLength, stride) {\n const map = new Int32Array(sourceLength);\n const scale = targetLength / sourceLength;\n for (let index = 0; index < sourceLength; index += 1) {\n map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;\n }\n return map;\n }\n /**\n * The palette slot a detection index names, or undefined when the palette has\n * no slot for it. Slot zero is the background, so the ids start at one and the\n * last detection the palette can name sits one index below the last slot.\n */\n function resolveIdMaskPaletteId(detectionIndex) {\n const detectionMaskId = detectionIndex + 1;\n return detectionMaskId > 0 && detectionMaskId < MAX_ID_MASK_PALETTE_ENTRIES\n ? detectionMaskId\n : undefined;\n }\n function writeIdMaskPaletteEntry(palette, detectionMaskId, color, alpha) {\n const offset = detectionMaskId * 4;\n palette[offset] = ((color >> 16) & 0xff) / 255;\n palette[offset + 1] = ((color >> 8) & 0xff) / 255;\n palette[offset + 2] = (color & 0xff) / 255;\n palette[offset + 3] = Math.max(0, Math.min(alpha, 1));\n }\n new Set(Object.values(DepthPreviewLevels));\n new Set(Object.values(DepthMapKind));\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read, or was refused before any read. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, and a gate that stops one has to bound its own wait\n * or a producer nothing answers for never runs again.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n /**\n * The picture is waiting on media the source has not handed over yet, which\n * is the bytes and their decode rather than anything downstream of them.\n * `PlaybackBuffering` is what a transport reports once it has already\n * stopped; this is reported from the read itself, including the reads a seek\n * makes while the transport still reads as paused.\n */\n MediaSessionActivityKind[\"MediaSourceReading\"] = \"mediaSourceReading\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n /**\n * The render-preparation gate held the picture for artifacts that never\n * arrived and let it go. Nothing is blocked: the frames reaching the screen\n * are the ones whose artifacts were given up on. What is waited on is\n * preparation finishing another frame, which is what lets the gate hold the\n * picture again; `RenderPreparing` covers the holds that are running.\n */\n MediaSessionActivityKind[\"RenderPreparationAbandoned\"] = \"renderPreparationAbandoned\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n /** Builds compact, independent alpha crops so overlapping masks stay exact. */\n function createRegionMaskCoverageFrame(instructions) {\n const entries = [];\n for (const instruction of instructions) {\n if (!instruction.regionCoverageMask) {\n continue;\n }\n const entry = cropCoverageMask(instruction.detectionIndex, decodeCompressedRleMask(instruction.regionCoverageMask));\n if (entry) {\n entries.push(entry);\n }\n }\n return entries.length > 0 ? { entries } : undefined;\n }\n function cropCoverageMask(detectionIndex, decodedMask) {\n let minX = decodedMask.width;\n let minY = decodedMask.height;\n let maxX = -1;\n let maxY = -1;\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n if (maxX < minX || maxY < minY) {\n return undefined;\n }\n const width = maxX - minX + 1;\n const height = maxY - minY + 1;\n const data = new Uint8Array(new ArrayBuffer(width * height));\n for (let y = minY; y <= maxY; y += 1) {\n for (let x = minX; x <= maxX; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n data[(y - minY) * width + (x - minX)] = 255;\n }\n }\n return { data, detectionIndex, height, width, x: minX, y: minY };\n }\n function compositeMaskFrame(instructions) {\n const maskInstructions = materializeMaskInstructions(instructions);\n if (maskInstructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));\n const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));\n const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));\n for (const instruction of maskInstructions) {\n compositeInstruction(data, width, instruction);\n }\n return { data, height, width };\n }\n function createIdMaskRasterFrame(instructions, maxRasterWidth) {\n try {\n return createIdMaskFrame(materializeMaskInstructions(instructions).map((instruction) => instruction.stroke?.widthUnit === \"screen\"\n ? { ...instruction, stroke: undefined }\n : instruction), {\n maxWidth: maxRasterWidth,\n });\n }\n catch {\n // The id raster is the fast path, not the only one: answering with nothing\n // puts the caller on the RGBA composite, which draws the same picture.\n return undefined;\n }\n }\n /**\n * Whether the palette has a slot for every detection this frame masks. An id\n * names a detection by its index in the frame, so one masked detection past the\n * last slot leaves the whole frame without an id raster however few masks it\n * carries.\n */\n function canIdMaskPaletteNameFrame(instructions) {\n return instructions.every((instruction) => instruction.visible === false ||\n resolveIdMaskPaletteId(instruction.detectionIndex) !== undefined);\n }\n function createIdMaskPlane(instructions, maxRasterWidth) {\n // The plane carries the ids a failed cook could not. A frame the palette\n // cannot name has none to carry, and cooking it again reaches the same\n // refusal after rasterizing every mask a second time.\n if (!canIdMaskPaletteNameFrame(instructions)) {\n return undefined;\n }\n const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);\n return frame\n ? { data: frame.data, height: frame.height, width: frame.width }\n : undefined;\n }\n function compositeInstruction(rgba, canvasWidth, instruction) {\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);\n if (instruction.stroke &&\n instruction.stroke.widthUnit !== \"screen\" &&\n bounds) {\n compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);\n }\n }\n function createMaskScreenStrokes(instructions, width, height, preparedRaster) {\n const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&\n stroke?.widthUnit === \"screen\" &&\n stroke.width > 0 &&\n stroke.alpha > 0);\n if (outlined.length === 0)\n return undefined;\n const masks = materializeMaskInstructions(instructions);\n const raster = preparedRaster ?? new Uint32Array(width * height);\n if (!preparedRaster) {\n for (const { detectionIndex, mask } of masks) {\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const length = counts[index];\n if (index % 2 === 1) {\n for (let pixel = offset; pixel < offset + length; pixel += 1) {\n const x = Math.floor(pixel / mask.height);\n const y = pixel % mask.height;\n raster[y * width + x] = detectionIndex + 1;\n }\n }\n offset += length;\n }\n }\n }\n const paths = traceMaskOutlinePaths(raster, width, height, new Set(outlined.map(({ detectionIndex }) => detectionIndex + 1)));\n return outlined.map(({ detectionIndex, stroke }) => ({\n detectionIndex,\n alpha: stroke.alpha,\n color: stroke.color,\n width: stroke.width,\n paths: paths.get(detectionIndex + 1) ?? [],\n }));\n }\n function materializeMaskInstructions(instructions) {\n return instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => {\n if (instruction.mask) {\n return instruction;\n }\n const { height, points, width } = instruction.polygon;\n return {\n alpha: instruction.alpha,\n color: instruction.color,\n detectionIndex: instruction.detectionIndex,\n mask: encodeBinaryMask(rasterizePolygonToMask(points, { height, width }), width, height),\n stroke: instruction.stroke,\n };\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function compositeMaskFill(rgba, canvasWidth, mask, fill) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n const maskArea = maskWidth * maskHeight;\n let maskOffset = 0;\n let minX = maskWidth;\n let minY = maskHeight;\n let maxX = -1;\n let maxY = -1;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let columnX = Math.floor(maskOffset / maskHeight);\n let columnY = maskOffset - columnX * maskHeight;\n for (let step = 0; step < runLength; step += 1) {\n // A run can outlast the columns the mask has; the pixel it then names\n // is wherever that column-major offset lands when read back row-major.\n const rowMajorOffset = columnY * maskWidth + columnX;\n if (rowMajorOffset < maskArea) {\n const x = rowMajorOffset % maskWidth;\n const y = (rowMajorOffset - x) / maskWidth;\n writePixel(rgba, canvasWidth, x, y, fill);\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n columnY += 1;\n if (columnY === maskHeight) {\n columnY = 0;\n columnX += 1;\n }\n }\n maskOffset += runLength;\n }\n return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {\n const width = Math.round(stroke.width);\n if (width <= 0) {\n return;\n }\n const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);\n for (let y = bounds.minY; y <= bounds.maxY; y += 1) {\n for (let x = bounds.minX; x <= bounds.maxX; x += 1) {\n if (!isMaskPixel(decodedMask, x, y) ||\n !isBoundaryPixel(decodedMask, x, y)) {\n continue;\n }\n for (let offsetY = -width; offsetY <= width; offsetY += 1) {\n for (let offsetX = -width; offsetX <= width; offsetX += 1) {\n const strokeX = x + offsetX;\n const strokeY = y + offsetY;\n if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||\n isMaskPixel(decodedMask, strokeX, strokeY)) {\n continue;\n }\n writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);\n }\n }\n }\n }\n }\n function isBoundaryPixel(mask, x, y) {\n for (let offsetY = -1; offsetY <= 1; offsetY += 1) {\n for (let offsetX = -1; offsetX <= 1; offsetX += 1) {\n if (offsetX === 0 && offsetY === 0) {\n continue;\n }\n const neighborX = x + offsetX;\n const neighborY = y + offsetY;\n if (isOutsideMaskBounds(mask, neighborX, neighborY) ||\n !isMaskPixel(mask, neighborX, neighborY)) {\n return true;\n }\n }\n }\n return false;\n }\n function isMaskPixel(mask, x, y) {\n return mask.data[y * mask.width + x] === 1;\n }\n function isOutsideMaskBounds(mask, x, y) {\n return x < 0 || y < 0 || x >= mask.width || y >= mask.height;\n }\n function resolveRgbaColor(color, alpha) {\n return {\n alpha: Math.round(Math.max(0, Math.min(alpha, 1)) * 255),\n blue: color & 0xff,\n green: (color >> 8) & 0xff,\n red: (color >> 16) & 0xff,\n };\n }\n function writePixel(rgba, canvasWidth, x, y, color) {\n const rgbaOffset = (y * canvasWidth + x) * 4;\n rgba[rgbaOffset] = color.red;\n rgba[rgbaOffset + 1] = color.green;\n rgba[rgbaOffset + 2] = color.blue;\n rgba[rgbaOffset + 3] = color.alpha;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"IdMask\"] = \"idMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\n /**\n * How an id raster is laid out for the texture it is uploaded into. WebGPU\n * takes any bytesPerRow, so ids go up one byte per pixel. WebGL aligns every\n * uploaded row to four bytes and rejects a single-channel upload whose width\n * is not a multiple of four, which is what the four-channel layout is for.\n */\n var IdMaskTextureFormat;\n (function (IdMaskTextureFormat) {\n IdMaskTextureFormat[\"R8\"] = \"r8unorm\";\n IdMaskTextureFormat[\"Rgba8\"] = \"rgba8unorm\";\n })(IdMaskTextureFormat || (IdMaskTextureFormat = {}));\n\n const DEFAULT_STOPS = [\n { position: 0, color: 0xffd600 },\n { position: 0.35, color: 0xffd600 },\n { position: 0.7, color: 0xff7a00 },\n { position: 1, color: 0xdc1e1e },\n ];\n const MAX_HEATMAP_PIXELS = 16_777_216;\n const MAX_HEATMAP_SIDE = 8_192;\n /** Turns semantic scores into an RGBA raster; no palette enters cold data. */\n function colorizeHeatmap(map, renderer) {\n const { bounds, width, height, values } = map;\n if (!Number.isFinite(bounds?.x) ||\n !Number.isFinite(bounds?.y) ||\n !Number.isFinite(bounds?.width) ||\n !Number.isFinite(bounds?.height) ||\n bounds.width <= 0 ||\n bounds.height <= 0 ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width <= 0 ||\n height <= 0 ||\n width > MAX_HEATMAP_SIDE ||\n height > MAX_HEATMAP_SIDE ||\n width * height > MAX_HEATMAP_PIXELS ||\n !(Array.isArray(values) ||\n values instanceof Float32Array ||\n values instanceof Float64Array ||\n values instanceof Uint16Array) ||\n values.length !== width * height) {\n throw new RangeError(\"Heatmap bounds and dimensions must be valid, match row-major values, and fit the raster limit.\");\n }\n const scale = map.valueScale ?? 1;\n const threshold = (map.threshold ?? 0) * (renderer.thresholdScale ?? 1);\n const maximumScore = renderer.maximumScore ?? 1;\n const opacity = clamp01(renderer.opacity ?? 1);\n const minimumAlpha = clamp01(renderer.minimumAlpha ?? 0);\n const stops = renderer.colorStops ?? DEFAULT_STOPS;\n if (!Number.isFinite(scale) ||\n scale <= 0 ||\n !Number.isFinite(threshold) ||\n !Number.isFinite(maximumScore) ||\n maximumScore <= 0 ||\n !Number.isFinite(renderer.thresholdScale ?? 1) ||\n (renderer.thresholdScale ?? 1) < 0 ||\n !Number.isFinite(renderer.opacity ?? 1) ||\n !Number.isFinite(renderer.minimumAlpha ?? 0) ||\n stops.length === 0 ||\n stops.some((stop, index) => !Number.isFinite(stop.position) ||\n stop.position < 0 ||\n stop.position > 1 ||\n !Number.isInteger(stop.color) ||\n stop.color < 0 ||\n stop.color > 0xffffff ||\n (index > 0 && stop.position < stops[index - 1].position))) {\n throw new RangeError(\"Invalid heatmap scale, threshold, or color stops.\");\n }\n const rgba = new Uint8ClampedArray(new ArrayBuffer(values.length * 4));\n if (maximumScore <= threshold || opacity === 0)\n return rgba;\n for (let index = 0; index < values.length; index += 1) {\n const score = values[index] * scale;\n if (!Number.isFinite(score) || score <= threshold)\n continue;\n const color = interpolateColor(stops, clamp01(score / maximumScore));\n const alphaIntensity = clamp01((score - threshold) / (maximumScore - threshold));\n const offset = index * 4;\n rgba[offset] = color >> 16;\n rgba[offset + 1] = (color >> 8) & 0xff;\n rgba[offset + 2] = color & 0xff;\n rgba[offset + 3] = Math.round(255 * opacity * (minimumAlpha + (1 - minimumAlpha) * alphaIntensity));\n }\n return rgba;\n }\n function interpolateColor(stops, value) {\n const upperIndex = stops.findIndex((stop) => stop.position >= value);\n if (upperIndex === -1)\n return stops[stops.length - 1].color;\n if (upperIndex === 0)\n return stops[0].color;\n const left = stops[upperIndex - 1];\n const right = stops[upperIndex];\n const span = right.position - left.position;\n const ratio = span === 0 ? 1 : (value - left.position) / span;\n const channel = (shift) => Math.round(((left.color >> shift) & 0xff) * (1 - ratio) +\n ((right.color >> shift) & 0xff) * ratio);\n return (channel(16) << 16) | (channel(8) << 8) | channel(0);\n }\n function clamp01(value) {\n return Math.max(0, Math.min(1, value));\n }\n\n var HeatmapPreparationWorkerMessageType;\n (function (HeatmapPreparationWorkerMessageType) {\n HeatmapPreparationWorkerMessageType[\"Prepare\"] = \"heatmap-prepare\";\n HeatmapPreparationWorkerMessageType[\"Complete\"] = \"heatmap-complete\";\n HeatmapPreparationWorkerMessageType[\"Error\"] = \"heatmap-error\";\n })(HeatmapPreparationWorkerMessageType || (HeatmapPreparationWorkerMessageType = {}));\n\n var MaskPreparationWorkerMessageType;\n (function (MaskPreparationWorkerMessageType) {\n MaskPreparationWorkerMessageType[\"Complete\"] = \"complete\";\n MaskPreparationWorkerMessageType[\"Empty\"] = \"empty\";\n MaskPreparationWorkerMessageType[\"Error\"] = \"error\";\n MaskPreparationWorkerMessageType[\"Prepare\"] = \"prepare\";\n })(MaskPreparationWorkerMessageType || (MaskPreparationWorkerMessageType = {}));\n\n /**\n * Messages between the depth source and the render-preparation worker, which\n * decodes depth and confidence PNGs, and copies decoded preview frames' luma,\n * off the main thread. Internal: the worker script is a deployment asset, and\n * its protocol is not public.\n */\n var DepthPreparationWorkerMessageType;\n (function (DepthPreparationWorkerMessageType) {\n DepthPreparationWorkerMessageType[\"Decode\"] = \"depth-decode\";\n DepthPreparationWorkerMessageType[\"Complete\"] = \"depth-decode-complete\";\n DepthPreparationWorkerMessageType[\"Error\"] = \"depth-decode-error\";\n DepthPreparationWorkerMessageType[\"PreviewLuma\"] = \"depth-preview-luma\";\n DepthPreparationWorkerMessageType[\"PreviewLumaComplete\"] = \"depth-preview-luma-complete\";\n })(DepthPreparationWorkerMessageType || (DepthPreparationWorkerMessageType = {}));\n\n /*\n * Grayscale PNG decoding for depth maps and their confidence planes.\n *\n * Every image path a browser offers (<img>, createImageBitmap, ImageDecoder,\n * canvas) hands back 8 bits per channel, so a 16-bit depth PNG is decoded\n * here: the browser's own zlib inflates the IDAT stream, and the row filters\n * are undone in JavaScript.\n *\n * This module imports nothing, so the render-preparation worker, the main\n * thread and the depth benchmark all run the same code.\n */\n const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];\n const COLOR_TYPE_GRAY = 0;\n /** 16384 x 16384, beyond any GPU texture; a header claiming more is refused. */\n const MAX_PNG_PIXELS = 16_384 * 16_384;\n async function decodePng16(bytes, options = {}) {\n const header = readPngHeader(bytes, 16);\n const rowBytes = header.width * 2 + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint16Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray16(raw, header.width, header.height, values);\n return {\n height: header.height,\n paddedUpload: options.padRowsForWebGl && header.width % 2 === 1\n ? padRows(values, header.width, header.height)\n : undefined,\n values,\n width: header.width,\n };\n }\n async function decodePng8Gray(bytes, options = {}) {\n const header = readPngHeader(bytes, 8);\n const rowBytes = header.width + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint8Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray8(raw, header.width, header.height, values);\n return { height: header.height, values, width: header.width };\n }\n /**\n * Inflates with the platform's `DecompressionStream(\"deflate\")` (zlib\n * framing, which is what PNG uses) straight into one buffer of the expected\n * size, so no second copy joins the output chunks.\n */\n async function inflateWithDecompressionStream(chunks, byteLength) {\n const stream = new DecompressionStream(\"deflate\");\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n // Writers split IDAT into many small chunks (libpng's default is 8 KiB).\n // One write of the joined stream costs a copy of the compressed bytes and\n // saves a stream round trip per chunk.\n const zlib = chunks.length === 1 ? chunks[0] : joinChunks(chunks);\n const writing = (async () => {\n await writer.write(zlib);\n await writer.close();\n })();\n const output = new Uint8Array(byteLength);\n let offset = 0;\n // A corrupt stream rejects both sides; the reader's error is the one thrown.\n writing.catch(() => undefined);\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done)\n break;\n if (offset + value.byteLength > byteLength) {\n throw new RangeError(\"PNG image data is longer than its header says.\");\n }\n output.set(value, offset);\n offset += value.byteLength;\n }\n await writing;\n }\n catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error instanceof RangeError\n ? error\n : new RangeError(`PNG image data does not inflate: ${error instanceof Error ? error.message : String(error)}`);\n }\n return offset === byteLength ? output : output.subarray(0, offset);\n }\n function joinChunks(chunks) {\n const joined = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n joined.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return joined;\n }\n function readPngHeader(input, bitDepth) {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n if (bytes.byteLength < 8 ||\n PNG_SIGNATURE.some((value, index) => bytes[index] !== value)) {\n throw new RangeError(\"Not a PNG file.\");\n }\n let offset = 8;\n let width = 0;\n let height = 0;\n let sawHeader = false;\n let sawEnd = false;\n const idat = [];\n while (offset + 12 <= bytes.byteLength) {\n const length = view.getUint32(offset);\n const type = view.getUint32(offset + 4);\n const dataStart = offset + 8;\n const dataEnd = dataStart + length;\n if (dataEnd + 4 > bytes.byteLength) {\n throw new RangeError(\"PNG file is truncated.\");\n }\n if (type === 0x49484452) {\n // IHDR\n if (length !== 13)\n throw new RangeError(\"PNG header is malformed.\");\n width = view.getUint32(dataStart);\n height = view.getUint32(dataStart + 4);\n const depth = bytes[dataStart + 8];\n const colorType = bytes[dataStart + 9];\n if (bytes[dataStart + 10] !== 0 || bytes[dataStart + 11] !== 0) {\n throw new RangeError(\"PNG uses an unknown compression or filter.\");\n }\n if (bytes[dataStart + 12] !== 0) {\n throw new RangeError(\"Interlaced PNGs are not supported.\");\n }\n if (colorType !== COLOR_TYPE_GRAY || depth !== bitDepth) {\n throw new RangeError(`Expected a ${bitDepth}-bit grayscale PNG, got bit depth ${depth} with colour type ${colorType}.`);\n }\n sawHeader = true;\n }\n else if (type === 0x49444154) {\n // IDAT\n idat.push(bytes.subarray(dataStart, dataEnd));\n }\n else if (type === 0x49454e44) {\n // IEND\n sawEnd = true;\n break;\n }\n offset = dataEnd + 4;\n }\n if (!sawHeader)\n throw new RangeError(\"PNG has no header.\");\n if (!sawEnd)\n throw new RangeError(\"PNG file is truncated.\");\n if (width === 0 || height === 0 || width * height > MAX_PNG_PIXELS) {\n throw new RangeError(`PNG size ${width}x${height} is not supported.`);\n }\n if (idat.length === 0)\n throw new RangeError(\"PNG has no image data.\");\n return { height, idat, width };\n }\n function checkInflatedLength(raw, expected) {\n if (raw.byteLength !== expected) {\n throw new RangeError(\"PNG image data is shorter than its header says.\");\n }\n }\n /*\n * Undoes the row filters of a 16-bit grayscale image and writes host-order\n * samples, in one pass. Each filter runs on bytes, and the two bytes of a\n * sample never carry into each other, so the loops keep a sample's high and\n * low bytes apart and join them only for the store. The previous row is read\n * back from the output, which already holds it unfiltered; the first row reads\n * a row of zeros, which is what PNG defines above the image.\n *\n * Each filter is its own small function with one loop, called once per row,\n * so engines compile each loop on its own: SpiderMonkey slowed a Paeth loop\n * that shared a function with the other filters.\n */\n function unfilterGray16(raw, width, height, out) {\n const rowBytes = width * 2 + 1;\n const zeroRow = new Uint16Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_16[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow16(raw, s, out, o, end) {\n for (let i = o; i < end; i += 1, s += 2) {\n out[i] = (raw[s] << 8) | raw[s + 1];\n }\n }\n function subRow16(raw, s, out, o, end) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2) {\n hi = (raw[s] + hi) & 0xff;\n lo = (raw[s + 1] + lo) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n function upRow16(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n out[i] = (((raw[s] + (up >> 8)) & 0xff) << 8) | ((raw[s + 1] + up) & 0xff);\n }\n }\n function averageRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n hi = (raw[s] + ((hi + (up >> 8)) >> 1)) & 0xff;\n lo = (raw[s + 1] + ((lo + (up & 0xff)) >> 1)) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n /** Paeth: per byte, whichever of left, up and up-left is closest to left + up - up-left. */\n function paethRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n let upLeftHi = 0;\n let upLeftLo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n const upHi = up >> 8;\n const upLo = up & 0xff;\n let pa = Math.abs(upHi - upLeftHi);\n let pb = Math.abs(hi - upLeftHi);\n let pc = Math.abs(hi + upHi - upLeftHi - upLeftHi);\n hi =\n (raw[s] + (pa <= pb && pa <= pc ? hi : pb <= pc ? upHi : upLeftHi)) &\n 0xff;\n pa = Math.abs(upLo - upLeftLo);\n pb = Math.abs(lo - upLeftLo);\n pc = Math.abs(lo + upLo - upLeftLo - upLeftLo);\n lo =\n (raw[s + 1] + (pa <= pb && pa <= pc ? lo : pb <= pc ? upLo : upLeftLo)) &\n 0xff;\n out[i] = (hi << 8) | lo;\n upLeftHi = upHi;\n upLeftLo = upLo;\n }\n }\n /** Indexed by the PNG filter type byte: None, Sub, Up, Average, Paeth. */\n const ROW_FILTERS_16 = [\n noneRow16,\n subRow16,\n upRow16,\n averageRow16,\n paethRow16,\n ];\n function unfilterGray8(raw, width, height, out) {\n const rowBytes = width + 1;\n const zeroRow = new Uint8Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_8[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow8(raw, s, out, o, end) {\n out.set(raw.subarray(s, s + end - o), o);\n }\n function subRow8(raw, s, out, o, end) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1) {\n left = (raw[s] + left) & 0xff;\n out[i] = left;\n }\n }\n function upRow8(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n out[i] = (raw[s] + prev[p]) & 0xff;\n }\n }\n function averageRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n left = (raw[s] + ((left + prev[p]) >> 1)) & 0xff;\n out[i] = left;\n }\n }\n function paethRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n let upLeft = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n const up = prev[p];\n const pa = Math.abs(up - upLeft);\n const pb = Math.abs(left - upLeft);\n const pc = Math.abs(left + up - upLeft - upLeft);\n left =\n (raw[s] + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft)) & 0xff;\n out[i] = left;\n upLeft = up;\n }\n }\n const ROW_FILTERS_8 = [\n noneRow8,\n subRow8,\n upRow8,\n averageRow8,\n paethRow8,\n ];\n /**\n * WebGL unpacks rows on four-byte boundaries, so an odd width of two-byte\n * texels needs one padding texel per row.\n */\n function padRows(values, width, height) {\n const textureWidth = width + 1;\n const padded = new Uint16Array(textureWidth * height);\n for (let y = 0; y < height; y += 1) {\n padded.set(values.subarray(y * width, (y + 1) * width), y * textureWidth);\n }\n return { bytes: new Uint8Array(padded.buffer), textureWidth };\n }\n\n /**\n * Answers one decode request, in the worker or on the main thread: the reply\n * and the buffers it can transfer. A file that does not decode is an error\n * reply, never a thrown exception, so the caller can tell a bad file from a\n * broken worker.\n */\n async function decodeDepthPreparationRequest(request) {\n const requestId = request.requestId ?? 0;\n try {\n if (request.bitDepth === 8) {\n const decoded = await decodePng8Gray(request.bytes);\n return {\n response: {\n bitDepth: 8,\n height: decoded.height,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer: [decoded.values.buffer],\n };\n }\n const decoded = await decodePng16(request.bytes, {\n padRowsForWebGl: request.padRowsForWebGl,\n });\n const padded = decoded.paddedUpload;\n const decimated = request.decimateBy !== undefined && request.decimateBy >= 2\n ? decimateDepthUpload(decoded.values, decoded.width, decoded.height, Math.floor(request.decimateBy), request.padRowsForWebGl === true)\n : null;\n const transfer = [decoded.values.buffer];\n if (padded)\n transfer.push(padded.bytes.buffer);\n if (decimated)\n transfer.push(decimated.bytes.buffer);\n return {\n response: {\n bitDepth: 16,\n decimatedUpload: decimated\n ? { ...decimated, bytes: decimated.bytes.buffer }\n : undefined,\n height: decoded.height,\n paddedUpload: padded\n ? {\n bytes: padded.bytes.buffer,\n textureWidth: padded.textureWidth,\n }\n : undefined,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer,\n };\n }\n catch (error) {\n return {\n response: {\n error: error instanceof Error ? error.message : \"Unable to decode depth.\",\n requestId,\n type: DepthPreparationWorkerMessageType.Error,\n },\n transfer: [],\n };\n }\n }\n /**\n * Depth for a texture `factor` times smaller on each side, each texel the\n * sample at its block's centre, as the renderer decimates a map larger than\n * the GPU allows.\n */\n function decimateDepthUpload(values, width, height, factor, padRowsForWebGl) {\n if (factor < 2)\n return null;\n const targetWidth = Math.ceil(width / factor);\n const targetHeight = Math.ceil(height / factor);\n // WebGL reads two-byte texels in rows of four bytes.\n const textureWidth = padRowsForWebGl\n ? targetWidth + (targetWidth % 2)\n : targetWidth;\n const texels = new Uint16Array(textureWidth * targetHeight);\n const columns = new Uint32Array(targetWidth);\n for (let x = 0; x < targetWidth; x += 1) {\n columns[x] = Math.min(width - 1, Math.floor(((x + 0.5) * width) / targetWidth));\n }\n for (let y = 0; y < targetHeight; y += 1) {\n const row = Math.min(height - 1, Math.floor(((y + 0.5) * height) / targetHeight)) *\n width;\n const target = y * textureWidth;\n for (let x = 0; x < targetWidth; x += 1) {\n texels[target + x] = values[row + columns[x]];\n }\n }\n return {\n bytes: new Uint8Array(texels.buffer),\n height: targetHeight,\n textureWidth,\n width: targetWidth,\n };\n }\n\n /** Formats whose first plane is 8-bit luma, which is the code as written. */\n const PLANAR_8_BIT_FORMATS = new Set([\n \"I420\",\n \"I420A\",\n \"I422\",\n \"I422A\",\n \"I444\",\n \"I444A\",\n \"NV12\",\n ]);\n const RGB_FORMATS = new Set([\"RGBA\", \"RGBX\", \"BGRA\", \"BGRX\"]);\n /**\n * Copies a decoded preview frame's luma codes out of it, mapped through\n * `correction` when one is given. The frame stays open; its owner closes\n * it.\n */\n async function readVideoFrameLuma(frame, scratch, correction = null) {\n const rect = frame.visibleRect ?? {\n height: frame.codedHeight,\n width: frame.codedWidth,\n x: 0,\n y: 0,\n };\n const { width, height } = rect;\n const format = frame.format;\n const planar = format !== null && PLANAR_8_BIT_FORMATS.has(format);\n let bytes;\n let plane;\n if (planar || (format !== null && RGB_FORMATS.has(format))) {\n const size = frame.allocationSize({ rect });\n if (scratch.buffer.byteLength < size)\n scratch.buffer = new ArrayBuffer(size);\n [plane] = await frame.copyTo(scratch.buffer, { rect });\n bytes = new Uint8Array(scratch.buffer);\n }\n else {\n // A frame in a layout no copy can read is drawn to a canvas, which\n // converts it to RGB as the browser sees fit.\n bytes = drawFrameRgba(frame, width, height);\n }\n const luma = new Uint8Array(width * height);\n const stride = plane?.stride ?? (planar ? width : width * 4);\n const offset = plane?.offset ?? 0;\n if (!planar) {\n // Every RGB layout keeps green second, and luma with neutral chroma\n // converts to three equal channels.\n for (let y = 0; y < height; y += 1) {\n const row = offset + y * stride + 1;\n const target = y * width;\n for (let x = 0; x < width; x += 1) {\n luma[target + x] = bytes[row + x * 4];\n }\n }\n }\n else if (stride === width) {\n luma.set(bytes.subarray(offset, offset + width * height));\n }\n else {\n for (let y = 0; y < height; y += 1) {\n luma.set(bytes.subarray(offset + y * stride, offset + y * stride + width), y * width);\n }\n }\n if (correction) {\n for (let index = 0; index < luma.length; index += 1) {\n luma[index] = correction[luma[index]];\n }\n }\n return { height, luma, path: planar ? \"plane\" : \"rgb\", width };\n }\n function drawFrameRgba(frame, width, height) {\n const canvas = new OffscreenCanvas(width, height);\n const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!context) {\n throw new Error(\"The depth preview needs a 2D canvas to read this frame.\");\n }\n context.drawImage(frame, 0, 0, width, height);\n const { data } = context.getImageData(0, 0, width, height);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n\n const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type === HeatmapPreparationWorkerMessageType.Prepare) {\n prepareHeatmap(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.Decode) {\n void decodeDepthFrame(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.PreviewLuma) {\n void copyPreviewLuma(message);\n return;\n }\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\n });\n function prepareHeatmap(message) {\n try {\n const data = colorizeHeatmap(message.map, message.renderer);\n const imageData = new ImageData(data, message.map.width, message.map.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n if (imageBitmap) {\n workerScope.postMessage({\n imageBitmap,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageBitmap]);\n return;\n }\n workerScope.postMessage({\n imageData,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error ? error.message : \"Unable to prepare heatmap.\",\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Error,\n });\n }\n }\n /** Decoding waits on the platform's inflate, so replies arrive out of order. */\n async function decodeDepthFrame(message) {\n const { response, transfer } = await decodeDepthPreparationRequest(message);\n workerScope.postMessage(response, transfer);\n }\n const previewLumaScratch = {\n buffer: new ArrayBuffer(0),\n };\n /** Frames arrive in order and are copied in order, through one scratch. */\n let previewLumaQueue = Promise.resolve();\n function copyPreviewLuma(message) {\n previewLumaQueue = previewLumaQueue.then(async () => {\n try {\n const copied = await readVideoFrameLuma(message.frame, previewLumaScratch, message.correction);\n workerScope.postMessage({\n height: copied.height,\n luma: copied.luma.buffer,\n path: copied.path,\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.PreviewLumaComplete,\n width: copied.width,\n }, [copied.luma.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to copy the depth preview frame.\",\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.Error,\n });\n }\n finally {\n message.frame.close();\n }\n });\n return previewLumaQueue;\n }\n function prepareMaskFrame(message) {\n try {\n const regionMaskCoverage = createRegionMaskCoverageFrame(message.job.instructions);\n const coverageTransfers = getRegionMaskCoverageTransfers(regionMaskCoverage);\n const idMaskFrame = createIdMaskRasterFrame(message.job.instructions, message.job.maxRasterWidth);\n if (idMaskFrame) {\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, idMaskFrame.width, idMaskFrame.height, idMaskFrame.data);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ?? [];\n workerScope.postMessage({\n artifactKind: PreparedMaskFrameKind.IdMask,\n fillPalette: idMaskFrame.fillPalette,\n hasStroke: idMaskFrame.hasStroke,\n height: idMaskFrame.height,\n key: message.job.key,\n maxStrokeWidth: idMaskFrame.maxStrokeWidth,\n raster: idMaskFrame.data,\n screenStrokes,\n regionMaskCoverage,\n requestId: message.requestId,\n sourceWidth: idMaskFrame.sourceWidth,\n strokePalette: idMaskFrame.strokePalette,\n strokeWidths: idMaskFrame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n width: idMaskFrame.width,\n }, [\n idMaskFrame.data.buffer,\n idMaskFrame.fillPalette.buffer,\n idMaskFrame.strokePalette.buffer,\n idMaskFrame.strokeWidths.buffer,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions);\n if (!compositedFrame && !regionMaskCoverage) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const preparedPixels = compositedFrame ?? createTransparentCoverageCarrier();\n const imageData = new ImageData(preparedPixels.data, preparedPixels.width, preparedPixels.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskPlane = createIdMaskPlane(message.job.instructions, message.job.maxRasterWidth);\n const idMaskTransfers = idMaskPlane ? [idMaskPlane.data.buffer] : [];\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, preparedPixels.width, preparedPixels.height);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ??\n [];\n if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageBitmap,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageData.data.buffer,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to prepare mask frame.\",\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Error,\n });\n }\n }\n /**\n * A frame that carries only region coverage has nothing to composite, and the\n * RGBA branch still has to produce an artifact for the coverage to ride on.\n */\n function createTransparentCoverageCarrier() {\n return {\n data: new Uint8ClampedArray(new ArrayBuffer(4)),\n height: 1,\n width: 1,\n };\n }\n function getRegionMaskCoverageTransfers(coverage) {\n return coverage?.entries.map(({ data }) => data.buffer) ?? [];\n }\n function createImageBitmapFromImageData(imageData) {\n if (typeof OffscreenCanvas === \"undefined\") {\n return null;\n }\n const canvas = new OffscreenCanvas(imageData.width, imageData.height);\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n context.putImageData(imageData, 0, 0);\n return canvas.transferToImageBitmap();\n }\n\n})();";
3116
+ const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** Trace closed pixel boundaries, retaining holes and disconnected components. */\n function traceMaskOutlinePaths(raster, width, height, ids) {\n const edgesById = new Map();\n const stride = width + 1;\n const steps = [1, stride, -1, -stride];\n const add = (id, vertex, direction) => {\n let edges = edgesById.get(id);\n if (!edges)\n edgesById.set(id, (edges = new Map()));\n const outgoing = edges.get(vertex);\n if (outgoing)\n outgoing.push(direction);\n else\n edges.set(vertex, [direction]);\n };\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const offset = y * width + x;\n const id = raster[offset];\n if (!ids.has(id))\n continue;\n const vertex = y * stride + x;\n if (y === 0 || raster[offset - width] !== id)\n add(id, vertex, 0);\n if (x === width - 1 || raster[offset + 1] !== id)\n add(id, vertex + 1, 1);\n if (y === height - 1 || raster[offset + width] !== id)\n add(id, vertex + stride + 1, 2);\n if (x === 0 || raster[offset - 1] !== id)\n add(id, vertex + stride, 3);\n }\n }\n const result = new Map();\n for (const [id, edges] of edgesById) {\n const paths = [];\n while (edges.size > 0) {\n const start = edges.keys().next().value;\n let vertex = start;\n let previousDirection = -1;\n let firstDirection = -1;\n const points = [];\n do {\n const outgoing = edges.get(vertex);\n // Turn right at diagonal contacts so separate islands stay separate.\n const direction = previousDirection < 0\n ? outgoing[0]\n : [1, 0, 3, 2]\n .map((turn) => (previousDirection + turn) % 4)\n .find((candidate) => outgoing.includes(candidate));\n if (firstDirection < 0)\n firstDirection = direction;\n if (direction !== previousDirection)\n points.push(vertex % stride, Math.floor(vertex / stride));\n outgoing.splice(outgoing.indexOf(direction), 1);\n if (outgoing.length === 0)\n edges.delete(vertex);\n vertex += steps[direction];\n previousDirection = direction;\n } while (vertex !== start);\n if (previousDirection === firstDirection)\n points.splice(0, 2);\n paths.push(new Float32Array(points));\n }\n result.set(id, paths);\n }\n return result;\n }\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n function decodeCompressedRleMask(mask) {\n if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {\n throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);\n }\n const data = new Uint8Array(mask.width * mask.height);\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n const isForeground = index % 2 === 1;\n if (isForeground) {\n for (let runOffset = 0; runOffset < runLength; runOffset += 1) {\n const maskOffset = offset + runOffset;\n const x = Math.floor(maskOffset / mask.height);\n const y = maskOffset % mask.height;\n const rowMajorOffset = y * mask.width + x;\n if (rowMajorOffset < data.length) {\n data[rowMajorOffset] = 1;\n }\n }\n }\n offset += runLength;\n }\n return {\n data,\n height: mask.height,\n width: mask.width,\n };\n }\n function decodeCompressedRleCounts(counts) {\n const decoded = [];\n let index = 0;\n while (index < counts.length) {\n let value = 0;\n let shift = 0;\n let charCode;\n do {\n charCode = counts.charCodeAt(index) - 48;\n index += 1;\n value |= (charCode & 0x1f) << shift;\n shift += 5;\n } while (charCode & 0x20);\n if (charCode & 0x10) {\n value |= -1 << shift;\n }\n if (decoded.length > 2) {\n value += decoded[decoded.length - 2] ?? 0;\n }\n decoded.push(value);\n }\n return decoded;\n }\n function encodeCompressedRleCounts(counts) {\n return counts\n .map((count, index) => {\n let value = index > 2 ? count - counts[index - 2] : count;\n let encoded = \"\";\n let more = true;\n while (more) {\n let charCode = value & 0x1f;\n value >>= 5;\n more = !((value === 0 && (charCode & 0x10) === 0) ||\n (value === -1 && (charCode & 0x10) !== 0));\n if (more) {\n charCode |= 0x20;\n }\n encoded += String.fromCharCode(charCode + 48);\n }\n return encoded;\n })\n .join(\"\");\n }\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n function encodeBinaryMask(data, width, height) {\n assertMaskDimensions(data, width, height);\n const runs = [];\n let currentValue = 0;\n let runLength = 0;\n for (let x = 0; x < width; x += 1) {\n for (let y = 0; y < height; y += 1) {\n const value = data[y * width + x] ? 1 : 0;\n if (value === currentValue) {\n runLength += 1;\n }\n else {\n runs.push(runLength);\n currentValue = value;\n runLength = 1;\n }\n }\n }\n runs.push(runLength);\n return {\n counts: encodeCompressedRleCounts(runs),\n encoding: DetectionMaskEncoding.CompressedRle,\n height,\n width,\n };\n }\n function assertMaskSize(width, height) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(\"Mask width must be a positive integer.\");\n }\n if (!Number.isInteger(height) || height <= 0) {\n throw new Error(\"Mask height must be a positive integer.\");\n }\n }\n function assertMaskDimensions(data, width, height) {\n assertMaskSize(width, height);\n if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\n\n function centerRectToTopLeftRect(rect) {\n return {\n height: rect.height,\n width: rect.width,\n x: rect.x - rect.width / 2,\n y: rect.y - rect.height / 2,\n };\n }\n function getPointsRect(points) {\n if (points.length === 0) {\n return undefined;\n }\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n minY = Math.min(minY, point.y);\n maxX = Math.max(maxX, point.x);\n maxY = Math.max(maxY, point.y);\n }\n return {\n height: maxY - minY,\n width: maxX - minX,\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n };\n }\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"OrientedBox\"] = \"orientedBox\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var PercentageBarPlacement;\n (function (PercentageBarPlacement) {\n PercentageBarPlacement[\"Top\"] = \"top\";\n PercentageBarPlacement[\"Bottom\"] = \"bottom\";\n PercentageBarPlacement[\"InsideTop\"] = \"insideTop\";\n PercentageBarPlacement[\"InsideBottom\"] = \"insideBottom\";\n })(PercentageBarPlacement || (PercentageBarPlacement = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n\n /**\n * What the stored values of a depth map measure.\n *\n * Every kind stores 0 where there is no depth.\n */\n const DepthMapKind = {\n /** Stereo disparity in pixels of the map. Larger is nearer. */\n DisparityPx: \"disparity_px\",\n /** Metric depth along the optical axis in metres. Smaller is nearer. */\n DepthM: \"depth_m\",\n /**\n * Unitless inverse depth up to an unknown scale and shift, as monocular\n * models produce it. Larger is nearer; it has no metric depth.\n */\n RelativeInverse: \"relative_inverse\",\n };\n /**\n * The quantity a depth renderer colours.\n *\n * Disparity is inverse depth, so it spends colour on near detail the way\n * stereo measures it. Either way the nearest end of the range is the warm or\n * bright end of the colour table.\n */\n const DepthQuantity = {\n Disparity: \"disparity\",\n Depth: \"depth\",\n };\n const DepthColormap = {\n Turbo: \"turbo\",\n Viridis: \"viridis\",\n Cividis: \"cividis\",\n Inferno: \"inferno\",\n Magma: \"magma\",\n Grayscale: \"grayscale\",\n };\n /** How a depth map is sampled where one map pixel does not cover one media pixel. */\n const DepthSampling = {\n /** Nearest when the map is at least media size, edge-aware otherwise. */\n Auto: \"auto\",\n Nearest: \"nearest\",\n /**\n * Bilinear over valid neighbours only, falling back to the nearest one\n * across a depth edge, so objects never bleed into the background.\n */\n EdgeAware: \"edge-aware\",\n };\n /**\n * The luma range a depth preview video's codes are written in.\n *\n * TV range is what browsers' hardware decoders return as written; a decoder\n * may squeeze full range into TV range or move codes converting it to RGB.\n */\n const DepthPreviewLevels = {\n /** Every code from 0 to 255; the top code is 255. */\n Full: \"full\",\n /** Limited range: black is 16 and the top code is 235. */\n Tv: \"tv\",\n };\n new Set(Object.values(DepthColormap));\n new Set(Object.values(DepthQuantity));\n new Set(Object.values(DepthSampling));\n function rasterizePolygonToMask(points, dimensions) {\n const data = createEmptyMask(dimensions);\n forEachRasterPolygonSpan(points, dimensions, (y, left, right) => {\n data.fill(1, y * dimensions.width + left, y * dimensions.width + right);\n });\n return data;\n }\n function forEachRasterPolygonSpan(points, dimensions, writeSpan) {\n assertMediaDimensions(dimensions);\n if (points.length < 3) {\n return;\n }\n const bounds = centerRectToTopLeftRect(getPointsRect(points));\n const startY = Math.max(0, Math.floor(bounds.y));\n const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));\n for (let y = startY; y <= endY; y += 1) {\n const scanY = y + 0.5;\n const intersections = [];\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index];\n const next = points[(index + 1) % points.length];\n if ((current.y <= scanY && next.y > scanY) ||\n (next.y <= scanY && current.y > scanY)) {\n const ratio = (scanY - current.y) / (next.y - current.y);\n intersections.push(current.x + ratio * (next.x - current.x));\n }\n }\n intersections.sort((left, right) => left - right);\n for (let index = 0; index < intersections.length - 1; index += 2) {\n const left = Math.max(0, Math.round(intersections[index]));\n const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));\n if (left < right) {\n writeSpan(y, left, right);\n }\n }\n }\n }\n function createEmptyMask(dimensions) {\n assertMediaDimensions(dimensions);\n return new Uint8Array(dimensions.width * dimensions.height);\n }\n function assertMediaDimensions(dimensions) {\n if (!Number.isInteger(dimensions.width) ||\n dimensions.width <= 0 ||\n !Number.isInteger(dimensions.height) ||\n dimensions.height <= 0) {\n throw new Error(\"Media dimensions must be positive integers.\");\n }\n }\n\n /**\n * Palette slots an id raster can name, one of them the background. A GLSL\n * fragment stage is guaranteed only 224 uniform vectors, and a palette entry\n * costs 2.5 of them, so this stays a multiple of four and well inside that\n * budget; past it a frame draws from the RGBA composite instead.\n */\n const MAX_ID_MASK_PALETTE_ENTRIES = 80;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions, options = {}) {\n if (instructions.length === 0) {\n return undefined;\n }\n /* Pixel-grid dimensions use integer parts throughout allocation and RLE\n traversal. */\n const maskWidth = Math.floor(Math.max(...instructions.map((instruction) => (instruction.mask ?? instruction.polygon).width)));\n const maskHeight = Math.floor(Math.max(...instructions.map((instruction) => (instruction.mask ?? instruction.polygon).height)));\n const width = resolveRasterWidth(maskWidth, options.maxWidth);\n const height = width === maskWidth\n ? maskHeight\n : Math.max(1, Math.round((width * maskHeight) / maskWidth));\n const scaledAxes = width === maskWidth\n ? undefined\n : createScaledMaskAxes({ height, maskHeight, maskWidth, width });\n const data = new Uint8Array(new ArrayBuffer(width * height));\n const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n const strokeAlignments = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n let hasStroke = false;\n let maxStrokeWidth = 0;\n for (const instruction of instructions) {\n const detectionMaskId = resolveIdMaskPaletteId(instruction.detectionIndex);\n if (detectionMaskId === undefined) {\n return undefined;\n }\n writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = instruction.stroke.width;\n const alignment = instruction.stroke.alignment === StrokeAlignment.Inside\n ? 1\n : instruction.stroke.alignment === StrokeAlignment.Center\n ? 0.5\n : 0;\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n strokeAlignments[detectionMaskId] = alignment;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth * (1 - alignment));\n writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n if (instruction.polygon) {\n writePolygonSpans(data, width, instruction.polygon, detectionMaskId, scaledAxes);\n }\n else if (scaledAxes) {\n writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);\n }\n else {\n writeMaskRuns(data, width, instruction.mask, detectionMaskId);\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n sourceWidth: maskWidth,\n strokePalette,\n strokeAlignments,\n strokeWidths,\n width,\n };\n }\n function writePolygonSpans(data, frameWidth, polygon, detectionMaskId, axes) {\n forEachRasterPolygonSpan(polygon.points, polygon, (y, left, right) => {\n const row = axes ? axes.rows[y] : y * frameWidth;\n const first = axes ? axes.columns[left] : left;\n const last = axes ? axes.columns[right - 1] + 1 : right;\n data.fill(detectionMaskId, row + first, row + last);\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let frameOffset = y * frameWidth + x;\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[frameOffset] = detectionMaskId;\n y += 1;\n frameOffset += frameWidth;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n frameOffset = x;\n }\n }\n maskOffset += runLength;\n }\n }\n /**\n * The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands\n * in, so a mask keeps every part of itself the smaller raster can hold and\n * gains up to a texel at its edges.\n */\n function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n const stride = axes.stride;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let remaining = runLength;\n /* A run walks down one column and wraps into the next, so it splits into\n column-sized segments. Within a segment the destination offset only ever\n steps by the row pitch or stays put: createMaskAxisMap builds\n `rows[i] = min(height - 1, floor(i * scale)) * width` with scale <= 1, so\n consecutive entries differ by exactly 0 or exactly `width`. Walking the\n destination directly therefore touches the same bytes the per-pixel loop\n touched, and drops the bounds test, the wrap test and one typed-array\n load from every source pixel. Where the scale collapses several source\n rows onto one destination row, this writes that byte once instead of\n once per source row. */\n while (remaining > 0 && x < maskWidth) {\n const span = Math.min(remaining, maskHeight - y);\n const column = axes.columns[x];\n const first = axes.rows[y];\n const last = axes.rows[y + span - 1];\n for (let offset = first; offset <= last; offset += stride) {\n data[offset + column] = detectionMaskId;\n }\n remaining -= span;\n y += span;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n }\n }\n maskOffset += runLength;\n }\n }\n function resolveRasterWidth(maskWidth, maxWidth) {\n return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth\n ? Math.max(1, Math.floor(maxWidth))\n : maskWidth;\n }\n /**\n * Converts total CSS outline width into raster texels. Each side is bounded\n * independently so centered outlines can use the full supported radius.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, displayWidth, rasterWidth, alignment = StrokeAlignment.Outside) {\n const scale = displayWidth > 0 ? rasterWidth / displayWidth : 1;\n const sideFraction = alignment === StrokeAlignment.Center ? 0.5 : 1;\n return Math.min(strokeWidth * scale, MAX_ID_MASK_STROKE_WIDTH / sideFraction);\n }\n function createScaledMaskAxes(frame) {\n return {\n columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),\n rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),\n stride: frame.width,\n };\n }\n /** Rows hold their destination offset, so writing a run is two lookups. */\n function createMaskAxisMap(sourceLength, targetLength, stride) {\n const map = new Int32Array(sourceLength);\n const scale = targetLength / sourceLength;\n for (let index = 0; index < sourceLength; index += 1) {\n map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;\n }\n return map;\n }\n /**\n * The palette slot a detection index names, or undefined when the palette has\n * no slot for it. Slot zero is the background, so the ids start at one and the\n * last detection the palette can name sits one index below the last slot.\n */\n function resolveIdMaskPaletteId(detectionIndex) {\n const detectionMaskId = detectionIndex + 1;\n return detectionMaskId > 0 && detectionMaskId < MAX_ID_MASK_PALETTE_ENTRIES\n ? detectionMaskId\n : undefined;\n }\n function writeIdMaskPaletteEntry(palette, detectionMaskId, color, alpha) {\n const offset = detectionMaskId * 4;\n palette[offset] = ((color >> 16) & 0xff) / 255;\n palette[offset + 1] = ((color >> 8) & 0xff) / 255;\n palette[offset + 2] = (color & 0xff) / 255;\n palette[offset + 3] = Math.max(0, Math.min(alpha, 1));\n }\n new Set(Object.values(DepthPreviewLevels));\n new Set(Object.values(DepthMapKind));\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read, or was refused before any read. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, and a gate that stops one has to bound its own wait\n * or a producer nothing answers for never runs again.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n /**\n * The picture is waiting on media the source has not handed over yet, which\n * is the bytes and their decode rather than anything downstream of them.\n * `PlaybackBuffering` is what a transport reports once it has already\n * stopped; this is reported from the read itself, including the reads a seek\n * makes while the transport still reads as paused.\n */\n MediaSessionActivityKind[\"MediaSourceReading\"] = \"mediaSourceReading\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n /**\n * The render-preparation gate held the picture for artifacts that never\n * arrived and let it go. Nothing is blocked: the frames reaching the screen\n * are the ones whose artifacts were given up on. What is waited on is\n * preparation finishing another frame, which is what lets the gate hold the\n * picture again; `RenderPreparing` covers the holds that are running.\n */\n MediaSessionActivityKind[\"RenderPreparationAbandoned\"] = \"renderPreparationAbandoned\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n /** Builds compact, independent alpha crops so overlapping masks stay exact. */\n function createRegionMaskCoverageFrame(instructions) {\n const entries = [];\n for (const instruction of instructions) {\n if (!instruction.regionCoverageMask) {\n continue;\n }\n const entry = cropCoverageMask(instruction.detectionIndex, decodeCompressedRleMask(instruction.regionCoverageMask));\n if (entry) {\n entries.push(entry);\n }\n }\n return entries.length > 0 ? { entries } : undefined;\n }\n function cropCoverageMask(detectionIndex, decodedMask) {\n let minX = decodedMask.width;\n let minY = decodedMask.height;\n let maxX = -1;\n let maxY = -1;\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n if (maxX < minX || maxY < minY) {\n return undefined;\n }\n const width = maxX - minX + 1;\n const height = maxY - minY + 1;\n const data = new Uint8Array(new ArrayBuffer(width * height));\n for (let y = minY; y <= maxY; y += 1) {\n for (let x = minX; x <= maxX; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n data[(y - minY) * width + (x - minX)] = 255;\n }\n }\n return { data, detectionIndex, height, width, x: minX, y: minY };\n }\n function compositeMaskFrame(instructions, displayWidth) {\n const maskInstructions = materializeMaskInstructions(instructions);\n if (maskInstructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));\n const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));\n const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));\n const strokeCoverage = maskInstructions.some(({ stroke }) => stroke && stroke.width > 0 && stroke.alpha > 0)\n ? new Uint8Array(width * height)\n : undefined;\n const strokeKernels = new Map();\n for (const instruction of maskInstructions) {\n compositeInstruction(data, width, instruction, strokeCoverage, strokeKernels, displayWidth);\n }\n return { data, height, width };\n }\n function createIdMaskRasterFrame(instructions, maxRasterWidth) {\n try {\n return createIdMaskFrame(instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => instruction.stroke?.widthUnit\n ? { ...instruction, stroke: undefined }\n : instruction), { maxWidth: maxRasterWidth });\n }\n catch {\n // The id raster is the fast path, not the only one: answering with nothing\n // puts the caller on the RGBA composite, which draws the same picture.\n return undefined;\n }\n }\n /**\n * Whether the palette has a slot for every detection this frame masks. An id\n * names a detection by its index in the frame, so one masked detection past the\n * last slot leaves the whole frame without an id raster however few masks it\n * carries.\n */\n function canIdMaskPaletteNameFrame(instructions) {\n return instructions.every((instruction) => instruction.visible === false ||\n resolveIdMaskPaletteId(instruction.detectionIndex) !== undefined);\n }\n function createIdMaskPlane(instructions, maxRasterWidth) {\n // The plane carries the ids a failed cook could not. A frame the palette\n // cannot name has none to carry, and cooking it again reaches the same\n // refusal after rasterizing every mask a second time.\n if (!canIdMaskPaletteNameFrame(instructions)) {\n return undefined;\n }\n const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);\n return frame\n ? { data: frame.data, height: frame.height, width: frame.width }\n : undefined;\n }\n function compositeInstruction(rgba, canvasWidth, instruction, strokeCoverage, strokeKernels, displayWidth) {\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);\n if (instruction.stroke &&\n instruction.stroke.widthUnit === undefined &&\n bounds &&\n strokeCoverage) {\n compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, {\n ...instruction.stroke,\n width: resolveIdMaskStrokeTexels(instruction.stroke.width, displayWidth ?? canvasWidth, canvasWidth, instruction.stroke.alignment),\n }, strokeCoverage, strokeKernels);\n }\n }\n function createMaskScreenStrokes(instructions, width, height, preparedRaster) {\n const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&\n stroke?.widthUnit !== undefined &&\n stroke.width > 0 &&\n stroke.alpha > 0);\n if (outlined.length === 0)\n return undefined;\n const raster = preparedRaster ?? new Uint32Array(width * height);\n if (!preparedRaster) {\n const masks = materializeMaskInstructions(instructions);\n for (const { detectionIndex, mask } of masks) {\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const length = counts[index];\n if (index % 2 === 1) {\n for (let pixel = offset; pixel < offset + length; pixel += 1) {\n const x = Math.floor(pixel / mask.height);\n const y = pixel % mask.height;\n raster[y * width + x] = detectionIndex + 1;\n }\n }\n offset += length;\n }\n }\n }\n const paths = traceMaskOutlinePaths(raster, width, height, new Set(outlined.map(({ detectionIndex }) => detectionIndex + 1)));\n return outlined.map(({ detectionIndex, stroke }) => ({\n detectionIndex,\n alpha: stroke.alpha,\n color: stroke.color,\n width: stroke.width,\n widthUnit: stroke.widthUnit,\n alignment: stroke.alignment,\n paths: paths.get(detectionIndex + 1) ?? [],\n }));\n }\n function materializeMaskInstructions(instructions) {\n return instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => {\n if (instruction.mask) {\n return instruction;\n }\n const { height, points, width } = instruction.polygon;\n return {\n alpha: instruction.alpha,\n color: instruction.color,\n detectionIndex: instruction.detectionIndex,\n mask: encodeBinaryMask(rasterizePolygonToMask(points, { height, width }), width, height),\n stroke: instruction.stroke,\n };\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function compositeMaskFill(rgba, canvasWidth, mask, fill) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n const maskArea = maskWidth * maskHeight;\n let maskOffset = 0;\n let minX = maskWidth;\n let minY = maskHeight;\n let maxX = -1;\n let maxY = -1;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let columnX = Math.floor(maskOffset / maskHeight);\n let columnY = maskOffset - columnX * maskHeight;\n for (let step = 0; step < runLength; step += 1) {\n // A run can outlast the columns the mask has; the pixel it then names\n // is wherever that column-major offset lands when read back row-major.\n const rowMajorOffset = columnY * maskWidth + columnX;\n if (rowMajorOffset < maskArea) {\n const x = rowMajorOffset % maskWidth;\n const y = (rowMajorOffset - x) / maskWidth;\n writePixel(rgba, canvasWidth, x, y, fill);\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n columnY += 1;\n if (columnY === maskHeight) {\n columnY = 0;\n columnX += 1;\n }\n }\n maskOffset += runLength;\n }\n return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };\n }\n function createStrokeKernel(width, alignment) {\n const innerFraction = alignment === StrokeAlignment.Inside\n ? 1\n : alignment === StrokeAlignment.Center\n ? 0.5\n : 0;\n const inside = [];\n const outside = [];\n const radius = Math.min(MAX_ID_MASK_STROKE_WIDTH + 1, Math.ceil(width * Math.max(innerFraction, 1 - innerFraction) + 0.5));\n const clamp = (value) => Math.max(0, Math.min(1, value));\n const coverage = (signed) => Math.round(255 *\n (clamp(0.5 + width * (1 - innerFraction) - signed) -\n clamp(0.5 - width * innerFraction - signed)));\n for (let y = -radius; y <= radius; y += 1) {\n for (let x = -radius; x <= radius; x += 1) {\n if (x === 0 && y === 0)\n continue;\n const distance = Math.max(Math.abs(x) - 0.5, Math.abs(y) - 0.5, 0);\n const innerCoverage = coverage(-distance);\n const outerCoverage = coverage(distance);\n if (innerCoverage > 0)\n inside.push([x, y, innerCoverage]);\n if (outerCoverage > 0)\n outside.push([x, y, outerCoverage]);\n }\n }\n return { inside, outside, radius };\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke, coverage, kernels) {\n if (stroke.width <= 0 || stroke.alpha <= 0)\n return;\n const key = `${stroke.width}:${stroke.alignment}`;\n let kernel = kernels.get(key);\n if (!kernel) {\n kernel = createStrokeKernel(stroke.width, stroke.alignment);\n kernels.set(key, kernel);\n }\n const visitedAir = kernel.inside.length > 0 ? new Set() : undefined;\n const mark = (sourceX, sourceY, offsets, foreground) => {\n for (const [dx, dy, value] of offsets) {\n const x = sourceX + dx;\n const y = sourceY + dy;\n if (isOutsideMaskBounds(decodedMask, x, y) ||\n isMaskPixel(decodedMask, x, y) !== foreground)\n continue;\n const index = y * canvasWidth + x;\n coverage[index] = Math.max(coverage[index], value);\n }\n };\n for (let y = bounds.minY; y <= bounds.maxY; y += 1) {\n for (let x = bounds.minX; x <= bounds.maxX; x += 1) {\n if (!isMaskPixel(decodedMask, x, y) ||\n !isBoundaryPixel(decodedMask, x, y))\n continue;\n mark(x, y, kernel.outside, false);\n if (!visitedAir)\n continue;\n for (let dy = -1; dy <= 1; dy += 1) {\n for (let dx = -1; dx <= 1; dx += 1) {\n const airX = x + dx;\n const airY = y + dy;\n if (isOutsideMaskBounds(decodedMask, airX, airY) ||\n isMaskPixel(decodedMask, airX, airY))\n continue;\n const airIndex = airY * decodedMask.width + airX;\n if (visitedAir.has(airIndex))\n continue;\n visitedAir.add(airIndex);\n mark(airX, airY, kernel.inside, true);\n }\n }\n }\n }\n const color = resolveRgbaColor(stroke.color, stroke.alpha);\n for (let y = Math.max(0, bounds.minY - kernel.radius); y <= Math.min(decodedMask.height - 1, bounds.maxY + kernel.radius); y += 1) {\n for (let x = Math.max(0, bounds.minX - kernel.radius); x <= Math.min(decodedMask.width - 1, bounds.maxX + kernel.radius); x += 1) {\n const index = y * canvasWidth + x;\n const amount = coverage[index] / 255;\n if (amount <= 0)\n continue;\n coverage[index] = 0;\n const offset = index * 4;\n const alpha = rgba[offset + 3] * (1 - amount) + color.alpha * amount;\n rgba[offset] =\n alpha > 0\n ? (rgba[offset] * rgba[offset + 3] * (1 - amount) +\n color.red * color.alpha * amount) /\n alpha\n : 0;\n rgba[offset + 1] =\n alpha > 0\n ? (rgba[offset + 1] * rgba[offset + 3] * (1 - amount) +\n color.green * color.alpha * amount) /\n alpha\n : 0;\n rgba[offset + 2] =\n alpha > 0\n ? (rgba[offset + 2] * rgba[offset + 3] * (1 - amount) +\n color.blue * color.alpha * amount) /\n alpha\n : 0;\n rgba[offset + 3] = alpha;\n }\n }\n }\n function isBoundaryPixel(mask, x, y) {\n for (let offsetY = -1; offsetY <= 1; offsetY += 1) {\n for (let offsetX = -1; offsetX <= 1; offsetX += 1) {\n if (offsetX === 0 && offsetY === 0) {\n continue;\n }\n const neighborX = x + offsetX;\n const neighborY = y + offsetY;\n if (isOutsideMaskBounds(mask, neighborX, neighborY) ||\n !isMaskPixel(mask, neighborX, neighborY)) {\n return true;\n }\n }\n }\n return false;\n }\n function isMaskPixel(mask, x, y) {\n return mask.data[y * mask.width + x] === 1;\n }\n function isOutsideMaskBounds(mask, x, y) {\n return x < 0 || y < 0 || x >= mask.width || y >= mask.height;\n }\n function resolveRgbaColor(color, alpha) {\n return {\n alpha: Math.round(Math.max(0, Math.min(alpha, 1)) * 255),\n blue: color & 0xff,\n green: (color >> 8) & 0xff,\n red: (color >> 16) & 0xff,\n };\n }\n function writePixel(rgba, canvasWidth, x, y, color) {\n const rgbaOffset = (y * canvasWidth + x) * 4;\n rgba[rgbaOffset] = color.red;\n rgba[rgbaOffset + 1] = color.green;\n rgba[rgbaOffset + 2] = color.blue;\n rgba[rgbaOffset + 3] = color.alpha;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"IdMask\"] = \"idMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\n /**\n * How an id raster is laid out for the texture it is uploaded into. WebGPU\n * takes any bytesPerRow, so ids go up one byte per pixel. WebGL aligns every\n * uploaded row to four bytes and rejects a single-channel upload whose width\n * is not a multiple of four, which is what the four-channel layout is for.\n */\n var IdMaskTextureFormat;\n (function (IdMaskTextureFormat) {\n IdMaskTextureFormat[\"R8\"] = \"r8unorm\";\n IdMaskTextureFormat[\"Rgba8\"] = \"rgba8unorm\";\n })(IdMaskTextureFormat || (IdMaskTextureFormat = {}));\n\n const DEFAULT_STOPS = [\n { position: 0, color: 0xffd600 },\n { position: 0.35, color: 0xffd600 },\n { position: 0.7, color: 0xff7a00 },\n { position: 1, color: 0xdc1e1e },\n ];\n const MAX_HEATMAP_PIXELS = 16_777_216;\n const MAX_HEATMAP_SIDE = 8_192;\n /** Turns semantic scores into an RGBA raster; no palette enters cold data. */\n function colorizeHeatmap(map, renderer) {\n const { bounds, width, height, values } = map;\n if (!Number.isFinite(bounds?.x) ||\n !Number.isFinite(bounds?.y) ||\n !Number.isFinite(bounds?.width) ||\n !Number.isFinite(bounds?.height) ||\n bounds.width <= 0 ||\n bounds.height <= 0 ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width <= 0 ||\n height <= 0 ||\n width > MAX_HEATMAP_SIDE ||\n height > MAX_HEATMAP_SIDE ||\n width * height > MAX_HEATMAP_PIXELS ||\n !(Array.isArray(values) ||\n values instanceof Float32Array ||\n values instanceof Float64Array ||\n values instanceof Uint16Array) ||\n values.length !== width * height) {\n throw new RangeError(\"Heatmap bounds and dimensions must be valid, match row-major values, and fit the raster limit.\");\n }\n const scale = map.valueScale ?? 1;\n const threshold = (map.threshold ?? 0) * (renderer.thresholdScale ?? 1);\n const maximumScore = renderer.maximumScore ?? 1;\n const opacity = clamp01(renderer.opacity ?? 1);\n const minimumAlpha = clamp01(renderer.minimumAlpha ?? 0);\n const stops = renderer.colorStops ?? DEFAULT_STOPS;\n if (!Number.isFinite(scale) ||\n scale <= 0 ||\n !Number.isFinite(threshold) ||\n !Number.isFinite(maximumScore) ||\n maximumScore <= 0 ||\n !Number.isFinite(renderer.thresholdScale ?? 1) ||\n (renderer.thresholdScale ?? 1) < 0 ||\n !Number.isFinite(renderer.opacity ?? 1) ||\n !Number.isFinite(renderer.minimumAlpha ?? 0) ||\n stops.length === 0 ||\n stops.some((stop, index) => !Number.isFinite(stop.position) ||\n stop.position < 0 ||\n stop.position > 1 ||\n !Number.isInteger(stop.color) ||\n stop.color < 0 ||\n stop.color > 0xffffff ||\n (index > 0 && stop.position < stops[index - 1].position))) {\n throw new RangeError(\"Invalid heatmap scale, threshold, or color stops.\");\n }\n const rgba = new Uint8ClampedArray(new ArrayBuffer(values.length * 4));\n if (maximumScore <= threshold || opacity === 0)\n return rgba;\n for (let index = 0; index < values.length; index += 1) {\n const score = values[index] * scale;\n if (!Number.isFinite(score) || score <= threshold)\n continue;\n const color = interpolateColor(stops, clamp01(score / maximumScore));\n const alphaIntensity = clamp01((score - threshold) / (maximumScore - threshold));\n const offset = index * 4;\n rgba[offset] = color >> 16;\n rgba[offset + 1] = (color >> 8) & 0xff;\n rgba[offset + 2] = color & 0xff;\n rgba[offset + 3] = Math.round(255 * opacity * (minimumAlpha + (1 - minimumAlpha) * alphaIntensity));\n }\n return rgba;\n }\n function interpolateColor(stops, value) {\n const upperIndex = stops.findIndex((stop) => stop.position >= value);\n if (upperIndex === -1)\n return stops[stops.length - 1].color;\n if (upperIndex === 0)\n return stops[0].color;\n const left = stops[upperIndex - 1];\n const right = stops[upperIndex];\n const span = right.position - left.position;\n const ratio = span === 0 ? 1 : (value - left.position) / span;\n const channel = (shift) => Math.round(((left.color >> shift) & 0xff) * (1 - ratio) +\n ((right.color >> shift) & 0xff) * ratio);\n return (channel(16) << 16) | (channel(8) << 8) | channel(0);\n }\n function clamp01(value) {\n return Math.max(0, Math.min(1, value));\n }\n\n var HeatmapPreparationWorkerMessageType;\n (function (HeatmapPreparationWorkerMessageType) {\n HeatmapPreparationWorkerMessageType[\"Prepare\"] = \"heatmap-prepare\";\n HeatmapPreparationWorkerMessageType[\"Complete\"] = \"heatmap-complete\";\n HeatmapPreparationWorkerMessageType[\"Error\"] = \"heatmap-error\";\n })(HeatmapPreparationWorkerMessageType || (HeatmapPreparationWorkerMessageType = {}));\n\n var MaskPreparationWorkerMessageType;\n (function (MaskPreparationWorkerMessageType) {\n MaskPreparationWorkerMessageType[\"Complete\"] = \"complete\";\n MaskPreparationWorkerMessageType[\"Empty\"] = \"empty\";\n MaskPreparationWorkerMessageType[\"Error\"] = \"error\";\n MaskPreparationWorkerMessageType[\"Prepare\"] = \"prepare\";\n })(MaskPreparationWorkerMessageType || (MaskPreparationWorkerMessageType = {}));\n\n /**\n * Messages between the depth source and the render-preparation worker, which\n * decodes depth and confidence PNGs, and copies decoded preview frames' luma,\n * off the main thread. Internal: the worker script is a deployment asset, and\n * its protocol is not public.\n */\n var DepthPreparationWorkerMessageType;\n (function (DepthPreparationWorkerMessageType) {\n DepthPreparationWorkerMessageType[\"Decode\"] = \"depth-decode\";\n DepthPreparationWorkerMessageType[\"Complete\"] = \"depth-decode-complete\";\n DepthPreparationWorkerMessageType[\"Error\"] = \"depth-decode-error\";\n DepthPreparationWorkerMessageType[\"PreviewLuma\"] = \"depth-preview-luma\";\n DepthPreparationWorkerMessageType[\"PreviewLumaComplete\"] = \"depth-preview-luma-complete\";\n })(DepthPreparationWorkerMessageType || (DepthPreparationWorkerMessageType = {}));\n\n /*\n * Grayscale PNG decoding for depth maps and their confidence planes.\n *\n * Every image path a browser offers (<img>, createImageBitmap, ImageDecoder,\n * canvas) hands back 8 bits per channel, so a 16-bit depth PNG is decoded\n * here: the browser's own zlib inflates the IDAT stream, and the row filters\n * are undone in JavaScript.\n *\n * This module imports nothing, so the render-preparation worker, the main\n * thread and the depth benchmark all run the same code.\n */\n const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];\n const COLOR_TYPE_GRAY = 0;\n /** 16384 x 16384, beyond any GPU texture; a header claiming more is refused. */\n const MAX_PNG_PIXELS = 16_384 * 16_384;\n async function decodePng16(bytes, options = {}) {\n const header = readPngHeader(bytes, 16);\n const rowBytes = header.width * 2 + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint16Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray16(raw, header.width, header.height, values);\n return {\n height: header.height,\n paddedUpload: options.padRowsForWebGl && header.width % 2 === 1\n ? padRows(values, header.width, header.height)\n : undefined,\n values,\n width: header.width,\n };\n }\n async function decodePng8Gray(bytes, options = {}) {\n const header = readPngHeader(bytes, 8);\n const rowBytes = header.width + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint8Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray8(raw, header.width, header.height, values);\n return { height: header.height, values, width: header.width };\n }\n /**\n * Inflates with the platform's `DecompressionStream(\"deflate\")` (zlib\n * framing, which is what PNG uses) straight into one buffer of the expected\n * size, so no second copy joins the output chunks.\n */\n async function inflateWithDecompressionStream(chunks, byteLength) {\n const stream = new DecompressionStream(\"deflate\");\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n // Writers split IDAT into many small chunks (libpng's default is 8 KiB).\n // One write of the joined stream costs a copy of the compressed bytes and\n // saves a stream round trip per chunk.\n const zlib = chunks.length === 1 ? chunks[0] : joinChunks(chunks);\n const writing = (async () => {\n await writer.write(zlib);\n await writer.close();\n })();\n const output = new Uint8Array(byteLength);\n let offset = 0;\n // A corrupt stream rejects both sides; the reader's error is the one thrown.\n writing.catch(() => undefined);\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done)\n break;\n if (offset + value.byteLength > byteLength) {\n throw new RangeError(\"PNG image data is longer than its header says.\");\n }\n output.set(value, offset);\n offset += value.byteLength;\n }\n await writing;\n }\n catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error instanceof RangeError\n ? error\n : new RangeError(`PNG image data does not inflate: ${error instanceof Error ? error.message : String(error)}`);\n }\n return offset === byteLength ? output : output.subarray(0, offset);\n }\n function joinChunks(chunks) {\n const joined = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n joined.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return joined;\n }\n function readPngHeader(input, bitDepth) {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n if (bytes.byteLength < 8 ||\n PNG_SIGNATURE.some((value, index) => bytes[index] !== value)) {\n throw new RangeError(\"Not a PNG file.\");\n }\n let offset = 8;\n let width = 0;\n let height = 0;\n let sawHeader = false;\n let sawEnd = false;\n const idat = [];\n while (offset + 12 <= bytes.byteLength) {\n const length = view.getUint32(offset);\n const type = view.getUint32(offset + 4);\n const dataStart = offset + 8;\n const dataEnd = dataStart + length;\n if (dataEnd + 4 > bytes.byteLength) {\n throw new RangeError(\"PNG file is truncated.\");\n }\n if (type === 0x49484452) {\n // IHDR\n if (length !== 13)\n throw new RangeError(\"PNG header is malformed.\");\n width = view.getUint32(dataStart);\n height = view.getUint32(dataStart + 4);\n const depth = bytes[dataStart + 8];\n const colorType = bytes[dataStart + 9];\n if (bytes[dataStart + 10] !== 0 || bytes[dataStart + 11] !== 0) {\n throw new RangeError(\"PNG uses an unknown compression or filter.\");\n }\n if (bytes[dataStart + 12] !== 0) {\n throw new RangeError(\"Interlaced PNGs are not supported.\");\n }\n if (colorType !== COLOR_TYPE_GRAY || depth !== bitDepth) {\n throw new RangeError(`Expected a ${bitDepth}-bit grayscale PNG, got bit depth ${depth} with colour type ${colorType}.`);\n }\n sawHeader = true;\n }\n else if (type === 0x49444154) {\n // IDAT\n idat.push(bytes.subarray(dataStart, dataEnd));\n }\n else if (type === 0x49454e44) {\n // IEND\n sawEnd = true;\n break;\n }\n offset = dataEnd + 4;\n }\n if (!sawHeader)\n throw new RangeError(\"PNG has no header.\");\n if (!sawEnd)\n throw new RangeError(\"PNG file is truncated.\");\n if (width === 0 || height === 0 || width * height > MAX_PNG_PIXELS) {\n throw new RangeError(`PNG size ${width}x${height} is not supported.`);\n }\n if (idat.length === 0)\n throw new RangeError(\"PNG has no image data.\");\n return { height, idat, width };\n }\n function checkInflatedLength(raw, expected) {\n if (raw.byteLength !== expected) {\n throw new RangeError(\"PNG image data is shorter than its header says.\");\n }\n }\n /*\n * Undoes the row filters of a 16-bit grayscale image and writes host-order\n * samples, in one pass. Each filter runs on bytes, and the two bytes of a\n * sample never carry into each other, so the loops keep a sample's high and\n * low bytes apart and join them only for the store. The previous row is read\n * back from the output, which already holds it unfiltered; the first row reads\n * a row of zeros, which is what PNG defines above the image.\n *\n * Each filter is its own small function with one loop, called once per row,\n * so engines compile each loop on its own: SpiderMonkey slowed a Paeth loop\n * that shared a function with the other filters.\n */\n function unfilterGray16(raw, width, height, out) {\n const rowBytes = width * 2 + 1;\n const zeroRow = new Uint16Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_16[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow16(raw, s, out, o, end) {\n for (let i = o; i < end; i += 1, s += 2) {\n out[i] = (raw[s] << 8) | raw[s + 1];\n }\n }\n function subRow16(raw, s, out, o, end) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2) {\n hi = (raw[s] + hi) & 0xff;\n lo = (raw[s + 1] + lo) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n function upRow16(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n out[i] = (((raw[s] + (up >> 8)) & 0xff) << 8) | ((raw[s + 1] + up) & 0xff);\n }\n }\n function averageRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n hi = (raw[s] + ((hi + (up >> 8)) >> 1)) & 0xff;\n lo = (raw[s + 1] + ((lo + (up & 0xff)) >> 1)) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n /** Paeth: per byte, whichever of left, up and up-left is closest to left + up - up-left. */\n function paethRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n let upLeftHi = 0;\n let upLeftLo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n const upHi = up >> 8;\n const upLo = up & 0xff;\n let pa = Math.abs(upHi - upLeftHi);\n let pb = Math.abs(hi - upLeftHi);\n let pc = Math.abs(hi + upHi - upLeftHi - upLeftHi);\n hi =\n (raw[s] + (pa <= pb && pa <= pc ? hi : pb <= pc ? upHi : upLeftHi)) &\n 0xff;\n pa = Math.abs(upLo - upLeftLo);\n pb = Math.abs(lo - upLeftLo);\n pc = Math.abs(lo + upLo - upLeftLo - upLeftLo);\n lo =\n (raw[s + 1] + (pa <= pb && pa <= pc ? lo : pb <= pc ? upLo : upLeftLo)) &\n 0xff;\n out[i] = (hi << 8) | lo;\n upLeftHi = upHi;\n upLeftLo = upLo;\n }\n }\n /** Indexed by the PNG filter type byte: None, Sub, Up, Average, Paeth. */\n const ROW_FILTERS_16 = [\n noneRow16,\n subRow16,\n upRow16,\n averageRow16,\n paethRow16,\n ];\n function unfilterGray8(raw, width, height, out) {\n const rowBytes = width + 1;\n const zeroRow = new Uint8Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_8[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow8(raw, s, out, o, end) {\n out.set(raw.subarray(s, s + end - o), o);\n }\n function subRow8(raw, s, out, o, end) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1) {\n left = (raw[s] + left) & 0xff;\n out[i] = left;\n }\n }\n function upRow8(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n out[i] = (raw[s] + prev[p]) & 0xff;\n }\n }\n function averageRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n left = (raw[s] + ((left + prev[p]) >> 1)) & 0xff;\n out[i] = left;\n }\n }\n function paethRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n let upLeft = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n const up = prev[p];\n const pa = Math.abs(up - upLeft);\n const pb = Math.abs(left - upLeft);\n const pc = Math.abs(left + up - upLeft - upLeft);\n left =\n (raw[s] + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft)) & 0xff;\n out[i] = left;\n upLeft = up;\n }\n }\n const ROW_FILTERS_8 = [\n noneRow8,\n subRow8,\n upRow8,\n averageRow8,\n paethRow8,\n ];\n /**\n * WebGL unpacks rows on four-byte boundaries, so an odd width of two-byte\n * texels needs one padding texel per row.\n */\n function padRows(values, width, height) {\n const textureWidth = width + 1;\n const padded = new Uint16Array(textureWidth * height);\n for (let y = 0; y < height; y += 1) {\n padded.set(values.subarray(y * width, (y + 1) * width), y * textureWidth);\n }\n return { bytes: new Uint8Array(padded.buffer), textureWidth };\n }\n\n /**\n * Answers one decode request, in the worker or on the main thread: the reply\n * and the buffers it can transfer. A file that does not decode is an error\n * reply, never a thrown exception, so the caller can tell a bad file from a\n * broken worker.\n */\n async function decodeDepthPreparationRequest(request) {\n const requestId = request.requestId ?? 0;\n try {\n if (request.bitDepth === 8) {\n const decoded = await decodePng8Gray(request.bytes);\n return {\n response: {\n bitDepth: 8,\n height: decoded.height,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer: [decoded.values.buffer],\n };\n }\n const decoded = await decodePng16(request.bytes, {\n padRowsForWebGl: request.padRowsForWebGl,\n });\n const padded = decoded.paddedUpload;\n const decimated = request.decimateBy !== undefined && request.decimateBy >= 2\n ? decimateDepthUpload(decoded.values, decoded.width, decoded.height, Math.floor(request.decimateBy), request.padRowsForWebGl === true)\n : null;\n const transfer = [decoded.values.buffer];\n if (padded)\n transfer.push(padded.bytes.buffer);\n if (decimated)\n transfer.push(decimated.bytes.buffer);\n return {\n response: {\n bitDepth: 16,\n decimatedUpload: decimated\n ? { ...decimated, bytes: decimated.bytes.buffer }\n : undefined,\n height: decoded.height,\n paddedUpload: padded\n ? {\n bytes: padded.bytes.buffer,\n textureWidth: padded.textureWidth,\n }\n : undefined,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer,\n };\n }\n catch (error) {\n return {\n response: {\n error: error instanceof Error ? error.message : \"Unable to decode depth.\",\n requestId,\n type: DepthPreparationWorkerMessageType.Error,\n },\n transfer: [],\n };\n }\n }\n /**\n * Depth for a texture `factor` times smaller on each side, each texel the\n * sample at its block's centre, as the renderer decimates a map larger than\n * the GPU allows.\n */\n function decimateDepthUpload(values, width, height, factor, padRowsForWebGl) {\n if (factor < 2)\n return null;\n const targetWidth = Math.ceil(width / factor);\n const targetHeight = Math.ceil(height / factor);\n // WebGL reads two-byte texels in rows of four bytes.\n const textureWidth = padRowsForWebGl\n ? targetWidth + (targetWidth % 2)\n : targetWidth;\n const texels = new Uint16Array(textureWidth * targetHeight);\n const columns = new Uint32Array(targetWidth);\n for (let x = 0; x < targetWidth; x += 1) {\n columns[x] = Math.min(width - 1, Math.floor(((x + 0.5) * width) / targetWidth));\n }\n for (let y = 0; y < targetHeight; y += 1) {\n const row = Math.min(height - 1, Math.floor(((y + 0.5) * height) / targetHeight)) *\n width;\n const target = y * textureWidth;\n for (let x = 0; x < targetWidth; x += 1) {\n texels[target + x] = values[row + columns[x]];\n }\n }\n return {\n bytes: new Uint8Array(texels.buffer),\n height: targetHeight,\n textureWidth,\n width: targetWidth,\n };\n }\n\n /** Formats whose first plane is 8-bit luma, which is the code as written. */\n const PLANAR_8_BIT_FORMATS = new Set([\n \"I420\",\n \"I420A\",\n \"I422\",\n \"I422A\",\n \"I444\",\n \"I444A\",\n \"NV12\",\n ]);\n const RGB_FORMATS = new Set([\"RGBA\", \"RGBX\", \"BGRA\", \"BGRX\"]);\n /**\n * Copies a decoded preview frame's luma codes out of it, mapped through\n * `correction` when one is given. The frame stays open; its owner closes\n * it.\n */\n async function readVideoFrameLuma(frame, scratch, correction = null) {\n const rect = frame.visibleRect ?? {\n height: frame.codedHeight,\n width: frame.codedWidth,\n x: 0,\n y: 0,\n };\n const { width, height } = rect;\n const format = frame.format;\n const planar = format !== null && PLANAR_8_BIT_FORMATS.has(format);\n let bytes;\n let plane;\n if (planar || (format !== null && RGB_FORMATS.has(format))) {\n const size = frame.allocationSize({ rect });\n if (scratch.buffer.byteLength < size)\n scratch.buffer = new ArrayBuffer(size);\n [plane] = await frame.copyTo(scratch.buffer, { rect });\n bytes = new Uint8Array(scratch.buffer);\n }\n else {\n // A frame in a layout no copy can read is drawn to a canvas, which\n // converts it to RGB as the browser sees fit.\n bytes = drawFrameRgba(frame, width, height);\n }\n const luma = new Uint8Array(width * height);\n const stride = plane?.stride ?? (planar ? width : width * 4);\n const offset = plane?.offset ?? 0;\n if (!planar) {\n // Every RGB layout keeps green second, and luma with neutral chroma\n // converts to three equal channels.\n for (let y = 0; y < height; y += 1) {\n const row = offset + y * stride + 1;\n const target = y * width;\n for (let x = 0; x < width; x += 1) {\n luma[target + x] = bytes[row + x * 4];\n }\n }\n }\n else if (stride === width) {\n luma.set(bytes.subarray(offset, offset + width * height));\n }\n else {\n for (let y = 0; y < height; y += 1) {\n luma.set(bytes.subarray(offset + y * stride, offset + y * stride + width), y * width);\n }\n }\n if (correction) {\n for (let index = 0; index < luma.length; index += 1) {\n luma[index] = correction[luma[index]];\n }\n }\n return { height, luma, path: planar ? \"plane\" : \"rgb\", width };\n }\n function drawFrameRgba(frame, width, height) {\n const canvas = new OffscreenCanvas(width, height);\n const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!context) {\n throw new Error(\"The depth preview needs a 2D canvas to read this frame.\");\n }\n context.drawImage(frame, 0, 0, width, height);\n const { data } = context.getImageData(0, 0, width, height);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n\n const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type === HeatmapPreparationWorkerMessageType.Prepare) {\n prepareHeatmap(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.Decode) {\n void decodeDepthFrame(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.PreviewLuma) {\n void copyPreviewLuma(message);\n return;\n }\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\n });\n function prepareHeatmap(message) {\n try {\n const data = colorizeHeatmap(message.map, message.renderer);\n const imageData = new ImageData(data, message.map.width, message.map.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n if (imageBitmap) {\n workerScope.postMessage({\n imageBitmap,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageBitmap]);\n return;\n }\n workerScope.postMessage({\n imageData,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error ? error.message : \"Unable to prepare heatmap.\",\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Error,\n });\n }\n }\n /** Decoding waits on the platform's inflate, so replies arrive out of order. */\n async function decodeDepthFrame(message) {\n const { response, transfer } = await decodeDepthPreparationRequest(message);\n workerScope.postMessage(response, transfer);\n }\n const previewLumaScratch = {\n buffer: new ArrayBuffer(0),\n };\n /** Frames arrive in order and are copied in order, through one scratch. */\n let previewLumaQueue = Promise.resolve();\n function copyPreviewLuma(message) {\n previewLumaQueue = previewLumaQueue.then(async () => {\n try {\n const copied = await readVideoFrameLuma(message.frame, previewLumaScratch, message.correction);\n workerScope.postMessage({\n height: copied.height,\n luma: copied.luma.buffer,\n path: copied.path,\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.PreviewLumaComplete,\n width: copied.width,\n }, [copied.luma.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to copy the depth preview frame.\",\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.Error,\n });\n }\n finally {\n message.frame.close();\n }\n });\n return previewLumaQueue;\n }\n function prepareMaskFrame(message) {\n try {\n const regionMaskCoverage = createRegionMaskCoverageFrame(message.job.instructions);\n const coverageTransfers = getRegionMaskCoverageTransfers(regionMaskCoverage);\n const idMaskFrame = createIdMaskRasterFrame(message.job.instructions, message.job.maxRasterWidth);\n if (idMaskFrame) {\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, idMaskFrame.width, idMaskFrame.height, idMaskFrame.data);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ?? [];\n workerScope.postMessage({\n artifactKind: PreparedMaskFrameKind.IdMask,\n fillPalette: idMaskFrame.fillPalette,\n hasStroke: idMaskFrame.hasStroke,\n height: idMaskFrame.height,\n key: message.job.key,\n maxStrokeWidth: idMaskFrame.maxStrokeWidth,\n raster: idMaskFrame.data,\n screenStrokes,\n regionMaskCoverage,\n requestId: message.requestId,\n sourceWidth: idMaskFrame.sourceWidth,\n strokePalette: idMaskFrame.strokePalette,\n strokeAlignments: idMaskFrame.strokeAlignments,\n strokeWidths: idMaskFrame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n width: idMaskFrame.width,\n }, [\n idMaskFrame.data.buffer,\n idMaskFrame.fillPalette.buffer,\n idMaskFrame.strokePalette.buffer,\n ...(idMaskFrame.strokeAlignments\n ? [idMaskFrame.strokeAlignments.buffer]\n : []),\n idMaskFrame.strokeWidths.buffer,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions, message.job.displayWidth);\n if (!compositedFrame && !regionMaskCoverage) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const preparedPixels = compositedFrame ?? createTransparentCoverageCarrier();\n const imageData = new ImageData(preparedPixels.data, preparedPixels.width, preparedPixels.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskPlane = createIdMaskPlane(message.job.instructions, message.job.maxRasterWidth);\n const idMaskTransfers = idMaskPlane ? [idMaskPlane.data.buffer] : [];\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, preparedPixels.width, preparedPixels.height);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ??\n [];\n if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageBitmap,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageData.data.buffer,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to prepare mask frame.\",\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Error,\n });\n }\n }\n /**\n * A frame that carries only region coverage has nothing to composite, and the\n * RGBA branch still has to produce an artifact for the coverage to ride on.\n */\n function createTransparentCoverageCarrier() {\n return {\n data: new Uint8ClampedArray(new ArrayBuffer(4)),\n height: 1,\n width: 1,\n };\n }\n function getRegionMaskCoverageTransfers(coverage) {\n return coverage?.entries.map(({ data }) => data.buffer) ?? [];\n }\n function createImageBitmapFromImageData(imageData) {\n if (typeof OffscreenCanvas === \"undefined\") {\n return null;\n }\n const canvas = new OffscreenCanvas(imageData.width, imageData.height);\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n context.putImageData(imageData, 0, 0);\n return canvas.transferToImageBitmap();\n }\n\n})();";
3117
3117
 
3118
3118
  const DEFAULT_WORKER_NAME = "supervision-js-render-preparation";
3119
3119
  let defaultWorkerUrl;
@@ -3890,11 +3890,11 @@ function describePreviewDecoding(decoding) {
3890
3890
  return `This browser's ${decoding.hardwareAcceleration} decoder changes depth preview codes: ${probe.mismatchedCodes} of ${probe.judgedCodes} come back different, by up to ${probe.maxError} (${probe.lumaPath ?? "unknown"} path)${corrected}. Preview depth during playback is off by up to ${decoding.correction ? decoding.residualError : probe.maxError} preview steps; exact depth at rest is not affected.`;
3891
3891
  }
3892
3892
  async function openDefaultPreviewTrack(url, options) {
3893
- const { openDepthPreviewTrack } = await import('./depth-preview-track-BvgwNEE9.js').then(function (n) { return n.d; });
3893
+ const { openDepthPreviewTrack } = await import('./depth-preview-track-BEag1ZWF.js').then(function (n) { return n.d; });
3894
3894
  return openDepthPreviewTrack(url, options);
3895
3895
  }
3896
3896
  async function chooseDefaultPreviewDecoding(levels) {
3897
- const { chooseDepthPreviewDecoding } = await import('./depth-preview-probe-DG7McQEN.js');
3897
+ const { chooseDepthPreviewDecoding } = await import('./depth-preview-probe-BG0k-33U.js');
3898
3898
  return chooseDepthPreviewDecoding(levels);
3899
3899
  }
3900
3900
  /**
@@ -5252,7 +5252,7 @@ function cropCoverageMask(detectionIndex, decodedMask) {
5252
5252
  }
5253
5253
  return { data, detectionIndex, height, width, x: minX, y: minY };
5254
5254
  }
5255
- function compositeMaskFrame(instructions) {
5255
+ function compositeMaskFrame(instructions, displayWidth) {
5256
5256
  const maskInstructions = materializeMaskInstructions(instructions);
5257
5257
  if (maskInstructions.length === 0) {
5258
5258
  return undefined;
@@ -5260,18 +5260,22 @@ function compositeMaskFrame(instructions) {
5260
5260
  const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));
5261
5261
  const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));
5262
5262
  const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));
5263
+ const strokeCoverage = maskInstructions.some(({ stroke }) => stroke && stroke.width > 0 && stroke.alpha > 0)
5264
+ ? new Uint8Array(width * height)
5265
+ : undefined;
5266
+ const strokeKernels = new Map();
5263
5267
  for (const instruction of maskInstructions) {
5264
- compositeInstruction(data, width, instruction);
5268
+ compositeInstruction(data, width, instruction, strokeCoverage, strokeKernels, displayWidth);
5265
5269
  }
5266
5270
  return { data, height, width };
5267
5271
  }
5268
5272
  function createIdMaskRasterFrame(instructions, maxRasterWidth) {
5269
5273
  try {
5270
- return createIdMaskFrame(materializeMaskInstructions(instructions).map((instruction) => instruction.stroke?.widthUnit === "screen"
5274
+ return createIdMaskFrame(instructions
5275
+ .filter((instruction) => instruction.visible !== false)
5276
+ .map((instruction) => instruction.stroke?.widthUnit
5271
5277
  ? { ...instruction, stroke: undefined }
5272
- : instruction), {
5273
- maxWidth: maxRasterWidth,
5274
- });
5278
+ : instruction), { maxWidth: maxRasterWidth });
5275
5279
  }
5276
5280
  catch {
5277
5281
  // The id raster is the fast path, not the only one: answering with nothing
@@ -5301,25 +5305,29 @@ function createIdMaskPlane(instructions, maxRasterWidth) {
5301
5305
  ? { data: frame.data, height: frame.height, width: frame.width }
5302
5306
  : undefined;
5303
5307
  }
5304
- function compositeInstruction(rgba, canvasWidth, instruction) {
5308
+ function compositeInstruction(rgba, canvasWidth, instruction, strokeCoverage, strokeKernels, displayWidth) {
5305
5309
  const fill = resolveRgbaColor(instruction.color, instruction.alpha);
5306
5310
  const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);
5307
5311
  if (instruction.stroke &&
5308
- instruction.stroke.widthUnit !== "screen" &&
5309
- bounds) {
5310
- compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);
5312
+ instruction.stroke.widthUnit === undefined &&
5313
+ bounds &&
5314
+ strokeCoverage) {
5315
+ compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, {
5316
+ ...instruction.stroke,
5317
+ width: resolveIdMaskStrokeTexels(instruction.stroke.width, displayWidth ?? canvasWidth, canvasWidth, instruction.stroke.alignment),
5318
+ }, strokeCoverage, strokeKernels);
5311
5319
  }
5312
5320
  }
5313
5321
  function createMaskScreenStrokes(instructions, width, height, preparedRaster) {
5314
5322
  const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&
5315
- stroke?.widthUnit === "screen" &&
5323
+ stroke?.widthUnit !== undefined &&
5316
5324
  stroke.width > 0 &&
5317
5325
  stroke.alpha > 0);
5318
5326
  if (outlined.length === 0)
5319
5327
  return undefined;
5320
- const masks = materializeMaskInstructions(instructions);
5321
5328
  const raster = preparedRaster ?? new Uint32Array(width * height);
5322
5329
  if (!preparedRaster) {
5330
+ const masks = materializeMaskInstructions(instructions);
5323
5331
  for (const { detectionIndex, mask } of masks) {
5324
5332
  const counts = decodeCompressedRleCounts(mask.counts);
5325
5333
  let offset = 0;
@@ -5342,6 +5350,8 @@ function createMaskScreenStrokes(instructions, width, height, preparedRaster) {
5342
5350
  alpha: stroke.alpha,
5343
5351
  color: stroke.color,
5344
5352
  width: stroke.width,
5353
+ widthUnit: stroke.widthUnit,
5354
+ alignment: stroke.alignment,
5345
5355
  paths: paths.get(detectionIndex + 1) ?? [],
5346
5356
  }));
5347
5357
  }
@@ -5407,31 +5417,110 @@ function compositeMaskFill(rgba, canvasWidth, mask, fill) {
5407
5417
  }
5408
5418
  return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };
5409
5419
  }
5410
- function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {
5411
- const width = Math.round(stroke.width);
5412
- if (width <= 0) {
5413
- return;
5420
+ function createStrokeKernel(width, alignment) {
5421
+ const innerFraction = alignment === StrokeAlignment.Inside
5422
+ ? 1
5423
+ : alignment === StrokeAlignment.Center
5424
+ ? 0.5
5425
+ : 0;
5426
+ const inside = [];
5427
+ const outside = [];
5428
+ const radius = Math.min(MAX_ID_MASK_STROKE_WIDTH + 1, Math.ceil(width * Math.max(innerFraction, 1 - innerFraction) + 0.5));
5429
+ const clamp = (value) => Math.max(0, Math.min(1, value));
5430
+ const coverage = (signed) => Math.round(255 *
5431
+ (clamp(0.5 + width * (1 - innerFraction) - signed) -
5432
+ clamp(0.5 - width * innerFraction - signed)));
5433
+ for (let y = -radius; y <= radius; y += 1) {
5434
+ for (let x = -radius; x <= radius; x += 1) {
5435
+ if (x === 0 && y === 0)
5436
+ continue;
5437
+ const distance = Math.max(Math.abs(x) - 0.5, Math.abs(y) - 0.5, 0);
5438
+ const innerCoverage = coverage(-distance);
5439
+ const outerCoverage = coverage(distance);
5440
+ if (innerCoverage > 0)
5441
+ inside.push([x, y, innerCoverage]);
5442
+ if (outerCoverage > 0)
5443
+ outside.push([x, y, outerCoverage]);
5444
+ }
5414
5445
  }
5415
- const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);
5446
+ return { inside, outside, radius };
5447
+ }
5448
+ function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke, coverage, kernels) {
5449
+ if (stroke.width <= 0 || stroke.alpha <= 0)
5450
+ return;
5451
+ const key = `${stroke.width}:${stroke.alignment}`;
5452
+ let kernel = kernels.get(key);
5453
+ if (!kernel) {
5454
+ kernel = createStrokeKernel(stroke.width, stroke.alignment);
5455
+ kernels.set(key, kernel);
5456
+ }
5457
+ const visitedAir = kernel.inside.length > 0 ? new Set() : undefined;
5458
+ const mark = (sourceX, sourceY, offsets, foreground) => {
5459
+ for (const [dx, dy, value] of offsets) {
5460
+ const x = sourceX + dx;
5461
+ const y = sourceY + dy;
5462
+ if (isOutsideMaskBounds(decodedMask, x, y) ||
5463
+ isMaskPixel(decodedMask, x, y) !== foreground)
5464
+ continue;
5465
+ const index = y * canvasWidth + x;
5466
+ coverage[index] = Math.max(coverage[index], value);
5467
+ }
5468
+ };
5416
5469
  for (let y = bounds.minY; y <= bounds.maxY; y += 1) {
5417
5470
  for (let x = bounds.minX; x <= bounds.maxX; x += 1) {
5418
5471
  if (!isMaskPixel(decodedMask, x, y) ||
5419
- !isBoundaryPixel(decodedMask, x, y)) {
5472
+ !isBoundaryPixel(decodedMask, x, y))
5420
5473
  continue;
5421
- }
5422
- for (let offsetY = -width; offsetY <= width; offsetY += 1) {
5423
- for (let offsetX = -width; offsetX <= width; offsetX += 1) {
5424
- const strokeX = x + offsetX;
5425
- const strokeY = y + offsetY;
5426
- if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||
5427
- isMaskPixel(decodedMask, strokeX, strokeY)) {
5474
+ mark(x, y, kernel.outside, false);
5475
+ if (!visitedAir)
5476
+ continue;
5477
+ for (let dy = -1; dy <= 1; dy += 1) {
5478
+ for (let dx = -1; dx <= 1; dx += 1) {
5479
+ const airX = x + dx;
5480
+ const airY = y + dy;
5481
+ if (isOutsideMaskBounds(decodedMask, airX, airY) ||
5482
+ isMaskPixel(decodedMask, airX, airY))
5428
5483
  continue;
5429
- }
5430
- writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);
5484
+ const airIndex = airY * decodedMask.width + airX;
5485
+ if (visitedAir.has(airIndex))
5486
+ continue;
5487
+ visitedAir.add(airIndex);
5488
+ mark(airX, airY, kernel.inside, true);
5431
5489
  }
5432
5490
  }
5433
5491
  }
5434
5492
  }
5493
+ const color = resolveRgbaColor(stroke.color, stroke.alpha);
5494
+ for (let y = Math.max(0, bounds.minY - kernel.radius); y <= Math.min(decodedMask.height - 1, bounds.maxY + kernel.radius); y += 1) {
5495
+ for (let x = Math.max(0, bounds.minX - kernel.radius); x <= Math.min(decodedMask.width - 1, bounds.maxX + kernel.radius); x += 1) {
5496
+ const index = y * canvasWidth + x;
5497
+ const amount = coverage[index] / 255;
5498
+ if (amount <= 0)
5499
+ continue;
5500
+ coverage[index] = 0;
5501
+ const offset = index * 4;
5502
+ const alpha = rgba[offset + 3] * (1 - amount) + color.alpha * amount;
5503
+ rgba[offset] =
5504
+ alpha > 0
5505
+ ? (rgba[offset] * rgba[offset + 3] * (1 - amount) +
5506
+ color.red * color.alpha * amount) /
5507
+ alpha
5508
+ : 0;
5509
+ rgba[offset + 1] =
5510
+ alpha > 0
5511
+ ? (rgba[offset + 1] * rgba[offset + 3] * (1 - amount) +
5512
+ color.green * color.alpha * amount) /
5513
+ alpha
5514
+ : 0;
5515
+ rgba[offset + 2] =
5516
+ alpha > 0
5517
+ ? (rgba[offset + 2] * rgba[offset + 3] * (1 - amount) +
5518
+ color.blue * color.alpha * amount) /
5519
+ alpha
5520
+ : 0;
5521
+ rgba[offset + 3] = alpha;
5522
+ }
5523
+ }
5435
5524
  }
5436
5525
  function isBoundaryPixel(mask, x, y) {
5437
5526
  for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
@@ -5615,7 +5704,7 @@ function createMainThreadMaskFramePreparer(workerStatus, message = null) {
5615
5704
  if (idMaskFrame) {
5616
5705
  return idMaskFrame;
5617
5706
  }
5618
- const compositedFrame = compositeMaskFrame(job.instructions);
5707
+ const compositedFrame = compositeMaskFrame(job.instructions, job.displayWidth);
5619
5708
  if (!compositedFrame && !regionMaskCoverage) {
5620
5709
  return undefined;
5621
5710
  }
@@ -5678,6 +5767,7 @@ function createPreparedIdMaskFrame(job, regionMaskCoverage) {
5678
5767
  regionMaskCoverage,
5679
5768
  sourceWidth: frame.sourceWidth,
5680
5769
  strokePalette: frame.strokePalette,
5770
+ strokeAlignments: frame.strokeAlignments,
5681
5771
  strokeWidths: frame.strokeWidths,
5682
5772
  width: frame.width,
5683
5773
  };
@@ -5803,6 +5893,7 @@ function createPreparedFrameFromWorkerResponse(message) {
5803
5893
  screenStrokes: message.screenStrokes,
5804
5894
  sourceWidth: message.sourceWidth,
5805
5895
  strokePalette: message.strokePalette,
5896
+ strokeAlignments: message.strokeAlignments,
5806
5897
  strokeWidths: message.strokeWidths,
5807
5898
  width: message.width,
5808
5899
  };
@@ -6013,7 +6104,8 @@ function createPreparedRenderWindow(options) {
6013
6104
  ? maskFrame.raster.byteLength +
6014
6105
  maskFrame.fillPalette.byteLength +
6015
6106
  maskFrame.strokePalette.byteLength +
6016
- maskFrame.strokeWidths.byteLength
6107
+ maskFrame.strokeWidths.byteLength +
6108
+ (maskFrame.strokeAlignments?.byteLength ?? 0)
6017
6109
  : pixels * 4 + (maskFrame.idMaskPlane?.data.byteLength ?? 0);
6018
6110
  const coverageBytes = maskFrame.regionMaskCoverage?.entries.reduce((total, entry) => total + entry.data.byteLength, 0) ?? 0;
6019
6111
  const backingBytes = options.resolveMaskFrameBackingBytes
@@ -6071,7 +6163,7 @@ function createPreparedRenderWindow(options) {
6071
6163
  let generation = 0;
6072
6164
  let preparationProgress = 0;
6073
6165
  let nextArtifactRevision = 0;
6074
- const artifactRevisions = new WeakMap();
6166
+ const artifactMetadata = new WeakMap();
6075
6167
  const largestMaskFrameBytesByTier = new Map();
6076
6168
  const preparedMaskFrames = new Map();
6077
6169
  /** Keys whose prepared raster is the coarse tier and owed a fine cook once settled. */
@@ -6095,15 +6187,23 @@ function createPreparedRenderWindow(options) {
6095
6187
  return false;
6096
6188
  }
6097
6189
  observeMaskFrame(frame, key);
6098
- const tier = scheduleOptions.tier ?? getRequestedRasterTier();
6190
+ let tier = scheduleOptions.tier ?? getRequestedRasterTier();
6099
6191
  if (emptyMaskFrameKeys.has(key)) {
6100
6192
  return false;
6101
6193
  }
6102
- if (preparedMaskFrames.has(key)) {
6194
+ const prepared = preparedMaskFrames.get(key);
6195
+ if (prepared) {
6196
+ const metadata = artifactMetadata.get(prepared);
6197
+ const owedDisplayWidth = prepared.kind === PreparedMaskFrameKind.RgbaImage &&
6198
+ metadata?.hasStroke === true &&
6199
+ metadata.displayWidth !== options.resolveMaskDisplayWidth?.();
6103
6200
  const owedFine = coarseMaskFrameKeys.has(key) && tier === PreparedRasterTier.Fine;
6104
- if (!owedFine) {
6201
+ if (!owedFine && !owedDisplayWidth) {
6105
6202
  return false;
6106
6203
  }
6204
+ if (!coarseMaskFrameKeys.has(key)) {
6205
+ tier = PreparedRasterTier.Fine;
6206
+ }
6107
6207
  }
6108
6208
  const pending = pendingMaskFrames.get(key);
6109
6209
  if (pending) {
@@ -6200,11 +6300,22 @@ function createPreparedRenderWindow(options) {
6200
6300
  emitDiagnostics();
6201
6301
  continue;
6202
6302
  }
6303
+ const hasStroke = instructions.some((instruction) => instruction.visible !== false &&
6304
+ instruction.stroke !== undefined &&
6305
+ instruction.stroke.widthUnit === undefined &&
6306
+ instruction.stroke.width > 0 &&
6307
+ instruction.stroke.alpha > 0);
6308
+ const displayWidth = options.resolveMaskDisplayWidth?.();
6203
6309
  void maskFramePreparer
6204
6310
  .prepare({
6311
+ displayWidth,
6205
6312
  instructions,
6206
6313
  key,
6207
- maxRasterWidth: resolveRasterWidthFor(job.tier),
6314
+ maxRasterWidth: resolveRasterWidthFor(job.tier, instructions.reduce((width, instruction) => instruction.visible === false
6315
+ ? width
6316
+ : Math.max(width, instruction.mask !== undefined
6317
+ ? instruction.mask.width
6318
+ : instruction.polygon.width), 0)),
6208
6319
  })
6209
6320
  .then((maskFrame) => {
6210
6321
  inFlightMaskFrames.delete(job);
@@ -6213,10 +6324,25 @@ function createPreparedRenderWindow(options) {
6213
6324
  if (pendingJob === job) {
6214
6325
  pendingMaskFrames.delete(key);
6215
6326
  }
6327
+ const staleDisplayWidth = maskFrame?.kind === PreparedMaskFrameKind.RgbaImage &&
6328
+ hasStroke &&
6329
+ displayWidth !== options.resolveMaskDisplayWidth?.();
6216
6330
  if (isDestroyed ||
6217
6331
  job.generation !== generation ||
6218
- pendingJob !== job) {
6332
+ pendingJob !== job ||
6333
+ staleDisplayWidth) {
6219
6334
  maskFrame?.close();
6335
+ if (staleDisplayWidth &&
6336
+ !isDestroyed &&
6337
+ job.generation === generation &&
6338
+ pendingJob === job) {
6339
+ scheduleMaskFrame(job.frame, job.mediaTime, {
6340
+ tier: upgradeToFine ? PreparedRasterTier.Fine : job.tier,
6341
+ priority: key === activeMaskFrame?.key
6342
+ ? PreparedRenderSchedulePriority.Active
6343
+ : PreparedRenderSchedulePriority.Background,
6344
+ });
6345
+ }
6220
6346
  schedulePreparedTargetBatch();
6221
6347
  emitDiagnostics();
6222
6348
  pumpMaskFrameQueue();
@@ -6232,13 +6358,16 @@ function createPreparedRenderWindow(options) {
6232
6358
  }
6233
6359
  const previous = preparedMaskFrames.get(key);
6234
6360
  if (previous) {
6235
- // an upgrade: the coarse raster gives way to the fine one
6236
6361
  releaseMaskFrame(key);
6237
6362
  options.onMaskFrameEvicted?.(key);
6238
6363
  previous.close();
6239
6364
  }
6240
6365
  preparedMaskFrames.set(key, maskFrame);
6241
- artifactRevisions.set(maskFrame, ++nextArtifactRevision);
6366
+ artifactMetadata.set(maskFrame, {
6367
+ revision: ++nextArtifactRevision,
6368
+ displayWidth,
6369
+ hasStroke,
6370
+ });
6242
6371
  const bytes = chargeMaskFrame(key, maskFrame, job.tier);
6243
6372
  if (job.tier === PreparedRasterTier.Coarse &&
6244
6373
  maskFrameNeedsRasterSizeChange(key, maskFrame, PreparedRasterTier.Fine)) {
@@ -6523,7 +6652,7 @@ function createPreparedRenderWindow(options) {
6523
6652
  getArtifactRevision(mediaTime) {
6524
6653
  const frame = options.detectionTimeline.selectFrame(mediaTime);
6525
6654
  const artifact = frame && preparedMaskFrames.get(getFrameKey(frame));
6526
- return artifact ? (artifactRevisions.get(artifact) ?? 0) : 0;
6655
+ return artifact ? (artifactMetadata.get(artifact)?.revision ?? 0) : 0;
6527
6656
  },
6528
6657
  invalidateRasterSize() {
6529
6658
  if (isDestroyed)
@@ -6557,6 +6686,13 @@ function createPreparedRenderWindow(options) {
6557
6686
  }
6558
6687
  }
6559
6688
  },
6689
+ invalidateMaskDisplayWidth() {
6690
+ if (isDestroyed)
6691
+ return;
6692
+ if (activeMaskFrame) {
6693
+ getFrame(activeMaskFrame.mediaTime, { forcePreparedWindow: true });
6694
+ }
6695
+ },
6560
6696
  getPreparationProgress() {
6561
6697
  return preparationProgress;
6562
6698
  },
@@ -6663,6 +6799,13 @@ function createPreparedRenderWindow(options) {
6663
6799
  return;
6664
6800
  }
6665
6801
  clearPreparedMaskFrames();
6802
+ if (!isDestroyed &&
6803
+ maskStyle &&
6804
+ activeMaskFrame &&
6805
+ isPlayheadFast &&
6806
+ settleTimer === undefined) {
6807
+ armSettleTimer();
6808
+ }
6666
6809
  },
6667
6810
  destroy() {
6668
6811
  if (isDestroyed) {
@@ -6751,8 +6894,11 @@ function createPreparedRenderWindow(options) {
6751
6894
  notifyReadinessWaiters();
6752
6895
  options.onPreparedWindowChange?.();
6753
6896
  }
6754
- function resolveRasterWidthFor(tier) {
6755
- const fine = options.resolveMaxRasterWidth?.();
6897
+ function resolveRasterWidthFor(tier, sourceWidth) {
6898
+ const cap = options.resolveMaxRasterWidth?.();
6899
+ const fine = cap !== undefined && sourceWidth !== undefined && sourceWidth > 0
6900
+ ? Math.min(cap, sourceWidth)
6901
+ : cap;
6756
6902
  if (tier === PreparedRasterTier.Fine || fine === undefined) {
6757
6903
  return fine;
6758
6904
  }
@@ -6772,10 +6918,10 @@ function createPreparedRenderWindow(options) {
6772
6918
  function maskFrameNeedsRasterSizeChange(key, frame, tier = coarseMaskFrameKeys.has(key)
6773
6919
  ? PreparedRasterTier.Coarse
6774
6920
  : PreparedRasterTier.Fine) {
6775
- const maxWidth = resolveRasterWidthFor(tier);
6776
6921
  const sourceWidth = frame.kind === PreparedMaskFrameKind.IdMask
6777
6922
  ? frame.sourceWidth
6778
6923
  : frame.width;
6924
+ const maxWidth = resolveRasterWidthFor(tier, sourceWidth);
6779
6925
  const width = maxWidth !== undefined && maxWidth > 0
6780
6926
  ? Math.min(sourceWidth, Math.max(1, Math.floor(maxWidth)))
6781
6927
  : sourceWidth;
@@ -9215,6 +9361,7 @@ async function createMediaRendererCore(options, providers) {
9215
9361
  ...presentation,
9216
9362
  });
9217
9363
  let currentPresentation = resolvePresentation({
9364
+ annotationAntialiasing: options.annotationAntialiasing,
9218
9365
  annotationOverlayStyle: options.annotationOverlayStyle,
9219
9366
  backgroundColor: options.backgroundColor,
9220
9367
  boxStyle: options.boxStyle,
@@ -10089,6 +10236,7 @@ async function createMediaRendererCore(options, providers) {
10089
10236
  const mediaDimensions = runtimeState.recordMediaMetadata(metadata);
10090
10237
  mediaSize = mediaDimensions;
10091
10238
  mediaScene = await providers.createScene({
10239
+ annotationAntialiasing: currentPresentation.annotationAntialiasing,
10092
10240
  annotationOverlayStyle: currentPresentation.annotationOverlayStyle,
10093
10241
  backgroundColor: currentPresentation.backgroundColor,
10094
10242
  boxStyle: currentPresentation.boxStyle,
@@ -11409,6 +11557,8 @@ function createPixiBoxLayer(options) {
11409
11557
  let retainedContainer = options.container;
11410
11558
  const retainedEntries = new Map();
11411
11559
  const invalidatedDetections = new Set();
11560
+ const renderedRects = new Map();
11561
+ let renderedBoxFrame;
11412
11562
  const usingRetainedEntries = Boolean(options.Container && options.Graphics);
11413
11563
  return {
11414
11564
  createContainer() {
@@ -11437,6 +11587,10 @@ function createPixiBoxLayer(options) {
11437
11587
  boxGraphics?.clear();
11438
11588
  const activeDetectionIndexes = [];
11439
11589
  const activeKeys = new Set();
11590
+ renderedRects.clear();
11591
+ renderedBoxFrame = detectionFrame
11592
+ ? { frame: detectionFrame, rects: renderedRects }
11593
+ : undefined;
11440
11594
  if (boxStyle && detectionFrame) {
11441
11595
  const orderedDetections = detectionFrame.detections
11442
11596
  .map((detection, detectionIndex) => ({ detection, detectionIndex }))
@@ -11468,10 +11622,13 @@ function createPixiBoxLayer(options) {
11468
11622
  display.position?.set?.(0, 0);
11469
11623
  display.clear();
11470
11624
  drawBoxInstruction(display, instruction, viewportScale);
11625
+ retainRenderedBox(detectionIndex, instruction);
11471
11626
  invalidatedDetections.delete(key);
11472
11627
  }
11473
11628
  else {
11474
11629
  drawBoxInstruction(boxGraphics, instruction, viewportScale);
11630
+ if (boxGraphics)
11631
+ retainRenderedBox(detectionIndex, instruction);
11475
11632
  }
11476
11633
  }
11477
11634
  }
@@ -11484,6 +11641,9 @@ function createPixiBoxLayer(options) {
11484
11641
  lastDrawnState = getBoxLayerState(detectionFrame, activeDetectionIndexes);
11485
11642
  return lastDrawnState;
11486
11643
  },
11644
+ getRenderedBoxes() {
11645
+ return renderedBoxFrame;
11646
+ },
11487
11647
  setBoxStyle(nextBoxStyle) {
11488
11648
  if (nextBoxStyle === undefined) {
11489
11649
  return;
@@ -11506,6 +11666,16 @@ function createPixiBoxLayer(options) {
11506
11666
  return true;
11507
11667
  },
11508
11668
  };
11669
+ function retainRenderedBox(detectionIndex, instruction) {
11670
+ if (instruction.rect.width > 0 &&
11671
+ instruction.rect.height > 0 &&
11672
+ ((instruction.fill && (instruction.fill.alpha ?? 1) > 0) ||
11673
+ (instruction.stroke &&
11674
+ instruction.stroke.width > 0 &&
11675
+ (instruction.stroke.alpha ?? 1) > 0))) {
11676
+ renderedRects.set(detectionIndex, instruction.rect);
11677
+ }
11678
+ }
11509
11679
  function ensureRetainedEntry(key) {
11510
11680
  let display = retainedEntries.get(key);
11511
11681
  if (!display) {
@@ -12599,7 +12769,7 @@ function createPixiHeatmapLayer(options) {
12599
12769
  sprite.removeFromParent();
12600
12770
  sprite.destroy();
12601
12771
  if (temporaryTexture)
12602
- texture.destroy(true);
12772
+ releaseTexture(texture);
12603
12773
  }
12604
12774
  positionedSprites.length = 0;
12605
12775
  activeKeys.clear();
@@ -12613,7 +12783,7 @@ function createPixiHeatmapLayer(options) {
12613
12783
  }
12614
12784
  clear();
12615
12785
  for (const entry of cache.values()) {
12616
- entry.texture.destroy(true);
12786
+ releaseTexture(entry.texture);
12617
12787
  entry.image.close();
12618
12788
  }
12619
12789
  cache.clear();
@@ -12651,7 +12821,7 @@ function createPixiHeatmapLayer(options) {
12651
12821
  continue;
12652
12822
  cache.delete(key);
12653
12823
  cachedBytes -= entry.bytes;
12654
- entry.texture.destroy(true);
12824
+ releaseTexture(entry.texture);
12655
12825
  entry.image.close();
12656
12826
  }
12657
12827
  };
@@ -12699,7 +12869,7 @@ function createPixiHeatmapLayer(options) {
12699
12869
  (right.detection.zIndex ?? right.index) || left.index - right.index);
12700
12870
  const result = [];
12701
12871
  let pixels = 0;
12702
- for (const { detection } of orderedDetections) {
12872
+ for (const { detection, index } of orderedDetections) {
12703
12873
  const map = detection.heatmap;
12704
12874
  if (!map || !options.isVisible(detection))
12705
12875
  continue;
@@ -12712,7 +12882,7 @@ function createPixiHeatmapLayer(options) {
12712
12882
  continue;
12713
12883
  }
12714
12884
  pixels += map.width * map.height;
12715
- result.push({ detection, map, renderer });
12885
+ result.push({ detection, detectionIndex: index, map, renderer });
12716
12886
  }
12717
12887
  }
12718
12888
  return result;
@@ -12747,7 +12917,8 @@ function createPixiHeatmapLayer(options) {
12747
12917
  await options.prepareTexture?.(texture);
12748
12918
  }
12749
12919
  catch (error) {
12750
- texture?.destroy(true);
12920
+ if (texture)
12921
+ releaseTexture(texture);
12751
12922
  image.close();
12752
12923
  if (destroyed || job.cancelled || job.generation !== generation) {
12753
12924
  return;
@@ -12765,7 +12936,7 @@ function createPixiHeatmapLayer(options) {
12765
12936
  job.cancelled ||
12766
12937
  job.generation !== generation ||
12767
12938
  !wantedKeys.has(job.key)) {
12768
- texture.destroy(true);
12939
+ releaseTexture(texture);
12769
12940
  image.close();
12770
12941
  return;
12771
12942
  }
@@ -12821,6 +12992,20 @@ function createPixiHeatmapLayer(options) {
12821
12992
  };
12822
12993
  return {
12823
12994
  createContainer: () => container,
12995
+ getActiveFocusArtifacts(frame) {
12996
+ if (!frame || lastFrame !== frame)
12997
+ return [];
12998
+ return positionedSprites.map(({ sprite, detectionIndex }) => ({
12999
+ detectionIndex,
13000
+ texture: sprite.texture,
13001
+ bounds: {
13002
+ x: sprite.x + sprite.width / 2,
13003
+ y: sprite.y + sprite.height / 2,
13004
+ width: sprite.width,
13005
+ height: sprite.height,
13006
+ },
13007
+ }));
13008
+ },
12824
13009
  setRenderers(next) {
12825
13010
  if (next.length === renderers.length &&
12826
13011
  next.every((renderer, index) => renderer === renderers[index])) {
@@ -12886,7 +13071,7 @@ function createPixiHeatmapLayer(options) {
12886
13071
  }
12887
13072
  const maps = visibleMaps(frame);
12888
13073
  const visiblePixelCount = maps.reduce((total, { map }) => total + map.width * map.height, 0);
12889
- for (const { detection, map, renderer } of maps) {
13074
+ for (const { detection, detectionIndex, map, renderer } of maps) {
12890
13075
  const key = keyFor(map, renderer);
12891
13076
  if (shouldPrepareAsync(map, visiblePixelCount)) {
12892
13077
  wantedKeys.add(key);
@@ -12927,6 +13112,7 @@ function createPixiHeatmapLayer(options) {
12927
13112
  positionedSprites.push({
12928
13113
  sprite,
12929
13114
  detectionId: detection.id,
13115
+ detectionIndex,
12930
13116
  x: sprite.x,
12931
13117
  y: sprite.y,
12932
13118
  temporaryTexture,
@@ -12957,6 +13143,10 @@ function createPixiHeatmapLayer(options) {
12957
13143
  container.destroy();
12958
13144
  },
12959
13145
  };
13146
+ function releaseTexture(texture) {
13147
+ options.onTextureRelease?.(texture);
13148
+ texture.destroy(true);
13149
+ }
12960
13150
  }
12961
13151
 
12962
13152
  /**
@@ -13042,6 +13232,268 @@ function installPixiBatchTextureBindings(pixi, renderer) {
13042
13232
  };
13043
13233
  }
13044
13234
 
13235
+ const maskUniformOrigins = new WeakMap();
13236
+ /** Keeps nested mask cutouts aligned and filter disposal scene-owned. */
13237
+ function installPixiFilterBindings(pixi, renderer) {
13238
+ const releaseMaskTarget = installMaskTargetBindings(renderer);
13239
+ if (renderer.name !== "webgpu")
13240
+ return releaseMaskTarget;
13241
+ const system = renderer.filter;
13242
+ const originalApplyFilter = system.applyFilter;
13243
+ let globalGroup;
13244
+ const maskGroups = new Map();
13245
+ const applyFilter = (filter, input, output, clear) => {
13246
+ if (filter.enabled && filter instanceof pixi.MaskFilter) {
13247
+ const source = filter.resources.filterUniforms;
13248
+ let entry = maskGroups.get(filter);
13249
+ if (!entry) {
13250
+ const original = maskUniformOrigins.get(source) ?? source;
13251
+ const owned = new pixi.UniformGroup(original.uniformStructures, {
13252
+ isStatic: original.isStatic,
13253
+ ubo: original.ubo,
13254
+ });
13255
+ maskUniformOrigins.set(owned, original);
13256
+ entry = { original, owned };
13257
+ maskGroups.set(filter, entry);
13258
+ }
13259
+ // Pixi pools MaskFilters across renderers. Their uniform buffers need
13260
+ // one GPU upload owner, even when a second scene borrows the same filter.
13261
+ if (source !== entry.owned) {
13262
+ Object.assign(entry.owned.uniforms, source.uniforms);
13263
+ filter.resources.filterUniforms = entry.owned;
13264
+ if (entry.owned.isStatic)
13265
+ entry.owned.update();
13266
+ }
13267
+ }
13268
+ originalApplyFilter.call(system, filter, input, output, clear);
13269
+ // Pixi replaces group 0 with its renderer-owned group when applying an
13270
+ // enabled filter. Disabled filters use a separate passthrough shader.
13271
+ if (filter.enabled)
13272
+ globalGroup = filter.groups[0];
13273
+ };
13274
+ system.applyFilter = applyFilter;
13275
+ return () => {
13276
+ releaseMaskTarget();
13277
+ if (system.applyFilter === applyFilter) {
13278
+ system.applyFilter = originalApplyFilter;
13279
+ }
13280
+ globalGroup?.destroy();
13281
+ globalGroup = undefined;
13282
+ for (const [filter, { original, owned }] of maskGroups) {
13283
+ if (filter.resources?.filterUniforms === owned) {
13284
+ Object.assign(original.uniforms, owned.uniforms);
13285
+ filter.resources.filterUniforms = original;
13286
+ }
13287
+ const buffer = owned.buffer;
13288
+ owned.buffer = undefined;
13289
+ buffer?.destroy();
13290
+ }
13291
+ maskGroups.clear();
13292
+ };
13293
+ }
13294
+ function installMaskTargetBindings(renderer) {
13295
+ const pipe = renderer.renderPipes?.alphaMask;
13296
+ if (!pipe)
13297
+ return () => undefined;
13298
+ const targets = renderer.renderTarget;
13299
+ const originalExecute = pipe.execute;
13300
+ const captures = [];
13301
+ const execute = (instruction) => {
13302
+ if (!instruction.mask.renderMaskToTexture) {
13303
+ originalExecute.call(pipe, instruction);
13304
+ return;
13305
+ }
13306
+ if (instruction.action === "pushMaskBegin") {
13307
+ captures.push(targets.getBindState());
13308
+ }
13309
+ if (instruction.action !== "pushMaskEnd") {
13310
+ originalExecute.call(pipe, instruction);
13311
+ return;
13312
+ }
13313
+ const binding = captures.pop();
13314
+ const originalPop = targets.pop;
13315
+ // Filters bind their captures without pushing them onto the target stack.
13316
+ // Returning from an alpha-mask capture must restore that bound surface.
13317
+ targets.pop = () => {
13318
+ const target = originalPop.call(targets);
13319
+ return binding ? targets.bind(binding) : target;
13320
+ };
13321
+ try {
13322
+ originalExecute.call(pipe, instruction);
13323
+ }
13324
+ finally {
13325
+ targets.pop = originalPop;
13326
+ }
13327
+ };
13328
+ pipe.execute = execute;
13329
+ return () => {
13330
+ if (pipe.execute === execute)
13331
+ pipe.execute = originalExecute;
13332
+ captures.length = 0;
13333
+ };
13334
+ }
13335
+
13336
+ /** Follows visible heatmap samples while preserving faint anomaly interiors. */
13337
+ function createPixiFocusHeatmapCutout(options) {
13338
+ const geometry = new options.MeshGeometry({
13339
+ indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
13340
+ positions: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
13341
+ shrinkBuffersToFit: true,
13342
+ topology: "triangle-list",
13343
+ uvs: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
13344
+ });
13345
+ const shader = options.Shader.from({
13346
+ gl: { vertex: tintedMaskVertexGlsl, fragment: heatmapCutoutGlsl },
13347
+ gpu: {
13348
+ vertex: { source: untintedMaskVertexWgsl, entryPoint: "mainVertex" },
13349
+ fragment: { source: heatmapCutoutWgsl, entryPoint: "mainFragment" },
13350
+ },
13351
+ resources: {
13352
+ uTexture: options.artifact.texture.source,
13353
+ uSampler: options.artifact.texture.source.style,
13354
+ heatmapCoverageUniforms: {
13355
+ uAntialiasing: { value: 0, type: "f32" },
13356
+ },
13357
+ },
13358
+ });
13359
+ const display = new options.Mesh({ geometry, shader });
13360
+ const uniforms = shader.resources.heatmapCoverageUniforms;
13361
+ return {
13362
+ display,
13363
+ render(artifact, antialiasing = false) {
13364
+ uniforms.uniforms.uAntialiasing = antialiasing ? 1 : 0;
13365
+ const { bounds } = artifact;
13366
+ display.position.set(bounds.x - bounds.width / 2, bounds.y - bounds.height / 2);
13367
+ display.scale.set(bounds.width, bounds.height);
13368
+ display.visible = true;
13369
+ },
13370
+ destroy() {
13371
+ display.removeFromParent();
13372
+ display.destroy();
13373
+ destroyShaderKeepingProgram(shader);
13374
+ const buffer = uniforms.buffer;
13375
+ uniforms.buffer = undefined;
13376
+ buffer?.destroy();
13377
+ geometry.destroy();
13378
+ },
13379
+ };
13380
+ }
13381
+ const heatmapCutoutGlsl = `#version 300 es
13382
+ precision highp float;
13383
+
13384
+ in vec2 vUV;
13385
+ uniform sampler2D uTexture;
13386
+ uniform float uAntialiasing;
13387
+ out vec4 finalColor;
13388
+
13389
+ float visibleSample(ivec2 position, ivec2 dimensions) {
13390
+ return texelFetch(uTexture, clamp(position, ivec2(0), dimensions - ivec2(1)), 0).a > 0.0 ? 1.0 : 0.0;
13391
+ }
13392
+
13393
+ void main(void) {
13394
+ float coverage;
13395
+ if (uAntialiasing > 0.0) {
13396
+ ivec2 dimensions = textureSize(uTexture, 0);
13397
+ vec2 position = vUV * vec2(dimensions) - 0.5;
13398
+ ivec2 base = ivec2(floor(position));
13399
+ vec2 weight = fract(position);
13400
+ coverage = mix(
13401
+ mix(visibleSample(base, dimensions), visibleSample(base + ivec2(1, 0), dimensions), weight.x),
13402
+ mix(visibleSample(base + ivec2(0, 1), dimensions), visibleSample(base + ivec2(1, 1), dimensions), weight.x),
13403
+ weight.y
13404
+ );
13405
+ } else {
13406
+ coverage = texture(uTexture, vUV).a > 0.0 ? 1.0 : 0.0;
13407
+ }
13408
+ finalColor = vec4(coverage);
13409
+ }
13410
+ `;
13411
+ const heatmapCutoutWgsl = `
13412
+ @group(2) @binding(0) var uTexture: texture_2d<f32>;
13413
+ @group(2) @binding(1) var uSampler: sampler;
13414
+ struct HeatmapCoverageUniforms {
13415
+ uAntialiasing: f32,
13416
+ };
13417
+ @group(2) @binding(2) var<uniform> heatmapCoverageUniforms: HeatmapCoverageUniforms;
13418
+
13419
+ fn visibleSample(position: vec2<i32>, dimensions: vec2<i32>) -> f32 {
13420
+ let alpha = textureLoad(uTexture, clamp(position, vec2<i32>(0), dimensions - vec2<i32>(1)), 0).a;
13421
+ return select(0.0, 1.0, alpha > 0.0);
13422
+ }
13423
+
13424
+ @fragment
13425
+ fn mainFragment(@location(0) vUV: vec2<f32>) -> @location(0) vec4<f32> {
13426
+ var coverage: f32;
13427
+ if (heatmapCoverageUniforms.uAntialiasing > 0.0) {
13428
+ let dimensions = vec2<i32>(textureDimensions(uTexture));
13429
+ let position = vUV * vec2<f32>(dimensions) - 0.5;
13430
+ let base = vec2<i32>(floor(position));
13431
+ let weight = fract(position);
13432
+ coverage = mix(
13433
+ mix(visibleSample(base, dimensions), visibleSample(base + vec2<i32>(1, 0), dimensions), weight.x),
13434
+ mix(visibleSample(base + vec2<i32>(0, 1), dimensions), visibleSample(base + vec2<i32>(1, 1), dimensions), weight.x),
13435
+ weight.y
13436
+ );
13437
+ } else {
13438
+ let alpha = textureSampleLevel(uTexture, uSampler, vUV, 0.0).a;
13439
+ coverage = select(0.0, 1.0, alpha > 0.0);
13440
+ }
13441
+ return vec4<f32>(coverage);
13442
+ }
13443
+ `;
13444
+
13445
+ function setPixiViewportBounds(display, bounds, viewport, Rectangle) {
13446
+ if (!viewport) {
13447
+ display.boundsArea = undefined;
13448
+ return;
13449
+ }
13450
+ const left = Math.max(bounds.x, viewport.x);
13451
+ const top = Math.max(bounds.y, viewport.y);
13452
+ const right = Math.min(bounds.x + bounds.width, viewport.x + viewport.width);
13453
+ const bottom = Math.min(bounds.y + bounds.height, viewport.y + viewport.height);
13454
+ const clipped = (display.boundsArea ??= new Rectangle());
13455
+ clipped.x = left;
13456
+ clipped.y = top;
13457
+ clipped.width = Math.max(0, right - left);
13458
+ clipped.height = Math.max(0, bottom - top);
13459
+ }
13460
+ /**
13461
+ * AlphaMask allocates from its mask's full bounds, including geometry outside
13462
+ * the canvas. A neutral parent clips that allocation without changing a
13463
+ * rotated or mirrored child's geometry or texture coordinates.
13464
+ */
13465
+ function syncPixiMaskViewportBounds(container, viewport, Rectangle) {
13466
+ if (!viewport) {
13467
+ container.boundsArea = undefined;
13468
+ return;
13469
+ }
13470
+ let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
13471
+ for (const child of container.children) {
13472
+ if (!child.visible)
13473
+ continue;
13474
+ const bounds = child.getLocalBounds();
13475
+ if (bounds.width <= 0 || bounds.height <= 0)
13476
+ continue;
13477
+ child.updateLocalTransform();
13478
+ const { a, b, c, d, tx, ty } = child.localTransform;
13479
+ left = Math.min(left, tx +
13480
+ Math.min(a * bounds.minX, a * bounds.maxX) +
13481
+ Math.min(c * bounds.minY, c * bounds.maxY));
13482
+ top = Math.min(top, ty +
13483
+ Math.min(b * bounds.minX, b * bounds.maxX) +
13484
+ Math.min(d * bounds.minY, d * bounds.maxY));
13485
+ right = Math.max(right, tx +
13486
+ Math.max(a * bounds.minX, a * bounds.maxX) +
13487
+ Math.max(c * bounds.minY, c * bounds.maxY));
13488
+ bottom = Math.max(bottom, ty +
13489
+ Math.max(b * bounds.minX, b * bounds.maxX) +
13490
+ Math.max(d * bounds.minY, d * bounds.maxY));
13491
+ }
13492
+ setPixiViewportBounds(container, left === Infinity
13493
+ ? { x: viewport.x, y: viewport.y, width: 0, height: 0 }
13494
+ : { x: left, y: top, width: right - left, height: bottom - top }, viewport, Rectangle);
13495
+ }
13496
+
13045
13497
  const MAX_FOCUS_MASK_IDS = 16;
13046
13498
  /**
13047
13499
  * How far the media may travel before a cutout drawn for an earlier frame stops
@@ -13064,6 +13516,11 @@ function createPixiFocusLayer(options) {
13064
13516
  : (options.focusStyle ?? new BaseFocusStyle());
13065
13517
  let focusGraphics;
13066
13518
  let focusMaskGraphics;
13519
+ let heatmapMaskContainer;
13520
+ let heatmapVectorGraphics;
13521
+ let usesHeatmapAlphaMask = false;
13522
+ let viewportBounds;
13523
+ const heatmapCutouts = new Map();
13067
13524
  let idMaskRenderer;
13068
13525
  let focusDisplay;
13069
13526
  let currentAlpha = 0;
@@ -13090,6 +13547,7 @@ function createPixiFocusLayer(options) {
13090
13547
  resetHeldFocus();
13091
13548
  focusGraphics = new options.Graphics();
13092
13549
  focusGraphics.visible = false;
13550
+ syncViewportBounds(true);
13093
13551
  idMaskRenderer = createIdMaskRenderer();
13094
13552
  if (!options.Container || !focusGraphics.setMask) {
13095
13553
  focusDisplay = focusGraphics;
@@ -13118,12 +13576,27 @@ function createPixiFocusLayer(options) {
13118
13576
  hide();
13119
13577
  idMaskRenderer?.destroy();
13120
13578
  idMaskRenderer = undefined;
13579
+ activateVectorMask();
13580
+ heatmapMaskContainer?.destroy({ children: true });
13581
+ heatmapMaskContainer = undefined;
13121
13582
  },
13122
13583
  releaseMaskTexture(texture) {
13123
- if (!isDestroyed)
13584
+ if (!isDestroyed) {
13124
13585
  idMaskRenderer?.releaseTexture(texture.source);
13586
+ const cutouts = heatmapCutouts.get(texture);
13587
+ if (cutouts) {
13588
+ for (const cutout of cutouts.values())
13589
+ cutout.destroy();
13590
+ heatmapCutouts.delete(texture);
13591
+ vectorFocusSignature = null;
13592
+ if (heatmapCutouts.size === 0 && usesHeatmapAlphaMask) {
13593
+ activateVectorMask()?.clear();
13594
+ }
13595
+ }
13596
+ }
13125
13597
  },
13126
13598
  drawFrame(context) {
13599
+ syncViewportBounds();
13127
13600
  if (isDestroyed || !focusStyle || mediaWidth <= 0 || mediaHeight <= 0) {
13128
13601
  hide();
13129
13602
  return;
@@ -13144,11 +13617,15 @@ function createPixiFocusLayer(options) {
13144
13617
  const instruction = resolvedInstruction
13145
13618
  ? {
13146
13619
  ...resolvedInstruction,
13147
- targets: resolvedInstruction.targets.filter(({ detection }) => options.isDetectionVisible?.(detection) ?? true),
13620
+ targets: resolvedInstruction.targets.filter(({ detection, frame }) => frame === context.frame &&
13621
+ (options.isDetectionVisible?.(detection) ?? true)),
13148
13622
  }
13149
13623
  : undefined;
13150
13624
  endHold();
13151
13625
  if (!instruction || instruction.targets.length === 0) {
13626
+ if (usesHeatmapAlphaMask && heldFill) {
13627
+ drawOverlayWithoutCutout(heldFill);
13628
+ }
13152
13629
  resetHeldFocus();
13153
13630
  transitionToHidden();
13154
13631
  return;
@@ -13158,12 +13635,13 @@ function createPixiFocusLayer(options) {
13158
13635
  if (focusDisplay)
13159
13636
  focusDisplay.visible = true;
13160
13637
  if (drawIdMaskFocus(context.idMaskArtifact, instruction)) {
13638
+ activateVectorMask();
13161
13639
  hideVectorFocus();
13162
13640
  markCutoutDrawn(context.mediaTime, context.frame.mediaTime);
13163
13641
  return;
13164
13642
  }
13165
13643
  idMaskRenderer?.hide();
13166
- if (drawVectorFocus(instruction, context.frame)) {
13644
+ if (drawVectorFocus(instruction, context)) {
13167
13645
  markCutoutDrawn(context.mediaTime, context.frame.mediaTime);
13168
13646
  }
13169
13647
  },
@@ -13191,7 +13669,29 @@ function createPixiFocusLayer(options) {
13191
13669
  }
13192
13670
  focusStyle = nextFocusStyle;
13193
13671
  },
13672
+ syncViewportBounds,
13194
13673
  };
13674
+ function syncViewportBounds(force = false) {
13675
+ if (!options.Rectangle)
13676
+ return;
13677
+ const next = options.getViewportBounds?.();
13678
+ if (!force &&
13679
+ next?.x === viewportBounds?.x &&
13680
+ next?.y === viewportBounds?.y &&
13681
+ next?.width === viewportBounds?.width &&
13682
+ next?.height === viewportBounds?.height)
13683
+ return;
13684
+ viewportBounds = next ? { ...next } : undefined;
13685
+ if (focusGraphics) {
13686
+ setPixiViewportBounds(focusGraphics, { x: 0, y: 0, width: mediaWidth, height: mediaHeight }, viewportBounds, options.Rectangle);
13687
+ }
13688
+ syncHeatmapViewportBounds();
13689
+ }
13690
+ function syncHeatmapViewportBounds() {
13691
+ if (heatmapMaskContainer && options.Rectangle) {
13692
+ syncPixiMaskViewportBounds(heatmapMaskContainer, viewportBounds, options.Rectangle);
13693
+ }
13694
+ }
13195
13695
  function drawIdMaskFocus(artifact, instruction) {
13196
13696
  if (!idMaskRenderer ||
13197
13697
  !artifact ||
@@ -13200,17 +13700,19 @@ function createPixiFocusLayer(options) {
13200
13700
  }
13201
13701
  const maskIds = getTargetMaskIds(instruction.targets);
13202
13702
  if (maskIds.length === 0 ||
13703
+ instruction.targets.some((target) => !target.detection.mask) ||
13203
13704
  (!instruction.ambient && maskIds.length !== instruction.targets.length)) {
13204
13705
  return false;
13205
13706
  }
13206
13707
  idMaskRenderer.render(artifact.frame, artifact.texture, maskIds, instruction.fill, instruction.ambient === true);
13207
13708
  return true;
13208
13709
  }
13209
- function drawVectorFocus(instruction, frame) {
13710
+ function drawVectorFocus(instruction, context) {
13210
13711
  if (!focusGraphics) {
13211
13712
  return false;
13212
13713
  }
13213
13714
  const targetsWithGeometry = instruction.targets.filter((target) => target.detection.mask ||
13715
+ target.detection.heatmap ||
13214
13716
  target.detection.rect ||
13215
13717
  target.detection.polygon);
13216
13718
  if (targetsWithGeometry.length === 0) {
@@ -13218,37 +13720,55 @@ function createPixiFocusLayer(options) {
13218
13720
  return false;
13219
13721
  }
13220
13722
  const signature = {
13723
+ annotationAntialiasing: options.getAnnotationAntialiasing?.() ?? false,
13221
13724
  cornerRadius: instruction.fallback?.cornerRadius,
13222
13725
  detections: targetsWithGeometry.map((target) => target.detection),
13223
13726
  fillAlpha: instruction.fill.alpha,
13224
13727
  fillColor: instruction.fill.color,
13225
- frame,
13728
+ frame: context.frame,
13729
+ heatmapsEnabled: (options.getHeatmapRenderers?.().length ?? 0) > 0,
13730
+ heatmapArtifacts: context.heatmapArtifacts ?? [],
13226
13731
  mediaHeight,
13227
13732
  mediaWidth,
13228
- shape: instruction.fallback?.shape,
13733
+ shape: instruction.fallback === null ? null : instruction.fallback?.shape,
13229
13734
  };
13230
13735
  focusGraphics.visible = true;
13231
13736
  if (focusMaskGraphics)
13232
- focusMaskGraphics.visible = true;
13737
+ focusMaskGraphics.visible = !usesHeatmapAlphaMask;
13233
13738
  // Tessellating thousands of mask runs on every draw is what makes this path
13234
13739
  // expensive, and nothing it draws moves while its inputs hold still.
13235
13740
  if (isSameVectorFocus(vectorFocusSignature, signature)) {
13236
13741
  return true;
13237
13742
  }
13238
13743
  vectorFocusSignature = signature;
13744
+ const gpuHeatmapTargets = context.heatmapArtifacts !== undefined &&
13745
+ signature.heatmapsEnabled &&
13746
+ options.AlphaMask &&
13747
+ options.Container &&
13748
+ options.Mesh &&
13749
+ options.MeshGeometry &&
13750
+ options.Shader &&
13751
+ focusMaskGraphics &&
13752
+ focusGraphics.setMask &&
13753
+ targetsWithGeometry.some(({ detection }) => detection.heatmap && !detection.mask);
13754
+ const cutoutGraphics = gpuHeatmapTargets
13755
+ ? activateHeatmapMask(instruction.targets, signature.annotationAntialiasing, signature.heatmapArtifacts)
13756
+ : activateVectorMask();
13239
13757
  focusGraphics.clear();
13240
13758
  focusGraphics.rect(0, 0, mediaWidth, mediaHeight);
13241
13759
  focusGraphics.fill(instruction.fill);
13242
- if (focusMaskGraphics) {
13243
- focusMaskGraphics.clear();
13244
- for (const target of targetsWithGeometry) {
13245
- if (drawCutoutShape(focusMaskGraphics, target, instruction) === "drawn") {
13246
- focusMaskGraphics.fill({ alpha: 1, color: 0xffffff });
13760
+ const vectorTargets = targetsWithGeometry.filter(({ detection }) => !signature.heatmapsEnabled || !detection.heatmap || detection.mask);
13761
+ if (cutoutGraphics) {
13762
+ cutoutGraphics.clear();
13763
+ for (const target of vectorTargets) {
13764
+ if (drawCutoutShape(cutoutGraphics, target, instruction) === "drawn") {
13765
+ cutoutGraphics.fill({ alpha: 1, color: 0xffffff });
13247
13766
  }
13248
13767
  }
13768
+ syncHeatmapViewportBounds();
13249
13769
  return true;
13250
13770
  }
13251
- for (const target of targetsWithGeometry) {
13771
+ for (const target of vectorTargets) {
13252
13772
  if (drawCutoutShape(focusGraphics, target, instruction) === "drawn") {
13253
13773
  focusGraphics.cut();
13254
13774
  }
@@ -13264,7 +13784,7 @@ function createPixiFocusLayer(options) {
13264
13784
  graphics.poly(target.detection.polygon.points.flatMap(({ x, y }) => [x, y]), true);
13265
13785
  return "drawn";
13266
13786
  }
13267
- if (!target.detection.rect) {
13787
+ if (!target.detection.rect || instruction.fallback === null) {
13268
13788
  return "empty";
13269
13789
  }
13270
13790
  const { rect } = target.detection;
@@ -13278,6 +13798,93 @@ function createPixiFocusLayer(options) {
13278
13798
  }
13279
13799
  return "drawn";
13280
13800
  }
13801
+ function activateVectorMask() {
13802
+ clearHeatmapCutouts();
13803
+ if (heatmapMaskContainer)
13804
+ heatmapMaskContainer.visible = false;
13805
+ if (usesHeatmapAlphaMask && focusMaskGraphics) {
13806
+ focusGraphics?.setMask?.({
13807
+ inverse: true,
13808
+ mask: focusMaskGraphics,
13809
+ });
13810
+ usesHeatmapAlphaMask = false;
13811
+ }
13812
+ if (focusMaskGraphics)
13813
+ focusMaskGraphics.visible = true;
13814
+ return focusMaskGraphics;
13815
+ }
13816
+ function activateHeatmapMask(targets, annotationAntialiasing, heatmapArtifacts) {
13817
+ const indexes = new Set(targets
13818
+ .filter(({ detection }) => !detection.mask)
13819
+ .map(({ detectionIndex }) => detectionIndex));
13820
+ const artifacts = heatmapArtifacts.filter((artifact) => indexes.has(artifact.detectionIndex));
13821
+ if (artifacts.length === 0)
13822
+ return activateVectorMask();
13823
+ if (!heatmapMaskContainer) {
13824
+ heatmapMaskContainer = new options.Container();
13825
+ heatmapVectorGraphics = new options.Graphics();
13826
+ heatmapMaskContainer.addChild(heatmapVectorGraphics);
13827
+ focusDisplay.addChild(heatmapMaskContainer);
13828
+ }
13829
+ heatmapMaskContainer.visible = true;
13830
+ if (!usesHeatmapAlphaMask) {
13831
+ focusGraphics?.setMask?.({
13832
+ inverse: true,
13833
+ channel: "alpha",
13834
+ mask: new options.AlphaMask({
13835
+ mask: heatmapMaskContainer,
13836
+ }),
13837
+ });
13838
+ usesHeatmapAlphaMask = true;
13839
+ }
13840
+ if (focusMaskGraphics)
13841
+ focusMaskGraphics.visible = false;
13842
+ const activeTextures = new Map();
13843
+ for (const artifact of artifacts) {
13844
+ let indexes = activeTextures.get(artifact.texture);
13845
+ if (!indexes) {
13846
+ indexes = new Set();
13847
+ activeTextures.set(artifact.texture, indexes);
13848
+ }
13849
+ indexes.add(artifact.detectionIndex);
13850
+ let textureCutouts = heatmapCutouts.get(artifact.texture);
13851
+ if (!textureCutouts) {
13852
+ textureCutouts = new Map();
13853
+ heatmapCutouts.set(artifact.texture, textureCutouts);
13854
+ }
13855
+ let cutout = textureCutouts.get(artifact.detectionIndex);
13856
+ if (!cutout) {
13857
+ cutout = createPixiFocusHeatmapCutout({
13858
+ artifact,
13859
+ Mesh: options.Mesh,
13860
+ MeshGeometry: options.MeshGeometry,
13861
+ Shader: options.Shader,
13862
+ });
13863
+ textureCutouts.set(artifact.detectionIndex, cutout);
13864
+ heatmapMaskContainer.addChild(cutout.display);
13865
+ }
13866
+ cutout.render(artifact, annotationAntialiasing);
13867
+ }
13868
+ for (const [texture, cutouts] of heatmapCutouts) {
13869
+ const activeIndexes = activeTextures.get(texture);
13870
+ for (const [index, cutout] of cutouts) {
13871
+ if (activeIndexes?.has(index))
13872
+ continue;
13873
+ cutout.destroy();
13874
+ cutouts.delete(index);
13875
+ }
13876
+ if (cutouts.size === 0)
13877
+ heatmapCutouts.delete(texture);
13878
+ }
13879
+ return heatmapVectorGraphics;
13880
+ }
13881
+ function clearHeatmapCutouts() {
13882
+ for (const cutouts of heatmapCutouts.values()) {
13883
+ for (const cutout of cutouts.values())
13884
+ cutout.destroy();
13885
+ }
13886
+ heatmapCutouts.clear();
13887
+ }
13281
13888
  function drawMaskCutout(graphics, target) {
13282
13889
  const mask = target.detection.mask;
13283
13890
  if (!mask) {
@@ -13339,7 +13946,8 @@ function createPixiFocusLayer(options) {
13339
13946
  targetAlpha = 1;
13340
13947
  if (focusDisplay)
13341
13948
  focusDisplay.visible = true;
13342
- if (isDrawnCutoutStillCurrent(mediaTime, heldMaskFrameTime)) {
13949
+ if (!usesHeatmapAlphaMask &&
13950
+ isDrawnCutoutStillCurrent(mediaTime, heldMaskFrameTime)) {
13343
13951
  return;
13344
13952
  }
13345
13953
  if (isMaskArtifactOwed) {
@@ -13422,6 +14030,7 @@ function createPixiFocusLayer(options) {
13422
14030
  cutoutMediaTime = null;
13423
14031
  cutoutFrameTime = null;
13424
14032
  drawnOverlayWithoutCutout = fill;
14033
+ activateVectorMask();
13425
14034
  if (idMaskRenderer) {
13426
14035
  hideVectorFocus();
13427
14036
  idMaskRenderer.renderWithoutCutout(fill);
@@ -13481,7 +14090,10 @@ function createPixiFocusLayer(options) {
13481
14090
  }
13482
14091
  function isSameVectorFocus(previous, next) {
13483
14092
  return (previous !== null &&
14093
+ previous.annotationAntialiasing === next.annotationAntialiasing &&
13484
14094
  previous.frame === next.frame &&
14095
+ previous.heatmapsEnabled === next.heatmapsEnabled &&
14096
+ sameHeatmapArtifacts(previous.heatmapArtifacts, next.heatmapArtifacts) &&
13485
14097
  previous.fillAlpha === next.fillAlpha &&
13486
14098
  previous.fillColor === next.fillColor &&
13487
14099
  previous.cornerRadius === next.cornerRadius &&
@@ -13491,6 +14103,18 @@ function isSameVectorFocus(previous, next) {
13491
14103
  previous.detections.length === next.detections.length &&
13492
14104
  previous.detections.every((detection, index) => detection === next.detections[index]));
13493
14105
  }
14106
+ function sameHeatmapArtifacts(left, right) {
14107
+ return (left.length === right.length &&
14108
+ left.every((artifact, index) => {
14109
+ const next = right[index];
14110
+ return (artifact.texture === next.texture &&
14111
+ artifact.detectionIndex === next.detectionIndex &&
14112
+ artifact.bounds.x === next.bounds.x &&
14113
+ artifact.bounds.y === next.bounds.y &&
14114
+ artifact.bounds.width === next.bounds.width &&
14115
+ artifact.bounds.height === next.bounds.height);
14116
+ }));
14117
+ }
13494
14118
  function getTargetMaskIds(targets) {
13495
14119
  return targets
13496
14120
  .filter((target) => target.detection.mask)
@@ -13775,68 +14399,478 @@ fn mainFragment(
13775
14399
  }
13776
14400
  `;
13777
14401
 
13778
- const DEFAULT_PICK_PADDING = 4;
13779
- const POINT_PRECISE_PICK_TARGETS = new Set([
13780
- DetectionPickTarget.Keypoint,
13781
- DetectionPickTarget.Edge,
13782
- DetectionPickTarget.Polyline,
13783
- ]);
13784
- function createPixiInteractionLayer(options) {
13785
- const mode = options.interaction.mode ?? MediaInteractionMode.PausedOnly;
13786
- const pickPadding = options.interaction.padding ?? DEFAULT_PICK_PADDING;
13787
- let container;
13788
- let displayDimensions;
13789
- let currentMediaTime = 0;
13790
- let hoveredPick = null;
13791
- let hoveredPickKey = null;
13792
- let selectedPick = null;
13793
- let selectedPicks = [];
13794
- let marqueeStart = null;
13795
- let marqueeRect = null;
13796
- let didDragMarquee = false;
13797
- let pointerPoint = null;
13798
- let suppressNextTap = false;
13799
- let activeFrame;
13800
- let isDestroyed = false;
13801
- return {
13802
- createDisplay({ width, height }) {
13803
- displayDimensions = { width, height };
13804
- container = new options.Container();
13805
- container.eventMode = "static";
13806
- container.cursor = "default";
13807
- container.hitArea = new options.Rectangle(0, 0, width, height);
13808
- container.on("pointermove", handlePointerMove);
13809
- container.on("globalpointermove", handleGlobalPointerMove);
13810
- container.on("pointerout", handlePointerOut);
13811
- container.on("pointertap", handlePointerTap);
13812
- container.on("pointerdown", handlePointerDown);
13813
- container.on("pointerup", handlePointerUp);
13814
- container.on("pointerupoutside", handlePointerUp);
13815
- return container;
13816
- },
13817
- drawFrame(mediaTime) {
13818
- currentMediaTime = mediaTime;
13819
- activeFrame = options.detectionTimeline.selectFrame(mediaTime);
13820
- if (canHandleInteraction()) {
13821
- clearStalePicks(activeFrame);
13822
- return;
13823
- }
13824
- pointerPoint = null;
13825
- marqueeRect = null;
13826
- setHoveredPick(null);
13827
- if (isDestroyed || mode === MediaInteractionMode.Disabled) {
13828
- setSelectedPick(null);
13829
- return;
14402
+ /*!
14403
+ * The FXAA algorithm is adapted from PixiJS v7.4.3's official shader:
14404
+ * https://github.com/pixijs/pixijs/blob/v7.4.3/packages/filter-fxaa/src/fxaa.frag
14405
+ * It includes alpha in contrast, filters premultiplied RGBA, clamps samples,
14406
+ * limits the span to four pixels and mixes the center back for thin strokes.
14407
+ *
14408
+ * From: https://github.com/mitsuhiko/webgl-meincraft
14409
+ * Copyright (c) 2011 by Armin Ronacher. Some rights reserved.
14410
+ *
14411
+ * Redistribution and use in source and binary forms, with or without
14412
+ * modification, are permitted provided that the following conditions are met:
14413
+ *
14414
+ * Redistributions of source code must retain the above copyright notice,
14415
+ * this list of conditions and the following disclaimer.
14416
+ *
14417
+ * Redistributions in binary form must reproduce the above copyright notice,
14418
+ * this list of conditions and the following disclaimer in the documentation
14419
+ * and/or other materials provided with the distribution.
14420
+ *
14421
+ * The names of the contributors may not be used to endorse or promote
14422
+ * products derived from this software without specific prior written permission.
14423
+ *
14424
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14425
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
14426
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
14427
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
14428
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
14429
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
14430
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
14431
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
14432
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
14433
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
14434
+ * POSSIBILITY OF SUCH DAMAGE.
14435
+ */
14436
+ const annotationFxaaGl = `
14437
+ vec4 applyAnnotationFxaa(vec2 uv) {
14438
+ vec2 pixel = uInputPixel.zw;
14439
+ vec4 center = readColor(uv);
14440
+ float nw = contrastValue(readColor(uv + vec2(-1.0, -1.0) * pixel));
14441
+ float ne = contrastValue(readColor(uv + vec2( 1.0, -1.0) * pixel));
14442
+ float sw = contrastValue(readColor(uv + vec2(-1.0, 1.0) * pixel));
14443
+ float se = contrastValue(readColor(uv + vec2( 1.0, 1.0) * pixel));
14444
+ float middle = contrastValue(center);
14445
+ float low = min(middle, min(min(nw, ne), min(sw, se)));
14446
+ float high = max(middle, max(max(nw, ne), max(sw, se)));
14447
+ if (high - low < max(0.03125, high * 0.0625)) {
14448
+ return center;
14449
+ }
14450
+ vec2 direction = vec2(-((nw + ne) - (sw + se)), (nw + sw) - (ne + se));
14451
+ float reduction = max((nw + ne + sw + se) * 0.03125, 0.0078125);
14452
+ direction *= 1.0 / (min(abs(direction.x), abs(direction.y)) + reduction);
14453
+ direction = clamp(direction, vec2(-4.0), vec2(4.0)) * pixel;
14454
+ vec4 narrow = 0.5 * (
14455
+ readColor(uv - direction / 6.0) +
14456
+ readColor(uv + direction / 6.0)
14457
+ );
14458
+ vec4 wide = 0.5 * narrow + 0.25 * (
14459
+ readColor(uv - direction * 0.5) +
14460
+ readColor(uv + direction * 0.5)
14461
+ );
14462
+ float value = contrastValue(wide);
14463
+ vec4 filtered = value < low || value > high ? narrow : wide;
14464
+ return mix(center, filtered, 0.75);
14465
+ }
14466
+ `;
14467
+ const annotationFxaaWgsl = `
14468
+ fn applyAnnotationFxaa(uv: vec2<f32>) -> vec4<f32> {
14469
+ let pixel = gfu.uInputPixel.zw;
14470
+ let center = readColor(uv);
14471
+ let nw = contrastValue(readColor(uv + vec2<f32>(-1.0, -1.0) * pixel));
14472
+ let ne = contrastValue(readColor(uv + vec2<f32>( 1.0, -1.0) * pixel));
14473
+ let sw = contrastValue(readColor(uv + vec2<f32>(-1.0, 1.0) * pixel));
14474
+ let se = contrastValue(readColor(uv + vec2<f32>( 1.0, 1.0) * pixel));
14475
+ let middle = contrastValue(center);
14476
+ let low = min(middle, min(min(nw, ne), min(sw, se)));
14477
+ let high = max(middle, max(max(nw, ne), max(sw, se)));
14478
+ if (high - low < max(0.03125, high * 0.0625)) { return center; }
14479
+ var direction = vec2<f32>(-((nw + ne) - (sw + se)), (nw + sw) - (ne + se));
14480
+ let reduction = max((nw + ne + sw + se) * 0.03125, 0.0078125);
14481
+ direction *= 1.0 / (min(abs(direction.x), abs(direction.y)) + reduction);
14482
+ direction = clamp(direction, vec2<f32>(-4.0), vec2<f32>(4.0)) * pixel;
14483
+ let narrow = 0.5 * (readColor(uv - direction / 6.0) + readColor(uv + direction / 6.0));
14484
+ let wide = 0.5 * narrow + 0.25 * (readColor(uv - direction * 0.5) + readColor(uv + direction * 0.5));
14485
+ let value = contrastValue(wide);
14486
+ let filtered = select(wide, narrow, value < low || value > high);
14487
+ return mix(center, filtered, 0.75);
14488
+ }
14489
+ `;
14490
+
14491
+ /** Smooths a captured annotation group without reading the media texture. */
14492
+ function createPixiAnnotationAntialiasFilter(options) {
14493
+ const source = createGpuSource(options.maskCoverage ?? false);
14494
+ const filter = options.Filter.from({
14495
+ antialias: false,
14496
+ resolution: options.resolution ?? 1,
14497
+ padding: 2,
14498
+ resources: options.maskCoverage
14499
+ ? {
14500
+ coverageAaUniforms: {
14501
+ uOutputOffset: {
14502
+ type: "vec4<f32>",
14503
+ value: new Float32Array([0, 0, 1, 0]),
14504
+ },
14505
+ },
13830
14506
  }
13831
- // Paused-only interaction gates new picks during playback, but an
13832
- // existing selection keeps following its detection so selection-driven
13833
- // presentation such as focus survives frame advances.
13834
- followSelectedPicks(activeFrame);
13835
- },
13836
- setSelectedDetection(selection) {
13837
- if (selection === null) {
13838
- setSelectedPick(null);
13839
- return null;
14507
+ : {},
14508
+ gl: {
14509
+ vertex: options.maskCoverage
14510
+ ? coverageFilterVert
14511
+ : options.defaultFilterVert,
14512
+ fragment: `#version 300 es
14513
+ precision highp float;
14514
+ in vec2 vTextureCoord;
14515
+ uniform sampler2D uTexture;
14516
+ uniform vec4 uInputClamp;
14517
+ uniform vec4 uInputPixel;
14518
+ uniform vec4 uInputSize;
14519
+ uniform vec4 uOutputFrame;
14520
+ uniform vec4 uOutputTexture;
14521
+ out vec4 finalColor;
14522
+
14523
+ vec4 readColor(vec2 uv) {
14524
+ ${options.maskCoverage
14525
+ ? "return texture(uTexture, clamp(uv, uInputClamp.xy, uInputClamp.zw));"
14526
+ : `vec2 lower = max(uInputClamp.xy, -uOutputFrame.xy * uInputSize.zw + 0.5 * uInputPixel.zw);
14527
+ vec2 upper = min(uInputClamp.zw, (uOutputTexture.xy - uOutputFrame.xy) * uInputSize.zw - 0.5 * uInputPixel.zw);
14528
+ return texture(uTexture, clamp(uv, lower, max(lower, upper)));`}
14529
+ }
14530
+
14531
+ float contrastValue(vec4 color) {
14532
+ return dot(color.rgb, vec3(0.299, 0.587, 0.114)) + color.a;
14533
+ }
14534
+ ${annotationFxaaGl}
14535
+
14536
+ void main(void) {
14537
+ finalColor = applyAnnotationFxaa(vTextureCoord);
14538
+ }
14539
+
14540
+ `,
14541
+ },
14542
+ gpu: {
14543
+ vertex: { entryPoint: "mainVertex", source },
14544
+ fragment: { entryPoint: "mainFragment", source },
14545
+ },
14546
+ });
14547
+ if (options.maskCoverage) {
14548
+ filter.apply = (manager, input, output, clear) => {
14549
+ const uniforms = filter.resources.coverageAaUniforms;
14550
+ const offset = uniforms.uniforms.uOutputOffset;
14551
+ const renderer = manager.renderer;
14552
+ const target = renderer.renderTarget.getRenderTarget(output);
14553
+ const global = renderer.globalUniforms.globalUniformData;
14554
+ // A mask target has its own origin outside Pixi's filter stack. The
14555
+ // global uniforms identify that target after the capture offset pops.
14556
+ offset[0] = global.offset.x;
14557
+ offset[1] = global.offset.y;
14558
+ offset[2] = target.resolution;
14559
+ offset[3] = global.resolution === target.size ? 1 : 0;
14560
+ uniforms.update();
14561
+ manager.applyFilter(filter, input, output, clear);
14562
+ };
14563
+ }
14564
+ return filter;
14565
+ }
14566
+ /** Keeps Pixi's power-of-two capture textures within the backend's limit. */
14567
+ function resolvePixiAnnotationAntialiasResolution(outputResolution, viewport, maxTextureSize, captureScale = 1) {
14568
+ const pooledLimit = 2 ** Math.floor(Math.log2(maxTextureSize));
14569
+ // Nested masks round logical bounds; FXAA adds two padding pixels per edge.
14570
+ const roundedSide = Math.max(1, Math.ceil(viewport.width) + 6, Math.ceil(viewport.height) + 6);
14571
+ return Math.min(outputResolution * captureScale, Math.max(1, pooledLimit - 2) / roundedSide);
14572
+ }
14573
+ const coverageFilterVert = `#version 300 es
14574
+ precision highp float;
14575
+ in vec2 aPosition;
14576
+ out vec2 vTextureCoord;
14577
+ uniform vec4 uOutputFrame;
14578
+ uniform vec4 uOutputTexture;
14579
+ uniform vec4 uInputSize;
14580
+ uniform vec4 uGlobalFrame;
14581
+ uniform vec4 uOutputOffset;
14582
+
14583
+ void main(void) {
14584
+ vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
14585
+ if (uOutputOffset.w > 0.0) {
14586
+ position -= uOutputOffset.xy - uGlobalFrame.xy / uOutputOffset.z;
14587
+ }
14588
+ gl_Position = vec4(
14589
+ position.x * (2.0 / uOutputTexture.x) - 1.0,
14590
+ position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z,
14591
+ 0.0, 1.0
14592
+ );
14593
+ vTextureCoord = aPosition * (uOutputFrame.zw * uInputSize.zw);
14594
+ }
14595
+ `;
14596
+ function createGpuSource(maskCoverage) {
14597
+ return `
14598
+ struct GlobalFilterUniforms {
14599
+ uInputSize: vec4<f32>,
14600
+ uInputPixel: vec4<f32>,
14601
+ uInputClamp: vec4<f32>,
14602
+ uOutputFrame: vec4<f32>,
14603
+ uGlobalFrame: vec4<f32>,
14604
+ uOutputTexture: vec4<f32>,
14605
+ }
14606
+ @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
14607
+ @group(0) @binding(1) var uTexture: texture_2d<f32>;
14608
+ @group(0) @binding(2) var uSampler: sampler;
14609
+ ${maskCoverage
14610
+ ? `struct CoverageAaUniforms { uOutputOffset: vec4<f32>, }
14611
+ @group(1) @binding(0) var<uniform> coverageAaUniforms: CoverageAaUniforms;`
14612
+ : ""}
14613
+
14614
+ struct FilterOutput {
14615
+ @builtin(position) position: vec4<f32>,
14616
+ @location(0) uv: vec2<f32>,
14617
+ }
14618
+
14619
+ @vertex
14620
+ fn mainVertex(@location(0) aPosition: vec2<f32>) -> FilterOutput {
14621
+ var out: FilterOutput;
14622
+ var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
14623
+ ${maskCoverage
14624
+ ? `let offset = coverageAaUniforms.uOutputOffset;
14625
+ if (offset.w > 0.0) {
14626
+ position -= offset.xy - gfu.uGlobalFrame.xy / offset.z;
14627
+ }`
14628
+ : ""}
14629
+ out.position = vec4<f32>(
14630
+ position.x * (2.0 / gfu.uOutputTexture.x) - 1.0,
14631
+ position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z,
14632
+ 0.0, 1.0
14633
+ );
14634
+ out.uv = aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
14635
+ return out;
14636
+ }
14637
+
14638
+ fn readColor(uv: vec2<f32>) -> vec4<f32> {
14639
+ ${maskCoverage
14640
+ ? "return textureSampleLevel(uTexture, uSampler, clamp(uv, gfu.uInputClamp.xy, gfu.uInputClamp.zw), 0.0);"
14641
+ : `let lower = max(gfu.uInputClamp.xy, -gfu.uOutputFrame.xy * gfu.uInputSize.zw + 0.5 * gfu.uInputPixel.zw);
14642
+ let upper = min(gfu.uInputClamp.zw, (gfu.uOutputTexture.xy - gfu.uOutputFrame.xy) * gfu.uInputSize.zw - 0.5 * gfu.uInputPixel.zw);
14643
+ return textureSampleLevel(uTexture, uSampler, clamp(uv, lower, max(lower, upper)), 0.0);`}
14644
+ }
14645
+
14646
+ fn contrastValue(color: vec4<f32>) -> f32 {
14647
+ return dot(color.rgb, vec3<f32>(0.299, 0.587, 0.114)) + color.a;
14648
+ }
14649
+ ${annotationFxaaWgsl}
14650
+
14651
+ @fragment
14652
+ fn mainFragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
14653
+ return applyAnnotationFxaa(uv);
14654
+ }
14655
+ `;
14656
+ }
14657
+
14658
+ var PixiSceneLayerKind;
14659
+ (function (PixiSceneLayerKind) {
14660
+ PixiSceneLayerKind["Media"] = "media";
14661
+ PixiSceneLayerKind["Depth"] = "depth";
14662
+ PixiSceneLayerKind["Heatmap"] = "heatmap";
14663
+ PixiSceneLayerKind["Mask"] = "mask";
14664
+ PixiSceneLayerKind["Box"] = "box";
14665
+ PixiSceneLayerKind["Vector"] = "vector";
14666
+ PixiSceneLayerKind["Region"] = "region";
14667
+ PixiSceneLayerKind["Focus"] = "focus";
14668
+ PixiSceneLayerKind["Preview"] = "preview";
14669
+ PixiSceneLayerKind["Guide"] = "guide";
14670
+ PixiSceneLayerKind["Handle"] = "handle";
14671
+ PixiSceneLayerKind["Interaction"] = "interaction";
14672
+ PixiSceneLayerKind["Label"] = "label";
14673
+ })(PixiSceneLayerKind || (PixiSceneLayerKind = {}));
14674
+ function createPixiSceneLayerSlot(kind, order = defaultLayerOrder(kind)) {
14675
+ let display;
14676
+ return {
14677
+ getDisplay() {
14678
+ return display;
14679
+ },
14680
+ kind,
14681
+ order,
14682
+ setDisplay(nextDisplay) {
14683
+ display = nextDisplay;
14684
+ },
14685
+ };
14686
+ }
14687
+ function syncPixiSceneLayerChildren(scene, slots) {
14688
+ if (!scene) {
14689
+ return;
14690
+ }
14691
+ const displays = [...slots]
14692
+ .sort((left, right) => left.order - right.order)
14693
+ .map((slot) => slot.getDisplay())
14694
+ .filter((display) => display !== undefined);
14695
+ clearSceneChildren(scene);
14696
+ if (displays.length === 0) {
14697
+ return;
14698
+ }
14699
+ scene.addChild(...displays);
14700
+ }
14701
+ function defaultLayerOrder(kind) {
14702
+ switch (kind) {
14703
+ case PixiSceneLayerKind.Media:
14704
+ return 0;
14705
+ case PixiSceneLayerKind.Focus:
14706
+ return 10;
14707
+ case PixiSceneLayerKind.Depth:
14708
+ return 25;
14709
+ case PixiSceneLayerKind.Heatmap:
14710
+ return 50;
14711
+ case PixiSceneLayerKind.Mask:
14712
+ return 100;
14713
+ case PixiSceneLayerKind.Box:
14714
+ return 200;
14715
+ case PixiSceneLayerKind.Vector:
14716
+ return 250;
14717
+ case PixiSceneLayerKind.Region:
14718
+ return 275;
14719
+ case PixiSceneLayerKind.Preview:
14720
+ return 400;
14721
+ case PixiSceneLayerKind.Guide:
14722
+ return 500;
14723
+ case PixiSceneLayerKind.Handle:
14724
+ return 750;
14725
+ case PixiSceneLayerKind.Interaction:
14726
+ return 700;
14727
+ case PixiSceneLayerKind.Label:
14728
+ return 800;
14729
+ default:
14730
+ return 350;
14731
+ }
14732
+ }
14733
+ function clearSceneChildren(scene) {
14734
+ if (typeof scene.removeChildren === "function") {
14735
+ scene.removeChildren();
14736
+ return;
14737
+ }
14738
+ const fakeScene = scene;
14739
+ if (Array.isArray(fakeScene.children)) {
14740
+ fakeScene.children.splice(0);
14741
+ }
14742
+ }
14743
+
14744
+ /** Keeps media pixels outside AA while retaining the scene's layer order. */
14745
+ function createPixiAnnotationAntialiasLayer(options) {
14746
+ const groups = [];
14747
+ let filter;
14748
+ let captureResolution = 1;
14749
+ return {
14750
+ getFxaaFilter() {
14751
+ return options.getEnabled() ? ensureFilter() : null;
14752
+ },
14753
+ setResolution(resolution) {
14754
+ captureResolution = resolution;
14755
+ if (filter)
14756
+ filter.resolution = resolution;
14757
+ },
14758
+ sync(scene, slots) {
14759
+ if (!scene)
14760
+ return;
14761
+ for (const group of groups) {
14762
+ group.removeChildren();
14763
+ group.filters = null;
14764
+ }
14765
+ if (!options.getEnabled()) {
14766
+ syncPixiSceneLayerChildren(scene, slots);
14767
+ return;
14768
+ }
14769
+ scene.removeChildren();
14770
+ let index = 0;
14771
+ let group;
14772
+ for (const slot of [...slots].sort((left, right) => left.order - right.order)) {
14773
+ const display = slot.getDisplay();
14774
+ if (!display)
14775
+ continue;
14776
+ if (slot.kind === PixiSceneLayerKind.Media ||
14777
+ slot.kind === PixiSceneLayerKind.Region ||
14778
+ slot.kind === PixiSceneLayerKind.Label) {
14779
+ // Regions smooth coverage separately from their copied video.
14780
+ // Labels capture backgrounds separately so text stays sharp.
14781
+ group = undefined;
14782
+ scene.addChild(display);
14783
+ continue;
14784
+ }
14785
+ if (!group) {
14786
+ const sharedFilter = ensureFilter();
14787
+ group = groups[index] ??= new options.Container();
14788
+ index += 1;
14789
+ group.filters = [sharedFilter];
14790
+ scene.addChild(group);
14791
+ }
14792
+ group.addChild(display);
14793
+ }
14794
+ },
14795
+ destroy() {
14796
+ for (const group of groups) {
14797
+ group.filters = null;
14798
+ if (!group.parent)
14799
+ group.destroy({ children: false });
14800
+ }
14801
+ filter?.destroy();
14802
+ },
14803
+ };
14804
+ function ensureFilter() {
14805
+ return (filter ??= createPixiAnnotationAntialiasFilter({
14806
+ ...options,
14807
+ resolution: captureResolution,
14808
+ }));
14809
+ }
14810
+ }
14811
+
14812
+ const DEFAULT_PICK_PADDING = 4;
14813
+ const POINT_PRECISE_PICK_TARGETS = new Set([
14814
+ DetectionPickTarget.Keypoint,
14815
+ DetectionPickTarget.Edge,
14816
+ DetectionPickTarget.Polyline,
14817
+ ]);
14818
+ function createPixiInteractionLayer(options) {
14819
+ const mode = options.interaction.mode ?? MediaInteractionMode.PausedOnly;
14820
+ const pickPadding = options.interaction.padding ?? DEFAULT_PICK_PADDING;
14821
+ let container;
14822
+ let displayDimensions;
14823
+ let currentMediaTime = 0;
14824
+ let hoveredPick = null;
14825
+ let hoveredPickKey = null;
14826
+ let selectedPick = null;
14827
+ let selectedPicks = [];
14828
+ let marqueeStart = null;
14829
+ let marqueeRect = null;
14830
+ let didDragMarquee = false;
14831
+ let pointerPoint = null;
14832
+ let suppressNextTap = false;
14833
+ let activeFrame;
14834
+ let isDestroyed = false;
14835
+ return {
14836
+ createDisplay({ width, height }) {
14837
+ displayDimensions = { width, height };
14838
+ container = new options.Container();
14839
+ container.eventMode = "static";
14840
+ container.cursor = "default";
14841
+ container.hitArea = new options.Rectangle(0, 0, width, height);
14842
+ container.on("pointermove", handlePointerMove);
14843
+ container.on("globalpointermove", handleGlobalPointerMove);
14844
+ container.on("pointerout", handlePointerOut);
14845
+ container.on("pointertap", handlePointerTap);
14846
+ container.on("pointerdown", handlePointerDown);
14847
+ container.on("pointerup", handlePointerUp);
14848
+ container.on("pointerupoutside", handlePointerUp);
14849
+ return container;
14850
+ },
14851
+ drawFrame(mediaTime) {
14852
+ currentMediaTime = mediaTime;
14853
+ activeFrame = options.detectionTimeline.selectFrame(mediaTime);
14854
+ if (canHandleInteraction()) {
14855
+ clearStalePicks(activeFrame);
14856
+ return;
14857
+ }
14858
+ pointerPoint = null;
14859
+ marqueeRect = null;
14860
+ setHoveredPick(null);
14861
+ if (isDestroyed || mode === MediaInteractionMode.Disabled) {
14862
+ setSelectedPick(null);
14863
+ return;
14864
+ }
14865
+ // Paused-only interaction gates new picks during playback, but an
14866
+ // existing selection keeps following its detection so selection-driven
14867
+ // presentation such as focus survives frame advances.
14868
+ followSelectedPicks(activeFrame);
14869
+ },
14870
+ setSelectedDetection(selection) {
14871
+ if (selection === null) {
14872
+ setSelectedPick(null);
14873
+ return null;
13840
14874
  }
13841
14875
  if (!canHandleInteraction()) {
13842
14876
  setSelectedPick(null);
@@ -14344,7 +15378,7 @@ function createPixiMaskScreenStrokeRenderer(options) {
14344
15378
  let strokes = frame.screenStrokes ?? [];
14345
15379
  if (overrides) {
14346
15380
  strokes = overrides.flatMap(({ detectionIndex, stroke }) => {
14347
- if (stroke?.widthUnit !== "screen" ||
15381
+ if (stroke?.widthUnit === undefined ||
14348
15382
  stroke.width <= 0 ||
14349
15383
  stroke.alpha <= 0)
14350
15384
  return [];
@@ -14366,21 +15400,36 @@ function createPixiMaskScreenStrokeRenderer(options) {
14366
15400
  hide();
14367
15401
  return;
14368
15402
  }
14369
- const signature = `${viewportScale}:${strokes.map(({ detectionIndex, width, color, alpha }) => `${detectionIndex}:${width}:${color}:${alpha}`).join(";")}`;
15403
+ const signature = `${viewportScale}:${strokes.map(({ detectionIndex, width, widthUnit, alignment, color, alpha }) => `${detectionIndex}:${width}:${widthUnit}:${alignment}:${color}:${alpha}`).join(";")}`;
14370
15404
  if (lastFrame !== frame || lastSignature !== signature) {
14371
15405
  display.clear();
14372
15406
  const scaleX = options.mediaWidth / frame.width;
14373
15407
  const scaleY = options.mediaHeight / frame.height;
14374
15408
  for (const stroke of strokes) {
14375
- for (const path of stroke.paths) {
14376
- if (path.length < 6)
14377
- continue;
14378
- display.moveTo(path[0] * scaleX, path[1] * scaleY);
14379
- for (let index = 2; index < path.length; index += 2)
14380
- display.lineTo(path[index] * scaleX, path[index + 1] * scaleY);
14381
- display.closePath();
15409
+ for (const { paths, alignment } of resolveContourGroups(stroke)) {
15410
+ for (const path of paths) {
15411
+ if (path.length < 6)
15412
+ continue;
15413
+ display.moveTo(path[0] * scaleX, path[1] * scaleY);
15414
+ for (let index = 2; index < path.length; index += 2)
15415
+ display.lineTo(path[index] * scaleX, path[index + 1] * scaleY);
15416
+ display.closePath();
15417
+ }
15418
+ display.stroke(resolvePixiStroke({
15419
+ ...stroke,
15420
+ alignment,
15421
+ width: stroke.widthUnit === "mask"
15422
+ ? (2 *
15423
+ stroke.width *
15424
+ options.mediaWidth *
15425
+ viewportScale) /
15426
+ (frame.kind === PreparedMaskFrameKind.IdMask
15427
+ ? frame.sourceWidth
15428
+ : frame.width)
15429
+ : stroke.width,
15430
+ join: "round",
15431
+ }, viewportScale));
14382
15432
  }
14383
- display.stroke(resolvePixiStroke({ ...stroke, join: "round" }, viewportScale));
14384
15433
  }
14385
15434
  lastFrame = frame;
14386
15435
  lastSignature = signature;
@@ -14389,22 +15438,56 @@ function createPixiMaskScreenStrokeRenderer(options) {
14389
15438
  },
14390
15439
  };
14391
15440
  }
15441
+ function resolveContourGroups(stroke) {
15442
+ if (stroke.alignment === undefined ||
15443
+ stroke.alignment === StrokeAlignment.Center) {
15444
+ return [{ paths: stroke.paths, alignment: stroke.alignment }];
15445
+ }
15446
+ const outer = [];
15447
+ const holes = [];
15448
+ // Pixi aligns each contour to its enclosed area. Hole alignment must reverse
15449
+ // so Inside and Outside remain relative to the mask's foreground pixels.
15450
+ for (const path of stroke.paths) {
15451
+ let area = 0;
15452
+ for (let index = 0, previous = path.length - 2; index < path.length; index += 2) {
15453
+ area +=
15454
+ path[previous] * path[index + 1] - path[index] * path[previous + 1];
15455
+ previous = index;
15456
+ }
15457
+ (area < 0 ? holes : outer).push(path);
15458
+ }
15459
+ return [
15460
+ { paths: outer, alignment: stroke.alignment },
15461
+ {
15462
+ paths: holes,
15463
+ alignment: stroke.alignment === StrokeAlignment.Outside
15464
+ ? StrokeAlignment.Inside
15465
+ : StrokeAlignment.Outside,
15466
+ },
15467
+ ].filter(({ paths }) => paths.length > 0);
15468
+ }
14392
15469
 
14393
15470
  const coverageBody = `
14394
- return clamp(0.5 + (width - distance) / pixelWidth, 0.0, 1.0)
14395
- - clamp(0.5 - (width + distance) / pixelWidth, 0.0, 1.0);
15471
+ return clamp(0.5 + (width * (1.0 - alignment) - distance) / pixelWidth, 0.0, 1.0)
15472
+ - clamp(0.5 - (width * alignment + distance) / pixelWidth, 0.0, 1.0);
14396
15473
  `;
14397
15474
  const distanceBody = `
14398
15475
  return max(max(offset - position, position - offset - 1.0), 0.0);
14399
15476
  `;
14400
15477
  const radiusBody = `
14401
- if (fractionalWidth > 0.0) {
14402
- return min(max(floor(maxWidth), ceil(fractionalWidth + pixelWidth * 0.5)), ${MAX_ID_MASK_STROKE_WIDTH}.0);
15478
+ if (maxWidth < 0.0) {
15479
+ return 0.0;
14403
15480
  }
14404
- return min(floor(maxWidth), ${MAX_ID_MASK_STROKE_WIDTH}.0);
15481
+ return min(ceil(maxWidth + pixelWidth * 0.5), ${MAX_ID_MASK_STROKE_WIDTH + 1}.0);
14405
15482
  `;
14406
15483
  const idMaskStrokeCoverageGlsl = `
14407
- float subtexelStrokeCoverage(float distance, float width, float pixelWidth) {
15484
+ float sampleMaskIdCell(ivec2 cell) {
15485
+ int x = clamp(cell.x, 0, int(uTextureSize.x) - 1);
15486
+ int y = clamp(cell.y, 0, int(uTextureSize.y) - 1);
15487
+ return floor(texelFetch(uTexture, ivec2(x, y), 0).r * 255.0 + 0.5);
15488
+ }
15489
+
15490
+ float subtexelStrokeCoverage(float distance, float width, float alignment, float pixelWidth) {
14408
15491
  ${coverageBody}
14409
15492
  }
14410
15493
 
@@ -14412,42 +15495,55 @@ float neighborCellDistance(float offset, float position) {
14412
15495
  ${distanceBody}
14413
15496
  }
14414
15497
 
14415
- float strokeScanRadius(float maxWidth, float fractionalWidth, float pixelWidth) {
15498
+ float strokeScanRadius(float maxWidth, float pixelWidth) {
14416
15499
  ${radiusBody}
14417
15500
  }
14418
15501
 
14419
- bool isBoundary(float centerId, vec2 texel) {
14420
- return
14421
- differs(sampleMaskId(vUV + vec2(texel.x, 0.0)), centerId) ||
14422
- differs(sampleMaskId(vUV + vec2(-texel.x, 0.0)), centerId) ||
14423
- differs(sampleMaskId(vUV + vec2(0.0, texel.y)), centerId) ||
14424
- differs(sampleMaskId(vUV + vec2(0.0, -texel.y)), centerId);
15502
+ float strokeWidthInTexels(float maskId, float pixelWidth) {
15503
+ float alignment = readStrokeAlignment(maskId);
15504
+ float sideFraction = max(alignment, 1.0 - alignment);
15505
+ return min(readStrokeWidth(maskId) * pixelWidth * uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0 / sideFraction);
14425
15506
  }
14426
15507
 
14427
- float innerStrokeCoverage(float centerId, vec2 texel, vec2 cell, float width, float pixelWidth) {
14428
- if (width >= 1.0) {
14429
- return isBoundary(centerId, texel) ? 1.0 : 0.0;
14430
- }
15508
+ float innerStrokeCoverage(float centerId, ivec2 sourceCell, vec2 cell, float width, float pixelWidth) {
15509
+ if (width <= 0.0) return 0.0;
15510
+ float alignment = readStrokeAlignment(centerId);
15511
+ int radius = int(strokeScanRadius(width * alignment, pixelWidth));
15512
+ float nearestDistance = width * alignment + pixelWidth * 0.5;
15513
+ float minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
14431
15514
 
14432
- float coverage = 0.0;
14433
- if (differs(sampleMaskId(vUV + vec2(texel.x, 0.0)), centerId)) {
14434
- coverage = max(coverage, subtexelStrokeCoverage(1.0 - cell.x, width, pixelWidth));
14435
- }
14436
- if (differs(sampleMaskId(vUV + vec2(-texel.x, 0.0)), centerId)) {
14437
- coverage = max(coverage, subtexelStrokeCoverage(cell.x, width, pixelWidth));
14438
- }
14439
- if (differs(sampleMaskId(vUV + vec2(0.0, texel.y)), centerId)) {
14440
- coverage = max(coverage, subtexelStrokeCoverage(1.0 - cell.y, width, pixelWidth));
14441
- }
14442
- if (differs(sampleMaskId(vUV + vec2(0.0, -texel.y)), centerId)) {
14443
- coverage = max(coverage, subtexelStrokeCoverage(cell.y, width, pixelWidth));
15515
+ for (int offsetY = radius; offsetY >= -radius; offsetY -= 1) {
15516
+ for (int offsetX = radius; offsetX >= -radius; offsetX -= 1) {
15517
+ if (offsetX == 0 && offsetY == 0) {
15518
+ continue;
15519
+ }
15520
+ float distance = max(
15521
+ neighborCellDistance(float(offsetX), cell.x),
15522
+ neighborCellDistance(float(offsetY), cell.y)
15523
+ );
15524
+ if (distance >= nearestDistance) {
15525
+ continue;
15526
+ }
15527
+ float maskId = sampleMaskIdCell(sourceCell + ivec2(offsetX, offsetY));
15528
+ if (differs(maskId, centerId)) {
15529
+ nearestDistance = distance;
15530
+ if (distance <= minimumDistance) {
15531
+ return subtexelStrokeCoverage(-distance, width, alignment, pixelWidth);
15532
+ }
15533
+ }
15534
+ }
14444
15535
  }
14445
- return coverage;
15536
+ return subtexelStrokeCoverage(-nearestDistance, width, alignment, pixelWidth);
14446
15537
  }
14447
15538
 
14448
15539
  // Descending offsets give the outer border its palette priority.
14449
- vec2 findNeighborStroke(float centerId, vec2 texel, vec2 cell, float pixelWidth) {
14450
- int radius = int(strokeScanRadius(uMaxStrokeWidth, uMaxFractionalStrokeWidth, pixelWidth));
15540
+ vec2 findNeighborStroke(float centerId, ivec2 sourceCell, vec2 cell, float pixelWidth) {
15541
+ float maxWidth = min(uMaxStrokeWidth * pixelWidth * uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0);
15542
+ int radius = int(strokeScanRadius(maxWidth, pixelWidth));
15543
+ vec2 stroke = vec2(0.0);
15544
+ float nearestDistance = maxWidth + pixelWidth * 0.5;
15545
+ float minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
15546
+ if (uBorderEnabled <= 0.5) return stroke;
14451
15547
 
14452
15548
  for (int offsetY = radius; offsetY >= -radius; offsetY -= 1) {
14453
15549
  for (int offsetX = radius; offsetX >= -radius; offsetX -= 1) {
@@ -14455,35 +15551,44 @@ vec2 findNeighborStroke(float centerId, vec2 texel, vec2 cell, float pixelWidth)
14455
15551
  continue;
14456
15552
  }
14457
15553
 
14458
- float maskId = sampleMaskId(vUV + vec2(float(offsetX), float(offsetY)) * texel);
14459
- if (maskId < 0.5 || !differs(maskId, centerId)) {
15554
+ float distance = max(
15555
+ neighborCellDistance(float(offsetX), cell.x),
15556
+ neighborCellDistance(float(offsetY), cell.y)
15557
+ );
15558
+ if (distance >= nearestDistance) {
15559
+ continue;
15560
+ }
15561
+ float maskId = sampleMaskIdCell(sourceCell + ivec2(offsetX, offsetY));
15562
+ if (maskId < 0.5 || !differs(maskId, centerId) ||
15563
+ (stroke.x > 0.5 && differs(maskId, stroke.x))) {
14460
15564
  continue;
14461
15565
  }
14462
15566
 
14463
- float width = readStrokeWidth(maskId);
14464
- if (width >= 1.0) {
14465
- float distance = max(abs(float(offsetX)), abs(float(offsetY)));
14466
- if (width >= distance && readStroke(maskId).a > 0.0) {
14467
- return vec2(maskId, 1.0);
14468
- }
14469
- } else if (width > 0.0 && readStroke(maskId).a > 0.0) {
14470
- float distance = max(
14471
- neighborCellDistance(float(offsetX), cell.x),
14472
- neighborCellDistance(float(offsetY), cell.y)
14473
- );
14474
- float coverage = subtexelStrokeCoverage(distance, width, pixelWidth);
15567
+ float width = strokeWidthInTexels(maskId, pixelWidth);
15568
+ if (width > 0.0 && readStroke(maskId).a > 0.0) {
15569
+ float coverage = subtexelStrokeCoverage(distance, width, readStrokeAlignment(maskId), pixelWidth);
14475
15570
  if (coverage > 0.0) {
14476
- return vec2(maskId, coverage);
15571
+ stroke = vec2(maskId, coverage);
15572
+ nearestDistance = distance;
15573
+ if (distance <= minimumDistance) {
15574
+ return stroke;
15575
+ }
14477
15576
  }
14478
15577
  }
14479
15578
  }
14480
15579
  }
14481
15580
 
14482
- return vec2(0.0);
15581
+ return stroke;
14483
15582
  }
14484
15583
  `;
14485
15584
  const idMaskStrokeCoverageWgsl = `
14486
- fn subtexelStrokeCoverage(distance: f32, width: f32, pixelWidth: f32) -> f32 {
15585
+ fn sampleMaskIdCell(cell: vec2<i32>) -> f32 {
15586
+ let x = clamp(cell.x, 0, i32(maskUniforms.uTextureSize.x) - 1);
15587
+ let y = clamp(cell.y, 0, i32(maskUniforms.uTextureSize.y) - 1);
15588
+ return floor(textureLoad(uTexture, vec2<i32>(x, y), 0).r * 255.0 + 0.5);
15589
+ }
15590
+
15591
+ fn subtexelStrokeCoverage(distance: f32, width: f32, alignment: f32, pixelWidth: f32) -> f32 {
14487
15592
  ${coverageBody}
14488
15593
  }
14489
15594
 
@@ -14491,45 +15596,55 @@ fn neighborCellDistance(offset: f32, position: f32) -> f32 {
14491
15596
  ${distanceBody}
14492
15597
  }
14493
15598
 
14494
- fn strokeScanRadius(maxWidth: f32, fractionalWidth: f32, pixelWidth: f32) -> f32 {
15599
+ fn strokeScanRadius(maxWidth: f32, pixelWidth: f32) -> f32 {
14495
15600
  ${radiusBody}
14496
15601
  }
14497
15602
 
14498
- fn isBoundary(uv: vec2<f32>, centerId: f32, texel: vec2<f32>) -> bool {
14499
- return
14500
- differs(sampleMaskId(uv + vec2<f32>(texel.x, 0.0)), centerId) ||
14501
- differs(sampleMaskId(uv + vec2<f32>(-texel.x, 0.0)), centerId) ||
14502
- differs(sampleMaskId(uv + vec2<f32>(0.0, texel.y)), centerId) ||
14503
- differs(sampleMaskId(uv + vec2<f32>(0.0, -texel.y)), centerId);
15603
+ fn strokeWidthInTexels(maskId: f32, pixelWidth: f32) -> f32 {
15604
+ let alignment = readStrokeAlignment(maskId);
15605
+ let sideFraction = max(alignment, 1.0 - alignment);
15606
+ return min(readStrokeWidth(maskId) * pixelWidth * maskUniforms.uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0 / sideFraction);
14504
15607
  }
14505
15608
 
14506
- fn innerStrokeCoverage(uv: vec2<f32>, centerId: f32, texel: vec2<f32>, cell: vec2<f32>, width: f32, pixelWidth: f32) -> f32 {
14507
- if (width >= 1.0) {
14508
- if (isBoundary(uv, centerId, texel)) {
14509
- return 1.0;
14510
- }
14511
- return 0.0;
14512
- }
15609
+ fn innerStrokeCoverage(centerId: f32, sourceCell: vec2<i32>, cell: vec2<f32>, width: f32, pixelWidth: f32) -> f32 {
15610
+ if (width <= 0.0) { return 0.0; }
15611
+ let alignment = readStrokeAlignment(centerId);
15612
+ let radius = i32(strokeScanRadius(width * alignment, pixelWidth));
15613
+ var nearestDistance = width * alignment + pixelWidth * 0.5;
15614
+ let minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
14513
15615
 
14514
- var coverage = 0.0;
14515
- if (differs(sampleMaskId(uv + vec2<f32>(texel.x, 0.0)), centerId)) {
14516
- coverage = max(coverage, subtexelStrokeCoverage(1.0 - cell.x, width, pixelWidth));
14517
- }
14518
- if (differs(sampleMaskId(uv + vec2<f32>(-texel.x, 0.0)), centerId)) {
14519
- coverage = max(coverage, subtexelStrokeCoverage(cell.x, width, pixelWidth));
14520
- }
14521
- if (differs(sampleMaskId(uv + vec2<f32>(0.0, texel.y)), centerId)) {
14522
- coverage = max(coverage, subtexelStrokeCoverage(1.0 - cell.y, width, pixelWidth));
14523
- }
14524
- if (differs(sampleMaskId(uv + vec2<f32>(0.0, -texel.y)), centerId)) {
14525
- coverage = max(coverage, subtexelStrokeCoverage(cell.y, width, pixelWidth));
15616
+ for (var offsetY = radius; offsetY >= -radius; offsetY -= 1) {
15617
+ for (var offsetX = radius; offsetX >= -radius; offsetX -= 1) {
15618
+ if (offsetX == 0 && offsetY == 0) {
15619
+ continue;
15620
+ }
15621
+ let distance = max(
15622
+ neighborCellDistance(f32(offsetX), cell.x),
15623
+ neighborCellDistance(f32(offsetY), cell.y)
15624
+ );
15625
+ if (distance >= nearestDistance) {
15626
+ continue;
15627
+ }
15628
+ let maskId = sampleMaskIdCell(sourceCell + vec2<i32>(offsetX, offsetY));
15629
+ if (differs(maskId, centerId)) {
15630
+ nearestDistance = distance;
15631
+ if (distance <= minimumDistance) {
15632
+ return subtexelStrokeCoverage(-distance, width, alignment, pixelWidth);
15633
+ }
15634
+ }
15635
+ }
14526
15636
  }
14527
- return coverage;
15637
+ return subtexelStrokeCoverage(-nearestDistance, width, alignment, pixelWidth);
14528
15638
  }
14529
15639
 
14530
15640
  // Descending offsets give the outer border its palette priority.
14531
- fn findNeighborStroke(uv: vec2<f32>, centerId: f32, texel: vec2<f32>, cell: vec2<f32>, pixelWidth: f32) -> vec2<f32> {
14532
- let radius = i32(strokeScanRadius(maskUniforms.uMaxStrokeWidth, maskUniforms.uMaxFractionalStrokeWidth, pixelWidth));
15641
+ fn findNeighborStroke(centerId: f32, sourceCell: vec2<i32>, cell: vec2<f32>, pixelWidth: f32) -> vec2<f32> {
15642
+ let maxWidth = min(maskUniforms.uMaxStrokeWidth * pixelWidth * maskUniforms.uStrokePixelRatio, ${MAX_ID_MASK_STROKE_WIDTH}.0);
15643
+ let radius = i32(strokeScanRadius(maxWidth, pixelWidth));
15644
+ var stroke = vec2<f32>(0.0);
15645
+ var nearestDistance = maxWidth + pixelWidth * 0.5;
15646
+ let minimumDistance = min(min(cell.x, 1.0 - cell.x), min(cell.y, 1.0 - cell.y));
15647
+ if (maskUniforms.uBorderEnabled <= 0.5) { return stroke; }
14533
15648
 
14534
15649
  for (var offsetY = radius; offsetY >= -radius; offsetY -= 1) {
14535
15650
  for (var offsetX = radius; offsetX >= -radius; offsetX -= 1) {
@@ -14537,45 +15652,44 @@ fn findNeighborStroke(uv: vec2<f32>, centerId: f32, texel: vec2<f32>, cell: vec2
14537
15652
  continue;
14538
15653
  }
14539
15654
 
14540
- let maskId = sampleMaskId(uv + vec2<f32>(f32(offsetX), f32(offsetY)) * texel);
14541
- if (maskId < 0.5 || !differs(maskId, centerId)) {
15655
+ let distance = max(
15656
+ neighborCellDistance(f32(offsetX), cell.x),
15657
+ neighborCellDistance(f32(offsetY), cell.y)
15658
+ );
15659
+ if (distance >= nearestDistance) {
15660
+ continue;
15661
+ }
15662
+ let maskId = sampleMaskIdCell(sourceCell + vec2<i32>(offsetX, offsetY));
15663
+ if (maskId < 0.5 || !differs(maskId, centerId) ||
15664
+ (stroke.x > 0.5 && differs(maskId, stroke.x))) {
14542
15665
  continue;
14543
15666
  }
14544
15667
 
14545
- let width = readStrokeWidth(maskId);
14546
- if (width >= 1.0) {
14547
- let distance = max(abs(f32(offsetX)), abs(f32(offsetY)));
14548
- if (width >= distance && readStroke(maskId).a > 0.0) {
14549
- return vec2<f32>(maskId, 1.0);
14550
- }
14551
- } else if (width > 0.0 && readStroke(maskId).a > 0.0) {
14552
- let distance = max(
14553
- neighborCellDistance(f32(offsetX), cell.x),
14554
- neighborCellDistance(f32(offsetY), cell.y)
14555
- );
14556
- let coverage = subtexelStrokeCoverage(distance, width, pixelWidth);
15668
+ let width = strokeWidthInTexels(maskId, pixelWidth);
15669
+ if (width > 0.0 && readStroke(maskId).a > 0.0) {
15670
+ let coverage = subtexelStrokeCoverage(distance, width, readStrokeAlignment(maskId), pixelWidth);
14557
15671
  if (coverage > 0.0) {
14558
- return vec2<f32>(maskId, coverage);
15672
+ stroke = vec2<f32>(maskId, coverage);
15673
+ nearestDistance = distance;
15674
+ if (distance <= minimumDistance) {
15675
+ return stroke;
15676
+ }
14559
15677
  }
14560
15678
  }
14561
15679
  }
14562
15680
  }
14563
15681
 
14564
- return vec2<f32>(0.0);
15682
+ return stroke;
14565
15683
  }
14566
15684
  `;
14567
15685
 
14568
- /**
14569
- * GLSL charges a uniform array of scalars one uniform vector per element, the
14570
- * same as an array of vec4, so the stroke widths ride four to a vector and an
14571
- * entry costs 2.25 vectors instead of 3. The lanes only line up while the entry
14572
- * count stays a multiple of four.
14573
- */
15686
+ /** Packs width and alignment scalars within the fragment uniform budget. */
14574
15687
  const ID_MASK_STROKE_WIDTH_LANES = MAX_ID_MASK_PALETTE_ENTRIES / 4;
14575
15688
  const idMaskPaletteGlsl = `
14576
15689
  uniform vec4 uFillPalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
14577
15690
  uniform vec4 uStrokePalette[${MAX_ID_MASK_PALETTE_ENTRIES}];
14578
15691
  uniform vec4 uStrokeWidths[${ID_MASK_STROKE_WIDTH_LANES}];
15692
+ uniform vec4 uStrokeAlignments[${ID_MASK_STROKE_WIDTH_LANES}];
14579
15693
 
14580
15694
  int paletteIndex(float maskId) {
14581
15695
  return int(clamp(maskId, 0.0, float(${MAX_ID_MASK_PALETTE_ENTRIES - 1})));
@@ -14594,10 +15708,16 @@ float readStrokeWidth(float maskId) {
14594
15708
 
14595
15709
  return uStrokeWidths[index / 4][index % 4];
14596
15710
  }
15711
+
15712
+ float readStrokeAlignment(float maskId) {
15713
+ int index = paletteIndex(maskId);
15714
+ return uStrokeAlignments[index / 4][index % 4];
15715
+ }
14597
15716
  `;
14598
15717
  const idMaskFillPaletteWgslField = `uFillPalette: array<vec4<f32>, ${MAX_ID_MASK_PALETTE_ENTRIES}>,`;
14599
15718
  const idMaskStrokePaletteWgslField = `uStrokePalette: array<vec4<f32>, ${MAX_ID_MASK_PALETTE_ENTRIES}>,`;
14600
15719
  const idMaskStrokeWidthsWgslField = `uStrokeWidths: array<vec4<f32>, ${ID_MASK_STROKE_WIDTH_LANES}>,`;
15720
+ const idMaskStrokeAlignmentsWgslField = `uStrokeAlignments: array<vec4<f32>, ${ID_MASK_STROKE_WIDTH_LANES}>,`;
14601
15721
  /** Reads the uniform group a fragment stage must bind under the name maskUniforms. */
14602
15722
  const idMaskPaletteWgsl = `
14603
15723
  fn paletteIndex(maskId: f32) -> i32 {
@@ -14617,20 +15737,31 @@ fn readStrokeWidth(maskId: f32) -> f32 {
14617
15737
 
14618
15738
  return maskUniforms.uStrokeWidths[index / 4][index % 4];
14619
15739
  }
15740
+
15741
+ fn readStrokeAlignment(maskId: f32) -> f32 {
15742
+ let index = paletteIndex(maskId);
15743
+ return maskUniforms.uStrokeAlignments[index / 4][index % 4];
15744
+ }
14620
15745
  `;
14621
15746
 
14622
- function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, labelStyle, resolveContextState, }) {
15747
+ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, labelStyle, getRenderedBoxes, resolveContextState, }) {
14623
15748
  const entries = [];
14624
15749
  let container;
14625
15750
  let currentLabelStyle = labelStyle;
14626
15751
  // A versioned source can replace a frame without changing its media time or
14627
15752
  // frame index. Buffered frames are immutable snapshots, so retain by object.
14628
15753
  let lastFrame;
15754
+ let lastBoxFrame;
14629
15755
  let styleVersion = 0;
14630
15756
  let drawnStyleVersion = -1;
14631
15757
  let lastViewportScale = 0;
14632
15758
  let hitRects = [];
14633
15759
  let previewEntry;
15760
+ let previewIndex = 0;
15761
+ let backgroundFilter = null;
15762
+ let backgroundFilters = null;
15763
+ let backgroundsGrouped = false;
15764
+ const backgroundGroups = [];
14634
15765
  const entriesByDetectionKey = new Map();
14635
15766
  const boundsByDetectionKey = new Map();
14636
15767
  const clearLayout = () => {
@@ -14642,6 +15773,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14642
15773
  for (let index = startIndex; index < entries.length; index += 1) {
14643
15774
  entries[index].background.visible = false;
14644
15775
  entries[index].label.visible = false;
15776
+ entries[index].instruction = undefined;
14645
15777
  }
14646
15778
  };
14647
15779
  const createEntry = () => {
@@ -14658,6 +15790,9 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14658
15790
  labelBaseY: 0,
14659
15791
  text: null,
14660
15792
  textStyleKey: null,
15793
+ textHeight: 0,
15794
+ textWidth: 0,
15795
+ instruction: undefined,
14661
15796
  };
14662
15797
  container?.addChild(entry.background, entry.label);
14663
15798
  return entry;
@@ -14673,10 +15808,71 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14673
15808
  const hideCreationPreview = () => {
14674
15809
  if (!previewEntry)
14675
15810
  return;
14676
- previewEntry.background.visible = false;
14677
- previewEntry.label.visible = false;
15811
+ previewEntry.background.visible = false;
15812
+ previewEntry.label.visible = false;
15813
+ previewEntry.instruction = undefined;
15814
+ };
15815
+ const syncBackgroundCaptures = () => {
15816
+ if (!container || (!backgroundFilter && !backgroundsGrouped))
15817
+ return;
15818
+ for (const group of backgroundGroups) {
15819
+ group.removeChildren();
15820
+ group.filters = null;
15821
+ }
15822
+ container.removeChildren();
15823
+ backgroundsGrouped = backgroundFilter !== null;
15824
+ const orderedEntries = previewEntry
15825
+ ? [
15826
+ ...entries.slice(0, previewIndex),
15827
+ previewEntry,
15828
+ ...entries.slice(previewIndex),
15829
+ ]
15830
+ : entries;
15831
+ if (!backgroundFilter) {
15832
+ for (const entry of orderedEntries) {
15833
+ container.addChild(entry.background, entry.label);
15834
+ }
15835
+ return;
15836
+ }
15837
+ let groupIndex = 0;
15838
+ const run = [];
15839
+ let runBounds;
15840
+ const padding = backgroundFilter.padding / Math.max(lastViewportScale, 1e-6);
15841
+ const flushRun = () => {
15842
+ if (run.length === 0)
15843
+ return;
15844
+ if (run.some((entry) => entry.background.visible)) {
15845
+ const group = (backgroundGroups[groupIndex] ??= new Container());
15846
+ groupIndex += 1;
15847
+ group.label = "label-background-aa";
15848
+ group.filters = backgroundFilters;
15849
+ container.addChild(group);
15850
+ for (const entry of run)
15851
+ group.addChild(entry.background);
15852
+ }
15853
+ else {
15854
+ for (const entry of run)
15855
+ container.addChild(entry.background);
15856
+ }
15857
+ for (const entry of run)
15858
+ container.addChild(entry.label);
15859
+ run.length = 0;
15860
+ runBounds = undefined;
15861
+ };
15862
+ for (const entry of orderedEntries) {
15863
+ if (entry.background.visible || entry.label.visible) {
15864
+ const padded = resolveCaptureBounds(entry, padding);
15865
+ // Reordering a disjoint run leaves translucent overlapping chips and
15866
+ // their text in the same paint order as the original label pairs.
15867
+ if (runBounds && rectanglesOverlap(runBounds, padded))
15868
+ flushRun();
15869
+ runBounds = runBounds ? unionCaptureBounds(runBounds, padded) : padded;
15870
+ }
15871
+ run.push(entry);
15872
+ }
15873
+ flushRun();
14678
15874
  };
14679
- const redrawFrame = (frame, mediaTime, viewportScale) => {
15875
+ const redrawFrame = (frame, mediaTime, viewportScale, renderedBoxes) => {
14680
15876
  let drawnCount = 0;
14681
15877
  clearLayout();
14682
15878
  if (!currentLabelStyle) {
@@ -14699,7 +15895,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14699
15895
  continue;
14700
15896
  }
14701
15897
  const entry = ensureEntry(drawnCount);
14702
- const hitRect = drawInstruction(entry, instruction, viewportScale);
15898
+ const hitRect = drawInstruction(entry, instruction, viewportScale, renderedBoxes?.get(detectionIndex));
14703
15899
  const key = detectionKey$1(detection, detectionIndex);
14704
15900
  entriesByDetectionKey.set(key, entry);
14705
15901
  boundsByDetectionKey.set(key, {
@@ -14722,24 +15918,31 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14722
15918
  drawFrame(mediaTime, viewportScale) {
14723
15919
  const resolvedViewportScale = viewportScale ?? 1;
14724
15920
  const frame = detectionTimeline.selectFrame(mediaTime);
15921
+ const boxFrame = getRenderedBoxes?.();
15922
+ const matchingBoxFrame = boxFrame?.frame === frame ? boxFrame : undefined;
14725
15923
  if (frame === lastFrame &&
15924
+ matchingBoxFrame === lastBoxFrame &&
14726
15925
  drawnStyleVersion === styleVersion &&
14727
15926
  resolvedViewportScale === lastViewportScale) {
14728
15927
  return;
14729
15928
  }
14730
15929
  lastFrame = frame;
15930
+ lastBoxFrame = matchingBoxFrame;
14731
15931
  drawnStyleVersion = styleVersion;
14732
15932
  lastViewportScale = resolvedViewportScale;
14733
15933
  if (!frame) {
14734
15934
  clearLayout();
14735
15935
  hideEntriesFrom(0);
15936
+ syncBackgroundCaptures();
14736
15937
  return;
14737
15938
  }
14738
- redrawFrame(frame, mediaTime, resolvedViewportScale);
15939
+ redrawFrame(frame, mediaTime, resolvedViewportScale, matchingBoxFrame?.rects);
15940
+ syncBackgroundCaptures();
14739
15941
  },
14740
- drawCreationPreview(detection, mediaTime, viewportScale) {
15942
+ drawCreationPreview(detection, mediaTime, viewportScale, renderedBox) {
14741
15943
  if (!detection || !currentLabelStyle) {
14742
15944
  hideCreationPreview();
15945
+ syncBackgroundCaptures();
14743
15946
  return;
14744
15947
  }
14745
15948
  const resolvedViewportScale = viewportScale ?? 1;
@@ -14755,15 +15958,39 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14755
15958
  });
14756
15959
  if (!instruction) {
14757
15960
  hideCreationPreview();
15961
+ syncBackgroundCaptures();
14758
15962
  return;
14759
15963
  }
14760
- previewEntry ??= createEntry();
14761
- drawInstruction(previewEntry, instruction, resolvedViewportScale);
15964
+ if (!previewEntry) {
15965
+ previewIndex = entries.length;
15966
+ previewEntry = createEntry();
15967
+ }
15968
+ drawInstruction(previewEntry, instruction, resolvedViewportScale, renderedBox);
15969
+ syncBackgroundCaptures();
15970
+ },
15971
+ updateEditingBox(id, renderedBox) {
15972
+ const key = `id:${String(id)}`;
15973
+ const entry = entriesByDetectionKey.get(key);
15974
+ const bounds = boundsByDetectionKey.get(key);
15975
+ const instruction = entry?.instruction;
15976
+ if (!entry || !bounds || !instruction?.background)
15977
+ return;
15978
+ const corners = resolveRoundedCorners(instruction.background, instruction.placement ?? LabelPlacement.Top, bounds, renderedBox, lastViewportScale);
15979
+ drawBackground(entry, instruction.background, bounds.baseX, bounds.baseY, bounds.width, bounds.height, lastViewportScale, corners);
15980
+ entry.background.x = bounds.x;
15981
+ entry.background.y = bounds.y;
14762
15982
  },
14763
15983
  setLabelStyle(nextLabelStyle) {
14764
15984
  currentLabelStyle = nextLabelStyle ?? undefined;
14765
15985
  styleVersion += 1;
14766
15986
  },
15987
+ setBackgroundAntialiasFilter(filter) {
15988
+ if (filter === backgroundFilter)
15989
+ return;
15990
+ backgroundFilter = filter;
15991
+ backgroundFilters = filter ? [filter] : null;
15992
+ syncBackgroundCaptures();
15993
+ },
14767
15994
  translateDetection(id, x, y) {
14768
15995
  const key = `id:${String(id)}`;
14769
15996
  const entry = entriesByDetectionKey.get(key);
@@ -14781,6 +16008,7 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14781
16008
  y: bounds.baseY + y,
14782
16009
  });
14783
16010
  }
16011
+ syncBackgroundCaptures();
14784
16012
  return true;
14785
16013
  },
14786
16014
  getDetectionLabelBounds(id) {
@@ -14817,6 +16045,13 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14817
16045
  : null;
14818
16046
  },
14819
16047
  destroy() {
16048
+ for (const group of backgroundGroups) {
16049
+ group.filters = null;
16050
+ if (!group.parent)
16051
+ group.destroy({ children: false });
16052
+ }
16053
+ backgroundFilter = null;
16054
+ backgroundFilters = null;
14820
16055
  hideEntriesFrom(0);
14821
16056
  hideCreationPreview();
14822
16057
  entries.length = 0;
@@ -14826,7 +16061,8 @@ function createPixiLabelLayer({ Container, Graphics, Text, detectionTimeline, la
14826
16061
  },
14827
16062
  };
14828
16063
  }
14829
- function drawInstruction(entry, instruction, viewportScale) {
16064
+ function drawInstruction(entry, instruction, viewportScale, renderedBox) {
16065
+ entry.instruction = instruction;
14830
16066
  const textStyle = resolveTextStyle(instruction.textStyle, viewportScale);
14831
16067
  const textStyleKey = createTextStyleKey(textStyle);
14832
16068
  const textAlpha = instruction.textStyle?.alpha ?? 1;
@@ -14846,8 +16082,10 @@ function drawInstruction(entry, instruction, viewportScale) {
14846
16082
  const background = instruction.background;
14847
16083
  const paddingX = (background?.paddingX ?? 0) / viewportScale;
14848
16084
  const paddingY = (background?.paddingY ?? 0) / viewportScale;
14849
- const width = entry.label.width + paddingX * 2;
14850
- const height = entry.label.height + paddingY * 2;
16085
+ entry.textWidth = entry.label.width;
16086
+ entry.textHeight = entry.label.height;
16087
+ const width = entry.textWidth + paddingX * 2;
16088
+ const height = entry.textHeight + paddingY * 2;
14851
16089
  const { x, y } = resolveLabelPosition(instruction, width, height);
14852
16090
  entry.label.x = x + paddingX;
14853
16091
  entry.label.y = y + paddingY;
@@ -14858,9 +16096,41 @@ function drawInstruction(entry, instruction, viewportScale) {
14858
16096
  entry.backgroundKey = null;
14859
16097
  return { height, width, x, y };
14860
16098
  }
14861
- drawBackground(entry, background, x, y, width, height, viewportScale);
16099
+ const corners = resolveRoundedCorners(background, instruction.placement ?? LabelPlacement.Top, { x, y, width, height }, renderedBox, viewportScale);
16100
+ drawBackground(entry, background, x, y, width, height, viewportScale, corners);
14862
16101
  return { height, width, x, y };
14863
16102
  }
16103
+ function resolveCaptureBounds(entry, padding) {
16104
+ const { background, label } = entry;
16105
+ const x = Math.min(background.visible ? background.x : Infinity, label.visible ? label.x : Infinity);
16106
+ const y = Math.min(background.visible ? background.y : Infinity, label.visible ? label.y : Infinity);
16107
+ return {
16108
+ height: Math.max(background.visible ? background.y + entry.backgroundHeight : -Infinity, label.visible ? label.y + entry.textHeight : -Infinity) -
16109
+ y +
16110
+ padding * 2,
16111
+ width: Math.max(background.visible ? background.x + entry.backgroundWidth : -Infinity, label.visible ? label.x + entry.textWidth : -Infinity) -
16112
+ x +
16113
+ padding * 2,
16114
+ x: x - padding,
16115
+ y: y - padding,
16116
+ };
16117
+ }
16118
+ function rectanglesOverlap(left, right) {
16119
+ return (left.x < right.x + right.width &&
16120
+ right.x < left.x + left.width &&
16121
+ left.y < right.y + right.height &&
16122
+ right.y < left.y + left.height);
16123
+ }
16124
+ function unionCaptureBounds(left, right) {
16125
+ const x = Math.min(left.x, right.x);
16126
+ const y = Math.min(left.y, right.y);
16127
+ return {
16128
+ height: Math.max(left.y + left.height, right.y + right.height) - y,
16129
+ width: Math.max(left.x + left.width, right.x + right.width) - x,
16130
+ x,
16131
+ y,
16132
+ };
16133
+ }
14864
16134
  function resolveLabelPosition(instruction, width, height) {
14865
16135
  const { rect } = instruction;
14866
16136
  const offsetX = instruction.offsetX ?? 0;
@@ -14894,9 +16164,9 @@ function resolveLabelPosition(instruction, width, height) {
14894
16164
  };
14895
16165
  }
14896
16166
  }
14897
- function drawBackground(entry, background, x, y, width, height, viewportScale) {
16167
+ function drawBackground(entry, background, x, y, width, height, viewportScale, corners) {
14898
16168
  const graphics = entry.background;
14899
- const backgroundKey = createBackgroundKey(background);
16169
+ const backgroundKey = createBackgroundKey(background, corners);
14900
16170
  graphics.visible = true;
14901
16171
  graphics.x = x;
14902
16172
  graphics.y = y;
@@ -14911,8 +16181,8 @@ function drawBackground(entry, background, x, y, width, height, viewportScale) {
14911
16181
  entry.backgroundWidth = width;
14912
16182
  entry.backgroundHeight = height;
14913
16183
  graphics.clear();
14914
- const radius = (background.cornerRadius ?? 0) / viewportScale;
14915
- if (background.topCornersOnly && radius > 0) {
16184
+ const radius = Math.min((background.cornerRadius ?? 0) / viewportScale, width / 2, height / 2);
16185
+ if (corners === "top" && radius > 0) {
14916
16186
  graphics
14917
16187
  .moveTo(0, height)
14918
16188
  .lineTo(0, radius)
@@ -14922,6 +16192,16 @@ function drawBackground(entry, background, x, y, width, height, viewportScale) {
14922
16192
  .lineTo(width, height)
14923
16193
  .closePath();
14924
16194
  }
16195
+ else if (corners === "bottom" && radius > 0) {
16196
+ graphics
16197
+ .moveTo(0, 0)
16198
+ .lineTo(width, 0)
16199
+ .lineTo(width, height - radius)
16200
+ .quadraticCurveTo(width, height, width - radius, height)
16201
+ .lineTo(radius, height)
16202
+ .quadraticCurveTo(0, height, 0, height - radius)
16203
+ .closePath();
16204
+ }
14925
16205
  else {
14926
16206
  graphics.roundRect(0, 0, width, height, radius);
14927
16207
  }
@@ -14946,14 +16226,46 @@ function createTextStyleKey(textStyle) {
14946
16226
  textStyle.fontWeight,
14947
16227
  ].join(":");
14948
16228
  }
14949
- function createBackgroundKey(background) {
16229
+ function resolveRoundedCorners(background, placement, label, renderedBox, viewportScale) {
16230
+ if (background.topCornersOnly !== undefined) {
16231
+ return background.topCornersOnly ? "top" : "all";
16232
+ }
16233
+ if (!renderedBox)
16234
+ return "all";
16235
+ const box = centerRectToTopLeftRect(renderedBox);
16236
+ const epsilon = 1e-6 / viewportScale;
16237
+ if (Math.min(label.x + label.width, box.x + box.width) -
16238
+ Math.max(label.x, box.x) <=
16239
+ epsilon) {
16240
+ return "all";
16241
+ }
16242
+ switch (placement) {
16243
+ case LabelPlacement.Top:
16244
+ return Math.abs(label.y + label.height - box.y) <= epsilon
16245
+ ? "top"
16246
+ : "all";
16247
+ case LabelPlacement.Bottom:
16248
+ return Math.abs(label.y - box.y - box.height) <= epsilon
16249
+ ? "bottom"
16250
+ : "all";
16251
+ case LabelPlacement.InsideTop:
16252
+ return Math.abs(label.y - box.y) <= epsilon ? "bottom" : "all";
16253
+ case LabelPlacement.InsideBottom:
16254
+ return Math.abs(label.y + label.height - box.y - box.height) <= epsilon
16255
+ ? "top"
16256
+ : "all";
16257
+ case LabelPlacement.Center:
16258
+ return "all";
16259
+ }
16260
+ }
16261
+ function createBackgroundKey(background, corners) {
14950
16262
  return [
14951
16263
  background.alpha,
14952
16264
  background.color,
14953
16265
  background.cornerRadius ?? 0,
14954
16266
  background.paddingX ?? 0,
14955
16267
  background.paddingY ?? 0,
14956
- background.topCornersOnly ? 1 : 0,
16268
+ corners,
14957
16269
  ].join(":");
14958
16270
  }
14959
16271
  function detectionKey$1(detection, detectionIndex) {
@@ -15256,7 +16568,9 @@ function createPixiInteractionPresentationLayer(options) {
15256
16568
  let activePicks = [];
15257
16569
  let currentMediaTime = 0;
15258
16570
  let viewportScale = 1;
16571
+ let strokePixelRatio = 1;
15259
16572
  let maskRenderer;
16573
+ let labelDisplay = null;
15260
16574
  let screenStrokeRenderer;
15261
16575
  let isDestroyed = false;
15262
16576
  const syntheticTimeline = createSyntheticTimeline(() => syntheticFrame);
@@ -15290,17 +16604,20 @@ function createPixiInteractionPresentationLayer(options) {
15290
16604
  mediaHeight = height;
15291
16605
  const container = new options.Container();
15292
16606
  const boxGraphics = new options.Graphics();
15293
- const labels = labelLayer.createContainer();
16607
+ labelDisplay = labelLayer.createContainer();
15294
16608
  const vectors = vectorLayer.createContainer();
15295
16609
  boxLayer.attachGraphics(boxGraphics);
15296
16610
  maskRenderer = createMaskRenderer();
15297
16611
  if (maskRenderer) {
15298
16612
  container.addChild(maskRenderer.mesh);
15299
16613
  }
15300
- container.addChild(boxGraphics, vectors, labels);
16614
+ container.addChild(boxGraphics, vectors);
15301
16615
  displayContainer = container;
15302
16616
  return container;
15303
16617
  },
16618
+ getLabelDisplay() {
16619
+ return labelDisplay;
16620
+ },
15304
16621
  destroy() {
15305
16622
  if (isDestroyed) {
15306
16623
  return;
@@ -15311,6 +16628,7 @@ function createPixiInteractionPresentationLayer(options) {
15311
16628
  maskRenderer?.destroy();
15312
16629
  screenStrokeRenderer?.destroy();
15313
16630
  labelLayer.destroy();
16631
+ labelDisplay = null;
15314
16632
  vectorLayer.destroy();
15315
16633
  },
15316
16634
  releaseMaskTexture(texture) {
@@ -15320,6 +16638,7 @@ function createPixiInteractionPresentationLayer(options) {
15320
16638
  drawFrame(context) {
15321
16639
  currentMediaTime = context.mediaTime;
15322
16640
  viewportScale = context.viewportScale ?? 1;
16641
+ strokePixelRatio = context.strokePixelRatio ?? 1;
15323
16642
  if (isDestroyed || !interactionStyle || !context.frame) {
15324
16643
  clear();
15325
16644
  return;
@@ -15353,6 +16672,9 @@ function createPixiInteractionPresentationLayer(options) {
15353
16672
  labelLayer.setLabelStyle(labelStyle);
15354
16673
  vectorLayer.setStyles({ polygonStyle, polylineStyle, keypointStyle });
15355
16674
  },
16675
+ setBackgroundAntialiasFilter(filter) {
16676
+ labelLayer.setBackgroundAntialiasFilter(filter);
16677
+ },
15356
16678
  };
15357
16679
  function getActivePicks({ frame, hoveredPick, mediaTime, selectedPick, selectedPicks, }) {
15358
16680
  if (!frame || !interactionStyle) {
@@ -15464,9 +16786,9 @@ function createPixiInteractionPresentationLayer(options) {
15464
16786
  screenStrokeRenderer?.hide();
15465
16787
  return;
15466
16788
  }
15467
- maskRenderer.render(artifact.frame, artifact.texture, instructions);
16789
+ maskRenderer.render(artifact.frame, artifact.texture, instructions, strokePixelRatio);
15468
16790
  if (!screenStrokeRenderer &&
15469
- instructions.some(({ instruction }) => instruction.stroke?.widthUnit === "screen")) {
16791
+ instructions.some(({ instruction }) => instruction.stroke?.widthUnit !== undefined)) {
15470
16792
  screenStrokeRenderer = createPixiMaskScreenStrokeRenderer({
15471
16793
  Graphics: options.Graphics,
15472
16794
  mediaWidth,
@@ -15633,6 +16955,7 @@ function createInteractionMaskRenderer(options) {
15633
16955
  const fillPalette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
15634
16956
  const strokePalette = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4);
15635
16957
  const strokeWidths = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
16958
+ const strokeAlignments = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
15636
16959
  const uniforms = new options.UniformGroup({
15637
16960
  uFillPalette: {
15638
16961
  size: MAX_ID_MASK_PALETTE_ENTRIES,
@@ -15640,7 +16963,8 @@ function createInteractionMaskRenderer(options) {
15640
16963
  value: fillPalette,
15641
16964
  },
15642
16965
  uMaxStrokeWidth: { type: "f32", value: 0 },
15643
- uMaxFractionalStrokeWidth: { type: "f32", value: 0 },
16966
+ uBorderEnabled: { type: "f32", value: 0 },
16967
+ uStrokePixelRatio: { type: "f32", value: 1 },
15644
16968
  uStrokePalette: {
15645
16969
  size: MAX_ID_MASK_PALETTE_ENTRIES,
15646
16970
  type: "vec4<f32>",
@@ -15651,6 +16975,11 @@ function createInteractionMaskRenderer(options) {
15651
16975
  type: "vec4<f32>",
15652
16976
  value: strokeWidths,
15653
16977
  },
16978
+ uStrokeAlignments: {
16979
+ size: ID_MASK_STROKE_WIDTH_LANES,
16980
+ type: "vec4<f32>",
16981
+ value: strokeAlignments,
16982
+ },
15654
16983
  uTextureSize: {
15655
16984
  type: "vec2<f32>",
15656
16985
  value: new Float32Array([options.mediaWidth, options.mediaHeight]),
@@ -15700,7 +17029,7 @@ function createInteractionMaskRenderer(options) {
15700
17029
  mesh.visible = false;
15701
17030
  },
15702
17031
  mesh,
15703
- render(frame, texture, instructions) {
17032
+ render(frame, texture, instructions, strokePixelRatio) {
15704
17033
  if (frame.kind !== PreparedMaskFrameKind.IdMask) {
15705
17034
  mesh.visible = false;
15706
17035
  return;
@@ -15709,8 +17038,9 @@ function createInteractionMaskRenderer(options) {
15709
17038
  fillPalette.fill(0);
15710
17039
  strokePalette.fill(0);
15711
17040
  strokeWidths.fill(0);
17041
+ strokeAlignments.fill(0);
15712
17042
  let maxStrokeWidth = 0;
15713
- let maxFractionalStrokeWidth = 0;
17043
+ let hasStroke = false;
15714
17044
  for (const { detectionIndex, instruction } of instructions) {
15715
17045
  const maskId = resolveIdMaskPaletteId(detectionIndex);
15716
17046
  // A raster naming this detection is one the palette accepted, so an id
@@ -15720,26 +17050,31 @@ function createInteractionMaskRenderer(options) {
15720
17050
  }
15721
17051
  writeIdMaskPaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
15722
17052
  if (instruction.stroke &&
15723
- instruction.stroke.widthUnit !== "screen" &&
17053
+ instruction.stroke.widthUnit === undefined &&
15724
17054
  instruction.stroke.width > 0) {
15725
17055
  writeIdMaskPaletteEntry(strokePalette, maskId, instruction.stroke.color, instruction.stroke.alpha);
15726
- strokeWidths[maskId] = resolveIdMaskStrokeTexels(instruction.stroke.width, frame.sourceWidth, frame.width);
15727
- maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidths[maskId] ?? 0);
15728
- const width = strokeWidths[maskId] ?? 0;
15729
- if (width > 0 && width < 1) {
15730
- maxFractionalStrokeWidth = Math.max(maxFractionalStrokeWidth, width);
15731
- }
17056
+ const alignment = instruction.stroke.alignment === StrokeAlignment.Inside
17057
+ ? 1
17058
+ : instruction.stroke.alignment === StrokeAlignment.Center
17059
+ ? 0.5
17060
+ : 0;
17061
+ strokeWidths[maskId] = instruction.stroke.width;
17062
+ strokeAlignments[maskId] = alignment;
17063
+ hasStroke = true;
17064
+ maxStrokeWidth = Math.max(maxStrokeWidth, instruction.stroke.width * (1 - alignment));
15732
17065
  }
15733
17066
  }
15734
17067
  uniforms.uniforms.uFillPalette = fillPalette;
15735
17068
  uniforms.uniforms.uStrokePalette = strokePalette;
15736
17069
  uniforms.uniforms.uStrokeWidths = strokeWidths;
17070
+ uniforms.uniforms.uStrokeAlignments = strokeAlignments;
17071
+ uniforms.uniforms.uBorderEnabled = hasStroke ? 1 : 0;
17072
+ uniforms.uniforms.uStrokePixelRatio = strokePixelRatio;
15737
17073
  uniforms.uniforms.uTextureSize = new Float32Array([
15738
17074
  frame.width,
15739
17075
  frame.height,
15740
17076
  ]);
15741
17077
  uniforms.uniforms.uMaxStrokeWidth = maxStrokeWidth;
15742
- uniforms.uniforms.uMaxFractionalStrokeWidth = maxFractionalStrokeWidth;
15743
17078
  uniforms.update();
15744
17079
  mesh.visible = true;
15745
17080
  },
@@ -15794,14 +17129,11 @@ in vec4 vColor;
15794
17129
  uniform sampler2D uTexture;
15795
17130
  uniform vec2 uTextureSize;
15796
17131
  uniform float uMaxStrokeWidth;
15797
- uniform float uMaxFractionalStrokeWidth;
17132
+ uniform float uBorderEnabled;
17133
+ uniform float uStrokePixelRatio;
15798
17134
  ${idMaskPaletteGlsl}
15799
17135
  out vec4 finalColor;
15800
17136
 
15801
- float sampleMaskId(vec2 uv) {
15802
- return floor(texture(uTexture, uv).r * 255.0 + 0.5);
15803
- }
15804
-
15805
17137
  vec4 premultiplyAlpha(vec4 color) {
15806
17138
  return vec4(color.rgb * color.a, color.a);
15807
17139
  }
@@ -15813,19 +17145,20 @@ bool differs(float left, float right) {
15813
17145
  ${idMaskStrokeCoverageGlsl}
15814
17146
 
15815
17147
  void main(void) {
15816
- float centerId = sampleMaskId(vUV);
15817
- vec2 texel = 1.0 / uTextureSize;
15818
- vec2 cell = fract(vUV * uTextureSize);
15819
- vec2 footprint = fwidth(vUV * uTextureSize);
17148
+ vec2 position = vUV * uTextureSize;
17149
+ ivec2 sourceCell = ivec2(floor(position));
17150
+ float centerId = sampleMaskIdCell(sourceCell);
17151
+ vec2 cell = fract(position);
17152
+ vec2 footprint = fwidth(position);
15820
17153
  float pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
15821
17154
 
15822
17155
  if (centerId > 0.5) {
15823
17156
  vec4 fill = readFill(centerId);
15824
17157
  vec4 stroke = readStroke(centerId);
15825
17158
 
15826
- float width = readStrokeWidth(centerId);
15827
- if (uMaxStrokeWidth > 0.0 && width > 0.0 && stroke.a > 0.0) {
15828
- float coverage = innerStrokeCoverage(centerId, texel, cell, width, pixelWidth);
17159
+ float width = strokeWidthInTexels(centerId, pixelWidth);
17160
+ if (uBorderEnabled > 0.5 && width > 0.0 && stroke.a > 0.0) {
17161
+ float coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
15829
17162
  if (coverage >= 1.0) {
15830
17163
  finalColor = premultiplyAlpha(stroke * vColor);
15831
17164
  return;
@@ -15840,8 +17173,8 @@ void main(void) {
15840
17173
  return;
15841
17174
  }
15842
17175
 
15843
- if (uMaxStrokeWidth > 0.0) {
15844
- vec2 border = findNeighborStroke(centerId, texel, cell, pixelWidth);
17176
+ if (uBorderEnabled > 0.5) {
17177
+ vec2 border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
15845
17178
 
15846
17179
  if (border.x > 0.5) {
15847
17180
  finalColor = premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
@@ -15856,9 +17189,11 @@ const interactionMaskFragmentWgsl = `
15856
17189
  struct InteractionMaskUniforms {
15857
17190
  ${idMaskFillPaletteWgslField}
15858
17191
  uMaxStrokeWidth: f32,
15859
- uMaxFractionalStrokeWidth: f32,
17192
+ uBorderEnabled: f32,
17193
+ uStrokePixelRatio: f32,
15860
17194
  ${idMaskStrokePaletteWgslField}
15861
17195
  ${idMaskStrokeWidthsWgslField}
17196
+ ${idMaskStrokeAlignmentsWgslField}
15862
17197
  uTextureSize: vec2<f32>,
15863
17198
  }
15864
17199
 
@@ -15866,9 +17201,6 @@ struct InteractionMaskUniforms {
15866
17201
  @group(2) @binding(1) var uTexture: texture_2d<f32>;
15867
17202
  @group(2) @binding(2) var uSampler: sampler;
15868
17203
 
15869
- fn sampleMaskId(uv: vec2<f32>) -> f32 {
15870
- return floor(textureSampleLevel(uTexture, uSampler, uv, 0.0).r * 255.0 + 0.5);
15871
- }
15872
17204
  ${idMaskPaletteWgsl}
15873
17205
  fn premultiplyAlpha(color: vec4<f32>) -> vec4<f32> {
15874
17206
  return vec4<f32>(color.rgb * color.a, color.a);
@@ -15885,19 +17217,20 @@ fn mainFragment(
15885
17217
  @location(0) vUV: vec2<f32>,
15886
17218
  @location(1) vColor: vec4<f32>,
15887
17219
  ) -> @location(0) vec4<f32> {
15888
- let centerId = sampleMaskId(vUV);
15889
- let texel = 1.0 / maskUniforms.uTextureSize;
15890
- let cell = fract(vUV * maskUniforms.uTextureSize);
15891
- let footprint = fwidth(vUV * maskUniforms.uTextureSize);
17220
+ let position = vUV * maskUniforms.uTextureSize;
17221
+ let sourceCell = vec2<i32>(floor(position));
17222
+ let centerId = sampleMaskIdCell(sourceCell);
17223
+ let cell = fract(position);
17224
+ let footprint = fwidth(position);
15892
17225
  let pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
15893
17226
 
15894
17227
  if (centerId > 0.5) {
15895
17228
  let fill = readFill(centerId);
15896
17229
  let stroke = readStroke(centerId);
15897
17230
 
15898
- let width = readStrokeWidth(centerId);
15899
- if (maskUniforms.uMaxStrokeWidth > 0.0 && width > 0.0 && stroke.a > 0.0) {
15900
- let coverage = innerStrokeCoverage(vUV, centerId, texel, cell, width, pixelWidth);
17231
+ let width = strokeWidthInTexels(centerId, pixelWidth);
17232
+ if (maskUniforms.uBorderEnabled > 0.5 && width > 0.0 && stroke.a > 0.0) {
17233
+ let coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
15901
17234
  if (coverage >= 1.0) {
15902
17235
  return premultiplyAlpha(stroke * vColor);
15903
17236
  }
@@ -15909,8 +17242,8 @@ fn mainFragment(
15909
17242
  return premultiplyAlpha(fill * vColor);
15910
17243
  }
15911
17244
 
15912
- if (maskUniforms.uMaxStrokeWidth > 0.0) {
15913
- let border = findNeighborStroke(vUV, centerId, texel, cell, pixelWidth);
17245
+ if (maskUniforms.uBorderEnabled > 0.5) {
17246
+ let border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
15914
17247
 
15915
17248
  if (border.x > 0.5) {
15916
17249
  return premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
@@ -15922,6 +17255,7 @@ fn mainFragment(
15922
17255
  `;
15923
17256
 
15924
17257
  function createPixiIdMaskShaderRenderer(options) {
17258
+ const defaultStrokeAlignments = new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES);
15925
17259
  const uniforms = new options.UniformGroup({
15926
17260
  uBorderEnabled: { type: "f32", value: 0 },
15927
17261
  uFillPalette: {
@@ -15930,7 +17264,7 @@ function createPixiIdMaskShaderRenderer(options) {
15930
17264
  value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES * 4),
15931
17265
  },
15932
17266
  uMaxStrokeWidth: { type: "f32", value: 0 },
15933
- uMaxFractionalStrokeWidth: { type: "f32", value: 0 },
17267
+ uStrokePixelRatio: { type: "f32", value: 1 },
15934
17268
  uStrokePalette: {
15935
17269
  size: MAX_ID_MASK_PALETTE_ENTRIES,
15936
17270
  type: "vec4<f32>",
@@ -15941,6 +17275,11 @@ function createPixiIdMaskShaderRenderer(options) {
15941
17275
  type: "vec4<f32>",
15942
17276
  value: new Float32Array(MAX_ID_MASK_PALETTE_ENTRIES),
15943
17277
  },
17278
+ uStrokeAlignments: {
17279
+ size: ID_MASK_STROKE_WIDTH_LANES,
17280
+ type: "vec4<f32>",
17281
+ value: defaultStrokeAlignments,
17282
+ },
15944
17283
  uTextureSize: {
15945
17284
  type: "vec2<f32>",
15946
17285
  value: new Float32Array([options.mediaWidth, options.mediaHeight]),
@@ -15993,24 +17332,30 @@ function createPixiIdMaskShaderRenderer(options) {
15993
17332
  mesh.visible = false;
15994
17333
  },
15995
17334
  mesh,
15996
- render(frame, texture) {
17335
+ render(frame, texture, strokePixelRatio = 1) {
15997
17336
  bindTexture(texture.source);
15998
17337
  uniforms.uniforms.uFillPalette = frame.fillPalette;
15999
17338
  uniforms.uniforms.uStrokePalette = frame.strokePalette;
16000
17339
  uniforms.uniforms.uStrokeWidths = frame.strokeWidths;
17340
+ uniforms.uniforms.uStrokeAlignments =
17341
+ frame.strokeAlignments ?? defaultStrokeAlignments;
17342
+ uniforms.uniforms.uStrokePixelRatio = strokePixelRatio;
16001
17343
  uniforms.uniforms.uTextureSize = new Float32Array([
16002
17344
  frame.width,
16003
17345
  frame.height,
16004
17346
  ]);
16005
17347
  uniforms.uniforms.uBorderEnabled = frame.hasStroke ? 1 : 0;
16006
- uniforms.uniforms.uMaxStrokeWidth = Math.min(frame.maxStrokeWidth, MAX_ID_MASK_STROKE_WIDTH);
16007
- uniforms.uniforms.uMaxFractionalStrokeWidth = frame.strokeWidths.reduce((maximum, width) => width > 0 && width < 1 ? Math.max(maximum, width) : maximum, 0);
17348
+ uniforms.uniforms.uMaxStrokeWidth = frame.maxStrokeWidth;
16008
17349
  uniforms.update();
16009
17350
  mesh.visible = true;
16010
17351
  },
16011
17352
  setOpacity(opacity) {
16012
17353
  mesh.alpha = opacity;
16013
17354
  },
17355
+ setStrokePixelRatio(pixelRatio) {
17356
+ uniforms.uniforms.uStrokePixelRatio = pixelRatio;
17357
+ uniforms.update();
17358
+ },
16014
17359
  };
16015
17360
  function bindTexture(source) {
16016
17361
  try {
@@ -16063,14 +17408,10 @@ uniform sampler2D uTexture;
16063
17408
  uniform vec2 uTextureSize;
16064
17409
  uniform float uBorderEnabled;
16065
17410
  uniform float uMaxStrokeWidth;
16066
- uniform float uMaxFractionalStrokeWidth;
17411
+ uniform float uStrokePixelRatio;
16067
17412
  ${idMaskPaletteGlsl}
16068
17413
  out vec4 finalColor;
16069
17414
 
16070
- float sampleMaskId(vec2 uv) {
16071
- return floor(texture(uTexture, uv).r * 255.0 + 0.5);
16072
- }
16073
-
16074
17415
  vec4 premultiplyAlpha(vec4 color) {
16075
17416
  return vec4(color.rgb * color.a, color.a);
16076
17417
  }
@@ -16082,15 +17423,16 @@ bool differs(float left, float right) {
16082
17423
  ${idMaskStrokeCoverageGlsl}
16083
17424
 
16084
17425
  void main(void) {
16085
- float centerId = sampleMaskId(vUV);
16086
- vec2 texel = 1.0 / uTextureSize;
16087
- vec2 cell = fract(vUV * uTextureSize);
16088
- vec2 footprint = fwidth(vUV * uTextureSize);
17426
+ vec2 position = vUV * uTextureSize;
17427
+ ivec2 sourceCell = ivec2(floor(position));
17428
+ float centerId = sampleMaskIdCell(sourceCell);
17429
+ vec2 cell = fract(position);
17430
+ vec2 footprint = fwidth(position);
16089
17431
  float pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
16090
17432
 
16091
17433
  if (centerId < 0.5) {
16092
- if (uBorderEnabled > 0.5 && uMaxStrokeWidth > 0.0) {
16093
- vec2 border = findNeighborStroke(centerId, texel, cell, pixelWidth);
17434
+ if (uBorderEnabled > 0.5) {
17435
+ vec2 border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
16094
17436
 
16095
17437
  if (border.x > 0.5) {
16096
17438
  finalColor = premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
@@ -16103,9 +17445,9 @@ void main(void) {
16103
17445
  }
16104
17446
 
16105
17447
  if (uBorderEnabled > 0.5) {
16106
- float width = readStrokeWidth(centerId);
17448
+ float width = strokeWidthInTexels(centerId, pixelWidth);
16107
17449
  if (width > 0.0 && readStroke(centerId).a > 0.0) {
16108
- float coverage = innerStrokeCoverage(centerId, texel, cell, width, pixelWidth);
17450
+ float coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
16109
17451
  if (coverage >= 1.0) {
16110
17452
  finalColor = premultiplyAlpha(readStroke(centerId) * vColor);
16111
17453
  return;
@@ -16129,9 +17471,10 @@ struct MaskUniforms {
16129
17471
  uBorderEnabled: f32,
16130
17472
  ${idMaskFillPaletteWgslField}
16131
17473
  uMaxStrokeWidth: f32,
16132
- uMaxFractionalStrokeWidth: f32,
17474
+ uStrokePixelRatio: f32,
16133
17475
  ${idMaskStrokePaletteWgslField}
16134
17476
  ${idMaskStrokeWidthsWgslField}
17477
+ ${idMaskStrokeAlignmentsWgslField}
16135
17478
  uTextureSize: vec2<f32>,
16136
17479
  }
16137
17480
 
@@ -16139,9 +17482,6 @@ struct MaskUniforms {
16139
17482
  @group(2) @binding(1) var uTexture: texture_2d<f32>;
16140
17483
  @group(2) @binding(2) var uSampler: sampler;
16141
17484
 
16142
- fn sampleMaskId(uv: vec2<f32>) -> f32 {
16143
- return floor(textureSampleLevel(uTexture, uSampler, uv, 0.0).r * 255.0 + 0.5);
16144
- }
16145
17485
  ${idMaskPaletteWgsl}
16146
17486
  fn premultiplyAlpha(color: vec4<f32>) -> vec4<f32> {
16147
17487
  return vec4<f32>(color.rgb * color.a, color.a);
@@ -16158,15 +17498,16 @@ fn mainFragment(
16158
17498
  @location(0) vUV: vec2<f32>,
16159
17499
  @location(1) vColor: vec4<f32>,
16160
17500
  ) -> @location(0) vec4<f32> {
16161
- let centerId = sampleMaskId(vUV);
16162
- let texel = 1.0 / maskUniforms.uTextureSize;
16163
- let cell = fract(vUV * maskUniforms.uTextureSize);
16164
- let footprint = fwidth(vUV * maskUniforms.uTextureSize);
17501
+ let position = vUV * maskUniforms.uTextureSize;
17502
+ let sourceCell = vec2<i32>(floor(position));
17503
+ let centerId = sampleMaskIdCell(sourceCell);
17504
+ let cell = fract(position);
17505
+ let footprint = fwidth(position);
16165
17506
  let pixelWidth = max(max(footprint.x, footprint.y), 0.00001);
16166
17507
 
16167
17508
  if (centerId < 0.5) {
16168
- if (maskUniforms.uBorderEnabled > 0.5 && maskUniforms.uMaxStrokeWidth > 0.0) {
16169
- let border = findNeighborStroke(vUV, centerId, texel, cell, pixelWidth);
17509
+ if (maskUniforms.uBorderEnabled > 0.5) {
17510
+ let border = findNeighborStroke(centerId, sourceCell, cell, pixelWidth);
16170
17511
 
16171
17512
  if (border.x > 0.5) {
16172
17513
  return premultiplyAlpha(readStroke(border.x) * vColor) * border.y;
@@ -16177,9 +17518,9 @@ fn mainFragment(
16177
17518
  }
16178
17519
 
16179
17520
  if (maskUniforms.uBorderEnabled > 0.5) {
16180
- let width = readStrokeWidth(centerId);
17521
+ let width = strokeWidthInTexels(centerId, pixelWidth);
16181
17522
  if (width > 0.0 && readStroke(centerId).a > 0.0) {
16182
- let coverage = innerStrokeCoverage(vUV, centerId, texel, cell, width, pixelWidth);
17523
+ let coverage = innerStrokeCoverage(centerId, sourceCell, cell, width, pixelWidth);
16183
17524
  if (coverage >= 1.0) {
16184
17525
  return premultiplyAlpha(readStroke(centerId) * vColor);
16185
17526
  }
@@ -16245,7 +17586,10 @@ function createPixiMaskLayer(options) {
16245
17586
  const haloTextures = new Map();
16246
17587
  const regionMaskTextures = new Map();
16247
17588
  const maskFrameOptions = options.renderPreparation?.maskFrame;
17589
+ const capsRasterToDisplay = maskFrameOptions?.display !== undefined;
16248
17590
  let rasterDisplay = maskFrameOptions?.display;
17591
+ let rasterResolutionScale = 1;
17592
+ let strokeViewportScale;
16249
17593
  const preparedRenderWindow = createPreparedRenderWindow({
16250
17594
  artifactKind: options.artifactKind,
16251
17595
  detectionTimeline: options.detectionTimeline,
@@ -16269,6 +17613,7 @@ function createPixiMaskLayer(options) {
16269
17613
  resolveInstructions: options.resolveInstructions,
16270
17614
  resolveMaxRasterWidth,
16271
17615
  resolveMaskFrameBackingBytes,
17616
+ resolveMaskDisplayWidth,
16272
17617
  });
16273
17618
  return {
16274
17619
  createSprite({ width, height }) {
@@ -16387,16 +17732,24 @@ function createPixiMaskLayer(options) {
16387
17732
  setPlaybackActive(active) {
16388
17733
  preparedRenderWindow.setPlaybackActive(active);
16389
17734
  },
16390
- setRasterDisplay(display) {
16391
- if (!rasterDisplay || isDestroyed)
17735
+ setRasterDisplay(display, resolutionScale = 1) {
17736
+ if (isDestroyed)
16392
17737
  return;
16393
17738
  const previousWidth = resolveMaxRasterWidth();
16394
17739
  rasterDisplay = display;
17740
+ rasterResolutionScale = resolutionScale;
17741
+ idMaskRenderer?.setStrokePixelRatio(resolveDisplayPixelRatio(display) * resolutionScale);
16395
17742
  const nextWidth = resolveMaxRasterWidth();
16396
17743
  if (mediaWidth > 0 && previousWidth !== nextWidth) {
16397
17744
  preparedRenderWindow.invalidateRasterSize();
16398
17745
  }
16399
17746
  },
17747
+ setViewportScale(scale) {
17748
+ if (isDestroyed || scale <= 0 || scale === strokeViewportScale)
17749
+ return;
17750
+ strokeViewportScale = scale;
17751
+ preparedRenderWindow.invalidateMaskDisplayWidth();
17752
+ },
16400
17753
  setTimelineContext(context) {
16401
17754
  preparedRenderWindow.setTimelineContext(context);
16402
17755
  },
@@ -16486,14 +17839,22 @@ function createPixiMaskLayer(options) {
16486
17839
  maskTextures.set(maskFrame.key, texture);
16487
17840
  return texture;
16488
17841
  }
16489
- /**
16490
- * The widest raster the picture can show. Polygon frames rasterize geometry
16491
- * at a size of their own choosing, so this sizes mask frames only.
16492
- */
17842
+ function resolveMaskDisplayWidth() {
17843
+ if (strokeViewportScale !== undefined && mediaWidth > 0) {
17844
+ return mediaWidth * strokeViewportScale;
17845
+ }
17846
+ const display = rasterDisplay;
17847
+ if (!display || mediaWidth <= 0 || mediaHeight <= 0)
17848
+ return undefined;
17849
+ const fit = Math.min(display.boxWidth / mediaWidth, display.boxHeight / mediaHeight);
17850
+ return fit > 0 ? mediaWidth * fit : undefined;
17851
+ }
17852
+ /** Mask detail follows the annotation capture density. */
16493
17853
  function resolveMaxRasterWidth() {
16494
17854
  const display = rasterDisplay;
16495
17855
  const artifactKind = options.artifactKind ?? RenderPreparationArtifactKind.MaskFrame;
16496
17856
  if (!display ||
17857
+ !capsRasterToDisplay ||
16497
17858
  artifactKind !== RenderPreparationArtifactKind.MaskFrame ||
16498
17859
  mediaWidth <= 0 ||
16499
17860
  mediaHeight <= 0) {
@@ -16502,7 +17863,7 @@ function createPixiMaskLayer(options) {
16502
17863
  const fit = Math.min(display.boxWidth / mediaWidth, display.boxHeight / mediaHeight);
16503
17864
  const pixelRatio = resolveDisplayPixelRatio(display);
16504
17865
  return fit > 0 && pixelRatio > 0
16505
- ? alignRasterWidth(Math.ceil(mediaWidth * fit * pixelRatio))
17866
+ ? alignRasterWidth(Math.ceil(mediaWidth * fit * pixelRatio * rasterResolutionScale))
16506
17867
  : undefined;
16507
17868
  }
16508
17869
  /**
@@ -16583,7 +17944,9 @@ function createPixiMaskLayer(options) {
16583
17944
  return;
16584
17945
  }
16585
17946
  maskSprite.visible = false;
16586
- idMaskRenderer.render(maskFrame, texture);
17947
+ idMaskRenderer.render(maskFrame, texture, rasterDisplay
17948
+ ? resolveDisplayPixelRatio(rasterDisplay) * rasterResolutionScale
17949
+ : 1);
16587
17950
  }
16588
17951
  function hideFill() {
16589
17952
  if (maskSprite) {
@@ -17022,11 +18385,15 @@ function createPixiPolygonLayer(options) {
17022
18385
  rasterLayer.setMaskStyle(nextPolygonStyle ? createArtifactStyle() : nextPolygonStyle);
17023
18386
  },
17024
18387
  needsRenderPreparationWait: rasterLayer.needsRenderPreparationWait,
18388
+ setPlaybackActive: rasterLayer.setPlaybackActive,
18389
+ setRasterDisplay: rasterLayer.setRasterDisplay,
17025
18390
  setTimelineContext: rasterLayer.setTimelineContext,
18391
+ setViewportScale: adoptViewportScale,
17026
18392
  waitForRenderPreparation: rasterLayer.waitForRenderPreparation,
17027
18393
  };
17028
18394
  function adoptViewportScale(nextViewportScale) {
17029
18395
  const normalizedViewportScale = Math.max(nextViewportScale, Number.EPSILON);
18396
+ rasterLayer.setViewportScale(normalizedViewportScale);
17030
18397
  if (normalizedViewportScale === viewportScale) {
17031
18398
  return;
17032
18399
  }
@@ -17090,9 +18457,10 @@ function resolvePreparedPolygonInstructions(options) {
17090
18457
  },
17091
18458
  stroke: instruction.stroke
17092
18459
  ? {
18460
+ alignment: BoxStrokeAlignment.Center,
17093
18461
  alpha: instruction.stroke.alpha,
17094
18462
  color: instruction.stroke.color,
17095
- width: resolveScreenLength(instruction.stroke.width, options.viewportScale),
18463
+ width: instruction.stroke.width,
17096
18464
  }
17097
18465
  : undefined,
17098
18466
  });
@@ -17237,17 +18605,30 @@ function createPixiRegionCoverageMask(options) {
17237
18605
  });
17238
18606
  let shader = createShader();
17239
18607
  const display = new options.Mesh({ geometry, shader });
17240
- const effect = new options.AlphaMask({ mask: display });
18608
+ const container = new options.Container();
18609
+ container.addChild(display);
18610
+ const effect = new options.AlphaMask({ mask: container });
18611
+ let antialiasFilter;
17241
18612
  return {
17242
18613
  destroy() {
18614
+ if (antialiasFilter && display.filters?.includes(antialiasFilter)) {
18615
+ display.filters = null;
18616
+ }
18617
+ antialiasFilter?.destroy();
17243
18618
  effect.destroy();
17244
18619
  display.destroy();
18620
+ container.destroy();
17245
18621
  destroyShaderKeepingProgram(shader);
17246
18622
  geometry.destroy();
17247
18623
  placeholderSource.destroy();
17248
18624
  },
18625
+ container,
17249
18626
  display,
17250
18627
  effect,
18628
+ setAntialiasResolution(resolution) {
18629
+ if (antialiasFilter)
18630
+ antialiasFilter.resolution = resolution;
18631
+ },
17251
18632
  releaseTexture(source) {
17252
18633
  if (shader.resources.uTexture !== source)
17253
18634
  return;
@@ -17256,6 +18637,7 @@ function createPixiRegionCoverageMask(options) {
17256
18637
  },
17257
18638
  render(renderOptions) {
17258
18639
  bindTexture(renderOptions.artifact.texture.source);
18640
+ const alphaCoverage = updateAntialiasing();
17259
18641
  display.visible = true;
17260
18642
  uniforms.uniforms.uCrop = new Float32Array([
17261
18643
  renderOptions.crop.x,
@@ -17273,8 +18655,34 @@ function createPixiRegionCoverageMask(options) {
17273
18655
  display.position.set(renderOptions.x, renderOptions.y);
17274
18656
  display.scale.set(renderOptions.width * (renderOptions.flipHorizontal ? -1 : 1), renderOptions.height * (renderOptions.flipVertical ? -1 : 1));
17275
18657
  display.rotation = renderOptions.rotation;
18658
+ syncViewportBounds();
18659
+ return alphaCoverage;
17276
18660
  },
18661
+ syncViewportBounds,
17277
18662
  };
18663
+ function syncViewportBounds() {
18664
+ syncPixiMaskViewportBounds(container, options.getViewportBounds?.(), options.Rectangle);
18665
+ }
18666
+ function updateAntialiasing() {
18667
+ if (options.getAnnotationAntialiasing?.() &&
18668
+ options.Filter &&
18669
+ options.defaultFilterVert) {
18670
+ antialiasFilter ??= createPixiAnnotationAntialiasFilter({
18671
+ maskCoverage: true,
18672
+ Filter: options.Filter,
18673
+ defaultFilterVert: options.defaultFilterVert,
18674
+ resolution: options.getAntialiasResolution?.(),
18675
+ });
18676
+ if (display.filters?.[0] !== antialiasFilter) {
18677
+ display.filters = [antialiasFilter];
18678
+ }
18679
+ return true;
18680
+ }
18681
+ if (antialiasFilter && display.filters?.includes(antialiasFilter)) {
18682
+ display.filters = null;
18683
+ }
18684
+ return false;
18685
+ }
17278
18686
  function bindTexture(source) {
17279
18687
  try {
17280
18688
  shader.resources.uTexture = source;
@@ -17576,6 +18984,7 @@ function createPixiRegionLayer(options) {
17576
18984
  let currentMediaTime = 0;
17577
18985
  let currentViewportScale = 1;
17578
18986
  let destroyed = false;
18987
+ let viewportBounds;
17579
18988
  let renderers = [...options.regionRenderers];
17580
18989
  const assets = new Map();
17581
18990
  const entries = new Map();
@@ -17656,6 +19065,7 @@ function createPixiRegionLayer(options) {
17656
19065
  }
17657
19066
  const position = positionSprite(entry.display, renderer, region, sourceSize, viewportScale);
17658
19067
  updateEffect(entry, renderer, viewportScale);
19068
+ updateAssetAntialiasing(entry, renderer);
17659
19069
  if (renderer.source.kind === RegionRendererSourceKind.Media &&
17660
19070
  mediaCrop &&
17661
19071
  !updateMediaCoverage(entry, renderer, detection, detectionIndex, mediaCrop)) {
@@ -17707,6 +19117,15 @@ function createPixiRegionLayer(options) {
17707
19117
  }
17708
19118
  }
17709
19119
  },
19120
+ setAntialiasResolution(resolution) {
19121
+ for (const entry of entries.values()) {
19122
+ updateEntryResolution(entry, resolution);
19123
+ }
19124
+ for (const pool of pools.values()) {
19125
+ for (const entry of pool)
19126
+ updateEntryResolution(entry, resolution);
19127
+ }
19128
+ },
17710
19129
  setRenderers(nextRenderers) {
17711
19130
  renderers = [...nextRenderers];
17712
19131
  syncSources();
@@ -17720,10 +19139,24 @@ function createPixiRegionLayer(options) {
17720
19139
  entry.display.position.set(entry.baseX + x, entry.baseY + y);
17721
19140
  entry.coverageMask?.position.set(entry.baseX + x, entry.baseY + y);
17722
19141
  entry.exactCoverageMask?.display.position.set(entry.baseX + x, entry.baseY + y);
19142
+ syncEntryViewportBounds(entry);
17723
19143
  translated = true;
17724
19144
  }
17725
19145
  return translated;
17726
19146
  },
19147
+ syncViewportBounds() {
19148
+ const next = options.getViewportBounds?.();
19149
+ if (next?.x === viewportBounds?.x &&
19150
+ next?.y === viewportBounds?.y &&
19151
+ next?.width === viewportBounds?.width &&
19152
+ next?.height === viewportBounds?.height)
19153
+ return;
19154
+ viewportBounds = next ? { ...next } : undefined;
19155
+ for (const entry of entries.values()) {
19156
+ if (entry.active)
19157
+ syncEntryViewportBounds(entry);
19158
+ }
19159
+ },
17727
19160
  destroy() {
17728
19161
  destroyed = true;
17729
19162
  destroyAllDisplays();
@@ -17845,11 +19278,18 @@ function createPixiRegionLayer(options) {
17845
19278
  return entry;
17846
19279
  }
17847
19280
  function updateMediaCoverage(entry, renderer, detection, detectionIndex, crop) {
17848
- if (renderer.source.kind !== RegionRendererSourceKind.Media ||
17849
- renderer.source.coverage === undefined) {
19281
+ if (renderer.source.kind !== RegionRendererSourceKind.Media) {
17850
19282
  removeCoverageMask(entry);
17851
19283
  return true;
17852
19284
  }
19285
+ if (renderer.source.coverage === undefined) {
19286
+ removeExactCoverageMask(entry);
19287
+ if (!annotationAntialiasingEnabled()) {
19288
+ removePolygonCoverageMask(entry);
19289
+ return true;
19290
+ }
19291
+ return updateGraphicsCoverage(entry, renderer, crop);
19292
+ }
17853
19293
  if (renderer.source.coverage.kind === RegionRendererCoverageKind.Mask) {
17854
19294
  removePolygonCoverageMask(entry);
17855
19295
  const artifact = options.getActiveRegionMaskCoverage(currentFrame?.mediaTime ?? null);
@@ -17867,7 +19307,14 @@ function createPixiRegionLayer(options) {
17867
19307
  }
17868
19308
  const mask = entry.exactCoverageMask ??
17869
19309
  createPixiRegionCoverageMask({
19310
+ getAnnotationAntialiasing: options.getAnnotationAntialiasing,
19311
+ getAntialiasResolution: options.getAntialiasResolution,
19312
+ getViewportBounds: options.getViewportBounds,
19313
+ defaultFilterVert: options.defaultFilterVert,
19314
+ Filter: options.Filter,
17870
19315
  AlphaMask: options.AlphaMask,
19316
+ Container: options.Container,
19317
+ Rectangle: options.Rectangle,
17871
19318
  ImageSource: options.ImageSource,
17872
19319
  Mesh: options.Mesh,
17873
19320
  MeshGeometry: options.MeshGeometry,
@@ -17876,7 +19323,7 @@ function createPixiRegionLayer(options) {
17876
19323
  });
17877
19324
  if (!entry.exactCoverageMask) {
17878
19325
  entry.exactCoverageMask = mask;
17879
- container?.addChild(mask.display);
19326
+ container?.addChild(mask.container);
17880
19327
  // A Mesh assigned through `display.mask` becomes a stencil mask in
17881
19328
  // Pixi v8, which clips by the quad geometry and ignores shader alpha.
17882
19329
  // AlphaMask renders the id-sampling mesh to a GPU texture first, so
@@ -17888,7 +19335,7 @@ function createPixiRegionLayer(options) {
17888
19335
  removeExactCoverageMask(entry);
17889
19336
  return false;
17890
19337
  }
17891
- mask.render({
19338
+ const alphaCoverage = mask.render({
17892
19339
  artifact: { texture: coverageTexture },
17893
19340
  coverage: coverageEntry,
17894
19341
  crop,
@@ -17900,6 +19347,7 @@ function createPixiRegionLayer(options) {
17900
19347
  x: entry.display.position.x,
17901
19348
  y: entry.display.position.y,
17902
19349
  });
19350
+ setCoverageChannel(entry, alphaCoverage);
17903
19351
  return true;
17904
19352
  }
17905
19353
  if (renderer.source.coverage.kind !== RegionRendererCoverageKind.Polygon ||
@@ -17909,26 +19357,95 @@ function createPixiRegionLayer(options) {
17909
19357
  return false;
17910
19358
  }
17911
19359
  removeExactCoverageMask(entry);
19360
+ return updateGraphicsCoverage(entry, renderer, crop, detection.polygon.points);
19361
+ }
19362
+ function updateGraphicsCoverage(entry, renderer, crop, points) {
17912
19363
  const mask = entry.coverageMask ?? new options.Graphics();
17913
19364
  if (!entry.coverageMask) {
17914
19365
  entry.coverageMask = mask;
17915
- container?.addChild(mask);
17916
- entry.display.mask = mask;
19366
+ entry.coverageMaskContainer = new options.Container();
19367
+ entry.coverageMaskContainer.addChild(mask);
19368
+ container?.addChild(entry.coverageMaskContainer);
19369
+ }
19370
+ mask.clear();
19371
+ if (points) {
19372
+ mask.poly(points.flatMap(({ x, y }) => [
19373
+ x - crop.x - crop.width / 2,
19374
+ y - crop.y - crop.height / 2,
19375
+ ]), true);
17917
19376
  }
17918
- mask
17919
- .clear()
17920
- .poly(detection.polygon.points.flatMap(({ x, y }) => [
17921
- x - crop.x - crop.width / 2,
17922
- y - crop.y - crop.height / 2,
17923
- ]), true)
17924
- .fill({ alpha: 1, color: 0xffffff });
19377
+ else {
19378
+ mask.rect(-crop.width / 2, -crop.height / 2, crop.width, crop.height);
19379
+ }
19380
+ mask.fill({ alpha: 1, color: 0xffffff });
17925
19381
  mask.position.set(entry.display.position.x, entry.display.position.y);
17926
19382
  mask.scale.set((entry.display.width / crop.width) *
17927
19383
  (renderer.transform?.flip?.horizontal ? -1 : 1), (entry.display.height / crop.height) *
17928
19384
  (renderer.transform?.flip?.vertical ? -1 : 1));
17929
19385
  mask.rotation = entry.display.rotation;
19386
+ if (annotationAntialiasingEnabled()) {
19387
+ entry.display.mask = null;
19388
+ const effect = entry.coverageMaskEffect ??
19389
+ new options.AlphaMask({ mask: entry.coverageMaskContainer });
19390
+ if (!entry.coverageMaskEffect)
19391
+ entry.coverageMaskEffect = effect;
19392
+ if (!entry.display.effects?.includes(effect)) {
19393
+ effect.init(entry.coverageMaskContainer);
19394
+ entry.display.addEffect(effect);
19395
+ }
19396
+ const filter = ensureAntialiasFilter(entry, true);
19397
+ if (filter && mask.filters?.[0] !== filter)
19398
+ mask.filters = [filter];
19399
+ syncEntryViewportBounds(entry);
19400
+ setCoverageChannel(entry, true);
19401
+ }
19402
+ else {
19403
+ if (entry.coverageMaskEffect) {
19404
+ entry.display.removeEffect(entry.coverageMaskEffect);
19405
+ entry.coverageMaskEffect.reset();
19406
+ }
19407
+ if (mask.filters?.length)
19408
+ mask.filters = null;
19409
+ setCoverageChannel(entry, false);
19410
+ entry.display.mask = mask;
19411
+ }
17930
19412
  return true;
17931
19413
  }
19414
+ function syncEntryViewportBounds(entry) {
19415
+ entry.exactCoverageMask?.syncViewportBounds();
19416
+ if (entry.coverageMaskContainer) {
19417
+ syncPixiMaskViewportBounds(entry.coverageMaskContainer, options.getViewportBounds?.(), options.Rectangle);
19418
+ }
19419
+ }
19420
+ function annotationAntialiasingEnabled() {
19421
+ return Boolean(options.getAnnotationAntialiasing?.() &&
19422
+ options.Filter &&
19423
+ options.defaultFilterVert);
19424
+ }
19425
+ function ensureAntialiasFilter(entry, maskCoverage = false) {
19426
+ if (!options.Filter || !options.defaultFilterVert)
19427
+ return undefined;
19428
+ entry.antialiasFilter ??= createPixiAnnotationAntialiasFilter({
19429
+ maskCoverage,
19430
+ Filter: options.Filter,
19431
+ defaultFilterVert: options.defaultFilterVert,
19432
+ resolution: options.getAntialiasResolution?.(),
19433
+ });
19434
+ return entry.antialiasFilter;
19435
+ }
19436
+ function updateAssetAntialiasing(entry, renderer) {
19437
+ if (renderer.source.kind !== RegionRendererSourceKind.Asset)
19438
+ return;
19439
+ if (annotationAntialiasingEnabled()) {
19440
+ const filter = ensureAntialiasFilter(entry);
19441
+ if (filter && entry.display.filters?.[0] !== filter) {
19442
+ entry.display.filters = [filter];
19443
+ }
19444
+ }
19445
+ else if (entry.antialiasFilter && entry.display.filters?.length) {
19446
+ entry.display.filters = null;
19447
+ }
19448
+ }
17932
19449
  function updateEffect(entry, renderer, viewportScale) {
17933
19450
  const effect = renderer.source.kind === RegionRendererSourceKind.Media
17934
19451
  ? renderer.source.effect
@@ -17948,26 +19465,6 @@ function createPixiRegionLayer(options) {
17948
19465
  }
17949
19466
  entry.effect?.apply(entry.display, viewportScale);
17950
19467
  }
17951
- function removeCoverageMask(entry) {
17952
- removePolygonCoverageMask(entry);
17953
- removeExactCoverageMask(entry);
17954
- }
17955
- function removePolygonCoverageMask(entry) {
17956
- if (!entry.coverageMask)
17957
- return;
17958
- entry.display.mask = null;
17959
- entry.coverageMask.removeFromParent();
17960
- entry.coverageMask.destroy();
17961
- entry.coverageMask = undefined;
17962
- }
17963
- function removeExactCoverageMask(entry) {
17964
- if (!entry.exactCoverageMask)
17965
- return;
17966
- entry.display.removeEffect(entry.exactCoverageMask.effect);
17967
- entry.exactCoverageMask.display.removeFromParent();
17968
- entry.exactCoverageMask.destroy();
17969
- entry.exactCoverageMask = undefined;
17970
- }
17971
19468
  function destroyRendererDisplays(rendererId) {
17972
19469
  for (const [key, entry] of entries) {
17973
19470
  if (entry.rendererId !== rendererId)
@@ -17999,17 +19496,53 @@ function createPixiRegionLayer(options) {
17999
19496
  pools.clear();
18000
19497
  }
18001
19498
  }
19499
+ function setCoverageChannel(entry, alpha) {
19500
+ if ((entry.alphaCoverage ?? false) === alpha)
19501
+ return;
19502
+ entry.display.setMask({ channel: alpha ? "alpha" : "red" });
19503
+ entry.alphaCoverage = alpha;
19504
+ }
19505
+ function removeCoverageMask(entry) {
19506
+ removePolygonCoverageMask(entry);
19507
+ removeExactCoverageMask(entry);
19508
+ }
19509
+ function removePolygonCoverageMask(entry) {
19510
+ if (!entry.coverageMask)
19511
+ return;
19512
+ entry.display.mask = null;
19513
+ if (entry.coverageMaskEffect) {
19514
+ entry.display.removeEffect(entry.coverageMaskEffect);
19515
+ entry.coverageMaskEffect.destroy();
19516
+ entry.coverageMaskEffect = undefined;
19517
+ }
19518
+ destroyEntryAntialiasFilter(entry);
19519
+ entry.coverageMask.removeFromParent();
19520
+ entry.coverageMask.destroy();
19521
+ entry.coverageMask = undefined;
19522
+ entry.coverageMaskContainer?.removeFromParent();
19523
+ entry.coverageMaskContainer?.destroy();
19524
+ entry.coverageMaskContainer = undefined;
19525
+ setCoverageChannel(entry, false);
19526
+ }
19527
+ function removeExactCoverageMask(entry) {
19528
+ if (!entry.exactCoverageMask)
19529
+ return;
19530
+ entry.display.removeEffect(entry.exactCoverageMask.effect);
19531
+ entry.exactCoverageMask.container.removeFromParent();
19532
+ entry.exactCoverageMask.destroy();
19533
+ entry.exactCoverageMask = undefined;
19534
+ setCoverageChannel(entry, false);
19535
+ }
19536
+ function updateEntryResolution(entry, resolution) {
19537
+ if (entry.antialiasFilter)
19538
+ entry.antialiasFilter.resolution = resolution;
19539
+ entry.exactCoverageMask?.setAntialiasResolution(resolution);
19540
+ }
18002
19541
  function destroyEntry(entry) {
18003
19542
  entry.effect?.destroy();
18004
19543
  entry.effect = undefined;
18005
- entry.display.mask = null;
18006
- entry.coverageMask?.removeFromParent();
18007
- entry.coverageMask?.destroy();
18008
- if (entry.exactCoverageMask) {
18009
- entry.display.removeEffect(entry.exactCoverageMask.effect);
18010
- entry.exactCoverageMask.display.removeFromParent();
18011
- entry.exactCoverageMask.destroy();
18012
- }
19544
+ removeCoverageMask(entry);
19545
+ destroyEntryAntialiasFilter(entry);
18013
19546
  entry.display.removeFromParent?.();
18014
19547
  // GifSprite sources are shared and released through Assets.unload().
18015
19548
  entry.display.destroy();
@@ -18017,6 +19550,18 @@ function destroyEntry(entry) {
18017
19550
  // lightweight crop texture; the scene destroys the shared source.
18018
19551
  entry.ownedTexture?.destroy(false);
18019
19552
  }
19553
+ function destroyEntryAntialiasFilter(entry) {
19554
+ if (!entry.antialiasFilter)
19555
+ return;
19556
+ if (entry.display.filters?.includes(entry.antialiasFilter)) {
19557
+ entry.display.filters = null;
19558
+ }
19559
+ if (entry.coverageMask?.filters?.includes(entry.antialiasFilter)) {
19560
+ entry.coverageMask.filters = null;
19561
+ }
19562
+ entry.antialiasFilter.destroy();
19563
+ entry.antialiasFilter = undefined;
19564
+ }
18020
19565
  function isAnimatedDisplay(display) {
18021
19566
  return "play" in display && typeof display.play === "function";
18022
19567
  }
@@ -18212,6 +19757,7 @@ function acquireAsset(loader, src) {
18212
19757
 
18213
19758
  function createPixiAnnotationOverlayLayer(editingEngine, initialStyle, initialKeypointStyle) {
18214
19759
  let graphics;
19760
+ let renderedEditingBox;
18215
19761
  let style = resolveAnnotationOverlayStyle(initialStyle);
18216
19762
  let keypointStyle = initialKeypointStyle === undefined
18217
19763
  ? new BaseKeypointStyle()
@@ -18228,12 +19774,16 @@ function createPixiAnnotationOverlayLayer(editingEngine, initialStyle, initialKe
18228
19774
  if (next !== undefined)
18229
19775
  keypointStyle = next;
18230
19776
  },
19777
+ getRenderedEditingBox() {
19778
+ return renderedEditingBox;
19779
+ },
18231
19780
  draw(context) {
19781
+ renderedEditingBox = undefined;
18232
19782
  if (!graphics)
18233
19783
  return;
18234
19784
  graphics.clear();
18235
19785
  drawExternalPreview(graphics, context.previewOverlay, context.viewportScale, style);
18236
- drawEditingPreview(graphics, editingEngine, context.frame, context.viewportScale, style, keypointStyle);
19786
+ renderedEditingBox = drawEditingPreview(graphics, editingEngine, context.frame, context.viewportScale, style, keypointStyle);
18237
19787
  drawSelectionHandles(graphics, context, style, editingEngine);
18238
19788
  drawMarquee(graphics, context.marquee, style);
18239
19789
  drawGuides(graphics, editingEngine, context, style);
@@ -18246,6 +19796,7 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
18246
19796
  const detection = state?.preview;
18247
19797
  if (!detection)
18248
19798
  return;
19799
+ let renderedBox;
18249
19800
  const styleContext = {
18250
19801
  gestureKind: state.kind,
18251
19802
  viewportScale,
@@ -18272,11 +19823,16 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
18272
19823
  { x: x + width / 2, y: y + height / 2 },
18273
19824
  { x: x - width / 2, y: y + height / 2 },
18274
19825
  ], true, stroke, viewportScale);
19826
+ if (width > 0 &&
19827
+ height > 0 &&
19828
+ (boxFill.alpha > 0 || (stroke.alpha > 0 && stroke.width > 0))) {
19829
+ renderedBox = { detection, rect: detection.rect };
19830
+ }
18275
19831
  }
18276
19832
  if (detection.polygon) {
18277
19833
  if (state.kind === AnnotationGestureStateKind.Creating) {
18278
19834
  drawCreatingPolygon(graphics, detection.polygon.points, viewportScale, stroke, closeZoneStroke);
18279
- return;
19835
+ return renderedBox;
18280
19836
  }
18281
19837
  graphics
18282
19838
  .poly(detection.polygon.points.flatMap(({ x, y }) => [x, y]), true)
@@ -18308,6 +19864,7 @@ function drawEditingPreview(graphics, engine, frame, viewportScale, style, keypo
18308
19864
  if (instruction)
18309
19865
  drawPixiKeypointInstruction(graphics, instruction, viewportScale);
18310
19866
  }
19867
+ return renderedBox;
18311
19868
  }
18312
19869
  const CREATION_VERTEX_RADIUS = 6;
18313
19870
  const CREATION_VERTEX_STROKE_WIDTH = 1.5;
@@ -18560,92 +20117,6 @@ function resolveAnnotationOverlayStyle(style) {
18560
20117
  };
18561
20118
  }
18562
20119
 
18563
- var PixiSceneLayerKind;
18564
- (function (PixiSceneLayerKind) {
18565
- PixiSceneLayerKind["Media"] = "media";
18566
- PixiSceneLayerKind["Depth"] = "depth";
18567
- PixiSceneLayerKind["Heatmap"] = "heatmap";
18568
- PixiSceneLayerKind["Mask"] = "mask";
18569
- PixiSceneLayerKind["Box"] = "box";
18570
- PixiSceneLayerKind["Vector"] = "vector";
18571
- PixiSceneLayerKind["Region"] = "region";
18572
- PixiSceneLayerKind["Focus"] = "focus";
18573
- PixiSceneLayerKind["Preview"] = "preview";
18574
- PixiSceneLayerKind["Guide"] = "guide";
18575
- PixiSceneLayerKind["Handle"] = "handle";
18576
- PixiSceneLayerKind["Interaction"] = "interaction";
18577
- PixiSceneLayerKind["Label"] = "label";
18578
- })(PixiSceneLayerKind || (PixiSceneLayerKind = {}));
18579
- function createPixiSceneLayerSlot(kind, order = defaultLayerOrder(kind)) {
18580
- let display;
18581
- return {
18582
- getDisplay() {
18583
- return display;
18584
- },
18585
- kind,
18586
- order,
18587
- setDisplay(nextDisplay) {
18588
- display = nextDisplay;
18589
- },
18590
- };
18591
- }
18592
- function syncPixiSceneLayerChildren(scene, slots) {
18593
- if (!scene) {
18594
- return;
18595
- }
18596
- const displays = [...slots]
18597
- .sort((left, right) => left.order - right.order)
18598
- .map((slot) => slot.getDisplay())
18599
- .filter((display) => display !== undefined);
18600
- clearSceneChildren(scene);
18601
- if (displays.length === 0) {
18602
- return;
18603
- }
18604
- scene.addChild(...displays);
18605
- }
18606
- function defaultLayerOrder(kind) {
18607
- switch (kind) {
18608
- case PixiSceneLayerKind.Media:
18609
- return 0;
18610
- case PixiSceneLayerKind.Depth:
18611
- return 25;
18612
- case PixiSceneLayerKind.Heatmap:
18613
- return 50;
18614
- case PixiSceneLayerKind.Mask:
18615
- return 100;
18616
- case PixiSceneLayerKind.Box:
18617
- return 200;
18618
- case PixiSceneLayerKind.Vector:
18619
- return 250;
18620
- case PixiSceneLayerKind.Region:
18621
- return 275;
18622
- case PixiSceneLayerKind.Focus:
18623
- return 300;
18624
- case PixiSceneLayerKind.Preview:
18625
- return 400;
18626
- case PixiSceneLayerKind.Guide:
18627
- return 500;
18628
- case PixiSceneLayerKind.Handle:
18629
- return 750;
18630
- case PixiSceneLayerKind.Interaction:
18631
- return 700;
18632
- case PixiSceneLayerKind.Label:
18633
- return 800;
18634
- default:
18635
- return 350;
18636
- }
18637
- }
18638
- function clearSceneChildren(scene) {
18639
- if (typeof scene.removeChildren === "function") {
18640
- scene.removeChildren();
18641
- return;
18642
- }
18643
- const fakeScene = scene;
18644
- if (Array.isArray(fakeScene.children)) {
18645
- fakeScene.children.splice(0);
18646
- }
18647
- }
18648
-
18649
20120
  function calculatePixiSceneFit(options) {
18650
20121
  const { fit, mediaHeight, mediaWidth, screenHeight, screenWidth } = options;
18651
20122
  if (mediaWidth <= 0 ||
@@ -18681,6 +20152,7 @@ const REGION_COVERAGE_ONLY_MASK_STYLE = {
18681
20152
  const STATIC_FOCUS_SETTLE_MS = 10_000;
18682
20153
  /** The presentation fields a render answers to that no renderer kind owns. */
18683
20154
  const RENDERED_PRESENTATION_TAIL = [
20155
+ "annotationAntialiasing",
18684
20156
  "annotationOverlayStyle",
18685
20157
  "backgroundColor",
18686
20158
  "focusStyle",
@@ -18776,6 +20248,14 @@ async function createPixiMediaScene(options) {
18776
20248
  let currentFocusStyle = options.focusStyle ?? null;
18777
20249
  let currentLabelStyle = options.labelStyle ?? null;
18778
20250
  let currentMaskStyle = options.maskStyle ?? null;
20251
+ let annotationAntialiasing = options.annotationAntialiasing ?? false;
20252
+ let annotationAntialiasResolution = 1;
20253
+ const annotationAntialiasLayer = createPixiAnnotationAntialiasLayer({
20254
+ Container,
20255
+ Filter,
20256
+ defaultFilterVert,
20257
+ getEnabled: () => Boolean(annotationAntialiasing),
20258
+ });
18779
20259
  let currentMaskHaloStyle = options.maskHaloStyle ?? null;
18780
20260
  let currentBoxCornerStyle = options.boxCornerStyle ?? null;
18781
20261
  let currentEllipseStyle = options.ellipseStyle ?? null;
@@ -18870,6 +20350,7 @@ async function createPixiMediaScene(options) {
18870
20350
  isVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
18871
20351
  // A partial heatmap changes pixels before the all-ready token changes.
18872
20352
  onPreparedWindowChange: () => redrawAnnotationsNow(true),
20353
+ onTextureRelease: (texture) => focusLayer?.releaseMaskTexture(texture),
18873
20354
  prepareTexture: (texture) => app.renderer.prepare.upload(texture),
18874
20355
  renderPreparation: options.renderPreparation,
18875
20356
  renderers: currentHeatmapRenderers,
@@ -18931,6 +20412,7 @@ async function createPixiMediaScene(options) {
18931
20412
  resolveContextState,
18932
20413
  })
18933
20414
  : undefined;
20415
+ polygonLayer?.setPlaybackActive(isPlaybackActive);
18934
20416
  const vectorLayer = createPixiVectorLayer({
18935
20417
  container: retainedGeometry,
18936
20418
  Container,
@@ -18960,6 +20442,9 @@ async function createPixiMediaScene(options) {
18960
20442
  Texture,
18961
20443
  UniformGroup,
18962
20444
  detectionTimeline: annotationDetectionTimeline,
20445
+ getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
20446
+ getAntialiasResolution: () => annotationAntialiasResolution,
20447
+ getViewportBounds: resolveViewportBounds,
18963
20448
  getActiveRegionMaskCoverage: (detectionFrameTime) => maskLayer?.getActiveRegionMaskCoverage(detectionFrameTime) ?? null,
18964
20449
  // Under GPU compositing the decoded frame lands in a texture the
18965
20450
  // compositor swaps onto the media sprite, leaving the canvas that
@@ -19011,6 +20496,7 @@ async function createPixiMediaScene(options) {
19011
20496
  Text,
19012
20497
  detectionTimeline: annotationDetectionTimeline,
19013
20498
  labelStyle: options.labelStyle,
20499
+ getRenderedBoxes: () => boxLayer.getRenderedBoxes(),
19014
20500
  resolveContextState: resolveLabelContextState,
19015
20501
  })
19016
20502
  : undefined;
@@ -19024,6 +20510,7 @@ async function createPixiMediaScene(options) {
19024
20510
  resolution: resolvePixiResolution(options.maxDevicePixelRatio),
19025
20511
  });
19026
20512
  const destroyBatchTextureBindings = installPixiBatchTextureBindings(pixi, app.renderer);
20513
+ const destroyFilterBindings = installPixiFilterBindings(pixi, app.renderer);
19027
20514
  const rendererCanvas = app.canvas;
19028
20515
  rendererCanvas.style.display = "block";
19029
20516
  rendererCanvas.style.position = "absolute";
@@ -19069,6 +20556,7 @@ async function createPixiMediaScene(options) {
19069
20556
  const focusSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Focus);
19070
20557
  const previewSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Preview);
19071
20558
  const interactionSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Interaction);
20559
+ const interactionLabelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label, interactionSlot.order);
19072
20560
  const handleSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Handle);
19073
20561
  const labelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label);
19074
20562
  const layerSlots = [
@@ -19083,6 +20571,7 @@ async function createPixiMediaScene(options) {
19083
20571
  previewSlot,
19084
20572
  handleSlot,
19085
20573
  interactionSlot,
20574
+ interactionLabelSlot,
19086
20575
  labelSlot,
19087
20576
  ];
19088
20577
  const viewport = createViewportController({ scale: 1 });
@@ -19254,13 +20743,18 @@ async function createPixiMediaScene(options) {
19254
20743
  }
19255
20744
  let focusLayer = options.focusStyle
19256
20745
  ? createPixiFocusLayer({
20746
+ AlphaMask,
19257
20747
  Container,
19258
20748
  Graphics,
19259
20749
  ImageSource,
19260
20750
  Mesh,
19261
20751
  MeshGeometry,
20752
+ Rectangle,
19262
20753
  Shader,
19263
20754
  UniformGroup,
20755
+ getHeatmapRenderers: () => currentHeatmapRenderers,
20756
+ getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
20757
+ getViewportBounds: resolveViewportBounds,
19264
20758
  focusStyle: options.focusStyle,
19265
20759
  isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
19266
20760
  })
@@ -19270,7 +20764,9 @@ async function createPixiMediaScene(options) {
19270
20764
  let stagingTextureSource;
19271
20765
  const collectFrameTimings = options.diagnostics?.frameTimings === true;
19272
20766
  const syncSceneChildren = () => {
19273
- syncPixiSceneLayerChildren(mediaScene, layerSlots);
20767
+ labelLayer?.setBackgroundAntialiasFilter(annotationAntialiasLayer.getFxaaFilter());
20768
+ interactionPresentationLayer?.setBackgroundAntialiasFilter(annotationAntialiasLayer.getFxaaFilter());
20769
+ annotationAntialiasLayer.sync(mediaScene, layerSlots);
19274
20770
  };
19275
20771
  const updateMediaSceneFit = () => {
19276
20772
  if (!mediaScene || mediaWidth <= 0 || mediaHeight <= 0) {
@@ -19336,8 +20832,24 @@ async function createPixiMediaScene(options) {
19336
20832
  rendererCanvas.style.left = `${next.left}px`;
19337
20833
  rendererCanvas.style.top = `${next.top}px`;
19338
20834
  app.renderer.resize(next.width, next.height, presentationResolution);
20835
+ syncAnnotationAntialiasResolution();
19339
20836
  return presentationBox;
19340
20837
  };
20838
+ const syncAnnotationAntialiasResolution = () => {
20839
+ annotationAntialiasResolution = resolvePixiAnnotationAntialiasResolution(presentationResolution, containerSize, maxTextureSize(), annotationAntialiasing === 2 ? 2 : 1);
20840
+ annotationAntialiasLayer.setResolution(annotationAntialiasResolution);
20841
+ regionLayer.setAntialiasResolution(annotationAntialiasResolution);
20842
+ syncMaskRasterDisplay();
20843
+ };
20844
+ function syncMaskRasterDisplay() {
20845
+ if (!rasterDisplay)
20846
+ return;
20847
+ const scale = annotationAntialiasing
20848
+ ? annotationAntialiasResolution / presentationResolution
20849
+ : 1;
20850
+ maskLayer?.setRasterDisplay(rasterDisplay, scale);
20851
+ polygonLayer?.setRasterDisplay(rasterDisplay, scale);
20852
+ }
19341
20853
  const syncDisplaySizing = () => {
19342
20854
  if (!baseFit || mediaWidth <= 0 || mediaHeight <= 0)
19343
20855
  return;
@@ -19354,7 +20866,7 @@ async function createPixiMediaScene(options) {
19354
20866
  return;
19355
20867
  }
19356
20868
  rasterDisplay = display;
19357
- maskLayer?.setRasterDisplay(display);
20869
+ syncMaskRasterDisplay();
19358
20870
  options.onDisplayChange?.(display);
19359
20871
  };
19360
20872
  const updateMediaSampling = () => {
@@ -19375,12 +20887,26 @@ async function createPixiMediaScene(options) {
19375
20887
  return;
19376
20888
  const transform = viewport.getTransform();
19377
20889
  viewportScale = baseFit.scale * transform.scale;
20890
+ maskLayer?.setViewportScale(viewportScale);
20891
+ polygonLayer?.setViewportScale(viewportScale);
19378
20892
  updateMediaSampling();
19379
20893
  mediaScene.scale.set(viewportScale);
19380
20894
  const box = syncPresentationBox();
19381
20895
  mediaScene.position.set(baseFit.x + transform.x - box.left, baseFit.y + transform.y - box.top);
20896
+ focusLayer?.syncViewportBounds();
20897
+ regionLayer.syncViewportBounds();
19382
20898
  maskBrushPreview?.setViewportScale(viewportScale);
19383
20899
  };
20900
+ function resolveViewportBounds() {
20901
+ if (!mediaScene || viewportScale <= 0)
20902
+ return undefined;
20903
+ return {
20904
+ x: -mediaScene.x / viewportScale,
20905
+ y: -mediaScene.y / viewportScale,
20906
+ width: app.renderer.screen.width / viewportScale,
20907
+ height: app.renderer.screen.height / viewportScale,
20908
+ };
20909
+ }
19384
20910
  const mediaPointToScreen = (point) => {
19385
20911
  const fit = baseFit ?? { scale: 1, x: 0, y: 0 };
19386
20912
  const screenPoint = viewport.mediaToScreen({
@@ -19402,6 +20928,7 @@ async function createPixiMediaScene(options) {
19402
20928
  syncPresentationBox();
19403
20929
  updateMediaSceneFit();
19404
20930
  syncDisplaySizing();
20931
+ drawInteractionPresentationLayer(currentMediaTime);
19405
20932
  renderOnChange();
19406
20933
  });
19407
20934
  measureContainer();
@@ -19617,6 +21144,7 @@ async function createPixiMediaScene(options) {
19617
21144
  interactionSlot.setDisplay(interactionPresentationDisplay || interactionHitDisplay
19618
21145
  ? interactionDisplay
19619
21146
  : undefined);
21147
+ interactionLabelSlot.setDisplay(interactionPresentationLayer?.getLabelDisplay() ?? undefined);
19620
21148
  attachFocusLayerDisplay();
19621
21149
  attachMaskLayerDisplay();
19622
21150
  attachLabelLayerDisplay();
@@ -19633,6 +21161,7 @@ async function createPixiMediaScene(options) {
19633
21161
  setPlaybackActive(active) {
19634
21162
  isPlaybackActive = active;
19635
21163
  maskLayer?.setPlaybackActive(active);
21164
+ polygonLayer?.setPlaybackActive(active);
19636
21165
  depthSource?.setPlaybackActive?.(active);
19637
21166
  },
19638
21167
  setScrubbing(scrubbing) {
@@ -19733,6 +21262,7 @@ async function createPixiMediaScene(options) {
19733
21262
  syncPresentationBox();
19734
21263
  updateMediaSceneFit();
19735
21264
  syncDisplaySizing();
21265
+ drawInteractionPresentationLayer(currentMediaTime);
19736
21266
  renderOnChange();
19737
21267
  },
19738
21268
  setDisplayAdjustments(adjustments) {
@@ -19862,6 +21392,12 @@ async function createPixiMediaScene(options) {
19862
21392
  setPresentation(presentation, mediaTime) {
19863
21393
  appliedPresentation = presentation;
19864
21394
  currentMediaTime = mediaTime;
21395
+ if (annotationAntialiasing !==
21396
+ (presentation.annotationAntialiasing ?? false)) {
21397
+ annotationAntialiasing = presentation.annotationAntialiasing ?? false;
21398
+ syncAnnotationAntialiasResolution();
21399
+ syncSceneChildren();
21400
+ }
19865
21401
  if (presentation.backgroundColor !== undefined) {
19866
21402
  app.renderer.background.color = presentation.backgroundColor;
19867
21403
  applyBackdropColor(backdrop, presentation.backgroundColor);
@@ -20064,6 +21600,10 @@ async function createPixiMediaScene(options) {
20064
21600
  // a decoder buffer in a producer that outlives the scene.
20065
21601
  frameChannel?.onPresentedFrame((presented) => presented.frame.close());
20066
21602
  destroyBatchTextureBindings();
21603
+ destroyFilterBindings();
21604
+ labelLayer?.setBackgroundAntialiasFilter(null);
21605
+ interactionPresentationLayer?.setBackgroundAntialiasFilter(null);
21606
+ annotationAntialiasLayer.destroy();
20067
21607
  inspectionFilter?.destroy();
20068
21608
  mediaCompositor?.destroy();
20069
21609
  interactionLayer?.destroy();
@@ -20172,8 +21712,8 @@ async function createPixiMediaScene(options) {
20172
21712
  renderPreparation: options.renderPreparation,
20173
21713
  resolveInstructions: resolveArtifactMaskInstructions,
20174
21714
  });
20175
- if (rasterDisplay)
20176
- maskLayer.setRasterDisplay(rasterDisplay);
21715
+ syncMaskRasterDisplay();
21716
+ maskLayer.setViewportScale(viewportScale);
20177
21717
  maskLayer.setPlaybackActive(isPlaybackActive);
20178
21718
  if (timelineContext) {
20179
21719
  maskLayer.setTimelineContext(timelineContext);
@@ -20427,6 +21967,9 @@ async function createPixiMediaScene(options) {
20427
21967
  renderPreparation: options.renderPreparation,
20428
21968
  resolveContextState,
20429
21969
  });
21970
+ polygonLayer.setPlaybackActive(isPlaybackActive);
21971
+ syncMaskRasterDisplay();
21972
+ polygonLayer.setViewportScale(viewportScale);
20430
21973
  if (timelineContext) {
20431
21974
  polygonLayer.setTimelineContext(timelineContext);
20432
21975
  }
@@ -20467,14 +22010,19 @@ async function createPixiMediaScene(options) {
20467
22010
  function ensureFocusLayer(focusStyle) {
20468
22011
  if (!focusLayer) {
20469
22012
  focusLayer = createPixiFocusLayer({
22013
+ AlphaMask,
20470
22014
  Container,
20471
22015
  Graphics,
20472
22016
  ImageSource,
20473
22017
  Mesh,
20474
22018
  MeshGeometry,
22019
+ Rectangle,
20475
22020
  Shader,
20476
22021
  UniformGroup,
20477
22022
  focusStyle,
22023
+ getHeatmapRenderers: () => currentHeatmapRenderers,
22024
+ getAnnotationAntialiasing: () => Boolean(annotationAntialiasing),
22025
+ getViewportBounds: resolveViewportBounds,
20478
22026
  isDetectionVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
20479
22027
  });
20480
22028
  }
@@ -20515,6 +22063,7 @@ async function createPixiMediaScene(options) {
20515
22063
  const interactionState = interactionLayer?.getState();
20516
22064
  focusLayer.drawFrame({
20517
22065
  frame,
22066
+ heatmapArtifacts: heatmapLayer?.getActiveFocusArtifacts(frame) ?? [],
20518
22067
  hoveredPick: withEditingPreview(filterVisiblePick(interactionState?.hoveredPick ?? null)),
20519
22068
  idMaskArtifact: maskLayer?.getActiveIdMaskFrameTexture(frame?.mediaTime ?? null) ??
20520
22069
  null,
@@ -20547,6 +22096,9 @@ async function createPixiMediaScene(options) {
20547
22096
  mediaTime,
20548
22097
  selectedPick: interactionState?.selectedPick ?? null,
20549
22098
  selectedPicks: interactionState?.selectedPicks,
22099
+ strokePixelRatio: annotationAntialiasing
22100
+ ? annotationAntialiasResolution
22101
+ : presentationResolution,
20550
22102
  viewportScale,
20551
22103
  });
20552
22104
  }
@@ -20625,9 +22177,6 @@ async function createPixiMediaScene(options) {
20625
22177
  const frame = annotationDetectionTimeline.selectFrame(mediaTime);
20626
22178
  const interactionState = interactionLayer?.getState();
20627
22179
  const editingState = options.editingEngine?.getState();
20628
- labelLayer?.drawCreationPreview(editingState?.kind === AnnotationGestureStateKind.Creating
20629
- ? editingState.preview
20630
- : null, mediaTime, viewportScale);
20631
22180
  const previewOverlay = options.previewOverlay?.() ?? null;
20632
22181
  drawnEditingState = editingState;
20633
22182
  // Only a scene that renders on change compares signatures.
@@ -20647,6 +22196,18 @@ async function createPixiMediaScene(options) {
20647
22196
  viewportScale,
20648
22197
  visibility: currentVisibility,
20649
22198
  });
22199
+ const editingBox = annotationOverlayLayer.getRenderedEditingBox();
22200
+ const creationPreview = editingState?.kind === AnnotationGestureStateKind.Creating
22201
+ ? editingState.preview
22202
+ : null;
22203
+ labelLayer?.drawCreationPreview(creationPreview, mediaTime, viewportScale, editingBox && editingBox.detection === creationPreview
22204
+ ? editingBox.rect
22205
+ : undefined);
22206
+ if (editingState?.activeDetectionId != null) {
22207
+ labelLayer?.updateEditingBox(editingState.activeDetectionId, editingBox?.detection.id === editingState.activeDetectionId
22208
+ ? editingBox.rect
22209
+ : undefined);
22210
+ }
20650
22211
  }
20651
22212
  /**
20652
22213
  * Milliseconds for animations that are not annotation geometry. A pull scene
@@ -20914,6 +22475,7 @@ async function createPixiMediaScene(options) {
20914
22475
  Text,
20915
22476
  detectionTimeline: annotationDetectionTimeline,
20916
22477
  labelStyle,
22478
+ getRenderedBoxes: () => boxLayer.getRenderedBoxes(),
20917
22479
  resolveContextState: resolveLabelContextState,
20918
22480
  });
20919
22481
  }
@@ -22130,6 +23692,7 @@ async function createMediaSession(options) {
22130
23692
  ...options.renderer,
22131
23693
  ...sessionMedia.rendererSourceOption,
22132
23694
  backgroundColor: initialPresentation.backgroundColor,
23695
+ annotationAntialiasing: initialPresentation.annotationAntialiasing,
22133
23696
  annotationOverlayStyle: initialPresentation.annotationOverlayStyle,
22134
23697
  boxStyle: initialPresentation.boxStyle,
22135
23698
  boxCornerStyle: initialPresentation.boxCornerStyle,