diff --git a/packages/studio-base/src/panels/ThreeDeeRender/foxglove.ts b/packages/studio-base/src/panels/ThreeDeeRender/foxglove.ts index 23b338841a..8f9b280db9 100644 --- a/packages/studio-base/src/panels/ThreeDeeRender/foxglove.ts +++ b/packages/studio-base/src/panels/ThreeDeeRender/foxglove.ts @@ -11,6 +11,12 @@ addFoxgloveSchema(POINTCLOUD_DATATYPES, "foxglove.PointCloud"); 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/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/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;