Skip to content

Commit 7eb3509

Browse files
Jason717717Averyyy
authored andcommitted
feat: add support for selecting topics
1 parent 93094b9 commit 7eb3509

5 files changed

Lines changed: 167 additions & 59 deletions

File tree

GEMstack/onboard/visualization/sr_viz/threeD/src/components/PanelManager.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function createPanel(type: "video" | "pointcloud" | "text"): PanelNode {
3838
export const PanelManager = ({
3939
messageMap,
4040
}: {
41-
messageMap: Record<string, any[]>;
41+
messageMap: Record<string, Record<string, any[]> | any[]>;
4242
}) => {
4343
const [rootPanel, setRootPanel] = useState<PanelNode>(createPanel("video"));
4444

@@ -94,12 +94,13 @@ export const PanelManager = ({
9494
rootPanel={rootPanel}
9595
/>
9696
{node.type === "video" && (
97-
<VideoPanel messages={messageMap["video"]} />
97+
<VideoPanel messages={messageMap["video"]} initialTopic={Object.keys(messageMap["video"])[0]} />
9898
)}
9999
{node.type === "pointcloud" && (
100100
<PointCloudPanel
101101
messages={messageMap["pointcloud"]}
102102
tfMessages={messageMap["tf"]}
103+
initialTopic={Object.keys(messageMap["pointcloud"])[0]}
103104
/>
104105
)}
105106
</Panel>
@@ -108,9 +109,6 @@ export const PanelManager = ({
108109

109110
return (
110111
<div className="fixed top-0 left-0 w-full h-full">
111-
{/* <div className="fixed top-5 left-55 flex gap-5 mb-2 z-10">
112-
<Button onClick={() => console.log(rootPanel)}>Print</Button>
113-
</div> */}
114112
<PanelGroup direction="horizontal">
115113
{renderPanelNode(rootPanel)}
116114
</PanelGroup>

GEMstack/onboard/visualization/sr_viz/threeD/src/components/PointCloudPanel.tsx

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"use client";
22

3-
import React, { useEffect, useRef } from "react";
3+
import React, { useEffect, useRef, useState } from "react";
44
import { useScrubber } from "./ScrubberContext";
55
import * as THREE from "three";
66
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
7+
import { Select, SelectChangeEvent, MenuItem } from "@mui/material";
78

89
function parsePointCloud2(msg: any): THREE.Points {
910
const { data, point_step, fields } = msg;
@@ -25,7 +26,13 @@ function parsePointCloud2(msg: any): THREE.Points {
2526

2627
positions.push(x, y, z);
2728

28-
if ("rgb" in fieldMap) {
29+
if ("rgba" in fieldMap) {
30+
const rgba = dv.getUint32(i + fieldMap["rgba"], true);
31+
const r = (rgba >> 24) & 0xff;
32+
const g = (rgba >> 16) & 0xff;
33+
const b = (rgba >> 8) & 0xff;
34+
colors.push(r / 255, g / 255, b / 255);
35+
} else if ("rgb" in fieldMap) {
2936
const rgb = dv.getUint32(i + fieldMap["rgb"], true);
3037
const r = (rgb >> 16) & 0xff;
3138
const g = (rgb >> 8) & 0xff;
@@ -100,9 +107,11 @@ function getTransformMatrix(
100107
export const PointCloudPanel = ({
101108
messages,
102109
tfMessages,
110+
initialTopic,
103111
}: {
104-
messages: any[];
112+
messages: Record<string, any[]>;
105113
tfMessages: any[];
114+
initialTopic?: string;
106115
}) => {
107116
const mountRef = useRef<HTMLDivElement>(null);
108117
const { startTime, currentTime } = useScrubber();
@@ -111,8 +120,18 @@ export const PointCloudPanel = ({
111120
const sceneRef = useRef<THREE.Scene>();
112121
const controlsRef = useRef<OrbitControls>();
113122
const pointCloudRef = useRef<THREE.Points>();
123+
const [selectedTopic, setSelectedTopic] = useState(
124+
initialTopic || Object.keys(messages)[0] || ""
125+
);
126+
127+
useEffect(() => {
128+
if (initialTopic && messages[initialTopic]) {
129+
setSelectedTopic(initialTopic);
130+
}
131+
}, [initialTopic]);
114132

115133
useEffect(() => {
134+
if (!selectedTopic || !messages[selectedTopic]) return;
116135
if (!mountRef.current) return;
117136

118137
const scene = new THREE.Scene();
@@ -159,10 +178,8 @@ export const PointCloudPanel = ({
159178
const scene = sceneRef.current;
160179
if (!scene) return;
161180

162-
const msg = messages.find(
163-
(m) =>
164-
m.timestamp >= startTime + currentTime &&
165-
m.data.header.frame_id === "velodyne"
181+
const msg = messages[selectedTopic].find(
182+
(m) => m.timestamp >= startTime + currentTime
166183
);
167184
if (!msg) return;
168185

@@ -208,5 +225,32 @@ export const PointCloudPanel = ({
208225
return () => observer.disconnect();
209226
}, []);
210227

211-
return <div ref={mountRef} className="w-full h-full" />;
228+
return (
229+
<div ref={mountRef} className="w-full h-full">
230+
<div className="absolute top-2 right-20">
231+
<Select
232+
value={selectedTopic}
233+
onChange={(e: SelectChangeEvent<string>) => {
234+
setSelectedTopic(e.target.value);
235+
}}
236+
size="small"
237+
disabled={Object.keys(messages).length === 0}
238+
sx={{
239+
backgroundColor: "white",
240+
opacity: 0.25,
241+
borderRadius: "9999px",
242+
"&:hover": {
243+
opacity: 0.5,
244+
},
245+
}}
246+
>
247+
{Object.keys(messages).map((topic) => (
248+
<MenuItem key={topic} value={topic}>
249+
{topic}
250+
</MenuItem>
251+
))}
252+
</Select>
253+
</div>
254+
</div>
255+
);
212256
};

GEMstack/onboard/visualization/sr_viz/threeD/src/components/RosbagViewer.tsx

Lines changed: 58 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ export default function RosbagViewer() {
3636
const [topics, setTopics] = useState<string[]>([]);
3737
const [types, setTypes] = useState<string[]>([]);
3838
const [loading, setLoading] = useState(false);
39-
const [messageMap, setMessageMap] = useState<Record<string, any[]>>({
40-
video: [],
41-
pointcloud: [],
39+
const [messageMap, setMessageMap] = useState<{
40+
video: Record<string, any[]>;
41+
pointcloud: Record<string, any[]>;
42+
tf: any[];
43+
}>({
44+
video: {},
45+
pointcloud: {},
46+
tf: [],
4247
});
4348

4449
const [duration, setDuration] = useState(0);
@@ -59,7 +64,7 @@ export default function RosbagViewer() {
5964
const reader = fileToReader(file);
6065
const bag = new Bag(reader);
6166
await bag.open();
62-
console.log("Bag opened:", bag);
67+
// console.log("Bag opened:", bag);
6368
const topicNames = Array.from(bag.connections.values()).map(
6469
(conn) => conn.topic
6570
);
@@ -68,63 +73,74 @@ export default function RosbagViewer() {
6873
);
6974
setTopics(topicNames);
7075
setTypes(topicTypes);
71-
console.log("Topics:", topicNames, topicTypes);
76+
// console.log("Topics:", topicNames, topicTypes);
7277

73-
const videoMessages: any[] = [];
74-
const pointcloudMessages: any[] = [];
78+
const videoMessages: Record<string, any[]> = {};
79+
const pointcloudMessages: Record<string, any[]> = {};
7580
const tfMessages: any[] = [];
76-
const velodyneScanMessages: any[] = [];
7781
for await (const msg of bag.messageIterator()) {
7882
const entry = {
7983
timestamp: msg.timestamp.sec + msg.timestamp.nsec * 1e-9,
8084
topic: msg.topic,
8185
data: msg.message,
8286
};
8387
const type = topicTypes[topicNames.indexOf(msg.topic)];
84-
if (type.includes("Image")) videoMessages.push(entry);
85-
if (type.includes("PointCloud2")) pointcloudMessages.push(entry);
88+
if (type.includes("Image")) {
89+
if (!videoMessages[msg.topic]) videoMessages[msg.topic] = [];
90+
videoMessages[msg.topic].push(entry);
91+
}
92+
if (type.includes("PointCloud2")) {
93+
if (!pointcloudMessages[msg.topic])
94+
pointcloudMessages[msg.topic] = [];
95+
pointcloudMessages[msg.topic].push(entry);
96+
}
8697
if (type.includes("TFMessage")) tfMessages.push(entry);
87-
if (type.includes("VelodyneScan")) velodyneScanMessages.push(entry);
8898
}
8999
setLoading(false);
90-
console.log(
91-
"Messages parsed:",
92-
videoMessages,
93-
pointcloudMessages,
94-
tfMessages,
95-
velodyneScanMessages
96-
);
100+
// console.log(
101+
// "Messages parsed:",
102+
// videoMessages,
103+
// pointcloudMessages,
104+
// tfMessages
105+
// );
97106
setMessageMap({
98107
video: videoMessages,
99108
pointcloud: pointcloudMessages,
100109
tf: tfMessages,
101110
});
102-
if (videoMessages.length > 0) {
103-
setDuration(
104-
Math.max(
105-
videoMessages[videoMessages.length - 1].timestamp -
106-
videoMessages[0].timestamp,
107-
0
108-
)
109-
);
110-
}
111-
if (pointcloudMessages.length > 0) {
112-
setDuration((prev) =>
113-
Math.max(
114-
prev,
115-
pointcloudMessages[pointcloudMessages.length - 1]
116-
.timestamp - pointcloudMessages[0].timestamp,
117-
0
118-
)
119-
);
120-
}
121-
const videoStart = videoMessages[0]?.timestamp || 0;
122-
const pointcloudStart = pointcloudMessages[0]?.timestamp || 0;
123-
const start =
111+
const getDuration = (
112+
messagesByTopic: Record<string, any[]>
113+
): number => {
114+
const durations = Object.values(messagesByTopic)
115+
.filter((msgs) => msgs.length > 1)
116+
.map(
117+
(msgs) =>
118+
msgs[msgs.length - 1].timestamp - msgs[0].timestamp
119+
);
120+
return durations.length > 0 ? Math.max(...durations) : 0;
121+
};
122+
123+
const getStart = (messagesByTopic: Record<string, any[]>): number => {
124+
const starts = Object.values(messagesByTopic)
125+
.filter((msgs) => msgs.length > 0)
126+
.map((msgs) => msgs[0].timestamp);
127+
return starts.length > 0 ? Math.min(...starts) : 0;
128+
};
129+
130+
setDuration(
131+
Math.max(
132+
getDuration(videoMessages),
133+
getDuration(pointcloudMessages),
134+
0
135+
)
136+
);
137+
const videoStart = getStart(videoMessages);
138+
const pointcloudStart = getStart(pointcloudMessages);
139+
setStartTime(
124140
videoStart > 0 && pointcloudStart > 0
125141
? Math.min(videoStart, pointcloudStart)
126-
: Math.max(videoStart, pointcloudStart);
127-
setStartTime(start);
142+
: Math.max(videoStart, pointcloudStart)
143+
);
128144
};
129145

130146
return (

GEMstack/onboard/visualization/sr_viz/threeD/src/components/Scrubber2.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ export const Scrubber2 = ({
8484
}
8585
}, [currentTime, duration]);
8686

87+
useEffect(() => {
88+
if (loading) {
89+
setIsPlaying(false);
90+
}
91+
}, [loading]);
92+
8793
return (
8894
<div
8995
className="px-5 fixed bottom-5 left-1/6 h-20 w-2/3 bg-black/40 text-white shadow-lg rounded-full flex justify-center items-center"

GEMstack/onboard/visualization/sr_viz/threeD/src/components/VideoPanel.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
import React, { useEffect, useRef, useState } from "react";
44
import { useScrubber } from "./ScrubberContext";
5-
import { Switch, FormControlLabel } from "@mui/material";
5+
import {
6+
Switch,
7+
FormControlLabel,
8+
Select,
9+
SelectChangeEvent,
10+
MenuItem,
11+
} from "@mui/material";
612

713
export function decodeImage(msg: {
814
encoding: string;
@@ -118,10 +124,14 @@ export function decodeImage(msg: {
118124
}
119125

120126
type VideoPanelProps = {
121-
messages: any[];
127+
messages: Record<string, any[]>;
128+
initialTopic?: string;
122129
};
123130

124-
export const VideoPanel: React.FC<VideoPanelProps> = ({ messages }) => {
131+
export const VideoPanel: React.FC<VideoPanelProps> = ({
132+
messages,
133+
initialTopic,
134+
}) => {
125135
const { startTime, currentTime } = useScrubber();
126136
const canvasRef = useRef<HTMLCanvasElement>(null);
127137
const containerRef = useRef<HTMLDivElement>(null);
@@ -132,9 +142,19 @@ export const VideoPanel: React.FC<VideoPanelProps> = ({ messages }) => {
132142
const [offset, setOffset] = useState({ x: 0, y: 0 });
133143
const [isDragging, setIsDragging] = useState(false);
134144
const lastMouse = useRef({ x: 0, y: 0 });
145+
const [selectedTopic, setSelectedTopic] = useState(
146+
initialTopic || Object.keys(messages)[0] || ""
147+
);
148+
149+
useEffect(() => {
150+
if (initialTopic && messages[initialTopic]) {
151+
setSelectedTopic(initialTopic);
152+
}
153+
}, [initialTopic]);
135154

136155
useEffect(() => {
137-
const msg = messages.find(
156+
if (!selectedTopic || !messages[selectedTopic]) return;
157+
const msg = messages[selectedTopic].find(
138158
(m) => m.timestamp >= startTime + currentTime
139159
);
140160
if (!msg || !msg.data) return;
@@ -268,6 +288,30 @@ export const VideoPanel: React.FC<VideoPanelProps> = ({ messages }) => {
268288
onMouseUp={handleMouseUp}
269289
onMouseLeave={handleMouseUp}
270290
>
291+
<div className="absolute top-2 right-70">
292+
<Select
293+
value={selectedTopic}
294+
onChange={(e: SelectChangeEvent<string>) => {
295+
setSelectedTopic(e.target.value);
296+
}}
297+
size="small"
298+
disabled={Object.keys(messages).length === 0}
299+
sx={{
300+
backgroundColor: "white",
301+
opacity: 0.25,
302+
borderRadius: "9999px",
303+
"&:hover": {
304+
opacity: 0.5,
305+
},
306+
}}
307+
>
308+
{Object.keys(messages).map((topic) => (
309+
<MenuItem key={topic} value={topic}>
310+
{topic}
311+
</MenuItem>
312+
))}
313+
</Select>
314+
</div>
271315
<div className="absolute top-2 right-20">
272316
<FormControlLabel
273317
label="Drag & Zoom"

0 commit comments

Comments
 (0)