@luxonis/visualizer-protobuf 2.67.2 → 2.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{index-CCsfCpZT.js → index-7BbbXb7C.js} +1 -1
  2. package/dist/{index-CZkF1YCY.js → index-B5d7u70w.js} +1 -1
  3. package/dist/{index-FM6vEyS1.js → index-BGrghyc8.js} +1 -1
  4. package/dist/{index-CaE-gPi_.js → index-BSEL9eaF.js} +1 -1
  5. package/dist/{index-CZ6h7zpz.js → index-Bjwf7lc9.js} +1 -1
  6. package/dist/{index-BNMXWFDA.js → index-Bndiqw1t.js} +1 -1
  7. package/dist/{index-CRTwfb76.js → index-BzPticWU.js} +1 -1
  8. package/dist/{index-B3cHfrcw.js → index-C99kj7Bj.js} +1 -1
  9. package/dist/{index-ugbolO1H.js → index-CO2KzlVr.js} +1 -1
  10. package/dist/{index-BuGHi09K.js → index-CfGQhG__.js} +1 -1
  11. package/dist/{index-C92x9EiC.js → index-CwMsPSeQ.js} +2 -2
  12. package/dist/{index-D7JYN_IN.js → index-D5xb8lsX.js} +914 -486
  13. package/dist/{index-BhLYvhjo.js → index-D9b6Lu-5.js} +1 -1
  14. package/dist/{index-KDfpYD4q.js → index-DEr2t1uV.js} +1 -1
  15. package/dist/{index-BzAo-6Ol.js → index-DXgxTZSz.js} +1 -1
  16. package/dist/{index-DW9FJFJz.js → index-DoCri7UR.js} +1 -1
  17. package/dist/{index-2Ajz1Y1F.js → index-DtErPDFb.js} +1 -1
  18. package/dist/{index-CLHPbWo6.js → index-DycPqoQx.js} +37 -24
  19. package/dist/{index-HcH5t5Pw.js → index-pfvcsvrO.js} +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/lib/src/connection/connection.d.ts +3 -1
  22. package/dist/lib/src/connection/connection.d.ts.map +1 -1
  23. package/dist/lib/src/connection/connection.js +3 -2
  24. package/dist/lib/src/connection/connection.js.map +1 -1
  25. package/dist/lib/src/output.css +4 -2
  26. package/dist/lib/src/utils/config-store.d.ts +3 -0
  27. package/dist/lib/src/utils/config-store.d.ts.map +1 -1
  28. package/dist/lib/src/utils/config-store.js +8 -0
  29. package/dist/lib/src/utils/config-store.js.map +1 -1
  30. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/ImageMode.d.ts.map +1 -1
  31. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/ImageMode.js +2 -2
  32. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/ImageMode.js.map +1 -1
  33. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandler.d.ts +3 -3
  34. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandler.d.ts.map +1 -1
  35. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandler.js +1 -1
  36. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandler.js.map +1 -1
  37. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerFactory.d.ts +11 -0
  38. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerFactory.d.ts.map +1 -0
  39. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerFactory.js +24 -0
  40. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerFactory.js.map +1 -0
  41. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerV2.d.ts +94 -0
  42. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerV2.d.ts.map +1 -0
  43. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerV2.js +410 -0
  44. package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerV2.js.map +1 -0
  45. package/package.json +1 -1
@@ -3,7 +3,7 @@ import React__default, { useReducer, useRef, useCallback, useLayoutEffect, Compo
3
3
  import ReactDOM__default from 'react-dom';
4
4
  import { Y as isSymbol, Z as toString, $ as keys, a0 as getSymbols$1, a1 as stubArray, a2 as arrayPush, a3 as baseGetAllKeys, g as getTag, a4 as getAllKeys, k as baseGet, c as baseIteratee, j as castPath, t as toKey, a5 as arrayMap$1, a6 as baseUniq, b as baseFlatten, a7 as useMustNotChange, a8 as useCurrentLayoutActions, a9 as useCurrentLayoutSelector, r as reportError, A as AppError, L as Logger, u as useGuaranteedContext, aa as usePanelMosaicId, ab as useSelectedPanels, ac as PANEL_TITLE_CONFIG_KEY, ad as noop$4, o as getPanelTypeFromId, M as useShallowMemo, T as TAB_PANEL_TYPE, J as filterMap, d as dist$2, ae as useAppConfiguration, af as useValueChangedDebugLog, ag as useJsonTreeTheme } from './depth-CFY2W_Vf.js';
5
5
  import { createStore, useStore } from 'zustand';
6
- import { g as generateUtilityClass, c as createAggregator, f as flatRest, b as baseSet, A as AnalyticsContext, P as PropTypes, E as ErrorDisplay, S as Stack$1, m as makeStyles$1, _ as _extends$1, W as WorkspaceContext, u as useAnalytics, a as AppEvent, L as LeftSidebarItemKeys, R as RightSidebarItemKeys, d as useTranslation, e as usePanelCatalog, h as EmptyState, i as isEmpty, j as PanelContext, k as PanelCatalogContext, l as usePanelStateStore, n as useDefaultPanelTitle, o as useWorkspaceStore, p as WorkspaceStoreSelectors, q as difference, r as usePanelContext, s as useMessagePipeline, v as v4, t as useHoverValue, w as useSetHoverValue, x as useClearHoverValue, y as useMessagePipelineGetter, z as usePanelSettingsTreeUpdate, B as PlayerCapabilities, C as assertNever, D as PlayerPresence, F as isEqual, G as isDesktopApp, H as createTheme, I as propTypesExports, J as DEFAULT_CAMERA_STATE$1, K as format$1, M as z, N as serializeError, O as stringify$1, Q as createIntl, T as createIntlCache } from './index-CLHPbWo6.js';
6
+ import { g as generateUtilityClass, c as createAggregator, f as flatRest, b as baseSet, A as AnalyticsContext, P as PropTypes, E as ErrorDisplay, S as Stack$1, m as makeStyles$1, _ as _extends$1, W as WorkspaceContext, u as useAnalytics, a as AppEvent, L as LeftSidebarItemKeys, R as RightSidebarItemKeys, d as useTranslation, e as usePanelCatalog, h as EmptyState, i as isEmpty, j as PanelContext, k as PanelCatalogContext, l as usePanelStateStore, n as useDefaultPanelTitle, o as useWorkspaceStore, p as WorkspaceStoreSelectors, q as difference, r as usePanelContext, s as useMessagePipeline, v as v4, t as useHoverValue, w as useSetHoverValue, x as useClearHoverValue, y as useMessagePipelineGetter, z as usePanelSettingsTreeUpdate, B as PlayerCapabilities, C as assertNever, D as PlayerPresence, F as isEqual, G as isDesktopApp, H as createTheme, I as propTypesExports, J as DEFAULT_CAMERA_STATE$1, K as format$1, M as z, N as serializeError, O as stringify$1, Q as createIntl, T as createIntlCache } from './index-DycPqoQx.js';
7
7
  import { MosaicDragType, MosaicContext, MosaicWindowContext, getOtherBranch, getNodeAtPath } from 'react-mosaic-component';
8
8
  import { g as getDefaultExportFromCjs, c as commonjsGlobal, d as getAugmentedNamespace } from './protobuf-BFCtaU7c.js';
9
9
  import { Link, Button, alpha, IconButton, Card, CardActionArea, CardMedia, CardContent, Typography, Container, Tooltip, Fade, ListItem, ListItemButton, ListItemText, List, TextField, InputAdornment, Popper, Grow, Paper, ClickAwayListener, Menu, MenuItem, Divider, buttonClasses, Backdrop, Chip, useTheme, alertClasses, darken, lighten, inputBaseClasses, autocompleteClasses, inputClasses, Checkbox, dialogActionsClasses, filledInputClasses, inputAdornmentClasses, listSubheaderClasses, selectClasses, tableCellClasses, ThemeProvider as ThemeProvider$1, SvgIcon, tabsClasses as tabsClasses$1, tabClasses, Tabs, Tab, ListItemIcon } from '@mui/material';
@@ -32221,54 +32221,29 @@ class ImageModeCamera extends PerspectiveCamera {
32221
32221
  // License, v2.0. If a copy of the MPL was not distributed with this
32222
32222
  // file, You can obtain one at http://mozilla.org/MPL/2.0/
32223
32223
 
32224
- function normalizeImageData(data) {
32225
- if (data == undefined) {
32226
- return new Uint8Array(0);
32227
- } else if (data instanceof Int8Array || data instanceof Uint8Array) {
32228
- return data;
32229
- } else {
32230
- return new Uint8Array(0);
32231
- }
32232
- }
32233
- function normalizeRosImage(message) {
32234
- return {
32235
- header: normalizeHeader(message.header),
32236
- height: message.height ?? 0,
32237
- width: message.width ?? 0,
32238
- encoding: message.encoding ?? "",
32239
- is_bigendian: message.is_bigendian ?? false,
32240
- step: message.step ?? 0,
32241
- data: normalizeImageData(message.data)
32242
- };
32243
- }
32244
- function normalizeRosCompressedImage(message) {
32245
- return {
32246
- header: normalizeHeader(message.header),
32247
- format: message.format ?? "",
32248
- data: normalizeByteArray(message.data)
32249
- };
32250
- }
32251
- function normalizeRawImage(message) {
32252
- return {
32253
- timestamp: normalizeTime(message.timestamp),
32254
- frame_id: message.frame_id ?? "",
32255
- height: message.height ?? 0,
32256
- width: message.width ?? 0,
32257
- encoding: message.encoding ?? "",
32258
- step: (message.stride || message.step) ?? 0,
32259
- data: normalizeImageData(message.data),
32260
- stride: message.stride,
32261
- planeStride: message.planeStride
32262
- };
32263
- }
32264
- function normalizeCompressedImage(message) {
32265
- return {
32266
- timestamp: normalizeTime(message.timestamp),
32267
- frame_id: message.frame_id ?? "",
32268
- format: message.format ?? "",
32269
- data: normalizeByteArray(message.data)
32270
- };
32271
- }
32224
+ /** Want to render after all other objects in the scene so that they are not occluded by other objects */
32225
+ const ANNOTATION_FRONT_POSITION = 100000;
32226
+
32227
+ /** Render order for given annotations. Higher numbers rendered after lower numbers */
32228
+ const ANNOTATION_RENDER_ORDER = {
32229
+ FILL: 1 + ANNOTATION_FRONT_POSITION,
32230
+ LINE_PREPASS: 2 + ANNOTATION_FRONT_POSITION,
32231
+ LINE: 3 + ANNOTATION_FRONT_POSITION,
32232
+ POINTS: 4 + ANNOTATION_FRONT_POSITION,
32233
+ TEXT: 5 + ANNOTATION_FRONT_POSITION
32234
+ };
32235
+
32236
+ /** we want annotations to show on top of the entire scene. These are material props to achieve that */
32237
+ const annotationRenderOrderMaterialProps = {
32238
+ /** We need to set transparent to true so that transparent objects aren't rendered on top of it.
32239
+ * Transparent objects are rendered after non-transparent objects. If this were set to false or
32240
+ * set based on color of annotations, then the foreground image with opacity would be rendered on top
32241
+ * until it is fully opaque.
32242
+ */
32243
+ transparent: true,
32244
+ depthWrite: false,
32245
+ depthTest: false
32246
+ };
32272
32247
 
32273
32248
  // This Source Code Form is subject to the terms of the Mozilla Public
32274
32249
  // License, v2.0. If a copy of the MPL was not distributed with this
@@ -32504,442 +32479,6 @@ function getAnnotationAtPath(message, path) {
32504
32479
  // License, v2.0. If a copy of the MPL was not distributed with this
32505
32480
  // file, You can obtain one at http://mozilla.org/MPL/2.0/
32506
32481
 
32507
- const syncToleranceSec = 0.15; // maximum acceptable time difference for selecting a “closest” annotation.
32508
- const maxStalenessSec = 1.1; // how long we can hold the last older annotation if no better match is available. 1s corresponds to 1 FPS + 100ms window for jitter.
32509
- const hysteresisSec = 0.1; // how much “stickiness” we allow before switching candidates.
32510
- const imageRetentionSec = 5; // keep small window of recent images
32511
- const maxImageBuffer = 150; // cap number of stored images
32512
-
32513
- // Have constants for the HUD items so that they don't need to be recreated and GCed every message
32514
- const WAITING_FOR_BOTH_HUD_ITEM = {
32515
- id: WAITING_FOR_BOTH_MESSAGES_HUD_ID,
32516
- group: IMAGE_MODE_HUD_GROUP_ID,
32517
- getMessage: () => t3D("waitingForCalibrationAndImages"),
32518
- displayType: "empty"
32519
- };
32520
- const WAITING_FOR_CALIBRATION_HUD_ITEM = {
32521
- id: WAITING_FOR_CALIBRATION_HUD_ID,
32522
- group: IMAGE_MODE_HUD_GROUP_ID,
32523
- getMessage: () => t3D("waitingForCalibration"),
32524
- displayType: "empty"
32525
- };
32526
- const WAITING_FOR_IMAGE_NOTICE_HUD_ITEM = {
32527
- id: WAITING_FOR_IMAGES_NOTICE_ID,
32528
- group: IMAGE_MODE_HUD_GROUP_ID,
32529
- getMessage: () => t3D("waitingForImages"),
32530
- displayType: "notice"
32531
- };
32532
- const WAITING_FOR_IMAGE_EMPTY_HUD_ITEM = {
32533
- id: WAITING_FOR_IMAGES_EMPTY_HUD_ID,
32534
- group: IMAGE_MODE_HUD_GROUP_ID,
32535
- getMessage: () => t3D("waitingForImages"),
32536
- displayType: "empty"
32537
- };
32538
-
32539
- /**
32540
- * Processes and normalizes incoming messages and manages state of
32541
- * messages to be rendered given the ImageMode config. A large part of this responsibility
32542
- * is managing state in synchronized mode and ensuring that the a synchronized set of image and
32543
- * annotations are handed off to the SceneExtension for rendering.
32544
- */
32545
- class MessageHandler {
32546
- /** settings that should reflect image mode config */
32547
- #config;
32548
-
32549
- /** Allows message handler push messages to overlay on top of the canvas */
32550
- #hud;
32551
-
32552
- /** last state passed to listeners */
32553
- #oldRenderState;
32554
-
32555
- /** internal state of last received messages */
32556
- #lastReceivedMessages;
32557
-
32558
- /** listener functions that are called when the state changes. */
32559
- #listeners = [];
32560
-
32561
- /** Holds what annotations are currently available on the given source. These are needed because annotations
32562
- * that are marked as visible may be present in the layout/config, but are not present on the source.
32563
- * This can cause synchronized annotations to never resolve if the source does not have the annotation topic
32564
- * with no indication to the user that the annotation is not available.
32565
- */
32566
-
32567
- #onAnnotationReceivedEvent;
32568
- #annotationsFpsRefreshInterval = null;
32569
- // New: keep a buffer of recent images for annotation-prioritized synchronization
32570
- imagesBuffer = [];
32571
- // Store a small buffer of recent annotations per topic with computed seconds timestamps
32572
- annotationsMap = new Map();
32573
- // Track last selected annotation per topic to add hysteresis and reduce flicker
32574
- lastSelectedByTopic = new Map();
32575
- /**
32576
- * Create a MessageHandler
32577
- * @param {Immutable<Config>} config - subset of ImageMode settings required for message handling
32578
- * @param {HUDItemManager} hud - HUD manager used to display informational messages on the canvas
32579
- */
32580
- constructor(config, hud) {
32581
- this.#config = config;
32582
- this.#hud = hud;
32583
- this.#lastReceivedMessages = {
32584
- annotationsByTopic: new Map()
32585
- };
32586
- this.availableAnnotationTopics = new Set();
32587
- }
32588
- dispose() {
32589
- if (this.#annotationsFpsRefreshInterval) {
32590
- clearInterval(this.#annotationsFpsRefreshInterval);
32591
- }
32592
- }
32593
- addListener(listener) {
32594
- this.#listeners.push(listener);
32595
- }
32596
- addAnnotationReceivedEventHandler(handler) {
32597
- this.#onAnnotationReceivedEvent = handler;
32598
- }
32599
- removeListener(listener) {
32600
- this.#listeners = this.#listeners.filter(fn => fn !== listener);
32601
- }
32602
- handleRosRawImage = messageEvent => {
32603
- this.handleImage(messageEvent, normalizeRosImage(messageEvent.message));
32604
- };
32605
- handleRosCompressedImage = messageEvent => {
32606
- this.handleImage(messageEvent, normalizeRosCompressedImage(messageEvent.message));
32607
- };
32608
- handleRawImage = messageEvent => {
32609
- this.handleImage(messageEvent, normalizeRawImage(messageEvent.message));
32610
- };
32611
- handleCompressedImage = messageEvent => {
32612
- this.handleImage(messageEvent, normalizeCompressedImage(messageEvent.message));
32613
- };
32614
- handleImage(message, image) {
32615
- this.lastImage = {
32616
- ...message,
32617
- message: image
32618
- };
32619
-
32620
- // Compute timestamp seconds for buffer storage (prefer image stamp, then receiveTime, then wall clock)
32621
- const stamp = getTimestampFromImage(image);
32622
- let stampTimeSec;
32623
- const stampSec = stamp?.sec;
32624
- const stampNsec = stamp?.nsec;
32625
- if (typeof stampSec === "number" && typeof stampNsec === "number") {
32626
- stampTimeSec = stampSec + stampNsec / 1e9;
32627
- } else {
32628
- const recv = message.receiveTime;
32629
- const recvSec = recv?.sec;
32630
- const recvNsec = recv?.nsec;
32631
- if (typeof recvSec === "number" && typeof recvNsec === "number") {
32632
- stampTimeSec = recvSec + recvNsec / 1e9;
32633
- }
32634
- }
32635
- const arrivalSec = performance.now() / 1000;
32636
- this.imagesBuffer.push({
32637
- image: this.lastImage,
32638
- stampTimeSec,
32639
- arrivalSec
32640
- });
32641
- // Trim by arrival time and length (do not use stamped time for retention)
32642
- const nowSec = performance.now() / 1000;
32643
- const cutoffArrival = nowSec - imageRetentionSec;
32644
- this.imagesBuffer = this.imagesBuffer.filter(e => e.arrivalSec >= cutoffArrival);
32645
- if (this.imagesBuffer.length > maxImageBuffer) {
32646
- this.imagesBuffer.splice(0, this.imagesBuffer.length - maxImageBuffer);
32647
- }
32648
- this.#emitState();
32649
- }
32650
- handleCameraInfo = message => {
32651
- this.#lastReceivedMessages.cameraInfo = normalizeCameraInfo(message.message);
32652
- this.#emitState();
32653
- };
32654
- handleAnnotations = messageEvent => {
32655
- const list = normalizeAnnotations(messageEvent.message, messageEvent.schemaName);
32656
- const arrivalSec = performance.now() / 1000;
32657
- if (list.length > 0 && messageEvent.topic) {
32658
- this.#onAnnotationReceivedEvent?.();
32659
-
32660
- // Prefer timestamps embedded in the annotations themselves
32661
- const stampTimes = [];
32662
- for (const a of list) {
32663
- const sec = a.stamp?.sec;
32664
- const nsec = a.stamp?.nsec;
32665
- if (typeof sec === "number" && typeof nsec === "number") {
32666
- stampTimes.push(sec + nsec / 1e9);
32667
- }
32668
- }
32669
- const timeSec = stampTimes.length > 0 ? Math.max(...stampTimes) : arrivalSec;
32670
- const entry = {
32671
- annotation: {
32672
- originalMessage: messageEvent,
32673
- annotations: list
32674
- },
32675
- timeSec,
32676
- arrivalSec
32677
- };
32678
- const topic = messageEvent.topic;
32679
- const buffer = this.annotationsMap.get(topic) ?? [];
32680
- buffer.push(entry);
32681
-
32682
- // Keep unlimited buffer length, but drop entries older than 20 seconds based on arrival time
32683
- const retentionSec = 20;
32684
- const nowSec = performance.now() / 1000;
32685
- const cutoffArrival = nowSec - retentionSec;
32686
- const trimmed = buffer.filter(e => e.arrivalSec >= cutoffArrival);
32687
- this.annotationsMap.set(topic, trimmed);
32688
- }
32689
-
32690
- // Always emit; render selection logic will pick appropriate annotations for the last image
32691
- this.#emitState();
32692
- if (this.#config.synchronize === false) {
32693
- return;
32694
- }
32695
-
32696
- // synchronized mode does selection in #getRenderState
32697
- };
32698
- setConfig(newConfig) {
32699
- let changed = false;
32700
- if (newConfig.synchronize != undefined && newConfig.synchronize !== this.#config.synchronize) {
32701
- this.#oldRenderState = undefined;
32702
- changed = true;
32703
- }
32704
- if ("imageTopic" in newConfig && this.#config.imageTopic !== newConfig.imageTopic) {
32705
- this.#lastReceivedMessages.image = undefined;
32706
- changed = true;
32707
- }
32708
- if (this.#config.calibrationTopic !== newConfig.calibrationTopic) {
32709
- this.#lastReceivedMessages.cameraInfo = undefined;
32710
- changed = true;
32711
- }
32712
- if (newConfig.annotations != undefined && this.#config.annotations && this.#config.annotations !== newConfig.annotations) {
32713
- const newVisibleTopics = new Set();
32714
- for (const [topic, settings] of Object.entries(newConfig.annotations)) {
32715
- if (settings?.visible === true) {
32716
- newVisibleTopics.add(topic);
32717
- }
32718
- }
32719
- for (const topic of this.#lastReceivedMessages.annotationsByTopic.keys()) {
32720
- if (!newVisibleTopics.has(topic)) {
32721
- this.#lastReceivedMessages.annotationsByTopic.delete(topic);
32722
- changed = true;
32723
- }
32724
- }
32725
- }
32726
- this.#config = newConfig;
32727
- if (changed) {
32728
- this.#emitState();
32729
- }
32730
- }
32731
- setAvailableAnnotationTopics(topicNames) {
32732
- this.availableAnnotationTopics = new Set(topicNames);
32733
- this.#emitState();
32734
- }
32735
- clear() {
32736
- this.#lastReceivedMessages = {
32737
- annotationsByTopic: new Map()
32738
- };
32739
- this.annotationsMap.clear();
32740
- this.lastSelectedByTopic.clear();
32741
- this.lastImage = undefined;
32742
- this.imagesBuffer = [];
32743
- this.#oldRenderState = undefined;
32744
- this.#emitState();
32745
- }
32746
- #emitState() {
32747
- const state = this.getRenderStateAndUpdateHUD();
32748
- this.#listeners.forEach(fn => {
32749
- fn(state, this.#oldRenderState);
32750
- });
32751
- this.#oldRenderState = state;
32752
- }
32753
-
32754
- /** Do not use. only public for testing */
32755
- getRenderStateAndUpdateHUD() {
32756
- const state = this.#getRenderState();
32757
- this.#updateHUDFromState(state);
32758
- return state;
32759
- }
32760
- #updateHUDFromState(state) {
32761
- const calibrationRequired = this.#config.calibrationTopic != undefined;
32762
- const waitingForImage = this.#lastReceivedMessages.image == undefined && state.image == undefined;
32763
- const waitingForCalibration = calibrationRequired && state.cameraInfo == undefined;
32764
- const waitingForBoth = waitingForImage && waitingForCalibration;
32765
- this.#hud.displayIfTrue(waitingForBoth, WAITING_FOR_BOTH_HUD_ITEM);
32766
- this.#hud.displayIfTrue(waitingForCalibration && !waitingForBoth, WAITING_FOR_CALIBRATION_HUD_ITEM);
32767
- this.#hud.displayIfTrue(waitingForImage && !calibrationRequired && !waitingForBoth, WAITING_FOR_IMAGE_EMPTY_HUD_ITEM);
32768
- this.#hud.displayIfTrue(waitingForImage && calibrationRequired, WAITING_FOR_IMAGE_NOTICE_HUD_ITEM);
32769
- }
32770
- #getRenderState() {
32771
- if (this.imagesBuffer.length === 0) {
32772
- return {
32773
- annotationsByTopic: new Map(),
32774
- presentAnnotationTopics: undefined,
32775
- missingAnnotationTopics: undefined
32776
- };
32777
- }
32778
-
32779
- // Visible topics
32780
- const visibleTopics = new Set();
32781
- if (this.#config.annotations) {
32782
- for (const [topic, settings] of Object.entries(this.#config.annotations)) {
32783
- if (settings?.visible) visibleTopics.add(topic);
32784
- }
32785
- }
32786
-
32787
- // If there are no visible annotation topics, just render the latest image
32788
- if (visibleTopics.size === 0) {
32789
- const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
32790
- return {
32791
- image: newestImageEntry.image,
32792
- annotationsByTopic: new Map(),
32793
- presentAnnotationTopics: undefined,
32794
- missingAnnotationTopics: undefined
32795
- };
32796
- }
32797
-
32798
- // Fast-path: if none of the visible topics has any annotation data yet, render image only
32799
- let hasAnyAnnotationForVisible = false;
32800
- for (const t of visibleTopics) {
32801
- const buf = this.annotationsMap.get(t);
32802
- if (buf && buf.length > 0) {
32803
- hasAnyAnnotationForVisible = true;
32804
- break;
32805
- }
32806
- }
32807
- if (!hasAnyAnnotationForVisible) {
32808
- const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
32809
- return {
32810
- image: newestImageEntry.image,
32811
- annotationsByTopic: new Map(),
32812
- presentAnnotationTopics: undefined,
32813
- missingAnnotationTopics: undefined
32814
- };
32815
- }
32816
-
32817
- // Required topics (available & visible)
32818
- const requiredTopics = [];
32819
- for (const t of visibleTopics) {
32820
- if (this.availableAnnotationTopics.size === 0 || this.availableAnnotationTopics.has(t)) {
32821
- requiredTopics.push(t);
32822
- }
32823
- }
32824
-
32825
- // New policy: always start from newest image by arrival (do not freeze on older frame).
32826
- const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
32827
- const imageTimestampSec = typeof newestImageEntry.stampTimeSec === "number" ? newestImageEntry.stampTimeSec : newestImageEntry.arrivalSec;
32828
- const selectedAnnotations = new Map();
32829
- const presentTopics = [];
32830
- const missingTopics = [];
32831
- for (const topic of visibleTopics) {
32832
- const buffer = this.annotationsMap.get(topic);
32833
- if (!buffer || buffer.length === 0) {
32834
- // No annotations yet for this topic
32835
- if (requiredTopics.includes(topic)) ;
32836
- continue;
32837
- }
32838
-
32839
- // Find preferred (newest <= image time within staleness)
32840
- let preferred;
32841
- for (let i = buffer.length - 1; i >= 0; i--) {
32842
- const entry = buffer[i];
32843
- const age = imageTimestampSec - entry.timeSec;
32844
- if (age >= 0 && age <= maxStalenessSec) {
32845
- preferred = entry;
32846
- break;
32847
- }
32848
- }
32849
-
32850
- // Fallback: closest within tolerance window
32851
- let closest = buffer[0];
32852
- let closestDelta = Math.abs(closest.timeSec - imageTimestampSec);
32853
- for (let i = 1; i < buffer.length; i++) {
32854
- const entry = buffer[i];
32855
- const delta = Math.abs(entry.timeSec - imageTimestampSec);
32856
- if (delta < closestDelta) {
32857
- closest = entry;
32858
- closestDelta = delta;
32859
- }
32860
- }
32861
- let candidate = preferred ?? closest;
32862
- if (!candidate) continue;
32863
- const candidateDelta = Math.abs(candidate.timeSec - imageTimestampSec);
32864
- const candidateAge = imageTimestampSec - candidate.timeSec;
32865
- const acceptable = preferred && candidateAge >= 0 && candidateAge <= maxStalenessSec || !preferred && candidateDelta <= syncToleranceSec;
32866
- const notFutureFar = candidate.timeSec <= imageTimestampSec + syncToleranceSec;
32867
- if (!acceptable || !notFutureFar) {
32868
- // can't use for now
32869
- continue;
32870
- }
32871
- // Hysteresis: keep previous if close
32872
- const prevTime = this.lastSelectedByTopic.get(topic);
32873
- if (prevTime != undefined) {
32874
- const prevEntry = buffer.find(e => Math.abs(e.timeSec - prevTime) < 1e-6);
32875
- if (prevEntry) {
32876
- const prevDelta = Math.abs(prevEntry.timeSec - imageTimestampSec);
32877
- const prevAge = imageTimestampSec - prevEntry.timeSec;
32878
- const prevValid = prevAge >= 0 && prevAge <= maxStalenessSec || prevDelta <= syncToleranceSec;
32879
- if (prevValid && Math.abs(prevEntry.timeSec - candidate.timeSec) <= hysteresisSec) {
32880
- candidate = prevEntry;
32881
- }
32882
- }
32883
- }
32884
- selectedAnnotations.set(topic, candidate.annotation);
32885
- presentTopics.push(topic);
32886
- this.lastSelectedByTopic.set(topic, candidate.timeSec);
32887
- }
32888
-
32889
- // Determine missing topics (we have at least some annotation messages for them but could not align one this frame)
32890
- for (const t of requiredTopics) {
32891
- if (!presentTopics.includes(t)) {
32892
- const buf = this.annotationsMap.get(t);
32893
- if (buf && buf.length > 0) {
32894
- // We received data but could not synchronize within tolerance -> mark missing
32895
- missingTopics.push(t);
32896
- }
32897
- }
32898
- }
32899
-
32900
- // Always return newest image; do not block rendering if annotations incomplete
32901
- return {
32902
- image: newestImageEntry.image,
32903
- annotationsByTopic: selectedAnnotations,
32904
- // Provide mismatch info only if we have both present and missing; otherwise undefined to reduce HUD noise
32905
- presentAnnotationTopics: presentTopics.length > 0 && missingTopics.length > 0 ? presentTopics.sort() : undefined,
32906
- missingAnnotationTopics: presentTopics.length > 0 && missingTopics.length > 0 ? missingTopics.sort() : undefined
32907
- };
32908
- }
32909
- }
32910
-
32911
- // This Source Code Form is subject to the terms of the Mozilla Public
32912
- // License, v2.0. If a copy of the MPL was not distributed with this
32913
- // file, You can obtain one at http://mozilla.org/MPL/2.0/
32914
-
32915
- /** Want to render after all other objects in the scene so that they are not occluded by other objects */
32916
- const ANNOTATION_FRONT_POSITION = 100000;
32917
-
32918
- /** Render order for given annotations. Higher numbers rendered after lower numbers */
32919
- const ANNOTATION_RENDER_ORDER = {
32920
- FILL: 1 + ANNOTATION_FRONT_POSITION,
32921
- LINE_PREPASS: 2 + ANNOTATION_FRONT_POSITION,
32922
- LINE: 3 + ANNOTATION_FRONT_POSITION,
32923
- POINTS: 4 + ANNOTATION_FRONT_POSITION,
32924
- TEXT: 5 + ANNOTATION_FRONT_POSITION
32925
- };
32926
-
32927
- /** we want annotations to show on top of the entire scene. These are material props to achieve that */
32928
- const annotationRenderOrderMaterialProps = {
32929
- /** We need to set transparent to true so that transparent objects aren't rendered on top of it.
32930
- * Transparent objects are rendered after non-transparent objects. If this were set to false or
32931
- * set based on color of annotations, then the foreground image with opacity would be rendered on top
32932
- * until it is fully opaque.
32933
- */
32934
- transparent: true,
32935
- depthWrite: false,
32936
- depthTest: false
32937
- };
32938
-
32939
- // This Source Code Form is subject to the terms of the Mozilla Public
32940
- // License, v2.0. If a copy of the MPL was not distributed with this
32941
- // file, You can obtain one at http://mozilla.org/MPL/2.0/
32942
-
32943
32482
  const tempVec3$8 = new Vector3();
32944
32483
  let PickingMaterial$2 = class PickingMaterial extends LineMaterialWithAlphaVertex {
32945
32484
  constructor() {
@@ -34082,6 +33621,895 @@ class ImageAnnotations extends Object3D {
34082
33621
  // License, v2.0. If a copy of the MPL was not distributed with this
34083
33622
  // file, You can obtain one at http://mozilla.org/MPL/2.0/
34084
33623
 
33624
+ function normalizeImageData(data) {
33625
+ if (data == undefined) {
33626
+ return new Uint8Array(0);
33627
+ } else if (data instanceof Int8Array || data instanceof Uint8Array) {
33628
+ return data;
33629
+ } else {
33630
+ return new Uint8Array(0);
33631
+ }
33632
+ }
33633
+ function normalizeRosImage(message) {
33634
+ return {
33635
+ header: normalizeHeader(message.header),
33636
+ height: message.height ?? 0,
33637
+ width: message.width ?? 0,
33638
+ encoding: message.encoding ?? "",
33639
+ is_bigendian: message.is_bigendian ?? false,
33640
+ step: message.step ?? 0,
33641
+ data: normalizeImageData(message.data)
33642
+ };
33643
+ }
33644
+ function normalizeRosCompressedImage(message) {
33645
+ return {
33646
+ header: normalizeHeader(message.header),
33647
+ format: message.format ?? "",
33648
+ data: normalizeByteArray(message.data)
33649
+ };
33650
+ }
33651
+ function normalizeRawImage(message) {
33652
+ return {
33653
+ timestamp: normalizeTime(message.timestamp),
33654
+ frame_id: message.frame_id ?? "",
33655
+ height: message.height ?? 0,
33656
+ width: message.width ?? 0,
33657
+ encoding: message.encoding ?? "",
33658
+ step: (message.stride || message.step) ?? 0,
33659
+ data: normalizeImageData(message.data),
33660
+ stride: message.stride,
33661
+ planeStride: message.planeStride
33662
+ };
33663
+ }
33664
+ function normalizeCompressedImage(message) {
33665
+ return {
33666
+ timestamp: normalizeTime(message.timestamp),
33667
+ frame_id: message.frame_id ?? "",
33668
+ format: message.format ?? "",
33669
+ data: normalizeByteArray(message.data)
33670
+ };
33671
+ }
33672
+
33673
+ // This Source Code Form is subject to the terms of the Mozilla Public
33674
+ // License, v2.0. If a copy of the MPL was not distributed with this
33675
+ // file, You can obtain one at http://mozilla.org/MPL/2.0/
33676
+
33677
+ const syncToleranceSec$1 = 0.15; // maximum acceptable time difference for selecting a “closest” annotation.
33678
+ const maxStalenessSec$1 = 1.1; // how long we can hold the last older annotation if no better match is available. 1s corresponds to 1 FPS + 100ms window for jitter.
33679
+ const hysteresisSec$1 = 0.1; // how much “stickiness” we allow before switching candidates.
33680
+ const imageRetentionSec$1 = 5; // keep small window of recent images
33681
+ const maxImageBuffer$1 = 150; // cap number of stored images
33682
+
33683
+ // Have constants for the HUD items so that they don't need to be recreated and GCed every message
33684
+ const WAITING_FOR_BOTH_HUD_ITEM$1 = {
33685
+ id: WAITING_FOR_BOTH_MESSAGES_HUD_ID,
33686
+ group: IMAGE_MODE_HUD_GROUP_ID,
33687
+ getMessage: () => t3D("waitingForCalibrationAndImages"),
33688
+ displayType: "empty"
33689
+ };
33690
+ const WAITING_FOR_CALIBRATION_HUD_ITEM$1 = {
33691
+ id: WAITING_FOR_CALIBRATION_HUD_ID,
33692
+ group: IMAGE_MODE_HUD_GROUP_ID,
33693
+ getMessage: () => t3D("waitingForCalibration"),
33694
+ displayType: "empty"
33695
+ };
33696
+ const WAITING_FOR_IMAGE_NOTICE_HUD_ITEM$1 = {
33697
+ id: WAITING_FOR_IMAGES_NOTICE_ID,
33698
+ group: IMAGE_MODE_HUD_GROUP_ID,
33699
+ getMessage: () => t3D("waitingForImages"),
33700
+ displayType: "notice"
33701
+ };
33702
+ const WAITING_FOR_IMAGE_EMPTY_HUD_ITEM$1 = {
33703
+ id: WAITING_FOR_IMAGES_EMPTY_HUD_ID,
33704
+ group: IMAGE_MODE_HUD_GROUP_ID,
33705
+ getMessage: () => t3D("waitingForImages"),
33706
+ displayType: "empty"
33707
+ };
33708
+
33709
+ /**
33710
+ * Processes and normalizes incoming messages and manages state of
33711
+ * messages to be rendered given the ImageMode config. A large part of this responsibility
33712
+ * is managing state in synchronized mode and ensuring that the a synchronized set of image and
33713
+ * annotations are handed off to the SceneExtension for rendering.
33714
+ */
33715
+ class MessageHandler {
33716
+ /** settings that should reflect image mode config */
33717
+ #config;
33718
+
33719
+ /** Allows message handler push messages to overlay on top of the canvas */
33720
+ #hud;
33721
+
33722
+ /** last state passed to listeners */
33723
+ #oldRenderState;
33724
+
33725
+ /** internal state of last received messages */
33726
+ #lastReceivedMessages;
33727
+
33728
+ /** listener functions that are called when the state changes. */
33729
+ #listeners = [];
33730
+
33731
+ /** Holds what annotations are currently available on the given source. These are needed because annotations
33732
+ * that are marked as visible may be present in the layout/config, but are not present on the source.
33733
+ * This can cause synchronized annotations to never resolve if the source does not have the annotation topic
33734
+ * with no indication to the user that the annotation is not available.
33735
+ */
33736
+
33737
+ #onAnnotationReceivedEvent;
33738
+ #annotationsFpsRefreshInterval = null;
33739
+ // New: keep a buffer of recent images for annotation-prioritized synchronization
33740
+ imagesBuffer = [];
33741
+ // Store a small buffer of recent annotations per topic with computed seconds timestamps
33742
+ annotationsMap = new Map();
33743
+ // Track last selected annotation per topic to add hysteresis and reduce flicker
33744
+ lastSelectedByTopic = new Map();
33745
+ /**
33746
+ * Create a MessageHandler
33747
+ * @param {Immutable<MessageHandlerConfig>} config - subset of ImageMode settings required for message handling
33748
+ * @param {HUDItemManager} hud - HUD manager used to display informational messages on the canvas
33749
+ */
33750
+ constructor(config, hud) {
33751
+ this.#config = config;
33752
+ this.#hud = hud;
33753
+ this.#lastReceivedMessages = {
33754
+ annotationsByTopic: new Map()
33755
+ };
33756
+ this.availableAnnotationTopics = new Set();
33757
+ }
33758
+ dispose() {
33759
+ if (this.#annotationsFpsRefreshInterval) {
33760
+ clearInterval(this.#annotationsFpsRefreshInterval);
33761
+ }
33762
+ }
33763
+ addListener(listener) {
33764
+ this.#listeners.push(listener);
33765
+ }
33766
+ addAnnotationReceivedEventHandler(handler) {
33767
+ this.#onAnnotationReceivedEvent = handler;
33768
+ }
33769
+ removeListener(listener) {
33770
+ this.#listeners = this.#listeners.filter(fn => fn !== listener);
33771
+ }
33772
+ handleRosRawImage = messageEvent => {
33773
+ this.handleImage(messageEvent, normalizeRosImage(messageEvent.message));
33774
+ };
33775
+ handleRosCompressedImage = messageEvent => {
33776
+ this.handleImage(messageEvent, normalizeRosCompressedImage(messageEvent.message));
33777
+ };
33778
+ handleRawImage = messageEvent => {
33779
+ this.handleImage(messageEvent, normalizeRawImage(messageEvent.message));
33780
+ };
33781
+ handleCompressedImage = messageEvent => {
33782
+ this.handleImage(messageEvent, normalizeCompressedImage(messageEvent.message));
33783
+ };
33784
+ handleImage(message, image) {
33785
+ this.lastImage = {
33786
+ ...message,
33787
+ message: image
33788
+ };
33789
+
33790
+ // Compute timestamp seconds for buffer storage (prefer image stamp, then receiveTime, then wall clock)
33791
+ const stamp = getTimestampFromImage(image);
33792
+ let stampTimeSec;
33793
+ const stampSec = stamp?.sec;
33794
+ const stampNsec = stamp?.nsec;
33795
+ if (typeof stampSec === "number" && typeof stampNsec === "number") {
33796
+ stampTimeSec = stampSec + stampNsec / 1e9;
33797
+ } else {
33798
+ const recv = message.receiveTime;
33799
+ const recvSec = recv?.sec;
33800
+ const recvNsec = recv?.nsec;
33801
+ if (typeof recvSec === "number" && typeof recvNsec === "number") {
33802
+ stampTimeSec = recvSec + recvNsec / 1e9;
33803
+ }
33804
+ }
33805
+ const arrivalSec = performance.now() / 1000;
33806
+ this.imagesBuffer.push({
33807
+ image: this.lastImage,
33808
+ stampTimeSec,
33809
+ arrivalSec
33810
+ });
33811
+ // Trim by arrival time and length (do not use stamped time for retention)
33812
+ const nowSec = performance.now() / 1000;
33813
+ const cutoffArrival = nowSec - imageRetentionSec$1;
33814
+ this.imagesBuffer = this.imagesBuffer.filter(e => e.arrivalSec >= cutoffArrival);
33815
+ if (this.imagesBuffer.length > maxImageBuffer$1) {
33816
+ this.imagesBuffer.splice(0, this.imagesBuffer.length - maxImageBuffer$1);
33817
+ }
33818
+ this.#emitState();
33819
+ }
33820
+ handleCameraInfo = message => {
33821
+ this.#lastReceivedMessages.cameraInfo = normalizeCameraInfo(message.message);
33822
+ this.#emitState();
33823
+ };
33824
+ handleAnnotations = messageEvent => {
33825
+ const list = normalizeAnnotations(messageEvent.message, messageEvent.schemaName);
33826
+ const arrivalSec = performance.now() / 1000;
33827
+ if (list.length > 0 && messageEvent.topic) {
33828
+ this.#onAnnotationReceivedEvent?.();
33829
+
33830
+ // Prefer timestamps embedded in the annotations themselves
33831
+ const stampTimes = [];
33832
+ for (const a of list) {
33833
+ const sec = a.stamp?.sec;
33834
+ const nsec = a.stamp?.nsec;
33835
+ if (typeof sec === "number" && typeof nsec === "number") {
33836
+ stampTimes.push(sec + nsec / 1e9);
33837
+ }
33838
+ }
33839
+ const timeSec = stampTimes.length > 0 ? Math.max(...stampTimes) : arrivalSec;
33840
+ const entry = {
33841
+ annotation: {
33842
+ originalMessage: messageEvent,
33843
+ annotations: list
33844
+ },
33845
+ timeSec,
33846
+ arrivalSec
33847
+ };
33848
+ const topic = messageEvent.topic;
33849
+ const buffer = this.annotationsMap.get(topic) ?? [];
33850
+ buffer.push(entry);
33851
+
33852
+ // Keep unlimited buffer length, but drop entries older than 20 seconds based on arrival time
33853
+ const retentionSec = 20;
33854
+ const nowSec = performance.now() / 1000;
33855
+ const cutoffArrival = nowSec - retentionSec;
33856
+ const trimmed = buffer.filter(e => e.arrivalSec >= cutoffArrival);
33857
+ this.annotationsMap.set(topic, trimmed);
33858
+ }
33859
+
33860
+ // Always emit; render selection logic will pick appropriate annotations for the last image
33861
+ this.#emitState();
33862
+ if (this.#config.synchronize === false) {
33863
+ return;
33864
+ }
33865
+
33866
+ // synchronized mode does selection in #getRenderState
33867
+ };
33868
+ setConfig(newConfig) {
33869
+ let changed = false;
33870
+ if (newConfig.synchronize != undefined && newConfig.synchronize !== this.#config.synchronize) {
33871
+ this.#oldRenderState = undefined;
33872
+ changed = true;
33873
+ }
33874
+ if ("imageTopic" in newConfig && this.#config.imageTopic !== newConfig.imageTopic) {
33875
+ this.#lastReceivedMessages.image = undefined;
33876
+ changed = true;
33877
+ }
33878
+ if (this.#config.calibrationTopic !== newConfig.calibrationTopic) {
33879
+ this.#lastReceivedMessages.cameraInfo = undefined;
33880
+ changed = true;
33881
+ }
33882
+ if (newConfig.annotations != undefined && this.#config.annotations && this.#config.annotations !== newConfig.annotations) {
33883
+ const newVisibleTopics = new Set();
33884
+ for (const [topic, settings] of Object.entries(newConfig.annotations)) {
33885
+ if (settings?.visible === true) {
33886
+ newVisibleTopics.add(topic);
33887
+ }
33888
+ }
33889
+ for (const topic of this.#lastReceivedMessages.annotationsByTopic.keys()) {
33890
+ if (!newVisibleTopics.has(topic)) {
33891
+ this.#lastReceivedMessages.annotationsByTopic.delete(topic);
33892
+ changed = true;
33893
+ }
33894
+ }
33895
+ }
33896
+ this.#config = newConfig;
33897
+ if (changed) {
33898
+ this.#emitState();
33899
+ }
33900
+ }
33901
+ setAvailableAnnotationTopics(topicNames) {
33902
+ this.availableAnnotationTopics = new Set(topicNames);
33903
+ this.#emitState();
33904
+ }
33905
+ clear() {
33906
+ this.#lastReceivedMessages = {
33907
+ annotationsByTopic: new Map()
33908
+ };
33909
+ this.annotationsMap.clear();
33910
+ this.lastSelectedByTopic.clear();
33911
+ this.lastImage = undefined;
33912
+ this.imagesBuffer = [];
33913
+ this.#oldRenderState = undefined;
33914
+ this.#emitState();
33915
+ }
33916
+ #emitState() {
33917
+ const state = this.getRenderStateAndUpdateHUD();
33918
+ this.#listeners.forEach(fn => {
33919
+ fn(state, this.#oldRenderState);
33920
+ });
33921
+ this.#oldRenderState = state;
33922
+ }
33923
+
33924
+ /** Do not use. only public for testing */
33925
+ getRenderStateAndUpdateHUD() {
33926
+ const state = this.#getRenderState();
33927
+ this.#updateHUDFromState(state);
33928
+ return state;
33929
+ }
33930
+ #updateHUDFromState(state) {
33931
+ const calibrationRequired = this.#config.calibrationTopic != undefined;
33932
+ const waitingForImage = this.#lastReceivedMessages.image == undefined && state.image == undefined;
33933
+ const waitingForCalibration = calibrationRequired && state.cameraInfo == undefined;
33934
+ const waitingForBoth = waitingForImage && waitingForCalibration;
33935
+ this.#hud.displayIfTrue(waitingForBoth, WAITING_FOR_BOTH_HUD_ITEM$1);
33936
+ this.#hud.displayIfTrue(waitingForCalibration && !waitingForBoth, WAITING_FOR_CALIBRATION_HUD_ITEM$1);
33937
+ this.#hud.displayIfTrue(waitingForImage && !calibrationRequired && !waitingForBoth, WAITING_FOR_IMAGE_EMPTY_HUD_ITEM$1);
33938
+ this.#hud.displayIfTrue(waitingForImage && calibrationRequired, WAITING_FOR_IMAGE_NOTICE_HUD_ITEM$1);
33939
+ }
33940
+ #getRenderState() {
33941
+ if (this.imagesBuffer.length === 0) {
33942
+ return {
33943
+ annotationsByTopic: new Map(),
33944
+ presentAnnotationTopics: undefined,
33945
+ missingAnnotationTopics: undefined
33946
+ };
33947
+ }
33948
+
33949
+ // Visible topics
33950
+ const visibleTopics = new Set();
33951
+ if (this.#config.annotations) {
33952
+ for (const [topic, settings] of Object.entries(this.#config.annotations)) {
33953
+ if (settings?.visible) visibleTopics.add(topic);
33954
+ }
33955
+ }
33956
+
33957
+ // If there are no visible annotation topics, just render the latest image
33958
+ if (visibleTopics.size === 0) {
33959
+ const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
33960
+ return {
33961
+ image: newestImageEntry.image,
33962
+ annotationsByTopic: new Map(),
33963
+ presentAnnotationTopics: undefined,
33964
+ missingAnnotationTopics: undefined
33965
+ };
33966
+ }
33967
+
33968
+ // Fast-path: if none of the visible topics has any annotation data yet, render image only
33969
+ let hasAnyAnnotationForVisible = false;
33970
+ for (const t of visibleTopics) {
33971
+ const buf = this.annotationsMap.get(t);
33972
+ if (buf && buf.length > 0) {
33973
+ hasAnyAnnotationForVisible = true;
33974
+ break;
33975
+ }
33976
+ }
33977
+ if (!hasAnyAnnotationForVisible) {
33978
+ const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
33979
+ return {
33980
+ image: newestImageEntry.image,
33981
+ annotationsByTopic: new Map(),
33982
+ presentAnnotationTopics: undefined,
33983
+ missingAnnotationTopics: undefined
33984
+ };
33985
+ }
33986
+
33987
+ // Required topics (available & visible)
33988
+ const requiredTopics = [];
33989
+ for (const t of visibleTopics) {
33990
+ if (this.availableAnnotationTopics.size === 0 || this.availableAnnotationTopics.has(t)) {
33991
+ requiredTopics.push(t);
33992
+ }
33993
+ }
33994
+
33995
+ // New policy: always start from newest image by arrival (do not freeze on older frame).
33996
+ const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
33997
+ const imageTimestampSec = typeof newestImageEntry.stampTimeSec === "number" ? newestImageEntry.stampTimeSec : newestImageEntry.arrivalSec;
33998
+ const selectedAnnotations = new Map();
33999
+ const presentTopics = [];
34000
+ const missingTopics = [];
34001
+ for (const topic of visibleTopics) {
34002
+ const buffer = this.annotationsMap.get(topic);
34003
+ if (!buffer || buffer.length === 0) {
34004
+ // No annotations yet for this topic
34005
+ if (requiredTopics.includes(topic)) ;
34006
+ continue;
34007
+ }
34008
+
34009
+ // Find preferred (newest <= image time within staleness)
34010
+ let preferred;
34011
+ for (let i = buffer.length - 1; i >= 0; i--) {
34012
+ const entry = buffer[i];
34013
+ const age = imageTimestampSec - entry.timeSec;
34014
+ if (age >= 0 && age <= maxStalenessSec$1) {
34015
+ preferred = entry;
34016
+ break;
34017
+ }
34018
+ }
34019
+
34020
+ // Fallback: closest within tolerance window
34021
+ let closest = buffer[0];
34022
+ let closestDelta = Math.abs(closest.timeSec - imageTimestampSec);
34023
+ for (let i = 1; i < buffer.length; i++) {
34024
+ const entry = buffer[i];
34025
+ const delta = Math.abs(entry.timeSec - imageTimestampSec);
34026
+ if (delta < closestDelta) {
34027
+ closest = entry;
34028
+ closestDelta = delta;
34029
+ }
34030
+ }
34031
+ let candidate = preferred ?? closest;
34032
+ if (!candidate) continue;
34033
+ const candidateDelta = Math.abs(candidate.timeSec - imageTimestampSec);
34034
+ const candidateAge = imageTimestampSec - candidate.timeSec;
34035
+ const acceptable = preferred && candidateAge >= 0 && candidateAge <= maxStalenessSec$1 || !preferred && candidateDelta <= syncToleranceSec$1;
34036
+ const notFutureFar = candidate.timeSec <= imageTimestampSec + syncToleranceSec$1;
34037
+ if (!acceptable || !notFutureFar) {
34038
+ // can't use for now
34039
+ continue;
34040
+ }
34041
+ // Hysteresis: keep previous if close
34042
+ const prevTime = this.lastSelectedByTopic.get(topic);
34043
+ if (prevTime != undefined) {
34044
+ const prevEntry = buffer.find(e => Math.abs(e.timeSec - prevTime) < 1e-6);
34045
+ if (prevEntry) {
34046
+ const prevDelta = Math.abs(prevEntry.timeSec - imageTimestampSec);
34047
+ const prevAge = imageTimestampSec - prevEntry.timeSec;
34048
+ const prevValid = prevAge >= 0 && prevAge <= maxStalenessSec$1 || prevDelta <= syncToleranceSec$1;
34049
+ if (prevValid && Math.abs(prevEntry.timeSec - candidate.timeSec) <= hysteresisSec$1) {
34050
+ candidate = prevEntry;
34051
+ }
34052
+ }
34053
+ }
34054
+ selectedAnnotations.set(topic, candidate.annotation);
34055
+ presentTopics.push(topic);
34056
+ this.lastSelectedByTopic.set(topic, candidate.timeSec);
34057
+ }
34058
+
34059
+ // Determine missing topics (we have at least some annotation messages for them but could not align one this frame)
34060
+ for (const t of requiredTopics) {
34061
+ if (!presentTopics.includes(t)) {
34062
+ const buf = this.annotationsMap.get(t);
34063
+ if (buf && buf.length > 0) {
34064
+ // We received data but could not synchronize within tolerance -> mark missing
34065
+ missingTopics.push(t);
34066
+ }
34067
+ }
34068
+ }
34069
+
34070
+ // Always return newest image; do not block rendering if annotations incomplete
34071
+ return {
34072
+ image: newestImageEntry.image,
34073
+ annotationsByTopic: selectedAnnotations,
34074
+ // Provide mismatch info only if we have both present and missing; otherwise undefined to reduce HUD noise
34075
+ presentAnnotationTopics: presentTopics.length > 0 && missingTopics.length > 0 ? presentTopics.sort() : undefined,
34076
+ missingAnnotationTopics: presentTopics.length > 0 && missingTopics.length > 0 ? missingTopics.sort() : undefined
34077
+ };
34078
+ }
34079
+ }
34080
+
34081
+ // This Source Code Form is subject to the terms of the Mozilla Public
34082
+ // License, v2.0. If a copy of the MPL was not distributed with this
34083
+ // file, You can obtain one at http://mozilla.org/MPL/2.0/
34084
+
34085
+ const syncToleranceSec = 0.15; // maximum acceptable time difference for selecting a “closest” annotation.
34086
+ const maxStalenessSec = 1.1; // how long we can hold the last older annotation if no better match is available. 1s corresponds to 1 FPS + 100ms window for jitter.
34087
+ const hysteresisSec = 0.1; // how much “stickiness” we allow before switching candidates.
34088
+ const imageRetentionSec = 5; // keep small window of recent images
34089
+ const maxImageBuffer = 150; // cap number of stored images
34090
+
34091
+ // Have constants for the HUD items so that they don't need to be recreated and GCed every message
34092
+ const WAITING_FOR_BOTH_HUD_ITEM = {
34093
+ id: WAITING_FOR_BOTH_MESSAGES_HUD_ID,
34094
+ group: IMAGE_MODE_HUD_GROUP_ID,
34095
+ getMessage: () => t3D("waitingForCalibrationAndImages"),
34096
+ displayType: "empty"
34097
+ };
34098
+ const WAITING_FOR_CALIBRATION_HUD_ITEM = {
34099
+ id: WAITING_FOR_CALIBRATION_HUD_ID,
34100
+ group: IMAGE_MODE_HUD_GROUP_ID,
34101
+ getMessage: () => t3D("waitingForCalibration"),
34102
+ displayType: "empty"
34103
+ };
34104
+ const WAITING_FOR_IMAGE_NOTICE_HUD_ITEM = {
34105
+ id: WAITING_FOR_IMAGES_NOTICE_ID,
34106
+ group: IMAGE_MODE_HUD_GROUP_ID,
34107
+ getMessage: () => t3D("waitingForImages"),
34108
+ displayType: "notice"
34109
+ };
34110
+ const WAITING_FOR_IMAGE_EMPTY_HUD_ITEM = {
34111
+ id: WAITING_FOR_IMAGES_EMPTY_HUD_ID,
34112
+ group: IMAGE_MODE_HUD_GROUP_ID,
34113
+ getMessage: () => t3D("waitingForImages"),
34114
+ displayType: "empty"
34115
+ };
34116
+
34117
+ /**
34118
+ * Processes and normalizes incoming messages and manages state of
34119
+ * messages to be rendered given the ImageMode config. A large part of this responsibility
34120
+ * is managing state in synchronized mode and ensuring that the a synchronized set of image and
34121
+ * annotations are handed off to the SceneExtension for rendering.
34122
+ */
34123
+ class MessageHandlerV2 {
34124
+ /** settings that should reflect image mode config */
34125
+ #config;
34126
+
34127
+ /** Allows message handler push messages to overlay on top of the canvas */
34128
+ #hud;
34129
+
34130
+ /** last state passed to listeners */
34131
+ #oldRenderState;
34132
+
34133
+ /** internal state of last received messages */
34134
+ #lastReceivedMessages;
34135
+
34136
+ /** listener functions that are called when the state changes. */
34137
+ #listeners = [];
34138
+
34139
+ /** Holds what annotations are currently available on the given source. These are needed because annotations
34140
+ * that are marked as visible may be present in the layout/config, but are not present on the source.
34141
+ * This can cause synchronized annotations to never resolve if the source does not have the annotation topic
34142
+ * with no indication to the user that the annotation is not available.
34143
+ */
34144
+
34145
+ #onAnnotationReceivedEvent;
34146
+ #annotationsFpsRefreshInterval = null;
34147
+ // New: keep a buffer of recent images for annotation-prioritized synchronization
34148
+ imagesBuffer = [];
34149
+ // Store a small buffer of recent annotations per topic with computed seconds timestamps
34150
+ annotationsMap = new Map();
34151
+ // Track last selected annotation per topic to add hysteresis and reduce flicker
34152
+ lastSelectedByTopic = new Map();
34153
+ /**
34154
+ * Create a MessageHandler
34155
+ * @param {Immutable<MessageHandlerConfig>} config - subset of ImageMode settings required for message handling
34156
+ * @param {HUDItemManager} hud - HUD manager used to display informational messages on the canvas
34157
+ */
34158
+ constructor(config, hud) {
34159
+ this.#config = config;
34160
+ this.#hud = hud;
34161
+ this.#lastReceivedMessages = {
34162
+ annotationsByTopic: new Map()
34163
+ };
34164
+ this.availableAnnotationTopics = new Set();
34165
+ }
34166
+ dispose() {
34167
+ if (this.#annotationsFpsRefreshInterval) {
34168
+ clearInterval(this.#annotationsFpsRefreshInterval);
34169
+ }
34170
+ }
34171
+ addListener(listener) {
34172
+ this.#listeners.push(listener);
34173
+ }
34174
+ addAnnotationReceivedEventHandler(handler) {
34175
+ this.#onAnnotationReceivedEvent = handler;
34176
+ }
34177
+ removeListener(listener) {
34178
+ this.#listeners = this.#listeners.filter(fn => fn !== listener);
34179
+ }
34180
+ handleRosRawImage = messageEvent => {
34181
+ this.handleImage(messageEvent, normalizeRosImage(messageEvent.message));
34182
+ };
34183
+ handleRosCompressedImage = messageEvent => {
34184
+ this.handleImage(messageEvent, normalizeRosCompressedImage(messageEvent.message));
34185
+ };
34186
+ handleRawImage = messageEvent => {
34187
+ this.handleImage(messageEvent, normalizeRawImage(messageEvent.message));
34188
+ };
34189
+ handleCompressedImage = messageEvent => {
34190
+ this.handleImage(messageEvent, normalizeCompressedImage(messageEvent.message));
34191
+ };
34192
+ handleImage(message, image) {
34193
+ this.lastImage = {
34194
+ ...message,
34195
+ message: image
34196
+ };
34197
+
34198
+ // Compute timestamp seconds for buffer storage (prefer image stamp, then receiveTime, then wall clock)
34199
+ const stamp = getTimestampFromImage(image);
34200
+ let stampTimeSec;
34201
+ const stampSec = stamp?.sec;
34202
+ const stampNsec = stamp?.nsec;
34203
+ if (typeof stampSec === "number" && typeof stampNsec === "number") {
34204
+ stampTimeSec = stampSec + stampNsec / 1e9;
34205
+ } else {
34206
+ const recv = message.receiveTime;
34207
+ const recvSec = recv?.sec;
34208
+ const recvNsec = recv?.nsec;
34209
+ if (typeof recvSec === "number" && typeof recvNsec === "number") {
34210
+ stampTimeSec = recvSec + recvNsec / 1e9;
34211
+ }
34212
+ }
34213
+ const arrivalSec = performance.now() / 1000;
34214
+ if (this.#hasVisibleAnnotationTopics()) {
34215
+ this.imagesBuffer.push({
34216
+ image: this.lastImage,
34217
+ stampTimeSec,
34218
+ arrivalSec
34219
+ });
34220
+ // Trim by arrival time and length (do not use stamped time for retention)
34221
+ const nowSec = performance.now() / 1000;
34222
+ const cutoffArrival = nowSec - imageRetentionSec;
34223
+ this.imagesBuffer = this.imagesBuffer.filter(e => e.arrivalSec >= cutoffArrival);
34224
+ if (this.imagesBuffer.length > maxImageBuffer) {
34225
+ this.imagesBuffer.splice(0, this.imagesBuffer.length - maxImageBuffer);
34226
+ }
34227
+ // Do not emit here; when annotations are visible we render on annotation arrival
34228
+ return;
34229
+ }
34230
+
34231
+ // No visible annotations -> render immediately
34232
+ this.#emitState();
34233
+ }
34234
+ handleCameraInfo = message => {
34235
+ this.#lastReceivedMessages.cameraInfo = normalizeCameraInfo(message.message);
34236
+ this.#emitState();
34237
+ };
34238
+ handleAnnotations = messageEvent => {
34239
+ const list = normalizeAnnotations(messageEvent.message, messageEvent.schemaName);
34240
+ const arrivalSec = performance.now() / 1000;
34241
+ if (list.length > 0 && messageEvent.topic) {
34242
+ this.#onAnnotationReceivedEvent?.();
34243
+
34244
+ // Prefer timestamps embedded in the annotations themselves
34245
+ const stampTimes = [];
34246
+ for (const a of list) {
34247
+ const sec = a.stamp?.sec;
34248
+ const nsec = a.stamp?.nsec;
34249
+ if (typeof sec === "number" && typeof nsec === "number") {
34250
+ stampTimes.push(sec + nsec / 1e9);
34251
+ }
34252
+ }
34253
+ const timeSec = stampTimes.length > 0 ? Math.max(...stampTimes) : arrivalSec;
34254
+ const entry = {
34255
+ annotation: {
34256
+ originalMessage: messageEvent,
34257
+ annotations: list
34258
+ },
34259
+ timeSec,
34260
+ arrivalSec
34261
+ };
34262
+ const topic = messageEvent.topic;
34263
+ const buffer = this.annotationsMap.get(topic) ?? [];
34264
+ buffer.push(entry);
34265
+
34266
+ // Keep unlimited buffer length, but drop entries older than 20 seconds based on arrival time
34267
+ const retentionSec = 20;
34268
+ const nowSec = performance.now() / 1000;
34269
+ const cutoffArrival = nowSec - retentionSec;
34270
+ const trimmed = buffer.filter(e => e.arrivalSec >= cutoffArrival);
34271
+ this.annotationsMap.set(topic, trimmed);
34272
+ }
34273
+
34274
+ // Always emit; render selection logic will pick appropriate annotations for the last image
34275
+ this.#emitState();
34276
+ if (this.#config.synchronize === false) {
34277
+ return;
34278
+ }
34279
+
34280
+ // synchronized mode does selection in #getRenderState
34281
+ };
34282
+ setConfig(newConfig) {
34283
+ let changed = false;
34284
+ if (newConfig.synchronize != undefined && newConfig.synchronize !== this.#config.synchronize) {
34285
+ this.#oldRenderState = undefined;
34286
+ changed = true;
34287
+ }
34288
+ if ("imageTopic" in newConfig && this.#config.imageTopic !== newConfig.imageTopic) {
34289
+ this.#lastReceivedMessages.image = undefined;
34290
+ changed = true;
34291
+ }
34292
+ if (this.#config.calibrationTopic !== newConfig.calibrationTopic) {
34293
+ this.#lastReceivedMessages.cameraInfo = undefined;
34294
+ changed = true;
34295
+ }
34296
+ if (newConfig.annotations != undefined && this.#config.annotations && this.#config.annotations !== newConfig.annotations) {
34297
+ const newVisibleTopics = new Set();
34298
+ for (const [topic, settings] of Object.entries(newConfig.annotations)) {
34299
+ if (settings?.visible === true) {
34300
+ newVisibleTopics.add(topic);
34301
+ }
34302
+ }
34303
+ for (const topic of this.#lastReceivedMessages.annotationsByTopic.keys()) {
34304
+ if (!newVisibleTopics.has(topic)) {
34305
+ this.#lastReceivedMessages.annotationsByTopic.delete(topic);
34306
+ changed = true;
34307
+ }
34308
+ }
34309
+ }
34310
+ this.#config = newConfig;
34311
+ if (changed) {
34312
+ this.#emitState();
34313
+ }
34314
+ }
34315
+ setAvailableAnnotationTopics(topicNames) {
34316
+ this.availableAnnotationTopics = new Set(topicNames);
34317
+ this.#emitState();
34318
+ }
34319
+ clear() {
34320
+ this.#lastReceivedMessages = {
34321
+ annotationsByTopic: new Map()
34322
+ };
34323
+ this.annotationsMap.clear();
34324
+ this.lastSelectedByTopic.clear();
34325
+ this.lastImage = undefined;
34326
+ this.imagesBuffer = [];
34327
+ this.#oldRenderState = undefined;
34328
+ this.#emitState();
34329
+ }
34330
+ #emitState() {
34331
+ const state = this.getRenderStateAndUpdateHUD();
34332
+ this.#listeners.forEach(fn => {
34333
+ fn(state, this.#oldRenderState);
34334
+ });
34335
+ this.#oldRenderState = state;
34336
+ }
34337
+
34338
+ /** Check if there are any visible annotation topics configured */
34339
+ #hasVisibleAnnotationTopics() {
34340
+ if (!this.#config.annotations) {
34341
+ return false;
34342
+ }
34343
+ for (const settings of Object.values(this.#config.annotations)) {
34344
+ if (settings?.visible === true) {
34345
+ return true;
34346
+ }
34347
+ }
34348
+ return false;
34349
+ }
34350
+
34351
+ /** Do not use. only public for testing */
34352
+ getRenderStateAndUpdateHUD() {
34353
+ const state = this.#getRenderState();
34354
+ this.#updateHUDFromState(state);
34355
+ return state;
34356
+ }
34357
+ #updateHUDFromState(state) {
34358
+ const calibrationRequired = this.#config.calibrationTopic != undefined;
34359
+ const waitingForImage = this.#lastReceivedMessages.image == undefined && state.image == undefined;
34360
+ const waitingForCalibration = calibrationRequired && state.cameraInfo == undefined;
34361
+ const waitingForBoth = waitingForImage && waitingForCalibration;
34362
+ this.#hud.displayIfTrue(waitingForBoth, WAITING_FOR_BOTH_HUD_ITEM);
34363
+ this.#hud.displayIfTrue(waitingForCalibration && !waitingForBoth, WAITING_FOR_CALIBRATION_HUD_ITEM);
34364
+ this.#hud.displayIfTrue(waitingForImage && !calibrationRequired && !waitingForBoth, WAITING_FOR_IMAGE_EMPTY_HUD_ITEM);
34365
+ this.#hud.displayIfTrue(waitingForImage && calibrationRequired, WAITING_FOR_IMAGE_NOTICE_HUD_ITEM);
34366
+ }
34367
+ #getRenderState() {
34368
+ // Visible topics
34369
+ const visibleTopics = new Set();
34370
+ if (this.#config.annotations) {
34371
+ for (const [topic, settings] of Object.entries(this.#config.annotations)) {
34372
+ if (settings?.visible) visibleTopics.add(topic);
34373
+ }
34374
+ }
34375
+ // If there are no visible annotation topics, just render the latest image
34376
+ if (visibleTopics.size === 0) {
34377
+ return {
34378
+ image: this.lastImage,
34379
+ annotationsByTopic: new Map(),
34380
+ presentAnnotationTopics: undefined,
34381
+ missingAnnotationTopics: undefined
34382
+ };
34383
+ }
34384
+
34385
+ // If annotations are visible but we have no buffered images yet, we cannot render
34386
+ if (this.imagesBuffer.length === 0) {
34387
+ return {
34388
+ annotationsByTopic: new Map(),
34389
+ presentAnnotationTopics: undefined,
34390
+ missingAnnotationTopics: undefined
34391
+ };
34392
+ }
34393
+
34394
+ // Required topics (available & visible)
34395
+ const requiredTopics = [];
34396
+ for (const t of visibleTopics) {
34397
+ if (this.availableAnnotationTopics.size === 0 || this.availableAnnotationTopics.has(t)) {
34398
+ requiredTopics.push(t);
34399
+ }
34400
+ }
34401
+
34402
+ // New policy: always start from newest image by arrival (do not freeze on older frame).
34403
+ const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
34404
+ const imageTimestampSec = typeof newestImageEntry.stampTimeSec === "number" ? newestImageEntry.stampTimeSec : newestImageEntry.arrivalSec;
34405
+ const selectedAnnotations = new Map();
34406
+ const presentTopics = [];
34407
+ const missingTopics = [];
34408
+ for (const topic of visibleTopics) {
34409
+ const buffer = this.annotationsMap.get(topic);
34410
+ if (!buffer || buffer.length === 0) {
34411
+ // No annotations yet for this topic
34412
+ if (requiredTopics.includes(topic)) ;
34413
+ continue;
34414
+ }
34415
+
34416
+ // Find preferred (newest <= image time within staleness)
34417
+ let preferred;
34418
+ for (let i = buffer.length - 1; i >= 0; i--) {
34419
+ const entry = buffer[i];
34420
+ const age = imageTimestampSec - entry.timeSec;
34421
+ if (age >= 0 && age <= maxStalenessSec) {
34422
+ preferred = entry;
34423
+ break;
34424
+ }
34425
+ }
34426
+
34427
+ // Fallback: closest within tolerance window
34428
+ let closest = buffer[0];
34429
+ let closestDelta = Math.abs(closest.timeSec - imageTimestampSec);
34430
+ for (let i = 1; i < buffer.length; i++) {
34431
+ const entry = buffer[i];
34432
+ const delta = Math.abs(entry.timeSec - imageTimestampSec);
34433
+ if (delta < closestDelta) {
34434
+ closest = entry;
34435
+ closestDelta = delta;
34436
+ }
34437
+ }
34438
+ let candidate = preferred ?? closest;
34439
+ if (!candidate) continue;
34440
+ const candidateDelta = Math.abs(candidate.timeSec - imageTimestampSec);
34441
+ const candidateAge = imageTimestampSec - candidate.timeSec;
34442
+ const acceptable = preferred && candidateAge >= 0 && candidateAge <= maxStalenessSec || !preferred && candidateDelta <= syncToleranceSec;
34443
+ const notFutureFar = candidate.timeSec <= imageTimestampSec + syncToleranceSec;
34444
+ if (!acceptable || !notFutureFar) {
34445
+ // can't use for now
34446
+ continue;
34447
+ }
34448
+ // Hysteresis: keep previous if close
34449
+ const prevTime = this.lastSelectedByTopic.get(topic);
34450
+ if (prevTime != undefined) {
34451
+ const prevEntry = buffer.find(e => Math.abs(e.timeSec - prevTime) < 1e-6);
34452
+ if (prevEntry) {
34453
+ const prevDelta = Math.abs(prevEntry.timeSec - imageTimestampSec);
34454
+ const prevAge = imageTimestampSec - prevEntry.timeSec;
34455
+ const prevValid = prevAge >= 0 && prevAge <= maxStalenessSec || prevDelta <= syncToleranceSec;
34456
+ if (prevValid && Math.abs(prevEntry.timeSec - candidate.timeSec) <= hysteresisSec) {
34457
+ candidate = prevEntry;
34458
+ }
34459
+ }
34460
+ }
34461
+ selectedAnnotations.set(topic, candidate.annotation);
34462
+ presentTopics.push(topic);
34463
+ this.lastSelectedByTopic.set(topic, candidate.timeSec);
34464
+ }
34465
+
34466
+ // Determine missing topics (we have at least some annotation messages for them but could not align one this frame)
34467
+ for (const t of requiredTopics) {
34468
+ if (!presentTopics.includes(t)) {
34469
+ const buf = this.annotationsMap.get(t);
34470
+ if (buf && buf.length > 0) {
34471
+ // We received data but could not synchronize within tolerance -> mark missing
34472
+ missingTopics.push(t);
34473
+ }
34474
+ }
34475
+ }
34476
+
34477
+ // Always return newest image; do not block rendering if annotations incomplete
34478
+ return {
34479
+ image: newestImageEntry.image,
34480
+ annotationsByTopic: selectedAnnotations,
34481
+ // Provide mismatch info only if we have both present and missing; otherwise undefined to reduce HUD noise
34482
+ presentAnnotationTopics: presentTopics.length > 0 && missingTopics.length > 0 ? presentTopics.sort() : undefined,
34483
+ missingAnnotationTopics: presentTopics.length > 0 && missingTopics.length > 0 ? missingTopics.sort() : undefined
34484
+ };
34485
+ }
34486
+ }
34487
+
34488
+ class ImageModeMessageHandlerFactory {
34489
+ static getMessageHandler(config, hud) {
34490
+ const messageHandlerVersion = ImageModeMessageHandlerFactory.getSupportedVersion();
34491
+
34492
+ // TODO: Remove
34493
+ console.debug(`Using message handler ${messageHandlerVersion}`);
34494
+ return messageHandlerVersion === "v2" ? new MessageHandlerV2(config, hud) : new MessageHandler(config, hud);
34495
+ }
34496
+ static getSupportedVersion() {
34497
+ const localStorageString = localStorage.getItem("connectionConfig");
34498
+ if (!localStorageString) {
34499
+ return "v1";
34500
+ }
34501
+ const parsedConfig = JSON.parse(localStorageString);
34502
+ if (parsedConfig.messageHandlerVersion === "v2") {
34503
+ return "v2";
34504
+ }
34505
+ return "v1";
34506
+ }
34507
+ }
34508
+
34509
+ // This Source Code Form is subject to the terms of the Mozilla Public
34510
+ // License, v2.0. If a copy of the MPL was not distributed with this
34511
+ // file, You can obtain one at http://mozilla.org/MPL/2.0/
34512
+
34085
34513
  const log$4 = Logger.getLogger("src/index.ts");
34086
34514
  const CALIBRATION_TOPIC_PATH = ["imageMode", "calibrationTopic"];
34087
34515
  const IMAGE_TOPIC_UNAVAILABLE = "IMAGE_TOPIC_UNAVAILABLE";
@@ -34189,7 +34617,7 @@ class ImageMode extends SceneExtension {
34189
34617
  this.#handleTopicsChanged();
34190
34618
  }
34191
34619
  initMessageHandler(config) {
34192
- return new MessageHandler(config, this.hud);
34620
+ return ImageModeMessageHandlerFactory.getMessageHandler(config, this.hud);
34193
34621
  }
34194
34622
  hasModifiedView() {
34195
34623
  return this.#hasModifiedView;