supervision 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editing.d.ts +1 -1
- package/dist/editing.d.ts.map +1 -1
- package/dist/index.js +527 -68
- package/dist/index.js.map +1 -1
- package/dist/mask-preparation.worker.js +130 -4
- package/dist/mask-preparation.worker.js.map +1 -1
- package/dist/render-preparation/mask-frame-artifact.d.ts +10 -0
- package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-compositor.d.ts +2 -1
- package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-preparer.d.ts.map +1 -1
- package/dist/render-preparation/mask-outline-paths.d.ts +3 -0
- package/dist/render-preparation/mask-outline-paths.d.ts.map +1 -0
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +2 -1
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
- package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
- package/dist/renderers/detection-render-order.d.ts +4 -0
- package/dist/renderers/detection-render-order.d.ts.map +1 -0
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-scene.d.ts +8 -1
- package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
- package/dist/renderers/pixi-box-layer.d.ts +1 -0
- package/dist/renderers/pixi-box-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-interaction-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-brush-preview.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-layer.d.ts +4 -2
- package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-screen-stroke.d.ts +17 -0
- package/dist/renderers/pixi-mask-screen-stroke.d.ts.map +1 -0
- package/dist/renderers/pixi-media-scene.d.ts +5 -0
- package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
- package/dist/renderers/pixi-region-layer.d.ts +2 -0
- package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-vector-layer.d.ts +1 -0
- package/dist/renderers/pixi-vector-layer.d.ts.map +1 -1
- package/dist/tracking.worker.js.map +1 -1
- package/dist/types/media-renderer.d.ts +21 -6
- package/dist/types/media-renderer.d.ts.map +1 -1
- package/dist/types/media-session.d.ts +2 -0
- package/dist/types/media-session.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.d.ts +1 -1
- package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.js +44 -11
- package/node_modules/supervision-js-core/dist/index.js.map +1 -1
- package/node_modules/supervision-js-core/dist/interactions/annotation-editing-engine.d.ts +2 -2
- package/node_modules/supervision-js-core/dist/interactions/annotation-editing-engine.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/interactions/detection-picker.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/interactions/viewport-controller.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/detections.d.ts +2 -1
- package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/editing.d.ts +7 -0
- package/node_modules/supervision-js-core/dist/types/editing.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/interaction.d.ts +11 -0
- package/node_modules/supervision-js-core/dist/types/interaction.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/mask-style.d.ts +3 -0
- package/node_modules/supervision-js-core/dist/types/mask-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/viewport.d.ts +3 -0
- package/node_modules/supervision-js-core/dist/types/viewport.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId,
|
|
1
|
+
import { copySortedDetectionFrames, detectionFrameOverlapsRange, sortedDetectionFrames, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, createDepthColormapLut, parseDepthManifest, validateDepthMap, resolveDepthFrameFile, PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS, decodeCompressedRleMask, decodeCompressedRleCounts, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, depthPreviewTopCode, resolveDepthColorMapping, resolveDepthQuantity, computeDepthPercentileRange, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, rebaseDetectionPickToFrame, pickDetectionAtPoint, createDetectionPickKey, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, MAX_ID_MASK_STROKE_WIDTH, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, resolveIdMaskStrokeTexels, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
|
|
2
2
|
export { BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BaseOrientedBoxStyle, BasePercentageBarStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DepthColormap, DepthMapKind, DepthPreviewLevels, DepthQuantity, DepthSampling, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, PercentageBarPlacement, PlaybackGateReach, RegionRendererComposeMode, RegionRendererCoverageKind, RegionRendererMediaEffectKind, RegionRendererRegionKind, RegionRendererSizeSpace, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, TrackingGeometry, annotationRendererKinds, annotationRenderers, computeDepthPercentileRange, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createWritableDetectionFrameSource, depthColormapColors, detectionPostProcessors, normalizeDetectionClassName, parseDepthManifest, pickDetectionAtPoint, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, readDepthAt, resolveDetectionClassColorStyle } from 'supervision-js-core';
|
|
3
3
|
import { c as createMediaFrameClock, M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-DOFsuGS4.js';
|
|
4
4
|
export { a as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-DOFsuGS4.js';
|
|
@@ -3113,7 +3113,7 @@ function assertMediaAspect(map, media) {
|
|
|
3113
3113
|
}
|
|
3114
3114
|
}
|
|
3115
3115
|
|
|
3116
|
-
const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n function decodeCompressedRleMask(mask) {\n if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {\n throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);\n }\n const data = new Uint8Array(mask.width * mask.height);\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n const isForeground = index % 2 === 1;\n if (isForeground) {\n for (let runOffset = 0; runOffset < runLength; runOffset += 1) {\n const maskOffset = offset + runOffset;\n const x = Math.floor(maskOffset / mask.height);\n const y = maskOffset % mask.height;\n const rowMajorOffset = y * mask.width + x;\n if (rowMajorOffset < data.length) {\n data[rowMajorOffset] = 1;\n }\n }\n }\n offset += runLength;\n }\n return {\n data,\n height: mask.height,\n width: mask.width,\n };\n }\n function decodeCompressedRleCounts(counts) {\n const decoded = [];\n let index = 0;\n while (index < counts.length) {\n let value = 0;\n let shift = 0;\n let charCode;\n do {\n charCode = counts.charCodeAt(index) - 48;\n index += 1;\n value |= (charCode & 0x1f) << shift;\n shift += 5;\n } while (charCode & 0x20);\n if (charCode & 0x10) {\n value |= -1 << shift;\n }\n if (decoded.length > 2) {\n value += decoded[decoded.length - 2] ?? 0;\n }\n decoded.push(value);\n }\n return decoded;\n }\n function encodeCompressedRleCounts(counts) {\n return counts\n .map((count, index) => {\n let value = index > 2 ? count - counts[index - 2] : count;\n let encoded = \"\";\n let more = true;\n while (more) {\n let charCode = value & 0x1f;\n value >>= 5;\n more = !((value === 0 && (charCode & 0x10) === 0) ||\n (value === -1 && (charCode & 0x10) !== 0));\n if (more) {\n charCode |= 0x20;\n }\n encoded += String.fromCharCode(charCode + 48);\n }\n return encoded;\n })\n .join(\"\");\n }\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n function encodeBinaryMask(data, width, height) {\n assertMaskDimensions(data, width, height);\n const runs = [];\n let currentValue = 0;\n let runLength = 0;\n for (let x = 0; x < width; x += 1) {\n for (let y = 0; y < height; y += 1) {\n const value = data[y * width + x] ? 1 : 0;\n if (value === currentValue) {\n runLength += 1;\n }\n else {\n runs.push(runLength);\n currentValue = value;\n runLength = 1;\n }\n }\n }\n runs.push(runLength);\n return {\n counts: encodeCompressedRleCounts(runs),\n encoding: DetectionMaskEncoding.CompressedRle,\n height,\n width,\n };\n }\n function assertMaskSize(width, height) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(\"Mask width must be a positive integer.\");\n }\n if (!Number.isInteger(height) || height <= 0) {\n throw new Error(\"Mask height must be a positive integer.\");\n }\n }\n function assertMaskDimensions(data, width, height) {\n assertMaskSize(width, height);\n if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\n\n function centerRectToTopLeftRect(rect) {\n return {\n height: rect.height,\n width: rect.width,\n x: rect.x - rect.width / 2,\n y: rect.y - rect.height / 2,\n };\n }\n function getPointsRect(points) {\n if (points.length === 0) {\n return undefined;\n }\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n minY = Math.min(minY, point.y);\n maxX = Math.max(maxX, point.x);\n maxY = Math.max(maxY, point.y);\n }\n return {\n height: maxY - minY,\n width: maxX - minX,\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n };\n }\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"OrientedBox\"] = \"orientedBox\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var PercentageBarPlacement;\n (function (PercentageBarPlacement) {\n PercentageBarPlacement[\"Top\"] = \"top\";\n PercentageBarPlacement[\"Bottom\"] = \"bottom\";\n PercentageBarPlacement[\"InsideTop\"] = \"insideTop\";\n PercentageBarPlacement[\"InsideBottom\"] = \"insideBottom\";\n })(PercentageBarPlacement || (PercentageBarPlacement = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n\n /**\n * What the stored values of a depth map measure.\n *\n * Every kind stores 0 where there is no depth.\n */\n const DepthMapKind = {\n /** Stereo disparity in pixels of the map. Larger is nearer. */\n DisparityPx: \"disparity_px\",\n /** Metric depth along the optical axis in metres. Smaller is nearer. */\n DepthM: \"depth_m\",\n /**\n * Unitless inverse depth up to an unknown scale and shift, as monocular\n * models produce it. Larger is nearer; it has no metric depth.\n */\n RelativeInverse: \"relative_inverse\",\n };\n /**\n * The quantity a depth renderer colours.\n *\n * Disparity is inverse depth, so it spends colour on near detail the way\n * stereo measures it. Either way the nearest end of the range is the warm or\n * bright end of the colour table.\n */\n const DepthQuantity = {\n Disparity: \"disparity\",\n Depth: \"depth\",\n };\n const DepthColormap = {\n Turbo: \"turbo\",\n Viridis: \"viridis\",\n Cividis: \"cividis\",\n Inferno: \"inferno\",\n Magma: \"magma\",\n Grayscale: \"grayscale\",\n };\n /** How a depth map is sampled where one map pixel does not cover one media pixel. */\n const DepthSampling = {\n /** Nearest when the map is at least media size, edge-aware otherwise. */\n Auto: \"auto\",\n Nearest: \"nearest\",\n /**\n * Bilinear over valid neighbours only, falling back to the nearest one\n * across a depth edge, so objects never bleed into the background.\n */\n EdgeAware: \"edge-aware\",\n };\n /**\n * The luma range a depth preview video's codes are written in.\n *\n * TV range is what browsers' hardware decoders return as written; a decoder\n * may squeeze full range into TV range or move codes converting it to RGB.\n */\n const DepthPreviewLevels = {\n /** Every code from 0 to 255; the top code is 255. */\n Full: \"full\",\n /** Limited range: black is 16 and the top code is 235. */\n Tv: \"tv\",\n };\n new Set(Object.values(DepthColormap));\n new Set(Object.values(DepthQuantity));\n new Set(Object.values(DepthSampling));\n function rasterizePolygonToMask(points, dimensions) {\n const data = createEmptyMask(dimensions);\n if (points.length < 3) {\n return data;\n }\n const bounds = centerRectToTopLeftRect(getPointsRect(points));\n const startY = Math.max(0, Math.floor(bounds.y));\n const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));\n for (let y = startY; y <= endY; y += 1) {\n const scanY = y + 0.5;\n const intersections = [];\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index];\n const next = points[(index + 1) % points.length];\n if ((current.y <= scanY && next.y > scanY) ||\n (next.y <= scanY && current.y > scanY)) {\n const ratio = (scanY - current.y) / (next.y - current.y);\n intersections.push(current.x + ratio * (next.x - current.x));\n }\n }\n intersections.sort((left, right) => left - right);\n for (let index = 0; index < intersections.length - 1; index += 2) {\n const left = Math.max(0, Math.round(intersections[index]));\n const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));\n for (let x = left; x < right; x += 1) {\n data[y * dimensions.width + x] = 1;\n }\n }\n }\n return data;\n }\n function createEmptyMask(dimensions) {\n if (!Number.isInteger(dimensions.width) ||\n dimensions.width <= 0 ||\n !Number.isInteger(dimensions.height) ||\n dimensions.height <= 0) {\n throw new Error(\"Media dimensions must be positive integers.\");\n }\n return new Uint8Array(dimensions.width * dimensions.height);\n }\n\n /**\n * Palette slots an id raster can name, one of them the background. A GLSL\n * fragment stage is guaranteed only 224 uniform vectors, and a palette entry\n * costs 2.25 of them, so this stays a multiple of four and well inside that\n * budget; past it a frame draws from the RGBA composite instead.\n */\n const MAX_ID_MASK_PALETTE_ENTRIES = 80;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions, options = {}) {\n if (instructions.length === 0) {\n return undefined;\n }\n /* Pixel-grid dimensions use integer parts throughout allocation and RLE\n traversal. */\n const maskWidth = Math.floor(Math.max(...instructions.map(({ mask }) => mask.width)));\n const maskHeight = Math.floor(Math.max(...instructions.map(({ mask }) => mask.height)));\n const width = resolveRasterWidth(maskWidth, options.maxWidth);\n const height = width === maskWidth\n ? maskHeight\n : Math.max(1, Math.round((width * maskHeight) / maskWidth));\n const scaledAxes = width === maskWidth\n ? undefined\n : createScaledMaskAxes({ height, maskHeight, maskWidth, width });\n const data = new Uint8Array(new ArrayBuffer(width * height));\n const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n let hasStroke = false;\n let maxStrokeWidth = 0;\n for (const instruction of instructions) {\n const detectionMaskId = resolveIdMaskPaletteId(instruction.detectionIndex);\n if (detectionMaskId === undefined) {\n return undefined;\n }\n writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = resolveIdMaskStrokeTexels(instruction.stroke.width, maskWidth, width);\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);\n writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n if (scaledAxes) {\n writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);\n }\n else {\n writeMaskRuns(data, width, instruction.mask, detectionMaskId);\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n sourceWidth: maskWidth,\n strokePalette,\n strokeWidths,\n width,\n };\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let frameOffset = y * frameWidth + x;\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[frameOffset] = detectionMaskId;\n y += 1;\n frameOffset += frameWidth;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n frameOffset = x;\n }\n }\n maskOffset += runLength;\n }\n }\n /**\n * The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands\n * in, so a mask keeps every part of itself the smaller raster can hold and\n * gains up to a texel at its edges.\n */\n function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n const stride = axes.stride;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let remaining = runLength;\n /* A run walks down one column and wraps into the next, so it splits into\n column-sized segments. Within a segment the destination offset only ever\n steps by the row pitch or stays put: createMaskAxisMap builds\n `rows[i] = min(height - 1, floor(i * scale)) * width` with scale <= 1, so\n consecutive entries differ by exactly 0 or exactly `width`. Walking the\n destination directly therefore touches the same bytes the per-pixel loop\n touched, and drops the bounds test, the wrap test and one typed-array\n load from every source pixel. Where the scale collapses several source\n rows onto one destination row, this writes that byte once instead of\n once per source row. */\n while (remaining > 0 && x < maskWidth) {\n const span = Math.min(remaining, maskHeight - y);\n const column = axes.columns[x];\n const first = axes.rows[y];\n const last = axes.rows[y + span - 1];\n for (let offset = first; offset <= last; offset += stride) {\n data[offset + column] = detectionMaskId;\n }\n remaining -= span;\n y += span;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n }\n }\n maskOffset += runLength;\n }\n }\n function resolveRasterWidth(maskWidth, maxWidth) {\n return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth\n ? Math.max(1, Math.floor(maxWidth))\n : maskWidth;\n }\n /**\n * Converts a source-grid stroke width into raster texels. Fractional widths\n * let coverage shaders draw a border within a texel. The ceiling bounds the\n * neighbourhood the shaders scan.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {\n const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;\n return Math.min(strokeWidth * scale, MAX_ID_MASK_STROKE_WIDTH);\n }\n function createScaledMaskAxes(frame) {\n return {\n columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),\n rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),\n stride: frame.width,\n };\n }\n /** Rows hold their destination offset, so writing a run is two lookups. */\n function createMaskAxisMap(sourceLength, targetLength, stride) {\n const map = new Int32Array(sourceLength);\n const scale = targetLength / sourceLength;\n for (let index = 0; index < sourceLength; index += 1) {\n map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;\n }\n return map;\n }\n /**\n * The palette slot a detection index names, or undefined when the palette has\n * no slot for it. Slot zero is the background, so the ids start at one and the\n * last detection the palette can name sits one index below the last slot.\n */\n function resolveIdMaskPaletteId(detectionIndex) {\n const detectionMaskId = detectionIndex + 1;\n return detectionMaskId > 0 && detectionMaskId < MAX_ID_MASK_PALETTE_ENTRIES\n ? detectionMaskId\n : undefined;\n }\n function writeIdMaskPaletteEntry(palette, detectionMaskId, color, alpha) {\n const offset = detectionMaskId * 4;\n palette[offset] = ((color >> 16) & 0xff) / 255;\n palette[offset + 1] = ((color >> 8) & 0xff) / 255;\n palette[offset + 2] = (color & 0xff) / 255;\n palette[offset + 3] = Math.max(0, Math.min(alpha, 1));\n }\n new Set(Object.values(DepthPreviewLevels));\n new Set(Object.values(DepthMapKind));\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read, or was refused before any read. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, and a gate that stops one has to bound its own wait\n * or a producer nothing answers for never runs again.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n /**\n * The picture is waiting on media the source has not handed over yet, which\n * is the bytes and their decode rather than anything downstream of them.\n * `PlaybackBuffering` is what a transport reports once it has already\n * stopped; this is reported from the read itself, including the reads a seek\n * makes while the transport still reads as paused.\n */\n MediaSessionActivityKind[\"MediaSourceReading\"] = \"mediaSourceReading\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n /**\n * The render-preparation gate held the picture for artifacts that never\n * arrived and let it go. Nothing is blocked: the frames reaching the screen\n * are the ones whose artifacts were given up on. What is waited on is\n * preparation finishing another frame, which is what lets the gate hold the\n * picture again; `RenderPreparing` covers the holds that are running.\n */\n MediaSessionActivityKind[\"RenderPreparationAbandoned\"] = \"renderPreparationAbandoned\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n /** Builds compact, independent alpha crops so overlapping masks stay exact. */\n function createRegionMaskCoverageFrame(instructions) {\n const entries = [];\n for (const instruction of instructions) {\n if (!instruction.regionCoverageMask) {\n continue;\n }\n const entry = cropCoverageMask(instruction.detectionIndex, decodeCompressedRleMask(instruction.regionCoverageMask));\n if (entry) {\n entries.push(entry);\n }\n }\n return entries.length > 0 ? { entries } : undefined;\n }\n function cropCoverageMask(detectionIndex, decodedMask) {\n let minX = decodedMask.width;\n let minY = decodedMask.height;\n let maxX = -1;\n let maxY = -1;\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n if (maxX < minX || maxY < minY) {\n return undefined;\n }\n const width = maxX - minX + 1;\n const height = maxY - minY + 1;\n const data = new Uint8Array(new ArrayBuffer(width * height));\n for (let y = minY; y <= maxY; y += 1) {\n for (let x = minX; x <= maxX; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n data[(y - minY) * width + (x - minX)] = 255;\n }\n }\n return { data, detectionIndex, height, width, x: minX, y: minY };\n }\n function compositeMaskFrame(instructions) {\n const maskInstructions = materializeMaskInstructions(instructions);\n if (maskInstructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));\n const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));\n const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));\n for (const instruction of maskInstructions) {\n compositeInstruction(data, width, instruction);\n }\n return { data, height, width };\n }\n function createIdMaskRasterFrame(instructions, maxRasterWidth) {\n try {\n return createIdMaskFrame(materializeMaskInstructions(instructions), {\n maxWidth: maxRasterWidth,\n });\n }\n catch {\n // The id raster is the fast path, not the only one: answering with nothing\n // puts the caller on the RGBA composite, which draws the same picture.\n return undefined;\n }\n }\n /**\n * Whether the palette has a slot for every detection this frame masks. An id\n * names a detection by its index in the frame, so one masked detection past the\n * last slot leaves the whole frame without an id raster however few masks it\n * carries.\n */\n function canIdMaskPaletteNameFrame(instructions) {\n return instructions.every((instruction) => instruction.visible === false ||\n resolveIdMaskPaletteId(instruction.detectionIndex) !== undefined);\n }\n function createIdMaskPlane(instructions, maxRasterWidth) {\n // The plane carries the ids a failed cook could not. A frame the palette\n // cannot name has none to carry, and cooking it again reaches the same\n // refusal after rasterizing every mask a second time.\n if (!canIdMaskPaletteNameFrame(instructions)) {\n return undefined;\n }\n const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);\n return frame\n ? { data: frame.data, height: frame.height, width: frame.width }\n : undefined;\n }\n function compositeInstruction(rgba, canvasWidth, instruction) {\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);\n if (instruction.stroke && bounds) {\n compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);\n }\n }\n function materializeMaskInstructions(instructions) {\n return instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => {\n if (instruction.mask) {\n return instruction;\n }\n const { height, points, width } = instruction.polygon;\n return {\n alpha: instruction.alpha,\n color: instruction.color,\n detectionIndex: instruction.detectionIndex,\n mask: encodeBinaryMask(rasterizePolygonToMask(points, { height, width }), width, height),\n stroke: instruction.stroke,\n };\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function compositeMaskFill(rgba, canvasWidth, mask, fill) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n const maskArea = maskWidth * maskHeight;\n let maskOffset = 0;\n let minX = maskWidth;\n let minY = maskHeight;\n let maxX = -1;\n let maxY = -1;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let columnX = Math.floor(maskOffset / maskHeight);\n let columnY = maskOffset - columnX * maskHeight;\n for (let step = 0; step < runLength; step += 1) {\n // A run can outlast the columns the mask has; the pixel it then names\n // is wherever that column-major offset lands when read back row-major.\n const rowMajorOffset = columnY * maskWidth + columnX;\n if (rowMajorOffset < maskArea) {\n const x = rowMajorOffset % maskWidth;\n const y = (rowMajorOffset - x) / maskWidth;\n writePixel(rgba, canvasWidth, x, y, fill);\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n columnY += 1;\n if (columnY === maskHeight) {\n columnY = 0;\n columnX += 1;\n }\n }\n maskOffset += runLength;\n }\n return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {\n const width = Math.round(stroke.width);\n if (width <= 0) {\n return;\n }\n const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);\n for (let y = bounds.minY; y <= bounds.maxY; y += 1) {\n for (let x = bounds.minX; x <= bounds.maxX; x += 1) {\n if (!isMaskPixel(decodedMask, x, y) ||\n !isBoundaryPixel(decodedMask, x, y)) {\n continue;\n }\n for (let offsetY = -width; offsetY <= width; offsetY += 1) {\n for (let offsetX = -width; offsetX <= width; offsetX += 1) {\n const strokeX = x + offsetX;\n const strokeY = y + offsetY;\n if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||\n isMaskPixel(decodedMask, strokeX, strokeY)) {\n continue;\n }\n writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);\n }\n }\n }\n }\n }\n function isBoundaryPixel(mask, x, y) {\n for (let offsetY = -1; offsetY <= 1; offsetY += 1) {\n for (let offsetX = -1; offsetX <= 1; offsetX += 1) {\n if (offsetX === 0 && offsetY === 0) {\n continue;\n }\n const neighborX = x + offsetX;\n const neighborY = y + offsetY;\n if (isOutsideMaskBounds(mask, neighborX, neighborY) ||\n !isMaskPixel(mask, neighborX, neighborY)) {\n return true;\n }\n }\n }\n return false;\n }\n function isMaskPixel(mask, x, y) {\n return mask.data[y * mask.width + x] === 1;\n }\n function isOutsideMaskBounds(mask, x, y) {\n return x < 0 || y < 0 || x >= mask.width || y >= mask.height;\n }\n function resolveRgbaColor(color, alpha) {\n return {\n alpha: Math.round(Math.max(0, Math.min(alpha, 1)) * 255),\n blue: color & 0xff,\n green: (color >> 8) & 0xff,\n red: (color >> 16) & 0xff,\n };\n }\n function writePixel(rgba, canvasWidth, x, y, color) {\n const rgbaOffset = (y * canvasWidth + x) * 4;\n rgba[rgbaOffset] = color.red;\n rgba[rgbaOffset + 1] = color.green;\n rgba[rgbaOffset + 2] = color.blue;\n rgba[rgbaOffset + 3] = color.alpha;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"IdMask\"] = \"idMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\n /**\n * How an id raster is laid out for the texture it is uploaded into. WebGPU\n * takes any bytesPerRow, so ids go up one byte per pixel. WebGL aligns every\n * uploaded row to four bytes and rejects a single-channel upload whose width\n * is not a multiple of four, which is what the four-channel layout is for.\n */\n var IdMaskTextureFormat;\n (function (IdMaskTextureFormat) {\n IdMaskTextureFormat[\"R8\"] = \"r8unorm\";\n IdMaskTextureFormat[\"Rgba8\"] = \"rgba8unorm\";\n })(IdMaskTextureFormat || (IdMaskTextureFormat = {}));\n\n const DEFAULT_STOPS = [\n { position: 0, color: 0xffd600 },\n { position: 0.35, color: 0xffd600 },\n { position: 0.7, color: 0xff7a00 },\n { position: 1, color: 0xdc1e1e },\n ];\n const MAX_HEATMAP_PIXELS = 16_777_216;\n const MAX_HEATMAP_SIDE = 8_192;\n /** Turns semantic scores into an RGBA raster; no palette enters cold data. */\n function colorizeHeatmap(map, renderer) {\n const { bounds, width, height, values } = map;\n if (!Number.isFinite(bounds?.x) ||\n !Number.isFinite(bounds?.y) ||\n !Number.isFinite(bounds?.width) ||\n !Number.isFinite(bounds?.height) ||\n bounds.width <= 0 ||\n bounds.height <= 0 ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width <= 0 ||\n height <= 0 ||\n width > MAX_HEATMAP_SIDE ||\n height > MAX_HEATMAP_SIDE ||\n width * height > MAX_HEATMAP_PIXELS ||\n !(Array.isArray(values) ||\n values instanceof Float32Array ||\n values instanceof Float64Array ||\n values instanceof Uint16Array) ||\n values.length !== width * height) {\n throw new RangeError(\"Heatmap bounds and dimensions must be valid, match row-major values, and fit the raster limit.\");\n }\n const scale = map.valueScale ?? 1;\n const threshold = (map.threshold ?? 0) * (renderer.thresholdScale ?? 1);\n const maximumScore = renderer.maximumScore ?? 1;\n const opacity = clamp01(renderer.opacity ?? 1);\n const minimumAlpha = clamp01(renderer.minimumAlpha ?? 0);\n const stops = renderer.colorStops ?? DEFAULT_STOPS;\n if (!Number.isFinite(scale) ||\n scale <= 0 ||\n !Number.isFinite(threshold) ||\n !Number.isFinite(maximumScore) ||\n maximumScore <= 0 ||\n !Number.isFinite(renderer.thresholdScale ?? 1) ||\n (renderer.thresholdScale ?? 1) < 0 ||\n !Number.isFinite(renderer.opacity ?? 1) ||\n !Number.isFinite(renderer.minimumAlpha ?? 0) ||\n stops.length === 0 ||\n stops.some((stop, index) => !Number.isFinite(stop.position) ||\n stop.position < 0 ||\n stop.position > 1 ||\n !Number.isInteger(stop.color) ||\n stop.color < 0 ||\n stop.color > 0xffffff ||\n (index > 0 && stop.position < stops[index - 1].position))) {\n throw new RangeError(\"Invalid heatmap scale, threshold, or color stops.\");\n }\n const rgba = new Uint8ClampedArray(new ArrayBuffer(values.length * 4));\n if (maximumScore <= threshold || opacity === 0)\n return rgba;\n for (let index = 0; index < values.length; index += 1) {\n const score = values[index] * scale;\n if (!Number.isFinite(score) || score <= threshold)\n continue;\n const color = interpolateColor(stops, clamp01(score / maximumScore));\n const alphaIntensity = clamp01((score - threshold) / (maximumScore - threshold));\n const offset = index * 4;\n rgba[offset] = color >> 16;\n rgba[offset + 1] = (color >> 8) & 0xff;\n rgba[offset + 2] = color & 0xff;\n rgba[offset + 3] = Math.round(255 * opacity * (minimumAlpha + (1 - minimumAlpha) * alphaIntensity));\n }\n return rgba;\n }\n function interpolateColor(stops, value) {\n const upperIndex = stops.findIndex((stop) => stop.position >= value);\n if (upperIndex === -1)\n return stops[stops.length - 1].color;\n if (upperIndex === 0)\n return stops[0].color;\n const left = stops[upperIndex - 1];\n const right = stops[upperIndex];\n const span = right.position - left.position;\n const ratio = span === 0 ? 1 : (value - left.position) / span;\n const channel = (shift) => Math.round(((left.color >> shift) & 0xff) * (1 - ratio) +\n ((right.color >> shift) & 0xff) * ratio);\n return (channel(16) << 16) | (channel(8) << 8) | channel(0);\n }\n function clamp01(value) {\n return Math.max(0, Math.min(1, value));\n }\n\n var HeatmapPreparationWorkerMessageType;\n (function (HeatmapPreparationWorkerMessageType) {\n HeatmapPreparationWorkerMessageType[\"Prepare\"] = \"heatmap-prepare\";\n HeatmapPreparationWorkerMessageType[\"Complete\"] = \"heatmap-complete\";\n HeatmapPreparationWorkerMessageType[\"Error\"] = \"heatmap-error\";\n })(HeatmapPreparationWorkerMessageType || (HeatmapPreparationWorkerMessageType = {}));\n\n var MaskPreparationWorkerMessageType;\n (function (MaskPreparationWorkerMessageType) {\n MaskPreparationWorkerMessageType[\"Complete\"] = \"complete\";\n MaskPreparationWorkerMessageType[\"Empty\"] = \"empty\";\n MaskPreparationWorkerMessageType[\"Error\"] = \"error\";\n MaskPreparationWorkerMessageType[\"Prepare\"] = \"prepare\";\n })(MaskPreparationWorkerMessageType || (MaskPreparationWorkerMessageType = {}));\n\n /**\n * Messages between the depth source and the render-preparation worker, which\n * decodes depth and confidence PNGs, and copies decoded preview frames' luma,\n * off the main thread. Internal: the worker script is a deployment asset, and\n * its protocol is not public.\n */\n var DepthPreparationWorkerMessageType;\n (function (DepthPreparationWorkerMessageType) {\n DepthPreparationWorkerMessageType[\"Decode\"] = \"depth-decode\";\n DepthPreparationWorkerMessageType[\"Complete\"] = \"depth-decode-complete\";\n DepthPreparationWorkerMessageType[\"Error\"] = \"depth-decode-error\";\n DepthPreparationWorkerMessageType[\"PreviewLuma\"] = \"depth-preview-luma\";\n DepthPreparationWorkerMessageType[\"PreviewLumaComplete\"] = \"depth-preview-luma-complete\";\n })(DepthPreparationWorkerMessageType || (DepthPreparationWorkerMessageType = {}));\n\n /*\n * Grayscale PNG decoding for depth maps and their confidence planes.\n *\n * Every image path a browser offers (<img>, createImageBitmap, ImageDecoder,\n * canvas) hands back 8 bits per channel, so a 16-bit depth PNG is decoded\n * here: the browser's own zlib inflates the IDAT stream, and the row filters\n * are undone in JavaScript.\n *\n * This module imports nothing, so the render-preparation worker, the main\n * thread and the depth benchmark all run the same code.\n */\n const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];\n const COLOR_TYPE_GRAY = 0;\n /** 16384 x 16384, beyond any GPU texture; a header claiming more is refused. */\n const MAX_PNG_PIXELS = 16_384 * 16_384;\n async function decodePng16(bytes, options = {}) {\n const header = readPngHeader(bytes, 16);\n const rowBytes = header.width * 2 + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint16Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray16(raw, header.width, header.height, values);\n return {\n height: header.height,\n paddedUpload: options.padRowsForWebGl && header.width % 2 === 1\n ? padRows(values, header.width, header.height)\n : undefined,\n values,\n width: header.width,\n };\n }\n async function decodePng8Gray(bytes, options = {}) {\n const header = readPngHeader(bytes, 8);\n const rowBytes = header.width + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint8Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray8(raw, header.width, header.height, values);\n return { height: header.height, values, width: header.width };\n }\n /**\n * Inflates with the platform's `DecompressionStream(\"deflate\")` (zlib\n * framing, which is what PNG uses) straight into one buffer of the expected\n * size, so no second copy joins the output chunks.\n */\n async function inflateWithDecompressionStream(chunks, byteLength) {\n const stream = new DecompressionStream(\"deflate\");\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n // Writers split IDAT into many small chunks (libpng's default is 8 KiB).\n // One write of the joined stream costs a copy of the compressed bytes and\n // saves a stream round trip per chunk.\n const zlib = chunks.length === 1 ? chunks[0] : joinChunks(chunks);\n const writing = (async () => {\n await writer.write(zlib);\n await writer.close();\n })();\n const output = new Uint8Array(byteLength);\n let offset = 0;\n // A corrupt stream rejects both sides; the reader's error is the one thrown.\n writing.catch(() => undefined);\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done)\n break;\n if (offset + value.byteLength > byteLength) {\n throw new RangeError(\"PNG image data is longer than its header says.\");\n }\n output.set(value, offset);\n offset += value.byteLength;\n }\n await writing;\n }\n catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error instanceof RangeError\n ? error\n : new RangeError(`PNG image data does not inflate: ${error instanceof Error ? error.message : String(error)}`);\n }\n return offset === byteLength ? output : output.subarray(0, offset);\n }\n function joinChunks(chunks) {\n const joined = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n joined.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return joined;\n }\n function readPngHeader(input, bitDepth) {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n if (bytes.byteLength < 8 ||\n PNG_SIGNATURE.some((value, index) => bytes[index] !== value)) {\n throw new RangeError(\"Not a PNG file.\");\n }\n let offset = 8;\n let width = 0;\n let height = 0;\n let sawHeader = false;\n let sawEnd = false;\n const idat = [];\n while (offset + 12 <= bytes.byteLength) {\n const length = view.getUint32(offset);\n const type = view.getUint32(offset + 4);\n const dataStart = offset + 8;\n const dataEnd = dataStart + length;\n if (dataEnd + 4 > bytes.byteLength) {\n throw new RangeError(\"PNG file is truncated.\");\n }\n if (type === 0x49484452) {\n // IHDR\n if (length !== 13)\n throw new RangeError(\"PNG header is malformed.\");\n width = view.getUint32(dataStart);\n height = view.getUint32(dataStart + 4);\n const depth = bytes[dataStart + 8];\n const colorType = bytes[dataStart + 9];\n if (bytes[dataStart + 10] !== 0 || bytes[dataStart + 11] !== 0) {\n throw new RangeError(\"PNG uses an unknown compression or filter.\");\n }\n if (bytes[dataStart + 12] !== 0) {\n throw new RangeError(\"Interlaced PNGs are not supported.\");\n }\n if (colorType !== COLOR_TYPE_GRAY || depth !== bitDepth) {\n throw new RangeError(`Expected a ${bitDepth}-bit grayscale PNG, got bit depth ${depth} with colour type ${colorType}.`);\n }\n sawHeader = true;\n }\n else if (type === 0x49444154) {\n // IDAT\n idat.push(bytes.subarray(dataStart, dataEnd));\n }\n else if (type === 0x49454e44) {\n // IEND\n sawEnd = true;\n break;\n }\n offset = dataEnd + 4;\n }\n if (!sawHeader)\n throw new RangeError(\"PNG has no header.\");\n if (!sawEnd)\n throw new RangeError(\"PNG file is truncated.\");\n if (width === 0 || height === 0 || width * height > MAX_PNG_PIXELS) {\n throw new RangeError(`PNG size ${width}x${height} is not supported.`);\n }\n if (idat.length === 0)\n throw new RangeError(\"PNG has no image data.\");\n return { height, idat, width };\n }\n function checkInflatedLength(raw, expected) {\n if (raw.byteLength !== expected) {\n throw new RangeError(\"PNG image data is shorter than its header says.\");\n }\n }\n /*\n * Undoes the row filters of a 16-bit grayscale image and writes host-order\n * samples, in one pass. Each filter runs on bytes, and the two bytes of a\n * sample never carry into each other, so the loops keep a sample's high and\n * low bytes apart and join them only for the store. The previous row is read\n * back from the output, which already holds it unfiltered; the first row reads\n * a row of zeros, which is what PNG defines above the image.\n *\n * Each filter is its own small function with one loop, called once per row,\n * so engines compile each loop on its own: SpiderMonkey slowed a Paeth loop\n * that shared a function with the other filters.\n */\n function unfilterGray16(raw, width, height, out) {\n const rowBytes = width * 2 + 1;\n const zeroRow = new Uint16Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_16[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow16(raw, s, out, o, end) {\n for (let i = o; i < end; i += 1, s += 2) {\n out[i] = (raw[s] << 8) | raw[s + 1];\n }\n }\n function subRow16(raw, s, out, o, end) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2) {\n hi = (raw[s] + hi) & 0xff;\n lo = (raw[s + 1] + lo) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n function upRow16(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n out[i] = (((raw[s] + (up >> 8)) & 0xff) << 8) | ((raw[s + 1] + up) & 0xff);\n }\n }\n function averageRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n hi = (raw[s] + ((hi + (up >> 8)) >> 1)) & 0xff;\n lo = (raw[s + 1] + ((lo + (up & 0xff)) >> 1)) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n /** Paeth: per byte, whichever of left, up and up-left is closest to left + up - up-left. */\n function paethRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n let upLeftHi = 0;\n let upLeftLo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n const upHi = up >> 8;\n const upLo = up & 0xff;\n let pa = Math.abs(upHi - upLeftHi);\n let pb = Math.abs(hi - upLeftHi);\n let pc = Math.abs(hi + upHi - upLeftHi - upLeftHi);\n hi =\n (raw[s] + (pa <= pb && pa <= pc ? hi : pb <= pc ? upHi : upLeftHi)) &\n 0xff;\n pa = Math.abs(upLo - upLeftLo);\n pb = Math.abs(lo - upLeftLo);\n pc = Math.abs(lo + upLo - upLeftLo - upLeftLo);\n lo =\n (raw[s + 1] + (pa <= pb && pa <= pc ? lo : pb <= pc ? upLo : upLeftLo)) &\n 0xff;\n out[i] = (hi << 8) | lo;\n upLeftHi = upHi;\n upLeftLo = upLo;\n }\n }\n /** Indexed by the PNG filter type byte: None, Sub, Up, Average, Paeth. */\n const ROW_FILTERS_16 = [\n noneRow16,\n subRow16,\n upRow16,\n averageRow16,\n paethRow16,\n ];\n function unfilterGray8(raw, width, height, out) {\n const rowBytes = width + 1;\n const zeroRow = new Uint8Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_8[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow8(raw, s, out, o, end) {\n out.set(raw.subarray(s, s + end - o), o);\n }\n function subRow8(raw, s, out, o, end) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1) {\n left = (raw[s] + left) & 0xff;\n out[i] = left;\n }\n }\n function upRow8(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n out[i] = (raw[s] + prev[p]) & 0xff;\n }\n }\n function averageRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n left = (raw[s] + ((left + prev[p]) >> 1)) & 0xff;\n out[i] = left;\n }\n }\n function paethRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n let upLeft = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n const up = prev[p];\n const pa = Math.abs(up - upLeft);\n const pb = Math.abs(left - upLeft);\n const pc = Math.abs(left + up - upLeft - upLeft);\n left =\n (raw[s] + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft)) & 0xff;\n out[i] = left;\n upLeft = up;\n }\n }\n const ROW_FILTERS_8 = [\n noneRow8,\n subRow8,\n upRow8,\n averageRow8,\n paethRow8,\n ];\n /**\n * WebGL unpacks rows on four-byte boundaries, so an odd width of two-byte\n * texels needs one padding texel per row.\n */\n function padRows(values, width, height) {\n const textureWidth = width + 1;\n const padded = new Uint16Array(textureWidth * height);\n for (let y = 0; y < height; y += 1) {\n padded.set(values.subarray(y * width, (y + 1) * width), y * textureWidth);\n }\n return { bytes: new Uint8Array(padded.buffer), textureWidth };\n }\n\n /**\n * Answers one decode request, in the worker or on the main thread: the reply\n * and the buffers it can transfer. A file that does not decode is an error\n * reply, never a thrown exception, so the caller can tell a bad file from a\n * broken worker.\n */\n async function decodeDepthPreparationRequest(request) {\n const requestId = request.requestId ?? 0;\n try {\n if (request.bitDepth === 8) {\n const decoded = await decodePng8Gray(request.bytes);\n return {\n response: {\n bitDepth: 8,\n height: decoded.height,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer: [decoded.values.buffer],\n };\n }\n const decoded = await decodePng16(request.bytes, {\n padRowsForWebGl: request.padRowsForWebGl,\n });\n const padded = decoded.paddedUpload;\n const decimated = request.decimateBy !== undefined && request.decimateBy >= 2\n ? decimateDepthUpload(decoded.values, decoded.width, decoded.height, Math.floor(request.decimateBy), request.padRowsForWebGl === true)\n : null;\n const transfer = [decoded.values.buffer];\n if (padded)\n transfer.push(padded.bytes.buffer);\n if (decimated)\n transfer.push(decimated.bytes.buffer);\n return {\n response: {\n bitDepth: 16,\n decimatedUpload: decimated\n ? { ...decimated, bytes: decimated.bytes.buffer }\n : undefined,\n height: decoded.height,\n paddedUpload: padded\n ? {\n bytes: padded.bytes.buffer,\n textureWidth: padded.textureWidth,\n }\n : undefined,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer,\n };\n }\n catch (error) {\n return {\n response: {\n error: error instanceof Error ? error.message : \"Unable to decode depth.\",\n requestId,\n type: DepthPreparationWorkerMessageType.Error,\n },\n transfer: [],\n };\n }\n }\n /**\n * Depth for a texture `factor` times smaller on each side, each texel the\n * sample at its block's centre, as the renderer decimates a map larger than\n * the GPU allows.\n */\n function decimateDepthUpload(values, width, height, factor, padRowsForWebGl) {\n if (factor < 2)\n return null;\n const targetWidth = Math.ceil(width / factor);\n const targetHeight = Math.ceil(height / factor);\n // WebGL reads two-byte texels in rows of four bytes.\n const textureWidth = padRowsForWebGl\n ? targetWidth + (targetWidth % 2)\n : targetWidth;\n const texels = new Uint16Array(textureWidth * targetHeight);\n const columns = new Uint32Array(targetWidth);\n for (let x = 0; x < targetWidth; x += 1) {\n columns[x] = Math.min(width - 1, Math.floor(((x + 0.5) * width) / targetWidth));\n }\n for (let y = 0; y < targetHeight; y += 1) {\n const row = Math.min(height - 1, Math.floor(((y + 0.5) * height) / targetHeight)) *\n width;\n const target = y * textureWidth;\n for (let x = 0; x < targetWidth; x += 1) {\n texels[target + x] = values[row + columns[x]];\n }\n }\n return {\n bytes: new Uint8Array(texels.buffer),\n height: targetHeight,\n textureWidth,\n width: targetWidth,\n };\n }\n\n /** Formats whose first plane is 8-bit luma, which is the code as written. */\n const PLANAR_8_BIT_FORMATS = new Set([\n \"I420\",\n \"I420A\",\n \"I422\",\n \"I422A\",\n \"I444\",\n \"I444A\",\n \"NV12\",\n ]);\n const RGB_FORMATS = new Set([\"RGBA\", \"RGBX\", \"BGRA\", \"BGRX\"]);\n /**\n * Copies a decoded preview frame's luma codes out of it, mapped through\n * `correction` when one is given. The frame stays open; its owner closes\n * it.\n */\n async function readVideoFrameLuma(frame, scratch, correction = null) {\n const rect = frame.visibleRect ?? {\n height: frame.codedHeight,\n width: frame.codedWidth,\n x: 0,\n y: 0,\n };\n const { width, height } = rect;\n const format = frame.format;\n const planar = format !== null && PLANAR_8_BIT_FORMATS.has(format);\n let bytes;\n let plane;\n if (planar || (format !== null && RGB_FORMATS.has(format))) {\n const size = frame.allocationSize({ rect });\n if (scratch.buffer.byteLength < size)\n scratch.buffer = new ArrayBuffer(size);\n [plane] = await frame.copyTo(scratch.buffer, { rect });\n bytes = new Uint8Array(scratch.buffer);\n }\n else {\n // A frame in a layout no copy can read is drawn to a canvas, which\n // converts it to RGB as the browser sees fit.\n bytes = drawFrameRgba(frame, width, height);\n }\n const luma = new Uint8Array(width * height);\n const stride = plane?.stride ?? (planar ? width : width * 4);\n const offset = plane?.offset ?? 0;\n if (!planar) {\n // Every RGB layout keeps green second, and luma with neutral chroma\n // converts to three equal channels.\n for (let y = 0; y < height; y += 1) {\n const row = offset + y * stride + 1;\n const target = y * width;\n for (let x = 0; x < width; x += 1) {\n luma[target + x] = bytes[row + x * 4];\n }\n }\n }\n else if (stride === width) {\n luma.set(bytes.subarray(offset, offset + width * height));\n }\n else {\n for (let y = 0; y < height; y += 1) {\n luma.set(bytes.subarray(offset + y * stride, offset + y * stride + width), y * width);\n }\n }\n if (correction) {\n for (let index = 0; index < luma.length; index += 1) {\n luma[index] = correction[luma[index]];\n }\n }\n return { height, luma, path: planar ? \"plane\" : \"rgb\", width };\n }\n function drawFrameRgba(frame, width, height) {\n const canvas = new OffscreenCanvas(width, height);\n const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!context) {\n throw new Error(\"The depth preview needs a 2D canvas to read this frame.\");\n }\n context.drawImage(frame, 0, 0, width, height);\n const { data } = context.getImageData(0, 0, width, height);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n\n const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type === HeatmapPreparationWorkerMessageType.Prepare) {\n prepareHeatmap(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.Decode) {\n void decodeDepthFrame(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.PreviewLuma) {\n void copyPreviewLuma(message);\n return;\n }\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\n });\n function prepareHeatmap(message) {\n try {\n const data = colorizeHeatmap(message.map, message.renderer);\n const imageData = new ImageData(data, message.map.width, message.map.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n if (imageBitmap) {\n workerScope.postMessage({\n imageBitmap,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageBitmap]);\n return;\n }\n workerScope.postMessage({\n imageData,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error ? error.message : \"Unable to prepare heatmap.\",\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Error,\n });\n }\n }\n /** Decoding waits on the platform's inflate, so replies arrive out of order. */\n async function decodeDepthFrame(message) {\n const { response, transfer } = await decodeDepthPreparationRequest(message);\n workerScope.postMessage(response, transfer);\n }\n const previewLumaScratch = {\n buffer: new ArrayBuffer(0),\n };\n /** Frames arrive in order and are copied in order, through one scratch. */\n let previewLumaQueue = Promise.resolve();\n function copyPreviewLuma(message) {\n previewLumaQueue = previewLumaQueue.then(async () => {\n try {\n const copied = await readVideoFrameLuma(message.frame, previewLumaScratch, message.correction);\n workerScope.postMessage({\n height: copied.height,\n luma: copied.luma.buffer,\n path: copied.path,\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.PreviewLumaComplete,\n width: copied.width,\n }, [copied.luma.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to copy the depth preview frame.\",\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.Error,\n });\n }\n finally {\n message.frame.close();\n }\n });\n return previewLumaQueue;\n }\n function prepareMaskFrame(message) {\n try {\n const regionMaskCoverage = createRegionMaskCoverageFrame(message.job.instructions);\n const coverageTransfers = getRegionMaskCoverageTransfers(regionMaskCoverage);\n const idMaskFrame = createIdMaskRasterFrame(message.job.instructions, message.job.maxRasterWidth);\n if (idMaskFrame) {\n workerScope.postMessage({\n artifactKind: PreparedMaskFrameKind.IdMask,\n fillPalette: idMaskFrame.fillPalette,\n hasStroke: idMaskFrame.hasStroke,\n height: idMaskFrame.height,\n key: message.job.key,\n maxStrokeWidth: idMaskFrame.maxStrokeWidth,\n raster: idMaskFrame.data,\n regionMaskCoverage,\n requestId: message.requestId,\n sourceWidth: idMaskFrame.sourceWidth,\n strokePalette: idMaskFrame.strokePalette,\n strokeWidths: idMaskFrame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n width: idMaskFrame.width,\n }, [\n idMaskFrame.data.buffer,\n idMaskFrame.fillPalette.buffer,\n idMaskFrame.strokePalette.buffer,\n idMaskFrame.strokeWidths.buffer,\n ...coverageTransfers,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions);\n if (!compositedFrame && !regionMaskCoverage) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const preparedPixels = compositedFrame ?? createTransparentCoverageCarrier();\n const imageData = new ImageData(preparedPixels.data, preparedPixels.width, preparedPixels.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskPlane = createIdMaskPlane(message.job.instructions, message.job.maxRasterWidth);\n const idMaskTransfers = idMaskPlane ? [idMaskPlane.data.buffer] : [];\n if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageBitmap, ...idMaskTransfers, ...coverageTransfers]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer, ...idMaskTransfers, ...coverageTransfers]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to prepare mask frame.\",\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Error,\n });\n }\n }\n /**\n * A frame that carries only region coverage has nothing to composite, and the\n * RGBA branch still has to produce an artifact for the coverage to ride on.\n */\n function createTransparentCoverageCarrier() {\n return {\n data: new Uint8ClampedArray(new ArrayBuffer(4)),\n height: 1,\n width: 1,\n };\n }\n function getRegionMaskCoverageTransfers(coverage) {\n return coverage?.entries.map(({ data }) => data.buffer) ?? [];\n }\n function createImageBitmapFromImageData(imageData) {\n if (typeof OffscreenCanvas === \"undefined\") {\n return null;\n }\n const canvas = new OffscreenCanvas(imageData.width, imageData.height);\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n context.putImageData(imageData, 0, 0);\n return canvas.transferToImageBitmap();\n }\n\n})();";
|
|
3116
|
+
const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** Trace closed pixel boundaries, retaining holes and disconnected components. */\n function traceMaskOutlinePaths(raster, width, height, ids) {\n const edgesById = new Map();\n const stride = width + 1;\n const steps = [1, stride, -1, -stride];\n const add = (id, vertex, direction) => {\n let edges = edgesById.get(id);\n if (!edges)\n edgesById.set(id, (edges = new Map()));\n const outgoing = edges.get(vertex);\n if (outgoing)\n outgoing.push(direction);\n else\n edges.set(vertex, [direction]);\n };\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const offset = y * width + x;\n const id = raster[offset];\n if (!ids.has(id))\n continue;\n const vertex = y * stride + x;\n if (y === 0 || raster[offset - width] !== id)\n add(id, vertex, 0);\n if (x === width - 1 || raster[offset + 1] !== id)\n add(id, vertex + 1, 1);\n if (y === height - 1 || raster[offset + width] !== id)\n add(id, vertex + stride + 1, 2);\n if (x === 0 || raster[offset - 1] !== id)\n add(id, vertex + stride, 3);\n }\n }\n const result = new Map();\n for (const [id, edges] of edgesById) {\n const paths = [];\n while (edges.size > 0) {\n const start = edges.keys().next().value;\n let vertex = start;\n let previousDirection = -1;\n let firstDirection = -1;\n const points = [];\n do {\n const outgoing = edges.get(vertex);\n // Turn right at diagonal contacts so separate islands stay separate.\n const direction = previousDirection < 0\n ? outgoing[0]\n : [1, 0, 3, 2]\n .map((turn) => (previousDirection + turn) % 4)\n .find((candidate) => outgoing.includes(candidate));\n if (firstDirection < 0)\n firstDirection = direction;\n if (direction !== previousDirection)\n points.push(vertex % stride, Math.floor(vertex / stride));\n outgoing.splice(outgoing.indexOf(direction), 1);\n if (outgoing.length === 0)\n edges.delete(vertex);\n vertex += steps[direction];\n previousDirection = direction;\n } while (vertex !== start);\n if (previousDirection === firstDirection)\n points.splice(0, 2);\n paths.push(new Float32Array(points));\n }\n result.set(id, paths);\n }\n return result;\n }\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n function decodeCompressedRleMask(mask) {\n if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {\n throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);\n }\n const data = new Uint8Array(mask.width * mask.height);\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n const isForeground = index % 2 === 1;\n if (isForeground) {\n for (let runOffset = 0; runOffset < runLength; runOffset += 1) {\n const maskOffset = offset + runOffset;\n const x = Math.floor(maskOffset / mask.height);\n const y = maskOffset % mask.height;\n const rowMajorOffset = y * mask.width + x;\n if (rowMajorOffset < data.length) {\n data[rowMajorOffset] = 1;\n }\n }\n }\n offset += runLength;\n }\n return {\n data,\n height: mask.height,\n width: mask.width,\n };\n }\n function decodeCompressedRleCounts(counts) {\n const decoded = [];\n let index = 0;\n while (index < counts.length) {\n let value = 0;\n let shift = 0;\n let charCode;\n do {\n charCode = counts.charCodeAt(index) - 48;\n index += 1;\n value |= (charCode & 0x1f) << shift;\n shift += 5;\n } while (charCode & 0x20);\n if (charCode & 0x10) {\n value |= -1 << shift;\n }\n if (decoded.length > 2) {\n value += decoded[decoded.length - 2] ?? 0;\n }\n decoded.push(value);\n }\n return decoded;\n }\n function encodeCompressedRleCounts(counts) {\n return counts\n .map((count, index) => {\n let value = index > 2 ? count - counts[index - 2] : count;\n let encoded = \"\";\n let more = true;\n while (more) {\n let charCode = value & 0x1f;\n value >>= 5;\n more = !((value === 0 && (charCode & 0x10) === 0) ||\n (value === -1 && (charCode & 0x10) !== 0));\n if (more) {\n charCode |= 0x20;\n }\n encoded += String.fromCharCode(charCode + 48);\n }\n return encoded;\n })\n .join(\"\");\n }\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n function encodeBinaryMask(data, width, height) {\n assertMaskDimensions(data, width, height);\n const runs = [];\n let currentValue = 0;\n let runLength = 0;\n for (let x = 0; x < width; x += 1) {\n for (let y = 0; y < height; y += 1) {\n const value = data[y * width + x] ? 1 : 0;\n if (value === currentValue) {\n runLength += 1;\n }\n else {\n runs.push(runLength);\n currentValue = value;\n runLength = 1;\n }\n }\n }\n runs.push(runLength);\n return {\n counts: encodeCompressedRleCounts(runs),\n encoding: DetectionMaskEncoding.CompressedRle,\n height,\n width,\n };\n }\n function assertMaskSize(width, height) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(\"Mask width must be a positive integer.\");\n }\n if (!Number.isInteger(height) || height <= 0) {\n throw new Error(\"Mask height must be a positive integer.\");\n }\n }\n function assertMaskDimensions(data, width, height) {\n assertMaskSize(width, height);\n if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\n\n function centerRectToTopLeftRect(rect) {\n return {\n height: rect.height,\n width: rect.width,\n x: rect.x - rect.width / 2,\n y: rect.y - rect.height / 2,\n };\n }\n function getPointsRect(points) {\n if (points.length === 0) {\n return undefined;\n }\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n minY = Math.min(minY, point.y);\n maxX = Math.max(maxX, point.x);\n maxY = Math.max(maxY, point.y);\n }\n return {\n height: maxY - minY,\n width: maxX - minX,\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n };\n }\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"OrientedBox\"] = \"orientedBox\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var PercentageBarPlacement;\n (function (PercentageBarPlacement) {\n PercentageBarPlacement[\"Top\"] = \"top\";\n PercentageBarPlacement[\"Bottom\"] = \"bottom\";\n PercentageBarPlacement[\"InsideTop\"] = \"insideTop\";\n PercentageBarPlacement[\"InsideBottom\"] = \"insideBottom\";\n })(PercentageBarPlacement || (PercentageBarPlacement = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n\n /**\n * What the stored values of a depth map measure.\n *\n * Every kind stores 0 where there is no depth.\n */\n const DepthMapKind = {\n /** Stereo disparity in pixels of the map. Larger is nearer. */\n DisparityPx: \"disparity_px\",\n /** Metric depth along the optical axis in metres. Smaller is nearer. */\n DepthM: \"depth_m\",\n /**\n * Unitless inverse depth up to an unknown scale and shift, as monocular\n * models produce it. Larger is nearer; it has no metric depth.\n */\n RelativeInverse: \"relative_inverse\",\n };\n /**\n * The quantity a depth renderer colours.\n *\n * Disparity is inverse depth, so it spends colour on near detail the way\n * stereo measures it. Either way the nearest end of the range is the warm or\n * bright end of the colour table.\n */\n const DepthQuantity = {\n Disparity: \"disparity\",\n Depth: \"depth\",\n };\n const DepthColormap = {\n Turbo: \"turbo\",\n Viridis: \"viridis\",\n Cividis: \"cividis\",\n Inferno: \"inferno\",\n Magma: \"magma\",\n Grayscale: \"grayscale\",\n };\n /** How a depth map is sampled where one map pixel does not cover one media pixel. */\n const DepthSampling = {\n /** Nearest when the map is at least media size, edge-aware otherwise. */\n Auto: \"auto\",\n Nearest: \"nearest\",\n /**\n * Bilinear over valid neighbours only, falling back to the nearest one\n * across a depth edge, so objects never bleed into the background.\n */\n EdgeAware: \"edge-aware\",\n };\n /**\n * The luma range a depth preview video's codes are written in.\n *\n * TV range is what browsers' hardware decoders return as written; a decoder\n * may squeeze full range into TV range or move codes converting it to RGB.\n */\n const DepthPreviewLevels = {\n /** Every code from 0 to 255; the top code is 255. */\n Full: \"full\",\n /** Limited range: black is 16 and the top code is 235. */\n Tv: \"tv\",\n };\n new Set(Object.values(DepthColormap));\n new Set(Object.values(DepthQuantity));\n new Set(Object.values(DepthSampling));\n function rasterizePolygonToMask(points, dimensions) {\n const data = createEmptyMask(dimensions);\n if (points.length < 3) {\n return data;\n }\n const bounds = centerRectToTopLeftRect(getPointsRect(points));\n const startY = Math.max(0, Math.floor(bounds.y));\n const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));\n for (let y = startY; y <= endY; y += 1) {\n const scanY = y + 0.5;\n const intersections = [];\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index];\n const next = points[(index + 1) % points.length];\n if ((current.y <= scanY && next.y > scanY) ||\n (next.y <= scanY && current.y > scanY)) {\n const ratio = (scanY - current.y) / (next.y - current.y);\n intersections.push(current.x + ratio * (next.x - current.x));\n }\n }\n intersections.sort((left, right) => left - right);\n for (let index = 0; index < intersections.length - 1; index += 2) {\n const left = Math.max(0, Math.round(intersections[index]));\n const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));\n for (let x = left; x < right; x += 1) {\n data[y * dimensions.width + x] = 1;\n }\n }\n }\n return data;\n }\n function createEmptyMask(dimensions) {\n if (!Number.isInteger(dimensions.width) ||\n dimensions.width <= 0 ||\n !Number.isInteger(dimensions.height) ||\n dimensions.height <= 0) {\n throw new Error(\"Media dimensions must be positive integers.\");\n }\n return new Uint8Array(dimensions.width * dimensions.height);\n }\n\n /**\n * Palette slots an id raster can name, one of them the background. A GLSL\n * fragment stage is guaranteed only 224 uniform vectors, and a palette entry\n * costs 2.25 of them, so this stays a multiple of four and well inside that\n * budget; past it a frame draws from the RGBA composite instead.\n */\n const MAX_ID_MASK_PALETTE_ENTRIES = 80;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions, options = {}) {\n if (instructions.length === 0) {\n return undefined;\n }\n /* Pixel-grid dimensions use integer parts throughout allocation and RLE\n traversal. */\n const maskWidth = Math.floor(Math.max(...instructions.map(({ mask }) => mask.width)));\n const maskHeight = Math.floor(Math.max(...instructions.map(({ mask }) => mask.height)));\n const width = resolveRasterWidth(maskWidth, options.maxWidth);\n const height = width === maskWidth\n ? maskHeight\n : Math.max(1, Math.round((width * maskHeight) / maskWidth));\n const scaledAxes = width === maskWidth\n ? undefined\n : createScaledMaskAxes({ height, maskHeight, maskWidth, width });\n const data = new Uint8Array(new ArrayBuffer(width * height));\n const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n let hasStroke = false;\n let maxStrokeWidth = 0;\n for (const instruction of instructions) {\n const detectionMaskId = resolveIdMaskPaletteId(instruction.detectionIndex);\n if (detectionMaskId === undefined) {\n return undefined;\n }\n writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = resolveIdMaskStrokeTexels(instruction.stroke.width, maskWidth, width);\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);\n writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n if (scaledAxes) {\n writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);\n }\n else {\n writeMaskRuns(data, width, instruction.mask, detectionMaskId);\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n sourceWidth: maskWidth,\n strokePalette,\n strokeWidths,\n width,\n };\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let frameOffset = y * frameWidth + x;\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[frameOffset] = detectionMaskId;\n y += 1;\n frameOffset += frameWidth;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n frameOffset = x;\n }\n }\n maskOffset += runLength;\n }\n }\n /**\n * The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands\n * in, so a mask keeps every part of itself the smaller raster can hold and\n * gains up to a texel at its edges.\n */\n function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = Math.floor(mask.width);\n const maskHeight = Math.floor(mask.height);\n let maskOffset = 0;\n const stride = axes.stride;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let remaining = runLength;\n /* A run walks down one column and wraps into the next, so it splits into\n column-sized segments. Within a segment the destination offset only ever\n steps by the row pitch or stays put: createMaskAxisMap builds\n `rows[i] = min(height - 1, floor(i * scale)) * width` with scale <= 1, so\n consecutive entries differ by exactly 0 or exactly `width`. Walking the\n destination directly therefore touches the same bytes the per-pixel loop\n touched, and drops the bounds test, the wrap test and one typed-array\n load from every source pixel. Where the scale collapses several source\n rows onto one destination row, this writes that byte once instead of\n once per source row. */\n while (remaining > 0 && x < maskWidth) {\n const span = Math.min(remaining, maskHeight - y);\n const column = axes.columns[x];\n const first = axes.rows[y];\n const last = axes.rows[y + span - 1];\n for (let offset = first; offset <= last; offset += stride) {\n data[offset + column] = detectionMaskId;\n }\n remaining -= span;\n y += span;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n }\n }\n maskOffset += runLength;\n }\n }\n function resolveRasterWidth(maskWidth, maxWidth) {\n return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth\n ? Math.max(1, Math.floor(maxWidth))\n : maskWidth;\n }\n /**\n * Converts a source-grid stroke width into raster texels. Fractional widths\n * let coverage shaders draw a border within a texel. The ceiling bounds the\n * neighbourhood the shaders scan.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {\n const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;\n return Math.min(strokeWidth * scale, MAX_ID_MASK_STROKE_WIDTH);\n }\n function createScaledMaskAxes(frame) {\n return {\n columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),\n rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),\n stride: frame.width,\n };\n }\n /** Rows hold their destination offset, so writing a run is two lookups. */\n function createMaskAxisMap(sourceLength, targetLength, stride) {\n const map = new Int32Array(sourceLength);\n const scale = targetLength / sourceLength;\n for (let index = 0; index < sourceLength; index += 1) {\n map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;\n }\n return map;\n }\n /**\n * The palette slot a detection index names, or undefined when the palette has\n * no slot for it. Slot zero is the background, so the ids start at one and the\n * last detection the palette can name sits one index below the last slot.\n */\n function resolveIdMaskPaletteId(detectionIndex) {\n const detectionMaskId = detectionIndex + 1;\n return detectionMaskId > 0 && detectionMaskId < MAX_ID_MASK_PALETTE_ENTRIES\n ? detectionMaskId\n : undefined;\n }\n function writeIdMaskPaletteEntry(palette, detectionMaskId, color, alpha) {\n const offset = detectionMaskId * 4;\n palette[offset] = ((color >> 16) & 0xff) / 255;\n palette[offset + 1] = ((color >> 8) & 0xff) / 255;\n palette[offset + 2] = (color & 0xff) / 255;\n palette[offset + 3] = Math.max(0, Math.min(alpha, 1));\n }\n new Set(Object.values(DepthPreviewLevels));\n new Set(Object.values(DepthMapKind));\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read, or was refused before any read. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, and a gate that stops one has to bound its own wait\n * or a producer nothing answers for never runs again.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n /**\n * The picture is waiting on media the source has not handed over yet, which\n * is the bytes and their decode rather than anything downstream of them.\n * `PlaybackBuffering` is what a transport reports once it has already\n * stopped; this is reported from the read itself, including the reads a seek\n * makes while the transport still reads as paused.\n */\n MediaSessionActivityKind[\"MediaSourceReading\"] = \"mediaSourceReading\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n /**\n * The render-preparation gate held the picture for artifacts that never\n * arrived and let it go. Nothing is blocked: the frames reaching the screen\n * are the ones whose artifacts were given up on. What is waited on is\n * preparation finishing another frame, which is what lets the gate hold the\n * picture again; `RenderPreparing` covers the holds that are running.\n */\n MediaSessionActivityKind[\"RenderPreparationAbandoned\"] = \"renderPreparationAbandoned\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n /** Builds compact, independent alpha crops so overlapping masks stay exact. */\n function createRegionMaskCoverageFrame(instructions) {\n const entries = [];\n for (const instruction of instructions) {\n if (!instruction.regionCoverageMask) {\n continue;\n }\n const entry = cropCoverageMask(instruction.detectionIndex, decodeCompressedRleMask(instruction.regionCoverageMask));\n if (entry) {\n entries.push(entry);\n }\n }\n return entries.length > 0 ? { entries } : undefined;\n }\n function cropCoverageMask(detectionIndex, decodedMask) {\n let minX = decodedMask.width;\n let minY = decodedMask.height;\n let maxX = -1;\n let maxY = -1;\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n if (maxX < minX || maxY < minY) {\n return undefined;\n }\n const width = maxX - minX + 1;\n const height = maxY - minY + 1;\n const data = new Uint8Array(new ArrayBuffer(width * height));\n for (let y = minY; y <= maxY; y += 1) {\n for (let x = minX; x <= maxX; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n data[(y - minY) * width + (x - minX)] = 255;\n }\n }\n return { data, detectionIndex, height, width, x: minX, y: minY };\n }\n function compositeMaskFrame(instructions) {\n const maskInstructions = materializeMaskInstructions(instructions);\n if (maskInstructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));\n const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));\n const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));\n for (const instruction of maskInstructions) {\n compositeInstruction(data, width, instruction);\n }\n return { data, height, width };\n }\n function createIdMaskRasterFrame(instructions, maxRasterWidth) {\n try {\n return createIdMaskFrame(materializeMaskInstructions(instructions).map((instruction) => instruction.stroke?.widthUnit === \"screen\"\n ? { ...instruction, stroke: undefined }\n : instruction), {\n maxWidth: maxRasterWidth,\n });\n }\n catch {\n // The id raster is the fast path, not the only one: answering with nothing\n // puts the caller on the RGBA composite, which draws the same picture.\n return undefined;\n }\n }\n /**\n * Whether the palette has a slot for every detection this frame masks. An id\n * names a detection by its index in the frame, so one masked detection past the\n * last slot leaves the whole frame without an id raster however few masks it\n * carries.\n */\n function canIdMaskPaletteNameFrame(instructions) {\n return instructions.every((instruction) => instruction.visible === false ||\n resolveIdMaskPaletteId(instruction.detectionIndex) !== undefined);\n }\n function createIdMaskPlane(instructions, maxRasterWidth) {\n // The plane carries the ids a failed cook could not. A frame the palette\n // cannot name has none to carry, and cooking it again reaches the same\n // refusal after rasterizing every mask a second time.\n if (!canIdMaskPaletteNameFrame(instructions)) {\n return undefined;\n }\n const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);\n return frame\n ? { data: frame.data, height: frame.height, width: frame.width }\n : undefined;\n }\n function compositeInstruction(rgba, canvasWidth, instruction) {\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);\n if (instruction.stroke &&\n instruction.stroke.widthUnit !== \"screen\" &&\n bounds) {\n compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);\n }\n }\n function createMaskScreenStrokes(instructions, width, height, preparedRaster) {\n const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&\n stroke?.widthUnit === \"screen\" &&\n stroke.width > 0 &&\n stroke.alpha > 0);\n if (outlined.length === 0)\n return undefined;\n const masks = materializeMaskInstructions(instructions);\n const raster = preparedRaster ?? new Uint32Array(width * height);\n if (!preparedRaster) {\n for (const { detectionIndex, mask } of masks) {\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const length = counts[index];\n if (index % 2 === 1) {\n for (let pixel = offset; pixel < offset + length; pixel += 1) {\n const x = Math.floor(pixel / mask.height);\n const y = pixel % mask.height;\n raster[y * width + x] = detectionIndex + 1;\n }\n }\n offset += length;\n }\n }\n }\n const paths = traceMaskOutlinePaths(raster, width, height, new Set(outlined.map(({ detectionIndex }) => detectionIndex + 1)));\n return outlined.map(({ detectionIndex, stroke }) => ({\n detectionIndex,\n alpha: stroke.alpha,\n color: stroke.color,\n width: stroke.width,\n paths: paths.get(detectionIndex + 1) ?? [],\n }));\n }\n function materializeMaskInstructions(instructions) {\n return instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => {\n if (instruction.mask) {\n return instruction;\n }\n const { height, points, width } = instruction.polygon;\n return {\n alpha: instruction.alpha,\n color: instruction.color,\n detectionIndex: instruction.detectionIndex,\n mask: encodeBinaryMask(rasterizePolygonToMask(points, { height, width }), width, height),\n stroke: instruction.stroke,\n };\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function compositeMaskFill(rgba, canvasWidth, mask, fill) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n const maskArea = maskWidth * maskHeight;\n let maskOffset = 0;\n let minX = maskWidth;\n let minY = maskHeight;\n let maxX = -1;\n let maxY = -1;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let columnX = Math.floor(maskOffset / maskHeight);\n let columnY = maskOffset - columnX * maskHeight;\n for (let step = 0; step < runLength; step += 1) {\n // A run can outlast the columns the mask has; the pixel it then names\n // is wherever that column-major offset lands when read back row-major.\n const rowMajorOffset = columnY * maskWidth + columnX;\n if (rowMajorOffset < maskArea) {\n const x = rowMajorOffset % maskWidth;\n const y = (rowMajorOffset - x) / maskWidth;\n writePixel(rgba, canvasWidth, x, y, fill);\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n columnY += 1;\n if (columnY === maskHeight) {\n columnY = 0;\n columnX += 1;\n }\n }\n maskOffset += runLength;\n }\n return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {\n const width = Math.round(stroke.width);\n if (width <= 0) {\n return;\n }\n const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);\n for (let y = bounds.minY; y <= bounds.maxY; y += 1) {\n for (let x = bounds.minX; x <= bounds.maxX; x += 1) {\n if (!isMaskPixel(decodedMask, x, y) ||\n !isBoundaryPixel(decodedMask, x, y)) {\n continue;\n }\n for (let offsetY = -width; offsetY <= width; offsetY += 1) {\n for (let offsetX = -width; offsetX <= width; offsetX += 1) {\n const strokeX = x + offsetX;\n const strokeY = y + offsetY;\n if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||\n isMaskPixel(decodedMask, strokeX, strokeY)) {\n continue;\n }\n writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);\n }\n }\n }\n }\n }\n function isBoundaryPixel(mask, x, y) {\n for (let offsetY = -1; offsetY <= 1; offsetY += 1) {\n for (let offsetX = -1; offsetX <= 1; offsetX += 1) {\n if (offsetX === 0 && offsetY === 0) {\n continue;\n }\n const neighborX = x + offsetX;\n const neighborY = y + offsetY;\n if (isOutsideMaskBounds(mask, neighborX, neighborY) ||\n !isMaskPixel(mask, neighborX, neighborY)) {\n return true;\n }\n }\n }\n return false;\n }\n function isMaskPixel(mask, x, y) {\n return mask.data[y * mask.width + x] === 1;\n }\n function isOutsideMaskBounds(mask, x, y) {\n return x < 0 || y < 0 || x >= mask.width || y >= mask.height;\n }\n function resolveRgbaColor(color, alpha) {\n return {\n alpha: Math.round(Math.max(0, Math.min(alpha, 1)) * 255),\n blue: color & 0xff,\n green: (color >> 8) & 0xff,\n red: (color >> 16) & 0xff,\n };\n }\n function writePixel(rgba, canvasWidth, x, y, color) {\n const rgbaOffset = (y * canvasWidth + x) * 4;\n rgba[rgbaOffset] = color.red;\n rgba[rgbaOffset + 1] = color.green;\n rgba[rgbaOffset + 2] = color.blue;\n rgba[rgbaOffset + 3] = color.alpha;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"IdMask\"] = \"idMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\n /**\n * How an id raster is laid out for the texture it is uploaded into. WebGPU\n * takes any bytesPerRow, so ids go up one byte per pixel. WebGL aligns every\n * uploaded row to four bytes and rejects a single-channel upload whose width\n * is not a multiple of four, which is what the four-channel layout is for.\n */\n var IdMaskTextureFormat;\n (function (IdMaskTextureFormat) {\n IdMaskTextureFormat[\"R8\"] = \"r8unorm\";\n IdMaskTextureFormat[\"Rgba8\"] = \"rgba8unorm\";\n })(IdMaskTextureFormat || (IdMaskTextureFormat = {}));\n\n const DEFAULT_STOPS = [\n { position: 0, color: 0xffd600 },\n { position: 0.35, color: 0xffd600 },\n { position: 0.7, color: 0xff7a00 },\n { position: 1, color: 0xdc1e1e },\n ];\n const MAX_HEATMAP_PIXELS = 16_777_216;\n const MAX_HEATMAP_SIDE = 8_192;\n /** Turns semantic scores into an RGBA raster; no palette enters cold data. */\n function colorizeHeatmap(map, renderer) {\n const { bounds, width, height, values } = map;\n if (!Number.isFinite(bounds?.x) ||\n !Number.isFinite(bounds?.y) ||\n !Number.isFinite(bounds?.width) ||\n !Number.isFinite(bounds?.height) ||\n bounds.width <= 0 ||\n bounds.height <= 0 ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width <= 0 ||\n height <= 0 ||\n width > MAX_HEATMAP_SIDE ||\n height > MAX_HEATMAP_SIDE ||\n width * height > MAX_HEATMAP_PIXELS ||\n !(Array.isArray(values) ||\n values instanceof Float32Array ||\n values instanceof Float64Array ||\n values instanceof Uint16Array) ||\n values.length !== width * height) {\n throw new RangeError(\"Heatmap bounds and dimensions must be valid, match row-major values, and fit the raster limit.\");\n }\n const scale = map.valueScale ?? 1;\n const threshold = (map.threshold ?? 0) * (renderer.thresholdScale ?? 1);\n const maximumScore = renderer.maximumScore ?? 1;\n const opacity = clamp01(renderer.opacity ?? 1);\n const minimumAlpha = clamp01(renderer.minimumAlpha ?? 0);\n const stops = renderer.colorStops ?? DEFAULT_STOPS;\n if (!Number.isFinite(scale) ||\n scale <= 0 ||\n !Number.isFinite(threshold) ||\n !Number.isFinite(maximumScore) ||\n maximumScore <= 0 ||\n !Number.isFinite(renderer.thresholdScale ?? 1) ||\n (renderer.thresholdScale ?? 1) < 0 ||\n !Number.isFinite(renderer.opacity ?? 1) ||\n !Number.isFinite(renderer.minimumAlpha ?? 0) ||\n stops.length === 0 ||\n stops.some((stop, index) => !Number.isFinite(stop.position) ||\n stop.position < 0 ||\n stop.position > 1 ||\n !Number.isInteger(stop.color) ||\n stop.color < 0 ||\n stop.color > 0xffffff ||\n (index > 0 && stop.position < stops[index - 1].position))) {\n throw new RangeError(\"Invalid heatmap scale, threshold, or color stops.\");\n }\n const rgba = new Uint8ClampedArray(new ArrayBuffer(values.length * 4));\n if (maximumScore <= threshold || opacity === 0)\n return rgba;\n for (let index = 0; index < values.length; index += 1) {\n const score = values[index] * scale;\n if (!Number.isFinite(score) || score <= threshold)\n continue;\n const color = interpolateColor(stops, clamp01(score / maximumScore));\n const alphaIntensity = clamp01((score - threshold) / (maximumScore - threshold));\n const offset = index * 4;\n rgba[offset] = color >> 16;\n rgba[offset + 1] = (color >> 8) & 0xff;\n rgba[offset + 2] = color & 0xff;\n rgba[offset + 3] = Math.round(255 * opacity * (minimumAlpha + (1 - minimumAlpha) * alphaIntensity));\n }\n return rgba;\n }\n function interpolateColor(stops, value) {\n const upperIndex = stops.findIndex((stop) => stop.position >= value);\n if (upperIndex === -1)\n return stops[stops.length - 1].color;\n if (upperIndex === 0)\n return stops[0].color;\n const left = stops[upperIndex - 1];\n const right = stops[upperIndex];\n const span = right.position - left.position;\n const ratio = span === 0 ? 1 : (value - left.position) / span;\n const channel = (shift) => Math.round(((left.color >> shift) & 0xff) * (1 - ratio) +\n ((right.color >> shift) & 0xff) * ratio);\n return (channel(16) << 16) | (channel(8) << 8) | channel(0);\n }\n function clamp01(value) {\n return Math.max(0, Math.min(1, value));\n }\n\n var HeatmapPreparationWorkerMessageType;\n (function (HeatmapPreparationWorkerMessageType) {\n HeatmapPreparationWorkerMessageType[\"Prepare\"] = \"heatmap-prepare\";\n HeatmapPreparationWorkerMessageType[\"Complete\"] = \"heatmap-complete\";\n HeatmapPreparationWorkerMessageType[\"Error\"] = \"heatmap-error\";\n })(HeatmapPreparationWorkerMessageType || (HeatmapPreparationWorkerMessageType = {}));\n\n var MaskPreparationWorkerMessageType;\n (function (MaskPreparationWorkerMessageType) {\n MaskPreparationWorkerMessageType[\"Complete\"] = \"complete\";\n MaskPreparationWorkerMessageType[\"Empty\"] = \"empty\";\n MaskPreparationWorkerMessageType[\"Error\"] = \"error\";\n MaskPreparationWorkerMessageType[\"Prepare\"] = \"prepare\";\n })(MaskPreparationWorkerMessageType || (MaskPreparationWorkerMessageType = {}));\n\n /**\n * Messages between the depth source and the render-preparation worker, which\n * decodes depth and confidence PNGs, and copies decoded preview frames' luma,\n * off the main thread. Internal: the worker script is a deployment asset, and\n * its protocol is not public.\n */\n var DepthPreparationWorkerMessageType;\n (function (DepthPreparationWorkerMessageType) {\n DepthPreparationWorkerMessageType[\"Decode\"] = \"depth-decode\";\n DepthPreparationWorkerMessageType[\"Complete\"] = \"depth-decode-complete\";\n DepthPreparationWorkerMessageType[\"Error\"] = \"depth-decode-error\";\n DepthPreparationWorkerMessageType[\"PreviewLuma\"] = \"depth-preview-luma\";\n DepthPreparationWorkerMessageType[\"PreviewLumaComplete\"] = \"depth-preview-luma-complete\";\n })(DepthPreparationWorkerMessageType || (DepthPreparationWorkerMessageType = {}));\n\n /*\n * Grayscale PNG decoding for depth maps and their confidence planes.\n *\n * Every image path a browser offers (<img>, createImageBitmap, ImageDecoder,\n * canvas) hands back 8 bits per channel, so a 16-bit depth PNG is decoded\n * here: the browser's own zlib inflates the IDAT stream, and the row filters\n * are undone in JavaScript.\n *\n * This module imports nothing, so the render-preparation worker, the main\n * thread and the depth benchmark all run the same code.\n */\n const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];\n const COLOR_TYPE_GRAY = 0;\n /** 16384 x 16384, beyond any GPU texture; a header claiming more is refused. */\n const MAX_PNG_PIXELS = 16_384 * 16_384;\n async function decodePng16(bytes, options = {}) {\n const header = readPngHeader(bytes, 16);\n const rowBytes = header.width * 2 + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint16Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray16(raw, header.width, header.height, values);\n return {\n height: header.height,\n paddedUpload: options.padRowsForWebGl && header.width % 2 === 1\n ? padRows(values, header.width, header.height)\n : undefined,\n values,\n width: header.width,\n };\n }\n async function decodePng8Gray(bytes, options = {}) {\n const header = readPngHeader(bytes, 8);\n const rowBytes = header.width + 1;\n const raw = await (options.inflate ?? inflateWithDecompressionStream)(header.idat, rowBytes * header.height);\n const values = new Uint8Array(header.width * header.height);\n checkInflatedLength(raw, rowBytes * header.height);\n unfilterGray8(raw, header.width, header.height, values);\n return { height: header.height, values, width: header.width };\n }\n /**\n * Inflates with the platform's `DecompressionStream(\"deflate\")` (zlib\n * framing, which is what PNG uses) straight into one buffer of the expected\n * size, so no second copy joins the output chunks.\n */\n async function inflateWithDecompressionStream(chunks, byteLength) {\n const stream = new DecompressionStream(\"deflate\");\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n // Writers split IDAT into many small chunks (libpng's default is 8 KiB).\n // One write of the joined stream costs a copy of the compressed bytes and\n // saves a stream round trip per chunk.\n const zlib = chunks.length === 1 ? chunks[0] : joinChunks(chunks);\n const writing = (async () => {\n await writer.write(zlib);\n await writer.close();\n })();\n const output = new Uint8Array(byteLength);\n let offset = 0;\n // A corrupt stream rejects both sides; the reader's error is the one thrown.\n writing.catch(() => undefined);\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done)\n break;\n if (offset + value.byteLength > byteLength) {\n throw new RangeError(\"PNG image data is longer than its header says.\");\n }\n output.set(value, offset);\n offset += value.byteLength;\n }\n await writing;\n }\n catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error instanceof RangeError\n ? error\n : new RangeError(`PNG image data does not inflate: ${error instanceof Error ? error.message : String(error)}`);\n }\n return offset === byteLength ? output : output.subarray(0, offset);\n }\n function joinChunks(chunks) {\n const joined = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));\n let offset = 0;\n for (const chunk of chunks) {\n joined.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return joined;\n }\n function readPngHeader(input, bitDepth) {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n if (bytes.byteLength < 8 ||\n PNG_SIGNATURE.some((value, index) => bytes[index] !== value)) {\n throw new RangeError(\"Not a PNG file.\");\n }\n let offset = 8;\n let width = 0;\n let height = 0;\n let sawHeader = false;\n let sawEnd = false;\n const idat = [];\n while (offset + 12 <= bytes.byteLength) {\n const length = view.getUint32(offset);\n const type = view.getUint32(offset + 4);\n const dataStart = offset + 8;\n const dataEnd = dataStart + length;\n if (dataEnd + 4 > bytes.byteLength) {\n throw new RangeError(\"PNG file is truncated.\");\n }\n if (type === 0x49484452) {\n // IHDR\n if (length !== 13)\n throw new RangeError(\"PNG header is malformed.\");\n width = view.getUint32(dataStart);\n height = view.getUint32(dataStart + 4);\n const depth = bytes[dataStart + 8];\n const colorType = bytes[dataStart + 9];\n if (bytes[dataStart + 10] !== 0 || bytes[dataStart + 11] !== 0) {\n throw new RangeError(\"PNG uses an unknown compression or filter.\");\n }\n if (bytes[dataStart + 12] !== 0) {\n throw new RangeError(\"Interlaced PNGs are not supported.\");\n }\n if (colorType !== COLOR_TYPE_GRAY || depth !== bitDepth) {\n throw new RangeError(`Expected a ${bitDepth}-bit grayscale PNG, got bit depth ${depth} with colour type ${colorType}.`);\n }\n sawHeader = true;\n }\n else if (type === 0x49444154) {\n // IDAT\n idat.push(bytes.subarray(dataStart, dataEnd));\n }\n else if (type === 0x49454e44) {\n // IEND\n sawEnd = true;\n break;\n }\n offset = dataEnd + 4;\n }\n if (!sawHeader)\n throw new RangeError(\"PNG has no header.\");\n if (!sawEnd)\n throw new RangeError(\"PNG file is truncated.\");\n if (width === 0 || height === 0 || width * height > MAX_PNG_PIXELS) {\n throw new RangeError(`PNG size ${width}x${height} is not supported.`);\n }\n if (idat.length === 0)\n throw new RangeError(\"PNG has no image data.\");\n return { height, idat, width };\n }\n function checkInflatedLength(raw, expected) {\n if (raw.byteLength !== expected) {\n throw new RangeError(\"PNG image data is shorter than its header says.\");\n }\n }\n /*\n * Undoes the row filters of a 16-bit grayscale image and writes host-order\n * samples, in one pass. Each filter runs on bytes, and the two bytes of a\n * sample never carry into each other, so the loops keep a sample's high and\n * low bytes apart and join them only for the store. The previous row is read\n * back from the output, which already holds it unfiltered; the first row reads\n * a row of zeros, which is what PNG defines above the image.\n *\n * Each filter is its own small function with one loop, called once per row,\n * so engines compile each loop on its own: SpiderMonkey slowed a Paeth loop\n * that shared a function with the other filters.\n */\n function unfilterGray16(raw, width, height, out) {\n const rowBytes = width * 2 + 1;\n const zeroRow = new Uint16Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_16[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow16(raw, s, out, o, end) {\n for (let i = o; i < end; i += 1, s += 2) {\n out[i] = (raw[s] << 8) | raw[s + 1];\n }\n }\n function subRow16(raw, s, out, o, end) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2) {\n hi = (raw[s] + hi) & 0xff;\n lo = (raw[s + 1] + lo) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n function upRow16(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n out[i] = (((raw[s] + (up >> 8)) & 0xff) << 8) | ((raw[s + 1] + up) & 0xff);\n }\n }\n function averageRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n hi = (raw[s] + ((hi + (up >> 8)) >> 1)) & 0xff;\n lo = (raw[s + 1] + ((lo + (up & 0xff)) >> 1)) & 0xff;\n out[i] = (hi << 8) | lo;\n }\n }\n /** Paeth: per byte, whichever of left, up and up-left is closest to left + up - up-left. */\n function paethRow16(raw, s, out, o, end, prev, p) {\n let hi = 0;\n let lo = 0;\n let upLeftHi = 0;\n let upLeftLo = 0;\n for (let i = o; i < end; i += 1, s += 2, p += 1) {\n const up = prev[p];\n const upHi = up >> 8;\n const upLo = up & 0xff;\n let pa = Math.abs(upHi - upLeftHi);\n let pb = Math.abs(hi - upLeftHi);\n let pc = Math.abs(hi + upHi - upLeftHi - upLeftHi);\n hi =\n (raw[s] + (pa <= pb && pa <= pc ? hi : pb <= pc ? upHi : upLeftHi)) &\n 0xff;\n pa = Math.abs(upLo - upLeftLo);\n pb = Math.abs(lo - upLeftLo);\n pc = Math.abs(lo + upLo - upLeftLo - upLeftLo);\n lo =\n (raw[s + 1] + (pa <= pb && pa <= pc ? lo : pb <= pc ? upLo : upLeftLo)) &\n 0xff;\n out[i] = (hi << 8) | lo;\n upLeftHi = upHi;\n upLeftLo = upLo;\n }\n }\n /** Indexed by the PNG filter type byte: None, Sub, Up, Average, Paeth. */\n const ROW_FILTERS_16 = [\n noneRow16,\n subRow16,\n upRow16,\n averageRow16,\n paethRow16,\n ];\n function unfilterGray8(raw, width, height, out) {\n const rowBytes = width + 1;\n const zeroRow = new Uint8Array(width);\n for (let y = 0; y < height; y += 1) {\n const filter = raw[y * rowBytes];\n const unfilterRow = ROW_FILTERS_8[filter];\n if (!unfilterRow) {\n throw new RangeError(`PNG row ${y} has unknown filter ${filter}.`);\n }\n const o = y * width;\n unfilterRow(raw, y * rowBytes + 1, out, o, o + width, y === 0 ? zeroRow : out, y === 0 ? 0 : o - width);\n }\n }\n function noneRow8(raw, s, out, o, end) {\n out.set(raw.subarray(s, s + end - o), o);\n }\n function subRow8(raw, s, out, o, end) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1) {\n left = (raw[s] + left) & 0xff;\n out[i] = left;\n }\n }\n function upRow8(raw, s, out, o, end, prev, p) {\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n out[i] = (raw[s] + prev[p]) & 0xff;\n }\n }\n function averageRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n left = (raw[s] + ((left + prev[p]) >> 1)) & 0xff;\n out[i] = left;\n }\n }\n function paethRow8(raw, s, out, o, end, prev, p) {\n let left = 0;\n let upLeft = 0;\n for (let i = o; i < end; i += 1, s += 1, p += 1) {\n const up = prev[p];\n const pa = Math.abs(up - upLeft);\n const pb = Math.abs(left - upLeft);\n const pc = Math.abs(left + up - upLeft - upLeft);\n left =\n (raw[s] + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft)) & 0xff;\n out[i] = left;\n upLeft = up;\n }\n }\n const ROW_FILTERS_8 = [\n noneRow8,\n subRow8,\n upRow8,\n averageRow8,\n paethRow8,\n ];\n /**\n * WebGL unpacks rows on four-byte boundaries, so an odd width of two-byte\n * texels needs one padding texel per row.\n */\n function padRows(values, width, height) {\n const textureWidth = width + 1;\n const padded = new Uint16Array(textureWidth * height);\n for (let y = 0; y < height; y += 1) {\n padded.set(values.subarray(y * width, (y + 1) * width), y * textureWidth);\n }\n return { bytes: new Uint8Array(padded.buffer), textureWidth };\n }\n\n /**\n * Answers one decode request, in the worker or on the main thread: the reply\n * and the buffers it can transfer. A file that does not decode is an error\n * reply, never a thrown exception, so the caller can tell a bad file from a\n * broken worker.\n */\n async function decodeDepthPreparationRequest(request) {\n const requestId = request.requestId ?? 0;\n try {\n if (request.bitDepth === 8) {\n const decoded = await decodePng8Gray(request.bytes);\n return {\n response: {\n bitDepth: 8,\n height: decoded.height,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer: [decoded.values.buffer],\n };\n }\n const decoded = await decodePng16(request.bytes, {\n padRowsForWebGl: request.padRowsForWebGl,\n });\n const padded = decoded.paddedUpload;\n const decimated = request.decimateBy !== undefined && request.decimateBy >= 2\n ? decimateDepthUpload(decoded.values, decoded.width, decoded.height, Math.floor(request.decimateBy), request.padRowsForWebGl === true)\n : null;\n const transfer = [decoded.values.buffer];\n if (padded)\n transfer.push(padded.bytes.buffer);\n if (decimated)\n transfer.push(decimated.bytes.buffer);\n return {\n response: {\n bitDepth: 16,\n decimatedUpload: decimated\n ? { ...decimated, bytes: decimated.bytes.buffer }\n : undefined,\n height: decoded.height,\n paddedUpload: padded\n ? {\n bytes: padded.bytes.buffer,\n textureWidth: padded.textureWidth,\n }\n : undefined,\n requestId,\n type: DepthPreparationWorkerMessageType.Complete,\n values: decoded.values.buffer,\n width: decoded.width,\n },\n transfer,\n };\n }\n catch (error) {\n return {\n response: {\n error: error instanceof Error ? error.message : \"Unable to decode depth.\",\n requestId,\n type: DepthPreparationWorkerMessageType.Error,\n },\n transfer: [],\n };\n }\n }\n /**\n * Depth for a texture `factor` times smaller on each side, each texel the\n * sample at its block's centre, as the renderer decimates a map larger than\n * the GPU allows.\n */\n function decimateDepthUpload(values, width, height, factor, padRowsForWebGl) {\n if (factor < 2)\n return null;\n const targetWidth = Math.ceil(width / factor);\n const targetHeight = Math.ceil(height / factor);\n // WebGL reads two-byte texels in rows of four bytes.\n const textureWidth = padRowsForWebGl\n ? targetWidth + (targetWidth % 2)\n : targetWidth;\n const texels = new Uint16Array(textureWidth * targetHeight);\n const columns = new Uint32Array(targetWidth);\n for (let x = 0; x < targetWidth; x += 1) {\n columns[x] = Math.min(width - 1, Math.floor(((x + 0.5) * width) / targetWidth));\n }\n for (let y = 0; y < targetHeight; y += 1) {\n const row = Math.min(height - 1, Math.floor(((y + 0.5) * height) / targetHeight)) *\n width;\n const target = y * textureWidth;\n for (let x = 0; x < targetWidth; x += 1) {\n texels[target + x] = values[row + columns[x]];\n }\n }\n return {\n bytes: new Uint8Array(texels.buffer),\n height: targetHeight,\n textureWidth,\n width: targetWidth,\n };\n }\n\n /** Formats whose first plane is 8-bit luma, which is the code as written. */\n const PLANAR_8_BIT_FORMATS = new Set([\n \"I420\",\n \"I420A\",\n \"I422\",\n \"I422A\",\n \"I444\",\n \"I444A\",\n \"NV12\",\n ]);\n const RGB_FORMATS = new Set([\"RGBA\", \"RGBX\", \"BGRA\", \"BGRX\"]);\n /**\n * Copies a decoded preview frame's luma codes out of it, mapped through\n * `correction` when one is given. The frame stays open; its owner closes\n * it.\n */\n async function readVideoFrameLuma(frame, scratch, correction = null) {\n const rect = frame.visibleRect ?? {\n height: frame.codedHeight,\n width: frame.codedWidth,\n x: 0,\n y: 0,\n };\n const { width, height } = rect;\n const format = frame.format;\n const planar = format !== null && PLANAR_8_BIT_FORMATS.has(format);\n let bytes;\n let plane;\n if (planar || (format !== null && RGB_FORMATS.has(format))) {\n const size = frame.allocationSize({ rect });\n if (scratch.buffer.byteLength < size)\n scratch.buffer = new ArrayBuffer(size);\n [plane] = await frame.copyTo(scratch.buffer, { rect });\n bytes = new Uint8Array(scratch.buffer);\n }\n else {\n // A frame in a layout no copy can read is drawn to a canvas, which\n // converts it to RGB as the browser sees fit.\n bytes = drawFrameRgba(frame, width, height);\n }\n const luma = new Uint8Array(width * height);\n const stride = plane?.stride ?? (planar ? width : width * 4);\n const offset = plane?.offset ?? 0;\n if (!planar) {\n // Every RGB layout keeps green second, and luma with neutral chroma\n // converts to three equal channels.\n for (let y = 0; y < height; y += 1) {\n const row = offset + y * stride + 1;\n const target = y * width;\n for (let x = 0; x < width; x += 1) {\n luma[target + x] = bytes[row + x * 4];\n }\n }\n }\n else if (stride === width) {\n luma.set(bytes.subarray(offset, offset + width * height));\n }\n else {\n for (let y = 0; y < height; y += 1) {\n luma.set(bytes.subarray(offset + y * stride, offset + y * stride + width), y * width);\n }\n }\n if (correction) {\n for (let index = 0; index < luma.length; index += 1) {\n luma[index] = correction[luma[index]];\n }\n }\n return { height, luma, path: planar ? \"plane\" : \"rgb\", width };\n }\n function drawFrameRgba(frame, width, height) {\n const canvas = new OffscreenCanvas(width, height);\n const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!context) {\n throw new Error(\"The depth preview needs a 2D canvas to read this frame.\");\n }\n context.drawImage(frame, 0, 0, width, height);\n const { data } = context.getImageData(0, 0, width, height);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n\n const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type === HeatmapPreparationWorkerMessageType.Prepare) {\n prepareHeatmap(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.Decode) {\n void decodeDepthFrame(message);\n return;\n }\n if (message.type === DepthPreparationWorkerMessageType.PreviewLuma) {\n void copyPreviewLuma(message);\n return;\n }\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\n });\n function prepareHeatmap(message) {\n try {\n const data = colorizeHeatmap(message.map, message.renderer);\n const imageData = new ImageData(data, message.map.width, message.map.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n if (imageBitmap) {\n workerScope.postMessage({\n imageBitmap,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageBitmap]);\n return;\n }\n workerScope.postMessage({\n imageData,\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error ? error.message : \"Unable to prepare heatmap.\",\n requestId: message.requestId,\n type: HeatmapPreparationWorkerMessageType.Error,\n });\n }\n }\n /** Decoding waits on the platform's inflate, so replies arrive out of order. */\n async function decodeDepthFrame(message) {\n const { response, transfer } = await decodeDepthPreparationRequest(message);\n workerScope.postMessage(response, transfer);\n }\n const previewLumaScratch = {\n buffer: new ArrayBuffer(0),\n };\n /** Frames arrive in order and are copied in order, through one scratch. */\n let previewLumaQueue = Promise.resolve();\n function copyPreviewLuma(message) {\n previewLumaQueue = previewLumaQueue.then(async () => {\n try {\n const copied = await readVideoFrameLuma(message.frame, previewLumaScratch, message.correction);\n workerScope.postMessage({\n height: copied.height,\n luma: copied.luma.buffer,\n path: copied.path,\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.PreviewLumaComplete,\n width: copied.width,\n }, [copied.luma.buffer]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to copy the depth preview frame.\",\n requestId: message.requestId,\n type: DepthPreparationWorkerMessageType.Error,\n });\n }\n finally {\n message.frame.close();\n }\n });\n return previewLumaQueue;\n }\n function prepareMaskFrame(message) {\n try {\n const regionMaskCoverage = createRegionMaskCoverageFrame(message.job.instructions);\n const coverageTransfers = getRegionMaskCoverageTransfers(regionMaskCoverage);\n const idMaskFrame = createIdMaskRasterFrame(message.job.instructions, message.job.maxRasterWidth);\n if (idMaskFrame) {\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, idMaskFrame.width, idMaskFrame.height, idMaskFrame.data);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ?? [];\n workerScope.postMessage({\n artifactKind: PreparedMaskFrameKind.IdMask,\n fillPalette: idMaskFrame.fillPalette,\n hasStroke: idMaskFrame.hasStroke,\n height: idMaskFrame.height,\n key: message.job.key,\n maxStrokeWidth: idMaskFrame.maxStrokeWidth,\n raster: idMaskFrame.data,\n screenStrokes,\n regionMaskCoverage,\n requestId: message.requestId,\n sourceWidth: idMaskFrame.sourceWidth,\n strokePalette: idMaskFrame.strokePalette,\n strokeWidths: idMaskFrame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n width: idMaskFrame.width,\n }, [\n idMaskFrame.data.buffer,\n idMaskFrame.fillPalette.buffer,\n idMaskFrame.strokePalette.buffer,\n idMaskFrame.strokeWidths.buffer,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions);\n if (!compositedFrame && !regionMaskCoverage) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const preparedPixels = compositedFrame ?? createTransparentCoverageCarrier();\n const imageData = new ImageData(preparedPixels.data, preparedPixels.width, preparedPixels.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskPlane = createIdMaskPlane(message.job.instructions, message.job.maxRasterWidth);\n const idMaskTransfers = idMaskPlane ? [idMaskPlane.data.buffer] : [];\n const screenStrokes = createMaskScreenStrokes(message.job.instructions, preparedPixels.width, preparedPixels.height);\n const strokeTransfers = screenStrokes?.flatMap(({ paths }) => paths.map((path) => path.buffer)) ??\n [];\n if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageBitmap,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n screenStrokes,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [\n imageData.data.buffer,\n ...idMaskTransfers,\n ...strokeTransfers,\n ...coverageTransfers,\n ]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to prepare mask frame.\",\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Error,\n });\n }\n }\n /**\n * A frame that carries only region coverage has nothing to composite, and the\n * RGBA branch still has to produce an artifact for the coverage to ride on.\n */\n function createTransparentCoverageCarrier() {\n return {\n data: new Uint8ClampedArray(new ArrayBuffer(4)),\n height: 1,\n width: 1,\n };\n }\n function getRegionMaskCoverageTransfers(coverage) {\n return coverage?.entries.map(({ data }) => data.buffer) ?? [];\n }\n function createImageBitmapFromImageData(imageData) {\n if (typeof OffscreenCanvas === \"undefined\") {\n return null;\n }\n const canvas = new OffscreenCanvas(imageData.width, imageData.height);\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n context.putImageData(imageData, 0, 0);\n return canvas.transferToImageBitmap();\n }\n\n})();";
|
|
3117
3117
|
|
|
3118
3118
|
const DEFAULT_WORKER_NAME = "supervision-js-render-preparation";
|
|
3119
3119
|
let defaultWorkerUrl;
|
|
@@ -5138,6 +5138,74 @@ function createExactFrameSource(options) {
|
|
|
5138
5138
|
};
|
|
5139
5139
|
}
|
|
5140
5140
|
|
|
5141
|
+
/** Trace closed pixel boundaries, retaining holes and disconnected components. */
|
|
5142
|
+
function traceMaskOutlinePaths(raster, width, height, ids) {
|
|
5143
|
+
const edgesById = new Map();
|
|
5144
|
+
const stride = width + 1;
|
|
5145
|
+
const steps = [1, stride, -1, -stride];
|
|
5146
|
+
const add = (id, vertex, direction) => {
|
|
5147
|
+
let edges = edgesById.get(id);
|
|
5148
|
+
if (!edges)
|
|
5149
|
+
edgesById.set(id, (edges = new Map()));
|
|
5150
|
+
const outgoing = edges.get(vertex);
|
|
5151
|
+
if (outgoing)
|
|
5152
|
+
outgoing.push(direction);
|
|
5153
|
+
else
|
|
5154
|
+
edges.set(vertex, [direction]);
|
|
5155
|
+
};
|
|
5156
|
+
for (let y = 0; y < height; y += 1) {
|
|
5157
|
+
for (let x = 0; x < width; x += 1) {
|
|
5158
|
+
const offset = y * width + x;
|
|
5159
|
+
const id = raster[offset];
|
|
5160
|
+
if (!ids.has(id))
|
|
5161
|
+
continue;
|
|
5162
|
+
const vertex = y * stride + x;
|
|
5163
|
+
if (y === 0 || raster[offset - width] !== id)
|
|
5164
|
+
add(id, vertex, 0);
|
|
5165
|
+
if (x === width - 1 || raster[offset + 1] !== id)
|
|
5166
|
+
add(id, vertex + 1, 1);
|
|
5167
|
+
if (y === height - 1 || raster[offset + width] !== id)
|
|
5168
|
+
add(id, vertex + stride + 1, 2);
|
|
5169
|
+
if (x === 0 || raster[offset - 1] !== id)
|
|
5170
|
+
add(id, vertex + stride, 3);
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
const result = new Map();
|
|
5174
|
+
for (const [id, edges] of edgesById) {
|
|
5175
|
+
const paths = [];
|
|
5176
|
+
while (edges.size > 0) {
|
|
5177
|
+
const start = edges.keys().next().value;
|
|
5178
|
+
let vertex = start;
|
|
5179
|
+
let previousDirection = -1;
|
|
5180
|
+
let firstDirection = -1;
|
|
5181
|
+
const points = [];
|
|
5182
|
+
do {
|
|
5183
|
+
const outgoing = edges.get(vertex);
|
|
5184
|
+
// Turn right at diagonal contacts so separate islands stay separate.
|
|
5185
|
+
const direction = previousDirection < 0
|
|
5186
|
+
? outgoing[0]
|
|
5187
|
+
: [1, 0, 3, 2]
|
|
5188
|
+
.map((turn) => (previousDirection + turn) % 4)
|
|
5189
|
+
.find((candidate) => outgoing.includes(candidate));
|
|
5190
|
+
if (firstDirection < 0)
|
|
5191
|
+
firstDirection = direction;
|
|
5192
|
+
if (direction !== previousDirection)
|
|
5193
|
+
points.push(vertex % stride, Math.floor(vertex / stride));
|
|
5194
|
+
outgoing.splice(outgoing.indexOf(direction), 1);
|
|
5195
|
+
if (outgoing.length === 0)
|
|
5196
|
+
edges.delete(vertex);
|
|
5197
|
+
vertex += steps[direction];
|
|
5198
|
+
previousDirection = direction;
|
|
5199
|
+
} while (vertex !== start);
|
|
5200
|
+
if (previousDirection === firstDirection)
|
|
5201
|
+
points.splice(0, 2);
|
|
5202
|
+
paths.push(new Float32Array(points));
|
|
5203
|
+
}
|
|
5204
|
+
result.set(id, paths);
|
|
5205
|
+
}
|
|
5206
|
+
return result;
|
|
5207
|
+
}
|
|
5208
|
+
|
|
5141
5209
|
/** Builds compact, independent alpha crops so overlapping masks stay exact. */
|
|
5142
5210
|
function createRegionMaskCoverageFrame(instructions) {
|
|
5143
5211
|
const entries = [];
|
|
@@ -5199,7 +5267,9 @@ function compositeMaskFrame(instructions) {
|
|
|
5199
5267
|
}
|
|
5200
5268
|
function createIdMaskRasterFrame(instructions, maxRasterWidth) {
|
|
5201
5269
|
try {
|
|
5202
|
-
return createIdMaskFrame(materializeMaskInstructions(instructions)
|
|
5270
|
+
return createIdMaskFrame(materializeMaskInstructions(instructions).map((instruction) => instruction.stroke?.widthUnit === "screen"
|
|
5271
|
+
? { ...instruction, stroke: undefined }
|
|
5272
|
+
: instruction), {
|
|
5203
5273
|
maxWidth: maxRasterWidth,
|
|
5204
5274
|
});
|
|
5205
5275
|
}
|
|
@@ -5234,10 +5304,47 @@ function createIdMaskPlane(instructions, maxRasterWidth) {
|
|
|
5234
5304
|
function compositeInstruction(rgba, canvasWidth, instruction) {
|
|
5235
5305
|
const fill = resolveRgbaColor(instruction.color, instruction.alpha);
|
|
5236
5306
|
const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);
|
|
5237
|
-
if (instruction.stroke &&
|
|
5307
|
+
if (instruction.stroke &&
|
|
5308
|
+
instruction.stroke.widthUnit !== "screen" &&
|
|
5309
|
+
bounds) {
|
|
5238
5310
|
compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);
|
|
5239
5311
|
}
|
|
5240
5312
|
}
|
|
5313
|
+
function createMaskScreenStrokes(instructions, width, height, preparedRaster) {
|
|
5314
|
+
const outlined = instructions.filter(({ visible, stroke }) => visible !== false &&
|
|
5315
|
+
stroke?.widthUnit === "screen" &&
|
|
5316
|
+
stroke.width > 0 &&
|
|
5317
|
+
stroke.alpha > 0);
|
|
5318
|
+
if (outlined.length === 0)
|
|
5319
|
+
return undefined;
|
|
5320
|
+
const masks = materializeMaskInstructions(instructions);
|
|
5321
|
+
const raster = preparedRaster ?? new Uint32Array(width * height);
|
|
5322
|
+
if (!preparedRaster) {
|
|
5323
|
+
for (const { detectionIndex, mask } of masks) {
|
|
5324
|
+
const counts = decodeCompressedRleCounts(mask.counts);
|
|
5325
|
+
let offset = 0;
|
|
5326
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
5327
|
+
const length = counts[index];
|
|
5328
|
+
if (index % 2 === 1) {
|
|
5329
|
+
for (let pixel = offset; pixel < offset + length; pixel += 1) {
|
|
5330
|
+
const x = Math.floor(pixel / mask.height);
|
|
5331
|
+
const y = pixel % mask.height;
|
|
5332
|
+
raster[y * width + x] = detectionIndex + 1;
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
offset += length;
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
const paths = traceMaskOutlinePaths(raster, width, height, new Set(outlined.map(({ detectionIndex }) => detectionIndex + 1)));
|
|
5340
|
+
return outlined.map(({ detectionIndex, stroke }) => ({
|
|
5341
|
+
detectionIndex,
|
|
5342
|
+
alpha: stroke.alpha,
|
|
5343
|
+
color: stroke.color,
|
|
5344
|
+
width: stroke.width,
|
|
5345
|
+
paths: paths.get(detectionIndex + 1) ?? [],
|
|
5346
|
+
}));
|
|
5347
|
+
}
|
|
5241
5348
|
function materializeMaskInstructions(instructions) {
|
|
5242
5349
|
return instructions
|
|
5243
5350
|
.filter((instruction) => instruction.visible !== false)
|
|
@@ -5531,6 +5638,7 @@ function createMainThreadMaskFramePreparer(workerStatus, message = null) {
|
|
|
5531
5638
|
key: job.key,
|
|
5532
5639
|
kind: PreparedMaskFrameKind.RgbaImage,
|
|
5533
5640
|
regionMaskCoverage,
|
|
5641
|
+
screenStrokes: createMaskScreenStrokes(job.instructions, preparedPixels.width, preparedPixels.height),
|
|
5534
5642
|
source: canvas,
|
|
5535
5643
|
width: preparedPixels.width,
|
|
5536
5644
|
};
|
|
@@ -5566,6 +5674,7 @@ function createPreparedIdMaskFrame(job, regionMaskCoverage) {
|
|
|
5566
5674
|
kind: PreparedMaskFrameKind.IdMask,
|
|
5567
5675
|
maxStrokeWidth: frame.maxStrokeWidth,
|
|
5568
5676
|
raster: frame.data,
|
|
5677
|
+
screenStrokes: createMaskScreenStrokes(job.instructions, frame.width, frame.height, frame.data),
|
|
5569
5678
|
regionMaskCoverage,
|
|
5570
5679
|
sourceWidth: frame.sourceWidth,
|
|
5571
5680
|
strokePalette: frame.strokePalette,
|
|
@@ -5691,6 +5800,7 @@ function createPreparedFrameFromWorkerResponse(message) {
|
|
|
5691
5800
|
maxStrokeWidth: message.maxStrokeWidth ?? 0,
|
|
5692
5801
|
raster: message.raster,
|
|
5693
5802
|
regionMaskCoverage: message.regionMaskCoverage,
|
|
5803
|
+
screenStrokes: message.screenStrokes,
|
|
5694
5804
|
sourceWidth: message.sourceWidth,
|
|
5695
5805
|
strokePalette: message.strokePalette,
|
|
5696
5806
|
strokeWidths: message.strokeWidths,
|
|
@@ -5707,6 +5817,7 @@ function createPreparedFrameFromWorkerResponse(message) {
|
|
|
5707
5817
|
key: message.key,
|
|
5708
5818
|
kind: PreparedMaskFrameKind.RgbaImage,
|
|
5709
5819
|
regionMaskCoverage: message.regionMaskCoverage,
|
|
5820
|
+
screenStrokes: message.screenStrokes,
|
|
5710
5821
|
source: message.imageBitmap,
|
|
5711
5822
|
width: message.imageBitmap.width,
|
|
5712
5823
|
};
|
|
@@ -5732,6 +5843,7 @@ function createPreparedFrameFromWorkerResponse(message) {
|
|
|
5732
5843
|
key: message.key,
|
|
5733
5844
|
kind: PreparedMaskFrameKind.RgbaImage,
|
|
5734
5845
|
regionMaskCoverage: message.regionMaskCoverage,
|
|
5846
|
+
screenStrokes: message.screenStrokes,
|
|
5735
5847
|
source: canvas,
|
|
5736
5848
|
width: message.imageData.width,
|
|
5737
5849
|
};
|
|
@@ -5907,7 +6019,8 @@ function createPreparedRenderWindow(options) {
|
|
|
5907
6019
|
const backingBytes = options.resolveMaskFrameBackingBytes
|
|
5908
6020
|
? options.resolveMaskFrameBackingBytes(maskFrame)
|
|
5909
6021
|
: pixels * (maskFrame.kind === PreparedMaskFrameKind.IdMask ? 1 : 4);
|
|
5910
|
-
const
|
|
6022
|
+
const strokeBytes = maskFrame.screenStrokes?.reduce((total, stroke) => total + stroke.paths.reduce((sum, path) => sum + path.byteLength, 0), 0) ?? 0;
|
|
6023
|
+
const bytes = Math.max(1, payloadBytes + coverageBytes + strokeBytes + backingBytes);
|
|
5911
6024
|
preparedMaskBytesByKey.set(key, bytes);
|
|
5912
6025
|
preparedMaskBytes += bytes;
|
|
5913
6026
|
largestMaskFrameBytesByTier.set(tier, Math.max(largestMaskFrameBytesByTier.get(tier) ?? 0, bytes));
|
|
@@ -9083,6 +9196,11 @@ function resolveRenderPreparationMaxWaitSeconds(value) {
|
|
|
9083
9196
|
*/
|
|
9084
9197
|
const RENDER_PREPARATION_GATE_MAX_STOP_SHARE_OF_RESUME = 0.5;
|
|
9085
9198
|
async function createMediaRendererCore(options, providers) {
|
|
9199
|
+
const pixelInspectionScale = options.pixelInspection?.minScale;
|
|
9200
|
+
if (pixelInspectionScale !== undefined &&
|
|
9201
|
+
(!Number.isFinite(pixelInspectionScale) || pixelInspectionScale <= 0)) {
|
|
9202
|
+
throw new RangeError("pixelInspection.minScale must be a finite number greater than zero.");
|
|
9203
|
+
}
|
|
9086
9204
|
const fit = options.fit ?? MediaRendererFit.Contain;
|
|
9087
9205
|
const initialPlaybackRate = options.playbackRate ?? 1;
|
|
9088
9206
|
if (!Number.isFinite(initialPlaybackRate) || initialPlaybackRate <= 0) {
|
|
@@ -9993,6 +10111,7 @@ async function createMediaRendererCore(options, providers) {
|
|
|
9993
10111
|
markerStyle: currentPresentation.markerStyle,
|
|
9994
10112
|
percentageBarStyle: currentPresentation.percentageBarStyle,
|
|
9995
10113
|
maxDevicePixelRatio: options.maxDevicePixelRatio,
|
|
10114
|
+
pixelInspection: options.pixelInspection,
|
|
9996
10115
|
orientedBoxStyle: currentPresentation.orientedBoxStyle,
|
|
9997
10116
|
onPresentationUpdate(presentedSample) {
|
|
9998
10117
|
if (!runtimeState.isDestroyed()) {
|
|
@@ -10013,6 +10132,9 @@ async function createMediaRendererCore(options, providers) {
|
|
|
10013
10132
|
polygonStyle: currentPresentation.polygonStyle,
|
|
10014
10133
|
polylineStyle: currentPresentation.polylineStyle,
|
|
10015
10134
|
presentedFrames: protectedPresentedFrames?.source,
|
|
10135
|
+
// A source with no duration, such as a still image, has one frame to
|
|
10136
|
+
// show and nothing to play.
|
|
10137
|
+
stillFrame: metadata.duration === 0,
|
|
10016
10138
|
regionRenderers: resolveRegionRenderers(currentPresentation),
|
|
10017
10139
|
heatmapRenderers: resolveHeatmapRenderers(currentPresentation),
|
|
10018
10140
|
depthRenderers: resolveDepthRenderers(currentPresentation),
|
|
@@ -11031,6 +11153,23 @@ function appendDashedPath(graphics, points, closed, dashPattern, viewportScale)
|
|
|
11031
11153
|
return true;
|
|
11032
11154
|
}
|
|
11033
11155
|
|
|
11156
|
+
const cachedOrders = new WeakMap();
|
|
11157
|
+
/** Dense ranks preserve zIndex and frame-order ties across retained shape layers. */
|
|
11158
|
+
function detectionRenderOrder(frame) {
|
|
11159
|
+
const cached = cachedOrders.get(frame);
|
|
11160
|
+
if (cached)
|
|
11161
|
+
return cached;
|
|
11162
|
+
const indexes = frame.detections.map((_, index) => index);
|
|
11163
|
+
indexes.sort((left, right) => (frame.detections[left].zIndex ?? left) -
|
|
11164
|
+
(frame.detections[right].zIndex ?? right) || left - right);
|
|
11165
|
+
const order = [];
|
|
11166
|
+
indexes.forEach((index, rank) => {
|
|
11167
|
+
order[index] = rank;
|
|
11168
|
+
});
|
|
11169
|
+
cachedOrders.set(frame, order);
|
|
11170
|
+
return order;
|
|
11171
|
+
}
|
|
11172
|
+
|
|
11034
11173
|
function createPixiBoxLayer(options) {
|
|
11035
11174
|
let boxStyle = options.boxStyle === undefined ? new BaseBoxStyle() : options.boxStyle;
|
|
11036
11175
|
let boxGraphics;
|
|
@@ -11042,7 +11181,7 @@ function createPixiBoxLayer(options) {
|
|
|
11042
11181
|
let hasDrawnDetectionFrame = false;
|
|
11043
11182
|
let isDirty = true;
|
|
11044
11183
|
let lastViewportScale = 0;
|
|
11045
|
-
let retainedContainer;
|
|
11184
|
+
let retainedContainer = options.container;
|
|
11046
11185
|
const retainedEntries = new Map();
|
|
11047
11186
|
const invalidatedDetections = new Set();
|
|
11048
11187
|
const usingRetainedEntries = Boolean(options.Container && options.Graphics);
|
|
@@ -11098,7 +11237,9 @@ function createPixiBoxLayer(options) {
|
|
|
11098
11237
|
activeKeys.add(key);
|
|
11099
11238
|
const display = ensureRetainedEntry(key);
|
|
11100
11239
|
display.visible = true;
|
|
11101
|
-
display.zIndex =
|
|
11240
|
+
display.zIndex = options.container
|
|
11241
|
+
? detectionRenderOrder(detectionFrame)[detectionIndex] * 2
|
|
11242
|
+
: (detection.zIndex ?? detectionIndex);
|
|
11102
11243
|
display.position?.set?.(0, 0);
|
|
11103
11244
|
display.clear();
|
|
11104
11245
|
drawBoxInstruction(display, instruction, viewportScale);
|
|
@@ -13424,7 +13565,6 @@ function createPixiInteractionLayer(options) {
|
|
|
13424
13565
|
let hoveredPick = null;
|
|
13425
13566
|
let hoveredPickKey = null;
|
|
13426
13567
|
let selectedPick = null;
|
|
13427
|
-
let selectedPickKey = null;
|
|
13428
13568
|
let selectedPicks = [];
|
|
13429
13569
|
let marqueeStart = null;
|
|
13430
13570
|
let marqueeRect = null;
|
|
@@ -13477,9 +13617,19 @@ function createPixiInteractionLayer(options) {
|
|
|
13477
13617
|
setSelectedPick(null);
|
|
13478
13618
|
return null;
|
|
13479
13619
|
}
|
|
13480
|
-
const
|
|
13481
|
-
|
|
13482
|
-
|
|
13620
|
+
const nextPicks = selection.detectionIds
|
|
13621
|
+
? [...new Set(selection.detectionIds)].flatMap((detectionId) => {
|
|
13622
|
+
const pick = createPickFromSelection({
|
|
13623
|
+
detectionId,
|
|
13624
|
+
mediaTime: selection.mediaTime,
|
|
13625
|
+
point: selection.point,
|
|
13626
|
+
target: selection.target,
|
|
13627
|
+
});
|
|
13628
|
+
return pick ? [pick] : [];
|
|
13629
|
+
})
|
|
13630
|
+
: [createPickFromSelection(selection)].filter((pick) => pick !== null);
|
|
13631
|
+
setSelectedPicks(nextPicks);
|
|
13632
|
+
return nextPicks.at(-1) ?? null;
|
|
13483
13633
|
},
|
|
13484
13634
|
getState() {
|
|
13485
13635
|
return createState();
|
|
@@ -13606,6 +13756,18 @@ function createPixiInteractionLayer(options) {
|
|
|
13606
13756
|
if (event.button !== undefined && event.button !== 0) {
|
|
13607
13757
|
return;
|
|
13608
13758
|
}
|
|
13759
|
+
if (options.interaction.multiSelect &&
|
|
13760
|
+
event.shiftKey &&
|
|
13761
|
+
pick &&
|
|
13762
|
+
!options.editingEngine.hasCreationTool() &&
|
|
13763
|
+
options.editingEngine.getState().kind ===
|
|
13764
|
+
AnnotationGestureStateKind.Idle) {
|
|
13765
|
+
// Selection modifiers must run before the editing engine captures a
|
|
13766
|
+
// move. The following pointertap belongs to this same toggle.
|
|
13767
|
+
toggleSelectedPick(pick);
|
|
13768
|
+
suppressNextTap = true;
|
|
13769
|
+
return;
|
|
13770
|
+
}
|
|
13609
13771
|
// Keep selection owned by this layer. The editing engine deliberately
|
|
13610
13772
|
// treats a click as a cancelled move, so it cannot establish selection
|
|
13611
13773
|
// itself; selecting here also makes resize handles available on the next
|
|
@@ -13645,7 +13807,7 @@ function createPixiInteractionLayer(options) {
|
|
|
13645
13807
|
if (!marqueeStart)
|
|
13646
13808
|
return;
|
|
13647
13809
|
if (didDragMarquee && marqueeRect && activeFrame) {
|
|
13648
|
-
|
|
13810
|
+
const nextPicks = activeFrame.detections.flatMap((detection, detectionIndex) => {
|
|
13649
13811
|
if (!canPickDetection(detection))
|
|
13650
13812
|
return [];
|
|
13651
13813
|
const rect = getDetectionRect(detection);
|
|
@@ -13666,10 +13828,7 @@ function createPixiInteractionLayer(options) {
|
|
|
13666
13828
|
},
|
|
13667
13829
|
];
|
|
13668
13830
|
});
|
|
13669
|
-
|
|
13670
|
-
selectedPickKey = createDetectionPickKey(selectedPick);
|
|
13671
|
-
options.interaction.onSelect?.(selectedPick);
|
|
13672
|
-
options.interaction.onSelectionChange?.(selectedPicks);
|
|
13831
|
+
setSelectedPicks(nextPicks);
|
|
13673
13832
|
}
|
|
13674
13833
|
marqueeStart = null;
|
|
13675
13834
|
marqueeRect = null;
|
|
@@ -13696,7 +13855,9 @@ function createPixiInteractionLayer(options) {
|
|
|
13696
13855
|
padding: pickPadding,
|
|
13697
13856
|
viewportScale: options.getViewportScale?.() ?? 1,
|
|
13698
13857
|
});
|
|
13699
|
-
if (geometryPick &&
|
|
13858
|
+
if (geometryPick &&
|
|
13859
|
+
(options.interaction.preferTopmost ||
|
|
13860
|
+
POINT_PRECISE_PICK_TARGETS.has(geometryPick.target))) {
|
|
13700
13861
|
return geometryPick;
|
|
13701
13862
|
}
|
|
13702
13863
|
const maskPick = options.pickMaskDetectionAtPoint?.(pickPoint, currentMediaTime);
|
|
@@ -13763,7 +13924,6 @@ function createPixiInteractionLayer(options) {
|
|
|
13763
13924
|
if (nextKeys === currentKeys) {
|
|
13764
13925
|
return;
|
|
13765
13926
|
}
|
|
13766
|
-
selectedPickKey = createDetectionPickKey(selectedPick);
|
|
13767
13927
|
// A followed pick re-bases onto every new frame, so its full pick key
|
|
13768
13928
|
// changes at playback rate. The scene's normal frame path consumes this
|
|
13769
13929
|
// refreshed state immediately; emitting onStateChange here would redraw
|
|
@@ -13795,15 +13955,17 @@ function createPixiInteractionLayer(options) {
|
|
|
13795
13955
|
return true;
|
|
13796
13956
|
}
|
|
13797
13957
|
function setSelectedPick(nextPick) {
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13958
|
+
setSelectedPicks(nextPick ? [nextPick] : []);
|
|
13959
|
+
}
|
|
13960
|
+
function setSelectedPicks(nextPicks) {
|
|
13961
|
+
const unchanged = selectedPicks.length === nextPicks.length &&
|
|
13962
|
+
selectedPicks.every((pick, index) => createDetectionPickKey(pick) ===
|
|
13963
|
+
createDetectionPickKey(nextPicks[index]));
|
|
13964
|
+
selectedPicks = nextPicks;
|
|
13965
|
+
selectedPick = nextPicks.at(-1) ?? null;
|
|
13966
|
+
if (unchanged)
|
|
13801
13967
|
return;
|
|
13802
|
-
|
|
13803
|
-
selectedPick = nextPick;
|
|
13804
|
-
selectedPicks = nextPick ? [nextPick] : [];
|
|
13805
|
-
selectedPickKey = nextKey;
|
|
13806
|
-
options.interaction.onSelect?.(nextPick);
|
|
13968
|
+
options.interaction.onSelect?.(selectedPick);
|
|
13807
13969
|
options.interaction.onSelectionChange?.(selectedPicks);
|
|
13808
13970
|
notifyStateChange();
|
|
13809
13971
|
}
|
|
@@ -13822,17 +13984,20 @@ function createPixiInteractionLayer(options) {
|
|
|
13822
13984
|
function toggleSelectedPick(pick) {
|
|
13823
13985
|
if (!pick)
|
|
13824
13986
|
return;
|
|
13825
|
-
const key =
|
|
13826
|
-
const index = selectedPicks.findIndex((candidate) =>
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13987
|
+
const key = createSelectionKey(pick);
|
|
13988
|
+
const index = selectedPicks.findIndex((candidate) => createSelectionKey(candidate) === key);
|
|
13989
|
+
setSelectedPicks(index >= 0
|
|
13990
|
+
? selectedPicks.filter((_, candidateIndex) => candidateIndex !== index)
|
|
13991
|
+
: [...selectedPicks, pick]);
|
|
13992
|
+
}
|
|
13993
|
+
function createSelectionKey(pick) {
|
|
13994
|
+
// Selection toggles the detection, regardless of whether its body, label,
|
|
13995
|
+
// or keypoint was hit. Keep the picker's frame/index fallback for duplicate ids.
|
|
13996
|
+
return createDetectionPickKey({
|
|
13997
|
+
...pick,
|
|
13998
|
+
geometryIndex: undefined,
|
|
13999
|
+
target: DetectionPickTarget.Box,
|
|
14000
|
+
});
|
|
13836
14001
|
}
|
|
13837
14002
|
function getPoint(event) {
|
|
13838
14003
|
if (!container)
|
|
@@ -13931,6 +14096,75 @@ function intersects(rect, marquee) {
|
|
|
13931
14096
|
top + rect.height >= marquee.y);
|
|
13932
14097
|
}
|
|
13933
14098
|
|
|
14099
|
+
function createPixiMaskScreenStrokeRenderer(options) {
|
|
14100
|
+
const display = new options.Graphics();
|
|
14101
|
+
display.visible = false;
|
|
14102
|
+
let lastFrame;
|
|
14103
|
+
let lastSignature = "";
|
|
14104
|
+
const interactionPaths = new WeakMap();
|
|
14105
|
+
function hide() {
|
|
14106
|
+
display.visible = false;
|
|
14107
|
+
if (lastFrame)
|
|
14108
|
+
display.clear();
|
|
14109
|
+
lastFrame = undefined;
|
|
14110
|
+
lastSignature = "";
|
|
14111
|
+
}
|
|
14112
|
+
return {
|
|
14113
|
+
display,
|
|
14114
|
+
hide,
|
|
14115
|
+
destroy() {
|
|
14116
|
+
display.destroy();
|
|
14117
|
+
},
|
|
14118
|
+
render(frame, viewportScale, overrides) {
|
|
14119
|
+
let strokes = frame.screenStrokes ?? [];
|
|
14120
|
+
if (overrides) {
|
|
14121
|
+
strokes = overrides.flatMap(({ detectionIndex, stroke }) => {
|
|
14122
|
+
if (stroke?.widthUnit !== "screen" ||
|
|
14123
|
+
stroke.width <= 0 ||
|
|
14124
|
+
stroke.alpha <= 0)
|
|
14125
|
+
return [];
|
|
14126
|
+
let paths = frame.screenStrokes?.find((entry) => entry.detectionIndex === detectionIndex)?.paths;
|
|
14127
|
+
if (!paths && frame.kind === PreparedMaskFrameKind.IdMask) {
|
|
14128
|
+
let cached = interactionPaths.get(frame);
|
|
14129
|
+
if (!cached) {
|
|
14130
|
+
const ids = new Set(frame.raster);
|
|
14131
|
+
ids.delete(0);
|
|
14132
|
+
cached = traceMaskOutlinePaths(frame.raster, frame.width, frame.height, ids);
|
|
14133
|
+
interactionPaths.set(frame, cached);
|
|
14134
|
+
}
|
|
14135
|
+
paths = cached.get(detectionIndex + 1);
|
|
14136
|
+
}
|
|
14137
|
+
return paths ? [{ ...stroke, detectionIndex, paths }] : [];
|
|
14138
|
+
});
|
|
14139
|
+
}
|
|
14140
|
+
if (strokes.length === 0) {
|
|
14141
|
+
hide();
|
|
14142
|
+
return;
|
|
14143
|
+
}
|
|
14144
|
+
const signature = `${viewportScale}:${strokes.map(({ detectionIndex, width, color, alpha }) => `${detectionIndex}:${width}:${color}:${alpha}`).join(";")}`;
|
|
14145
|
+
if (lastFrame !== frame || lastSignature !== signature) {
|
|
14146
|
+
display.clear();
|
|
14147
|
+
const scaleX = options.mediaWidth / frame.width;
|
|
14148
|
+
const scaleY = options.mediaHeight / frame.height;
|
|
14149
|
+
for (const stroke of strokes) {
|
|
14150
|
+
for (const path of stroke.paths) {
|
|
14151
|
+
if (path.length < 6)
|
|
14152
|
+
continue;
|
|
14153
|
+
display.moveTo(path[0] * scaleX, path[1] * scaleY);
|
|
14154
|
+
for (let index = 2; index < path.length; index += 2)
|
|
14155
|
+
display.lineTo(path[index] * scaleX, path[index + 1] * scaleY);
|
|
14156
|
+
display.closePath();
|
|
14157
|
+
}
|
|
14158
|
+
display.stroke(resolvePixiStroke({ ...stroke, join: "round" }, viewportScale));
|
|
14159
|
+
}
|
|
14160
|
+
lastFrame = frame;
|
|
14161
|
+
lastSignature = signature;
|
|
14162
|
+
}
|
|
14163
|
+
display.visible = true;
|
|
14164
|
+
},
|
|
14165
|
+
};
|
|
14166
|
+
}
|
|
14167
|
+
|
|
13934
14168
|
const coverageBody = `
|
|
13935
14169
|
return clamp(0.5 + (width - distance) / pixelWidth, 0.0, 1.0)
|
|
13936
14170
|
- clamp(0.5 - (width + distance) / pixelWidth, 0.0, 1.0);
|
|
@@ -14504,7 +14738,7 @@ function detectionKey$1(detection, detectionIndex) {
|
|
|
14504
14738
|
}
|
|
14505
14739
|
|
|
14506
14740
|
function createPixiVectorLayer(options) {
|
|
14507
|
-
let container;
|
|
14741
|
+
let container = options.container;
|
|
14508
14742
|
let polygonStyle = options.polygonStyle === undefined
|
|
14509
14743
|
? new BasePolygonStyle()
|
|
14510
14744
|
: options.polygonStyle;
|
|
@@ -14530,8 +14764,8 @@ function createPixiVectorLayer(options) {
|
|
|
14530
14764
|
createContainer() {
|
|
14531
14765
|
if (!container) {
|
|
14532
14766
|
container = new options.Container();
|
|
14533
|
-
container.sortableChildren = true;
|
|
14534
14767
|
}
|
|
14768
|
+
container.sortableChildren = true;
|
|
14535
14769
|
return container;
|
|
14536
14770
|
},
|
|
14537
14771
|
drawFrame(mediaTime, viewportScale = 1) {
|
|
@@ -14645,7 +14879,10 @@ function createPixiVectorLayer(options) {
|
|
|
14645
14879
|
polygon,
|
|
14646
14880
|
polyline,
|
|
14647
14881
|
shapes,
|
|
14648
|
-
|
|
14882
|
+
// Vector details stay above their own box even if the box is retained later.
|
|
14883
|
+
zIndex: options.container
|
|
14884
|
+
? detectionRenderOrder(frame)[detectionIndex] * 2 + 1
|
|
14885
|
+
: (detection.zIndex ?? detectionIndex),
|
|
14649
14886
|
};
|
|
14650
14887
|
}
|
|
14651
14888
|
function drawDetection(entry, detection, viewportScale) {
|
|
@@ -14787,6 +15024,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
14787
15024
|
let interactionStyle = options.interactionStyle === undefined
|
|
14788
15025
|
? new BaseInteractionStyle()
|
|
14789
15026
|
: options.interactionStyle;
|
|
15027
|
+
let displayContainer;
|
|
14790
15028
|
let mediaHeight = 0;
|
|
14791
15029
|
let mediaWidth = 0;
|
|
14792
15030
|
let syntheticFrame;
|
|
@@ -14794,6 +15032,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
14794
15032
|
let currentMediaTime = 0;
|
|
14795
15033
|
let viewportScale = 1;
|
|
14796
15034
|
let maskRenderer;
|
|
15035
|
+
let screenStrokeRenderer;
|
|
14797
15036
|
let isDestroyed = false;
|
|
14798
15037
|
const syntheticTimeline = createSyntheticTimeline(() => syntheticFrame);
|
|
14799
15038
|
const boxStyle = createInteractionBoxStyle(() => activePicks);
|
|
@@ -14834,6 +15073,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
14834
15073
|
container.addChild(maskRenderer.mesh);
|
|
14835
15074
|
}
|
|
14836
15075
|
container.addChild(boxGraphics, vectors, labels);
|
|
15076
|
+
displayContainer = container;
|
|
14837
15077
|
return container;
|
|
14838
15078
|
},
|
|
14839
15079
|
destroy() {
|
|
@@ -14844,6 +15084,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
14844
15084
|
syntheticFrame = undefined;
|
|
14845
15085
|
activePicks = [];
|
|
14846
15086
|
maskRenderer?.destroy();
|
|
15087
|
+
screenStrokeRenderer?.destroy();
|
|
14847
15088
|
labelLayer.destroy();
|
|
14848
15089
|
vectorLayer.destroy();
|
|
14849
15090
|
},
|
|
@@ -14952,6 +15193,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
14952
15193
|
syntheticFrame = undefined;
|
|
14953
15194
|
activePicks = [];
|
|
14954
15195
|
maskRenderer?.hide();
|
|
15196
|
+
screenStrokeRenderer?.hide();
|
|
14955
15197
|
boxLayer.setBoxStyle(null);
|
|
14956
15198
|
boxLayer.drawFrame(currentMediaTime, viewportScale);
|
|
14957
15199
|
labelLayer.setLabelStyle(null);
|
|
@@ -14966,6 +15208,7 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
14966
15208
|
function drawMasks(artifact) {
|
|
14967
15209
|
if (!maskRenderer || !artifact) {
|
|
14968
15210
|
maskRenderer?.hide();
|
|
15211
|
+
screenStrokeRenderer?.hide();
|
|
14969
15212
|
return;
|
|
14970
15213
|
}
|
|
14971
15214
|
const instructions = activePicks
|
|
@@ -14993,9 +15236,23 @@ function createPixiInteractionPresentationLayer(options) {
|
|
|
14993
15236
|
.filter((instruction) => Boolean(instruction));
|
|
14994
15237
|
if (instructions.length === 0) {
|
|
14995
15238
|
maskRenderer.hide();
|
|
15239
|
+
screenStrokeRenderer?.hide();
|
|
14996
15240
|
return;
|
|
14997
15241
|
}
|
|
14998
15242
|
maskRenderer.render(artifact.frame, artifact.texture, instructions);
|
|
15243
|
+
if (!screenStrokeRenderer &&
|
|
15244
|
+
instructions.some(({ instruction }) => instruction.stroke?.widthUnit === "screen")) {
|
|
15245
|
+
screenStrokeRenderer = createPixiMaskScreenStrokeRenderer({
|
|
15246
|
+
Graphics: options.Graphics,
|
|
15247
|
+
mediaWidth,
|
|
15248
|
+
mediaHeight,
|
|
15249
|
+
});
|
|
15250
|
+
displayContainer?.addChildAt(screenStrokeRenderer.display, maskRenderer ? 1 : 0);
|
|
15251
|
+
}
|
|
15252
|
+
screenStrokeRenderer?.render(artifact.frame, viewportScale, instructions.map(({ detectionIndex, instruction }) => ({
|
|
15253
|
+
detectionIndex,
|
|
15254
|
+
stroke: instruction.stroke,
|
|
15255
|
+
})));
|
|
14999
15256
|
}
|
|
15000
15257
|
function createMaskRenderer() {
|
|
15001
15258
|
if (!options.ImageSource ||
|
|
@@ -15237,7 +15494,9 @@ function createInteractionMaskRenderer(options) {
|
|
|
15237
15494
|
continue;
|
|
15238
15495
|
}
|
|
15239
15496
|
writeIdMaskPaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
|
|
15240
|
-
if (instruction.stroke &&
|
|
15497
|
+
if (instruction.stroke &&
|
|
15498
|
+
instruction.stroke.widthUnit !== "screen" &&
|
|
15499
|
+
instruction.stroke.width > 0) {
|
|
15241
15500
|
writeIdMaskPaletteEntry(strokePalette, maskId, instruction.stroke.color, instruction.stroke.alpha);
|
|
15242
15501
|
strokeWidths[maskId] = resolveIdMaskStrokeTexels(instruction.stroke.width, frame.sourceWidth, frame.width);
|
|
15243
15502
|
maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidths[maskId] ?? 0);
|
|
@@ -15747,6 +16006,8 @@ function createPixiMaskLayer(options) {
|
|
|
15747
16006
|
let haloRenderer;
|
|
15748
16007
|
let currentMaskHaloStyle = options.maskHaloStyle ?? null;
|
|
15749
16008
|
let maskSprite;
|
|
16009
|
+
let maskContainer;
|
|
16010
|
+
let screenStrokeRenderer;
|
|
15750
16011
|
let visibleMaskFrameKey = null;
|
|
15751
16012
|
let activeIdMaskFrame = null;
|
|
15752
16013
|
let activeRgbaMaskFrame = null;
|
|
@@ -15802,7 +16063,7 @@ function createPixiMaskLayer(options) {
|
|
|
15802
16063
|
if (!idMaskRenderer || !options.Container) {
|
|
15803
16064
|
return maskSprite;
|
|
15804
16065
|
}
|
|
15805
|
-
|
|
16066
|
+
maskContainer = new options.Container();
|
|
15806
16067
|
// The halo draws beneath the mask so the glow bleeds outward from the
|
|
15807
16068
|
// silhouette while fills and borders stay crisp on top.
|
|
15808
16069
|
if (haloRenderer) {
|
|
@@ -15811,14 +16072,21 @@ function createPixiMaskLayer(options) {
|
|
|
15811
16072
|
maskContainer.addChild(maskSprite, idMaskRenderer.mesh);
|
|
15812
16073
|
return maskContainer;
|
|
15813
16074
|
},
|
|
15814
|
-
drawFrame(mediaTime, presentedFrameId = null) {
|
|
16075
|
+
drawFrame(mediaTime, presentedFrameId = null, viewportScale = 1) {
|
|
15815
16076
|
const preparedFrame = preparedRenderWindow.getFrame(mediaTime);
|
|
15816
16077
|
if (preparedFrame?.maskFrame) {
|
|
15817
|
-
showMaskFrame(preparedFrame.maskFrame, preparedFrame.detectionFrame.mediaTime, presentedFrameId);
|
|
16078
|
+
showMaskFrame(preparedFrame.maskFrame, preparedFrame.detectionFrame.mediaTime, presentedFrameId, viewportScale);
|
|
15818
16079
|
return;
|
|
15819
16080
|
}
|
|
15820
16081
|
hideSprite();
|
|
15821
16082
|
},
|
|
16083
|
+
redrawViewportStyles(viewportScale) {
|
|
16084
|
+
if (!isFillVisible)
|
|
16085
|
+
return;
|
|
16086
|
+
const frame = activeIdMaskFrame ?? activeRgbaMaskFrame;
|
|
16087
|
+
if (frame)
|
|
16088
|
+
screenStrokeRenderer?.render(frame, viewportScale);
|
|
16089
|
+
},
|
|
15822
16090
|
getDrawnState() {
|
|
15823
16091
|
return {
|
|
15824
16092
|
drawnFrameTime: visibleMaskMediaTime,
|
|
@@ -15936,10 +16204,11 @@ function createPixiMaskLayer(options) {
|
|
|
15936
16204
|
preparedRenderWindow.destroy();
|
|
15937
16205
|
destroyTextures();
|
|
15938
16206
|
idMaskRenderer?.destroy();
|
|
16207
|
+
screenStrokeRenderer?.destroy();
|
|
15939
16208
|
haloRenderer?.destroy();
|
|
15940
16209
|
},
|
|
15941
16210
|
};
|
|
15942
|
-
function showMaskFrame(maskFrame, mediaTime, presentedFrameId) {
|
|
16211
|
+
function showMaskFrame(maskFrame, mediaTime, presentedFrameId, viewportScale) {
|
|
15943
16212
|
visibleMaskMediaTime = mediaTime;
|
|
15944
16213
|
visibleMaskFrameKey = maskFrame.key;
|
|
15945
16214
|
visibleMaskFrameId = presentedFrameId;
|
|
@@ -15953,6 +16222,18 @@ function createPixiMaskLayer(options) {
|
|
|
15953
16222
|
hideFill();
|
|
15954
16223
|
return;
|
|
15955
16224
|
}
|
|
16225
|
+
if (!screenStrokeRenderer &&
|
|
16226
|
+
maskFrame.screenStrokes?.length &&
|
|
16227
|
+
options.Graphics) {
|
|
16228
|
+
screenStrokeRenderer = createPixiMaskScreenStrokeRenderer({
|
|
16229
|
+
Graphics: options.Graphics,
|
|
16230
|
+
mediaWidth,
|
|
16231
|
+
mediaHeight,
|
|
16232
|
+
});
|
|
16233
|
+
screenStrokeRenderer.display.alpha = maskOpacity;
|
|
16234
|
+
maskContainer?.addChild(screenStrokeRenderer.display);
|
|
16235
|
+
}
|
|
16236
|
+
screenStrokeRenderer?.render(maskFrame, viewportScale);
|
|
15956
16237
|
if (maskFrame.kind === PreparedMaskFrameKind.IdMask && idMaskRenderer) {
|
|
15957
16238
|
showIdMaskFrame(maskFrame);
|
|
15958
16239
|
return;
|
|
@@ -16084,6 +16365,7 @@ function createPixiMaskLayer(options) {
|
|
|
16084
16365
|
maskSprite.visible = false;
|
|
16085
16366
|
}
|
|
16086
16367
|
idMaskRenderer?.hide();
|
|
16368
|
+
screenStrokeRenderer?.hide();
|
|
16087
16369
|
}
|
|
16088
16370
|
/**
|
|
16089
16371
|
* Everything the layer hands other layers is a view of the raster on screen,
|
|
@@ -16103,6 +16385,8 @@ function createPixiMaskLayer(options) {
|
|
|
16103
16385
|
maskSprite.alpha = maskOpacity;
|
|
16104
16386
|
}
|
|
16105
16387
|
idMaskRenderer?.setOpacity(maskOpacity);
|
|
16388
|
+
if (screenStrokeRenderer)
|
|
16389
|
+
screenStrokeRenderer.display.alpha = maskOpacity;
|
|
16106
16390
|
}
|
|
16107
16391
|
function destroyTexture(key) {
|
|
16108
16392
|
destroyHaloTexture(key);
|
|
@@ -16399,9 +16683,9 @@ function createPixiMaskBrushPreview(options) {
|
|
|
16399
16683
|
sprite.width = editor.canvas.width;
|
|
16400
16684
|
sprite.height = editor.canvas.height;
|
|
16401
16685
|
sprite.alpha = options.preview.alpha ?? 0.4;
|
|
16402
|
-
sprite.tint = options.preview.color ?? 0x22c55e;
|
|
16403
16686
|
display.addChild(sprite, cursor);
|
|
16404
16687
|
const updateTexture = () => {
|
|
16688
|
+
sprite.tint = options.preview.color ?? 0x22c55e;
|
|
16405
16689
|
source.update();
|
|
16406
16690
|
scheduleInvalidation();
|
|
16407
16691
|
};
|
|
@@ -16437,6 +16721,7 @@ function createPixiMaskBrushPreview(options) {
|
|
|
16437
16721
|
});
|
|
16438
16722
|
}
|
|
16439
16723
|
function drawCursor() {
|
|
16724
|
+
sprite.tint = options.preview.color ?? 0x22c55e;
|
|
16440
16725
|
cursor.clear();
|
|
16441
16726
|
const state = editor.getCursor();
|
|
16442
16727
|
if (!state.point)
|
|
@@ -17178,6 +17463,13 @@ function createPixiRegionLayer(options) {
|
|
|
17178
17463
|
}
|
|
17179
17464
|
return { activeDetectionIndexes: [...activeDetectionIndexes] };
|
|
17180
17465
|
},
|
|
17466
|
+
hasAnimatedDisplay() {
|
|
17467
|
+
for (const entry of entries.values()) {
|
|
17468
|
+
if (isAnimatedDisplay(entry.display))
|
|
17469
|
+
return true;
|
|
17470
|
+
}
|
|
17471
|
+
return false;
|
|
17472
|
+
},
|
|
17181
17473
|
releaseMaskTexture(texture) {
|
|
17182
17474
|
if (destroyed)
|
|
17183
17475
|
return;
|
|
@@ -17500,12 +17792,15 @@ function destroyEntry(entry) {
|
|
|
17500
17792
|
// lightweight crop texture; the scene destroys the shared source.
|
|
17501
17793
|
entry.ownedTexture?.destroy(false);
|
|
17502
17794
|
}
|
|
17795
|
+
function isAnimatedDisplay(display) {
|
|
17796
|
+
return "play" in display && typeof display.play === "function";
|
|
17797
|
+
}
|
|
17503
17798
|
function pauseAnimatedDisplay(display) {
|
|
17504
17799
|
if ("stop" in display && typeof display.stop === "function")
|
|
17505
17800
|
display.stop();
|
|
17506
17801
|
}
|
|
17507
17802
|
function resumeAnimatedDisplay(display) {
|
|
17508
|
-
if (
|
|
17803
|
+
if (isAnimatedDisplay(display))
|
|
17509
17804
|
display.play();
|
|
17510
17805
|
}
|
|
17511
17806
|
function matchesTarget(renderer, detection, context) {
|
|
@@ -17973,6 +18268,7 @@ function drawLoading(graphics, context, style) {
|
|
|
17973
18268
|
const rect = getDetectionRect(detection);
|
|
17974
18269
|
if (!rect)
|
|
17975
18270
|
continue;
|
|
18271
|
+
graphics.beginPath();
|
|
17976
18272
|
graphics.arc(rect.x, rect.y, resolveScreenLength(10, context.viewportScale), angle, angle + Math.PI * 1.5);
|
|
17977
18273
|
const stroke = resolvePixiStroke(style.loading.stroke, context.viewportScale);
|
|
17978
18274
|
graphics.stroke({
|
|
@@ -18205,6 +18501,11 @@ function observePixiContainerResize(container, resize) {
|
|
|
18205
18501
|
* sample it wants, hands it to `presentSample`, and Pixi's ticker paints the
|
|
18206
18502
|
* scene every frame.
|
|
18207
18503
|
*
|
|
18504
|
+
* A still frame is pulled once and then renders on change the way push
|
|
18505
|
+
* presentation does, with the sample it presents as one more change. The
|
|
18506
|
+
* ticker runs only while a looping GIF region or a loading spinner is on the
|
|
18507
|
+
* frame.
|
|
18508
|
+
*
|
|
18208
18509
|
* Push presentation engages when the opened source carries a presented-frame
|
|
18209
18510
|
* channel. The producer then decides which frame is on screen and announces it,
|
|
18210
18511
|
* so the ticker paints nothing and every render is an explicit answer to a
|
|
@@ -18235,6 +18536,18 @@ async function createPixiMediaScene(options) {
|
|
|
18235
18536
|
let maxTextureSide;
|
|
18236
18537
|
const maxTextureSize = () => (maxTextureSide ??= queryMaxTextureSize(app.renderer));
|
|
18237
18538
|
const frameChannel = options.presentedFrames;
|
|
18539
|
+
/**
|
|
18540
|
+
* A still frame has nothing to play, so it renders on change exactly as push
|
|
18541
|
+
* presentation does. The ticker runs only while something on the frame
|
|
18542
|
+
* animates by the clock.
|
|
18543
|
+
*/
|
|
18544
|
+
const stillFrame = frameChannel === undefined && options.stillFrame === true;
|
|
18545
|
+
const rendersOnChange = frameChannel !== undefined || stillFrame;
|
|
18546
|
+
let stillFrameTickerRunning = false;
|
|
18547
|
+
/** The content of the `previewOverlay` answer the overlay on screen drew. */
|
|
18548
|
+
let drawnPreviewOverlayContent = null;
|
|
18549
|
+
/** The editing state the overlay on screen was drawn from. */
|
|
18550
|
+
let drawnEditingState;
|
|
18238
18551
|
let currentFocusStyle = options.focusStyle ?? null;
|
|
18239
18552
|
let currentLabelStyle = options.labelStyle ?? null;
|
|
18240
18553
|
let currentMaskStyle = options.maskStyle ?? null;
|
|
@@ -18307,7 +18620,11 @@ async function createPixiMediaScene(options) {
|
|
|
18307
18620
|
hidden: currentVisibility?.labelsHidden === true || state.hidden,
|
|
18308
18621
|
};
|
|
18309
18622
|
};
|
|
18623
|
+
// Editable geometry uses retained displays, so boxes and vectors can share
|
|
18624
|
+
// one ordered parent without changing the dense, non-editing raster paths.
|
|
18625
|
+
const retainedGeometry = options.editingEngine ? new Container() : undefined;
|
|
18310
18626
|
const boxLayer = createPixiBoxLayer({
|
|
18627
|
+
container: retainedGeometry,
|
|
18311
18628
|
boxStyle: options.boxStyle,
|
|
18312
18629
|
Container: options.editingEngine ? Container : undefined,
|
|
18313
18630
|
detectionTimeline: annotationDetectionTimeline,
|
|
@@ -18387,6 +18704,7 @@ async function createPixiMediaScene(options) {
|
|
|
18387
18704
|
})
|
|
18388
18705
|
: undefined;
|
|
18389
18706
|
const vectorLayer = createPixiVectorLayer({
|
|
18707
|
+
container: retainedGeometry,
|
|
18390
18708
|
Container,
|
|
18391
18709
|
Graphics,
|
|
18392
18710
|
detectionTimeline: annotationDetectionTimeline,
|
|
@@ -18438,6 +18756,7 @@ async function createPixiMediaScene(options) {
|
|
|
18438
18756
|
BlurFilter,
|
|
18439
18757
|
BufferImageSource,
|
|
18440
18758
|
Container,
|
|
18759
|
+
Graphics,
|
|
18441
18760
|
ImageSource,
|
|
18442
18761
|
Mesh,
|
|
18443
18762
|
MeshGeometry,
|
|
@@ -18469,7 +18788,7 @@ async function createPixiMediaScene(options) {
|
|
|
18469
18788
|
: undefined;
|
|
18470
18789
|
await app.init({
|
|
18471
18790
|
autoDensity: true,
|
|
18472
|
-
autoStart:
|
|
18791
|
+
autoStart: !rendersOnChange,
|
|
18473
18792
|
backgroundColor: options.backgroundColor ?? 0x111111,
|
|
18474
18793
|
/* Only the video-engine media path supplies a presented-frame channel, so
|
|
18475
18794
|
* which reader opens the clip is also what decides which backend draws. */
|
|
@@ -18488,7 +18807,10 @@ async function createPixiMediaScene(options) {
|
|
|
18488
18807
|
if (!stagingContext) {
|
|
18489
18808
|
throw new Error("Unable to create staging canvas context.");
|
|
18490
18809
|
}
|
|
18491
|
-
const renderScheduler = createSceneRenderScheduler(() =>
|
|
18810
|
+
const renderScheduler = createSceneRenderScheduler(() => {
|
|
18811
|
+
app.render();
|
|
18812
|
+
syncStillFrameTicker();
|
|
18813
|
+
});
|
|
18492
18814
|
const maskBrushPreview = options.maskBrush
|
|
18493
18815
|
? createPixiMaskBrushPreview({
|
|
18494
18816
|
CanvasSource,
|
|
@@ -18497,7 +18819,7 @@ async function createPixiMediaScene(options) {
|
|
|
18497
18819
|
Sprite,
|
|
18498
18820
|
Texture,
|
|
18499
18821
|
preview: options.maskBrush,
|
|
18500
|
-
onInvalidate:
|
|
18822
|
+
onInvalidate: rendersOnChange ? renderNow : undefined,
|
|
18501
18823
|
})
|
|
18502
18824
|
: undefined;
|
|
18503
18825
|
let appliedPresentation;
|
|
@@ -18666,6 +18988,17 @@ async function createPixiMediaScene(options) {
|
|
|
18666
18988
|
labelLayer?.drawFrame(currentMediaTime, viewportScale);
|
|
18667
18989
|
renderNow();
|
|
18668
18990
|
}
|
|
18991
|
+
// With no ticker to pick it up, an edit the host drives itself, such as a
|
|
18992
|
+
// cancel or a commit, reaches the screen through this notification. A
|
|
18993
|
+
// pointer gesture draws and renders its own state before the microtask
|
|
18994
|
+
// runs, which then finds nothing left to draw or render.
|
|
18995
|
+
if (rendersOnChange) {
|
|
18996
|
+
queueMicrotask(() => {
|
|
18997
|
+
if (isDestroyed || isPresenting)
|
|
18998
|
+
return;
|
|
18999
|
+
renderOnChange();
|
|
19000
|
+
});
|
|
19001
|
+
}
|
|
18669
19002
|
});
|
|
18670
19003
|
const handleKeyDown = (event) => {
|
|
18671
19004
|
if (event.key === "Tab" && interactionLayer) {
|
|
@@ -18796,11 +19129,25 @@ async function createPixiMediaScene(options) {
|
|
|
18796
19129
|
maskLayer?.setRasterDisplay(display);
|
|
18797
19130
|
options.onDisplayChange?.(display);
|
|
18798
19131
|
};
|
|
19132
|
+
const updateMediaSampling = () => {
|
|
19133
|
+
if (!mediaSprite)
|
|
19134
|
+
return;
|
|
19135
|
+
const threshold = options.pixelInspection?.minScale;
|
|
19136
|
+
if (threshold === undefined)
|
|
19137
|
+
return;
|
|
19138
|
+
const source = mediaSprite.texture.source;
|
|
19139
|
+
const mode = viewportScale >= threshold ? "nearest" : "linear";
|
|
19140
|
+
if (source.scaleMode === mode)
|
|
19141
|
+
return;
|
|
19142
|
+
source.scaleMode = mode;
|
|
19143
|
+
source.style.update();
|
|
19144
|
+
};
|
|
18799
19145
|
const applyViewportTransform = () => {
|
|
18800
19146
|
if (!mediaScene || !baseFit)
|
|
18801
19147
|
return;
|
|
18802
19148
|
const transform = viewport.getTransform();
|
|
18803
19149
|
viewportScale = baseFit.scale * transform.scale;
|
|
19150
|
+
updateMediaSampling();
|
|
18804
19151
|
mediaScene.scale.set(viewportScale);
|
|
18805
19152
|
const box = syncPresentationBox();
|
|
18806
19153
|
mediaScene.position.set(baseFit.x + transform.x - box.left, baseFit.y + transform.y - box.top);
|
|
@@ -18841,15 +19188,17 @@ async function createPixiMediaScene(options) {
|
|
|
18841
19188
|
renderScene();
|
|
18842
19189
|
};
|
|
18843
19190
|
let unsubscribeDetectionTimeline;
|
|
18844
|
-
if (
|
|
18845
|
-
frameChannel.onPresentedFrame(handlePresentedFrame);
|
|
19191
|
+
if (rendersOnChange) {
|
|
18846
19192
|
document.addEventListener?.("visibilitychange", handleVisibilityChange);
|
|
18847
19193
|
// A detection load landing moves what the window covers exactly as a cook
|
|
18848
19194
|
// landing does. Nothing else brings that to a resting playhead: the layers
|
|
18849
19195
|
// read the timeline only while drawing, and this is what makes the draw.
|
|
18850
19196
|
unsubscribeDetectionTimeline = options.detectionTimeline.subscribe?.(handlePreparedWindowChange);
|
|
18851
19197
|
}
|
|
18852
|
-
|
|
19198
|
+
if (frameChannel) {
|
|
19199
|
+
frameChannel.onPresentedFrame(handlePresentedFrame);
|
|
19200
|
+
}
|
|
19201
|
+
else if (!stillFrame) {
|
|
18853
19202
|
app.ticker.add(updateMediaSceneFit);
|
|
18854
19203
|
app.ticker.add(drawAnnotationOverlayNow);
|
|
18855
19204
|
app.ticker.add(tickFocusLayer);
|
|
@@ -18919,11 +19268,14 @@ async function createPixiMediaScene(options) {
|
|
|
18919
19268
|
advanceFocus: () => undefined,
|
|
18920
19269
|
drawAnnotationOverlay: frameChannel
|
|
18921
19270
|
? presentLayers.drawAnnotationOverlay
|
|
18922
|
-
:
|
|
19271
|
+
: stillFrame
|
|
19272
|
+
? (mediaTime) => drawAnnotationOverlay(mediaTime, overlayNow())
|
|
19273
|
+
: () => undefined,
|
|
18923
19274
|
};
|
|
18924
19275
|
// A viewport-style redraw covers the layers whose geometry is a function of
|
|
18925
|
-
// the viewport scale.
|
|
18926
|
-
//
|
|
19276
|
+
// the viewport scale. Mask outlines reuse the artifact already on screen;
|
|
19277
|
+
// the mask raster and the interaction hit map stay untouched, so a pan
|
|
19278
|
+
// does not change which detection is hovered.
|
|
18927
19279
|
const viewportStyleRedrawLayers = {
|
|
18928
19280
|
...annotationDrawLayers,
|
|
18929
19281
|
drawBox: (mediaTime) => {
|
|
@@ -18932,7 +19284,7 @@ async function createPixiMediaScene(options) {
|
|
|
18932
19284
|
},
|
|
18933
19285
|
drawDepth: undefined,
|
|
18934
19286
|
drawInteraction: undefined,
|
|
18935
|
-
drawMask:
|
|
19287
|
+
drawMask: () => maskLayer?.redrawViewportStyles(viewportScale),
|
|
18936
19288
|
};
|
|
18937
19289
|
let frameDrawTimings = createFrameDrawTimings();
|
|
18938
19290
|
let presentStartMs = 0;
|
|
@@ -18968,7 +19320,7 @@ async function createPixiMediaScene(options) {
|
|
|
18968
19320
|
};
|
|
18969
19321
|
return {
|
|
18970
19322
|
getRenderCount() {
|
|
18971
|
-
return
|
|
19323
|
+
return rendersOnChange ? renderScheduler.getRenderCount() : null;
|
|
18972
19324
|
},
|
|
18973
19325
|
getPreparedAnnotationWindow() {
|
|
18974
19326
|
return frameChannel ? annotationWindow.getSnapshot() : null;
|
|
@@ -19009,7 +19361,7 @@ async function createPixiMediaScene(options) {
|
|
|
19009
19361
|
if (currentHeatmapRenderers.length > 0) {
|
|
19010
19362
|
heatmapSlot.setDisplay(ensureHeatmapLayer().createContainer());
|
|
19011
19363
|
}
|
|
19012
|
-
boxSlot.setDisplay(retainedBoxes ?? boxes);
|
|
19364
|
+
boxSlot.setDisplay(retainedGeometry ? undefined : (retainedBoxes ?? boxes));
|
|
19013
19365
|
vectorDisplay = new Container();
|
|
19014
19366
|
attachPolygonLayerDisplay();
|
|
19015
19367
|
vectorDisplay.addChild(vectorLayer.createContainer());
|
|
@@ -19081,6 +19433,7 @@ async function createPixiMediaScene(options) {
|
|
|
19081
19433
|
stagingTexture?.update();
|
|
19082
19434
|
const { boxState, regionState } = drawFramePresentLayers(annotationDrawLayers, sample.timestamp);
|
|
19083
19435
|
updateMediaSceneFit();
|
|
19436
|
+
renderOnChange();
|
|
19084
19437
|
return {
|
|
19085
19438
|
...createPresentedSampleState(sample.timestamp, boxState, regionState),
|
|
19086
19439
|
duration: sample.duration,
|
|
@@ -19096,6 +19449,7 @@ async function createPixiMediaScene(options) {
|
|
|
19096
19449
|
});
|
|
19097
19450
|
const { boxState, regionState } = drawFramePresentLayers(measuredAnnotationDrawLayers, sample.timestamp);
|
|
19098
19451
|
const fitMs = measure(updateMediaSceneFit);
|
|
19452
|
+
renderOnChange();
|
|
19099
19453
|
return {
|
|
19100
19454
|
...createPresentedSampleState(sample.timestamp, boxState, regionState),
|
|
19101
19455
|
duration: sample.duration,
|
|
@@ -19156,6 +19510,8 @@ async function createPixiMediaScene(options) {
|
|
|
19156
19510
|
const brightness = adjustments.brightness ?? 1;
|
|
19157
19511
|
const contrast = adjustments.contrast ?? 1;
|
|
19158
19512
|
const filter = new ColorMatrixFilter();
|
|
19513
|
+
// Preserve the renderer's pixel grid through the adjustment pass.
|
|
19514
|
+
filter.resolution = "inherit";
|
|
19159
19515
|
filter.brightness(brightness, false);
|
|
19160
19516
|
filter.contrast(contrast - 1, true);
|
|
19161
19517
|
if (mediaSprite)
|
|
@@ -19224,6 +19580,8 @@ async function createPixiMediaScene(options) {
|
|
|
19224
19580
|
};
|
|
19225
19581
|
},
|
|
19226
19582
|
panViewportBy(dx, dy) {
|
|
19583
|
+
if (viewport.getTransform().locked)
|
|
19584
|
+
return;
|
|
19227
19585
|
const before = viewport.getTransform();
|
|
19228
19586
|
viewport.panBy(dx, dy);
|
|
19229
19587
|
const after = viewport.getTransform();
|
|
@@ -19234,7 +19592,7 @@ async function createPixiMediaScene(options) {
|
|
|
19234
19592
|
renderOnChange();
|
|
19235
19593
|
},
|
|
19236
19594
|
zoomViewportAt(point, factor) {
|
|
19237
|
-
if (!baseFit)
|
|
19595
|
+
if (!baseFit || viewport.getTransform().locked)
|
|
19238
19596
|
return;
|
|
19239
19597
|
viewport.zoomAt({ x: point.x - baseFit.x, y: point.y - baseFit.y }, factor);
|
|
19240
19598
|
const transform = viewport.getTransform();
|
|
@@ -19247,7 +19605,7 @@ async function createPixiMediaScene(options) {
|
|
|
19247
19605
|
renderOnChange();
|
|
19248
19606
|
},
|
|
19249
19607
|
zoomViewportFromWheel(point, deltaY) {
|
|
19250
|
-
if (!baseFit)
|
|
19608
|
+
if (!baseFit || viewport.getTransform().locked)
|
|
19251
19609
|
return;
|
|
19252
19610
|
viewport.zoomFromWheel({ x: point.x - baseFit.x, y: point.y - baseFit.y }, deltaY);
|
|
19253
19611
|
const transform = viewport.getTransform();
|
|
@@ -19553,6 +19911,7 @@ async function createPixiMediaScene(options) {
|
|
|
19553
19911
|
BlurFilter,
|
|
19554
19912
|
BufferImageSource,
|
|
19555
19913
|
Container,
|
|
19914
|
+
Graphics,
|
|
19556
19915
|
ImageSource,
|
|
19557
19916
|
Mesh,
|
|
19558
19917
|
MeshGeometry,
|
|
@@ -19969,7 +20328,7 @@ async function createPixiMediaScene(options) {
|
|
|
19969
20328
|
return;
|
|
19970
20329
|
}
|
|
19971
20330
|
const { boxState, regionState } = drawAnnotationFrame(currentMediaTime);
|
|
19972
|
-
if (
|
|
20331
|
+
if (rendersOnChange) {
|
|
19973
20332
|
const signature = describeSceneRender();
|
|
19974
20333
|
if (forceRender)
|
|
19975
20334
|
renderScheduler.render(signature);
|
|
@@ -19991,7 +20350,7 @@ async function createPixiMediaScene(options) {
|
|
|
19991
20350
|
return;
|
|
19992
20351
|
}
|
|
19993
20352
|
if (isFramePrepared(mediaTime)) {
|
|
19994
|
-
maskLayer.drawFrame(mediaTime, presentedFrameId);
|
|
20353
|
+
maskLayer.drawFrame(mediaTime, presentedFrameId, viewportScale);
|
|
19995
20354
|
return;
|
|
19996
20355
|
}
|
|
19997
20356
|
maskLayer.clearFrame();
|
|
@@ -20026,6 +20385,13 @@ async function createPixiMediaScene(options) {
|
|
|
20026
20385
|
labelLayer?.drawCreationPreview(editingState?.kind === AnnotationGestureStateKind.Creating
|
|
20027
20386
|
? editingState.preview
|
|
20028
20387
|
: null, mediaTime, viewportScale);
|
|
20388
|
+
const previewOverlay = options.previewOverlay?.() ?? null;
|
|
20389
|
+
drawnEditingState = editingState;
|
|
20390
|
+
// Only a scene that renders on change compares signatures.
|
|
20391
|
+
if (rendersOnChange) {
|
|
20392
|
+
drawnPreviewOverlayContent =
|
|
20393
|
+
describePreviewOverlayContent(previewOverlay);
|
|
20394
|
+
}
|
|
20029
20395
|
annotationOverlayLayer.draw({
|
|
20030
20396
|
frame,
|
|
20031
20397
|
marquee: interactionState?.marqueeRect ?? null,
|
|
@@ -20033,7 +20399,7 @@ async function createPixiMediaScene(options) {
|
|
|
20033
20399
|
mediaWidth,
|
|
20034
20400
|
now: overlayNowMs,
|
|
20035
20401
|
pointer: interactionState?.pointerPoint ?? null,
|
|
20036
|
-
previewOverlay
|
|
20402
|
+
previewOverlay,
|
|
20037
20403
|
selectedDetectionIds: (interactionState?.selectedPicks ?? []).flatMap(({ detection }) => (detection.id === undefined ? [] : [detection.id])),
|
|
20038
20404
|
viewportScale,
|
|
20039
20405
|
visibility: currentVisibility,
|
|
@@ -20041,8 +20407,9 @@ async function createPixiMediaScene(options) {
|
|
|
20041
20407
|
}
|
|
20042
20408
|
/**
|
|
20043
20409
|
* Milliseconds for animations that are not annotation geometry. A pull scene
|
|
20044
|
-
* has a ticker and reads the wall clock
|
|
20045
|
-
*
|
|
20410
|
+
* has a ticker and reads the wall clock, as does a still frame while its
|
|
20411
|
+
* ticker runs; a push scene has the presented media time and nothing else,
|
|
20412
|
+
* so its animations move only when the media does.
|
|
20046
20413
|
*/
|
|
20047
20414
|
function overlayNow() {
|
|
20048
20415
|
return frameChannel ? currentMediaTime * MILLISECONDS_PER_SECOND : now();
|
|
@@ -20091,8 +20458,21 @@ async function createPixiMediaScene(options) {
|
|
|
20091
20458
|
focusLayer.tick(focusAnimationTimeMs);
|
|
20092
20459
|
}
|
|
20093
20460
|
function renderScene() {
|
|
20461
|
+
drawOverlayForEditingState();
|
|
20094
20462
|
renderScheduler.render(describeSceneRender());
|
|
20095
20463
|
}
|
|
20464
|
+
/**
|
|
20465
|
+
* The render signature names the editing state, so a render must not record
|
|
20466
|
+
* a state the overlay has not drawn: the redraw that follows would match the
|
|
20467
|
+
* recorded signature and be skipped, leaving the old preview on screen.
|
|
20468
|
+
*/
|
|
20469
|
+
function drawOverlayForEditingState() {
|
|
20470
|
+
const editingState = options.editingEngine?.getState();
|
|
20471
|
+
if (editingState === undefined || editingState === drawnEditingState) {
|
|
20472
|
+
return;
|
|
20473
|
+
}
|
|
20474
|
+
drawAnnotationOverlay(currentMediaTime, overlayNow());
|
|
20475
|
+
}
|
|
20096
20476
|
/**
|
|
20097
20477
|
* Renders a present at most once per display refresh. Frames arrive as
|
|
20098
20478
|
* messages from the producer's own thread, so a main thread that falls
|
|
@@ -20118,15 +20498,46 @@ async function createPixiMediaScene(options) {
|
|
|
20118
20498
|
presentFrameNow(deferred);
|
|
20119
20499
|
}
|
|
20120
20500
|
function renderNow() {
|
|
20121
|
-
if (
|
|
20501
|
+
if (rendersOnChange)
|
|
20122
20502
|
renderScene();
|
|
20123
20503
|
}
|
|
20124
20504
|
function renderOnChange() {
|
|
20125
|
-
if (!
|
|
20505
|
+
if (!rendersOnChange)
|
|
20126
20506
|
return;
|
|
20127
20507
|
settleFocusAnimation();
|
|
20508
|
+
drawOverlayForEditingState();
|
|
20128
20509
|
renderScheduler.renderOnChange(describeSceneRender());
|
|
20129
20510
|
}
|
|
20511
|
+
/**
|
|
20512
|
+
* Runs the ticker on a still frame for as long as something on it moves by
|
|
20513
|
+
* the clock: a looping GIF region or a loading spinner. Pixi renders on
|
|
20514
|
+
* every tick it runs, and the overlay redraws so the spinner turns. Each
|
|
20515
|
+
* render checks again, so the ticker stops with the render that removes the
|
|
20516
|
+
* last of them.
|
|
20517
|
+
*/
|
|
20518
|
+
function syncStillFrameTicker() {
|
|
20519
|
+
if (!stillFrame || isDestroyed)
|
|
20520
|
+
return;
|
|
20521
|
+
const animating = hasPresentedSample &&
|
|
20522
|
+
(regionLayer.hasAnimatedDisplay() || hasLoadingDetections());
|
|
20523
|
+
if (animating === stillFrameTickerRunning)
|
|
20524
|
+
return;
|
|
20525
|
+
stillFrameTickerRunning = animating;
|
|
20526
|
+
if (animating) {
|
|
20527
|
+
app.ticker.add(drawAnnotationOverlayNow);
|
|
20528
|
+
app.ticker.start();
|
|
20529
|
+
}
|
|
20530
|
+
else {
|
|
20531
|
+
app.ticker.stop();
|
|
20532
|
+
app.ticker.remove(drawAnnotationOverlayNow);
|
|
20533
|
+
}
|
|
20534
|
+
}
|
|
20535
|
+
function hasLoadingDetections() {
|
|
20536
|
+
const loading = currentVisibility?.loadingDetectionIds;
|
|
20537
|
+
if (!loading)
|
|
20538
|
+
return false;
|
|
20539
|
+
return "size" in loading ? loading.size > 0 : loading.length > 0;
|
|
20540
|
+
}
|
|
20130
20541
|
/**
|
|
20131
20542
|
* Everything a render would put on screen, as values that can be compared.
|
|
20132
20543
|
* Styles are opaque resolvers, so they are compared by identity: handing the
|
|
@@ -20164,6 +20575,9 @@ async function createPixiMediaScene(options) {
|
|
|
20164
20575
|
interactionState?.marqueeRect,
|
|
20165
20576
|
options.editingEngine?.getState(),
|
|
20166
20577
|
options.editingEngine ? interactionState?.pointerPoint : null,
|
|
20578
|
+
// The host's preview is read when the overlay draws, and a host may edit
|
|
20579
|
+
// its arrays in place, so its content is what tells a redraw it changed.
|
|
20580
|
+
drawnPreviewOverlayContent,
|
|
20167
20581
|
];
|
|
20168
20582
|
}
|
|
20169
20583
|
/**
|
|
@@ -20189,10 +20603,23 @@ async function createPixiMediaScene(options) {
|
|
|
20189
20603
|
});
|
|
20190
20604
|
detached.width = mediaWidth;
|
|
20191
20605
|
detached.height = mediaHeight;
|
|
20606
|
+
const source = sprite.texture.source;
|
|
20607
|
+
const displayScaleMode = source.scaleMode;
|
|
20192
20608
|
try {
|
|
20609
|
+
// Capture retains its baseline sampling even when pixel inspection
|
|
20610
|
+
// changes display sampling. Extraction is synchronous, so the shared
|
|
20611
|
+
// source can be restored before another frame draws.
|
|
20612
|
+
if (displayScaleMode !== "linear") {
|
|
20613
|
+
source.scaleMode = "linear";
|
|
20614
|
+
source.style.update();
|
|
20615
|
+
}
|
|
20193
20616
|
return app.renderer.extract.canvas(detached);
|
|
20194
20617
|
}
|
|
20195
20618
|
finally {
|
|
20619
|
+
if (displayScaleMode !== "linear") {
|
|
20620
|
+
source.scaleMode = displayScaleMode;
|
|
20621
|
+
source.style.update();
|
|
20622
|
+
}
|
|
20196
20623
|
detached.destroy();
|
|
20197
20624
|
}
|
|
20198
20625
|
};
|
|
@@ -20207,6 +20634,7 @@ async function createPixiMediaScene(options) {
|
|
|
20207
20634
|
// takes the new size's scale while still drawing the previous size's
|
|
20208
20635
|
// quad, so a preview-sized frame paints the media oversized.
|
|
20209
20636
|
sprite.texture = new Texture({ dynamic: true, source: externalSource });
|
|
20637
|
+
updateMediaSampling();
|
|
20210
20638
|
sizeMediaSprite();
|
|
20211
20639
|
return externalSource;
|
|
20212
20640
|
},
|
|
@@ -20488,6 +20916,37 @@ function elapsedSince(start) {
|
|
|
20488
20916
|
function now() {
|
|
20489
20917
|
return globalThis.performance?.now?.() ?? Date.now();
|
|
20490
20918
|
}
|
|
20919
|
+
/**
|
|
20920
|
+
* A key for what a preview overlay draws, hashed from its coordinates rather
|
|
20921
|
+
* than its arrays, so a preview the host edits in place still changes it.
|
|
20922
|
+
* Coordinates count to a 256th of a media pixel, finer than anything drawn.
|
|
20923
|
+
*/
|
|
20924
|
+
function describePreviewOverlayContent(overlay) {
|
|
20925
|
+
if (!overlay)
|
|
20926
|
+
return null;
|
|
20927
|
+
let hash = 0x811c9dc5;
|
|
20928
|
+
const mix = (value) => {
|
|
20929
|
+
hash = Math.imul(hash ^ value, 0x01000193);
|
|
20930
|
+
};
|
|
20931
|
+
const mixPoint = (point) => {
|
|
20932
|
+
mix(Math.round(point.x * 256));
|
|
20933
|
+
mix(Math.round(point.y * 256));
|
|
20934
|
+
};
|
|
20935
|
+
for (const polygons of [overlay.hoverPolygons, overlay.draftPolygons]) {
|
|
20936
|
+
mix(polygons?.length ?? -1);
|
|
20937
|
+
for (const polygon of polygons ?? []) {
|
|
20938
|
+
mix(polygon.length);
|
|
20939
|
+
for (const point of polygon)
|
|
20940
|
+
mixPoint(point);
|
|
20941
|
+
}
|
|
20942
|
+
}
|
|
20943
|
+
mix(overlay.points?.length ?? -1);
|
|
20944
|
+
for (const point of overlay.points ?? []) {
|
|
20945
|
+
mixPoint(point);
|
|
20946
|
+
mix(point.positive ? 1 : 0);
|
|
20947
|
+
}
|
|
20948
|
+
return hash;
|
|
20949
|
+
}
|
|
20491
20950
|
/**
|
|
20492
20951
|
* Fills the container and paints the margin around the canvas in the colour the
|
|
20493
20952
|
* renderer clears to. The scene owns this element so the canvas is positioned
|