xiaoyuxi commited on
Commit
d508015
·
1 Parent(s): 66fdf9a
Files changed (1) hide show
  1. _viz/viz_template.html +0 -1769
_viz/viz_template.html DELETED
@@ -1,1769 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>3D Point Cloud Visualizer</title>
7
- <style>
8
- :root {
9
- --primary: #9b59b6; /* Brighter purple for dark mode */
10
- --primary-light: #3a2e4a;
11
- --secondary: #a86add;
12
- --accent: #ff6e6e;
13
- --bg: #1a1a1a;
14
- --surface: #2c2c2c;
15
- --text: #e0e0e0;
16
- --text-secondary: #a0a0a0;
17
- --border: #444444;
18
- --shadow: rgba(0, 0, 0, 0.2);
19
- --shadow-hover: rgba(0, 0, 0, 0.3);
20
-
21
- --space-sm: 16px;
22
- --space-md: 24px;
23
- --space-lg: 32px;
24
- }
25
-
26
- body {
27
- margin: 0;
28
- overflow: hidden;
29
- background: var(--bg);
30
- color: var(--text);
31
- font-family: 'Inter', sans-serif;
32
- -webkit-font-smoothing: antialiased;
33
- }
34
-
35
- #canvas-container {
36
- position: absolute;
37
- width: 100%;
38
- height: 100%;
39
- }
40
-
41
- #ui-container {
42
- position: absolute;
43
- top: 0;
44
- left: 0;
45
- width: 100%;
46
- height: 100%;
47
- pointer-events: none;
48
- z-index: 10;
49
- }
50
-
51
- #status-bar {
52
- position: absolute;
53
- top: 16px;
54
- left: 16px;
55
- background: rgba(30, 30, 30, 0.9);
56
- padding: 8px 16px;
57
- border-radius: 8px;
58
- pointer-events: auto;
59
- box-shadow: 0 4px 6px var(--shadow);
60
- backdrop-filter: blur(4px);
61
- border: 1px solid var(--border);
62
- color: var(--text);
63
- transition: opacity 0.5s ease, transform 0.5s ease;
64
- font-weight: 500;
65
- }
66
-
67
- #status-bar.hidden {
68
- opacity: 0;
69
- transform: translateY(-20px);
70
- pointer-events: none;
71
- }
72
-
73
- #control-panel {
74
- position: absolute;
75
- bottom: 16px;
76
- left: 50%;
77
- transform: translateX(-50%);
78
- background: rgba(44, 44, 44, 0.95);
79
- padding: 12px 16px;
80
- border-radius: 12px;
81
- display: flex;
82
- gap: 16px;
83
- align-items: center;
84
- pointer-events: auto;
85
- box-shadow: 0 4px 10px var(--shadow);
86
- backdrop-filter: blur(4px);
87
- border: 1px solid var(--border);
88
- }
89
-
90
- #timeline {
91
- width: 400px;
92
- height: 8px;
93
- background: rgba(255, 255, 255, 0.1);
94
- border-radius: 4px;
95
- position: relative;
96
- cursor: pointer;
97
- }
98
-
99
- #progress {
100
- position: absolute;
101
- height: 100%;
102
- background: var(--primary);
103
- border-radius: 4px;
104
- width: 0%;
105
- }
106
-
107
- #playback-controls {
108
- display: flex;
109
- gap: 8px;
110
- align-items: center;
111
- }
112
-
113
- button {
114
- background: rgba(255, 255, 255, 0.08);
115
- border: 1px solid var(--border);
116
- color: var(--text);
117
- padding: 8px 12px;
118
- border-radius: 6px;
119
- cursor: pointer;
120
- display: flex;
121
- align-items: center;
122
- justify-content: center;
123
- transition: background 0.2s, transform 0.2s;
124
- font-family: 'Inter', sans-serif;
125
- font-weight: 500;
126
- }
127
-
128
- button:hover {
129
- background: rgba(255, 255, 255, 0.15);
130
- transform: translateY(-1px);
131
- }
132
-
133
- button.active {
134
- background: var(--primary);
135
- color: white;
136
- box-shadow: 0 2px 8px rgba(155, 89, 182, 0.4);
137
- }
138
-
139
- select, input {
140
- background: rgba(255, 255, 255, 0.08);
141
- border: 1px solid var(--border);
142
- color: var(--text);
143
- padding: 8px 12px;
144
- border-radius: 6px;
145
- cursor: pointer;
146
- font-family: 'Inter', sans-serif;
147
- }
148
-
149
- .icon {
150
- width: 20px;
151
- height: 20px;
152
- fill: currentColor;
153
- }
154
-
155
- .tooltip {
156
- position: absolute;
157
- bottom: 100%;
158
- left: 50%;
159
- transform: translateX(-50%);
160
- background: var(--surface);
161
- color: var(--text);
162
- padding: 6px 12px;
163
- border-radius: 6px;
164
- font-size: 14px;
165
- white-space: nowrap;
166
- margin-bottom: 8px;
167
- opacity: 0;
168
- transition: opacity 0.2s;
169
- pointer-events: none;
170
- box-shadow: 0 2px 4px var(--shadow);
171
- border: 1px solid var(--border);
172
- }
173
-
174
- button:hover .tooltip {
175
- opacity: 1;
176
- }
177
-
178
- #settings-panel {
179
- position: absolute;
180
- top: 16px;
181
- right: 16px;
182
- background: rgba(44, 44, 44, 0.98);
183
- padding: 20px;
184
- border-radius: 12px;
185
- width: 300px;
186
- max-height: calc(100vh - 40px);
187
- overflow-y: auto;
188
- pointer-events: auto;
189
- box-shadow: 0 4px 15px var(--shadow);
190
- backdrop-filter: blur(4px);
191
- border: 1px solid var(--border);
192
- display: block;
193
- opacity: 1;
194
- scrollbar-width: thin;
195
- scrollbar-color: var(--primary-light) transparent;
196
- transition: transform 0.35s ease-in-out, opacity 0.3s ease-in-out;
197
- }
198
-
199
- #settings-panel.is-hidden {
200
- transform: translateX(calc(100% + 20px));
201
- opacity: 0;
202
- pointer-events: none;
203
- }
204
-
205
- #settings-panel::-webkit-scrollbar {
206
- width: 6px;
207
- }
208
-
209
- #settings-panel::-webkit-scrollbar-track {
210
- background: transparent;
211
- }
212
-
213
- #settings-panel::-webkit-scrollbar-thumb {
214
- background-color: var(--primary-light);
215
- border-radius: 6px;
216
- }
217
-
218
- @media (max-height: 700px) {
219
- #settings-panel {
220
- max-height: calc(100vh - 40px);
221
- }
222
- }
223
-
224
- @media (max-width: 768px) {
225
- #control-panel {
226
- width: 90%;
227
- flex-wrap: wrap;
228
- justify-content: center;
229
- }
230
-
231
- #timeline {
232
- width: 100%;
233
- order: 3;
234
- margin-top: 10px;
235
- }
236
-
237
- #settings-panel {
238
- width: 280px;
239
- right: 10px;
240
- top: 10px;
241
- max-height: calc(100vh - 20px);
242
- }
243
- }
244
-
245
- .settings-group {
246
- margin-bottom: 16px;
247
- }
248
-
249
- .settings-group h3 {
250
- margin: 0 0 8px 0;
251
- font-size: 14px;
252
- font-weight: 500;
253
- color: var(--text-secondary);
254
- }
255
-
256
- .slider-container {
257
- display: flex;
258
- align-items: center;
259
- gap: 12px;
260
- }
261
-
262
- .slider-container label {
263
- min-width: 80px;
264
- font-size: 14px;
265
- }
266
-
267
- input[type="range"] {
268
- flex-grow: 1;
269
- height: 4px;
270
- -webkit-appearance: none;
271
- background: rgba(255, 255, 255, 0.1);
272
- border-radius: 2px;
273
- }
274
-
275
- input[type="range"]::-webkit-slider-thumb {
276
- -webkit-appearance: none;
277
- width: 16px;
278
- height: 16px;
279
- border-radius: 50%;
280
- background: var(--primary);
281
- cursor: pointer;
282
- }
283
-
284
- .toggle-switch {
285
- position: relative;
286
- display: inline-block;
287
- width: 40px;
288
- height: 20px;
289
- }
290
-
291
- .toggle-switch input {
292
- opacity: 0;
293
- width: 0;
294
- height: 0;
295
- }
296
-
297
- .toggle-slider {
298
- position: absolute;
299
- cursor: pointer;
300
- top: 0;
301
- left: 0;
302
- right: 0;
303
- bottom: 0;
304
- background: rgba(255, 255, 255, 0.1);
305
- transition: .4s;
306
- border-radius: 20px;
307
- }
308
-
309
- .toggle-slider:before {
310
- position: absolute;
311
- content: "";
312
- height: 16px;
313
- width: 16px;
314
- left: 2px;
315
- bottom: 2px;
316
- background: var(--surface);
317
- border: 1px solid var(--border);
318
- transition: .4s;
319
- border-radius: 50%;
320
- }
321
-
322
- input:checked + .toggle-slider {
323
- background: var(--primary);
324
- }
325
-
326
- input:checked + .toggle-slider:before {
327
- transform: translateX(20px);
328
- }
329
-
330
- .checkbox-container {
331
- display: flex;
332
- align-items: center;
333
- gap: 8px;
334
- margin-bottom: 8px;
335
- }
336
-
337
- .checkbox-container label {
338
- font-size: 14px;
339
- cursor: pointer;
340
- }
341
-
342
- #loading-overlay {
343
- position: absolute;
344
- top: 0;
345
- left: 0;
346
- width: 100%;
347
- height: 100%;
348
- background: var(--bg);
349
- display: flex;
350
- flex-direction: column;
351
- align-items: center;
352
- justify-content: center;
353
- z-index: 100;
354
- transition: opacity 0.5s;
355
- }
356
-
357
- #loading-overlay.fade-out {
358
- opacity: 0;
359
- pointer-events: none;
360
- }
361
-
362
- .spinner {
363
- width: 50px;
364
- height: 50px;
365
- border: 5px solid rgba(155, 89, 182, 0.2);
366
- border-radius: 50%;
367
- border-top-color: var(--primary);
368
- animation: spin 1s ease-in-out infinite;
369
- margin-bottom: 16px;
370
- }
371
-
372
- @keyframes spin {
373
- to { transform: rotate(360deg); }
374
- }
375
-
376
- #loading-text {
377
- margin-top: 16px;
378
- font-size: 18px;
379
- color: var(--text);
380
- font-weight: 500;
381
- }
382
-
383
- #frame-counter {
384
- color: var(--text-secondary);
385
- font-size: 14px;
386
- font-weight: 500;
387
- min-width: 120px;
388
- text-align: center;
389
- padding: 0 8px;
390
- }
391
-
392
- .control-btn {
393
- background: rgba(255, 255, 255, 0.08);
394
- border: 1px solid var(--border);
395
- padding: 8px 12px;
396
- border-radius: 6px;
397
- cursor: pointer;
398
- display: flex;
399
- align-items: center;
400
- justify-content: center;
401
- transition: all 0.2s ease;
402
- }
403
-
404
- .control-btn:hover {
405
- background: rgba(255, 255, 255, 0.15);
406
- transform: translateY(-1px);
407
- }
408
-
409
- .control-btn.active {
410
- background: var(--primary);
411
- color: white;
412
- }
413
-
414
- .control-btn.active:hover {
415
- background: var(--primary);
416
- box-shadow: 0 2px 8px rgba(155, 89, 182, 0.4);
417
- }
418
-
419
- #settings-toggle-btn {
420
- position: relative;
421
- border-radius: 6px;
422
- z-index: 20;
423
- }
424
-
425
- #settings-toggle-btn.active {
426
- background: var(--primary);
427
- color: white;
428
- }
429
-
430
- #status-bar,
431
- #control-panel,
432
- #settings-panel,
433
- button,
434
- input,
435
- select,
436
- .toggle-switch {
437
- pointer-events: auto;
438
- }
439
-
440
- h2 {
441
- font-size: 1.2rem;
442
- font-weight: 600;
443
- margin-top: 0;
444
- margin-bottom: var(--space-md);
445
- color: var(--primary);
446
- cursor: move;
447
- user-select: none;
448
- display: flex;
449
- align-items: center;
450
- }
451
-
452
- .drag-handle {
453
- font-size: 14px;
454
- margin-right: 8px;
455
- opacity: 0.6;
456
- }
457
-
458
- h2:hover .drag-handle {
459
- opacity: 1;
460
- }
461
-
462
- .loading-subtitle {
463
- font-size: 14px;
464
- color: var(--text-secondary);
465
- margin-top: 8px;
466
- }
467
-
468
- #reset-view-btn {
469
- background: var(--primary-light);
470
- color: var(--primary);
471
- border: 1px solid rgba(155, 89, 182, 0.2);
472
- font-weight: 600;
473
- transition: all 0.2s;
474
- }
475
-
476
- #reset-view-btn:hover {
477
- background: var(--primary);
478
- color: white;
479
- transform: translateY(-2px);
480
- box-shadow: 0 4px 8px rgba(155, 89, 182, 0.3);
481
- }
482
-
483
- #settings-panel.visible {
484
- display: block;
485
- opacity: 1;
486
- animation: slideIn 0.3s ease forwards;
487
- }
488
-
489
- @keyframes slideIn {
490
- from {
491
- transform: translateY(20px);
492
- opacity: 0;
493
- }
494
- to {
495
- transform: translateY(0);
496
- opacity: 1;
497
- }
498
- }
499
-
500
- .dragging {
501
- opacity: 0.9;
502
- box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15) !important;
503
- transition: none !important;
504
- }
505
-
506
- /* Tooltip for draggable element */
507
- .tooltip-drag {
508
- position: absolute;
509
- left: 50%;
510
- transform: translateX(-50%);
511
- background: var(--primary);
512
- color: white;
513
- font-size: 12px;
514
- padding: 4px 8px;
515
- border-radius: 4px;
516
- opacity: 0;
517
- pointer-events: none;
518
- transition: opacity 0.3s;
519
- white-space: nowrap;
520
- bottom: 100%;
521
- margin-bottom: 8px;
522
- }
523
-
524
- h2:hover .tooltip-drag {
525
- opacity: 1;
526
- }
527
-
528
- .btn-group {
529
- display: flex;
530
- margin-top: 16px;
531
- }
532
-
533
- #reset-view-btn, #reset-settings-btn {
534
- background: var(--primary-light);
535
- color: var(--primary);
536
- border: 1px solid rgba(155, 89, 182, 0.2);
537
- font-weight: 600;
538
- transition: all 0.2s;
539
- }
540
-
541
- #reset-view-btn:hover, #reset-settings-btn:hover {
542
- background: var(--primary);
543
- color: white;
544
- transform: translateY(-2px);
545
- box-shadow: 0 4px 8px rgba(155, 89, 182, 0.3);
546
- }
547
-
548
- #show-settings-btn {
549
- position: absolute;
550
- top: 16px;
551
- right: 16px;
552
- z-index: 15;
553
- display: none;
554
- }
555
- </style>
556
- </head>
557
- <body>
558
- <link rel="preconnect" href="https://fonts.googleapis.com">
559
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
560
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
561
-
562
- <div id="canvas-container"></div>
563
-
564
- <div id="ui-container">
565
- <div id="status-bar">Initializing...</div>
566
-
567
- <div id="control-panel">
568
- <button id="play-pause-btn" class="control-btn">
569
- <svg class="icon" viewBox="0 0 24 24">
570
- <path id="play-icon" d="M8 5v14l11-7z"/>
571
- <path id="pause-icon" d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" style="display: none;"/>
572
- </svg>
573
- <span class="tooltip">Play/Pause</span>
574
- </button>
575
-
576
- <div id="timeline">
577
- <div id="progress"></div>
578
- </div>
579
-
580
- <div id="frame-counter">Frame 0 / 0</div>
581
-
582
- <div id="playback-controls">
583
- <button id="speed-btn" class="control-btn">1x</button>
584
- </div>
585
- </div>
586
-
587
- <div id="settings-panel">
588
- <h2>
589
- <span class="drag-handle">☰</span>
590
- Visualization Settings
591
- <button id="hide-settings-btn" class="control-btn" style="margin-left: auto; padding: 4px;" title="Hide Panel">
592
- <svg class="icon" viewBox="0 0 24 24" style="width: 18px; height: 18px;">
593
- <path d="M14.59 7.41L18.17 11H4v2h14.17l-3.58 3.59L16 18l6-6-6-6-1.41 1.41z"/>
594
- </svg>
595
- </button>
596
- </h2>
597
-
598
- <div class="settings-group">
599
- <h3>Point Cloud</h3>
600
- <div class="slider-container">
601
- <label for="point-size">Size</label>
602
- <input type="range" id="point-size" min="0.005" max="0.1" step="0.005" value="0.03">
603
- </div>
604
- <div class="slider-container">
605
- <label for="point-opacity">Opacity</label>
606
- <input type="range" id="point-opacity" min="0.1" max="1" step="0.05" value="1">
607
- </div>
608
- <div class="slider-container">
609
- <label for="max-depth">Max Depth</label>
610
- <input type="range" id="max-depth" min="0.1" max="10" step="0.2" value="100">
611
- </div>
612
- </div>
613
-
614
- <div class="settings-group">
615
- <h3>Trajectory</h3>
616
- <div class="checkbox-container">
617
- <label class="toggle-switch">
618
- <input type="checkbox" id="show-trajectory" checked>
619
- <span class="toggle-slider"></span>
620
- </label>
621
- <label for="show-trajectory">Show Trajectory</label>
622
- </div>
623
- <div class="checkbox-container">
624
- <label class="toggle-switch">
625
- <input type="checkbox" id="enable-rich-trail">
626
- <span class="toggle-slider"></span>
627
- </label>
628
- <label for="enable-rich-trail">Visual-Rich Trail</label>
629
- </div>
630
- <div class="slider-container">
631
- <label for="trajectory-line-width">Line Width</label>
632
- <input type="range" id="trajectory-line-width" min="0.5" max="5" step="0.5" value="1.5">
633
- </div>
634
- <div class="slider-container">
635
- <label for="trajectory-ball-size">Ball Size</label>
636
- <input type="range" id="trajectory-ball-size" min="0.005" max="0.05" step="0.001" value="0.02">
637
- </div>
638
- <div class="slider-container">
639
- <label for="trajectory-history">History Frames</label>
640
- <input type="range" id="trajectory-history" min="1" max="500" step="1" value="30">
641
- </div>
642
- <div class="slider-container" id="tail-opacity-container" style="display: none;">
643
- <label for="trajectory-fade">Tail Opacity</label>
644
- <input type="range" id="trajectory-fade" min="0" max="1" step="0.05" value="0.0">
645
- </div>
646
- </div>
647
-
648
- <div class="settings-group">
649
- <h3>Camera</h3>
650
- <div class="checkbox-container">
651
- <label class="toggle-switch">
652
- <input type="checkbox" id="show-camera-frustum" checked>
653
- <span class="toggle-slider"></span>
654
- </label>
655
- <label for="show-camera-frustum">Show Camera Frustum</label>
656
- </div>
657
- <div class="slider-container">
658
- <label for="frustum-size">Size</label>
659
- <input type="range" id="frustum-size" min="0.02" max="0.5" step="0.01" value="0.2">
660
- </div>
661
- </div>
662
-
663
- <div class="settings-group">
664
- <div class="btn-group">
665
- <button id="reset-view-btn" style="flex: 1; margin-right: 5px;">Reset View</button>
666
- <button id="reset-settings-btn" style="flex: 1; margin-left: 5px;">Reset Settings</button>
667
- </div>
668
- </div>
669
- </div>
670
-
671
- <button id="show-settings-btn" class="control-btn" title="Show Settings">
672
- <svg class="icon" viewBox="0 0 24 24">
673
- <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"/>
674
- </svg>
675
- </button>
676
- </div>
677
-
678
- <div id="loading-overlay">
679
- <!-- <div class="spinner"></div> -->
680
- <div id="loading-text"></div>
681
- <div class="loading-subtitle" style="font-size: xx-large;">Interactive Viewer of 3D Tracking</div>
682
- </div>
683
-
684
- <!-- Libraries -->
685
- <script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
686
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
687
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
688
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/dat.gui.min.js"></script>
689
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/LineSegmentsGeometry.js"></script>
690
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/LineGeometry.js"></script>
691
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/LineMaterial.js"></script>
692
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/LineSegments2.js"></script>
693
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/lines/Line2.js"></script>
694
-
695
- <script>
696
- class PointCloudVisualizer {
697
- constructor() {
698
- this.data = null;
699
- this.config = {};
700
- this.currentFrame = 0;
701
- this.isPlaying = false;
702
- this.playbackSpeed = 1;
703
- this.lastFrameTime = 0;
704
- this.defaultSettings = null;
705
-
706
- this.ui = {
707
- statusBar: document.getElementById('status-bar'),
708
- playPauseBtn: document.getElementById('play-pause-btn'),
709
- speedBtn: document.getElementById('speed-btn'),
710
- timeline: document.getElementById('timeline'),
711
- progress: document.getElementById('progress'),
712
- settingsPanel: document.getElementById('settings-panel'),
713
- loadingOverlay: document.getElementById('loading-overlay'),
714
- loadingText: document.getElementById('loading-text'),
715
- settingsToggleBtn: document.getElementById('settings-toggle-btn'),
716
- frameCounter: document.getElementById('frame-counter'),
717
- pointSize: document.getElementById('point-size'),
718
- pointOpacity: document.getElementById('point-opacity'),
719
- maxDepth: document.getElementById('max-depth'),
720
- showTrajectory: document.getElementById('show-trajectory'),
721
- enableRichTrail: document.getElementById('enable-rich-trail'),
722
- trajectoryLineWidth: document.getElementById('trajectory-line-width'),
723
- trajectoryBallSize: document.getElementById('trajectory-ball-size'),
724
- trajectoryHistory: document.getElementById('trajectory-history'),
725
- trajectoryFade: document.getElementById('trajectory-fade'),
726
- tailOpacityContainer: document.getElementById('tail-opacity-container'),
727
- resetViewBtn: document.getElementById('reset-view-btn'),
728
- showCameraFrustum: document.getElementById('show-camera-frustum'),
729
- frustumSize: document.getElementById('frustum-size'),
730
- hideSettingsBtn: document.getElementById('hide-settings-btn'),
731
- showSettingsBtn: document.getElementById('show-settings-btn')
732
- };
733
-
734
- this.scene = null;
735
- this.camera = null;
736
- this.renderer = null;
737
- this.controls = null;
738
- this.pointCloud = null;
739
- this.trajectories = [];
740
- this.cameraFrustum = null;
741
-
742
- this.initThreeJS();
743
- this.loadDefaultSettings().then(() => {
744
- this.initEventListeners();
745
- this.loadData();
746
- });
747
- }
748
-
749
- async loadDefaultSettings() {
750
- try {
751
- const urlParams = new URLSearchParams(window.location.search);
752
- const dataPath = urlParams.get('data') || '';
753
-
754
- const defaultSettings = {
755
- pointSize: 0.03,
756
- pointOpacity: 1.0,
757
- showTrajectory: true,
758
- trajectoryLineWidth: 2.5,
759
- trajectoryBallSize: 0.015,
760
- trajectoryHistory: 0,
761
- showCameraFrustum: true,
762
- frustumSize: 0.2
763
- };
764
-
765
- if (!dataPath) {
766
- this.defaultSettings = defaultSettings;
767
- this.applyDefaultSettings();
768
- return;
769
- }
770
-
771
- // Try to extract dataset and videoId from the data path
772
- // Expected format: demos/datasetname/videoid.bin
773
- const pathParts = dataPath.split('/');
774
- if (pathParts.length < 3) {
775
- this.defaultSettings = defaultSettings;
776
- this.applyDefaultSettings();
777
- return;
778
- }
779
-
780
- const datasetName = pathParts[pathParts.length - 2];
781
- let videoId = pathParts[pathParts.length - 1].replace('.bin', '');
782
-
783
- // Load settings from data.json
784
- const response = await fetch('./data.json');
785
- if (!response.ok) {
786
- this.defaultSettings = defaultSettings;
787
- this.applyDefaultSettings();
788
- return;
789
- }
790
-
791
- const settingsData = await response.json();
792
-
793
- // Check if this dataset and video exist
794
- if (settingsData[datasetName] && settingsData[datasetName][videoId]) {
795
- this.defaultSettings = settingsData[datasetName][videoId];
796
- } else {
797
- this.defaultSettings = defaultSettings;
798
- }
799
-
800
- this.applyDefaultSettings();
801
- } catch (error) {
802
- console.error("Error loading default settings:", error);
803
-
804
- this.defaultSettings = {
805
- pointSize: 0.03,
806
- pointOpacity: 1.0,
807
- showTrajectory: true,
808
- trajectoryLineWidth: 2.5,
809
- trajectoryBallSize: 0.015,
810
- trajectoryHistory: 0,
811
- showCameraFrustum: true,
812
- frustumSize: 0.2
813
- };
814
-
815
- this.applyDefaultSettings();
816
- }
817
- }
818
-
819
- applyDefaultSettings() {
820
- if (!this.defaultSettings) return;
821
-
822
- if (this.ui.pointSize) {
823
- this.ui.pointSize.value = this.defaultSettings.pointSize;
824
- }
825
-
826
- if (this.ui.pointOpacity) {
827
- this.ui.pointOpacity.value = this.defaultSettings.pointOpacity;
828
- }
829
-
830
- if (this.ui.maxDepth) {
831
- this.ui.maxDepth.value = this.defaultSettings.maxDepth || 100.0;
832
- }
833
-
834
- if (this.ui.showTrajectory) {
835
- this.ui.showTrajectory.checked = this.defaultSettings.showTrajectory;
836
- }
837
-
838
- if (this.ui.trajectoryLineWidth) {
839
- this.ui.trajectoryLineWidth.value = this.defaultSettings.trajectoryLineWidth;
840
- }
841
-
842
- if (this.ui.trajectoryBallSize) {
843
- this.ui.trajectoryBallSize.value = this.defaultSettings.trajectoryBallSize;
844
- }
845
-
846
- if (this.ui.trajectoryHistory) {
847
- this.ui.trajectoryHistory.value = this.defaultSettings.trajectoryHistory;
848
- }
849
-
850
- if (this.ui.showCameraFrustum) {
851
- this.ui.showCameraFrustum.checked = this.defaultSettings.showCameraFrustum;
852
- }
853
-
854
- if (this.ui.frustumSize) {
855
- this.ui.frustumSize.value = this.defaultSettings.frustumSize;
856
- }
857
- }
858
-
859
- initThreeJS() {
860
- this.scene = new THREE.Scene();
861
- this.scene.background = new THREE.Color(0x1a1a1a);
862
-
863
- this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
864
- this.camera.position.set(0, 0, 0);
865
-
866
- this.renderer = new THREE.WebGLRenderer({ antialias: true });
867
- this.renderer.setPixelRatio(window.devicePixelRatio);
868
- this.renderer.setSize(window.innerWidth, window.innerHeight);
869
- document.getElementById('canvas-container').appendChild(this.renderer.domElement);
870
-
871
- this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
872
- this.controls.enableDamping = true;
873
- this.controls.dampingFactor = 0.05;
874
- this.controls.target.set(0, 0, 0);
875
- this.controls.minDistance = 0.1;
876
- this.controls.maxDistance = 1000;
877
- this.controls.update();
878
-
879
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
880
- this.scene.add(ambientLight);
881
-
882
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
883
- directionalLight.position.set(1, 1, 1);
884
- this.scene.add(directionalLight);
885
- }
886
-
887
- initEventListeners() {
888
- window.addEventListener('resize', () => this.onWindowResize());
889
-
890
- this.ui.playPauseBtn.addEventListener('click', () => this.togglePlayback());
891
-
892
- this.ui.timeline.addEventListener('click', (e) => {
893
- const rect = this.ui.timeline.getBoundingClientRect();
894
- const pos = (e.clientX - rect.left) / rect.width;
895
- this.seekTo(pos);
896
- });
897
-
898
- this.ui.speedBtn.addEventListener('click', () => this.cyclePlaybackSpeed());
899
-
900
- this.ui.pointSize.addEventListener('input', () => this.updatePointCloudSettings());
901
- this.ui.pointOpacity.addEventListener('input', () => this.updatePointCloudSettings());
902
- this.ui.maxDepth.addEventListener('input', () => this.updatePointCloudSettings());
903
- this.ui.showTrajectory.addEventListener('change', () => {
904
- this.trajectories.forEach(trajectory => {
905
- trajectory.visible = this.ui.showTrajectory.checked;
906
- });
907
- });
908
-
909
- this.ui.enableRichTrail.addEventListener('change', () => {
910
- this.ui.tailOpacityContainer.style.display = this.ui.enableRichTrail.checked ? 'flex' : 'none';
911
- this.updateTrajectories(this.currentFrame);
912
- });
913
-
914
- this.ui.trajectoryLineWidth.addEventListener('input', () => this.updateTrajectorySettings());
915
- this.ui.trajectoryBallSize.addEventListener('input', () => this.updateTrajectorySettings());
916
- this.ui.trajectoryHistory.addEventListener('input', () => {
917
- this.updateTrajectories(this.currentFrame);
918
- });
919
- this.ui.trajectoryFade.addEventListener('input', () => {
920
- this.updateTrajectories(this.currentFrame);
921
- });
922
-
923
- this.ui.resetViewBtn.addEventListener('click', () => this.resetView());
924
-
925
- const resetSettingsBtn = document.getElementById('reset-settings-btn');
926
- if (resetSettingsBtn) {
927
- resetSettingsBtn.addEventListener('click', () => this.resetSettings());
928
- }
929
-
930
- document.addEventListener('keydown', (e) => {
931
- if (e.key === 'Escape' && this.ui.settingsPanel.classList.contains('visible')) {
932
- this.ui.settingsPanel.classList.remove('visible');
933
- this.ui.settingsToggleBtn.classList.remove('active');
934
- }
935
- });
936
-
937
- if (this.ui.settingsToggleBtn) {
938
- this.ui.settingsToggleBtn.addEventListener('click', () => {
939
- const isVisible = this.ui.settingsPanel.classList.toggle('visible');
940
- this.ui.settingsToggleBtn.classList.toggle('active', isVisible);
941
-
942
- if (isVisible) {
943
- const panelRect = this.ui.settingsPanel.getBoundingClientRect();
944
- const viewportHeight = window.innerHeight;
945
-
946
- if (panelRect.bottom > viewportHeight) {
947
- this.ui.settingsPanel.style.bottom = 'auto';
948
- this.ui.settingsPanel.style.top = '80px';
949
- }
950
- }
951
- });
952
- }
953
-
954
- if (this.ui.frustumSize) {
955
- this.ui.frustumSize.addEventListener('input', () => this.updateFrustumDimensions());
956
- }
957
-
958
- this.makeElementDraggable(this.ui.settingsPanel);
959
-
960
- if (this.ui.hideSettingsBtn && this.ui.showSettingsBtn && this.ui.settingsPanel) {
961
- this.ui.hideSettingsBtn.addEventListener('click', () => {
962
- this.ui.settingsPanel.classList.add('is-hidden');
963
- this.ui.showSettingsBtn.style.display = 'flex';
964
- });
965
-
966
- this.ui.showSettingsBtn.addEventListener('click', () => {
967
- this.ui.settingsPanel.classList.remove('is-hidden');
968
- this.ui.showSettingsBtn.style.display = 'none';
969
- });
970
- }
971
- }
972
-
973
- makeElementDraggable(element) {
974
- let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
975
-
976
- const dragHandle = element.querySelector('h2');
977
-
978
- if (dragHandle) {
979
- dragHandle.onmousedown = dragMouseDown;
980
- dragHandle.title = "Drag to move panel";
981
- } else {
982
- element.onmousedown = dragMouseDown;
983
- }
984
-
985
- function dragMouseDown(e) {
986
- e = e || window.event;
987
- e.preventDefault();
988
- pos3 = e.clientX;
989
- pos4 = e.clientY;
990
- document.onmouseup = closeDragElement;
991
- document.onmousemove = elementDrag;
992
-
993
- element.classList.add('dragging');
994
- }
995
-
996
- function elementDrag(e) {
997
- e = e || window.event;
998
- e.preventDefault();
999
- pos1 = pos3 - e.clientX;
1000
- pos2 = pos4 - e.clientY;
1001
- pos3 = e.clientX;
1002
- pos4 = e.clientY;
1003
-
1004
- const newTop = element.offsetTop - pos2;
1005
- const newLeft = element.offsetLeft - pos1;
1006
-
1007
- const viewportWidth = window.innerWidth;
1008
- const viewportHeight = window.innerHeight;
1009
-
1010
- const panelRect = element.getBoundingClientRect();
1011
-
1012
- const maxTop = viewportHeight - 50;
1013
- const maxLeft = viewportWidth - 50;
1014
-
1015
- element.style.top = Math.min(Math.max(newTop, 0), maxTop) + "px";
1016
- element.style.left = Math.min(Math.max(newLeft, 0), maxLeft) + "px";
1017
-
1018
- // Remove bottom/right settings when dragging
1019
- element.style.bottom = 'auto';
1020
- element.style.right = 'auto';
1021
- }
1022
-
1023
- function closeDragElement() {
1024
- document.onmouseup = null;
1025
- document.onmousemove = null;
1026
-
1027
- element.classList.remove('dragging');
1028
- }
1029
- }
1030
-
1031
- async loadData() {
1032
- try {
1033
- // this.ui.loadingText.textContent = "Loading binary data...";
1034
-
1035
- let arrayBuffer;
1036
-
1037
- if (window.embeddedBase64) {
1038
- // Base64 embedded path
1039
- const binaryString = atob(window.embeddedBase64);
1040
- const len = binaryString.length;
1041
- const bytes = new Uint8Array(len);
1042
- for (let i = 0; i < len; i++) {
1043
- bytes[i] = binaryString.charCodeAt(i);
1044
- }
1045
- arrayBuffer = bytes.buffer;
1046
- } else {
1047
- // Default fetch path (fallback)
1048
- const urlParams = new URLSearchParams(window.location.search);
1049
- const dataPath = urlParams.get('data') || 'data.bin';
1050
-
1051
- const response = await fetch(dataPath);
1052
- if (!response.ok) throw new Error(`Failed to load ${dataPath}`);
1053
- arrayBuffer = await response.arrayBuffer();
1054
- }
1055
-
1056
- const dataView = new DataView(arrayBuffer);
1057
- const headerLen = dataView.getUint32(0, true);
1058
-
1059
- const headerText = new TextDecoder("utf-8").decode(arrayBuffer.slice(4, 4 + headerLen));
1060
- const header = JSON.parse(headerText);
1061
-
1062
- const compressedBlob = new Uint8Array(arrayBuffer, 4 + headerLen);
1063
- const decompressed = pako.inflate(compressedBlob).buffer;
1064
-
1065
- const arrays = {};
1066
- for (const key in header) {
1067
- if (key === "meta") continue;
1068
-
1069
- const meta = header[key];
1070
- const { dtype, shape, offset, length } = meta;
1071
- const slice = decompressed.slice(offset, offset + length);
1072
-
1073
- let typedArray;
1074
- switch (dtype) {
1075
- case "uint8": typedArray = new Uint8Array(slice); break;
1076
- case "uint16": typedArray = new Uint16Array(slice); break;
1077
- case "float32": typedArray = new Float32Array(slice); break;
1078
- case "float64": typedArray = new Float64Array(slice); break;
1079
- default: throw new Error(`Unknown dtype: ${dtype}`);
1080
- }
1081
-
1082
- arrays[key] = { data: typedArray, shape: shape };
1083
- }
1084
-
1085
- this.data = arrays;
1086
- this.config = header.meta;
1087
-
1088
- this.initCameraWithCorrectFOV();
1089
- this.ui.loadingText.textContent = "Creating point cloud...";
1090
-
1091
- this.initPointCloud();
1092
- this.initTrajectories();
1093
-
1094
- setTimeout(() => {
1095
- this.ui.loadingOverlay.classList.add('fade-out');
1096
- this.ui.statusBar.classList.add('hidden');
1097
- this.startAnimation();
1098
- }, 500);
1099
- } catch (error) {
1100
- console.error("Error loading data:", error);
1101
- this.ui.statusBar.textContent = `Error: ${error.message}`;
1102
- // this.ui.loadingText.textContent = `Error loading data: ${error.message}`;
1103
- }
1104
- }
1105
-
1106
- initPointCloud() {
1107
- const numPoints = this.config.resolution[0] * this.config.resolution[1];
1108
- const positions = new Float32Array(numPoints * 3);
1109
- const colors = new Float32Array(numPoints * 3);
1110
-
1111
- const geometry = new THREE.BufferGeometry();
1112
- geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3).setUsage(THREE.DynamicDrawUsage));
1113
- geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage));
1114
-
1115
- const pointSize = parseFloat(this.ui.pointSize.value) || this.defaultSettings.pointSize;
1116
- const pointOpacity = parseFloat(this.ui.pointOpacity.value) || this.defaultSettings.pointOpacity;
1117
-
1118
- const material = new THREE.PointsMaterial({
1119
- size: pointSize,
1120
- vertexColors: true,
1121
- transparent: true,
1122
- opacity: pointOpacity,
1123
- sizeAttenuation: true
1124
- });
1125
-
1126
- this.pointCloud = new THREE.Points(geometry, material);
1127
- this.scene.add(this.pointCloud);
1128
- }
1129
-
1130
- initTrajectories() {
1131
- if (!this.data.trajectories) return;
1132
-
1133
- this.trajectories.forEach(trajectory => {
1134
- if (trajectory.userData.lineSegments) {
1135
- trajectory.userData.lineSegments.forEach(segment => {
1136
- segment.geometry.dispose();
1137
- segment.material.dispose();
1138
- });
1139
- }
1140
- this.scene.remove(trajectory);
1141
- });
1142
- this.trajectories = [];
1143
-
1144
- const shape = this.data.trajectories.shape;
1145
- if (!shape || shape.length < 2) return;
1146
-
1147
- const [totalFrames, numTrajectories] = shape;
1148
- const palette = this.createColorPalette(numTrajectories);
1149
- const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
1150
- const maxHistory = 500; // Max value of the history slider, for the object pool
1151
-
1152
- for (let i = 0; i < numTrajectories; i++) {
1153
- const trajectoryGroup = new THREE.Group();
1154
-
1155
- const ballSize = parseFloat(this.ui.trajectoryBallSize.value);
1156
- const sphereGeometry = new THREE.SphereGeometry(ballSize, 16, 16);
1157
- const sphereMaterial = new THREE.MeshBasicMaterial({ color: palette[i], transparent: true });
1158
- const positionMarker = new THREE.Mesh(sphereGeometry, sphereMaterial);
1159
- trajectoryGroup.add(positionMarker);
1160
-
1161
- // High-Performance Line (default)
1162
- const simpleLineGeometry = new THREE.BufferGeometry();
1163
- const simpleLinePositions = new Float32Array(maxHistory * 3);
1164
- simpleLineGeometry.setAttribute('position', new THREE.BufferAttribute(simpleLinePositions, 3).setUsage(THREE.DynamicDrawUsage));
1165
- const simpleLine = new THREE.Line(simpleLineGeometry, new THREE.LineBasicMaterial({ color: palette[i] }));
1166
- simpleLine.frustumCulled = false;
1167
- trajectoryGroup.add(simpleLine);
1168
-
1169
- // High-Quality Line Segments (for rich trail)
1170
- const lineSegments = [];
1171
- const lineWidth = parseFloat(this.ui.trajectoryLineWidth.value);
1172
-
1173
- // Create a pool of line segment objects
1174
- for (let j = 0; j < maxHistory - 1; j++) {
1175
- const lineGeometry = new THREE.LineGeometry();
1176
- lineGeometry.setPositions([0, 0, 0, 0, 0, 0]);
1177
- const lineMaterial = new THREE.LineMaterial({
1178
- color: palette[i],
1179
- linewidth: lineWidth,
1180
- resolution: resolution,
1181
- transparent: true,
1182
- depthWrite: false, // Correctly handle transparency
1183
- opacity: 0
1184
- });
1185
- const segment = new THREE.Line2(lineGeometry, lineMaterial);
1186
- segment.frustumCulled = false;
1187
- segment.visible = false; // Start with all segments hidden
1188
- trajectoryGroup.add(segment);
1189
- lineSegments.push(segment);
1190
- }
1191
-
1192
- trajectoryGroup.userData = {
1193
- marker: positionMarker,
1194
- simpleLine: simpleLine,
1195
- lineSegments: lineSegments,
1196
- color: palette[i]
1197
- };
1198
-
1199
- this.scene.add(trajectoryGroup);
1200
- this.trajectories.push(trajectoryGroup);
1201
- }
1202
-
1203
- const showTrajectory = this.ui.showTrajectory.checked;
1204
- this.trajectories.forEach(trajectory => trajectory.visible = showTrajectory);
1205
- }
1206
-
1207
- createColorPalette(count) {
1208
- const colors = [];
1209
- const hueStep = 360 / count;
1210
-
1211
- for (let i = 0; i < count; i++) {
1212
- const hue = (i * hueStep) % 360;
1213
- const color = new THREE.Color().setHSL(hue / 360, 0.8, 0.6);
1214
- colors.push(color);
1215
- }
1216
-
1217
- return colors;
1218
- }
1219
-
1220
- updatePointCloud(frameIndex) {
1221
- if (!this.data || !this.pointCloud) return;
1222
-
1223
- const positions = this.pointCloud.geometry.attributes.position.array;
1224
- const colors = this.pointCloud.geometry.attributes.color.array;
1225
-
1226
- const rgbVideo = this.data.rgb_video;
1227
- const depthsRgb = this.data.depths_rgb;
1228
- const intrinsics = this.data.intrinsics;
1229
- const invExtrinsics = this.data.inv_extrinsics;
1230
-
1231
- const width = this.config.resolution[0];
1232
- const height = this.config.resolution[1];
1233
- const numPoints = width * height;
1234
-
1235
- const K = this.get3x3Matrix(intrinsics.data, intrinsics.shape, frameIndex);
1236
- const fx = K[0][0], fy = K[1][1], cx = K[0][2], cy = K[1][2];
1237
-
1238
- const invExtrMat = this.get4x4Matrix(invExtrinsics.data, invExtrinsics.shape, frameIndex);
1239
- const transform = this.getTransformElements(invExtrMat);
1240
-
1241
- const rgbFrame = this.getFrame(rgbVideo.data, rgbVideo.shape, frameIndex);
1242
- const depthFrame = this.getFrame(depthsRgb.data, depthsRgb.shape, frameIndex);
1243
-
1244
- const maxDepth = parseFloat(this.ui.maxDepth.value) || 10.0;
1245
-
1246
- let validPointCount = 0;
1247
-
1248
- for (let i = 0; i < numPoints; i++) {
1249
- const xPix = i % width;
1250
- const yPix = Math.floor(i / width);
1251
-
1252
- const d0 = depthFrame[i * 3];
1253
- const d1 = depthFrame[i * 3 + 1];
1254
- const depthEncoded = d0 | (d1 << 8);
1255
- const depthValue = (depthEncoded / ((1 << 16) - 1)) *
1256
- (this.config.depthRange[1] - this.config.depthRange[0]) +
1257
- this.config.depthRange[0];
1258
-
1259
- if (depthValue === 0 || depthValue > maxDepth) {
1260
- continue;
1261
- }
1262
-
1263
- const X = ((xPix - cx) * depthValue) / fx;
1264
- const Y = ((yPix - cy) * depthValue) / fy;
1265
- const Z = depthValue;
1266
-
1267
- const tx = transform.m11 * X + transform.m12 * Y + transform.m13 * Z + transform.m14;
1268
- const ty = transform.m21 * X + transform.m22 * Y + transform.m23 * Z + transform.m24;
1269
- const tz = transform.m31 * X + transform.m32 * Y + transform.m33 * Z + transform.m34;
1270
-
1271
- const index = validPointCount * 3;
1272
- positions[index] = tx;
1273
- positions[index + 1] = -ty;
1274
- positions[index + 2] = -tz;
1275
-
1276
- colors[index] = rgbFrame[i * 3] / 255;
1277
- colors[index + 1] = rgbFrame[i * 3 + 1] / 255;
1278
- colors[index + 2] = rgbFrame[i * 3 + 2] / 255;
1279
-
1280
- validPointCount++;
1281
- }
1282
-
1283
- this.pointCloud.geometry.setDrawRange(0, validPointCount);
1284
- this.pointCloud.geometry.attributes.position.needsUpdate = true;
1285
- this.pointCloud.geometry.attributes.color.needsUpdate = true;
1286
- this.pointCloud.geometry.computeBoundingSphere(); // Important for camera culling
1287
-
1288
- this.updateTrajectories(frameIndex);
1289
-
1290
- const progress = (frameIndex + 1) / this.config.totalFrames;
1291
- this.ui.progress.style.width = `${progress * 100}%`;
1292
-
1293
- if (this.ui.frameCounter && this.config.totalFrames) {
1294
- this.ui.frameCounter.textContent = `Frame ${frameIndex} / ${this.config.totalFrames - 1}`;
1295
- }
1296
-
1297
- this.updateCameraFrustum(frameIndex);
1298
- }
1299
-
1300
- updateTrajectories(frameIndex) {
1301
- if (!this.data.trajectories || this.trajectories.length === 0) return;
1302
-
1303
- const trajectoryData = this.data.trajectories.data;
1304
- const [totalFrames, numTrajectories] = this.data.trajectories.shape;
1305
- const historyFrames = parseInt(this.ui.trajectoryHistory.value);
1306
- const tailOpacity = parseFloat(this.ui.trajectoryFade.value);
1307
-
1308
- const isRichMode = this.ui.enableRichTrail.checked;
1309
-
1310
- for (let i = 0; i < numTrajectories; i++) {
1311
- const trajectoryGroup = this.trajectories[i];
1312
- const { marker, simpleLine, lineSegments } = trajectoryGroup.userData;
1313
-
1314
- const currentPos = new THREE.Vector3();
1315
- const currentOffset = (frameIndex * numTrajectories + i) * 3;
1316
-
1317
- currentPos.x = trajectoryData[currentOffset];
1318
- currentPos.y = -trajectoryData[currentOffset + 1];
1319
- currentPos.z = -trajectoryData[currentOffset + 2];
1320
-
1321
- marker.position.copy(currentPos);
1322
- marker.material.opacity = 1.0;
1323
-
1324
- const historyToShow = Math.min(historyFrames, frameIndex + 1);
1325
-
1326
- if (isRichMode) {
1327
- // --- High-Quality Mode ---
1328
- simpleLine.visible = false;
1329
-
1330
- for (let j = 0; j < lineSegments.length; j++) {
1331
- const segment = lineSegments[j];
1332
- if (j < historyToShow - 1) {
1333
- const headFrame = frameIndex - j;
1334
- const tailFrame = frameIndex - j - 1;
1335
- const headOffset = (headFrame * numTrajectories + i) * 3;
1336
- const tailOffset = (tailFrame * numTrajectories + i) * 3;
1337
- const positions = [
1338
- trajectoryData[headOffset], -trajectoryData[headOffset + 1], -trajectoryData[headOffset + 2],
1339
- trajectoryData[tailOffset], -trajectoryData[tailOffset + 1], -trajectoryData[tailOffset + 2]
1340
- ];
1341
- segment.geometry.setPositions(positions);
1342
- const headOpacity = 1.0;
1343
- const normalizedAge = j / Math.max(1, historyToShow - 2);
1344
- const alpha = headOpacity - (headOpacity - tailOpacity) * normalizedAge;
1345
- segment.material.opacity = Math.max(0, alpha);
1346
- segment.visible = true;
1347
- } else {
1348
- segment.visible = false;
1349
- }
1350
- }
1351
- } else {
1352
- // --- Performance Mode ---
1353
- lineSegments.forEach(s => s.visible = false);
1354
- simpleLine.visible = true;
1355
-
1356
- const positions = simpleLine.geometry.attributes.position.array;
1357
- for (let j = 0; j < historyToShow; j++) {
1358
- const historyFrame = Math.max(0, frameIndex - j);
1359
- const offset = (historyFrame * numTrajectories + i) * 3;
1360
- positions[j * 3] = trajectoryData[offset];
1361
- positions[j * 3 + 1] = -trajectoryData[offset + 1];
1362
- positions[j * 3 + 2] = -trajectoryData[offset + 2];
1363
- }
1364
- simpleLine.geometry.setDrawRange(0, historyToShow);
1365
- simpleLine.geometry.attributes.position.needsUpdate = true;
1366
- }
1367
- }
1368
- }
1369
-
1370
- updateTrajectorySettings() {
1371
- if (!this.trajectories || this.trajectories.length === 0) return;
1372
-
1373
- const ballSize = parseFloat(this.ui.trajectoryBallSize.value);
1374
- const lineWidth = parseFloat(this.ui.trajectoryLineWidth.value);
1375
-
1376
- this.trajectories.forEach(trajectoryGroup => {
1377
- const { marker, lineSegments } = trajectoryGroup.userData;
1378
-
1379
- marker.geometry.dispose();
1380
- marker.geometry = new THREE.SphereGeometry(ballSize, 16, 16);
1381
-
1382
- // Line width only affects rich mode
1383
- lineSegments.forEach(segment => {
1384
- if (segment.material) {
1385
- segment.material.linewidth = lineWidth;
1386
- }
1387
- });
1388
- });
1389
-
1390
- this.updateTrajectories(this.currentFrame);
1391
- }
1392
-
1393
- getDepthColor(normalizedDepth) {
1394
- const hue = (1 - normalizedDepth) * 240 / 360;
1395
- const color = new THREE.Color().setHSL(hue, 1.0, 0.5);
1396
- return color;
1397
- }
1398
-
1399
- getFrame(typedArray, shape, frameIndex) {
1400
- const [T, H, W, C] = shape;
1401
- const frameSize = H * W * C;
1402
- const offset = frameIndex * frameSize;
1403
- return typedArray.subarray(offset, offset + frameSize);
1404
- }
1405
-
1406
- get3x3Matrix(typedArray, shape, frameIndex) {
1407
- const frameSize = 9;
1408
- const offset = frameIndex * frameSize;
1409
- const K = [];
1410
- for (let i = 0; i < 3; i++) {
1411
- const row = [];
1412
- for (let j = 0; j < 3; j++) {
1413
- row.push(typedArray[offset + i * 3 + j]);
1414
- }
1415
- K.push(row);
1416
- }
1417
- return K;
1418
- }
1419
-
1420
- get4x4Matrix(typedArray, shape, frameIndex) {
1421
- const frameSize = 16;
1422
- const offset = frameIndex * frameSize;
1423
- const M = [];
1424
- for (let i = 0; i < 4; i++) {
1425
- const row = [];
1426
- for (let j = 0; j < 4; j++) {
1427
- row.push(typedArray[offset + i * 4 + j]);
1428
- }
1429
- M.push(row);
1430
- }
1431
- return M;
1432
- }
1433
-
1434
- getTransformElements(matrix) {
1435
- return {
1436
- m11: matrix[0][0], m12: matrix[0][1], m13: matrix[0][2], m14: matrix[0][3],
1437
- m21: matrix[1][0], m22: matrix[1][1], m23: matrix[1][2], m24: matrix[1][3],
1438
- m31: matrix[2][0], m32: matrix[2][1], m33: matrix[2][2], m34: matrix[2][3]
1439
- };
1440
- }
1441
-
1442
- togglePlayback() {
1443
- this.isPlaying = !this.isPlaying;
1444
-
1445
- const playIcon = document.getElementById('play-icon');
1446
- const pauseIcon = document.getElementById('pause-icon');
1447
-
1448
- if (this.isPlaying) {
1449
- playIcon.style.display = 'none';
1450
- pauseIcon.style.display = 'block';
1451
- this.lastFrameTime = performance.now();
1452
- } else {
1453
- playIcon.style.display = 'block';
1454
- pauseIcon.style.display = 'none';
1455
- }
1456
- }
1457
-
1458
- cyclePlaybackSpeed() {
1459
- const speeds = [0.5, 1, 2, 4, 8];
1460
- const speedRates = speeds.map(s => s * this.config.baseFrameRate);
1461
-
1462
- let currentIndex = 0;
1463
- const normalizedSpeed = this.playbackSpeed / this.config.baseFrameRate;
1464
-
1465
- for (let i = 0; i < speeds.length; i++) {
1466
- if (Math.abs(normalizedSpeed - speeds[i]) < Math.abs(normalizedSpeed - speeds[currentIndex])) {
1467
- currentIndex = i;
1468
- }
1469
- }
1470
-
1471
- const nextIndex = (currentIndex + 1) % speeds.length;
1472
- this.playbackSpeed = speedRates[nextIndex];
1473
- this.ui.speedBtn.textContent = `${speeds[nextIndex]}x`;
1474
-
1475
- if (speeds[nextIndex] === 1) {
1476
- this.ui.speedBtn.classList.remove('active');
1477
- } else {
1478
- this.ui.speedBtn.classList.add('active');
1479
- }
1480
- }
1481
-
1482
- seekTo(position) {
1483
- const frameIndex = Math.floor(position * this.config.totalFrames);
1484
- this.currentFrame = Math.max(0, Math.min(frameIndex, this.config.totalFrames - 1));
1485
- this.updatePointCloud(this.currentFrame);
1486
- }
1487
-
1488
- updatePointCloudSettings() {
1489
- if (!this.pointCloud) return;
1490
-
1491
- const size = parseFloat(this.ui.pointSize.value);
1492
- const opacity = parseFloat(this.ui.pointOpacity.value);
1493
-
1494
- this.pointCloud.material.size = size;
1495
- this.pointCloud.material.opacity = opacity;
1496
- this.pointCloud.material.needsUpdate = true;
1497
-
1498
- this.updatePointCloud(this.currentFrame);
1499
- }
1500
-
1501
- updateControls() {
1502
- if (!this.controls) return;
1503
- this.controls.update();
1504
- }
1505
-
1506
- resetView() {
1507
- if (!this.camera || !this.controls) return;
1508
-
1509
- // Reset camera position
1510
- this.camera.position.set(0, 0, this.config.cameraZ || 0);
1511
-
1512
- // Reset controls
1513
- this.controls.reset();
1514
-
1515
- // Set target slightly in front of camera
1516
- this.controls.target.set(0, 0, -1);
1517
- this.controls.update();
1518
-
1519
- // Show status message
1520
- this.ui.statusBar.textContent = "View reset";
1521
- this.ui.statusBar.classList.remove('hidden');
1522
-
1523
- // Hide status message after a few seconds
1524
- setTimeout(() => {
1525
- this.ui.statusBar.classList.add('hidden');
1526
- }, 3000);
1527
- }
1528
-
1529
- onWindowResize() {
1530
- if (!this.camera || !this.renderer) return;
1531
-
1532
- const windowAspect = window.innerWidth / window.innerHeight;
1533
- this.camera.aspect = windowAspect;
1534
- this.camera.updateProjectionMatrix();
1535
- this.renderer.setSize(window.innerWidth, window.innerHeight);
1536
-
1537
- if (this.trajectories && this.trajectories.length > 0) {
1538
- const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
1539
- this.trajectories.forEach(trajectory => {
1540
- const { lineSegments } = trajectory.userData;
1541
- if (lineSegments && lineSegments.length > 0) {
1542
- lineSegments.forEach(segment => {
1543
- if (segment.material && segment.material.resolution) {
1544
- segment.material.resolution.copy(resolution);
1545
- }
1546
- });
1547
- }
1548
- });
1549
- }
1550
-
1551
- if (this.cameraFrustum) {
1552
- const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
1553
- this.cameraFrustum.children.forEach(line => {
1554
- if (line.material && line.material.resolution) {
1555
- line.material.resolution.copy(resolution);
1556
- }
1557
- });
1558
- }
1559
- }
1560
-
1561
- startAnimation() {
1562
- this.isPlaying = true;
1563
- this.lastFrameTime = performance.now();
1564
-
1565
- this.camera.position.set(0, 0, this.config.cameraZ || 0);
1566
- this.controls.target.set(0, 0, -1);
1567
- this.controls.update();
1568
-
1569
- this.playbackSpeed = this.config.baseFrameRate;
1570
-
1571
- document.getElementById('play-icon').style.display = 'none';
1572
- document.getElementById('pause-icon').style.display = 'block';
1573
-
1574
- this.animate();
1575
- }
1576
-
1577
- animate() {
1578
- requestAnimationFrame(() => this.animate());
1579
-
1580
- if (this.controls) {
1581
- this.controls.update();
1582
- }
1583
-
1584
- if (this.isPlaying && this.data) {
1585
- const now = performance.now();
1586
- const delta = (now - this.lastFrameTime) / 1000;
1587
-
1588
- const framesToAdvance = Math.floor(delta * this.config.baseFrameRate * this.playbackSpeed);
1589
- if (framesToAdvance > 0) {
1590
- this.currentFrame = (this.currentFrame + framesToAdvance) % this.config.totalFrames;
1591
- this.lastFrameTime = now;
1592
- this.updatePointCloud(this.currentFrame);
1593
- }
1594
- }
1595
-
1596
- if (this.renderer && this.scene && this.camera) {
1597
- this.renderer.render(this.scene, this.camera);
1598
- }
1599
- }
1600
-
1601
- initCameraWithCorrectFOV() {
1602
- const fov = this.config.fov || 60;
1603
-
1604
- const windowAspect = window.innerWidth / window.innerHeight;
1605
-
1606
- this.camera = new THREE.PerspectiveCamera(
1607
- fov,
1608
- windowAspect,
1609
- 0.1,
1610
- 10000
1611
- );
1612
-
1613
- this.controls.object = this.camera;
1614
- this.controls.update();
1615
-
1616
- this.initCameraFrustum();
1617
- }
1618
-
1619
- initCameraFrustum() {
1620
- this.cameraFrustum = new THREE.Group();
1621
-
1622
- this.scene.add(this.cameraFrustum);
1623
-
1624
- this.initCameraFrustumGeometry();
1625
-
1626
- const showCameraFrustum = this.ui.showCameraFrustum ? this.ui.showCameraFrustum.checked : (this.defaultSettings ? this.defaultSettings.showCameraFrustum : false);
1627
-
1628
- this.cameraFrustum.visible = showCameraFrustum;
1629
- }
1630
-
1631
- initCameraFrustumGeometry() {
1632
- const fov = this.config.fov || 60;
1633
- const originalAspect = this.config.original_aspect_ratio || 1.33;
1634
-
1635
- const size = parseFloat(this.ui.frustumSize.value) || this.defaultSettings.frustumSize;
1636
-
1637
- const halfHeight = Math.tan(THREE.MathUtils.degToRad(fov / 2)) * size;
1638
- const halfWidth = halfHeight * originalAspect;
1639
-
1640
- const vertices = [
1641
- new THREE.Vector3(0, 0, 0),
1642
- new THREE.Vector3(-halfWidth, -halfHeight, size),
1643
- new THREE.Vector3(halfWidth, -halfHeight, size),
1644
- new THREE.Vector3(halfWidth, halfHeight, size),
1645
- new THREE.Vector3(-halfWidth, halfHeight, size)
1646
- ];
1647
-
1648
- const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
1649
-
1650
- const linePairs = [
1651
- [1, 2], [2, 3], [3, 4], [4, 1],
1652
- [0, 1], [0, 2], [0, 3], [0, 4]
1653
- ];
1654
-
1655
- const colors = {
1656
- edge: new THREE.Color(0x3366ff),
1657
- ray: new THREE.Color(0x33cc66)
1658
- };
1659
-
1660
- linePairs.forEach((pair, index) => {
1661
- const positions = [
1662
- vertices[pair[0]].x, vertices[pair[0]].y, vertices[pair[0]].z,
1663
- vertices[pair[1]].x, vertices[pair[1]].y, vertices[pair[1]].z
1664
- ];
1665
-
1666
- const lineGeometry = new THREE.LineGeometry();
1667
- lineGeometry.setPositions(positions);
1668
-
1669
- let color = index < 4 ? colors.edge : colors.ray;
1670
-
1671
- const lineMaterial = new THREE.LineMaterial({
1672
- color: color,
1673
- linewidth: 2,
1674
- resolution: resolution,
1675
- dashed: false
1676
- });
1677
-
1678
- const line = new THREE.Line2(lineGeometry, lineMaterial);
1679
- this.cameraFrustum.add(line);
1680
- });
1681
- }
1682
-
1683
- updateCameraFrustum(frameIndex) {
1684
- if (!this.cameraFrustum || !this.data) return;
1685
-
1686
- const invExtrinsics = this.data.inv_extrinsics;
1687
- if (!invExtrinsics) return;
1688
-
1689
- const invExtrMat = this.get4x4Matrix(invExtrinsics.data, invExtrinsics.shape, frameIndex);
1690
-
1691
- const matrix = new THREE.Matrix4();
1692
- matrix.set(
1693
- invExtrMat[0][0], invExtrMat[0][1], invExtrMat[0][2], invExtrMat[0][3],
1694
- invExtrMat[1][0], invExtrMat[1][1], invExtrMat[1][2], invExtrMat[1][3],
1695
- invExtrMat[2][0], invExtrMat[2][1], invExtrMat[2][2], invExtrMat[2][3],
1696
- invExtrMat[3][0], invExtrMat[3][1], invExtrMat[3][2], invExtrMat[3][3]
1697
- );
1698
-
1699
- const position = new THREE.Vector3();
1700
- position.setFromMatrixPosition(matrix);
1701
-
1702
- const rotMatrix = new THREE.Matrix4().extractRotation(matrix);
1703
-
1704
- const coordinateCorrection = new THREE.Matrix4().makeRotationX(Math.PI);
1705
-
1706
- const finalRotation = new THREE.Matrix4().multiplyMatrices(coordinateCorrection, rotMatrix);
1707
-
1708
- const quaternion = new THREE.Quaternion();
1709
- quaternion.setFromRotationMatrix(finalRotation);
1710
-
1711
- position.y = -position.y;
1712
- position.z = -position.z;
1713
-
1714
- this.cameraFrustum.position.copy(position);
1715
- this.cameraFrustum.quaternion.copy(quaternion);
1716
-
1717
- const showCameraFrustum = this.ui.showCameraFrustum ? this.ui.showCameraFrustum.checked : this.defaultSettings.showCameraFrustum;
1718
-
1719
- if (this.cameraFrustum.visible !== showCameraFrustum) {
1720
- this.cameraFrustum.visible = showCameraFrustum;
1721
- }
1722
-
1723
- const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
1724
- this.cameraFrustum.children.forEach(line => {
1725
- if (line.material && line.material.resolution) {
1726
- line.material.resolution.copy(resolution);
1727
- }
1728
- });
1729
- }
1730
-
1731
- updateFrustumDimensions() {
1732
- if (!this.cameraFrustum) return;
1733
-
1734
- while(this.cameraFrustum.children.length > 0) {
1735
- const child = this.cameraFrustum.children[0];
1736
- if (child.geometry) child.geometry.dispose();
1737
- if (child.material) child.material.dispose();
1738
- this.cameraFrustum.remove(child);
1739
- }
1740
-
1741
- this.initCameraFrustumGeometry();
1742
-
1743
- this.updateCameraFrustum(this.currentFrame);
1744
- }
1745
-
1746
- resetSettings() {
1747
- if (!this.defaultSettings) return;
1748
-
1749
- this.applyDefaultSettings();
1750
-
1751
- this.updatePointCloudSettings();
1752
- this.updateTrajectorySettings();
1753
- this.updateFrustumDimensions();
1754
-
1755
- this.ui.statusBar.textContent = "Settings reset to defaults";
1756
- this.ui.statusBar.classList.remove('hidden');
1757
-
1758
- setTimeout(() => {
1759
- this.ui.statusBar.classList.add('hidden');
1760
- }, 3000);
1761
- }
1762
- }
1763
-
1764
- window.addEventListener('DOMContentLoaded', () => {
1765
- new PointCloudVisualizer();
1766
- });
1767
- </script>
1768
- </body>
1769
- </html>