diff --git a/packages/studio-base/src/panels/ThreeDeeRender/foxglove.ts b/packages/studio-base/src/panels/ThreeDeeRender/foxglove.ts index 23b338841a..d5234dde2a 100644 --- a/packages/studio-base/src/panels/ThreeDeeRender/foxglove.ts +++ b/packages/studio-base/src/panels/ThreeDeeRender/foxglove.ts @@ -8,9 +8,24 @@ addFoxgloveSchema(FRAME_TRANSFORM_DATATYPES, "foxglove.FrameTransform"); export const POINTCLOUD_DATATYPES = new Set(); addFoxgloveSchema(POINTCLOUD_DATATYPES, "foxglove.PointCloud"); +export const RAW_IMAGE_DATATYPES = new Set(); +addFoxgloveSchema(RAW_IMAGE_DATATYPES, "foxglove.RawImage"); + +export const COMPRESSED_IMAGE_DATATYPES = new Set(); +addFoxgloveSchema(COMPRESSED_IMAGE_DATATYPES, "foxglove.CompressedImage"); + +export const CAMERA_CALIBRATION_DATATYPES = new Set(); +addFoxgloveSchema(CAMERA_CALIBRATION_DATATYPES, "foxglove.CameraCalibration"); + export const SCENE_UPDATE_DATATYPES = new Set(); addFoxgloveSchema(SCENE_UPDATE_DATATYPES, "foxglove.SceneUpdate"); +export const POSE_IN_FRAME_DATATYPES = new Set(); +addFoxgloveSchema(POSE_IN_FRAME_DATATYPES, "foxglove.PoseInFrame"); + +export const POSES_IN_FRAME_DATATYPES = new Set(); +addFoxgloveSchema(POSES_IN_FRAME_DATATYPES, "foxglove.PosesInFrame"); + // Expand a single Foxglove dataType into variations for ROS1 and ROS2 then add // them to the given output set function addFoxgloveSchema(output: Set, dataType: string): Set { diff --git a/packages/studio-base/src/panels/ThreeDeeRender/renderables/Cameras.ts b/packages/studio-base/src/panels/ThreeDeeRender/renderables/Cameras.ts index f1e425a233..05728d72bb 100644 --- a/packages/studio-base/src/panels/ThreeDeeRender/renderables/Cameras.ts +++ b/packages/studio-base/src/panels/ThreeDeeRender/renderables/Cameras.ts @@ -5,6 +5,7 @@ import { PinholeCameraModel } from "@foxglove/den/image"; import Logger from "@foxglove/log"; import { toNanoSec } from "@foxglove/rostime"; +import { CameraCalibration } from "@foxglove/schemas/schemas/typescript"; import { SettingsTreeAction, SettingsTreeFields } from "@foxglove/studio"; import type { RosValue } from "@foxglove/studio-base/players/types"; import { MutablePoint } from "@foxglove/studio-base/types/Messages"; @@ -14,10 +15,11 @@ import { Renderer } from "../Renderer"; import { PartialMessage, PartialMessageEvent, SceneExtension } from "../SceneExtension"; import { SettingsTreeEntry } from "../SettingsManager"; import { makeRgba, rgbaToCssString, stringToRgba } from "../color"; -import { normalizeHeader } from "../normalizeMessages"; +import { CAMERA_CALIBRATION_DATATYPES } from "../foxglove"; +import { normalizeHeader, normalizeTime } from "../normalizeMessages"; import { CameraInfo, - CAMERA_INFO_DATATYPES, + CAMERA_INFO_DATATYPES as ROS_CAMERA_INFO_DATATYPES, IncomingCameraInfo, Marker, MarkerAction, @@ -61,6 +63,7 @@ export type CameraInfoUserData = BaseUserData & { settings: LayerSettingsCameraInfo; topic: string; cameraInfo: CameraInfo | undefined; + originalMessage: Record | undefined; cameraModel: PinholeCameraModel | undefined; lines: RenderableLineList | undefined; }; @@ -72,7 +75,7 @@ export class CameraInfoRenderable extends Renderable { } public override details(): Record { - return this.userData.cameraInfo ?? {}; + return this.userData.originalMessage ?? {}; } } @@ -80,7 +83,8 @@ export class Cameras extends SceneExtension { public constructor(renderer: Renderer) { super("foxglove.Cameras", renderer); - renderer.addDatatypeSubscriptions(CAMERA_INFO_DATATYPES, this.handleCameraInfo); + renderer.addDatatypeSubscriptions(ROS_CAMERA_INFO_DATATYPES, this.handleCameraInfo); + renderer.addDatatypeSubscriptions(CAMERA_CALIBRATION_DATATYPES, this.handleCameraInfo); } public override settingsNodes(): SettingsTreeEntry[] { @@ -88,7 +92,10 @@ export class Cameras extends SceneExtension { const handler = this.handleSettingsAction; const entries: SettingsTreeEntry[] = []; for (const topic of this.renderer.topics ?? []) { - if (CAMERA_INFO_DATATYPES.has(topic.datatype)) { + if ( + ROS_CAMERA_INFO_DATATYPES.has(topic.datatype) || + CAMERA_CALIBRATION_DATATYPES.has(topic.datatype) + ) { const config = (configTopics[topic.name] ?? {}) as Partial; // prettier-ignore @@ -125,17 +132,25 @@ export class Cameras extends SceneExtension { const topicName = path[1]!; const renderable = this.renderables.get(topicName); if (renderable) { - const { cameraInfo, receiveTime } = renderable.userData; + const { cameraInfo, receiveTime, originalMessage } = renderable.userData; if (cameraInfo) { const settings = this.renderer.config.topics[topicName] as | Partial | undefined; - this._updateCameraInfoRenderable(renderable, cameraInfo, receiveTime, settings); + this._updateCameraInfoRenderable( + renderable, + cameraInfo, + originalMessage, + receiveTime, + settings, + ); } } }; - private handleCameraInfo = (messageEvent: PartialMessageEvent): void => { + private handleCameraInfo = ( + messageEvent: PartialMessageEvent, + ): void => { const topic = messageEvent.topic; const cameraInfo = normalizeCameraInfo(messageEvent.message); const receiveTime = toNanoSec(messageEvent.receiveTime); @@ -160,6 +175,7 @@ export class Cameras extends SceneExtension { settings, topic, cameraInfo: undefined, + originalMessage: undefined, cameraModel: undefined, lines: undefined, }); @@ -171,6 +187,7 @@ export class Cameras extends SceneExtension { this._updateCameraInfoRenderable( renderable, cameraInfo, + messageEvent.message, receiveTime, renderable.userData.settings, ); @@ -179,6 +196,7 @@ export class Cameras extends SceneExtension { private _updateCameraInfoRenderable( renderable: CameraInfoRenderable, cameraInfo: CameraInfo, + originalMessage: Record | undefined, receiveTime: bigint, settings: Partial | undefined, ): void { @@ -200,6 +218,7 @@ export class Cameras extends SceneExtension { if (!dataEqual) { // log.warn(`CameraInfo changed on topic "${topic}", updating rectification model`); renderable.userData.cameraInfo = cameraInfo; + renderable.userData.originalMessage = originalMessage; if (cameraInfo.P.length === 12) { try { @@ -431,7 +450,9 @@ function normalizeRegionOfInterest( }; } -function normalizeCameraInfo(message: PartialMessage): CameraInfo { +function normalizeCameraInfo( + message: PartialMessage & PartialMessage, +): CameraInfo { // Handle lowercase field names as well (ROS2 compatibility) const D = message.D ?? message.d; const K = message.K ?? message.k; @@ -444,7 +465,10 @@ function normalizeCameraInfo(message: PartialMessage): Camer const Plen = P?.length ?? 0; return { - header: normalizeHeader(message.header), + header: + "timestamp" in message + ? { stamp: normalizeTime(message.timestamp), frame_id: message.frame_id ?? "" } + : normalizeHeader(message.header), height: message.height ?? 0, width: message.width ?? 0, distortion_model: message.distortion_model ?? "", diff --git a/packages/studio-base/src/panels/ThreeDeeRender/renderables/Images.ts b/packages/studio-base/src/panels/ThreeDeeRender/renderables/Images.ts index 6ae292a521..814e0383d6 100644 --- a/packages/studio-base/src/panels/ThreeDeeRender/renderables/Images.ts +++ b/packages/studio-base/src/panels/ThreeDeeRender/renderables/Images.ts @@ -21,6 +21,7 @@ import { } from "@foxglove/den/image"; import Logger from "@foxglove/log"; import { toNanoSec } from "@foxglove/rostime"; +import { CameraCalibration, CompressedImage, RawImage } from "@foxglove/schemas/schemas/typescript"; import { SettingsTreeAction, SettingsTreeFields } from "@foxglove/studio"; import type { RosValue } from "@foxglove/studio-base/players/types"; import { MutablePoint } from "@foxglove/studio-base/types/Messages"; @@ -30,13 +31,18 @@ import type { Renderer } from "../Renderer"; import { PartialMessage, PartialMessageEvent, SceneExtension } from "../SceneExtension"; import { SettingsTreeEntry } from "../SettingsManager"; import { stringToRgba } from "../color"; -import { normalizeByteArray, normalizeHeader } from "../normalizeMessages"; import { - CameraInfo, - Image, - CompressedImage, - IMAGE_DATATYPES, + CAMERA_CALIBRATION_DATATYPES, COMPRESSED_IMAGE_DATATYPES, + RAW_IMAGE_DATATYPES, +} from "../foxglove"; +import { normalizeByteArray, normalizeHeader, normalizeTime } from "../normalizeMessages"; +import { + CameraInfo, + Image as RosImage, + CompressedImage as RosCompressedImage, + IMAGE_DATATYPES as ROS_IMAGE_DATATYPES, + COMPRESSED_IMAGE_DATATYPES as ROS_COMPRESSED_IMAGE_DATATYPES, CAMERA_INFO_DATATYPES, } from "../ros"; import { BaseSettings, PRECISION_DISTANCE, SelectEntry } from "../settings"; @@ -46,6 +52,8 @@ import { CameraInfoUserData } from "./Cameras"; const log = Logger.getLogger(__filename); void log; +type AnyImage = RosImage | RosCompressedImage | RawImage | CompressedImage; + export type LayerSettingsImage = BaseSettings & { cameraInfoTopic: string | undefined; distance: number; @@ -69,7 +77,7 @@ const DEFAULT_SETTINGS: LayerSettingsImage = { export type ImageUserData = BaseUserData & { topic: string; settings: LayerSettingsImage; - image: Image | CompressedImage; + image: AnyImage; texture: THREE.Texture | undefined; material: THREE.MeshBasicMaterial | undefined; geometry: THREE.PlaneGeometry | undefined; @@ -102,13 +110,23 @@ export class Images extends SceneExtension { public constructor(renderer: Renderer) { super("foxglove.Images", renderer); - renderer.addDatatypeSubscriptions(IMAGE_DATATYPES, this.handleRawImage); - renderer.addDatatypeSubscriptions(COMPRESSED_IMAGE_DATATYPES, this.handleCompressedImage); + renderer.addDatatypeSubscriptions(ROS_IMAGE_DATATYPES, this.handleRosRawImage); + renderer.addDatatypeSubscriptions( + ROS_COMPRESSED_IMAGE_DATATYPES, + this.handleRosCompressedImage, + ); // Unconditionally subscribe to CameraInfo messages so the `foxglove.Cameras` extension will // always receive them and parse into camera models. This extension reuses the parsed camera // models from `foxglove.Cameras` renderer.addDatatypeSubscriptions(CAMERA_INFO_DATATYPES, { - handler: this.handleCameraInfo, + handler: this.handleRosCameraInfo, + forced: true, + }); + + renderer.addDatatypeSubscriptions(RAW_IMAGE_DATATYPES, this.handleRawImage); + renderer.addDatatypeSubscriptions(COMPRESSED_IMAGE_DATATYPES, this.handleCompressedImage); + renderer.addDatatypeSubscriptions(CAMERA_CALIBRATION_DATATYPES, { + handler: this.handleCameraCalibration, forced: true, }); } @@ -118,7 +136,12 @@ export class Images extends SceneExtension { const handler = this.handleSettingsAction; const entries: SettingsTreeEntry[] = []; for (const topic of this.renderer.topics ?? []) { - if (IMAGE_DATATYPES.has(topic.datatype) || COMPRESSED_IMAGE_DATATYPES.has(topic.datatype)) { + if ( + ROS_IMAGE_DATATYPES.has(topic.datatype) || + ROS_COMPRESSED_IMAGE_DATATYPES.has(topic.datatype) || + RAW_IMAGE_DATATYPES.has(topic.datatype) || + COMPRESSED_IMAGE_DATATYPES.has(topic.datatype) + ) { const config = (configTopics[topic.name] ?? {}) as Partial; // Build a list of all matching CameraInfo topics @@ -175,18 +198,25 @@ export class Images extends SceneExtension { } }; - private handleRawImage = (messageEvent: PartialMessageEvent): void => { - this.handleImage(messageEvent, normalizeImage(messageEvent.message)); + private handleRosRawImage = (messageEvent: PartialMessageEvent): void => { + this.handleImage(messageEvent, normalizeRosImage(messageEvent.message)); + }; + + private handleRosCompressedImage = ( + messageEvent: PartialMessageEvent, + ): void => { + this.handleImage(messageEvent, normalizeRosCompressedImage(messageEvent.message)); + }; + + private handleRawImage = (messageEvent: PartialMessageEvent): void => { + this.handleImage(messageEvent, normalizeRawImage(messageEvent.message)); }; private handleCompressedImage = (messageEvent: PartialMessageEvent): void => { this.handleImage(messageEvent, normalizeCompressedImage(messageEvent.message)); }; - private handleImage = ( - messageEvent: PartialMessageEvent, - image: Image | CompressedImage, - ): void => { + private handleImage = (messageEvent: PartialMessageEvent, image: AnyImage): void => { const topic = messageEvent.topic; const receiveTime = toNanoSec(messageEvent.receiveTime); @@ -199,8 +229,10 @@ export class Images extends SceneExtension { if (!renderable) { renderable = new ImageRenderable(topic, this.renderer, { receiveTime, - messageTime: toNanoSec(image.header.stamp), - frameId: this.renderer.normalizeFrameId(image.header.frame_id), + messageTime: toNanoSec("header" in image ? image.header.stamp : image.timestamp), + frameId: this.renderer.normalizeFrameId( + "header" in image ? image.header.frame_id : image.frame_id, + ), pose: makePose(), settingsPath: ["topics", topic], topic, @@ -241,7 +273,25 @@ export class Images extends SceneExtension { this._updateImageRenderable(renderable, image, receiveTime, renderable.userData.settings); }; - private handleCameraInfo = (messageEvent: PartialMessageEvent): void => { + private handleRosCameraInfo = (messageEvent: PartialMessageEvent): void => { + const topic = messageEvent.topic; + const updated = !this.cameraInfoTopics.has(topic); + this.cameraInfoTopics.add(topic); + + const renderable = this.renderables.get(topic); + if (renderable) { + const { image, settings } = renderable.userData; + this._updateImageRenderable(renderable, image, renderable.userData.receiveTime, settings); + } + + if (updated) { + this.updateSettingsTree(); + } + }; + + private handleCameraCalibration = ( + messageEvent: PartialMessageEvent, + ): void => { const topic = messageEvent.topic; const updated = !this.cameraInfoTopics.has(topic); this.cameraInfoTopics.add(topic); @@ -259,7 +309,7 @@ export class Images extends SceneExtension { private _updateImageRenderable( renderable: ImageRenderable, - image: Image | CompressedImage, + image: AnyImage, receiveTime: bigint, settings: Partial | undefined, ): void { @@ -272,9 +322,13 @@ export class Images extends SceneExtension { const topic = renderable.userData.topic; renderable.userData.image = image; - renderable.userData.frameId = this.renderer.normalizeFrameId(image.header.frame_id); + renderable.userData.frameId = this.renderer.normalizeFrameId( + "header" in image ? image.header.frame_id : image.frame_id, + ); renderable.userData.receiveTime = receiveTime; - renderable.userData.messageTime = toNanoSec(image.header.stamp); + renderable.userData.messageTime = toNanoSec( + "header" in image ? image.header.stamp : image.timestamp, + ); renderable.userData.settings = newSettings; // Dispose of the current geometry if the settings have changed @@ -313,9 +367,8 @@ export class Images extends SceneExtension { } // Create or update the bitmap texture - if ((image as Partial).format) { - const compressed = image as CompressedImage; - const bitmapData = new Blob([image.data], { type: `image/${compressed.format}` }); + if ("format" in image) { + const bitmapData = new Blob([image.data], { type: `image/${image.format}` }); self .createImageBitmap(bitmapData, { resizeWidth: DEFAULT_IMAGE_WIDTH }) .then((bitmap) => { @@ -342,8 +395,7 @@ export class Images extends SceneExtension { ); }); } else { - const raw = image as Image; - const { width, height } = raw; + const { width, height } = image; const prevTexture = renderable.userData.texture as THREE.DataTexture | undefined; if ( prevTexture == undefined || @@ -360,7 +412,7 @@ export class Images extends SceneExtension { } const texture = renderable.userData.texture as THREE.DataTexture; - rawImageToDataTexture(raw, {}, texture); + rawImageToDataTexture(image, {}, texture); texture.needsUpdate = true; } @@ -551,11 +603,12 @@ function autoSelectCameraInfoTopic( } function rawImageToDataTexture( - image: Image, + image: RosImage | RawImage, options: RawImageOptions, output: THREE.DataTexture, ): void { - const { encoding, width, height, is_bigendian } = image; + const { encoding, width, height } = image; + const is_bigendian = "is_bigendian" in image ? image.is_bigendian : false; const rawData = image.data as Uint8Array; switch (encoding) { case "yuv422": @@ -602,6 +655,9 @@ function rawImageToDataTexture( } } +function normalizeImageData(data: Int8Array): Int8Array; +function normalizeImageData(data: PartialMessage | undefined): Uint8Array; +function normalizeImageData(data: unknown): Int8Array | Uint8Array; function normalizeImageData(data: unknown): Int8Array | Uint8Array { if (data == undefined) { return new Uint8Array(0); @@ -612,7 +668,7 @@ function normalizeImageData(data: unknown): Int8Array | Uint8Array { } } -function normalizeImage(message: PartialMessage): Image { +function normalizeRosImage(message: PartialMessage): RosImage { return { header: normalizeHeader(message.header), height: message.height ?? 0, @@ -624,10 +680,33 @@ function normalizeImage(message: PartialMessage): Image { }; } -function normalizeCompressedImage(message: PartialMessage): CompressedImage { +function normalizeRosCompressedImage( + message: PartialMessage, +): RosCompressedImage { return { header: normalizeHeader(message.header), format: message.format ?? "", data: normalizeByteArray(message.data), }; } + +function normalizeRawImage(message: PartialMessage): RawImage { + return { + timestamp: normalizeTime(message.timestamp), + frame_id: message.frame_id ?? "", + height: message.height ?? 0, + width: message.width ?? 0, + encoding: message.encoding ?? "", + step: message.step ?? 0, + data: normalizeImageData(message.data), + }; +} + +function normalizeCompressedImage(message: PartialMessage): CompressedImage { + return { + timestamp: normalizeTime(message.timestamp), + frame_id: message.frame_id ?? "", + format: message.format ?? "", + data: normalizeByteArray(message.data), + }; +} diff --git a/packages/studio-base/src/panels/ThreeDeeRender/renderables/PoseArrays.ts b/packages/studio-base/src/panels/ThreeDeeRender/renderables/PoseArrays.ts index a6fbce7a16..55bd6d060d 100644 --- a/packages/studio-base/src/panels/ThreeDeeRender/renderables/PoseArrays.ts +++ b/packages/studio-base/src/panels/ThreeDeeRender/renderables/PoseArrays.ts @@ -5,6 +5,7 @@ import * as THREE from "three"; import { toNanoSec } from "@foxglove/rostime"; +import { PosesInFrame } from "@foxglove/schemas/schemas/typescript"; import { SettingsTreeAction, SettingsTreeFields, Topic } from "@foxglove/studio"; import type { RosValue } from "@foxglove/studio-base/players/types"; @@ -13,8 +14,9 @@ import { Renderer } from "../Renderer"; import { PartialMessage, PartialMessageEvent, SceneExtension } from "../SceneExtension"; import { SettingsTreeEntry } from "../SettingsManager"; import { makeRgba, rgbaGradient, rgbaToCssString, stringToRgba } from "../color"; +import { POSES_IN_FRAME_DATATYPES } from "../foxglove"; import { vecEqual } from "../math"; -import { normalizeHeader, normalizePose } from "../normalizeMessages"; +import { normalizeHeader, normalizePose, normalizeTime } from "../normalizeMessages"; import { PoseArray, POSE_ARRAY_DATATYPES, @@ -93,6 +95,7 @@ export type PoseArrayUserData = BaseUserData & { settings: LayerSettingsPoseArray; topic: string; poseArrayMessage: PoseArray; + originalMessage: Record; axes: Axis[]; arrows: RenderableArrow[]; lineStrip?: RenderableLineStrip; @@ -107,7 +110,7 @@ export class PoseArrayRenderable extends Renderable { } public override details(): Record { - return this.userData.poseArrayMessage; + return this.userData.originalMessage; } public removeArrows(): void { @@ -140,6 +143,7 @@ export class PoseArrays extends SceneExtension { super("foxglove.PoseArrays", renderer); renderer.addDatatypeSubscriptions(POSE_ARRAY_DATATYPES, this.handlePoseArray); + renderer.addDatatypeSubscriptions(POSES_IN_FRAME_DATATYPES, this.handlePosesInFrame); renderer.addDatatypeSubscriptions(NAV_PATH_DATATYPES, this.handleNavPath); } @@ -148,7 +152,11 @@ export class PoseArrays extends SceneExtension { const handler = this.handleSettingsAction; const entries: SettingsTreeEntry[] = []; for (const topic of this.renderer.topics ?? []) { - if (POSE_ARRAY_DATATYPES.has(topic.datatype) || NAV_PATH_DATATYPES.has(topic.datatype)) { + if ( + POSE_ARRAY_DATATYPES.has(topic.datatype) || + NAV_PATH_DATATYPES.has(topic.datatype) || + POSES_IN_FRAME_DATATYPES.has(topic.datatype) + ) { const config = (configTopics[topic.name] ?? {}) as Partial; const displayType = config.type ?? getDefaultType(topic); const { axisScale, lineWidth } = config; @@ -208,6 +216,7 @@ export class PoseArrays extends SceneExtension { this._updatePoseArrayRenderable( renderable, renderable.userData.poseArrayMessage, + renderable.userData.originalMessage, renderable.userData.receiveTime, { ...DEFAULT_SETTINGS, ...settings }, ); @@ -217,7 +226,7 @@ export class PoseArrays extends SceneExtension { private handlePoseArray = (messageEvent: PartialMessageEvent): void => { const poseArrayMessage = normalizePoseArray(messageEvent.message); const receiveTime = toNanoSec(messageEvent.receiveTime); - this.addPoseArray(messageEvent.topic, poseArrayMessage, receiveTime); + this.addPoseArray(messageEvent.topic, poseArrayMessage, messageEvent.message, receiveTime); }; private handleNavPath = (messageEvent: PartialMessageEvent): void => { @@ -227,10 +236,21 @@ export class PoseArrays extends SceneExtension { const poseArrayMessage = normalizeNavPathToPoseArray(messageEvent.message); const receiveTime = toNanoSec(messageEvent.receiveTime); - this.addPoseArray(messageEvent.topic, poseArrayMessage, receiveTime); + this.addPoseArray(messageEvent.topic, poseArrayMessage, messageEvent.message, receiveTime); }; - private addPoseArray(topic: string, poseArrayMessage: PoseArray, receiveTime: bigint): void { + private handlePosesInFrame = (messageEvent: PartialMessageEvent): void => { + const poseArrayMessage = normalizePosesInFrameToPoseArray(messageEvent.message); + const receiveTime = toNanoSec(messageEvent.receiveTime); + this.addPoseArray(messageEvent.topic, poseArrayMessage, messageEvent.message, receiveTime); + }; + + private addPoseArray( + topic: string, + poseArrayMessage: PoseArray, + originalMessage: Record, + receiveTime: bigint, + ): void { let renderable = this.renderables.get(topic); if (!renderable) { // Set the initial settings from default values merged with any user settings @@ -249,6 +269,7 @@ export class PoseArrays extends SceneExtension { settings, topic, poseArrayMessage, + originalMessage, axes: [], arrows: [], }); @@ -260,6 +281,7 @@ export class PoseArrays extends SceneExtension { this._updatePoseArrayRenderable( renderable, poseArrayMessage, + originalMessage, receiveTime, renderable.userData.settings, ); @@ -338,6 +360,7 @@ export class PoseArrays extends SceneExtension { private _updatePoseArrayRenderable( renderable: PoseArrayRenderable, poseArrayMessage: PoseArray, + originalMessage: Record, receiveTime: bigint, settings: LayerSettingsPoseArray, ): void { @@ -345,6 +368,7 @@ export class PoseArrays extends SceneExtension { renderable.userData.messageTime = toNanoSec(poseArrayMessage.header.stamp); renderable.userData.frameId = this.renderer.normalizeFrameId(poseArrayMessage.header.frame_id); renderable.userData.poseArrayMessage = poseArrayMessage; + renderable.userData.originalMessage = originalMessage; const { topic, settings: prevSettings } = renderable.userData; const axisOrArrowSettingsChanged = @@ -485,6 +509,13 @@ function normalizeNavPathToPoseArray(navPath: PartialMessage): PoseArra }; } +function normalizePosesInFrameToPoseArray(poseArray: PartialMessage): PoseArray { + return { + header: { stamp: normalizeTime(poseArray.timestamp), frame_id: poseArray.frame_id ?? "" }, + poses: poseArray.poses?.map(normalizePose) ?? [], + }; +} + function validateNavPath(messageEvent: PartialMessageEvent, renderer: Renderer): boolean { const { topic, message: navPath } = messageEvent; if (navPath.poses) { diff --git a/packages/studio-base/src/panels/ThreeDeeRender/renderables/Poses.ts b/packages/studio-base/src/panels/ThreeDeeRender/renderables/Poses.ts index 06e42fe2fc..388e5185f9 100644 --- a/packages/studio-base/src/panels/ThreeDeeRender/renderables/Poses.ts +++ b/packages/studio-base/src/panels/ThreeDeeRender/renderables/Poses.ts @@ -5,6 +5,7 @@ import * as THREE from "three"; import { toNanoSec } from "@foxglove/rostime"; +import { PoseInFrame } from "@foxglove/schemas/schemas/typescript"; import { SettingsTreeAction, SettingsTreeFields } from "@foxglove/studio"; import type { RosValue } from "@foxglove/studio-base/players/types"; @@ -13,8 +14,14 @@ import { Renderer } from "../Renderer"; import { PartialMessage, PartialMessageEvent, SceneExtension } from "../SceneExtension"; import { SettingsTreeEntry } from "../SettingsManager"; import { makeRgba, rgbaToCssString, stringToRgba } from "../color"; +import { POSE_IN_FRAME_DATATYPES } from "../foxglove"; import { vecEqual } from "../math"; -import { normalizeHeader, normalizeMatrix6, normalizePose } from "../normalizeMessages"; +import { + normalizeHeader, + normalizeMatrix6, + normalizePose, + normalizeTime, +} from "../normalizeMessages"; import { Marker, PoseWithCovarianceStamped, @@ -73,6 +80,7 @@ export type PoseUserData = BaseUserData & { settings: LayerSettingsPose; topic: string; poseMessage: PoseStamped | PoseWithCovarianceStamped; + originalMessage: Record; axis?: Axis; arrow?: RenderableArrow; sphere?: RenderableSphere; @@ -87,7 +95,7 @@ export class PoseRenderable extends Renderable { } public override details(): Record { - return this.userData.poseMessage; + return this.userData.originalMessage; } } @@ -96,6 +104,7 @@ export class Poses extends SceneExtension { super("foxglove.Poses", renderer); renderer.addDatatypeSubscriptions(POSE_STAMPED_DATATYPES, this.handlePoseStamped); + renderer.addDatatypeSubscriptions(POSE_IN_FRAME_DATATYPES, this.handlePoseInFrame); renderer.addDatatypeSubscriptions( POSE_WITH_COVARIANCE_STAMPED_DATATYPES, this.handlePoseWithCovariance, @@ -108,10 +117,11 @@ export class Poses extends SceneExtension { const entries: SettingsTreeEntry[] = []; for (const topic of this.renderer.topics ?? []) { const isPoseStamped = POSE_STAMPED_DATATYPES.has(topic.datatype); + const isPoseInFrame = POSE_IN_FRAME_DATATYPES.has(topic.datatype); const isPoseWithCovarianceStamped = isPoseStamped ? false : POSE_WITH_COVARIANCE_STAMPED_DATATYPES.has(topic.datatype); - if (isPoseStamped || isPoseWithCovarianceStamped) { + if (isPoseStamped || isPoseWithCovarianceStamped || isPoseInFrame) { const config = (configTopics[topic.name] ?? {}) as Partial; const type = config.type ?? DEFAULT_TYPE; @@ -195,6 +205,7 @@ export class Poses extends SceneExtension { this._updatePoseRenderable( renderable, renderable.userData.poseMessage, + renderable.userData.originalMessage, renderable.userData.receiveTime, { ...DEFAULT_SETTINGS, ...settings }, ); @@ -204,7 +215,13 @@ export class Poses extends SceneExtension { private handlePoseStamped = (messageEvent: PartialMessageEvent): void => { const poseMessage = normalizePoseStamped(messageEvent.message); const receiveTime = toNanoSec(messageEvent.receiveTime); - this.addPose(messageEvent.topic, poseMessage, receiveTime); + this.addPose(messageEvent.topic, poseMessage, messageEvent.message, receiveTime); + }; + + private handlePoseInFrame = (messageEvent: PartialMessageEvent): void => { + const poseMessage = normalizePoseInFrameToPoseStamped(messageEvent.message); + const receiveTime = toNanoSec(messageEvent.receiveTime); + this.addPose(messageEvent.topic, poseMessage, messageEvent.message, receiveTime); }; private handlePoseWithCovariance = ( @@ -212,12 +229,13 @@ export class Poses extends SceneExtension { ): void => { const poseMessage = normalizePoseWithCovarianceStamped(messageEvent.message); const receiveTime = toNanoSec(messageEvent.receiveTime); - this.addPose(messageEvent.topic, poseMessage, receiveTime); + this.addPose(messageEvent.topic, poseMessage, messageEvent.message, receiveTime); }; private addPose( topic: string, poseMessage: PoseStamped | PoseWithCovarianceStamped, + originalMessage: Record, receiveTime: bigint, ): void { let renderable = this.renderables.get(topic); @@ -237,6 +255,7 @@ export class Poses extends SceneExtension { settings, topic, poseMessage, + originalMessage, axis: undefined, arrow: undefined, sphere: undefined, @@ -246,12 +265,19 @@ export class Poses extends SceneExtension { this.renderables.set(topic, renderable); } - this._updatePoseRenderable(renderable, poseMessage, receiveTime, renderable.userData.settings); + this._updatePoseRenderable( + renderable, + poseMessage, + originalMessage, + receiveTime, + renderable.userData.settings, + ); } private _updatePoseRenderable( renderable: PoseRenderable, poseMessage: PoseStamped | PoseWithCovarianceStamped, + originalMessage: Record, receiveTime: bigint, settings: LayerSettingsPose, ): void { @@ -259,6 +285,7 @@ export class Poses extends SceneExtension { renderable.userData.messageTime = toNanoSec(poseMessage.header.stamp); renderable.userData.frameId = this.renderer.normalizeFrameId(poseMessage.header.frame_id); renderable.userData.poseMessage = poseMessage; + renderable.userData.originalMessage = originalMessage; // Default the covariance sphere to hidden. If showCovariance is set and a valid covariance // matrix is present, it will be shown @@ -403,6 +430,13 @@ export function normalizePoseStamped(pose: PartialMessage): PoseSta }; } +function normalizePoseInFrameToPoseStamped(pose: PartialMessage): PoseStamped { + return { + header: { stamp: normalizeTime(pose.timestamp), frame_id: pose.frame_id ?? "" }, + pose: normalizePose(pose.pose), + }; +} + function normalizePoseWithCovariance( pose: PartialMessage | undefined, ): PoseWithCovariance { diff --git a/packages/studio-base/src/panels/ThreeDeeRender/stories/GeometryMsgs_PoseArray.stories.tsx b/packages/studio-base/src/panels/ThreeDeeRender/stories/GeometryMsgs_PoseArray.stories.tsx index dd2896ba57..75c1c4ee2a 100644 --- a/packages/studio-base/src/panels/ThreeDeeRender/stories/GeometryMsgs_PoseArray.stories.tsx +++ b/packages/studio-base/src/panels/ThreeDeeRender/stories/GeometryMsgs_PoseArray.stories.tsx @@ -21,7 +21,7 @@ type Vec4 = [number, number, number, number]; const vec4ToOrientation = ([x, y, z, w]: Vec4) => ({ x, y, z, w }); GeometryMsgs_PoseArray.parameters = { colorScheme: "dark" }; -function GeometryMsgs_PoseArray(): JSX.Element { +export function GeometryMsgs_PoseArray(): JSX.Element { const topics: Topic[] = [ { name: "/baselink_path", datatype: "geometry_msgs/PoseArray" }, { name: "/sensor_path", datatype: "geometry_msgs/PoseArray" }, diff --git a/packages/studio-base/src/panels/ThreeDeeRender/stories/ImageRender.stories.tsx b/packages/studio-base/src/panels/ThreeDeeRender/stories/ImageRender.stories.tsx index 9fd9dd88c1..24b21bb65e 100644 --- a/packages/studio-base/src/panels/ThreeDeeRender/stories/ImageRender.stories.tsx +++ b/packages/studio-base/src/panels/ThreeDeeRender/stories/ImageRender.stories.tsx @@ -2,11 +2,12 @@ // License, v2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/ +import { CompressedImage, FrameTransform, RawImage } from "@foxglove/schemas/schemas/typescript"; import { MessageEvent, Topic } from "@foxglove/studio"; import PanelSetup from "@foxglove/studio-base/stories/PanelSetup"; import ThreeDeeRender from "../index"; -import { CameraInfo, CompressedImage, Image, TransformStamped } from "../ros"; +import { CameraInfo, CompressedImage as RosCompressedImage, Image, TransformStamped } from "../ros"; import { BASE_LINK_FRAME_ID, FIXED_FRAME_ID, @@ -102,7 +103,7 @@ export function ImageRender(): JSX.Element { sizeInBytes: 0, }; - const cam1Png: MessageEvent> = { + const cam1Png: MessageEvent> = { topic: "/cam1/png", receiveTime: { sec: 10, nsec: 0 }, message: { @@ -204,3 +205,185 @@ export function ImageRender(): JSX.Element { ); } + +FoxgloveImageRender.parameters = { colorScheme: "light" }; +export function FoxgloveImageRender(): JSX.Element { + const topics: Topic[] = [ + { name: "/tf", datatype: "foxglove.FrameTransform" }, + { name: "/cam1/info", datatype: "foxglove.CameraCalibration" }, + { name: "/cam2/info", datatype: "foxglove.CameraCalibration" }, + { name: "/cam1/png", datatype: "foxglove.CompressedImage" }, + { name: "/cam2/raw", datatype: "foxglove.RawImage" }, + ]; + + const tf1: MessageEvent = { + topic: "/tf", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + parent_frame_id: FIXED_FRAME_ID, + child_frame_id: BASE_LINK_FRAME_ID, + translation: { x: 1e7, y: 0, z: 0 }, + rotation: QUAT_IDENTITY, + }, + sizeInBytes: 0, + }; + const tf2: MessageEvent = { + topic: "/tf", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + parent_frame_id: BASE_LINK_FRAME_ID, + child_frame_id: SENSOR_FRAME_ID, + translation: { x: 0, y: 0, z: 1 }, + rotation: { x: 0.383, y: 0, z: 0, w: 0.924 }, + }, + sizeInBytes: 0, + }; + + const cam1: MessageEvent> = { + topic: "/cam1/info", + receiveTime: { sec: 10, nsec: 0 }, + message: { + header: { seq: 0, stamp: { sec: 0, nsec: 0 }, frame_id: SENSOR_FRAME_ID }, + height: 480, + width: 640, + distortion_model: "rational_polynomial", + D: [0.452407, 0.273748, -0.00011, 0.000152, 0.027904, 0.817958, 0.358389, 0.108657], + K: [ + 381.22076416015625, 0, 318.88323974609375, 0, 381.22076416015625, 233.90321350097656, 0, 0, + 1, + ], + R: [1, 0, 0, 1, 0, 0, 1, 0, 0], + P: [ + 381.22076416015625, 0, 318.88323974609375, 0.015031411312520504, 0, 381.22076416015625, + 233.90321350097656, -0.00011014656047336757, 0, 0, 1, 0.000024338871298823506, + ], + }, + sizeInBytes: 0, + }; + + const cam2: MessageEvent> = { + topic: "/cam2/info", + receiveTime: { sec: 10, nsec: 0 }, + message: { + header: { seq: 0, stamp: { sec: 0, nsec: 0 }, frame_id: SENSOR_FRAME_ID }, + height: 900, + width: 1600, + distortion_model: "", + D: [], + K: [ + 1266.417203046554, 0, 816.2670197447984, 0, 1266.417203046554, 491.50706579294757, 0, 0, 1, + ], + R: [1, 0, 0, 1, 0, 0, 1, 0, 0], + P: [ + 1266.417203046554, 0, 816.2670197447984, 0, 0, 1266.417203046554, 491.50706579294757, 0, 0, + 0, 1, 0, + ], + }, + sizeInBytes: 0, + }; + + const cam1Png: MessageEvent> = { + topic: "/cam1/png", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + frame_id: SENSOR_FRAME_ID, + format: "png", + data: PNG_TEST_IMAGE, + }, + sizeInBytes: 0, + }; + + // Create a Uint8Array 8x8 RGBA image + const SIZE = 8; + const rgba8 = new Uint8Array(SIZE * SIZE * 4); + for (let y = 0; y < SIZE; y++) { + for (let x = 0; x < SIZE; x++) { + const i = (y * SIZE + x) * 4; + rgba8[i + 0] = Math.trunc((x / (SIZE - 1)) * 255); + rgba8[i + 1] = Math.trunc((y / (SIZE - 1)) * 255); + rgba8[i + 2] = 0; + rgba8[i + 3] = 255; + } + } + + const cam2Raw: MessageEvent> = { + topic: "/cam2/raw", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + frame_id: SENSOR_FRAME_ID, + height: SIZE, + width: SIZE, + encoding: "rgba8", + step: SIZE * 4, + data: rgba8, + }, + sizeInBytes: 0, + }; + + const fixture = useDelayedFixture({ + topics, + frame: { + "/tf": [tf1, tf2], + "/cam1/info": [cam1], + "/cam2/info": [cam2], + "/cam1/png": [cam1Png], + "/cam2/raw": [cam2Raw], + }, + capabilities: [], + activeData: { + currentTime: { sec: 0, nsec: 0 }, + }, + }); + + return ( + + + + ); +} diff --git a/packages/studio-base/src/panels/ThreeDeeRender/stories/foxglove.PoseInFrame.stories.tsx b/packages/studio-base/src/panels/ThreeDeeRender/stories/foxglove.PoseInFrame.stories.tsx new file mode 100644 index 0000000000..4116319cc5 --- /dev/null +++ b/packages/studio-base/src/panels/ThreeDeeRender/stories/foxglove.PoseInFrame.stories.tsx @@ -0,0 +1,158 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import { quat } from "gl-matrix"; + +import { FrameTransform, PoseInFrame } from "@foxglove/schemas/schemas/typescript"; +import { MessageEvent, Topic } from "@foxglove/studio"; +import PanelSetup from "@foxglove/studio-base/stories/PanelSetup"; + +import ThreeDeeRender from "../index"; +import { QUAT_IDENTITY, rad2deg } from "./common"; +import useDelayedFixture from "./useDelayedFixture"; + +export default { + title: "panels/ThreeDeeRender", + component: ThreeDeeRender, +}; + +type Vec4 = [number, number, number, number]; + +const vec4ToOrientation = ([x, y, z, w]: Vec4) => ({ x, y, z, w }); + +Foxglove_PoseInFrame.parameters = { colorScheme: "dark" }; +export function Foxglove_PoseInFrame(): JSX.Element { + const topics: Topic[] = [ + { name: "/tf", datatype: "foxglove.FrameTransform" }, + { name: "/pose1", datatype: "foxglove.PoseInFrame" }, + { name: "/pose2", datatype: "foxglove.PoseInFrame" }, + { name: "/pose3", datatype: "foxglove.PoseInFrame" }, + ]; + + const tf1: MessageEvent = { + topic: "/tf", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + parent_frame_id: "map", + child_frame_id: "base_link", + translation: { x: 1e7, y: 0, z: 0 }, + rotation: QUAT_IDENTITY, + }, + sizeInBytes: 0, + }; + const tf2: MessageEvent = { + topic: "/tf", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + parent_frame_id: "base_link", + child_frame_id: "sensor", + translation: { x: 0, y: -5, z: 0 }, + rotation: QUAT_IDENTITY, + }, + sizeInBytes: 0, + }; + + const pose1: MessageEvent = { + topic: "/pose1", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + frame_id: "base_link", + pose: { + position: { x: 2, y: 0, z: 0 }, + orientation: QUAT_IDENTITY, + }, + }, + sizeInBytes: 0, + }; + + const pose2: MessageEvent = { + topic: "/pose2", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + frame_id: "sensor", + pose: { + position: { x: 0, y: 3, z: 0 }, + orientation: vec4ToOrientation( + quat.rotateZ(quat.create(), quat.create(), Math.PI / 2) as Vec4, + ), + }, + }, + sizeInBytes: 0, + }; + + const pose3: MessageEvent = { + topic: "/pose3", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + frame_id: "base_link", + pose: { + position: { x: 0, y: 2, z: 0 }, + orientation: vec4ToOrientation( + quat.rotateZ(quat.create(), quat.create(), Math.PI / 4) as Vec4, + ), + }, + }, + sizeInBytes: 0, + }; + + const fixture = useDelayedFixture({ + topics, + frame: { + "/tf": [tf1, tf2], + "/pose1": [pose1], + "/pose2": [pose2], + "/pose3": [pose3], + }, + capabilities: [], + activeData: { + currentTime: { sec: 0, nsec: 0 }, + }, + }); + + return ( + + + + ); +} diff --git a/packages/studio-base/src/panels/ThreeDeeRender/stories/foxglove.PosesInFrame.stories.tsx b/packages/studio-base/src/panels/ThreeDeeRender/stories/foxglove.PosesInFrame.stories.tsx new file mode 100644 index 0000000000..737b74463b --- /dev/null +++ b/packages/studio-base/src/panels/ThreeDeeRender/stories/foxglove.PosesInFrame.stories.tsx @@ -0,0 +1,168 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import { quat } from "gl-matrix"; + +import { FrameTransform, PosesInFrame } from "@foxglove/schemas/schemas/typescript"; +import { MessageEvent, Topic } from "@foxglove/studio"; +import PanelSetup from "@foxglove/studio-base/stories/PanelSetup"; + +import ThreeDeeRender from "../index"; +import { QUAT_IDENTITY, rad2deg } from "./common"; +import useDelayedFixture from "./useDelayedFixture"; + +export default { + title: "panels/ThreeDeeRender", + component: ThreeDeeRender, +}; + +type Vec4 = [number, number, number, number]; +const vec4ToOrientation = ([x, y, z, w]: Vec4) => ({ x, y, z, w }); + +Foxglove_PosesInFrame.parameters = { colorScheme: "dark" }; +export function Foxglove_PosesInFrame(): JSX.Element { + const topics: Topic[] = [ + { name: "/baselink_path", datatype: "foxglove.PosesInFrame" }, + { name: "/sensor_path", datatype: "foxglove.PosesInFrame" }, + { name: "/sensor_path2", datatype: "foxglove.PosesInFrame" }, + { name: "/tf", datatype: "foxglove.FrameTransform" }, + ]; + const tf1: MessageEvent = { + topic: "/tf", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + parent_frame_id: "map", + child_frame_id: "base_link", + translation: { x: 1e7, y: 0, z: 0 }, + rotation: QUAT_IDENTITY, + }, + sizeInBytes: 0, + }; + const tf2: MessageEvent = { + topic: "/tf", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + parent_frame_id: "base_link", + child_frame_id: "sensor", + translation: { x: 0, y: 0, z: 1 }, + rotation: vec4ToOrientation(quat.rotateZ(quat.create(), quat.create(), Math.PI / 2) as Vec4), + }, + sizeInBytes: 0, + }; + const tf3: MessageEvent = { + topic: "/tf", + receiveTime: { sec: 10, nsec: 0 }, + message: { + timestamp: { sec: 10, nsec: 0 }, + parent_frame_id: "base_link", + child_frame_id: "sensor", + translation: { x: 0, y: 5, z: 1 }, + rotation: QUAT_IDENTITY, + }, + sizeInBytes: 0, + }; + + const q = (): quat => [0, 0, 0, 1]; + const identity = q(); + const makeOrientation = (i: number) => { + const o = quat.rotateZ(q(), identity, (Math.PI / 2) * (i / 9)); + return { x: o[0], y: o[1], z: o[2], w: o[3] }; + }; + + const baseLinkPath: MessageEvent = { + topic: "/baselink_path", + receiveTime: { sec: 3, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + frame_id: "base_link", + poses: [...Array(10)].map((_, i) => ({ + position: { x: 3, y: i / 4, z: 1 }, + orientation: makeOrientation(i), + })), + }, + sizeInBytes: 0, + }; + + const sensorPath: MessageEvent = { + topic: "/sensor_path", + receiveTime: { sec: 3, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + frame_id: "sensor", + poses: [...Array(10)].map((_, i) => ({ + position: { x: 2, y: i / 4, z: 0 }, + orientation: makeOrientation(i), + })), + }, + sizeInBytes: 0, + }; + + const sensorPath2: MessageEvent = { + topic: "/sensor_path2", + receiveTime: { sec: 3, nsec: 0 }, + message: { + timestamp: { sec: 0, nsec: 0 }, + frame_id: "sensor", + poses: [...Array(10)].map((_, i) => ({ + position: { x: -i / 4, y: 2, z: 0 }, + orientation: makeOrientation(i), + })), + }, + sizeInBytes: 0, + }; + + const fixture = useDelayedFixture({ + topics, + frame: { + "/baselink_path": [baseLinkPath], + "/sensor_path": [sensorPath], + "/sensor_path2": [sensorPath2], + "/tf": [tf1, tf2, tf3], + }, + capabilities: [], + activeData: { + currentTime: { sec: 3, nsec: 0 }, + }, + }); + + return ( + + + + ); +} diff --git a/packages/studio-base/src/players/types.ts b/packages/studio-base/src/players/types.ts index 33d83ef453..773b98c1e4 100644 --- a/packages/studio-base/src/players/types.ts +++ b/packages/studio-base/src/players/types.ts @@ -226,12 +226,11 @@ type RosTypedArray = | Float32Array | Float64Array; -type RosSingularField = number | string | boolean | RosObject; // No time -- consider it a message. +type RosSingularField = number | string | boolean | RosObject | undefined; // No time -- consider it a message. export type RosValue = | RosSingularField | readonly RosSingularField[] | RosTypedArray - | undefined // eslint-disable-next-line no-restricted-syntax | null; diff --git a/packages/studio-base/src/screens/LaunchingInDesktopScreen.tsx b/packages/studio-base/src/screens/LaunchingInDesktopScreen.tsx index f09371a118..6ab253cf7e 100644 --- a/packages/studio-base/src/screens/LaunchingInDesktopScreen.tsx +++ b/packages/studio-base/src/screens/LaunchingInDesktopScreen.tsx @@ -36,6 +36,9 @@ export function LaunchingInDesktopScreen(): ReactElement { case "ds.deviceId": desktopURL.searchParams.set("deviceId", v); break; + case "ds.importId": + desktopURL.searchParams.set("importId", v); + break; case "ds.end": desktopURL.searchParams.set("end", v); break; diff --git a/packages/studio-base/src/util/appURLState.test.ts b/packages/studio-base/src/util/appURLState.test.ts index d26ddafcbc..9ef025aa7a 100644 --- a/packages/studio-base/src/util/appURLState.test.ts +++ b/packages/studio-base/src/util/appURLState.test.ts @@ -58,6 +58,7 @@ describe("app state url parser", () => { url.searchParams.append("ds", "foxglove-data-platform"); url.searchParams.append("layoutId", "1234"); url.searchParams.append("time", time); + url.searchParams.append("ds.importId", "dummyImportId"); url.searchParams.append("ds.deviceId", "dummy"); url.searchParams.append("ds.start", start); url.searchParams.append("ds.end", end); @@ -70,6 +71,7 @@ describe("app state url parser", () => { time: { sec: now.sec + 500, nsec: 0 }, dsParams: { deviceId: "dummy", + importId: "dummyImportId", start, end, eventId: "dummyEventId",