Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,831 changes: 2,831 additions & 0 deletions GEMstack/onboard/visualization/sr_viz/threeD/public/example.json

Large diffs are not rendered by default.

Binary file not shown.
13 changes: 11 additions & 2 deletions GEMstack/onboard/visualization/sr_viz/threeD/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import ControlPanel from "@/components/ControlPanel";
import CanvasWrapper from "@/components/CanvasWrapper";
import Scrubber from "@/components/Scrubber";
import { VehicleInfoPanel } from "@/components/VehicleInfoPanel";
import { usePlaybackTime } from "@/hooks/usePlaybackTime";

export default function HomePage() {
Expand All @@ -20,7 +21,10 @@ export default function HomePage() {
setDuration,
} = usePlaybackTime();

const [searchParams, setSearchParams] = useState<{ folder?: string; file?: string }>({});
const [searchParams, setSearchParams] = useState<{
folder?: string;
file?: string;
}>({});

useEffect(() => {
if (typeof window !== "undefined") {
Expand All @@ -33,7 +37,11 @@ export default function HomePage() {

return (
<main className="relative w-screen h-screen bg-white">
<ControlPanel reset={reset} folder={searchParams.folder} file={searchParams.file} />
<ControlPanel
reset={reset}
folder={searchParams.folder}
file={searchParams.file}
/>
<CanvasWrapper time={time} setDuration={setDuration} />
<Scrubber
time={time}
Expand All @@ -44,6 +52,7 @@ export default function HomePage() {
moveToTime={moveToTime}
duration={duration}
/>
<VehicleInfoPanel time={time} />
</main>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Agent from "./Agent";
import TrafficLight from "./TrafficLight";
import OtherVehicle from "./OtherVehicle";
import Ground from "./Ground";
import TrafficCone from "./TrafficCone";

export default function CanvasWrapper({
time,
Expand All @@ -17,7 +18,8 @@ export default function CanvasWrapper({
time: number;
setDuration: (duration: number) => void;
}) {
const { vehicle, agents, trafficLights, otherVehicles } = useTimelineStore();
const { vehicle, agents, trafficLights, trafficCones, otherVehicles } =
useTimelineStore();

useEffect(() => {
if (vehicle.length > 0) {
Expand Down Expand Up @@ -52,6 +54,10 @@ export default function CanvasWrapper({
<TrafficLight key={id} id={id} timeline={timeline} time={syncedTime} />
))}

{Object.entries(trafficCones).map(([id, timeline]) => (
<TrafficCone key={id} id={id} timeline={timeline} time={syncedTime} />
))}

{Object.entries(otherVehicles).map(([id, timeline]) => (
<OtherVehicle key={id} id={id} timeline={timeline} time={syncedTime} />
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ export default function ControlPanel({
reset();
setFileName(file);

console.log("[✔] Timeline loaded from Flask API:", timeline);
console.log("Timeline loaded from Flask API:", timeline);
} catch (err) {
console.error("[✘] Failed to load remote log file:", err);
console.error("Failed to load remote log file:", err);
}
};

Expand Down Expand Up @@ -119,10 +119,6 @@ export default function ControlPanel({
{fileName ? `${fileName}` : "No file loaded"}
</p>
</div>

<div className="px-4 text-xs text-gray-400 border-t border-white/10 pt-4">
<p>🚧 Feature toggles coming soon...</p>
</div>
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { useRef, useMemo, useEffect, useState } from "react";
import { useFrame } from "@react-three/fiber";
import { Mesh, Object3D, MeshStandardMaterial } from "three";
import { useGLTF } from "@react-three/drei";
import { FrameData } from "@/types/FrameData";
import { currentTrafficCone } from "@/config/trafficConeConfig";

interface TrafficConeProps {
id: string;
timeline: FrameData[];
time: number;
}

export default function TrafficCone({ id, timeline, time }: TrafficConeProps) {
const [mounted, setMounted] = useState(false);

const ref = useRef<Mesh>(null);
const { modelPath, scale, rotation, offset, bodyColor } = currentTrafficCone;
const { scene } = useGLTF(modelPath);
const clonedScene = useMemo(() => scene.clone(true), [scene]);

useEffect(() => {
setMounted(true);
}, []);

useEffect(() => {
clonedScene.traverse((child) => {
if (
child instanceof Mesh &&
child.material instanceof MeshStandardMaterial
) {
child.material = child.material.clone();
child.material.color.set(bodyColor);
}
});
}, [clonedScene, bodyColor]);

useFrame(() => {
const frame = timeline.find((f) => f.time >= time);
if (frame && ref.current) {
ref.current.position.set(frame.x, frame.z, frame.y);
ref.current.rotation.y = -frame.yaw;
}
});

const hasSpawned = timeline.length > 0 && timeline[0].time <= time;
if (!mounted || !hasSpawned) return null;

return (
<primitive
ref={ref as React.RefObject<Object3D>}
object={clonedScene}
scale={scale}
rotation={rotation}
position={offset}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client";
import React, { useState } from "react";
import { useTimelineStore } from "@/hooks/useTimelineStore";
import { RxCross2 } from "react-icons/rx";

export function VehicleInfoPanel({ time }: { time: number }) {
const [isOpen, setIsOpen] = useState(false);
const frames = useTimelineStore((s) => s.vehicle);

const toggleOpen = (e?: React.MouseEvent) => {
if (e) e.preventDefault();
setIsOpen((open) => !open);
};

if (frames.length === 0) return null;

const startTimestamp = frames[0].time;
const currentTimestamp = startTimestamp + time;
const current = frames.filter((f) => f.time <= currentTimestamp).pop();

return (
<React.Fragment>
<div
className={`fixed top-0 right-0 w-64 bg-black/80 text-white shadow-lg transform transition-transform duration-500 ease-in-out z-40 p-4 ${
isOpen ? "translate-x-0" : "translate-x-full"
}`}
onContextMenu={toggleOpen}
>
{isOpen && (
<>
<div className="flex justify-between items-center mb-2">
<h2 className="text-lg font-semibold">Current Vehicle State</h2>
<button
onClick={toggleOpen}
className="text-white p-1 rounded hover:bg-white/10"
title="Close Panel"
>
<RxCross2 className="w-4 h-4" />
</button>
</div>
{current ? (
<ul className="space-y-1 text-sm">
<li>
<span className="font-medium">Timestamp:</span> {current.time}
</li>
<li>
<span className="font-medium">X:</span> {current.x}
</li>
<li>
<span className="font-medium">Y:</span> {current.y}
</li>
<li>
<span className="font-medium">Z:</span> {current.z}
</li>
<li>
<span className="font-medium">Yaw:</span> {current.yaw}
</li>
<li>
<span className="font-medium">Pitch:</span> {current.pitch}
</li>
<li>
<span className="font-medium">Roll:</span> {current.roll}
</li>
</ul>
) : (
<p className="text-sm italic">No vehicle data at this time</p>
)}
</>
)}
</div>
{!isOpen && (
<button
onClick={toggleOpen}
className="fixed top-[15%] right-0 z-50 bg-black/80 text-white w-3 h-8 flex items-center justify-center rounded-l border-r border-white/20"
title="Open Vehicle Panel"
onContextMenu={(e) => e.preventDefault()}
>
<span className="text-xs">⟨</span>
</button>
)}
</React.Fragment>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ type CameraConfigMap = {

const cameraConfig: CameraConfigMap = {
first: {
position: [0, 1.3, 0], // on top of vehicle
lookAt: [10, 2, 0], // looking forward (+Z)
position: [0, 1.5, 0],
lookAt: [10, 2, 0],
damping: 0.1,
},
chase: {
position: [-6, 3, 0], // behind vehicle
lookAt: [0, 0, 0], // looking at the rear
position: [-6, 3, 0],
lookAt: [0, 0, 0],
damping: 0.1,
},
top: {
position: [0, 10, 0], // overhead
lookAt: [0, 0, 0], // looking forward
position: [0, 10, 0],
lookAt: [0, 0, 0],
damping: 0.1,
},
side: {
position: [0, 2, 5], // from the right
lookAt: [0, 0, -10], // looking at vehicle side
position: [0, 2, 5],
lookAt: [0, 0, -10],
damping: 0.1,
},
free: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const trafficConeConfig = {
trafficCone: {
name: "Traffic Cone",
modelPath: "/models/traffic_cone.glb",
scale: [0.03, 0.03, 0.03],
rotation: [0, 0, 0],
offset: [0, 0, 0],
bodyColor: "#FFFFFF",
},
};

const currentTrafficCone = trafficConeConfig.trafficCone;

export { trafficConeConfig, currentTrafficCone };
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const useTimelineStore = create<TimelineStore>((set) => ({
vehicle: [],
agents: {},
trafficLights: {},
trafficCones: {},
otherVehicles: {},
setTimeline: (timeline) => set(timeline),
}));
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export interface TimelineData {
vehicle: FrameData[];
agents: Record<string, FrameData[]>;
trafficLights: Record<string, FrameData[]>;
trafficCones: Record<string, FrameData[]>;
otherVehicles: Record<string, FrameData[]>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function buildTimeline(entries: LogEntry[]): TimelineData {
const vehicle: FrameData[] = [];
const agents: Record<string, FrameData[]> = {};
const trafficLights: Record<string, FrameData[]> = {};
const trafficCones: Record<string, FrameData[]> = {};
const otherVehicles: Record<string, FrameData[]> = {};

for (const entry of entries) {
Expand All @@ -32,12 +33,16 @@ export function buildTimeline(entries: LogEntry[]): TimelineData {
const key = entry.key.trim();
if (!trafficLights[key]) trafficLights[key] = [];
trafficLights[key].push(frame);
} else if (entry.type === 'OtherVehicleState') {
} else if (entry.type === "TrafficConeState") {
const key = entry.key.trim();
if (!trafficCones[key]) trafficCones[key] = [];
trafficCones[key].push(frame);
}else if (entry.type === 'OtherVehicleState') {
const key = entry.key.trim();
if (!otherVehicles[key]) otherVehicles[key] = [];
otherVehicles[key].push(frame);
}
}

return { vehicle, agents, trafficLights, otherVehicles };
return { vehicle, agents, trafficLights, trafficCones, otherVehicles };
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function parseLogFile(file: File): Promise<LogEntry[]> {
continue;
}

if (key === 'agents' || key === 'traffic_lights' || key === 'other_vehicles') {
if (key === 'agents' || key === 'traffic_lights' || key === 'traffic_cones' || key === 'other_vehicles') {
for (const [itemId, itemValue] of Object.entries(value)) {
if (typeof itemValue === 'object' && itemValue !== null && 'data' in itemValue) {
const frame = (itemValue as any).data?.pose?.frame;
Expand Down Expand Up @@ -52,6 +52,7 @@ export async function parseLogFile(file: File): Promise<LogEntry[]> {
function guessTypeFromKey(key: string): string {
if (key === 'agents') return 'AgentState';
if (key === 'traffic_lights') return 'TrafficLightState';
if (key === 'traffic_cones') return 'TrafficConeState';
if (key === 'other_vehicles') return 'OtherVehicleState';
return 'Unknown';
}
1 change: 1 addition & 0 deletions log_dashboard/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
flask
flask-cors
pyyaml
cachelib
numpy
Expand Down
Loading