supervision 0.2.3 → 0.2.4

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 (38) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +672 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/mask-preparation.worker.js +129 -0
  6. package/dist/mask-preparation.worker.js.map +1 -1
  7. package/dist/render-preparation/heatmap-frame-preparer.d.ts +13 -0
  8. package/dist/render-preparation/heatmap-frame-preparer.d.ts.map +1 -0
  9. package/dist/render-preparation/heatmap-preparation-worker-protocol.d.ts +24 -0
  10. package/dist/render-preparation/heatmap-preparation-worker-protocol.d.ts.map +1 -0
  11. package/dist/renderers/heatmap-color.d.ts +7 -0
  12. package/dist/renderers/heatmap-color.d.ts.map +1 -0
  13. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  14. package/dist/renderers/media-renderer-scene.d.ts +2 -1
  15. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  16. package/dist/renderers/pixi-frame-present.d.ts +1 -0
  17. package/dist/renderers/pixi-frame-present.d.ts.map +1 -1
  18. package/dist/renderers/pixi-heatmap-layer.d.ts +42 -0
  19. package/dist/renderers/pixi-heatmap-layer.d.ts.map +1 -0
  20. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  21. package/dist/renderers/pixi-scene-layer-slot.d.ts +1 -0
  22. package/dist/renderers/pixi-scene-layer-slot.d.ts.map +1 -1
  23. package/dist/tracking.worker.js.map +1 -1
  24. package/dist/workers/worker-rpc-client.d.ts +1 -1
  25. package/dist/workers/worker-rpc-client.d.ts.map +1 -1
  26. package/node_modules/supervision-js-core/dist/index.d.ts +2 -2
  27. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  28. package/node_modules/supervision-js-core/dist/index.js +109 -1
  29. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  30. package/node_modules/supervision-js-core/dist/interactions/annotation-handles.d.ts.map +1 -1
  31. package/node_modules/supervision-js-core/dist/styles/annotation-renderer-presentation.d.ts.map +1 -1
  32. package/node_modules/supervision-js-core/dist/styles/annotation-renderer-registry.d.ts.map +1 -1
  33. package/node_modules/supervision-js-core/dist/types/annotation-renderer.d.ts +24 -3
  34. package/node_modules/supervision-js-core/dist/types/annotation-renderer.d.ts.map +1 -1
  35. package/node_modules/supervision-js-core/dist/types/detections.d.ts +18 -0
  36. package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -1
  37. package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -1
  38. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -556,7 +556,7 @@ function createWorkerRpcClient(options) {
556
556
  getFailureMessage() {
557
557
  return failureMessage;
558
558
  },
559
- request(message) {
559
+ request(message, transfer) {
560
560
  if (isDestroyed) {
561
561
  return Promise.reject(new Error("Worker RPC client has been destroyed."));
562
562
  }
@@ -571,7 +571,7 @@ function createWorkerRpcClient(options) {
571
571
  return new Promise((resolve, reject) => {
572
572
  pendingRequests.set(request.requestId, { reject, resolve });
573
573
  try {
574
- options.worker.postMessage(request);
574
+ options.worker.postMessage(request, transfer ?? []);
575
575
  }
576
576
  catch (error) {
577
577
  pendingRequests.delete(request.requestId);
@@ -4747,6 +4747,7 @@ async function createMediaRendererCore(options, providers) {
4747
4747
  polylineStyle: currentPresentation.polylineStyle,
4748
4748
  presentedFrames: protectedPresentedFrames?.source,
4749
4749
  regionRenderers: resolveRegionRenderers(currentPresentation),
4750
+ heatmapRenderers: resolveHeatmapRenderers(currentPresentation),
4750
4751
  previewOverlay: options.previewOverlay,
4751
4752
  renderPreparation: options.renderPreparation
4752
4753
  ? {
@@ -4997,6 +4998,10 @@ function resolveRegionRenderers(presentation) {
4997
4998
  return (presentation.renderers?.filter((renderer) => renderer.kind === "region") ??
4998
4999
  []);
4999
5000
  }
5001
+ function resolveHeatmapRenderers(presentation) {
5002
+ return (presentation.renderers?.filter((renderer) => renderer.kind === "heatmap") ??
5003
+ []);
5004
+ }
5000
5005
  async function openRendererMediaSource(options, providers) {
5001
5006
  if (options.src !== undefined && options.source !== undefined) {
5002
5007
  throw new Error("Provide either src or source, not both.");
@@ -5125,6 +5130,7 @@ function presentVideoFrame(presented, targets) {
5125
5130
  * to check its steps against.
5126
5131
  */
5127
5132
  function drawFramePresentLayers(layers, mediaTime, presented) {
5133
+ maybeAt(layers.drawHeatmap, mediaTime, presented);
5128
5134
  maskAt(layers.drawMask, mediaTime, presented);
5129
5135
  const boxState = at(layers.drawBox, mediaTime, presented);
5130
5136
  maybeAt(layers.drawPolygon, mediaTime, presented);
@@ -5153,6 +5159,7 @@ function measureFramePresentLayers(layers, measureStep) {
5153
5159
  drawAnnotationOverlay: timed("drawAnnotationOverlay", layers.drawAnnotationOverlay),
5154
5160
  drawBox: timed("drawBox", layers.drawBox),
5155
5161
  drawFocus: timed("drawFocus", layers.drawFocus),
5162
+ drawHeatmap: maybeTimed("drawHeatmap", layers.drawHeatmap),
5156
5163
  drawInteraction: maybeTimed("drawInteraction", layers.drawInteraction),
5157
5164
  drawInteractionPresentation: timed("drawInteractionPresentation", layers.drawInteractionPresentation),
5158
5165
  drawLabel: maybeTimed("drawLabel", layers.drawLabel),
@@ -5452,7 +5459,7 @@ function writePixel(rgba, canvasWidth, x, y, color) {
5452
5459
  rgba[rgbaOffset + 3] = color.alpha;
5453
5460
  }
5454
5461
 
5455
- 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 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 const maskWidth = Math.max(...instructions.map(({ mask }) => mask.width));\n const maskHeight = 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 = mask.width;\n const maskHeight = 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 = mask.width;\n const maskHeight = 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 column = axes.columns[x];\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[axes.rows[y] + column] = detectionMaskId;\n y += 1;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n column = axes.columns[x];\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 * A stroke is measured in texels of the raster it is drawn on, so a coarser\n * raster measures it in coarser texels. A stroke of a texel or more keeps at\n * least one, the thinnest line the shader can draw; a narrower one keeps its\n * own width, which the shader draws as an inner boundary at any scale. The\n * ceiling is the widest neighbourhood the shaders scan, so a wider stroke would\n * be drawn at the ceiling anyway and every layer drawing the same annotation\n * has to arrive at the same width.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {\n const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;\n return Math.min(Math.max(strokeWidth * scale, Math.min(strokeWidth, 1)), 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 };\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\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 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 const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\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})();";
5462
+ 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 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 const maskWidth = Math.max(...instructions.map(({ mask }) => mask.width));\n const maskHeight = 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 = mask.width;\n const maskHeight = 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 = mask.width;\n const maskHeight = 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 column = axes.columns[x];\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[axes.rows[y] + column] = detectionMaskId;\n y += 1;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n column = axes.columns[x];\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 * A stroke is measured in texels of the raster it is drawn on, so a coarser\n * raster measures it in coarser texels. A stroke of a texel or more keeps at\n * least one, the thinnest line the shader can draw; a narrower one keeps its\n * own width, which the shader draws as an inner boundary at any scale. The\n * ceiling is the widest neighbourhood the shaders scan, so a wider stroke would\n * be drawn at the ceiling anyway and every layer drawing the same annotation\n * has to arrive at the same width.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {\n const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;\n return Math.min(Math.max(strokeWidth * scale, Math.min(strokeWidth, 1)), 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 };\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\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 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 !== 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 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})();";
5456
5463
 
5457
5464
  const DEFAULT_WORKER_NAME = "supervision-js-render-preparation";
5458
5465
  let defaultWorkerUrl;
@@ -7663,6 +7670,616 @@ function getBoxLayerState(detectionFrame, activeDetectionIndexes) {
7663
7670
  };
7664
7671
  }
7665
7672
 
7673
+ const DEFAULT_STOPS = [
7674
+ { position: 0, color: 0xffd600 },
7675
+ { position: 0.35, color: 0xffd600 },
7676
+ { position: 0.7, color: 0xff7a00 },
7677
+ { position: 1, color: 0xdc1e1e },
7678
+ ];
7679
+ const MAX_HEATMAP_PIXELS = 16_777_216;
7680
+ const MAX_HEATMAP_SIDE = 8_192;
7681
+ /** Turns semantic scores into an RGBA raster; no palette enters cold data. */
7682
+ function colorizeHeatmap(map, renderer) {
7683
+ const { bounds, width, height, values } = map;
7684
+ if (!Number.isFinite(bounds?.x) ||
7685
+ !Number.isFinite(bounds?.y) ||
7686
+ !Number.isFinite(bounds?.width) ||
7687
+ !Number.isFinite(bounds?.height) ||
7688
+ bounds.width <= 0 ||
7689
+ bounds.height <= 0 ||
7690
+ !Number.isInteger(width) ||
7691
+ !Number.isInteger(height) ||
7692
+ width <= 0 ||
7693
+ height <= 0 ||
7694
+ width > MAX_HEATMAP_SIDE ||
7695
+ height > MAX_HEATMAP_SIDE ||
7696
+ width * height > MAX_HEATMAP_PIXELS ||
7697
+ !(Array.isArray(values) ||
7698
+ values instanceof Float32Array ||
7699
+ values instanceof Float64Array ||
7700
+ values instanceof Uint16Array) ||
7701
+ values.length !== width * height) {
7702
+ throw new RangeError("Heatmap bounds and dimensions must be valid, match row-major values, and fit the raster limit.");
7703
+ }
7704
+ const scale = map.valueScale ?? 1;
7705
+ const threshold = (map.threshold ?? 0) * (renderer.thresholdScale ?? 1);
7706
+ const maximumScore = renderer.maximumScore ?? 1;
7707
+ const opacity = clamp01(renderer.opacity ?? 1);
7708
+ const minimumAlpha = clamp01(renderer.minimumAlpha ?? 0);
7709
+ const stops = renderer.colorStops ?? DEFAULT_STOPS;
7710
+ if (!Number.isFinite(scale) ||
7711
+ scale <= 0 ||
7712
+ !Number.isFinite(threshold) ||
7713
+ !Number.isFinite(maximumScore) ||
7714
+ maximumScore <= 0 ||
7715
+ !Number.isFinite(renderer.thresholdScale ?? 1) ||
7716
+ (renderer.thresholdScale ?? 1) < 0 ||
7717
+ !Number.isFinite(renderer.opacity ?? 1) ||
7718
+ !Number.isFinite(renderer.minimumAlpha ?? 0) ||
7719
+ stops.length === 0 ||
7720
+ stops.some((stop, index) => !Number.isFinite(stop.position) ||
7721
+ stop.position < 0 ||
7722
+ stop.position > 1 ||
7723
+ !Number.isInteger(stop.color) ||
7724
+ stop.color < 0 ||
7725
+ stop.color > 0xffffff ||
7726
+ (index > 0 && stop.position < stops[index - 1].position))) {
7727
+ throw new RangeError("Invalid heatmap scale, threshold, or color stops.");
7728
+ }
7729
+ const rgba = new Uint8ClampedArray(new ArrayBuffer(values.length * 4));
7730
+ if (maximumScore <= threshold || opacity === 0)
7731
+ return rgba;
7732
+ for (let index = 0; index < values.length; index += 1) {
7733
+ const score = values[index] * scale;
7734
+ if (!Number.isFinite(score) || score <= threshold)
7735
+ continue;
7736
+ const color = interpolateColor(stops, clamp01(score / maximumScore));
7737
+ const alphaIntensity = clamp01((score - threshold) / (maximumScore - threshold));
7738
+ const offset = index * 4;
7739
+ rgba[offset] = color >> 16;
7740
+ rgba[offset + 1] = (color >> 8) & 0xff;
7741
+ rgba[offset + 2] = color & 0xff;
7742
+ rgba[offset + 3] = Math.round(255 * opacity * (minimumAlpha + (1 - minimumAlpha) * alphaIntensity));
7743
+ }
7744
+ return rgba;
7745
+ }
7746
+ function interpolateColor(stops, value) {
7747
+ const upperIndex = stops.findIndex((stop) => stop.position >= value);
7748
+ if (upperIndex === -1)
7749
+ return stops[stops.length - 1].color;
7750
+ if (upperIndex === 0)
7751
+ return stops[0].color;
7752
+ const left = stops[upperIndex - 1];
7753
+ const right = stops[upperIndex];
7754
+ const span = right.position - left.position;
7755
+ const ratio = span === 0 ? 1 : (value - left.position) / span;
7756
+ const channel = (shift) => Math.round(((left.color >> shift) & 0xff) * (1 - ratio) +
7757
+ ((right.color >> shift) & 0xff) * ratio);
7758
+ return (channel(16) << 16) | (channel(8) << 8) | channel(0);
7759
+ }
7760
+ function clamp01(value) {
7761
+ return Math.max(0, Math.min(1, value));
7762
+ }
7763
+
7764
+ var HeatmapPreparationWorkerMessageType;
7765
+ (function (HeatmapPreparationWorkerMessageType) {
7766
+ HeatmapPreparationWorkerMessageType["Prepare"] = "heatmap-prepare";
7767
+ HeatmapPreparationWorkerMessageType["Complete"] = "heatmap-complete";
7768
+ HeatmapPreparationWorkerMessageType["Error"] = "heatmap-error";
7769
+ })(HeatmapPreparationWorkerMessageType || (HeatmapPreparationWorkerMessageType = {}));
7770
+
7771
+ /** The same bundled worker also cooks masks, but heatmaps use their own queue. */
7772
+ function createHeatmapFramePreparer(options) {
7773
+ const mode = options?.mode ?? RenderPreparationMode.Auto;
7774
+ let isDestroyed = false;
7775
+ let rpc = null;
7776
+ if (mode !== RenderPreparationMode.MainThread) {
7777
+ try {
7778
+ if (!options?.workerFactory && typeof Worker === "undefined") {
7779
+ throw new Error("Heatmap preparation worker is unavailable.");
7780
+ }
7781
+ const factory = options?.workerFactory ?? createDefaultRenderPreparationWorkerFactory();
7782
+ rpc = createWorkerRpcClient({
7783
+ defaultErrorMessage: "Heatmap preparation worker failed.",
7784
+ isResponse: isHeatmapResponse,
7785
+ onOrphanedResponse: closeHeatmapResponse,
7786
+ worker: factory.createWorker(),
7787
+ });
7788
+ }
7789
+ catch (error) {
7790
+ if (mode === RenderPreparationMode.Worker)
7791
+ throw error;
7792
+ }
7793
+ }
7794
+ return {
7795
+ destroy() {
7796
+ isDestroyed = true;
7797
+ rpc?.destroy();
7798
+ rpc = null;
7799
+ },
7800
+ async prepare(map, renderer) {
7801
+ if (isDestroyed) {
7802
+ throw new Error("Heatmap frame preparer has been destroyed.");
7803
+ }
7804
+ if (rpc) {
7805
+ const copiedMap = await copyHeatmapForTransfer(map, () => isDestroyed);
7806
+ if (isDestroyed) {
7807
+ throw new Error("Heatmap frame preparer has been destroyed.");
7808
+ }
7809
+ let response;
7810
+ try {
7811
+ response = await rpc.request({
7812
+ map: copiedMap,
7813
+ renderer,
7814
+ type: HeatmapPreparationWorkerMessageType.Prepare,
7815
+ }, [copiedMap.values.buffer]);
7816
+ }
7817
+ catch (error) {
7818
+ if (mode === RenderPreparationMode.Worker || isDestroyed)
7819
+ throw error;
7820
+ rpc?.destroy();
7821
+ rpc = null;
7822
+ console.warn("Heatmap preparation worker failed; falling back to the main thread.", error);
7823
+ }
7824
+ if (response?.type === HeatmapPreparationWorkerMessageType.Error) {
7825
+ throw new RangeError(response.error);
7826
+ }
7827
+ if (response?.type === HeatmapPreparationWorkerMessageType.Complete) {
7828
+ const resource = response.imageBitmap ?? response.imageData;
7829
+ if (!resource)
7830
+ throw new Error("Heatmap worker returned no raster.");
7831
+ return {
7832
+ resource,
7833
+ close() {
7834
+ if ("close" in resource)
7835
+ resource.close();
7836
+ },
7837
+ };
7838
+ }
7839
+ }
7840
+ // Deferred from the present, even in environments without workers.
7841
+ await new Promise((resolve) => setTimeout(resolve, 0));
7842
+ if (isDestroyed) {
7843
+ throw new Error("Heatmap frame preparer has been destroyed.");
7844
+ }
7845
+ const resource = new ImageData(colorizeHeatmap(map, renderer), map.width, map.height);
7846
+ return { resource, close() { } };
7847
+ },
7848
+ };
7849
+ }
7850
+ /** Copy in bounded tasks so posting a large map never serializes caller-owned scores. */
7851
+ async function copyHeatmapForTransfer(map, isDestroyed) {
7852
+ const values = map.values;
7853
+ if (values.length > 16_777_216) {
7854
+ throw new RangeError("Heatmap exceeds the raster pixel limit.");
7855
+ }
7856
+ const copied = values instanceof Float32Array
7857
+ ? new Float32Array(values.length)
7858
+ : values instanceof Uint16Array
7859
+ ? new Uint16Array(values.length)
7860
+ : new Float64Array(values.length);
7861
+ const chunkSize = 131_072;
7862
+ for (let start = 0; start < values.length; start += chunkSize) {
7863
+ if (isDestroyed()) {
7864
+ throw new Error("Heatmap frame preparer has been destroyed.");
7865
+ }
7866
+ const end = Math.min(start + chunkSize, values.length);
7867
+ if (values instanceof Float32Array || values instanceof Uint16Array) {
7868
+ copied.set(values.subarray(start, end), start);
7869
+ }
7870
+ else {
7871
+ for (let index = start; index < end; index += 1)
7872
+ copied[index] = values[index];
7873
+ }
7874
+ if (end < values.length) {
7875
+ await new Promise((resolve) => setTimeout(resolve, 0));
7876
+ }
7877
+ }
7878
+ return { ...map, values: copied };
7879
+ }
7880
+ function isHeatmapResponse(value) {
7881
+ if (typeof value !== "object" || value === null)
7882
+ return false;
7883
+ const type = value.type;
7884
+ return (type === HeatmapPreparationWorkerMessageType.Complete ||
7885
+ type === HeatmapPreparationWorkerMessageType.Error);
7886
+ }
7887
+ function closeHeatmapResponse(response) {
7888
+ if (response.type === HeatmapPreparationWorkerMessageType.Complete) {
7889
+ response.imageBitmap?.close();
7890
+ }
7891
+ }
7892
+
7893
+ const ASYNC_HEATMAP_PIXELS = 65_536;
7894
+ const MAX_VISIBLE_HEATMAP_PIXELS = 16_777_216;
7895
+ const MAX_CACHED_HEATMAP_BYTES = 128 * 1024 * 1024;
7896
+ function createPixiHeatmapLayer(options) {
7897
+ const container = new options.Container();
7898
+ let renderers = options.renderers;
7899
+ let lastFrame;
7900
+ let dirty = true;
7901
+ let warnedInvalidHeatmap = false;
7902
+ let warnedFrameBudget = false;
7903
+ let generation = 0;
7904
+ let nextIdentity = 0;
7905
+ let cachedBytes = 0;
7906
+ let destroyed = false;
7907
+ let activeJob;
7908
+ let preparer = options.preparer;
7909
+ const identities = new WeakMap();
7910
+ const cache = new Map();
7911
+ const pending = new Map();
7912
+ const failed = new Set();
7913
+ const activeKeys = new Set();
7914
+ const wantedKeys = new Set();
7915
+ const currentKeys = new Set();
7916
+ const positionedSprites = [];
7917
+ const clear = () => {
7918
+ for (const { sprite, temporaryTexture } of positionedSprites) {
7919
+ const texture = sprite.texture;
7920
+ sprite.removeFromParent();
7921
+ sprite.destroy();
7922
+ if (temporaryTexture)
7923
+ texture.destroy(true);
7924
+ }
7925
+ positionedSprites.length = 0;
7926
+ activeKeys.clear();
7927
+ };
7928
+ const clearCache = () => {
7929
+ if (activeJob && (!options.preparer || options.preparerFactory)) {
7930
+ activeJob.cancelled = true;
7931
+ preparer?.destroy();
7932
+ preparer = undefined;
7933
+ activeJob = undefined;
7934
+ }
7935
+ clear();
7936
+ for (const entry of cache.values()) {
7937
+ entry.texture.destroy(true);
7938
+ entry.image.close();
7939
+ }
7940
+ cache.clear();
7941
+ pending.clear();
7942
+ failed.clear();
7943
+ wantedKeys.clear();
7944
+ currentKeys.clear();
7945
+ cachedBytes = 0;
7946
+ generation += 1;
7947
+ };
7948
+ const keyFor = (map, renderer) => {
7949
+ const identity = (value) => {
7950
+ let id = identities.get(value);
7951
+ if (id === undefined) {
7952
+ id = ++nextIdentity;
7953
+ identities.set(value, id);
7954
+ }
7955
+ return id;
7956
+ };
7957
+ return `${identity(map)}:${identity(renderer)}`;
7958
+ };
7959
+ const touch = (key) => {
7960
+ const entry = cache.get(key);
7961
+ if (entry) {
7962
+ cache.delete(key);
7963
+ cache.set(key, entry);
7964
+ }
7965
+ return entry;
7966
+ };
7967
+ const evict = () => {
7968
+ for (const [key, entry] of cache) {
7969
+ if (cachedBytes <= MAX_CACHED_HEATMAP_BYTES)
7970
+ break;
7971
+ if (activeKeys.has(key))
7972
+ continue;
7973
+ cache.delete(key);
7974
+ cachedBytes -= entry.bytes;
7975
+ entry.texture.destroy(true);
7976
+ entry.image.close();
7977
+ }
7978
+ };
7979
+ const makeCanvasTexture = (map, rgba) => {
7980
+ const canvas = document.createElement("canvas");
7981
+ canvas.width = map.width;
7982
+ canvas.height = map.height;
7983
+ const context = canvas.getContext("2d");
7984
+ if (!context)
7985
+ return undefined;
7986
+ const pixels = context.createImageData(map.width, map.height);
7987
+ pixels.data.set(rgba);
7988
+ context.putImageData(pixels, 0, 0);
7989
+ return new options.Texture({
7990
+ source: new options.CanvasSource({
7991
+ resource: canvas,
7992
+ width: map.width,
7993
+ height: map.height,
7994
+ }),
7995
+ });
7996
+ };
7997
+ const makePreparedTexture = (map, image) => {
7998
+ if (typeof ImageBitmap !== "undefined" &&
7999
+ image.resource instanceof ImageBitmap &&
8000
+ options.ImageSource) {
8001
+ return new options.Texture({
8002
+ source: new options.ImageSource({
8003
+ dynamic: false,
8004
+ resource: image.resource,
8005
+ width: map.width,
8006
+ height: map.height,
8007
+ }),
8008
+ });
8009
+ }
8010
+ const imageData = image.resource;
8011
+ const texture = makeCanvasTexture(map, imageData.data);
8012
+ if (!texture)
8013
+ throw new Error("Unable to create heatmap texture.");
8014
+ return texture;
8015
+ };
8016
+ const visibleMaps = (frame) => {
8017
+ const orderedDetections = frame.detections
8018
+ .map((detection, index) => ({ detection, index }))
8019
+ .sort((left, right) => (left.detection.zIndex ?? left.index) -
8020
+ (right.detection.zIndex ?? right.index) || left.index - right.index);
8021
+ const result = [];
8022
+ let pixels = 0;
8023
+ for (const { detection } of orderedDetections) {
8024
+ const map = detection.heatmap;
8025
+ if (!map || !options.isVisible(detection))
8026
+ continue;
8027
+ for (const renderer of renderers) {
8028
+ if (pixels + map.width * map.height > MAX_VISIBLE_HEATMAP_PIXELS) {
8029
+ if (!warnedFrameBudget) {
8030
+ console.warn("Skipping heatmaps beyond the 16M-pixel frame budget.");
8031
+ warnedFrameBudget = true;
8032
+ }
8033
+ continue;
8034
+ }
8035
+ pixels += map.width * map.height;
8036
+ result.push({ detection, map, renderer });
8037
+ }
8038
+ }
8039
+ return result;
8040
+ };
8041
+ const shouldPrepareAsync = (map, visiblePixelCount) => map.width * map.height >= ASYNC_HEATMAP_PIXELS ||
8042
+ visiblePixelCount >= ASYNC_HEATMAP_PIXELS;
8043
+ const pump = () => {
8044
+ if (destroyed || activeJob || pending.size === 0)
8045
+ return;
8046
+ const job = pending.values().next().value;
8047
+ if (!job)
8048
+ return;
8049
+ pending.delete(job.key);
8050
+ activeJob = job;
8051
+ preparer ??=
8052
+ options.preparerFactory?.() ??
8053
+ options.preparer ??
8054
+ createHeatmapFramePreparer(options.renderPreparation);
8055
+ void preparer
8056
+ .prepare(job.map, job.renderer)
8057
+ .then(async (image) => {
8058
+ if (destroyed ||
8059
+ job.cancelled ||
8060
+ job.generation !== generation ||
8061
+ !wantedKeys.has(job.key)) {
8062
+ image.close();
8063
+ return;
8064
+ }
8065
+ let texture;
8066
+ try {
8067
+ texture = makePreparedTexture(job.map, image);
8068
+ await options.prepareTexture?.(texture);
8069
+ }
8070
+ catch (error) {
8071
+ texture?.destroy(true);
8072
+ image.close();
8073
+ if (destroyed || job.cancelled || job.generation !== generation) {
8074
+ return;
8075
+ }
8076
+ failed.add(job.key);
8077
+ if (!warnedInvalidHeatmap) {
8078
+ console.warn("Skipping an invalid detection heatmap.", error);
8079
+ warnedInvalidHeatmap = true;
8080
+ }
8081
+ if (currentKeys.has(job.key))
8082
+ options.onPreparedWindowChange?.();
8083
+ return;
8084
+ }
8085
+ if (destroyed ||
8086
+ job.cancelled ||
8087
+ job.generation !== generation ||
8088
+ !wantedKeys.has(job.key)) {
8089
+ texture.destroy(true);
8090
+ image.close();
8091
+ return;
8092
+ }
8093
+ cache.set(job.key, {
8094
+ texture,
8095
+ image,
8096
+ bytes: job.map.width * job.map.height * 4,
8097
+ });
8098
+ cachedBytes += job.map.width * job.map.height * 4;
8099
+ evict();
8100
+ dirty = true;
8101
+ if (currentKeys.has(job.key))
8102
+ options.onPreparedWindowChange?.();
8103
+ })
8104
+ .catch((error) => {
8105
+ if (destroyed || job.cancelled || job.generation !== generation)
8106
+ return;
8107
+ failed.add(job.key);
8108
+ if (!warnedInvalidHeatmap) {
8109
+ console.warn("Skipping an invalid detection heatmap.", error);
8110
+ warnedInvalidHeatmap = true;
8111
+ }
8112
+ if (currentKeys.has(job.key))
8113
+ options.onPreparedWindowChange?.();
8114
+ })
8115
+ .finally(() => {
8116
+ if (activeJob === job)
8117
+ activeJob = undefined;
8118
+ pump();
8119
+ });
8120
+ };
8121
+ const enqueue = (map, renderer) => {
8122
+ const key = keyFor(map, renderer);
8123
+ if (cache.has(key) ||
8124
+ failed.has(key) ||
8125
+ pending.has(key) ||
8126
+ activeJob?.key === key)
8127
+ return;
8128
+ pending.set(key, { key, map, renderer, generation });
8129
+ pump();
8130
+ };
8131
+ const cancelUnwantedJob = () => {
8132
+ if (!activeJob ||
8133
+ wantedKeys.has(activeJob.key) ||
8134
+ (options.preparer && !options.preparerFactory)) {
8135
+ return;
8136
+ }
8137
+ activeJob.cancelled = true;
8138
+ preparer?.destroy();
8139
+ preparer = undefined;
8140
+ activeJob = undefined;
8141
+ pump();
8142
+ };
8143
+ return {
8144
+ createContainer: () => container,
8145
+ setRenderers(next) {
8146
+ if (next.length === renderers.length &&
8147
+ next.every((renderer, index) => renderer === renderers[index])) {
8148
+ return;
8149
+ }
8150
+ renderers = next;
8151
+ clearCache();
8152
+ dirty = true;
8153
+ },
8154
+ invalidate() {
8155
+ dirty = true;
8156
+ },
8157
+ isArtifactPrepared(mediaTime) {
8158
+ const frame = options.detectionTimeline.selectFrame(mediaTime);
8159
+ if (!frame || renderers.length === 0)
8160
+ return true;
8161
+ const maps = visibleMaps(frame);
8162
+ const visiblePixelCount = maps.reduce((total, { map }) => total + map.width * map.height, 0);
8163
+ return maps.every(({ map, renderer }) => {
8164
+ if (!shouldPrepareAsync(map, visiblePixelCount))
8165
+ return true;
8166
+ const key = keyFor(map, renderer);
8167
+ return cache.has(key) || failed.has(key);
8168
+ });
8169
+ },
8170
+ prepareFrame(mediaTime) {
8171
+ const frame = options.detectionTimeline.selectFrame(mediaTime);
8172
+ if (!frame || renderers.length === 0)
8173
+ return;
8174
+ const maps = visibleMaps(frame);
8175
+ const visiblePixelCount = maps.reduce((total, { map }) => total + map.width * map.height, 0);
8176
+ for (const { map, renderer } of maps) {
8177
+ if (shouldPrepareAsync(map, visiblePixelCount)) {
8178
+ wantedKeys.add(keyFor(map, renderer));
8179
+ enqueue(map, renderer);
8180
+ }
8181
+ }
8182
+ },
8183
+ translateDetection(id, dx, dy) {
8184
+ let translated = false;
8185
+ for (const entry of positionedSprites) {
8186
+ if (entry.detectionId !== id)
8187
+ continue;
8188
+ entry.sprite.x = entry.x + dx;
8189
+ entry.sprite.y = entry.y + dy;
8190
+ translated = true;
8191
+ }
8192
+ return translated;
8193
+ },
8194
+ drawFrame(mediaTime) {
8195
+ const frame = options.detectionTimeline.selectFrame(mediaTime);
8196
+ if (!dirty && frame === lastFrame)
8197
+ return;
8198
+ clear();
8199
+ pending.clear();
8200
+ wantedKeys.clear();
8201
+ currentKeys.clear();
8202
+ if (!frame || renderers.length === 0) {
8203
+ lastFrame = frame;
8204
+ dirty = false;
8205
+ cancelUnwantedJob();
8206
+ return;
8207
+ }
8208
+ const maps = visibleMaps(frame);
8209
+ const visiblePixelCount = maps.reduce((total, { map }) => total + map.width * map.height, 0);
8210
+ for (const { detection, map, renderer } of maps) {
8211
+ const key = keyFor(map, renderer);
8212
+ if (shouldPrepareAsync(map, visiblePixelCount)) {
8213
+ wantedKeys.add(key);
8214
+ currentKeys.add(key);
8215
+ }
8216
+ let texture = touch(key)?.texture;
8217
+ let temporaryTexture = false;
8218
+ if (!texture && !failed.has(key)) {
8219
+ if (shouldPrepareAsync(map, visiblePixelCount)) {
8220
+ enqueue(map, renderer);
8221
+ continue;
8222
+ }
8223
+ try {
8224
+ texture = makeCanvasTexture(map, colorizeHeatmap(map, renderer));
8225
+ temporaryTexture = true;
8226
+ }
8227
+ catch (error) {
8228
+ if (!(error instanceof RangeError))
8229
+ throw error;
8230
+ failed.add(key);
8231
+ if (!warnedInvalidHeatmap) {
8232
+ console.warn("Skipping an invalid detection heatmap.", error);
8233
+ warnedInvalidHeatmap = true;
8234
+ }
8235
+ continue;
8236
+ }
8237
+ }
8238
+ if (!texture)
8239
+ continue;
8240
+ const sprite = new options.Sprite({ texture });
8241
+ sprite.x = map.bounds.x - map.bounds.width / 2;
8242
+ sprite.y = map.bounds.y - map.bounds.height / 2;
8243
+ sprite.width = map.bounds.width;
8244
+ sprite.height = map.bounds.height;
8245
+ container.addChild(sprite);
8246
+ if (!temporaryTexture)
8247
+ activeKeys.add(key);
8248
+ positionedSprites.push({
8249
+ sprite,
8250
+ detectionId: detection.id,
8251
+ x: sprite.x,
8252
+ y: sprite.y,
8253
+ temporaryTexture,
8254
+ });
8255
+ }
8256
+ evict();
8257
+ lastFrame = frame;
8258
+ dirty = false;
8259
+ const frames = getBufferedDetectionTimelineFrameSnapshot(options.detectionTimeline);
8260
+ const frameIndex = frames.indexOf(frame);
8261
+ const nextFrame = frames[frameIndex + 1];
8262
+ if (frameIndex >= 0 && nextFrame) {
8263
+ const nextMaps = visibleMaps(nextFrame);
8264
+ const nextPixelCount = nextMaps.reduce((total, { map }) => total + map.width * map.height, 0);
8265
+ for (const { map, renderer } of nextMaps) {
8266
+ if (shouldPrepareAsync(map, nextPixelCount)) {
8267
+ wantedKeys.add(keyFor(map, renderer));
8268
+ enqueue(map, renderer);
8269
+ }
8270
+ }
8271
+ }
8272
+ cancelUnwantedJob();
8273
+ },
8274
+ destroy() {
8275
+ destroyed = true;
8276
+ clearCache();
8277
+ preparer?.destroy();
8278
+ container.destroy();
8279
+ },
8280
+ };
8281
+ }
8282
+
7666
8283
  const MAX_FOCUS_MASK_IDS = 16;
7667
8284
  /**
7668
8285
  * How far the media may travel before a cutout drawn for an earlier frame stops
@@ -12850,6 +13467,7 @@ function resolveAnnotationOverlayStyle(style) {
12850
13467
  var PixiSceneLayerKind;
12851
13468
  (function (PixiSceneLayerKind) {
12852
13469
  PixiSceneLayerKind["Media"] = "media";
13470
+ PixiSceneLayerKind["Heatmap"] = "heatmap";
12853
13471
  PixiSceneLayerKind["Mask"] = "mask";
12854
13472
  PixiSceneLayerKind["Box"] = "box";
12855
13473
  PixiSceneLayerKind["Vector"] = "vector";
@@ -12892,6 +13510,8 @@ function defaultLayerOrder(kind) {
12892
13510
  switch (kind) {
12893
13511
  case PixiSceneLayerKind.Media:
12894
13512
  return 0;
13513
+ case PixiSceneLayerKind.Heatmap:
13514
+ return 50;
12895
13515
  case PixiSceneLayerKind.Mask:
12896
13516
  return 100;
12897
13517
  case PixiSceneLayerKind.Box:
@@ -12981,6 +13601,7 @@ const RENDERED_PRESENTATION_FIELDS = [
12981
13601
  const FRAME_DRAW_TIMING_BUCKETS = {
12982
13602
  drawBox: "boxMs",
12983
13603
  drawFocus: "focusMs",
13604
+ drawHeatmap: "maskMs",
12984
13605
  drawInteraction: "interactionMs",
12985
13606
  drawInteractionPresentation: "interactionMs",
12986
13607
  drawLabel: "labelMs",
@@ -13020,7 +13641,9 @@ function observePixiContainerResize(container, resize) {
13020
13641
  */
13021
13642
  async function createPixiMediaScene(options) {
13022
13643
  const pixi = await import('pixi.js');
13023
- const { AlphaMask, Application, Assets, BlurFilter, BufferImageSource, CanvasSource, Container, defaultFilterVert, Filter, Graphics, ImageSource, Mesh, MeshGeometry, Rectangle, Sprite, Shader, Text, Texture, UniformGroup, } = pixi;
13644
+ const { AlphaMask, Application, Assets, BlurFilter, BufferImageSource, CanvasSource, Container, defaultFilterVert, Filter, Graphics, ImageSource, Mesh, MeshGeometry, PrepareSystem, Rectangle, Sprite, Shader, Text, Texture, UniformGroup, extensions, } = pixi;
13645
+ // The package build drops side-effect-only imports, so register explicitly.
13646
+ extensions.add(PrepareSystem);
13024
13647
  const { GifSprite } = await import('pixi.js/gif');
13025
13648
  const app = new Application();
13026
13649
  /**
@@ -13041,6 +13664,7 @@ async function createPixiMediaScene(options) {
13041
13664
  let currentOrientedBoxStyle = options.orientedBoxStyle ?? null;
13042
13665
  let currentPercentageBarStyle = options.percentageBarStyle ?? null;
13043
13666
  let currentRegionRenderers = options.regionRenderers;
13667
+ let currentHeatmapRenderers = options.heatmapRenderers ?? [];
13044
13668
  let regionMaskCoverageKey = resolveRegionMaskCoverageKey(currentRegionRenderers);
13045
13669
  let currentPolygonStyle = options.polygonStyle === undefined
13046
13670
  ? new BasePolygonStyle()
@@ -13061,7 +13685,7 @@ async function createPixiMediaScene(options) {
13061
13685
  let regionCoverageStyleCache = null;
13062
13686
  const annotationWindow = createPreparedAnnotationWindow({
13063
13687
  detectionTimeline: options.detectionTimeline,
13064
- getLayers: () => [maskLayer, polygonLayer].filter((layer) => layer !== undefined),
13688
+ getLayers: () => [maskLayer, polygonLayer, heatmapLayer].filter((layer) => layer !== undefined),
13065
13689
  getPlayheadMediaTime: () => currentMediaTime,
13066
13690
  renderPreparation: options.renderPreparation,
13067
13691
  });
@@ -13105,6 +13729,21 @@ async function createPixiMediaScene(options) {
13105
13729
  Graphics: options.editingEngine ? Graphics : undefined,
13106
13730
  resolveContextState,
13107
13731
  });
13732
+ let heatmapLayer;
13733
+ const ensureHeatmapLayer = () => (heatmapLayer ??= createPixiHeatmapLayer({
13734
+ CanvasSource,
13735
+ Container,
13736
+ ImageSource,
13737
+ Sprite,
13738
+ Texture,
13739
+ detectionTimeline: annotationDetectionTimeline,
13740
+ isVisible: (detection) => !resolveAnnotationStyleState(detection, currentVisibility).hidden,
13741
+ // A partial heatmap changes pixels before the all-ready token changes.
13742
+ onPreparedWindowChange: () => redrawAnnotationsNow(true),
13743
+ prepareTexture: (texture) => app.renderer.prepare.upload(texture),
13744
+ renderPreparation: options.renderPreparation,
13745
+ renderers: currentHeatmapRenderers,
13746
+ }));
13108
13747
  let polygonLayer = options.polygonStyle !== undefined &&
13109
13748
  currentPolygonStyle &&
13110
13749
  !options.editingEngine
@@ -13248,6 +13887,7 @@ async function createPixiMediaScene(options) {
13248
13887
  let polygonDisplay;
13249
13888
  let timelineContext;
13250
13889
  const mediaSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Media);
13890
+ const heatmapSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Heatmap);
13251
13891
  const maskSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Mask);
13252
13892
  const boxSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Box);
13253
13893
  const vectorSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Vector);
@@ -13259,6 +13899,7 @@ async function createPixiMediaScene(options) {
13259
13899
  const labelSlot = createPixiSceneLayerSlot(PixiSceneLayerKind.Label);
13260
13900
  const layerSlots = [
13261
13901
  mediaSlot,
13902
+ heatmapSlot,
13262
13903
  maskSlot,
13263
13904
  boxSlot,
13264
13905
  vectorSlot,
@@ -13354,6 +13995,7 @@ async function createPixiMediaScene(options) {
13354
13995
  fastTranslatedDetectionId = id;
13355
13996
  regionLayer.translateDetection(id, dx, dy);
13356
13997
  labelLayer?.translateDetection(id, dx, dy);
13998
+ heatmapLayer?.translateDetection(id, dx, dy);
13357
13999
  renderNow();
13358
14000
  });
13359
14001
  const redrawDetectionLayers = (id) => {
@@ -13390,6 +14032,7 @@ async function createPixiMediaScene(options) {
13390
14032
  fastTranslatedDetectionId = null;
13391
14033
  regionLayer.translateDetection(id, 0, 0);
13392
14034
  labelLayer?.translateDetection(id, 0, 0);
14035
+ heatmapLayer?.translateDetection(id, 0, 0);
13393
14036
  regionLayer.drawFrame(currentMediaTime, viewportScale);
13394
14037
  labelLayer?.drawFrame(currentMediaTime, viewportScale);
13395
14038
  renderNow();
@@ -13567,6 +14210,7 @@ async function createPixiMediaScene(options) {
13567
14210
  stagingTexture?.update();
13568
14211
  };
13569
14212
  const presentLayers = {
14213
+ drawHeatmap: (mediaTime) => heatmapLayer?.drawFrame(mediaTime),
13570
14214
  advanceFocus: advanceFocusAnimation,
13571
14215
  drawAnnotationOverlay: (mediaTime) => drawAnnotationOverlay(mediaTime, mediaTime * MILLISECONDS_PER_SECOND),
13572
14216
  drawBox: (mediaTime) => boxLayer.drawFrame(mediaTime, viewportScale),
@@ -13707,6 +14351,9 @@ async function createPixiMediaScene(options) {
13707
14351
  if (!retainedBoxes)
13708
14352
  boxLayer.attachGraphics(boxes);
13709
14353
  mediaSlot.setDisplay(mediaSprite);
14354
+ if (currentHeatmapRenderers.length > 0) {
14355
+ heatmapSlot.setDisplay(ensureHeatmapLayer().createContainer());
14356
+ }
13710
14357
  boxSlot.setDisplay(retainedBoxes ?? boxes);
13711
14358
  vectorDisplay = new Container();
13712
14359
  attachPolygonLayerDisplay();
@@ -13953,6 +14600,7 @@ async function createPixiMediaScene(options) {
13953
14600
  if (presentation.visibility !== undefined) {
13954
14601
  const maskVisibilityChanged = !canReuseMaskVisibilityArtifacts(currentVisibility, presentation.visibility);
13955
14602
  currentVisibility = presentation.visibility;
14603
+ heatmapLayer?.invalidate();
13956
14604
  boxLayer.invalidate();
13957
14605
  vectorLayer.setStyles({});
13958
14606
  labelLayer?.setLabelStyle(currentLabelStyle);
@@ -14006,6 +14654,17 @@ async function createPixiMediaScene(options) {
14006
14654
  }
14007
14655
  currentRegionRenderers =
14008
14656
  presentation.renderers?.filter((renderer) => renderer.kind === "region") ?? [];
14657
+ currentHeatmapRenderers =
14658
+ presentation.renderers?.filter((renderer) => renderer.kind === "heatmap") ?? [];
14659
+ if (currentHeatmapRenderers.length > 0) {
14660
+ const layer = ensureHeatmapLayer();
14661
+ layer.setRenderers(currentHeatmapRenderers);
14662
+ heatmapSlot.setDisplay(layer.createContainer());
14663
+ syncSceneChildren();
14664
+ }
14665
+ else {
14666
+ heatmapLayer?.setRenderers([]);
14667
+ }
14009
14668
  const nextRegionMaskCoverageKey = resolveRegionMaskCoverageKey(currentRegionRenderers);
14010
14669
  const regionMaskCoverageChanged = nextRegionMaskCoverageKey !== regionMaskCoverageKey;
14011
14670
  regionMaskCoverageKey = nextRegionMaskCoverageKey;
@@ -14105,6 +14764,7 @@ async function createPixiMediaScene(options) {
14105
14764
  labelLayer?.destroy();
14106
14765
  vectorLayer.destroy();
14107
14766
  regionLayer.destroy();
14767
+ heatmapLayer?.destroy();
14108
14768
  maskBrushPreview?.destroy();
14109
14769
  unsubscribeFastTranslate?.();
14110
14770
  unsubscribeEditingState?.();
@@ -14584,15 +15244,18 @@ async function createPixiMediaScene(options) {
14584
15244
  redrawAnnotationsNow();
14585
15245
  }
14586
15246
  /** Redraws the frame on screen whether or not its readiness moved. */
14587
- function redrawAnnotationsNow() {
15247
+ function redrawAnnotationsNow(forceRender = false) {
14588
15248
  if (isPresenting || isDestroyed) {
14589
15249
  return;
14590
15250
  }
14591
15251
  const { boxState, regionState } = drawAnnotationFrame(currentMediaTime);
14592
- if (!frameChannel ||
14593
- !renderScheduler.renderOnChange(describeSceneRender())) {
15252
+ if (!frameChannel)
15253
+ return;
15254
+ const signature = describeSceneRender();
15255
+ if (forceRender)
15256
+ renderScheduler.render(signature);
15257
+ else if (!renderScheduler.renderOnChange(signature))
14594
15258
  return;
14595
- }
14596
15259
  options.onPresentationUpdate?.(createPresentedSampleState(currentMediaTime, boxState, regionState));
14597
15260
  }
14598
15261
  function drawAnnotationFrame(mediaTime) {