@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.
- package/dist/{index-CCsfCpZT.js → index-7BbbXb7C.js} +1 -1
- package/dist/{index-CZkF1YCY.js → index-B5d7u70w.js} +1 -1
- package/dist/{index-FM6vEyS1.js → index-BGrghyc8.js} +1 -1
- package/dist/{index-CaE-gPi_.js → index-BSEL9eaF.js} +1 -1
- package/dist/{index-CZ6h7zpz.js → index-Bjwf7lc9.js} +1 -1
- package/dist/{index-BNMXWFDA.js → index-Bndiqw1t.js} +1 -1
- package/dist/{index-CRTwfb76.js → index-BzPticWU.js} +1 -1
- package/dist/{index-B3cHfrcw.js → index-C99kj7Bj.js} +1 -1
- package/dist/{index-ugbolO1H.js → index-CO2KzlVr.js} +1 -1
- package/dist/{index-BuGHi09K.js → index-CfGQhG__.js} +1 -1
- package/dist/{index-C92x9EiC.js → index-CwMsPSeQ.js} +2 -2
- package/dist/{index-D7JYN_IN.js → index-D5xb8lsX.js} +914 -486
- package/dist/{index-BhLYvhjo.js → index-D9b6Lu-5.js} +1 -1
- package/dist/{index-KDfpYD4q.js → index-DEr2t1uV.js} +1 -1
- package/dist/{index-BzAo-6Ol.js → index-DXgxTZSz.js} +1 -1
- package/dist/{index-DW9FJFJz.js → index-DoCri7UR.js} +1 -1
- package/dist/{index-2Ajz1Y1F.js → index-DtErPDFb.js} +1 -1
- package/dist/{index-CLHPbWo6.js → index-DycPqoQx.js} +37 -24
- package/dist/{index-HcH5t5Pw.js → index-pfvcsvrO.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/src/connection/connection.d.ts +3 -1
- package/dist/lib/src/connection/connection.d.ts.map +1 -1
- package/dist/lib/src/connection/connection.js +3 -2
- package/dist/lib/src/connection/connection.js.map +1 -1
- package/dist/lib/src/output.css +4 -2
- package/dist/lib/src/utils/config-store.d.ts +3 -0
- package/dist/lib/src/utils/config-store.d.ts.map +1 -1
- package/dist/lib/src/utils/config-store.js +8 -0
- package/dist/lib/src/utils/config-store.js.map +1 -1
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/ImageMode.d.ts.map +1 -1
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/ImageMode.js +2 -2
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/ImageMode.js.map +1 -1
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandler.d.ts +3 -3
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandler.d.ts.map +1 -1
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandler.js +1 -1
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandler.js.map +1 -1
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerFactory.d.ts +11 -0
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerFactory.d.ts.map +1 -0
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerFactory.js +24 -0
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerFactory.js.map +1 -0
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerV2.d.ts +94 -0
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerV2.d.ts.map +1 -0
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerV2.js +410 -0
- package/dist/packages/studio-base/src/panels/ThreeDeeRender/renderables/ImageMode/MessageHandlerV2.js.map +1 -0
- 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-
|
|
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
|
-
|
|
32225
|
-
|
|
32226
|
-
|
|
32227
|
-
|
|
32228
|
-
|
|
32229
|
-
|
|
32230
|
-
|
|
32231
|
-
|
|
32232
|
-
|
|
32233
|
-
|
|
32234
|
-
|
|
32235
|
-
|
|
32236
|
-
|
|
32237
|
-
|
|
32238
|
-
|
|
32239
|
-
|
|
32240
|
-
|
|
32241
|
-
|
|
32242
|
-
|
|
32243
|
-
|
|
32244
|
-
|
|
32245
|
-
|
|
32246
|
-
|
|
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
|
|
34620
|
+
return ImageModeMessageHandlerFactory.getMessageHandler(config, this.hud);
|
|
34193
34621
|
}
|
|
34194
34622
|
hasModifiedView() {
|
|
34195
34623
|
return this.#hasModifiedView;
|