supervision 0.2.6 → 0.2.7

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 (57) hide show
  1. package/dist/editing.d.ts +1 -1
  2. package/dist/editing.d.ts.map +1 -1
  3. package/dist/index.js +325 -24
  4. package/dist/index.js.map +1 -1
  5. package/dist/mask-preparation.worker.js +130 -4
  6. package/dist/mask-preparation.worker.js.map +1 -1
  7. package/dist/render-preparation/mask-frame-artifact.d.ts +10 -0
  8. package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
  9. package/dist/render-preparation/mask-frame-compositor.d.ts +2 -1
  10. package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
  11. package/dist/render-preparation/mask-frame-preparer.d.ts.map +1 -1
  12. package/dist/render-preparation/mask-outline-paths.d.ts +3 -0
  13. package/dist/render-preparation/mask-outline-paths.d.ts.map +1 -0
  14. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +2 -1
  15. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
  16. package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
  17. package/dist/renderers/detection-render-order.d.ts +4 -0
  18. package/dist/renderers/detection-render-order.d.ts.map +1 -0
  19. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  20. package/dist/renderers/media-renderer-scene.d.ts +2 -1
  21. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  22. package/dist/renderers/pixi-box-layer.d.ts +1 -0
  23. package/dist/renderers/pixi-box-layer.d.ts.map +1 -1
  24. package/dist/renderers/pixi-interaction-layer.d.ts.map +1 -1
  25. package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
  26. package/dist/renderers/pixi-mask-brush-preview.d.ts.map +1 -1
  27. package/dist/renderers/pixi-mask-layer.d.ts +4 -2
  28. package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
  29. package/dist/renderers/pixi-mask-screen-stroke.d.ts +17 -0
  30. package/dist/renderers/pixi-mask-screen-stroke.d.ts.map +1 -0
  31. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  32. package/dist/renderers/pixi-vector-layer.d.ts +1 -0
  33. package/dist/renderers/pixi-vector-layer.d.ts.map +1 -1
  34. package/dist/tracking.worker.js.map +1 -1
  35. package/dist/types/media-renderer.d.ts +14 -0
  36. package/dist/types/media-renderer.d.ts.map +1 -1
  37. package/dist/types/media-session.d.ts +2 -0
  38. package/dist/types/media-session.d.ts.map +1 -1
  39. package/node_modules/supervision-js-core/dist/index.d.ts +1 -1
  40. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  41. package/node_modules/supervision-js-core/dist/index.js +44 -11
  42. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  43. package/node_modules/supervision-js-core/dist/interactions/annotation-editing-engine.d.ts +2 -2
  44. package/node_modules/supervision-js-core/dist/interactions/annotation-editing-engine.d.ts.map +1 -1
  45. package/node_modules/supervision-js-core/dist/interactions/detection-picker.d.ts.map +1 -1
  46. package/node_modules/supervision-js-core/dist/interactions/viewport-controller.d.ts.map +1 -1
  47. package/node_modules/supervision-js-core/dist/types/detections.d.ts +2 -1
  48. package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -1
  49. package/node_modules/supervision-js-core/dist/types/editing.d.ts +7 -0
  50. package/node_modules/supervision-js-core/dist/types/editing.d.ts.map +1 -1
  51. package/node_modules/supervision-js-core/dist/types/interaction.d.ts +4 -0
  52. package/node_modules/supervision-js-core/dist/types/interaction.d.ts.map +1 -1
  53. package/node_modules/supervision-js-core/dist/types/mask-style.d.ts +3 -0
  54. package/node_modules/supervision-js-core/dist/types/mask-style.d.ts.map +1 -1
  55. package/node_modules/supervision-js-core/dist/types/viewport.d.ts +3 -0
  56. package/node_modules/supervision-js-core/dist/types/viewport.d.ts.map +1 -1
  57. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, decodeCompressedRleCounts, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, createDetectionPickKey, rebaseDetectionPickToFrame, pickDetectionAtPoint, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, MAX_ID_MASK_STROKE_WIDTH, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, resolveIdMaskStrokeTexels, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
1
+ import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, decodeCompressedRleCounts, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, createDetectionPickKey, rebaseDetectionPickToFrame, pickDetectionAtPoint, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, MAX_ID_MASK_STROKE_WIDTH, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, resolveIdMaskStrokeTexels, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
2
2
  export { BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BaseOrientedBoxStyle, BasePercentageBarStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DepthColormap, DepthMapKind, DepthPreviewLevels, DepthQuantity, DepthSampling, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, PercentageBarPlacement, PlaybackGateReach, RegionRendererComposeMode, RegionRendererCoverageKind, RegionRendererMediaEffectKind, RegionRendererRegionKind, RegionRendererSizeSpace, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, TrackingGeometry, annotationRendererKinds, annotationRenderers, computeDepthPercentileRange, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createWritableDetectionFrameSource, depthColormapColors, detectionPostProcessors, normalizeDetectionClassName, parseDepthManifest, pickDetectionAtPoint, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, readDepthAt, resolveDetectionClassColorStyle } from 'supervision-js-core';
3
3
  import { c as createMediaFrameClock, M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-DOFsuGS4.js';
4
4
  export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-DOFsuGS4.js';
@@ -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 /** 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), {\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 && bounds) {\n compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);\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 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 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 ...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 if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageBitmap, ...idMaskTransfers, ...coverageTransfers]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer, ...idMaskTransfers, ...coverageTransfers]);\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 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})();";
3117
3117
 
3118
3118
  const DEFAULT_WORKER_NAME = "supervision-js-render-preparation";
3119
3119
  let defaultWorkerUrl;
@@ -5138,6 +5138,74 @@ function createExactFrameSource(options) {
5138
5138
  };
5139
5139
  }
5140
5140
 
5141
+ /** Trace closed pixel boundaries, retaining holes and disconnected components. */
5142
+ function traceMaskOutlinePaths(raster, width, height, ids) {
5143
+ const edgesById = new Map();
5144
+ const stride = width + 1;
5145
+ const steps = [1, stride, -1, -stride];
5146
+ const add = (id, vertex, direction) => {
5147
+ let edges = edgesById.get(id);
5148
+ if (!edges)
5149
+ edgesById.set(id, (edges = new Map()));
5150
+ const outgoing = edges.get(vertex);
5151
+ if (outgoing)
5152
+ outgoing.push(direction);
5153
+ else
5154
+ edges.set(vertex, [direction]);
5155
+ };
5156
+ for (let y = 0; y < height; y += 1) {
5157
+ for (let x = 0; x < width; x += 1) {
5158
+ const offset = y * width + x;
5159
+ const id = raster[offset];
5160
+ if (!ids.has(id))
5161
+ continue;
5162
+ const vertex = y * stride + x;
5163
+ if (y === 0 || raster[offset - width] !== id)
5164
+ add(id, vertex, 0);
5165
+ if (x === width - 1 || raster[offset + 1] !== id)
5166
+ add(id, vertex + 1, 1);
5167
+ if (y === height - 1 || raster[offset + width] !== id)
5168
+ add(id, vertex + stride + 1, 2);
5169
+ if (x === 0 || raster[offset - 1] !== id)
5170
+ add(id, vertex + stride, 3);
5171
+ }
5172
+ }
5173
+ const result = new Map();
5174
+ for (const [id, edges] of edgesById) {
5175
+ const paths = [];
5176
+ while (edges.size > 0) {
5177
+ const start = edges.keys().next().value;
5178
+ let vertex = start;
5179
+ let previousDirection = -1;
5180
+ let firstDirection = -1;
5181
+ const points = [];
5182
+ do {
5183
+ const outgoing = edges.get(vertex);
5184
+ // Turn right at diagonal contacts so separate islands stay separate.
5185
+ const direction = previousDirection < 0
5186
+ ? outgoing[0]
5187
+ : [1, 0, 3, 2]
5188
+ .map((turn) => (previousDirection + turn) % 4)
5189
+ .find((candidate) => outgoing.includes(candidate));
5190
+ if (firstDirection < 0)
5191
+ firstDirection = direction;
5192
+ if (direction !== previousDirection)
5193
+ points.push(vertex % stride, Math.floor(vertex / stride));
5194
+ outgoing.splice(outgoing.indexOf(direction), 1);
5195
+ if (outgoing.length === 0)
5196
+ edges.delete(vertex);
5197
+ vertex += steps[direction];
5198
+ previousDirection = direction;
5199
+ } while (vertex !== start);
5200
+ if (previousDirection === firstDirection)
5201
+ points.splice(0, 2);
5202
+ paths.push(new Float32Array(points));
5203
+ }
5204
+ result.set(id, paths);
5205
+ }
5206
+ return result;
5207
+ }
5208
+
5141
5209
  /** Builds compact, independent alpha crops so overlapping masks stay exact. */
5142
5210
  function createRegionMaskCoverageFrame(instructions) {
5143
5211
  const entries = [];
@@ -5199,7 +5267,9 @@ function compositeMaskFrame(instructions) {
5199
5267
  }
5200
5268
  function createIdMaskRasterFrame(instructions, maxRasterWidth) {
5201
5269
  try {
5202
- return createIdMaskFrame(materializeMaskInstructions(instructions), {
5270
+ return createIdMaskFrame(materializeMaskInstructions(instructions).map((instruction) => instruction.stroke?.widthUnit === "screen"
5271
+ ? { ...instruction, stroke: undefined }
5272
+ : instruction), {
5203
5273
  maxWidth: maxRasterWidth,
5204
5274
  });
5205
5275
  }
@@ -5234,10 +5304,47 @@ function createIdMaskPlane(instructions, maxRasterWidth) {
5234
5304
  function compositeInstruction(rgba, canvasWidth, instruction) {
5235
5305
  const fill = resolveRgbaColor(instruction.color, instruction.alpha);
5236
5306
  const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);
5237
- if (instruction.stroke && bounds) {
5307
+ if (instruction.stroke &&
5308
+ instruction.stroke.widthUnit !== "screen" &&
5309
+ bounds) {
5238
5310
  compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);
5239
5311
  }
5240
5312
  }
5313
+ function createMaskScreenStrokes(instructions, width, height, preparedRaster) {
5314
+ const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&
5315
+ stroke?.widthUnit === "screen" &&
5316
+ stroke.width > 0 &&
5317
+ stroke.alpha > 0);
5318
+ if (outlined.length === 0)
5319
+ return undefined;
5320
+ const masks = materializeMaskInstructions(instructions);
5321
+ const raster = preparedRaster ?? new Uint32Array(width * height);
5322
+ if (!preparedRaster) {
5323
+ for (const { detectionIndex, mask } of masks) {
5324
+ const counts = decodeCompressedRleCounts(mask.counts);
5325
+ let offset = 0;
5326
+ for (let index = 0; index < counts.length; index += 1) {
5327
+ const length = counts[index];
5328
+ if (index % 2 === 1) {
5329
+ for (let pixel = offset; pixel < offset + length; pixel += 1) {
5330
+ const x = Math.floor(pixel / mask.height);
5331
+ const y = pixel % mask.height;
5332
+ raster[y * width + x] = detectionIndex + 1;
5333
+ }
5334
+ }
5335
+ offset += length;
5336
+ }
5337
+ }
5338
+ }
5339
+ const paths = traceMaskOutlinePaths(raster, width, height, new Set(outlined.map(({ detectionIndex }) => detectionIndex + 1)));
5340
+ return outlined.map(({ detectionIndex, stroke }) => ({
5341
+ detectionIndex,
5342
+ alpha: stroke.alpha,
5343
+ color: stroke.color,
5344
+ width: stroke.width,
5345
+ paths: paths.get(detectionIndex + 1) ?? [],
5346
+ }));
5347
+ }
5241
5348
  function materializeMaskInstructions(instructions) {
5242
5349
  return instructions
5243
5350
  .filter((instruction) => instruction.visible !== false)
@@ -5531,6 +5638,7 @@ function createMainThreadMaskFramePreparer(workerStatus, message = null) {
5531
5638
  key: job.key,
5532
5639
  kind: PreparedMaskFrameKind.RgbaImage,
5533
5640
  regionMaskCoverage,
5641
+ screenStrokes: createMaskScreenStrokes(job.instructions, preparedPixels.width, preparedPixels.height),
5534
5642
  source: canvas,
5535
5643
  width: preparedPixels.width,
5536
5644
  };
@@ -5566,6 +5674,7 @@ function createPreparedIdMaskFrame(job, regionMaskCoverage) {
5566
5674
  kind: PreparedMaskFrameKind.IdMask,
5567
5675
  maxStrokeWidth: frame.maxStrokeWidth,
5568
5676
  raster: frame.data,
5677
+ screenStrokes: createMaskScreenStrokes(job.instructions, frame.width, frame.height, frame.data),
5569
5678
  regionMaskCoverage,
5570
5679
  sourceWidth: frame.sourceWidth,
5571
5680
  strokePalette: frame.strokePalette,
@@ -5691,6 +5800,7 @@ function createPreparedFrameFromWorkerResponse(message) {
5691
5800
  maxStrokeWidth: message.maxStrokeWidth ?? 0,
5692
5801
  raster: message.raster,
5693
5802
  regionMaskCoverage: message.regionMaskCoverage,
5803
+ screenStrokes: message.screenStrokes,
5694
5804
  sourceWidth: message.sourceWidth,
5695
5805
  strokePalette: message.strokePalette,
5696
5806
  strokeWidths: message.strokeWidths,
@@ -5707,6 +5817,7 @@ function createPreparedFrameFromWorkerResponse(message) {
5707
5817
  key: message.key,
5708
5818
  kind: PreparedMaskFrameKind.RgbaImage,
5709
5819
  regionMaskCoverage: message.regionMaskCoverage,
5820
+ screenStrokes: message.screenStrokes,
5710
5821
  source: message.imageBitmap,
5711
5822
  width: message.imageBitmap.width,
5712
5823
  };
@@ -5732,6 +5843,7 @@ function createPreparedFrameFromWorkerResponse(message) {
5732
5843
  key: message.key,
5733
5844
  kind: PreparedMaskFrameKind.RgbaImage,
5734
5845
  regionMaskCoverage: message.regionMaskCoverage,
5846
+ screenStrokes: message.screenStrokes,
5735
5847
  source: canvas,
5736
5848
  width: message.imageData.width,
5737
5849
  };
@@ -5907,7 +6019,8 @@ function createPreparedRenderWindow(options) {
5907
6019
  const backingBytes = options.resolveMaskFrameBackingBytes
5908
6020
  ? options.resolveMaskFrameBackingBytes(maskFrame)
5909
6021
  : pixels * (maskFrame.kind === PreparedMaskFrameKind.IdMask ? 1 : 4);
5910
- const bytes = Math.max(1, payloadBytes + coverageBytes + backingBytes);
6022
+ const strokeBytes = maskFrame.screenStrokes?.reduce((total, stroke) => total + stroke.paths.reduce((sum, path) => sum + path.byteLength, 0), 0) ?? 0;
6023
+ const bytes = Math.max(1, payloadBytes + coverageBytes + strokeBytes + backingBytes);
5911
6024
  preparedMaskBytesByKey.set(key, bytes);
5912
6025
  preparedMaskBytes += bytes;
5913
6026
  largestMaskFrameBytesByTier.set(tier, Math.max(largestMaskFrameBytesByTier.get(tier) ?? 0, bytes));
@@ -9083,6 +9196,11 @@ function resolveRenderPreparationMaxWaitSeconds(value) {
9083
9196
  */
9084
9197
  const RENDER_PREPARATION_GATE_MAX_STOP_SHARE_OF_RESUME = 0.5;
9085
9198
  async function createMediaRendererCore(options, providers) {
9199
+ const pixelInspectionScale = options.pixelInspection?.minScale;
9200
+ if (pixelInspectionScale !== undefined &&
9201
+ (!Number.isFinite(pixelInspectionScale) || pixelInspectionScale <= 0)) {
9202
+ throw new RangeError("pixelInspection.minScale must be a finite number greater than zero.");
9203
+ }
9086
9204
  const fit = options.fit ?? MediaRendererFit.Contain;
9087
9205
  const initialPlaybackRate = options.playbackRate ?? 1;
9088
9206
  if (!Number.isFinite(initialPlaybackRate) || initialPlaybackRate <= 0) {
@@ -9993,6 +10111,7 @@ async function createMediaRendererCore(options, providers) {
9993
10111
  markerStyle: currentPresentation.markerStyle,
9994
10112
  percentageBarStyle: currentPresentation.percentageBarStyle,
9995
10113
  maxDevicePixelRatio: options.maxDevicePixelRatio,
10114
+ pixelInspection: options.pixelInspection,
9996
10115
  orientedBoxStyle: currentPresentation.orientedBoxStyle,
9997
10116
  onPresentationUpdate(presentedSample) {
9998
10117
  if (!runtimeState.isDestroyed()) {
@@ -11031,6 +11150,23 @@ function appendDashedPath(graphics, points, closed, dashPattern, viewportScale)
11031
11150
  return true;
11032
11151
  }
11033
11152
 
11153
+ const cachedOrders = new WeakMap();
11154
+ /** Dense ranks preserve zIndex and frame-order ties across retained shape layers. */
11155
+ function detectionRenderOrder(frame) {
11156
+ const cached = cachedOrders.get(frame);
11157
+ if (cached)
11158
+ return cached;
11159
+ const indexes = frame.detections.map((_, index) => index);
11160
+ indexes.sort((left, right) => (frame.detections[left].zIndex ?? left) -
11161
+ (frame.detections[right].zIndex ?? right) || left - right);
11162
+ const order = [];
11163
+ indexes.forEach((index, rank) => {
11164
+ order[index] = rank;
11165
+ });
11166
+ cachedOrders.set(frame, order);
11167
+ return order;
11168
+ }
11169
+
11034
11170
  function createPixiBoxLayer(options) {
11035
11171
  let boxStyle = options.boxStyle === undefined ? new BaseBoxStyle() : options.boxStyle;
11036
11172
  let boxGraphics;
@@ -11042,7 +11178,7 @@ function createPixiBoxLayer(options) {
11042
11178
  let hasDrawnDetectionFrame = false;
11043
11179
  let isDirty = true;
11044
11180
  let lastViewportScale = 0;
11045
- let retainedContainer;
11181
+ let retainedContainer = options.container;
11046
11182
  const retainedEntries = new Map();
11047
11183
  const invalidatedDetections = new Set();
11048
11184
  const usingRetainedEntries = Boolean(options.Container && options.Graphics);
@@ -11098,7 +11234,9 @@ function createPixiBoxLayer(options) {
11098
11234
  activeKeys.add(key);
11099
11235
  const display = ensureRetainedEntry(key);
11100
11236
  display.visible = true;
11101
- display.zIndex = detection.zIndex ?? detectionIndex;
11237
+ display.zIndex = options.container
11238
+ ? detectionRenderOrder(detectionFrame)[detectionIndex] * 2
11239
+ : (detection.zIndex ?? detectionIndex);
11102
11240
  display.position?.set?.(0, 0);
11103
11241
  display.clear();
11104
11242
  drawBoxInstruction(display, instruction, viewportScale);
@@ -13696,7 +13834,9 @@ function createPixiInteractionLayer(options) {
13696
13834
  padding: pickPadding,
13697
13835
  viewportScale: options.getViewportScale?.() ?? 1,
13698
13836
  });
13699
- if (geometryPick && POINT_PRECISE_PICK_TARGETS.has(geometryPick.target)) {
13837
+ if (geometryPick &&
13838
+ (options.interaction.preferTopmost ||
13839
+ POINT_PRECISE_PICK_TARGETS.has(geometryPick.target))) {
13700
13840
  return geometryPick;
13701
13841
  }
13702
13842
  const maskPick = options.pickMaskDetectionAtPoint?.(pickPoint, currentMediaTime);
@@ -13931,6 +14071,75 @@ function intersects(rect, marquee) {
13931
14071
  top + rect.height >= marquee.y);
13932
14072
  }
13933
14073
 
14074
+ function createPixiMaskScreenStrokeRenderer(options) {
14075
+ const display = new options.Graphics();
14076
+ display.visible = false;
14077
+ let lastFrame;
14078
+ let lastSignature = "";
14079
+ const interactionPaths = new WeakMap();
14080
+ function hide() {
14081
+ display.visible = false;
14082
+ if (lastFrame)
14083
+ display.clear();
14084
+ lastFrame = undefined;
14085
+ lastSignature = "";
14086
+ }
14087
+ return {
14088
+ display,
14089
+ hide,
14090
+ destroy() {
14091
+ display.destroy();
14092
+ },
14093
+ render(frame, viewportScale, overrides) {
14094
+ let strokes = frame.screenStrokes ?? [];
14095
+ if (overrides) {
14096
+ strokes = overrides.flatMap(({ detectionIndex, stroke }) => {
14097
+ if (stroke?.widthUnit !== "screen" ||
14098
+ stroke.width <= 0 ||
14099
+ stroke.alpha <= 0)
14100
+ return [];
14101
+ let paths = frame.screenStrokes?.find((entry) => entry.detectionIndex === detectionIndex)?.paths;
14102
+ if (!paths && frame.kind === PreparedMaskFrameKind.IdMask) {
14103
+ let cached = interactionPaths.get(frame);
14104
+ if (!cached) {
14105
+ const ids = new Set(frame.raster);
14106
+ ids.delete(0);
14107
+ cached = traceMaskOutlinePaths(frame.raster, frame.width, frame.height, ids);
14108
+ interactionPaths.set(frame, cached);
14109
+ }
14110
+ paths = cached.get(detectionIndex + 1);
14111
+ }
14112
+ return paths ? [{ ...stroke, detectionIndex, paths }] : [];
14113
+ });
14114
+ }
14115
+ if (strokes.length === 0) {
14116
+ hide();
14117
+ return;
14118
+ }
14119
+ const signature = `${viewportScale}:${strokes.map(({ detectionIndex, width, color, alpha }) => `${detectionIndex}:${width}:${color}:${alpha}`).join(";")}`;
14120
+ if (lastFrame !== frame || lastSignature !== signature) {
14121
+ display.clear();
14122
+ const scaleX = options.mediaWidth / frame.width;
14123
+ const scaleY = options.mediaHeight / frame.height;
14124
+ for (const stroke of strokes) {
14125
+ for (const path of stroke.paths) {
14126
+ if (path.length < 6)
14127
+ continue;
14128
+ display.moveTo(path[0] * scaleX, path[1] * scaleY);
14129
+ for (let index = 2; index < path.length; index += 2)
14130
+ display.lineTo(path[index] * scaleX, path[index + 1] * scaleY);
14131
+ display.closePath();
14132
+ }
14133
+ display.stroke(resolvePixiStroke({ ...stroke, join: "round" }, viewportScale));
14134
+ }
14135
+ lastFrame = frame;
14136
+ lastSignature = signature;
14137
+ }
14138
+ display.visible = true;
14139
+ },
14140
+ };
14141
+ }
14142
+
13934
14143
  const coverageBody = `
13935
14144
  return clamp(0.5 + (width - distance) / pixelWidth, 0.0, 1.0)
13936
14145
  - clamp(0.5 - (width + distance) / pixelWidth, 0.0, 1.0);
@@ -14504,7 +14713,7 @@ function detectionKey$1(detection, detectionIndex) {
14504
14713
  }
14505
14714
 
14506
14715
  function createPixiVectorLayer(options) {
14507
- let container;
14716
+ let container = options.container;
14508
14717
  let polygonStyle = options.polygonStyle === undefined
14509
14718
  ? new BasePolygonStyle()
14510
14719
  : options.polygonStyle;
@@ -14530,8 +14739,8 @@ function createPixiVectorLayer(options) {
14530
14739
  createContainer() {
14531
14740
  if (!container) {
14532
14741
  container = new options.Container();
14533
- container.sortableChildren = true;
14534
14742
  }
14743
+ container.sortableChildren = true;
14535
14744
  return container;
14536
14745
  },
14537
14746
  drawFrame(mediaTime, viewportScale = 1) {
@@ -14645,7 +14854,10 @@ function createPixiVectorLayer(options) {
14645
14854
  polygon,
14646
14855
  polyline,
14647
14856
  shapes,
14648
- zIndex: detection.zIndex ?? detectionIndex,
14857
+ // Vector details stay above their own box even if the box is retained later.
14858
+ zIndex: options.container
14859
+ ? detectionRenderOrder(frame)[detectionIndex] * 2 + 1
14860
+ : (detection.zIndex ?? detectionIndex),
14649
14861
  };
14650
14862
  }
14651
14863
  function drawDetection(entry, detection, viewportScale) {
@@ -14787,6 +14999,7 @@ function createPixiInteractionPresentationLayer(options) {
14787
14999
  let interactionStyle = options.interactionStyle === undefined
14788
15000
  ? new BaseInteractionStyle()
14789
15001
  : options.interactionStyle;
15002
+ let displayContainer;
14790
15003
  let mediaHeight = 0;
14791
15004
  let mediaWidth = 0;
14792
15005
  let syntheticFrame;
@@ -14794,6 +15007,7 @@ function createPixiInteractionPresentationLayer(options) {
14794
15007
  let currentMediaTime = 0;
14795
15008
  let viewportScale = 1;
14796
15009
  let maskRenderer;
15010
+ let screenStrokeRenderer;
14797
15011
  let isDestroyed = false;
14798
15012
  const syntheticTimeline = createSyntheticTimeline(() => syntheticFrame);
14799
15013
  const boxStyle = createInteractionBoxStyle(() => activePicks);
@@ -14834,6 +15048,7 @@ function createPixiInteractionPresentationLayer(options) {
14834
15048
  container.addChild(maskRenderer.mesh);
14835
15049
  }
14836
15050
  container.addChild(boxGraphics, vectors, labels);
15051
+ displayContainer = container;
14837
15052
  return container;
14838
15053
  },
14839
15054
  destroy() {
@@ -14844,6 +15059,7 @@ function createPixiInteractionPresentationLayer(options) {
14844
15059
  syntheticFrame = undefined;
14845
15060
  activePicks = [];
14846
15061
  maskRenderer?.destroy();
15062
+ screenStrokeRenderer?.destroy();
14847
15063
  labelLayer.destroy();
14848
15064
  vectorLayer.destroy();
14849
15065
  },
@@ -14952,6 +15168,7 @@ function createPixiInteractionPresentationLayer(options) {
14952
15168
  syntheticFrame = undefined;
14953
15169
  activePicks = [];
14954
15170
  maskRenderer?.hide();
15171
+ screenStrokeRenderer?.hide();
14955
15172
  boxLayer.setBoxStyle(null);
14956
15173
  boxLayer.drawFrame(currentMediaTime, viewportScale);
14957
15174
  labelLayer.setLabelStyle(null);
@@ -14966,6 +15183,7 @@ function createPixiInteractionPresentationLayer(options) {
14966
15183
  function drawMasks(artifact) {
14967
15184
  if (!maskRenderer || !artifact) {
14968
15185
  maskRenderer?.hide();
15186
+ screenStrokeRenderer?.hide();
14969
15187
  return;
14970
15188
  }
14971
15189
  const instructions = activePicks
@@ -14993,9 +15211,23 @@ function createPixiInteractionPresentationLayer(options) {
14993
15211
  .filter((instruction) => Boolean(instruction));
14994
15212
  if (instructions.length === 0) {
14995
15213
  maskRenderer.hide();
15214
+ screenStrokeRenderer?.hide();
14996
15215
  return;
14997
15216
  }
14998
15217
  maskRenderer.render(artifact.frame, artifact.texture, instructions);
15218
+ if (!screenStrokeRenderer &&
15219
+ instructions.some(({ instruction }) => instruction.stroke?.widthUnit === "screen")) {
15220
+ screenStrokeRenderer = createPixiMaskScreenStrokeRenderer({
15221
+ Graphics: options.Graphics,
15222
+ mediaWidth,
15223
+ mediaHeight,
15224
+ });
15225
+ displayContainer?.addChildAt(screenStrokeRenderer.display, maskRenderer ? 1 : 0);
15226
+ }
15227
+ screenStrokeRenderer?.render(artifact.frame, viewportScale, instructions.map(({ detectionIndex, instruction }) => ({
15228
+ detectionIndex,
15229
+ stroke: instruction.stroke,
15230
+ })));
14999
15231
  }
15000
15232
  function createMaskRenderer() {
15001
15233
  if (!options.ImageSource ||
@@ -15237,7 +15469,9 @@ function createInteractionMaskRenderer(options) {
15237
15469
  continue;
15238
15470
  }
15239
15471
  writeIdMaskPaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
15240
- if (instruction.stroke && instruction.stroke.width > 0) {
15472
+ if (instruction.stroke &&
15473
+ instruction.stroke.widthUnit !== "screen" &&
15474
+ instruction.stroke.width > 0) {
15241
15475
  writeIdMaskPaletteEntry(strokePalette, maskId, instruction.stroke.color, instruction.stroke.alpha);
15242
15476
  strokeWidths[maskId] = resolveIdMaskStrokeTexels(instruction.stroke.width, frame.sourceWidth, frame.width);
15243
15477
  maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidths[maskId] ?? 0);
@@ -15747,6 +15981,8 @@ function createPixiMaskLayer(options) {
15747
15981
  let haloRenderer;
15748
15982
  let currentMaskHaloStyle = options.maskHaloStyle ?? null;
15749
15983
  let maskSprite;
15984
+ let maskContainer;
15985
+ let screenStrokeRenderer;
15750
15986
  let visibleMaskFrameKey = null;
15751
15987
  let activeIdMaskFrame = null;
15752
15988
  let activeRgbaMaskFrame = null;
@@ -15802,7 +16038,7 @@ function createPixiMaskLayer(options) {
15802
16038
  if (!idMaskRenderer || !options.Container) {
15803
16039
  return maskSprite;
15804
16040
  }
15805
- const maskContainer = new options.Container();
16041
+ maskContainer = new options.Container();
15806
16042
  // The halo draws beneath the mask so the glow bleeds outward from the
15807
16043
  // silhouette while fills and borders stay crisp on top.
15808
16044
  if (haloRenderer) {
@@ -15811,14 +16047,21 @@ function createPixiMaskLayer(options) {
15811
16047
  maskContainer.addChild(maskSprite, idMaskRenderer.mesh);
15812
16048
  return maskContainer;
15813
16049
  },
15814
- drawFrame(mediaTime, presentedFrameId = null) {
16050
+ drawFrame(mediaTime, presentedFrameId = null, viewportScale = 1) {
15815
16051
  const preparedFrame = preparedRenderWindow.getFrame(mediaTime);
15816
16052
  if (preparedFrame?.maskFrame) {
15817
- showMaskFrame(preparedFrame.maskFrame, preparedFrame.detectionFrame.mediaTime, presentedFrameId);
16053
+ showMaskFrame(preparedFrame.maskFrame, preparedFrame.detectionFrame.mediaTime, presentedFrameId, viewportScale);
15818
16054
  return;
15819
16055
  }
15820
16056
  hideSprite();
15821
16057
  },
16058
+ redrawViewportStyles(viewportScale) {
16059
+ if (!isFillVisible)
16060
+ return;
16061
+ const frame = activeIdMaskFrame ?? activeRgbaMaskFrame;
16062
+ if (frame)
16063
+ screenStrokeRenderer?.render(frame, viewportScale);
16064
+ },
15822
16065
  getDrawnState() {
15823
16066
  return {
15824
16067
  drawnFrameTime: visibleMaskMediaTime,
@@ -15936,10 +16179,11 @@ function createPixiMaskLayer(options) {
15936
16179
  preparedRenderWindow.destroy();
15937
16180
  destroyTextures();
15938
16181
  idMaskRenderer?.destroy();
16182
+ screenStrokeRenderer?.destroy();
15939
16183
  haloRenderer?.destroy();
15940
16184
  },
15941
16185
  };
15942
- function showMaskFrame(maskFrame, mediaTime, presentedFrameId) {
16186
+ function showMaskFrame(maskFrame, mediaTime, presentedFrameId, viewportScale) {
15943
16187
  visibleMaskMediaTime = mediaTime;
15944
16188
  visibleMaskFrameKey = maskFrame.key;
15945
16189
  visibleMaskFrameId = presentedFrameId;
@@ -15953,6 +16197,18 @@ function createPixiMaskLayer(options) {
15953
16197
  hideFill();
15954
16198
  return;
15955
16199
  }
16200
+ if (!screenStrokeRenderer &&
16201
+ maskFrame.screenStrokes?.length &&
16202
+ options.Graphics) {
16203
+ screenStrokeRenderer = createPixiMaskScreenStrokeRenderer({
16204
+ Graphics: options.Graphics,
16205
+ mediaWidth,
16206
+ mediaHeight,
16207
+ });
16208
+ screenStrokeRenderer.display.alpha = maskOpacity;
16209
+ maskContainer?.addChild(screenStrokeRenderer.display);
16210
+ }
16211
+ screenStrokeRenderer?.render(maskFrame, viewportScale);
15956
16212
  if (maskFrame.kind === PreparedMaskFrameKind.IdMask && idMaskRenderer) {
15957
16213
  showIdMaskFrame(maskFrame);
15958
16214
  return;
@@ -16084,6 +16340,7 @@ function createPixiMaskLayer(options) {
16084
16340
  maskSprite.visible = false;
16085
16341
  }
16086
16342
  idMaskRenderer?.hide();
16343
+ screenStrokeRenderer?.hide();
16087
16344
  }
16088
16345
  /**
16089
16346
  * Everything the layer hands other layers is a view of the raster on screen,
@@ -16103,6 +16360,8 @@ function createPixiMaskLayer(options) {
16103
16360
  maskSprite.alpha = maskOpacity;
16104
16361
  }
16105
16362
  idMaskRenderer?.setOpacity(maskOpacity);
16363
+ if (screenStrokeRenderer)
16364
+ screenStrokeRenderer.display.alpha = maskOpacity;
16106
16365
  }
16107
16366
  function destroyTexture(key) {
16108
16367
  destroyHaloTexture(key);
@@ -16399,9 +16658,9 @@ function createPixiMaskBrushPreview(options) {
16399
16658
  sprite.width = editor.canvas.width;
16400
16659
  sprite.height = editor.canvas.height;
16401
16660
  sprite.alpha = options.preview.alpha ?? 0.4;
16402
- sprite.tint = options.preview.color ?? 0x22c55e;
16403
16661
  display.addChild(sprite, cursor);
16404
16662
  const updateTexture = () => {
16663
+ sprite.tint = options.preview.color ?? 0x22c55e;
16405
16664
  source.update();
16406
16665
  scheduleInvalidation();
16407
16666
  };
@@ -16437,6 +16696,7 @@ function createPixiMaskBrushPreview(options) {
16437
16696
  });
16438
16697
  }
16439
16698
  function drawCursor() {
16699
+ sprite.tint = options.preview.color ?? 0x22c55e;
16440
16700
  cursor.clear();
16441
16701
  const state = editor.getCursor();
16442
16702
  if (!state.point)
@@ -17973,6 +18233,7 @@ function drawLoading(graphics, context, style) {
17973
18233
  const rect = getDetectionRect(detection);
17974
18234
  if (!rect)
17975
18235
  continue;
18236
+ graphics.beginPath();
17976
18237
  graphics.arc(rect.x, rect.y, resolveScreenLength(10, context.viewportScale), angle, angle + Math.PI * 1.5);
17977
18238
  const stroke = resolvePixiStroke(style.loading.stroke, context.viewportScale);
17978
18239
  graphics.stroke({
@@ -18307,7 +18568,11 @@ async function createPixiMediaScene(options) {
18307
18568
  hidden: currentVisibility?.labelsHidden === true || state.hidden,
18308
18569
  };
18309
18570
  };
18571
+ // Editable geometry uses retained displays, so boxes and vectors can share
18572
+ // one ordered parent without changing the dense, non-editing raster paths.
18573
+ const retainedGeometry = options.editingEngine ? new Container() : undefined;
18310
18574
  const boxLayer = createPixiBoxLayer({
18575
+ container: retainedGeometry,
18311
18576
  boxStyle: options.boxStyle,
18312
18577
  Container: options.editingEngine ? Container : undefined,
18313
18578
  detectionTimeline: annotationDetectionTimeline,
@@ -18387,6 +18652,7 @@ async function createPixiMediaScene(options) {
18387
18652
  })
18388
18653
  : undefined;
18389
18654
  const vectorLayer = createPixiVectorLayer({
18655
+ container: retainedGeometry,
18390
18656
  Container,
18391
18657
  Graphics,
18392
18658
  detectionTimeline: annotationDetectionTimeline,
@@ -18438,6 +18704,7 @@ async function createPixiMediaScene(options) {
18438
18704
  BlurFilter,
18439
18705
  BufferImageSource,
18440
18706
  Container,
18707
+ Graphics,
18441
18708
  ImageSource,
18442
18709
  Mesh,
18443
18710
  MeshGeometry,
@@ -18796,11 +19063,25 @@ async function createPixiMediaScene(options) {
18796
19063
  maskLayer?.setRasterDisplay(display);
18797
19064
  options.onDisplayChange?.(display);
18798
19065
  };
19066
+ const updateMediaSampling = () => {
19067
+ if (!mediaSprite)
19068
+ return;
19069
+ const threshold = options.pixelInspection?.minScale;
19070
+ if (threshold === undefined)
19071
+ return;
19072
+ const source = mediaSprite.texture.source;
19073
+ const mode = viewportScale >= threshold ? "nearest" : "linear";
19074
+ if (source.scaleMode === mode)
19075
+ return;
19076
+ source.scaleMode = mode;
19077
+ source.style.update();
19078
+ };
18799
19079
  const applyViewportTransform = () => {
18800
19080
  if (!mediaScene || !baseFit)
18801
19081
  return;
18802
19082
  const transform = viewport.getTransform();
18803
19083
  viewportScale = baseFit.scale * transform.scale;
19084
+ updateMediaSampling();
18804
19085
  mediaScene.scale.set(viewportScale);
18805
19086
  const box = syncPresentationBox();
18806
19087
  mediaScene.position.set(baseFit.x + transform.x - box.left, baseFit.y + transform.y - box.top);
@@ -18922,8 +19203,9 @@ async function createPixiMediaScene(options) {
18922
19203
  : () => undefined,
18923
19204
  };
18924
19205
  // A viewport-style redraw covers the layers whose geometry is a function of
18925
- // the viewport scale. Neither the mask raster nor the interaction hit map
18926
- // takes one, and the hit map is what decides the hover a pan must not move.
19206
+ // the viewport scale. Mask outlines reuse the artifact already on screen;
19207
+ // the mask raster and the interaction hit map stay untouched, so a pan
19208
+ // does not change which detection is hovered.
18927
19209
  const viewportStyleRedrawLayers = {
18928
19210
  ...annotationDrawLayers,
18929
19211
  drawBox: (mediaTime) => {
@@ -18932,7 +19214,7 @@ async function createPixiMediaScene(options) {
18932
19214
  },
18933
19215
  drawDepth: undefined,
18934
19216
  drawInteraction: undefined,
18935
- drawMask: undefined,
19217
+ drawMask: () => maskLayer?.redrawViewportStyles(viewportScale),
18936
19218
  };
18937
19219
  let frameDrawTimings = createFrameDrawTimings();
18938
19220
  let presentStartMs = 0;
@@ -19009,7 +19291,7 @@ async function createPixiMediaScene(options) {
19009
19291
  if (currentHeatmapRenderers.length > 0) {
19010
19292
  heatmapSlot.setDisplay(ensureHeatmapLayer().createContainer());
19011
19293
  }
19012
- boxSlot.setDisplay(retainedBoxes ?? boxes);
19294
+ boxSlot.setDisplay(retainedGeometry ? undefined : (retainedBoxes ?? boxes));
19013
19295
  vectorDisplay = new Container();
19014
19296
  attachPolygonLayerDisplay();
19015
19297
  vectorDisplay.addChild(vectorLayer.createContainer());
@@ -19156,6 +19438,8 @@ async function createPixiMediaScene(options) {
19156
19438
  const brightness = adjustments.brightness ?? 1;
19157
19439
  const contrast = adjustments.contrast ?? 1;
19158
19440
  const filter = new ColorMatrixFilter();
19441
+ // Preserve the renderer's pixel grid through the adjustment pass.
19442
+ filter.resolution = "inherit";
19159
19443
  filter.brightness(brightness, false);
19160
19444
  filter.contrast(contrast - 1, true);
19161
19445
  if (mediaSprite)
@@ -19224,6 +19508,8 @@ async function createPixiMediaScene(options) {
19224
19508
  };
19225
19509
  },
19226
19510
  panViewportBy(dx, dy) {
19511
+ if (viewport.getTransform().locked)
19512
+ return;
19227
19513
  const before = viewport.getTransform();
19228
19514
  viewport.panBy(dx, dy);
19229
19515
  const after = viewport.getTransform();
@@ -19234,7 +19520,7 @@ async function createPixiMediaScene(options) {
19234
19520
  renderOnChange();
19235
19521
  },
19236
19522
  zoomViewportAt(point, factor) {
19237
- if (!baseFit)
19523
+ if (!baseFit || viewport.getTransform().locked)
19238
19524
  return;
19239
19525
  viewport.zoomAt({ x: point.x - baseFit.x, y: point.y - baseFit.y }, factor);
19240
19526
  const transform = viewport.getTransform();
@@ -19247,7 +19533,7 @@ async function createPixiMediaScene(options) {
19247
19533
  renderOnChange();
19248
19534
  },
19249
19535
  zoomViewportFromWheel(point, deltaY) {
19250
- if (!baseFit)
19536
+ if (!baseFit || viewport.getTransform().locked)
19251
19537
  return;
19252
19538
  viewport.zoomFromWheel({ x: point.x - baseFit.x, y: point.y - baseFit.y }, deltaY);
19253
19539
  const transform = viewport.getTransform();
@@ -19553,6 +19839,7 @@ async function createPixiMediaScene(options) {
19553
19839
  BlurFilter,
19554
19840
  BufferImageSource,
19555
19841
  Container,
19842
+ Graphics,
19556
19843
  ImageSource,
19557
19844
  Mesh,
19558
19845
  MeshGeometry,
@@ -19991,7 +20278,7 @@ async function createPixiMediaScene(options) {
19991
20278
  return;
19992
20279
  }
19993
20280
  if (isFramePrepared(mediaTime)) {
19994
- maskLayer.drawFrame(mediaTime, presentedFrameId);
20281
+ maskLayer.drawFrame(mediaTime, presentedFrameId, viewportScale);
19995
20282
  return;
19996
20283
  }
19997
20284
  maskLayer.clearFrame();
@@ -20189,10 +20476,23 @@ async function createPixiMediaScene(options) {
20189
20476
  });
20190
20477
  detached.width = mediaWidth;
20191
20478
  detached.height = mediaHeight;
20479
+ const source = sprite.texture.source;
20480
+ const displayScaleMode = source.scaleMode;
20192
20481
  try {
20482
+ // Capture retains its baseline sampling even when pixel inspection
20483
+ // changes display sampling. Extraction is synchronous, so the shared
20484
+ // source can be restored before another frame draws.
20485
+ if (displayScaleMode !== "linear") {
20486
+ source.scaleMode = "linear";
20487
+ source.style.update();
20488
+ }
20193
20489
  return app.renderer.extract.canvas(detached);
20194
20490
  }
20195
20491
  finally {
20492
+ if (displayScaleMode !== "linear") {
20493
+ source.scaleMode = displayScaleMode;
20494
+ source.style.update();
20495
+ }
20196
20496
  detached.destroy();
20197
20497
  }
20198
20498
  };
@@ -20207,6 +20507,7 @@ async function createPixiMediaScene(options) {
20207
20507
  // takes the new size's scale while still drawing the previous size's
20208
20508
  // quad, so a preview-sized frame paints the media oversized.
20209
20509
  sprite.texture = new Texture({ dynamic: true, source: externalSource });
20510
+ updateMediaSampling();
20210
20511
  sizeMediaSprite();
20211
20512
  return externalSource;
20212
20513
  },