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
46 changes: 27 additions & 19 deletions GEMstack/onboard/visualization/sr_viz/threeD/.gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Node.js / Next.js
node_modules/
.next/
out/

# dependencies
/node_modules
/build

# Python / Flask
__pycache__/
*.pyc
.venv/
venv/

# Environment files
.env
.env.*

# OS and editor files
.DS_Store
.idea/
.vscode/

# Yarn / PNP
/.pnp
.pnp.*
.yarn/*
Expand All @@ -10,32 +29,21 @@
!.yarn/releases
!.yarn/versions

# testing
# Testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
# Misc
*.pem

# debug
# Debug logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
# Vercel
.vercel

# typescript
# TypeScript
*.tsbuildinfo
next-env.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"nuxt": "^3.16.1",
"nuxt-app": "file:",
"three": "^0.174.0",
"urdf-loader": "^0.12.4",
"vue": "^3.5.13",
Expand Down
Binary file not shown.
30 changes: 26 additions & 4 deletions GEMstack/onboard/visualization/sr_viz/threeD/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
"use client";

import { useSearchParams } from "next/navigation";
import ControlPanel from "@/components/ControlPanel";
import CanvasWrapper from "@/components/CanvasWrapper";
import Scrubber from "@/components/Scrubber";
import { usePlaybackTime } from "@/hooks/usePlaybackTime";

export default function HomePage() {
const { time, reset, restart, play, togglePlay, speed, setPlaybackSpeed, moveToTime, duration, setDuration } = usePlaybackTime();

const {
time,
reset,
restart,
play,
togglePlay,
speed,
setPlaybackSpeed,
moveToTime,
duration,
setDuration,
} = usePlaybackTime();
const searchParams = useSearchParams();
const folder = searchParams.get("folder") || undefined;
const file = searchParams.get("file") || undefined;
return (
<main className="relative w-screen h-screen bg-white">
<ControlPanel reset={reset} />
<ControlPanel reset={reset} folder={folder} file={file} />
<CanvasWrapper time={time} setDuration={setDuration} />
<Scrubber time={time} play={play} togglePlay={togglePlay} restart={restart} setPlaybackSpeed={setPlaybackSpeed} moveToTime={moveToTime} duration={duration} />
<Scrubber
time={time}
play={play}
togglePlay={togglePlay}
restart={restart}
setPlaybackSpeed={setPlaybackSpeed}
moveToTime={moveToTime}
duration={duration}
/>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useRef, useMemo, useEffect } from "react";
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";
Expand All @@ -14,13 +14,17 @@ interface AgentProps {
}

export default function Agent({ id, timeline, time }: AgentProps) {
const ref = useRef<Mesh>(null);
const [mounted, setMounted] = useState(false);

const ref = useRef<Mesh>(null);
const { modelPath, scale, rotation, offset, bodyColor } = currentAgent;

const { scene } = useGLTF(modelPath);
const clonedScene = useMemo(() => scene.clone(true), [scene]);

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

useEffect(() => {
clonedScene.traverse((child) => {
if (
Expand All @@ -34,19 +38,21 @@ export default function Agent({ id, timeline, time }: AgentProps) {
}, [clonedScene, bodyColor]);

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

const frame = timeline.find((f) => f.time >= time) ?? timeline.at(-1);
if (!frame) return;

ref.current.position.set(frame.x, 0, frame.y);
ref.current.rotation.y = -frame.yaw;
});

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

return (
<primitive
ref={ref as React.MutableRefObject<Object3D>}
ref={ref as React.RefObject<Object3D>}
object={clonedScene}
scale={scale}
rotation={rotation}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,57 @@ import { Environment } from "@react-three/drei";
import { useTimelineStore } from "@/hooks/useTimelineStore";
import Vehicle from "./Vehicle";
import Agent from "./Agent";
import TrafficLight from "./TrafficLight";
import OtherVehicle from "./OtherVehicle";
import Ground from "./Ground";

export default function CanvasWrapper({
time,
setDuration,
time,
setDuration,
}: {
time: number;
setDuration: (duration: number) => void;
time: number;
setDuration: (duration: number) => void;
}) {
const { vehicle, agents } = useTimelineStore();
useEffect(() => {
if (vehicle.length > 0) {
setDuration(vehicle[vehicle.length - 1].time - vehicle[0].time);
}
}, [vehicle]);
const startTime = vehicle.length > 0 ? vehicle[0].time : 0;
const syncedTime = startTime + time;

return (
<Canvas
shadows
camera={{ position: [0, 5, 15], fov: 55, near: 0.1, far: 100000 }}
style={{
background: "#fdfdfd",
position: "fixed",
inset: 0,
zIndex: 0,
}}
>
<Environment preset="city" />
<ambientLight intensity={0.3} />
<directionalLight
position={[10, 10, 5]}
intensity={1.5}
castShadow
/>
<Vehicle timeline={vehicle} time={syncedTime} />
{Object.entries(agents).map(([id, timeline]) => (
<Agent key={id} id={id} timeline={timeline} time={syncedTime} />
))}
<Ground />
</Canvas>
);
const { vehicle, agents, trafficLights, otherVehicles } = useTimelineStore();

useEffect(() => {
if (vehicle.length > 0) {
setDuration(vehicle[vehicle.length - 1].time - vehicle[0].time);
}
}, [vehicle]);

const startTime = vehicle.length > 0 ? vehicle[0].time : 0;
const syncedTime = startTime + time;

return (
<Canvas
shadows
camera={{ position: [0, 5, 15], fov: 55, near: 0.1, far: 100000 }}
style={{
background: "#fdfdfd",
position: "fixed",
inset: 0,
zIndex: 0,
}}
>
<Environment preset="city" />
<ambientLight intensity={0.3} />
<directionalLight position={[10, 10, 5]} intensity={1.5} castShadow />
<Vehicle timeline={vehicle} time={syncedTime} />

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

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

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

<Ground />
</Canvas>
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
"use client";

import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { TiUpload } from "react-icons/ti";
import { RxCross2 } from "react-icons/rx";
import { buildTimeline } from "@/utils/buildTimeline";
import { parseLogFile } from "@/utils/parseLogFile";
import { useTimelineStore } from "@/hooks/useTimelineStore";
import { TimelineData } from "@/types/TimelineData";

export default function ControlPanel({ reset }: { reset: () => void }) {
export default function ControlPanel({
reset,
folder,
file,
}: {
reset: () => void;
folder?: string;
file?: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const [fileName, setFileName] = useState<string | null>(null);
const setTimeline = useTimelineStore((state) => state.setTimeline);
Expand All @@ -25,10 +33,10 @@ export default function ControlPanel({ reset }: { reset: () => void }) {
const entries = await parseLogFile(file);
const timeline: TimelineData = buildTimeline(entries);
setTimeline(timeline);
reset(); // ⏪ Restart animation time
console.log("timeline loaded:", timeline);
reset();
console.log("timeline loaded:", timeline);
} catch (err) {
console.error("Failed to parse log file:", err);
console.error("Failed to parse log file:", err);
}
};
const handleContextMenu = (event: React.MouseEvent) => {
Expand All @@ -40,6 +48,41 @@ export default function ControlPanel({ reset }: { reset: () => void }) {
}
}

useEffect(() => {
if (!folder || !file) return;

const fetchLog = async () => {
const url = `http://localhost:5000/raw_logs/${encodeURIComponent(
folder
)}/${encodeURIComponent(file)}`;

try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Failed to fetch ${file} from ${url} (status ${res.status})`
);
}

const text = await res.text();
const fakeFile = new File([text], file, { type: "text/plain" });

const entries = await parseLogFile(fakeFile);
const timeline = buildTimeline(entries);

setTimeline(timeline);
reset();
setFileName(file);

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

fetchLog();
}, [folder, file]);

return (
<>
<div
Expand Down Expand Up @@ -73,7 +116,7 @@ export default function ControlPanel({ reset }: { reset: () => void }) {
/>
</label>
<p className="mt-2 text-xs text-gray-400 truncate">
{fileName ? `${fileName}` : "No file loaded"}
{fileName ? `${fileName}` : "No file loaded"}
</p>
</div>

Expand Down
Loading
Loading