SpatialTrackerV2 / _viz /viz_template.html
xiaoyuxi
Cleaned history, reset to current state
c8d9d42
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Point Cloud Visualizer</title>
<style>
:root {
--primary: #9b59b6; /* Brighter purple for dark mode */
--primary-light: #3a2e4a;
--secondary: #a86add;
--accent: #ff6e6e;
--bg: #1a1a1a;
--surface: #2c2c2c;
--text: #e0e0e0;
--text-secondary: #a0a0a0;
--border: #444444;
--shadow: rgba(0, 0, 0, 0.2);
--shadow-hover: rgba(0, 0, 0, 0.3);
--space-sm: 16px;
--space-md: 24px;
--space-lg: 32px;
}
body {
margin: 0;
overflow: hidden;
background: var(--bg);
color: var(--text);
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
}
#canvas-container {
position: absolute;
width: 100%;
height: 100%;
}
#ui-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
#status-bar {
position: absolute;
top: 16px;
left: 16px;
background: rgba(30, 30, 30, 0.9);
padding: 8px 16px;
border-radius: 8px;
pointer-events: auto;
box-shadow: 0 4px 6px var(--shadow);
backdrop-filter: blur(4px);
border: 1px solid var(--border);
color: var(--text);
transition: opacity 0.5s ease, transform 0.5s ease;
font-weight: 500;
}
#status-bar.hidden {
opacity: 0;
transform: translateY(-20px);
pointer-events: none;
}
#control-panel {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
background: rgba(44, 44, 44, 0.95);
padding: 6px 8px;
border-radius: 6px;
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
pointer-events: auto;
box-shadow: 0 4px 10px var(--shadow);
backdrop-filter: blur(4px);
border: 1px solid var(--border);
}
#timeline {
width: 150px;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
position: relative;
cursor: pointer;
}
#progress {
position: absolute;
height: 100%;
background: var(--primary);
border-radius: 2px;
width: 0%;
}
#playback-controls {
display: flex;
gap: 4px;
align-items: center;
}
button {
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--border);
color: var(--text);
padding: 4px 6px;
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, transform 0.2s;
font-family: 'Inter', sans-serif;
font-weight: 500;
font-size: 6px;
}
button:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
button.active {
background: var(--primary);
color: white;
box-shadow: 0 2px 8px rgba(155, 89, 182, 0.4);
}
select, input {
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--border);
color: var(--text);
padding: 4px 6px;
border-radius: 3px;
cursor: pointer;
font-family: 'Inter', sans-serif;
font-size: 6px;
}
.icon {
width: 10px;
height: 10px;
fill: currentColor;
}
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--surface);
color: var(--text);
padding: 3px 6px;
border-radius: 3px;
font-size: 7px;
white-space: nowrap;
margin-bottom: 4px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
box-shadow: 0 2px 4px var(--shadow);
border: 1px solid var(--border);
}
button:hover .tooltip {
opacity: 1;
}
#settings-panel {
position: absolute;
top: 16px;
right: 16px;
background: rgba(44, 44, 44, 0.98);
padding: 10px;
border-radius: 6px;
width: 195px;
max-height: calc(100vh - 40px);
overflow-y: auto;
pointer-events: auto;
box-shadow: 0 4px 15px var(--shadow);
backdrop-filter: blur(4px);
border: 1px solid var(--border);
display: block;
opacity: 1;
scrollbar-width: thin;
scrollbar-color: var(--primary-light) transparent;
transition: transform 0.35s ease-in-out, opacity 0.3s ease-in-out;
}
#settings-panel.is-hidden {
transform: translateX(calc(100% + 20px));
opacity: 0;
pointer-events: none;
}
#settings-panel::-webkit-scrollbar {
width: 3px;
}
#settings-panel::-webkit-scrollbar-track {
background: transparent;
}
#settings-panel::-webkit-scrollbar-thumb {
background-color: var(--primary-light);
border-radius: 3px;
}
@media (max-height: 700px) {
#settings-panel {
max-height: calc(100vh - 40px);
}
}
@media (max-width: 768px) {
#control-panel {
width: 90%;
flex-wrap: wrap;
justify-content: center;
}
#timeline {
width: 100%;
order: 3;
margin-top: 10px;
}
#settings-panel {
width: 140px;
right: 10px;
top: 10px;
max-height: calc(100vh - 20px);
}
}
.settings-group {
margin-bottom: 8px;
}
.settings-group h3 {
margin: 0 0 6px 0;
font-size: 10px;
font-weight: 500;
color: var(--text-secondary);
}
.slider-container {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
}
.slider-container label {
min-width: 60px;
font-size: 10px;
flex-shrink: 0;
}
input[type="range"] {
flex: 1;
height: 2px;
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 1px;
min-width: 0;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 20px;
height: 10px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.1);
transition: .4s;
border-radius: 10px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 8px;
width: 8px;
left: 1px;
bottom: 1px;
background: var(--surface);
border: 1px solid var(--border);
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background: var(--primary);
}
input:checked + .toggle-slider:before {
transform: translateX(10px);
}
.checkbox-container {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.checkbox-container label {
font-size: 10px;
cursor: pointer;
}
#loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
transition: opacity 0.5s;
}
#loading-overlay.fade-out {
opacity: 0;
pointer-events: none;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(155, 89, 182, 0.2);
border-radius: 50%;
border-top-color: var(--primary);
animation: spin 1s ease-in-out infinite;
margin-bottom: 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
#loading-text {
margin-top: 16px;
font-size: 18px;
color: var(--text);
font-weight: 500;
}
#frame-counter {
color: var(--text-secondary);
font-size: 7px;
font-weight: 500;
min-width: 60px;
text-align: center;
padding: 0 4px;
}
.control-btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--border);
padding: 4px 6px;
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 6px;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
.control-btn.active {
background: var(--primary);
color: white;
}
.control-btn.active:hover {
background: var(--primary);
box-shadow: 0 2px 8px rgba(155, 89, 182, 0.4);
}
#settings-toggle-btn {
position: relative;
border-radius: 6px;
z-index: 20;
}
#settings-toggle-btn.active {
background: var(--primary);
color: white;
}
#status-bar,
#control-panel,
#settings-panel,
button,
input,
select,
.toggle-switch {
pointer-events: auto;
}
h2 {
font-size: 0.9rem;
font-weight: 600;
margin-top: 0;
margin-bottom: 12px;
color: var(--primary);
cursor: move;
user-select: none;
display: flex;
align-items: center;
}
.drag-handle {
font-size: 10px;
margin-right: 4px;
opacity: 0.6;
}
h2:hover .drag-handle {
opacity: 1;
}
.loading-subtitle {
font-size: 7px;
color: var(--text-secondary);
margin-top: 4px;
}
#reset-view-btn {
background: var(--primary-light);
color: var(--primary);
border: 1px solid rgba(155, 89, 182, 0.2);
font-weight: 600;
font-size: 9px;
padding: 4px 6px;
transition: all 0.2s;
}
#reset-view-btn:hover {
background: var(--primary);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(155, 89, 182, 0.3);
}
#show-settings-btn {
position: absolute;
top: 16px;
right: 16px;
z-index: 15;
display: none;
}
#settings-panel.visible {
display: block;
opacity: 1;
animation: slideIn 0.3s ease forwards;
}
@keyframes slideIn {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.dragging {
opacity: 0.9;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15) !important;
transition: none !important;
}
/* Tooltip for draggable element */
.tooltip-drag {
position: absolute;
left: 50%;
transform: translateX(-50%);
background: var(--primary);
color: white;
font-size: 9px;
padding: 2px 4px;
border-radius: 2px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
white-space: nowrap;
bottom: 100%;
margin-bottom: 4px;
}
h2:hover .tooltip-drag {
opacity: 1;
}
.btn-group {
display: flex;
margin-top: 8px;
}
#reset-settings-btn {
background: var(--primary-light);
color: var(--primary);
border: 1px solid rgba(155, 89, 182, 0.2);
font-weight: 600;
font-size: 9px;
padding: 4px 6px;
transition: all 0.2s;
}
#reset-settings-btn:hover {
background: var(--primary);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(155, 89, 182, 0.3);
}
</style>
</head>
<body>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<div id="canvas-container"></div>
<div id="ui-container">
<div id="status-bar">Initializing...</div>
<div id="control-panel">
<button id="play-pause-btn" class="control-btn">
<svg class="icon" viewBox="0 0 24 24">
<path id="play-icon" d="M8 5v14l11-7z"/>
<path id="pause-icon" d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" style="display: none;"/>
</svg>
<span class="tooltip">Play/Pause</span>
</button>
<div id="timeline">
<div id="progress"></div>
</div>
<div id="frame-counter">Frame 0 / 0</div>
<div id="playback-controls">
<button id="speed-btn" class="control-btn">1x</button>
</div>
</div>
<div id="settings-panel">
<h2>
<span class="drag-handle"></span>
Visualization Settings
<button id="hide-settings-btn" class="control-btn" style="margin-left: auto; padding: 2px;" title="Hide Panel">
<svg class="icon" viewBox="0 0 24 24" style="width: 9px; height: 9px;">
<path d="M14.59 7.41L18.17 11H4v2h14.17l-3.58 3.59L16 18l6-6-6-6-1.41 1.41z"/>
</svg>
</button>
</h2>
<div class="settings-group">
<h3>Point Cloud</h3>
<div class="slider-container">
<label for="point-size">Size</label>
<input type="range" id="point-size" min="0.005" max="0.1" step="0.005" value="0.03">
</div>
<div class="slider-container">
<label for="point-opacity">Opacity</label>
<input type="range" id="point-opacity" min="0.1" max="1" step="0.05" value="1">
</div>
<div class="slider-container">
<label for="max-depth">Max Depth</label>
<input type="range" id="max-depth" min="0.1" max="10" step="0.2" value="100">
</div>
</div>
<div class="settings-group">
<h3>Trajectory</h3>
<div class="checkbox-container">
<label class="toggle-switch">
<input type="checkbox" id="show-trajectory" checked>
<span class="toggle-slider"></span>
</label>
<label for="show-trajectory">Show Trajectory</label>
</div>
<div class="checkbox-container">
<label class="toggle-switch">
<input type="checkbox" id="enable-rich-trail">
<span class="toggle-slider"></span>
</label>
<label for="enable-rich-trail">Visual-Rich Trail</label>
</div>
<div class="slider-container">
<label for="trajectory-line-width">Line Width</label>
<input type="range" id="trajectory-line-width" min="0.5" max="5" step="0.5" value="1.5">
</div>
<div class="slider-container">
<label for="trajectory-ball-size">Ball Size</label>
<input type="range" id="trajectory-ball-size" min="0.005" max="0.05" step="0.001" value="0.02">
</div>
<div class="slider-container">
<label for="trajectory-history">History Frames</label>
<input type="range" id="trajectory-history" min="1" max="500" step="1" value="30">
</div>
<div class="slider-container" id="tail-opacity-container" style="display: none;">
<label for="trajectory-fade">Tail Opacity</label>
<input type="range" id="trajectory-fade" min="0" max="1" step="0.05" value="0.0">
</div>
</div>
<div class="settings-group">
<h3>Camera</h3>
<div class="checkbox-container">
<label class="toggle-switch">
<input type="checkbox" id="show-camera-frustum" checked>
<span class="toggle-slider"></span>
</label>
<label for="show-camera-frustum">Show Camera Frustum</label>
</div>
<div class="slider-container">
<label for="frustum-size">Size</label>
<input type="range" id="frustum-size" min="0.02" max="0.5" step="0.01" value="0.2">
</div>
</div>
<div class="settings-group">
<div class="btn-group">
<button id="reset-view-btn" style="flex: 1; margin-right: 5px;">Reset View</button>
<button id="reset-settings-btn" style="flex: 1; margin-left: 5px;">Reset Settings</button>
</div>
</div>
</div>
<button id="show-settings-btn" class="control-btn" title="Show Settings">
<svg class="icon" viewBox="0 0 24 24">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.04,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>
</button>
</div>
<div id="loading-overlay">
<!-- <div class="spinner"></div> -->
<div id="loading-text"></div>
<div class="loading-subtitle" style="font-size: medium;">Interactive Viewer of 3D Tracking</div>
</div>
<!-- Libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/dat.gui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/LineSegmentsGeometry.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/LineGeometry.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/LineMaterial.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/LineSegments2.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/Line2.js"></script>
<script>
class PointCloudVisualizer {
constructor() {
this.data = null;
this.config = {};
this.currentFrame = 0;
this.isPlaying = false;
this.playbackSpeed = 1;
this.lastFrameTime = 0;
this.defaultSettings = null;
this.ui = {
statusBar: document.getElementById('status-bar'),
playPauseBtn: document.getElementById('play-pause-btn'),
speedBtn: document.getElementById('speed-btn'),
timeline: document.getElementById('timeline'),
progress: document.getElementById('progress'),
settingsPanel: document.getElementById('settings-panel'),
loadingOverlay: document.getElementById('loading-overlay'),
loadingText: document.getElementById('loading-text'),
settingsToggleBtn: document.getElementById('settings-toggle-btn'),
frameCounter: document.getElementById('frame-counter'),
pointSize: document.getElementById('point-size'),
pointOpacity: document.getElementById('point-opacity'),
maxDepth: document.getElementById('max-depth'),
showTrajectory: document.getElementById('show-trajectory'),
enableRichTrail: document.getElementById('enable-rich-trail'),
trajectoryLineWidth: document.getElementById('trajectory-line-width'),
trajectoryBallSize: document.getElementById('trajectory-ball-size'),
trajectoryHistory: document.getElementById('trajectory-history'),
trajectoryFade: document.getElementById('trajectory-fade'),
tailOpacityContainer: document.getElementById('tail-opacity-container'),
resetViewBtn: document.getElementById('reset-view-btn'),
showCameraFrustum: document.getElementById('show-camera-frustum'),
frustumSize: document.getElementById('frustum-size'),
hideSettingsBtn: document.getElementById('hide-settings-btn'),
showSettingsBtn: document.getElementById('show-settings-btn')
};
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.pointCloud = null;
this.trajectories = [];
this.cameraFrustum = null;
this.initThreeJS();
this.loadDefaultSettings().then(() => {
this.initEventListeners();
this.loadData();
});
}
async loadDefaultSettings() {
try {
const urlParams = new URLSearchParams(window.location.search);
const dataPath = urlParams.get('data') || '';
const defaultSettings = {
pointSize: 0.03,
pointOpacity: 1.0,
showTrajectory: true,
trajectoryLineWidth: 2.5,
trajectoryBallSize: 0.015,
trajectoryHistory: 0,
showCameraFrustum: true,
frustumSize: 0.2
};
if (!dataPath) {
this.defaultSettings = defaultSettings;
this.applyDefaultSettings();
return;
}
// Try to extract dataset and videoId from the data path
// Expected format: demos/datasetname/videoid.bin
const pathParts = dataPath.split('/');
if (pathParts.length < 3) {
this.defaultSettings = defaultSettings;
this.applyDefaultSettings();
return;
}
const datasetName = pathParts[pathParts.length - 2];
let videoId = pathParts[pathParts.length - 1].replace('.bin', '');
// Load settings from data.json
const response = await fetch('./data.json');
if (!response.ok) {
this.defaultSettings = defaultSettings;
this.applyDefaultSettings();
return;
}
const settingsData = await response.json();
// Check if this dataset and video exist
if (settingsData[datasetName] && settingsData[datasetName][videoId]) {
this.defaultSettings = settingsData[datasetName][videoId];
} else {
this.defaultSettings = defaultSettings;
}
this.applyDefaultSettings();
} catch (error) {
console.error("Error loading default settings:", error);
this.defaultSettings = {
pointSize: 0.03,
pointOpacity: 1.0,
showTrajectory: true,
trajectoryLineWidth: 2.5,
trajectoryBallSize: 0.015,
trajectoryHistory: 0,
showCameraFrustum: true,
frustumSize: 0.2
};
this.applyDefaultSettings();
}
}
applyDefaultSettings() {
if (!this.defaultSettings) return;
if (this.ui.pointSize) {
this.ui.pointSize.value = this.defaultSettings.pointSize;
}
if (this.ui.pointOpacity) {
this.ui.pointOpacity.value = this.defaultSettings.pointOpacity;
}
if (this.ui.maxDepth) {
this.ui.maxDepth.value = this.defaultSettings.maxDepth || 100.0;
}
if (this.ui.showTrajectory) {
this.ui.showTrajectory.checked = this.defaultSettings.showTrajectory;
}
if (this.ui.trajectoryLineWidth) {
this.ui.trajectoryLineWidth.value = this.defaultSettings.trajectoryLineWidth;
}
if (this.ui.trajectoryBallSize) {
this.ui.trajectoryBallSize.value = this.defaultSettings.trajectoryBallSize;
}
if (this.ui.trajectoryHistory) {
this.ui.trajectoryHistory.value = this.defaultSettings.trajectoryHistory;
}
if (this.ui.showCameraFrustum) {
this.ui.showCameraFrustum.checked = this.defaultSettings.showCameraFrustum;
}
if (this.ui.frustumSize) {
this.ui.frustumSize.value = this.defaultSettings.frustumSize;
}
}
initThreeJS() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x1a1a1a);
this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
this.camera.position.set(0, 0, 0);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('canvas-container').appendChild(this.renderer.domElement);
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.target.set(0, 0, 0);
this.controls.minDistance = 0.1;
this.controls.maxDistance = 1000;
this.controls.update();
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
this.scene.add(directionalLight);
}
initEventListeners() {
window.addEventListener('resize', () => this.onWindowResize());
this.ui.playPauseBtn.addEventListener('click', () => this.togglePlayback());
this.ui.timeline.addEventListener('click', (e) => {
const rect = this.ui.timeline.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
this.seekTo(pos);
});
this.ui.speedBtn.addEventListener('click', () => this.cyclePlaybackSpeed());
this.ui.pointSize.addEventListener('input', () => this.updatePointCloudSettings());
this.ui.pointOpacity.addEventListener('input', () => this.updatePointCloudSettings());
this.ui.maxDepth.addEventListener('input', () => this.updatePointCloudSettings());
this.ui.showTrajectory.addEventListener('change', () => {
this.trajectories.forEach(trajectory => {
trajectory.visible = this.ui.showTrajectory.checked;
});
});
this.ui.enableRichTrail.addEventListener('change', () => {
this.ui.tailOpacityContainer.style.display = this.ui.enableRichTrail.checked ? 'flex' : 'none';
this.updateTrajectories(this.currentFrame);
});
this.ui.trajectoryLineWidth.addEventListener('input', () => this.updateTrajectorySettings());
this.ui.trajectoryBallSize.addEventListener('input', () => this.updateTrajectorySettings());
this.ui.trajectoryHistory.addEventListener('input', () => {
this.updateTrajectories(this.currentFrame);
});
this.ui.trajectoryFade.addEventListener('input', () => {
this.updateTrajectories(this.currentFrame);
});
this.ui.resetViewBtn.addEventListener('click', () => this.resetView());
const resetSettingsBtn = document.getElementById('reset-settings-btn');
if (resetSettingsBtn) {
resetSettingsBtn.addEventListener('click', () => this.resetSettings());
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.ui.settingsPanel.classList.contains('visible')) {
this.ui.settingsPanel.classList.remove('visible');
this.ui.settingsToggleBtn.classList.remove('active');
}
});
if (this.ui.settingsToggleBtn) {
this.ui.settingsToggleBtn.addEventListener('click', () => {
const isVisible = this.ui.settingsPanel.classList.toggle('visible');
this.ui.settingsToggleBtn.classList.toggle('active', isVisible);
if (isVisible) {
const panelRect = this.ui.settingsPanel.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (panelRect.bottom > viewportHeight) {
this.ui.settingsPanel.style.bottom = 'auto';
this.ui.settingsPanel.style.top = '80px';
}
}
});
}
if (this.ui.frustumSize) {
this.ui.frustumSize.addEventListener('input', () => this.updateFrustumDimensions());
}
if (this.ui.hideSettingsBtn && this.ui.showSettingsBtn && this.ui.settingsPanel) {
this.ui.hideSettingsBtn.addEventListener('click', () => {
this.ui.settingsPanel.classList.add('is-hidden');
this.ui.showSettingsBtn.style.display = 'flex';
});
this.ui.showSettingsBtn.addEventListener('click', () => {
this.ui.settingsPanel.classList.remove('is-hidden');
this.ui.showSettingsBtn.style.display = 'none';
});
}
}
makeElementDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const dragHandle = element.querySelector('h2');
if (dragHandle) {
dragHandle.onmousedown = dragMouseDown;
dragHandle.title = "Drag to move panel";
} else {
element.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
element.classList.add('dragging');
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
const newTop = element.offsetTop - pos2;
const newLeft = element.offsetLeft - pos1;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const panelRect = element.getBoundingClientRect();
const maxTop = viewportHeight - 50;
const maxLeft = viewportWidth - 50;
element.style.top = Math.min(Math.max(newTop, 0), maxTop) + "px";
element.style.left = Math.min(Math.max(newLeft, 0), maxLeft) + "px";
// Remove bottom/right settings when dragging
element.style.bottom = 'auto';
element.style.right = 'auto';
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
element.classList.remove('dragging');
}
}
async loadData() {
try {
// this.ui.loadingText.textContent = "Loading binary data...";
let arrayBuffer;
if (window.embeddedBase64) {
// Base64 embedded path
const binaryString = atob(window.embeddedBase64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
arrayBuffer = bytes.buffer;
} else {
// Default fetch path (fallback)
const urlParams = new URLSearchParams(window.location.search);
const dataPath = urlParams.get('data') || 'data.bin';
const response = await fetch(dataPath);
if (!response.ok) throw new Error(`Failed to load ${dataPath}`);
arrayBuffer = await response.arrayBuffer();
}
const dataView = new DataView(arrayBuffer);
const headerLen = dataView.getUint32(0, true);
const headerText = new TextDecoder("utf-8").decode(arrayBuffer.slice(4, 4 + headerLen));
const header = JSON.parse(headerText);
const compressedBlob = new Uint8Array(arrayBuffer, 4 + headerLen);
const decompressed = pako.inflate(compressedBlob).buffer;
const arrays = {};
for (const key in header) {
if (key === "meta") continue;
const meta = header[key];
const { dtype, shape, offset, length } = meta;
const slice = decompressed.slice(offset, offset + length);
let typedArray;
switch (dtype) {
case "uint8": typedArray = new Uint8Array(slice); break;
case "uint16": typedArray = new Uint16Array(slice); break;
case "float32": typedArray = new Float32Array(slice); break;
case "float64": typedArray = new Float64Array(slice); break;
default: throw new Error(`Unknown dtype: ${dtype}`);
}
arrays[key] = { data: typedArray, shape: shape };
}
this.data = arrays;
this.config = header.meta;
this.initCameraWithCorrectFOV();
this.ui.loadingText.textContent = "Creating point cloud...";
this.initPointCloud();
this.initTrajectories();
setTimeout(() => {
this.ui.loadingOverlay.classList.add('fade-out');
this.ui.statusBar.classList.add('hidden');
this.startAnimation();
}, 500);
} catch (error) {
console.error("Error loading data:", error);
this.ui.statusBar.textContent = `Error: ${error.message}`;
// this.ui.loadingText.textContent = `Error loading data: ${error.message}`;
}
}
initPointCloud() {
const numPoints = this.config.resolution[0] * this.config.resolution[1];
const positions = new Float32Array(numPoints * 3);
const colors = new Float32Array(numPoints * 3);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3).setUsage(THREE.DynamicDrawUsage));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage));
const pointSize = parseFloat(this.ui.pointSize.value) || this.defaultSettings.pointSize;
const pointOpacity = parseFloat(this.ui.pointOpacity.value) || this.defaultSettings.pointOpacity;
const material = new THREE.PointsMaterial({
size: pointSize,
vertexColors: true,
transparent: true,
opacity: pointOpacity,
sizeAttenuation: true
});
this.pointCloud = new THREE.Points(geometry, material);
this.scene.add(this.pointCloud);
}
initTrajectories() {
if (!this.data.trajectories) return;
this.trajectories.forEach(trajectory => {
if (trajectory.userData.lineSegments) {
trajectory.userData.lineSegments.forEach(segment => {
segment.geometry.dispose();
segment.material.dispose();
});
}
this.scene.remove(trajectory);
});
this.trajectories = [];
const shape = this.data.trajectories.shape;
if (!shape || shape.length < 2) return;
const [totalFrames, numTrajectories] = shape;
const palette = this.createColorPalette(numTrajectories);
const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
const maxHistory = 500; // Max value of the history slider, for the object pool
for (let i = 0; i < numTrajectories; i++) {
const trajectoryGroup = new THREE.Group();
const ballSize = parseFloat(this.ui.trajectoryBallSize.value);
const sphereGeometry = new THREE.SphereGeometry(ballSize, 16, 16);
const sphereMaterial = new THREE.MeshBasicMaterial({ color: palette[i], transparent: true });
const positionMarker = new THREE.Mesh(sphereGeometry, sphereMaterial);
trajectoryGroup.add(positionMarker);
// High-Performance Line (default)
const simpleLineGeometry = new THREE.BufferGeometry();
const simpleLinePositions = new Float32Array(maxHistory * 3);
simpleLineGeometry.setAttribute('position', new THREE.BufferAttribute(simpleLinePositions, 3).setUsage(THREE.DynamicDrawUsage));
const simpleLine = new THREE.Line(simpleLineGeometry, new THREE.LineBasicMaterial({ color: palette[i] }));
simpleLine.frustumCulled = false;
trajectoryGroup.add(simpleLine);
// High-Quality Line Segments (for rich trail)
const lineSegments = [];
const lineWidth = parseFloat(this.ui.trajectoryLineWidth.value);
// Create a pool of line segment objects
for (let j = 0; j < maxHistory - 1; j++) {
const lineGeometry = new THREE.LineGeometry();
lineGeometry.setPositions([0, 0, 0, 0, 0, 0]);
const lineMaterial = new THREE.LineMaterial({
color: palette[i],
linewidth: lineWidth,
resolution: resolution,
transparent: true,
depthWrite: false, // Correctly handle transparency
opacity: 0
});
const segment = new THREE.Line2(lineGeometry, lineMaterial);
segment.frustumCulled = false;
segment.visible = false; // Start with all segments hidden
trajectoryGroup.add(segment);
lineSegments.push(segment);
}
trajectoryGroup.userData = {
marker: positionMarker,
simpleLine: simpleLine,
lineSegments: lineSegments,
color: palette[i]
};
this.scene.add(trajectoryGroup);
this.trajectories.push(trajectoryGroup);
}
const showTrajectory = this.ui.showTrajectory.checked;
this.trajectories.forEach(trajectory => trajectory.visible = showTrajectory);
}
createColorPalette(count) {
const colors = [];
const hueStep = 360 / count;
for (let i = 0; i < count; i++) {
const hue = (i * hueStep) % 360;
const color = new THREE.Color().setHSL(hue / 360, 0.8, 0.6);
colors.push(color);
}
return colors;
}
updatePointCloud(frameIndex) {
if (!this.data || !this.pointCloud) return;
const positions = this.pointCloud.geometry.attributes.position.array;
const colors = this.pointCloud.geometry.attributes.color.array;
const rgbVideo = this.data.rgb_video;
const depthsRgb = this.data.depths_rgb;
const intrinsics = this.data.intrinsics;
const invExtrinsics = this.data.inv_extrinsics;
const width = this.config.resolution[0];
const height = this.config.resolution[1];
const numPoints = width * height;
const K = this.get3x3Matrix(intrinsics.data, intrinsics.shape, frameIndex);
const fx = K[0][0], fy = K[1][1], cx = K[0][2], cy = K[1][2];
const invExtrMat = this.get4x4Matrix(invExtrinsics.data, invExtrinsics.shape, frameIndex);
const transform = this.getTransformElements(invExtrMat);
const rgbFrame = this.getFrame(rgbVideo.data, rgbVideo.shape, frameIndex);
const depthFrame = this.getFrame(depthsRgb.data, depthsRgb.shape, frameIndex);
const maxDepth = parseFloat(this.ui.maxDepth.value) || 10.0;
let validPointCount = 0;
for (let i = 0; i < numPoints; i++) {
const xPix = i % width;
const yPix = Math.floor(i / width);
const d0 = depthFrame[i * 3];
const d1 = depthFrame[i * 3 + 1];
const depthEncoded = d0 | (d1 << 8);
const depthValue = (depthEncoded / ((1 << 16) - 1)) *
(this.config.depthRange[1] - this.config.depthRange[0]) +
this.config.depthRange[0];
if (depthValue === 0 || depthValue > maxDepth) {
continue;
}
const X = ((xPix - cx) * depthValue) / fx;
const Y = ((yPix - cy) * depthValue) / fy;
const Z = depthValue;
const tx = transform.m11 * X + transform.m12 * Y + transform.m13 * Z + transform.m14;
const ty = transform.m21 * X + transform.m22 * Y + transform.m23 * Z + transform.m24;
const tz = transform.m31 * X + transform.m32 * Y + transform.m33 * Z + transform.m34;
const index = validPointCount * 3;
positions[index] = tx;
positions[index + 1] = -ty;
positions[index + 2] = -tz;
colors[index] = rgbFrame[i * 3] / 255;
colors[index + 1] = rgbFrame[i * 3 + 1] / 255;
colors[index + 2] = rgbFrame[i * 3 + 2] / 255;
validPointCount++;
}
this.pointCloud.geometry.setDrawRange(0, validPointCount);
this.pointCloud.geometry.attributes.position.needsUpdate = true;
this.pointCloud.geometry.attributes.color.needsUpdate = true;
this.pointCloud.geometry.computeBoundingSphere(); // Important for camera culling
this.updateTrajectories(frameIndex);
const progress = (frameIndex + 1) / this.config.totalFrames;
this.ui.progress.style.width = `${progress * 100}%`;
if (this.ui.frameCounter && this.config.totalFrames) {
this.ui.frameCounter.textContent = `Frame ${frameIndex} / ${this.config.totalFrames - 1}`;
}
this.updateCameraFrustum(frameIndex);
}
updateTrajectories(frameIndex) {
if (!this.data.trajectories || this.trajectories.length === 0) return;
const trajectoryData = this.data.trajectories.data;
const [totalFrames, numTrajectories] = this.data.trajectories.shape;
const historyFrames = parseInt(this.ui.trajectoryHistory.value);
const tailOpacity = parseFloat(this.ui.trajectoryFade.value);
const isRichMode = this.ui.enableRichTrail.checked;
for (let i = 0; i < numTrajectories; i++) {
const trajectoryGroup = this.trajectories[i];
const { marker, simpleLine, lineSegments } = trajectoryGroup.userData;
const currentPos = new THREE.Vector3();
const currentOffset = (frameIndex * numTrajectories + i) * 3;
currentPos.x = trajectoryData[currentOffset];
currentPos.y = -trajectoryData[currentOffset + 1];
currentPos.z = -trajectoryData[currentOffset + 2];
marker.position.copy(currentPos);
marker.material.opacity = 1.0;
const historyToShow = Math.min(historyFrames, frameIndex + 1);
if (isRichMode) {
// --- High-Quality Mode ---
simpleLine.visible = false;
for (let j = 0; j < lineSegments.length; j++) {
const segment = lineSegments[j];
if (j < historyToShow - 1) {
const headFrame = frameIndex - j;
const tailFrame = frameIndex - j - 1;
const headOffset = (headFrame * numTrajectories + i) * 3;
const tailOffset = (tailFrame * numTrajectories + i) * 3;
const positions = [
trajectoryData[headOffset], -trajectoryData[headOffset + 1], -trajectoryData[headOffset + 2],
trajectoryData[tailOffset], -trajectoryData[tailOffset + 1], -trajectoryData[tailOffset + 2]
];
segment.geometry.setPositions(positions);
const headOpacity = 1.0;
const normalizedAge = j / Math.max(1, historyToShow - 2);
const alpha = headOpacity - (headOpacity - tailOpacity) * normalizedAge;
segment.material.opacity = Math.max(0, alpha);
segment.visible = true;
} else {
segment.visible = false;
}
}
} else {
// --- Performance Mode ---
lineSegments.forEach(s => s.visible = false);
simpleLine.visible = true;
const positions = simpleLine.geometry.attributes.position.array;
for (let j = 0; j < historyToShow; j++) {
const historyFrame = Math.max(0, frameIndex - j);
const offset = (historyFrame * numTrajectories + i) * 3;
positions[j * 3] = trajectoryData[offset];
positions[j * 3 + 1] = -trajectoryData[offset + 1];
positions[j * 3 + 2] = -trajectoryData[offset + 2];
}
simpleLine.geometry.setDrawRange(0, historyToShow);
simpleLine.geometry.attributes.position.needsUpdate = true;
}
}
}
updateTrajectorySettings() {
if (!this.trajectories || this.trajectories.length === 0) return;
const ballSize = parseFloat(this.ui.trajectoryBallSize.value);
const lineWidth = parseFloat(this.ui.trajectoryLineWidth.value);
this.trajectories.forEach(trajectoryGroup => {
const { marker, lineSegments } = trajectoryGroup.userData;
marker.geometry.dispose();
marker.geometry = new THREE.SphereGeometry(ballSize, 16, 16);
// Line width only affects rich mode
lineSegments.forEach(segment => {
if (segment.material) {
segment.material.linewidth = lineWidth;
}
});
});
this.updateTrajectories(this.currentFrame);
}
getDepthColor(normalizedDepth) {
const hue = (1 - normalizedDepth) * 240 / 360;
const color = new THREE.Color().setHSL(hue, 1.0, 0.5);
return color;
}
getFrame(typedArray, shape, frameIndex) {
const [T, H, W, C] = shape;
const frameSize = H * W * C;
const offset = frameIndex * frameSize;
return typedArray.subarray(offset, offset + frameSize);
}
get3x3Matrix(typedArray, shape, frameIndex) {
const frameSize = 9;
const offset = frameIndex * frameSize;
const K = [];
for (let i = 0; i < 3; i++) {
const row = [];
for (let j = 0; j < 3; j++) {
row.push(typedArray[offset + i * 3 + j]);
}
K.push(row);
}
return K;
}
get4x4Matrix(typedArray, shape, frameIndex) {
const frameSize = 16;
const offset = frameIndex * frameSize;
const M = [];
for (let i = 0; i < 4; i++) {
const row = [];
for (let j = 0; j < 4; j++) {
row.push(typedArray[offset + i * 4 + j]);
}
M.push(row);
}
return M;
}
getTransformElements(matrix) {
return {
m11: matrix[0][0], m12: matrix[0][1], m13: matrix[0][2], m14: matrix[0][3],
m21: matrix[1][0], m22: matrix[1][1], m23: matrix[1][2], m24: matrix[1][3],
m31: matrix[2][0], m32: matrix[2][1], m33: matrix[2][2], m34: matrix[2][3]
};
}
togglePlayback() {
this.isPlaying = !this.isPlaying;
const playIcon = document.getElementById('play-icon');
const pauseIcon = document.getElementById('pause-icon');
if (this.isPlaying) {
playIcon.style.display = 'none';
pauseIcon.style.display = 'block';
this.lastFrameTime = performance.now();
} else {
playIcon.style.display = 'block';
pauseIcon.style.display = 'none';
}
}
cyclePlaybackSpeed() {
const speeds = [0.5, 1, 2, 4, 8];
const speedRates = speeds.map(s => s * this.config.baseFrameRate);
let currentIndex = 0;
const normalizedSpeed = this.playbackSpeed / this.config.baseFrameRate;
for (let i = 0; i < speeds.length; i++) {
if (Math.abs(normalizedSpeed - speeds[i]) < Math.abs(normalizedSpeed - speeds[currentIndex])) {
currentIndex = i;
}
}
const nextIndex = (currentIndex + 1) % speeds.length;
this.playbackSpeed = speedRates[nextIndex];
this.ui.speedBtn.textContent = `${speeds[nextIndex]}x`;
if (speeds[nextIndex] === 1) {
this.ui.speedBtn.classList.remove('active');
} else {
this.ui.speedBtn.classList.add('active');
}
}
seekTo(position) {
const frameIndex = Math.floor(position * this.config.totalFrames);
this.currentFrame = Math.max(0, Math.min(frameIndex, this.config.totalFrames - 1));
this.updatePointCloud(this.currentFrame);
}
updatePointCloudSettings() {
if (!this.pointCloud) return;
const size = parseFloat(this.ui.pointSize.value);
const opacity = parseFloat(this.ui.pointOpacity.value);
this.pointCloud.material.size = size;
this.pointCloud.material.opacity = opacity;
this.pointCloud.material.needsUpdate = true;
this.updatePointCloud(this.currentFrame);
}
updateControls() {
if (!this.controls) return;
this.controls.update();
}
resetView() {
if (!this.camera || !this.controls) return;
// Reset camera position
this.camera.position.set(0, 0, this.config.cameraZ || 0);
// Reset controls
this.controls.reset();
// Set target slightly in front of camera
this.controls.target.set(0, 0, -1);
this.controls.update();
// Show status message
this.ui.statusBar.textContent = "View reset";
this.ui.statusBar.classList.remove('hidden');
// Hide status message after a few seconds
setTimeout(() => {
this.ui.statusBar.classList.add('hidden');
}, 3000);
}
onWindowResize() {
if (!this.camera || !this.renderer) return;
const windowAspect = window.innerWidth / window.innerHeight;
this.camera.aspect = windowAspect;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
if (this.trajectories && this.trajectories.length > 0) {
const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
this.trajectories.forEach(trajectory => {
const { lineSegments } = trajectory.userData;
if (lineSegments && lineSegments.length > 0) {
lineSegments.forEach(segment => {
if (segment.material && segment.material.resolution) {
segment.material.resolution.copy(resolution);
}
});
}
});
}
if (this.cameraFrustum) {
const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
this.cameraFrustum.children.forEach(line => {
if (line.material && line.material.resolution) {
line.material.resolution.copy(resolution);
}
});
}
}
startAnimation() {
this.isPlaying = true;
this.lastFrameTime = performance.now();
this.camera.position.set(0, 0, this.config.cameraZ || 0);
this.controls.target.set(0, 0, -1);
this.controls.update();
this.playbackSpeed = this.config.baseFrameRate;
document.getElementById('play-icon').style.display = 'none';
document.getElementById('pause-icon').style.display = 'block';
this.animate();
}
animate() {
requestAnimationFrame(() => this.animate());
if (this.controls) {
this.controls.update();
}
if (this.isPlaying && this.data) {
const now = performance.now();
const delta = (now - this.lastFrameTime) / 1000;
const framesToAdvance = Math.floor(delta * this.config.baseFrameRate * this.playbackSpeed);
if (framesToAdvance > 0) {
this.currentFrame = (this.currentFrame + framesToAdvance) % this.config.totalFrames;
this.lastFrameTime = now;
this.updatePointCloud(this.currentFrame);
}
}
if (this.renderer && this.scene && this.camera) {
this.renderer.render(this.scene, this.camera);
}
}
initCameraWithCorrectFOV() {
const fov = this.config.fov || 60;
const windowAspect = window.innerWidth / window.innerHeight;
this.camera = new THREE.PerspectiveCamera(
fov,
windowAspect,
0.1,
10000
);
this.controls.object = this.camera;
this.controls.update();
this.initCameraFrustum();
}
initCameraFrustum() {
this.cameraFrustum = new THREE.Group();
this.scene.add(this.cameraFrustum);
this.initCameraFrustumGeometry();
const showCameraFrustum = this.ui.showCameraFrustum ? this.ui.showCameraFrustum.checked : (this.defaultSettings ? this.defaultSettings.showCameraFrustum : false);
this.cameraFrustum.visible = showCameraFrustum;
}
initCameraFrustumGeometry() {
const fov = this.config.fov || 60;
const originalAspect = this.config.original_aspect_ratio || 1.33;
const size = parseFloat(this.ui.frustumSize.value) || this.defaultSettings.frustumSize;
const halfHeight = Math.tan(THREE.MathUtils.degToRad(fov / 2)) * size;
const halfWidth = halfHeight * originalAspect;
const vertices = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(-halfWidth, -halfHeight, size),
new THREE.Vector3(halfWidth, -halfHeight, size),
new THREE.Vector3(halfWidth, halfHeight, size),
new THREE.Vector3(-halfWidth, halfHeight, size)
];
const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
const linePairs = [
[1, 2], [2, 3], [3, 4], [4, 1],
[0, 1], [0, 2], [0, 3], [0, 4]
];
const colors = {
edge: new THREE.Color(0x3366ff),
ray: new THREE.Color(0x33cc66)
};
linePairs.forEach((pair, index) => {
const positions = [
vertices[pair[0]].x, vertices[pair[0]].y, vertices[pair[0]].z,
vertices[pair[1]].x, vertices[pair[1]].y, vertices[pair[1]].z
];
const lineGeometry = new THREE.LineGeometry();
lineGeometry.setPositions(positions);
let color = index < 4 ? colors.edge : colors.ray;
const lineMaterial = new THREE.LineMaterial({
color: color,
linewidth: 2,
resolution: resolution,
dashed: false
});
const line = new THREE.Line2(lineGeometry, lineMaterial);
this.cameraFrustum.add(line);
});
}
updateCameraFrustum(frameIndex) {
if (!this.cameraFrustum || !this.data) return;
const invExtrinsics = this.data.inv_extrinsics;
if (!invExtrinsics) return;
const invExtrMat = this.get4x4Matrix(invExtrinsics.data, invExtrinsics.shape, frameIndex);
const matrix = new THREE.Matrix4();
matrix.set(
invExtrMat[0][0], invExtrMat[0][1], invExtrMat[0][2], invExtrMat[0][3],
invExtrMat[1][0], invExtrMat[1][1], invExtrMat[1][2], invExtrMat[1][3],
invExtrMat[2][0], invExtrMat[2][1], invExtrMat[2][2], invExtrMat[2][3],
invExtrMat[3][0], invExtrMat[3][1], invExtrMat[3][2], invExtrMat[3][3]
);
const position = new THREE.Vector3();
position.setFromMatrixPosition(matrix);
const rotMatrix = new THREE.Matrix4().extractRotation(matrix);
const coordinateCorrection = new THREE.Matrix4().makeRotationX(Math.PI);
const finalRotation = new THREE.Matrix4().multiplyMatrices(coordinateCorrection, rotMatrix);
const quaternion = new THREE.Quaternion();
quaternion.setFromRotationMatrix(finalRotation);
position.y = -position.y;
position.z = -position.z;
this.cameraFrustum.position.copy(position);
this.cameraFrustum.quaternion.copy(quaternion);
const showCameraFrustum = this.ui.showCameraFrustum ? this.ui.showCameraFrustum.checked : this.defaultSettings.showCameraFrustum;
if (this.cameraFrustum.visible !== showCameraFrustum) {
this.cameraFrustum.visible = showCameraFrustum;
}
const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
this.cameraFrustum.children.forEach(line => {
if (line.material && line.material.resolution) {
line.material.resolution.copy(resolution);
}
});
}
updateFrustumDimensions() {
if (!this.cameraFrustum) return;
while(this.cameraFrustum.children.length > 0) {
const child = this.cameraFrustum.children[0];
if (child.geometry) child.geometry.dispose();
if (child.material) child.material.dispose();
this.cameraFrustum.remove(child);
}
this.initCameraFrustumGeometry();
this.updateCameraFrustum(this.currentFrame);
}
resetSettings() {
if (!this.defaultSettings) return;
this.applyDefaultSettings();
this.updatePointCloudSettings();
this.updateTrajectorySettings();
this.updateFrustumDimensions();
this.ui.statusBar.textContent = "Settings reset to defaults";
this.ui.statusBar.classList.remove('hidden');
setTimeout(() => {
this.ui.statusBar.classList.add('hidden');
}, 3000);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PointCloudVisualizer();
});
</script>
</body>
</html>