Spaces:
Sleeping
Sleeping
Zhen Ye Claude Opus 4.6 (1M context) commited on
Commit ·
2c64c05
1
Parent(s): 100e159
refactor: remove /app frontend, consolidate on demo as sole UI
Browse filesThe demo (/) is the primary UI with command bar, state machine, event
log, and reasoning trace. The /app frontend was a secondary analyst tool
with less operational capability. All Tripo3D and inspection features
are already ported to the demo.
- Remove frontend/ directory and /app mount from app.py
- Update CLAUDE.md to reflect demo/ as the sole frontend
- Update no-cache middleware to target /demo instead of /app
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CLAUDE.md +14 -16
- app.py +1 -4
- demo/js/inspect.js +14 -0
- frontend/index.html +0 -310
- frontend/js/api/client.js +0 -221
- frontend/js/api/inspection-api.js +0 -228
- frontend/js/core/config.js +0 -15
- frontend/js/core/demo.js +0 -141
- frontend/js/core/gptMapping.js +0 -37
- frontend/js/core/physics.js +0 -18
- frontend/js/core/state.js +0 -92
- frontend/js/core/timeline.js +0 -167
- frontend/js/core/tracker.js +0 -288
- frontend/js/core/utils.js +0 -55
- frontend/js/core/video.js +0 -367
- frontend/js/init.js +0 -6
- frontend/js/main.js +0 -762
- frontend/js/ui/animations.js +0 -146
- frontend/js/ui/cards.js +0 -122
- frontend/js/ui/chat.js +0 -320
- frontend/js/ui/cursor.js +0 -90
- frontend/js/ui/explainability.js +0 -367
- frontend/js/ui/inspection-3d.js +0 -474
- frontend/js/ui/inspection-renders.js +0 -446
- frontend/js/ui/inspection.js +0 -520
- frontend/js/ui/logging.js +0 -55
- frontend/js/ui/overlays.js +0 -187
- frontend/style.css +0 -2028
- frontend/vendor/GLTFLoader.js +0 -0
- frontend/vendor/OrbitControls.js +0 -1101
- frontend/vendor/d3-hierarchy.v3.min.js +0 -2
- frontend/vendor/d3-path.v3.min.js +0 -2
- frontend/vendor/d3-shape.v3.min.js +0 -2
- frontend/vendor/three.min.js +0 -0
CLAUDE.md
CHANGED
|
@@ -92,24 +92,22 @@ Frontend (index.html) → POST /detect/async → background task → MJPEG strea
|
|
| 92 |
- **`background.py`** — `process_video_async()` coroutine dispatches to the right inference function
|
| 93 |
- **`streaming.py`** — MJPEG frame queue + `asyncio.Event` publisher; `publish_frame()` resizes to 640px
|
| 94 |
|
| 95 |
-
### Frontend (
|
| 96 |
|
| 97 |
-
Single-page
|
| 98 |
|
| 99 |
-
**
|
| 100 |
-
- `init.js` → bootstraps `window.
|
| 101 |
-
- `
|
| 102 |
-
- `
|
| 103 |
-
- `
|
| 104 |
-
- `
|
| 105 |
-
- `
|
| 106 |
-
- `
|
| 107 |
-
- `
|
| 108 |
-
- `
|
| 109 |
-
- `ui/logging.js` → system log, status indicators
|
| 110 |
-
- `main.js` → event wiring, app entry point
|
| 111 |
|
| 112 |
-
The frontend infers `mode` from
|
| 113 |
|
| 114 |
## Models
|
| 115 |
|
|
@@ -144,7 +142,7 @@ Single entry: key `depth` → `DepthAnythingV2Estimator`. Optional, enabled via
|
|
| 144 |
1. Create class in `models/detectors/` implementing `ObjectDetector.predict()` → `DetectionResult`
|
| 145 |
2. If weights need downloading, add `ensure_weights()` classmethod for thread-safe prefetch
|
| 146 |
3. Register in `models/model_loader.py` `_REGISTRY`
|
| 147 |
-
4. Add `<option>` to `
|
| 148 |
|
| 149 |
## Key Patterns
|
| 150 |
|
|
|
|
| 92 |
- **`background.py`** — `process_video_async()` coroutine dispatches to the right inference function
|
| 93 |
- **`streaming.py`** — MJPEG frame queue + `asyncio.Event` publisher; `publish_frame()` resizes to 640px
|
| 94 |
|
| 95 |
+
### Frontend (demo/)
|
| 96 |
|
| 97 |
+
Single-page command center UI served at `/` (mounted at `/demo`). No build step. Uses `window.ISR` global namespace.
|
| 98 |
|
| 99 |
+
**Key scripts:**
|
| 100 |
+
- `init.js` → bootstraps `window.ISR`, wires UI, initializes state machine
|
| 101 |
+
- `state-machine.js` → explicit FSM for UI flow (idle → detecting → playing → inspect)
|
| 102 |
+
- `api.js` → all backend API calls (`startDetection`, `fetchTracks`, `fetchPointCloud`, etc.)
|
| 103 |
+
- `real-backend.js` → streaming + polling + prefetch logic for live detection jobs
|
| 104 |
+
- `inspect.js` → 4-quadrant inspection panel (seg, edge, depth, 3D) with Tripo3D support
|
| 105 |
+
- `render.js` → canvas overlays for bounding boxes and tracks
|
| 106 |
+
- `ui.js` → panel layout, drawer tabs, command bar
|
| 107 |
+
- `analysis.js` → track analysis and timeline rendering
|
| 108 |
+
- `helpers.js` → viridis colormap, Sobel filter, RLE decode, utility functions
|
|
|
|
|
|
|
| 109 |
|
| 110 |
+
The frontend infers `mode` from the detector select element's `data-kind` attribute.
|
| 111 |
|
| 112 |
## Models
|
| 113 |
|
|
|
|
| 142 |
1. Create class in `models/detectors/` implementing `ObjectDetector.predict()` → `DetectionResult`
|
| 143 |
2. If weights need downloading, add `ensure_weights()` classmethod for thread-safe prefetch
|
| 144 |
3. Register in `models/model_loader.py` `_REGISTRY`
|
| 145 |
+
4. Add `<option>` to `demo/index.html` `#detectorSelect` with appropriate `data-kind`
|
| 146 |
|
| 147 |
## Key Patterns
|
| 148 |
|
app.py
CHANGED
|
@@ -101,16 +101,13 @@ async def add_no_cache_header(request: Request, call_next):
|
|
| 101 |
"""Ensure frontend assets are not cached by the browser (important for HF Spaces updates)."""
|
| 102 |
response = await call_next(request)
|
| 103 |
# Apply to all static files and the root page
|
| 104 |
-
if request.url.path.startswith("/
|
| 105 |
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
| 106 |
response.headers["Pragma"] = "no-cache"
|
| 107 |
response.headers["Expires"] = "0"
|
| 108 |
return response
|
| 109 |
|
| 110 |
app.mount("/demo", StaticFiles(directory="demo", html=True), name="demo")
|
| 111 |
-
_FRONTEND_DIR = Path(__file__).with_name("frontend")
|
| 112 |
-
if _FRONTEND_DIR.exists():
|
| 113 |
-
app.mount("/app", StaticFiles(directory=_FRONTEND_DIR, html=True), name="frontend")
|
| 114 |
|
| 115 |
# Valid detection modes
|
| 116 |
VALID_MODES = {"object_detection", "segmentation", "drone_detection"}
|
|
|
|
| 101 |
"""Ensure frontend assets are not cached by the browser (important for HF Spaces updates)."""
|
| 102 |
response = await call_next(request)
|
| 103 |
# Apply to all static files and the root page
|
| 104 |
+
if request.url.path.startswith("/demo") or request.url.path == "/":
|
| 105 |
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
| 106 |
response.headers["Pragma"] = "no-cache"
|
| 107 |
response.headers["Expires"] = "0"
|
| 108 |
return response
|
| 109 |
|
| 110 |
app.mount("/demo", StaticFiles(directory="demo", html=True), name="demo")
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
# Valid detection modes
|
| 113 |
VALID_MODES = {"object_detection", "segmentation", "drone_detection"}
|
demo/js/inspect.js
CHANGED
|
@@ -552,9 +552,15 @@ async function renderRealSegmentation(jobId, frameIdx, trackId) {
|
|
| 552 |
const dpr = window.devicePixelRatio || 1;
|
| 553 |
canvas.width = canvas.offsetWidth * dpr;
|
| 554 |
canvas.height = canvas.offsetHeight * dpr;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
try {
|
| 556 |
// Fetch the cropped frame image first (for overlay rendering)
|
| 557 |
const frameBlob = await ISR.fetchFrame(jobId, frameIdx, trackId);
|
|
|
|
| 558 |
const maskData = await ISR.fetchMask(jobId, frameIdx, trackId);
|
| 559 |
if (!maskData) { showQuadPlaceholder(ctx, canvas, 'MASK UNAVAILABLE'); if (metric) metric.textContent = '\u2014'; return; }
|
| 560 |
|
|
@@ -638,6 +644,10 @@ async function renderRealEdge(jobId, frameIdx, trackId) {
|
|
| 638 |
const dpr = window.devicePixelRatio || 1;
|
| 639 |
canvas.width = canvas.offsetWidth * dpr;
|
| 640 |
canvas.height = canvas.offsetHeight * dpr;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
try {
|
| 642 |
const blob = await ISR.fetchFrame(jobId, frameIdx, trackId);
|
| 643 |
if (!blob) { showQuadPlaceholder(ctx, canvas, 'FRAME UNAVAILABLE'); return; }
|
|
@@ -658,6 +668,10 @@ async function renderRealDepth(jobId, frameIdx, trackId) {
|
|
| 658 |
const dpr = window.devicePixelRatio || 1;
|
| 659 |
canvas.width = canvas.offsetWidth * dpr;
|
| 660 |
canvas.height = canvas.offsetHeight * dpr;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
try {
|
| 662 |
const depthData = await ISR.fetchDepth(jobId, frameIdx, trackId);
|
| 663 |
if (!depthData || !depthData.data) { showQuadPlaceholder(ctx, canvas, 'DEPTH UNAVAILABLE'); if (metric) metric.textContent = '\u2014'; return; }
|
|
|
|
| 552 |
const dpr = window.devicePixelRatio || 1;
|
| 553 |
canvas.width = canvas.offsetWidth * dpr;
|
| 554 |
canvas.height = canvas.offsetHeight * dpr;
|
| 555 |
+
if (canvas.width === 0 || canvas.height === 0) {
|
| 556 |
+
// Layout not ready — retry after next frame
|
| 557 |
+
requestAnimationFrame(() => renderRealSegmentation(jobId, frameIdx, trackId));
|
| 558 |
+
return;
|
| 559 |
+
}
|
| 560 |
try {
|
| 561 |
// Fetch the cropped frame image first (for overlay rendering)
|
| 562 |
const frameBlob = await ISR.fetchFrame(jobId, frameIdx, trackId);
|
| 563 |
+
if (!frameBlob) { showQuadPlaceholder(ctx, canvas, 'FRAME UNAVAILABLE'); if (metric) metric.textContent = '\u2014'; return; }
|
| 564 |
const maskData = await ISR.fetchMask(jobId, frameIdx, trackId);
|
| 565 |
if (!maskData) { showQuadPlaceholder(ctx, canvas, 'MASK UNAVAILABLE'); if (metric) metric.textContent = '\u2014'; return; }
|
| 566 |
|
|
|
|
| 644 |
const dpr = window.devicePixelRatio || 1;
|
| 645 |
canvas.width = canvas.offsetWidth * dpr;
|
| 646 |
canvas.height = canvas.offsetHeight * dpr;
|
| 647 |
+
if (canvas.width === 0 || canvas.height === 0) {
|
| 648 |
+
requestAnimationFrame(() => renderRealEdge(jobId, frameIdx, trackId));
|
| 649 |
+
return;
|
| 650 |
+
}
|
| 651 |
try {
|
| 652 |
const blob = await ISR.fetchFrame(jobId, frameIdx, trackId);
|
| 653 |
if (!blob) { showQuadPlaceholder(ctx, canvas, 'FRAME UNAVAILABLE'); return; }
|
|
|
|
| 668 |
const dpr = window.devicePixelRatio || 1;
|
| 669 |
canvas.width = canvas.offsetWidth * dpr;
|
| 670 |
canvas.height = canvas.offsetHeight * dpr;
|
| 671 |
+
if (canvas.width === 0 || canvas.height === 0) {
|
| 672 |
+
requestAnimationFrame(() => renderRealDepth(jobId, frameIdx, trackId));
|
| 673 |
+
return;
|
| 674 |
+
}
|
| 675 |
try {
|
| 676 |
const depthData = await ISR.fetchDepth(jobId, frameIdx, trackId);
|
| 677 |
if (!depthData || !depthData.data) { showQuadPlaceholder(ctx, canvas, 'DEPTH UNAVAILABLE'); if (metric) metric.textContent = '\u2014'; return; }
|
frontend/index.html
DELETED
|
@@ -1,310 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8" />
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<link rel="stylesheet" href="style.css?v=20260323">
|
| 8 |
-
<title>ISR Analytics</title>
|
| 9 |
-
</head>
|
| 10 |
-
|
| 11 |
-
<body>
|
| 12 |
-
<!-- Ambient animated background -->
|
| 13 |
-
<div id="ambientBg">
|
| 14 |
-
<div class="orb orb-1"></div>
|
| 15 |
-
<div class="orb orb-2"></div>
|
| 16 |
-
<div class="orb orb-3"></div>
|
| 17 |
-
</div>
|
| 18 |
-
|
| 19 |
-
<div id="app">
|
| 20 |
-
<header>
|
| 21 |
-
<div class="brand">
|
| 22 |
-
<div class="logo" aria-hidden="true"></div>
|
| 23 |
-
<div>
|
| 24 |
-
<h1>ISR Analytics</h1>
|
| 25 |
-
<div class="sub">Real-time video intelligence</div>
|
| 26 |
-
</div>
|
| 27 |
-
</div>
|
| 28 |
-
<div class="status-row">
|
| 29 |
-
<div class="pill">
|
| 30 |
-
<span class="dot" id="sys-dot"></span>
|
| 31 |
-
<span id="sys-status">Standby</span>
|
| 32 |
-
</div>
|
| 33 |
-
<div class="pill">
|
| 34 |
-
<span class="kbd">Live</span>
|
| 35 |
-
<span>Continuous tracking</span>
|
| 36 |
-
</div>
|
| 37 |
-
</div>
|
| 38 |
-
</header>
|
| 39 |
-
|
| 40 |
-
<div class="workspace">
|
| 41 |
-
<aside>
|
| 42 |
-
<div class="card">
|
| 43 |
-
<h2>Input</h2>
|
| 44 |
-
<div class="hint">Upload a video to begin analysis.</div>
|
| 45 |
-
|
| 46 |
-
<div class="row mt-md">
|
| 47 |
-
<label for="videoFile">Video file</label>
|
| 48 |
-
<span class="badge"><span id="videoMeta">No file</span></span>
|
| 49 |
-
</div>
|
| 50 |
-
<input id="videoFile" type="file" accept="video/*" />
|
| 51 |
-
|
| 52 |
-
<div class="mt-md">
|
| 53 |
-
<label>Detection Query (optional)</label>
|
| 54 |
-
<textarea id="missionText" rows="3"
|
| 55 |
-
placeholder="e.g., person, vehicle, hazard"></textarea>
|
| 56 |
-
|
| 57 |
-
<div class="hint mt-sm">
|
| 58 |
-
Specify object classes to filter detection. Leave blank to detect all objects.
|
| 59 |
-
<div class="mini mt-xs" id="hfBackendStatus">Backend: Standby</div>
|
| 60 |
-
</div>
|
| 61 |
-
</div>
|
| 62 |
-
|
| 63 |
-
<div class="btnrow">
|
| 64 |
-
<button id="btnLoadSample" class="btn secondary" title="Load a sample video" disabled>Load
|
| 65 |
-
Sample</button>
|
| 66 |
-
<button id="btnEject" class="btn danger" title="Unload video">Eject</button>
|
| 67 |
-
</div>
|
| 68 |
-
|
| 69 |
-
<div class="grid2">
|
| 70 |
-
<div>
|
| 71 |
-
<label>Detector</label>
|
| 72 |
-
<select id="detectorSelect">
|
| 73 |
-
<optgroup label="Object Detection Models">
|
| 74 |
-
<option value="yolo11" data-kind="object" selected>Lite</option>
|
| 75 |
-
<option value="detr_resnet50" data-kind="object">Big</option>
|
| 76 |
-
<option value="grounding_dino" data-kind="object">Large</option>
|
| 77 |
-
</optgroup>
|
| 78 |
-
<optgroup label="Segmentation Models">
|
| 79 |
-
<option value="GSAM2-L" data-kind="segmentation">GSAM2-L</option>
|
| 80 |
-
<option value="GSAM2-B" data-kind="segmentation">GSAM2-B</option>
|
| 81 |
-
<option value="GSAM2-S" data-kind="segmentation">GSAM2-S</option>
|
| 82 |
-
<option value="YSAM2-L" data-kind="segmentation">YSAM2-L (Fast)</option>
|
| 83 |
-
<option value="YSAM2-B" data-kind="segmentation">YSAM2-B (Fast)</option>
|
| 84 |
-
<option value="YSAM2-S" data-kind="segmentation">YSAM2-S (Fast)</option>
|
| 85 |
-
</optgroup>
|
| 86 |
-
<optgroup label="Drone Detection Models">
|
| 87 |
-
<option value="yolov8_visdrone" data-kind="drone">VisDrone (YOLOv8)</option>
|
| 88 |
-
</optgroup>
|
| 89 |
-
</select>
|
| 90 |
-
</div>
|
| 91 |
-
<div>
|
| 92 |
-
<label>Tracking</label>
|
| 93 |
-
<select id="trackerSelect">
|
| 94 |
-
<option value="iou">IOU + velocity (built-in)</option>
|
| 95 |
-
<option value="external">External hook (user API)</option>
|
| 96 |
-
</select>
|
| 97 |
-
</div>
|
| 98 |
-
|
| 99 |
-
<label class="checkbox-row" for="enableDepthToggle" style="display:none">
|
| 100 |
-
<input type="checkbox" id="enableDepthToggle">
|
| 101 |
-
<span>Enable Depth Map</span>
|
| 102 |
-
</label>
|
| 103 |
-
<label class="checkbox-row" for="enableStreamToggle" style="margin-top: 4px;">
|
| 104 |
-
<input type="checkbox" id="enableStreamToggle" checked>
|
| 105 |
-
<span>Enable Stream Processing</span>
|
| 106 |
-
</label>
|
| 107 |
-
</div>
|
| 108 |
-
|
| 109 |
-
</div>
|
| 110 |
-
|
| 111 |
-
<div class="card" style="flex:1; min-height:0">
|
| 112 |
-
<h2>System Log</h2>
|
| 113 |
-
<div class="log" id="sysLog"></div>
|
| 114 |
-
</div>
|
| 115 |
-
</aside>
|
| 116 |
-
|
| 117 |
-
<main>
|
| 118 |
-
<section class="tab active" id="tab-engage">
|
| 119 |
-
<div class="engage-grid">
|
| 120 |
-
<div class="panel">
|
| 121 |
-
<h3>
|
| 122 |
-
<span>Video Analysis</span>
|
| 123 |
-
<div style="display: flex; gap: 8px; align-items: center;">
|
| 124 |
-
<button class="collapse-btn" id="btnToggleSidebar">Hide Panel</button>
|
| 125 |
-
<span class="rightnote" id="engageNote">Awaiting video</span>
|
| 126 |
-
</div>
|
| 127 |
-
</h3>
|
| 128 |
-
|
| 129 |
-
<div class="viewbox" style="min-height: 420px;">
|
| 130 |
-
<video id="videoEngage" playsinline muted></video>
|
| 131 |
-
|
| 132 |
-
<!-- Progress ring overlay (shown during detection) -->
|
| 133 |
-
<div class="progress-ring-wrap" id="progressRing">
|
| 134 |
-
<svg viewBox="0 0 90 90">
|
| 135 |
-
<circle class="progress-ring-bg" cx="45" cy="45" r="42" />
|
| 136 |
-
<circle class="progress-ring-fg" id="progressRingFg" cx="45" cy="45" r="42" />
|
| 137 |
-
</svg>
|
| 138 |
-
<div class="progress-ring-text" id="progressRingText">Detecting...</div>
|
| 139 |
-
</div>
|
| 140 |
-
|
| 141 |
-
<div class="watermark">ISR Analytics</div>
|
| 142 |
-
<div class="empty" id="engageEmpty">
|
| 143 |
-
<div class="big">No video loaded</div>
|
| 144 |
-
<div class="small">Upload a video file, then run <b>Detect</b> to identify objects. Use <b>Track</b> to follow them in real time.</div>
|
| 145 |
-
</div>
|
| 146 |
-
</div>
|
| 147 |
-
|
| 148 |
-
<div id="timelineWrap" class="timeline-wrap">
|
| 149 |
-
<canvas id="timelineBar"></canvas>
|
| 150 |
-
</div>
|
| 151 |
-
|
| 152 |
-
<div class="btnrow mt-md">
|
| 153 |
-
<button id="btnReason" class="btn">Detect</button>
|
| 154 |
-
<button id="btnCancelReason" class="btn danger" style="display: none;">Cancel</button>
|
| 155 |
-
<button id="btnEngage" class="btn">Track</button>
|
| 156 |
-
<button id="btnPause" class="btn secondary">Pause</button>
|
| 157 |
-
<button id="btnReset" class="btn secondary">Reset</button>
|
| 158 |
-
</div>
|
| 159 |
-
|
| 160 |
-
<div class="strip mt-md">
|
| 161 |
-
<span class="chip" id="chipTracks">TRACKS:0</span>
|
| 162 |
-
<span class="chip" id="chipFeed" title="Toggle raw vs HF-processed feed (if available)">FEED:RAW</span>
|
| 163 |
-
<span class="chip" id="chipDepth" title="Toggle depth view (if available)" style="display:none">VIEW:DEFAULT</span>
|
| 164 |
-
</div>
|
| 165 |
-
</div>
|
| 166 |
-
|
| 167 |
-
<div class="engage-right">
|
| 168 |
-
<div class="panel" style="flex:1; min-height:0">
|
| 169 |
-
<h3>
|
| 170 |
-
<span>Detected Objects</span>
|
| 171 |
-
<span class="rightnote" id="liveStamp"></span>
|
| 172 |
-
</h3>
|
| 173 |
-
<div class="list" id="trackList" style="max-height:360px; overflow-y:auto"></div>
|
| 174 |
-
</div>
|
| 175 |
-
</div>
|
| 176 |
-
|
| 177 |
-
<div class="panel panel-inspection" id="inspectionPanel" style="display:none">
|
| 178 |
-
<div class="inspection-header">
|
| 179 |
-
<div class="inspection-header-left">
|
| 180 |
-
<span class="inspection-obj-name" id="inspectionObjName">--</span>
|
| 181 |
-
<span class="inspection-conf" id="inspectionConf">--</span>
|
| 182 |
-
<span class="inspection-status pending" id="inspectionStatus">PENDING</span>
|
| 183 |
-
</div>
|
| 184 |
-
<div class="inspection-header-right">
|
| 185 |
-
<span class="inspection-frame" id="inspectionFrame">FRM -- / --</span>
|
| 186 |
-
<button class="collapse-btn" id="btnCloseInspection">CLOSE</button>
|
| 187 |
-
</div>
|
| 188 |
-
</div>
|
| 189 |
-
|
| 190 |
-
<div class="inspection-quad" id="inspectionQuad">
|
| 191 |
-
<div class="inspection-quadrant" data-mode="seg" id="quadSeg">
|
| 192 |
-
<div class="quad-label"><span class="quad-dot quad-dot-seg"></span>SEGMENTATION</div>
|
| 193 |
-
<canvas class="quad-canvas" id="quadCanvasSeg"></canvas>
|
| 194 |
-
<div class="quad-metric" id="quadMetricSeg"></div>
|
| 195 |
-
<div class="quad-loading" style="display:none"><div class="inspection-spinner"></div></div>
|
| 196 |
-
<div class="quad-error" style="display:none"></div>
|
| 197 |
-
</div>
|
| 198 |
-
<div class="inspection-quadrant" data-mode="edge" id="quadEdge">
|
| 199 |
-
<div class="quad-label"><span class="quad-dot quad-dot-edge"></span>EDGE</div>
|
| 200 |
-
<canvas class="quad-canvas" id="quadCanvasEdge"></canvas>
|
| 201 |
-
<div class="quad-metric" id="quadMetricEdge">SOBEL</div>
|
| 202 |
-
<div class="quad-error" style="display:none"></div>
|
| 203 |
-
</div>
|
| 204 |
-
<div class="inspection-quadrant" data-mode="depth" id="quadDepth">
|
| 205 |
-
<div class="quad-label"><span class="quad-dot quad-dot-depth"></span>DEPTH</div>
|
| 206 |
-
<canvas class="quad-canvas" id="quadCanvasDepth"></canvas>
|
| 207 |
-
<div class="quad-metric" id="quadMetricDepth"></div>
|
| 208 |
-
<div class="quad-loading" style="display:none"><div class="inspection-spinner"></div></div>
|
| 209 |
-
<div class="quad-error" style="display:none"></div>
|
| 210 |
-
</div>
|
| 211 |
-
<div class="inspection-quadrant" data-mode="3d" id="quad3d">
|
| 212 |
-
<div class="quad-label"><span class="quad-dot quad-dot-3d"></span>3D MESH</div>
|
| 213 |
-
<div class="quad-3d-container" id="quad3dContainer"></div>
|
| 214 |
-
<div class="quad-metric" id="quadMetric3d"></div>
|
| 215 |
-
<div class="quad-loading" style="display:none"><div class="inspection-spinner"></div></div>
|
| 216 |
-
<div class="quad-error" style="display:none"></div>
|
| 217 |
-
</div>
|
| 218 |
-
</div>
|
| 219 |
-
|
| 220 |
-
<div class="inspection-metrics" id="inspectionMetrics"></div>
|
| 221 |
-
<div class="interpretability-graph" id="interpretabilityGraph"></div>
|
| 222 |
-
|
| 223 |
-
<div class="inspection-empty" id="inspectionEmpty">
|
| 224 |
-
Select an object from the track cards or click a bounding box to inspect it.
|
| 225 |
-
</div>
|
| 226 |
-
</div>
|
| 227 |
-
|
| 228 |
-
<div class="panel panel-chat" id="chatPanel">
|
| 229 |
-
<h3>
|
| 230 |
-
<span>AI Assistant</span>
|
| 231 |
-
<div style="display: flex; gap: 8px; align-items: center;">
|
| 232 |
-
<button class="collapse-btn" id="chatClear">Clear</button>
|
| 233 |
-
</div>
|
| 234 |
-
</h3>
|
| 235 |
-
<div class="chat-context-header" id="chatContextHeader" style="display:none;">
|
| 236 |
-
<span class="chat-context-label">CONTEXT</span>
|
| 237 |
-
<div class="chat-context-chips" id="chatContextChips"></div>
|
| 238 |
-
</div>
|
| 239 |
-
<div class="chat-suggestions" id="chatSuggestions" style="display:none;"></div>
|
| 240 |
-
<div class="chat-container">
|
| 241 |
-
<div class="chat-messages" id="chatMessages"></div>
|
| 242 |
-
<div class="chat-input-row">
|
| 243 |
-
<input type="text" id="chatInput" placeholder="Select a track to start..." />
|
| 244 |
-
<button class="btn" id="chatSend">Send</button>
|
| 245 |
-
</div>
|
| 246 |
-
</div>
|
| 247 |
-
</div>
|
| 248 |
-
</div><!-- engage-grid -->
|
| 249 |
-
</section>
|
| 250 |
-
|
| 251 |
-
</div>
|
| 252 |
-
|
| 253 |
-
<footer>
|
| 254 |
-
<div class="footer-left">ISR Analytics Platform</div>
|
| 255 |
-
<div class="stat-bar" id="statBar">
|
| 256 |
-
<div class="stat-item">
|
| 257 |
-
<span class="stat-label">Objects</span>
|
| 258 |
-
<span class="stat-value" id="statTracks">0</span>
|
| 259 |
-
</div>
|
| 260 |
-
<div class="stat-item">
|
| 261 |
-
<span class="stat-label">Model</span>
|
| 262 |
-
<span class="stat-value" id="statModel">YOLO11</span>
|
| 263 |
-
</div>
|
| 264 |
-
<div class="stat-item">
|
| 265 |
-
<span class="stat-label">Status</span>
|
| 266 |
-
<span class="stat-value" id="statStatus">Ready</span>
|
| 267 |
-
</div>
|
| 268 |
-
</div>
|
| 269 |
-
</footer>
|
| 270 |
-
|
| 271 |
-
</div>
|
| 272 |
-
|
| 273 |
-
<!-- Toast notification container -->
|
| 274 |
-
<div class="toast-container" id="toastContainer"></div>
|
| 275 |
-
|
| 276 |
-
<script>
|
| 277 |
-
window.API_CONFIG = {
|
| 278 |
-
BACKEND_BASE: "https://biaslab2025-isr.hf.space"
|
| 279 |
-
};
|
| 280 |
-
</script>
|
| 281 |
-
<script src="./js/init.js?v=20260318"></script>
|
| 282 |
-
<script src="./js/core/config.js?v=20260318"></script>
|
| 283 |
-
<script src="./js/core/utils.js?v=20260318"></script>
|
| 284 |
-
<script src="./js/core/state.js?v=20260318"></script>
|
| 285 |
-
<script src="./js/core/physics.js?v=20260318"></script>
|
| 286 |
-
<script src="./js/core/video.js?v=20260318"></script>
|
| 287 |
-
<script src="./js/ui/logging.js?v=20260318"></script>
|
| 288 |
-
<script src="./js/core/gptMapping.js?v=20260318"></script>
|
| 289 |
-
<script src="./js/core/tracker.js?v=20260318"></script>
|
| 290 |
-
<script src="./js/core/timeline.js?v=20260318"></script>
|
| 291 |
-
<script src="./js/api/client.js?v=20260318"></script>
|
| 292 |
-
<script src="./js/api/inspection-api.js?v=20260318"></script>
|
| 293 |
-
<script src="./js/ui/overlays.js?v=20260318"></script>
|
| 294 |
-
<script src="./js/ui/cards.js?v=20260318"></script>
|
| 295 |
-
<script src="./js/ui/chat.js?v=20260318"></script>
|
| 296 |
-
<script src="./vendor/d3-hierarchy.v3.min.js?v=20260323"></script>
|
| 297 |
-
<script src="./vendor/d3-path.v3.min.js?v=20260323"></script>
|
| 298 |
-
<script src="./vendor/d3-shape.v3.min.js?v=20260323"></script>
|
| 299 |
-
<script src="./js/ui/inspection.js?v=20260318"></script>
|
| 300 |
-
<script src="./js/ui/inspection-renders.js?v=20260318"></script>
|
| 301 |
-
<script src="./js/ui/inspection-3d.js?v=20260318"></script>
|
| 302 |
-
<script src="./js/ui/explainability.js?v=20260323"></script>
|
| 303 |
-
<script src="./js/ui/cursor.js?v=20260318"></script>
|
| 304 |
-
<script src="./js/ui/animations.js?v=20260318"></script>
|
| 305 |
-
<script src="./js/core/demo.js?v=20260318"></script>
|
| 306 |
-
<script src="./js/main.js?v=20260318"></script>
|
| 307 |
-
|
| 308 |
-
</body>
|
| 309 |
-
|
| 310 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/api/client.js
DELETED
|
@@ -1,221 +0,0 @@
|
|
| 1 |
-
// API Client Module - Backend communication
|
| 2 |
-
APP.api.client = {};
|
| 3 |
-
|
| 4 |
-
APP.api.client.hfDetectAsync = async function (formData) {
|
| 5 |
-
const { state } = APP.core;
|
| 6 |
-
if (!state.hf.baseUrl) return;
|
| 7 |
-
|
| 8 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/async`, {
|
| 9 |
-
method: "POST",
|
| 10 |
-
body: formData
|
| 11 |
-
});
|
| 12 |
-
|
| 13 |
-
if (!resp.ok) {
|
| 14 |
-
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
| 15 |
-
throw new Error(err.detail || "Async detection submission failed");
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
const data = await resp.json();
|
| 19 |
-
|
| 20 |
-
// Store URLs from response
|
| 21 |
-
if (data.status_url) {
|
| 22 |
-
state.hf.statusUrl = data.status_url.startsWith("http")
|
| 23 |
-
? data.status_url
|
| 24 |
-
: `${state.hf.baseUrl}${data.status_url}`;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
if (data.video_url) {
|
| 28 |
-
state.hf.videoUrl = data.video_url.startsWith("http")
|
| 29 |
-
? data.video_url
|
| 30 |
-
: `${state.hf.baseUrl}${data.video_url}`;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
if (data.depth_video_url) {
|
| 34 |
-
state.hf.depthVideoUrl = data.depth_video_url.startsWith("http")
|
| 35 |
-
? data.depth_video_url
|
| 36 |
-
: `${state.hf.baseUrl}${data.depth_video_url}`;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
return data;
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
APP.api.client.checkJobStatus = async function (jobId) {
|
| 43 |
-
const { state } = APP.core;
|
| 44 |
-
if (!state.hf.baseUrl) return { status: "error" };
|
| 45 |
-
|
| 46 |
-
const url = state.hf.statusUrl || `${state.hf.baseUrl}/detect/job/${jobId}`;
|
| 47 |
-
const resp = await fetch(url, { cache: "no-store" });
|
| 48 |
-
|
| 49 |
-
if (!resp.ok) {
|
| 50 |
-
if (resp.status === 404) return { status: "not_found" };
|
| 51 |
-
throw new Error(`Status check failed: ${resp.status}`);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
return await resp.json();
|
| 55 |
-
};
|
| 56 |
-
|
| 57 |
-
APP.api.client.cancelBackendJob = async function (jobId, reason) {
|
| 58 |
-
const { state } = APP.core;
|
| 59 |
-
const { log } = APP.ui.logging;
|
| 60 |
-
|
| 61 |
-
if (!state.hf.baseUrl || !jobId) return;
|
| 62 |
-
|
| 63 |
-
// Don't attempt cancel on HF Space (it doesn't support it)
|
| 64 |
-
if (state.hf.baseUrl.includes("hf.space")) {
|
| 65 |
-
log(`Job cancel skipped for HF Space (${reason || "user request"})`, "w");
|
| 66 |
-
return { status: "skipped", message: "Cancel disabled for HF Space" };
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
try {
|
| 70 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/job/${jobId}`, {
|
| 71 |
-
method: "DELETE"
|
| 72 |
-
});
|
| 73 |
-
|
| 74 |
-
if (resp.ok) {
|
| 75 |
-
const result = await resp.json();
|
| 76 |
-
log(`Job ${jobId.substring(0, 8)} cancelled`, "w");
|
| 77 |
-
return result;
|
| 78 |
-
}
|
| 79 |
-
if (resp.status === 404) return { status: "not_found" };
|
| 80 |
-
throw new Error("Cancel failed");
|
| 81 |
-
} catch (err) {
|
| 82 |
-
log(`Cancel error: ${err.message}`, "e");
|
| 83 |
-
return { status: "error", message: err.message };
|
| 84 |
-
}
|
| 85 |
-
};
|
| 86 |
-
|
| 87 |
-
APP.api.client.pollAsyncJob = async function () {
|
| 88 |
-
const { state } = APP.core;
|
| 89 |
-
const { log, setHfStatus } = APP.ui.logging;
|
| 90 |
-
const { fetchProcessedVideo, fetchDepthVideo } = APP.core.video;
|
| 91 |
-
|
| 92 |
-
const pollInterval = 3000; // 3 seconds
|
| 93 |
-
const maxAttempts = 200; // 10 minutes max
|
| 94 |
-
let attempts = 0;
|
| 95 |
-
let fetchingVideo = false;
|
| 96 |
-
|
| 97 |
-
return new Promise((resolve, reject) => {
|
| 98 |
-
state.hf.asyncPollInterval = setInterval(async () => {
|
| 99 |
-
attempts++;
|
| 100 |
-
|
| 101 |
-
try {
|
| 102 |
-
const resp = await fetch(state.hf.statusUrl, { cache: "no-store" });
|
| 103 |
-
|
| 104 |
-
if (!resp.ok) {
|
| 105 |
-
if (resp.status === 404) {
|
| 106 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 107 |
-
reject(new Error("Job expired or not found"));
|
| 108 |
-
return;
|
| 109 |
-
}
|
| 110 |
-
throw new Error(`Status check failed: ${resp.statusText}`);
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
const status = await resp.json();
|
| 114 |
-
state.hf.asyncStatus = status.status;
|
| 115 |
-
state.hf.asyncProgress = status;
|
| 116 |
-
|
| 117 |
-
if (status.status === "completed") {
|
| 118 |
-
if (fetchingVideo) return;
|
| 119 |
-
fetchingVideo = true;
|
| 120 |
-
|
| 121 |
-
const completedJobId = state.hf.asyncJobId;
|
| 122 |
-
log(`Job ${completedJobId.substring(0, 8)}: completed`, "g");
|
| 123 |
-
setHfStatus("job completed, fetching video...");
|
| 124 |
-
|
| 125 |
-
try {
|
| 126 |
-
await fetchProcessedVideo();
|
| 127 |
-
await fetchDepthVideo();
|
| 128 |
-
|
| 129 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 130 |
-
state.hf.completedJobId = state.hf.asyncJobId; // preserve for post-completion sync
|
| 131 |
-
// Fetch timeline summary for heatmap
|
| 132 |
-
APP.core.timeline.loadSummary();
|
| 133 |
-
state.hf.asyncJobId = null;
|
| 134 |
-
setHfStatus("ready");
|
| 135 |
-
resolve();
|
| 136 |
-
} catch (err) {
|
| 137 |
-
if (err && err.code === "VIDEO_PENDING") {
|
| 138 |
-
setHfStatus("job completed, finalizing video...");
|
| 139 |
-
fetchingVideo = false;
|
| 140 |
-
return;
|
| 141 |
-
}
|
| 142 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 143 |
-
state.hf.asyncJobId = null;
|
| 144 |
-
reject(err);
|
| 145 |
-
}
|
| 146 |
-
} else if (status.status === "failed") {
|
| 147 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 148 |
-
const errMsg = status.error || "Processing failed";
|
| 149 |
-
log(`Job ${state.hf.asyncJobId.substring(0, 8)}: failed - ${errMsg}`, "e");
|
| 150 |
-
state.hf.asyncJobId = null;
|
| 151 |
-
setHfStatus(`error: ${errMsg}`);
|
| 152 |
-
reject(new Error(errMsg));
|
| 153 |
-
} else {
|
| 154 |
-
// Still processing
|
| 155 |
-
const progress = status.progress || 0;
|
| 156 |
-
const progressPct = Math.round(progress * 100);
|
| 157 |
-
const progressInfo = progress ? ` (${progressPct}%)` : "";
|
| 158 |
-
setHfStatus(`job ${state.hf.asyncJobId.substring(0, 8)}: ${status.status}${progressInfo}`);
|
| 159 |
-
|
| 160 |
-
// Update progress ring animation
|
| 161 |
-
if (APP.ui.animations && APP.ui.animations.updateProgressRing) {
|
| 162 |
-
const fraction = progress || Math.min(attempts / maxAttempts * 2, 0.9);
|
| 163 |
-
const label = progress ? (progressPct + "%") : "Processing...";
|
| 164 |
-
APP.ui.animations.updateProgressRing(fraction, label);
|
| 165 |
-
}
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
if (attempts >= maxAttempts) {
|
| 169 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 170 |
-
reject(new Error("Polling timeout (10 minutes)"));
|
| 171 |
-
}
|
| 172 |
-
} catch (err) {
|
| 173 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 174 |
-
reject(err);
|
| 175 |
-
}
|
| 176 |
-
}, pollInterval);
|
| 177 |
-
});
|
| 178 |
-
};
|
| 179 |
-
|
| 180 |
-
// External detection hook (can be replaced by user)
|
| 181 |
-
APP.api.client.externalDetect = async function (input) {
|
| 182 |
-
console.log("externalDetect called", input);
|
| 183 |
-
return [];
|
| 184 |
-
};
|
| 185 |
-
|
| 186 |
-
// External features hook (can be replaced by user)
|
| 187 |
-
APP.api.client.externalFeatures = async function (detections, frameInfo) {
|
| 188 |
-
console.log("externalFeatures called for", detections.length, "objects");
|
| 189 |
-
return {};
|
| 190 |
-
};
|
| 191 |
-
|
| 192 |
-
// External tracker hook (can be replaced by user)
|
| 193 |
-
APP.api.client.externalTrack = async function (videoEl) {
|
| 194 |
-
console.log("externalTrack called");
|
| 195 |
-
return [];
|
| 196 |
-
};
|
| 197 |
-
|
| 198 |
-
APP.api.client.chatSend = async function (message, mission, activeObjects, history) {
|
| 199 |
-
const { state } = APP.core;
|
| 200 |
-
const base = state.hf.baseUrl;
|
| 201 |
-
if (!base) throw new Error("Backend not configured");
|
| 202 |
-
|
| 203 |
-
const resp = await fetch(`${base}/chat`, {
|
| 204 |
-
method: "POST",
|
| 205 |
-
headers: { "Content-Type": "application/json" },
|
| 206 |
-
body: JSON.stringify({
|
| 207 |
-
message,
|
| 208 |
-
mission: mission || "",
|
| 209 |
-
active_objects: activeObjects || [],
|
| 210 |
-
history: history || []
|
| 211 |
-
})
|
| 212 |
-
});
|
| 213 |
-
|
| 214 |
-
if (!resp.ok) {
|
| 215 |
-
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
| 216 |
-
throw new Error(err.detail || "Chat request failed");
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
const data = await resp.json();
|
| 220 |
-
return data.response;
|
| 221 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/api/inspection-api.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
| 1 |
-
// Inspection API Client — fetches data for the deep-inspection panel
|
| 2 |
-
APP.api.inspection = {};
|
| 3 |
-
|
| 4 |
-
/**
|
| 5 |
-
* Fetch a raw video frame as an Image element, optionally cropped to a track.
|
| 6 |
-
* @param {string} jobId
|
| 7 |
-
* @param {number} frameIdx
|
| 8 |
-
* @param {string} [trackId] — if provided, crops to the track's bbox + padding
|
| 9 |
-
* @param {number} [padding=0.20] — padding ratio around the bbox
|
| 10 |
-
* @returns {Promise<HTMLImageElement>}
|
| 11 |
-
*/
|
| 12 |
-
APP.api.inspection.fetchFrame = async function (jobId, frameIdx, trackId, padding) {
|
| 13 |
-
const base = APP.core.state.hf.baseUrl;
|
| 14 |
-
let url = `${base}/inspect/frame/${jobId}/${frameIdx}`;
|
| 15 |
-
if (trackId) {
|
| 16 |
-
const p = padding != null ? padding : 0.20;
|
| 17 |
-
url += `?track_id=${encodeURIComponent(trackId)}&padding=${p}`;
|
| 18 |
-
}
|
| 19 |
-
const resp = await fetch(url);
|
| 20 |
-
if (!resp.ok) throw new Error(`Frame fetch failed: ${resp.status}`);
|
| 21 |
-
|
| 22 |
-
const blob = await resp.blob();
|
| 23 |
-
const blobUrl = URL.createObjectURL(blob);
|
| 24 |
-
|
| 25 |
-
return new Promise((resolve, reject) => {
|
| 26 |
-
const img = new Image();
|
| 27 |
-
img.onload = () => resolve(img);
|
| 28 |
-
img.onerror = () => reject(new Error("Failed to decode frame image"));
|
| 29 |
-
img.src = blobUrl;
|
| 30 |
-
// Store blobUrl on the image so caller can revoke later
|
| 31 |
-
img._blobUrl = blobUrl;
|
| 32 |
-
});
|
| 33 |
-
};
|
| 34 |
-
|
| 35 |
-
/**
|
| 36 |
-
* Fetch segmentation mask for a specific track on a specific frame.
|
| 37 |
-
* @param {string} jobId
|
| 38 |
-
* @param {number} frameIdx
|
| 39 |
-
* @param {string} trackId
|
| 40 |
-
* @returns {Promise<Object>} { track_id, bbox, color, mask (ImageData or RLE) }
|
| 41 |
-
*/
|
| 42 |
-
APP.api.inspection.fetchMask = async function (jobId, frameIdx, trackId) {
|
| 43 |
-
const base = APP.core.state.hf.baseUrl;
|
| 44 |
-
const url = `${base}/inspect/mask/${jobId}/${frameIdx}/${encodeURIComponent(trackId)}`;
|
| 45 |
-
const resp = await fetch(url);
|
| 46 |
-
|
| 47 |
-
if (resp.status === 404) {
|
| 48 |
-
// No pre-computed mask — generate one on-demand via SAM2
|
| 49 |
-
return await APP.api.inspection.generateMask(jobId, frameIdx, trackId);
|
| 50 |
-
}
|
| 51 |
-
if (!resp.ok) throw new Error(`Mask fetch failed: ${resp.status}`);
|
| 52 |
-
return await resp.json();
|
| 53 |
-
};
|
| 54 |
-
|
| 55 |
-
/**
|
| 56 |
-
* Generate a mask on-demand via SAM2 (for detection mode jobs).
|
| 57 |
-
* Result is cached server-side — subsequent fetchMask calls will find it.
|
| 58 |
-
* @param {string} jobId
|
| 59 |
-
* @param {number} frameIdx
|
| 60 |
-
* @param {string} trackId
|
| 61 |
-
* @returns {Promise<Object>} Same shape as fetchMask response
|
| 62 |
-
*/
|
| 63 |
-
APP.api.inspection.generateMask = async function (jobId, frameIdx, trackId) {
|
| 64 |
-
const base = APP.core.state.hf.baseUrl;
|
| 65 |
-
const resp = await fetch(`${base}/inspect/generate-mask/${jobId}/${frameIdx}/${encodeURIComponent(trackId)}`, {
|
| 66 |
-
method: "POST",
|
| 67 |
-
headers: { "Content-Type": "application/json" },
|
| 68 |
-
body: JSON.stringify({ sam2_size: "large" })
|
| 69 |
-
});
|
| 70 |
-
if (!resp.ok) throw new Error(`Mask generation failed: ${resp.status}`);
|
| 71 |
-
return await resp.json();
|
| 72 |
-
};
|
| 73 |
-
|
| 74 |
-
/**
|
| 75 |
-
* Fetch depth map for a specific frame, optionally cropped to a track.
|
| 76 |
-
* @param {string} jobId
|
| 77 |
-
* @param {number} frameIdx
|
| 78 |
-
* @param {string} [trackId] — if provided, crops depth to the track's bbox
|
| 79 |
-
* @returns {Promise<Object>} { width, height, min, max, data: Float32Array }
|
| 80 |
-
*/
|
| 81 |
-
APP.api.inspection.fetchDepth = async function (jobId, frameIdx, trackId) {
|
| 82 |
-
const base = APP.core.state.hf.baseUrl;
|
| 83 |
-
// Try binary format first; fall back to JSON if CORS strips custom headers
|
| 84 |
-
let url = `${base}/inspect/depth/${jobId}/${frameIdx}?format=raw`;
|
| 85 |
-
if (trackId) {
|
| 86 |
-
url += `&track_id=${encodeURIComponent(trackId)}`;
|
| 87 |
-
}
|
| 88 |
-
const resp = await fetch(url);
|
| 89 |
-
if (!resp.ok) throw new Error(`Depth fetch failed: ${resp.status}`);
|
| 90 |
-
|
| 91 |
-
const contentType = resp.headers.get("content-type") || "";
|
| 92 |
-
|
| 93 |
-
if (contentType.includes("application/octet-stream")) {
|
| 94 |
-
// Binary float32 format with metadata in headers
|
| 95 |
-
const w = parseInt(resp.headers.get("X-Depth-Width"), 10);
|
| 96 |
-
const h = parseInt(resp.headers.get("X-Depth-Height"), 10);
|
| 97 |
-
const minD = parseFloat(resp.headers.get("X-Depth-Min"));
|
| 98 |
-
const maxD = parseFloat(resp.headers.get("X-Depth-Max"));
|
| 99 |
-
const buf = await resp.arrayBuffer();
|
| 100 |
-
const data = new Float32Array(buf);
|
| 101 |
-
|
| 102 |
-
// If CORS stripped headers, infer dimensions from data length
|
| 103 |
-
if (isNaN(w) || isNaN(h)) {
|
| 104 |
-
// Fall back to JSON format
|
| 105 |
-
return await APP.api.inspection._fetchDepthJson(jobId, frameIdx, trackId);
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
return {
|
| 109 |
-
width: w,
|
| 110 |
-
height: h,
|
| 111 |
-
min: isNaN(minD) ? 0 : minD,
|
| 112 |
-
max: isNaN(maxD) ? 1 : maxD,
|
| 113 |
-
data: data
|
| 114 |
-
};
|
| 115 |
-
} else {
|
| 116 |
-
// JSON + base64 format
|
| 117 |
-
const json = await resp.json();
|
| 118 |
-
return APP.api.inspection._decodeDepthJson(json);
|
| 119 |
-
}
|
| 120 |
-
};
|
| 121 |
-
|
| 122 |
-
/**
|
| 123 |
-
* Fallback: fetch depth in JSON format if raw binary headers are unavailable.
|
| 124 |
-
*/
|
| 125 |
-
APP.api.inspection._fetchDepthJson = async function (jobId, frameIdx, trackId) {
|
| 126 |
-
const base = APP.core.state.hf.baseUrl;
|
| 127 |
-
let url = `${base}/inspect/depth/${jobId}/${frameIdx}?format=json`;
|
| 128 |
-
if (trackId) {
|
| 129 |
-
url += `&track_id=${encodeURIComponent(trackId)}`;
|
| 130 |
-
}
|
| 131 |
-
const resp = await fetch(url);
|
| 132 |
-
if (!resp.ok) throw new Error(`Depth (JSON) fetch failed: ${resp.status}`);
|
| 133 |
-
const json = await resp.json();
|
| 134 |
-
return APP.api.inspection._decodeDepthJson(json);
|
| 135 |
-
};
|
| 136 |
-
|
| 137 |
-
/**
|
| 138 |
-
* Decode a JSON depth response to the standard { width, height, min, max, data } format.
|
| 139 |
-
*/
|
| 140 |
-
APP.api.inspection._decodeDepthJson = function (json) {
|
| 141 |
-
const raw = atob(json.data_b64);
|
| 142 |
-
const buf = new ArrayBuffer(raw.length);
|
| 143 |
-
const view = new Uint8Array(buf);
|
| 144 |
-
for (let i = 0; i < raw.length; i++) view[i] = raw.charCodeAt(i);
|
| 145 |
-
return {
|
| 146 |
-
width: json.width,
|
| 147 |
-
height: json.height,
|
| 148 |
-
min: json.min_depth,
|
| 149 |
-
max: json.max_depth,
|
| 150 |
-
data: new Float32Array(buf)
|
| 151 |
-
};
|
| 152 |
-
};
|
| 153 |
-
|
| 154 |
-
/**
|
| 155 |
-
* Fetch 3D mesh or point cloud data for a specific object.
|
| 156 |
-
* @param {string} jobId
|
| 157 |
-
* @param {number} frameIdx
|
| 158 |
-
* @param {string} trackId
|
| 159 |
-
* @param {boolean} [useGenerative=false] — if true, request Tripo3D generative model (API cost)
|
| 160 |
-
* @returns {Promise<Object>} { renderMode, numVertices, numTriangles, positions, colors, indices?, bbox3d }
|
| 161 |
-
*/
|
| 162 |
-
APP.api.inspection.fetchPointCloud = async function (jobId, frameIdx, trackId, useGenerative) {
|
| 163 |
-
const base = APP.core.state.hf.baseUrl;
|
| 164 |
-
const resp = await fetch(`${base}/inspect/pointcloud/${jobId}/${frameIdx}`, {
|
| 165 |
-
method: "POST",
|
| 166 |
-
headers: { "Content-Type": "application/json" },
|
| 167 |
-
body: JSON.stringify({
|
| 168 |
-
track_id: trackId,
|
| 169 |
-
max_points: 50000,
|
| 170 |
-
render_mode: "mesh",
|
| 171 |
-
use_generative: !!useGenerative
|
| 172 |
-
})
|
| 173 |
-
});
|
| 174 |
-
if (!resp.ok) throw new Error(`Point cloud fetch failed: ${resp.status}`);
|
| 175 |
-
|
| 176 |
-
const contentType = resp.headers.get("content-type") || "";
|
| 177 |
-
|
| 178 |
-
// Tripo3D returns GLB binary
|
| 179 |
-
if (contentType.includes("model/gltf-binary")) {
|
| 180 |
-
const buffer = await resp.arrayBuffer();
|
| 181 |
-
return { type: "glb", buffer: buffer };
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
// Depth-based fallback returns JSON
|
| 185 |
-
const json = await resp.json();
|
| 186 |
-
|
| 187 |
-
function b64decode(b64, TypedArray) {
|
| 188 |
-
const raw = atob(b64);
|
| 189 |
-
const buf = new ArrayBuffer(raw.length);
|
| 190 |
-
const view = new Uint8Array(buf);
|
| 191 |
-
for (let i = 0; i < raw.length; i++) view[i] = raw.charCodeAt(i);
|
| 192 |
-
return new TypedArray(buf);
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
const result = {
|
| 196 |
-
type: "legacy",
|
| 197 |
-
renderMode: json.render_mode || "points",
|
| 198 |
-
numVertices: json.num_vertices || json.num_points,
|
| 199 |
-
numTriangles: json.num_triangles || 0,
|
| 200 |
-
positions: b64decode(json.positions, Float32Array),
|
| 201 |
-
colors: b64decode(json.colors, Uint8Array),
|
| 202 |
-
bbox3d: json.bbox_3d
|
| 203 |
-
};
|
| 204 |
-
|
| 205 |
-
if (json.indices) {
|
| 206 |
-
result.indices = b64decode(json.indices, Uint32Array);
|
| 207 |
-
}
|
| 208 |
-
|
| 209 |
-
return result;
|
| 210 |
-
};
|
| 211 |
-
|
| 212 |
-
/**
|
| 213 |
-
* Fetch multi-LLM explainability tree for a tracked object.
|
| 214 |
-
* @param {string} jobId
|
| 215 |
-
* @param {string} trackId
|
| 216 |
-
* @param {AbortSignal} [signal] — for aborting in-flight requests
|
| 217 |
-
* @returns {Promise<Object>} Explanation tree with consensus data
|
| 218 |
-
*/
|
| 219 |
-
APP.api.inspection.explainTrack = async function (jobId, trackId, signal) {
|
| 220 |
-
const base = APP.core.state.hf.baseUrl;
|
| 221 |
-
const url = `${base}/inspect/explain/${jobId}/${encodeURIComponent(trackId)}`;
|
| 222 |
-
const resp = await fetch(url, { signal });
|
| 223 |
-
if (!resp.ok) {
|
| 224 |
-
const body = await resp.json().catch(() => ({}));
|
| 225 |
-
throw new Error(body.detail || `Explain failed: ${resp.status}`);
|
| 226 |
-
}
|
| 227 |
-
return await resp.json();
|
| 228 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/config.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
APP.core.CONFIG = {
|
| 2 |
-
// API Endpoints will be loaded from window.API_CONFIG or defaults
|
| 3 |
-
BACKEND_BASE: (window.API_CONFIG?.BACKEND_BASE || window.API_CONFIG?.BASE_URL || "").replace(/\/$/, "") || window.location.origin,
|
| 4 |
-
HF_TOKEN: window.API_CONFIG?.HF_TOKEN || "",
|
| 5 |
-
PROXY_URL: (window.API_CONFIG?.PROXY_URL || "").trim(),
|
| 6 |
-
|
| 7 |
-
// Tracking Constants
|
| 8 |
-
REASON_INTERVAL: 30,
|
| 9 |
-
MAX_TRACKS: 50,
|
| 10 |
-
TRACK_PRUNE_MS: 1500,
|
| 11 |
-
TRACK_MATCH_THRESHOLD: 0.25,
|
| 12 |
-
|
| 13 |
-
// Default Queries
|
| 14 |
-
DEFAULT_QUERY_CLASSES: ["drone", "uav", "quadcopter", "fixed-wing", "missile", "person", "vehicle"]
|
| 15 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/demo.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
APP.core.demo = {};
|
| 2 |
-
|
| 3 |
-
APP.core.demo.data = null;
|
| 4 |
-
APP.core.demo.active = false;
|
| 5 |
-
|
| 6 |
-
APP.core.demo.load = async function () {
|
| 7 |
-
// Demo data is now loaded per-video via loadForVideo()
|
| 8 |
-
// This function is kept for compatibility but does nothing
|
| 9 |
-
};
|
| 10 |
-
|
| 11 |
-
APP.core.demo.getFrameData = function (currentTime) {
|
| 12 |
-
if (!APP.core.demo.data) return null;
|
| 13 |
-
|
| 14 |
-
// Use interpolation for keyframe format
|
| 15 |
-
if (APP.core.demo.data.format === "keyframes") {
|
| 16 |
-
return APP.core.demo.getFrameDataInterpolated(currentTime);
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
// Original logic for frame-by-frame data
|
| 20 |
-
const fps = APP.core.demo.data.fps || 30;
|
| 21 |
-
const frameIdx = Math.floor(currentTime * fps);
|
| 22 |
-
|
| 23 |
-
// Get tracks for this frame
|
| 24 |
-
// Handle both string and number keys if needed
|
| 25 |
-
const tracks = APP.core.demo.data.frames[frameIdx] || APP.core.demo.data.frames[frameIdx.toString()];
|
| 26 |
-
|
| 27 |
-
if (!tracks) return [];
|
| 28 |
-
return tracks;
|
| 29 |
-
};
|
| 30 |
-
|
| 31 |
-
APP.core.demo.enable = function (force = true) {
|
| 32 |
-
const { log } = APP.ui.logging;
|
| 33 |
-
APP.core.demo.active = force;
|
| 34 |
-
if (force) {
|
| 35 |
-
log("DEMO MODE ACTIVATED", "g");
|
| 36 |
-
const chipFeed = document.getElementById("chipFeed");
|
| 37 |
-
if (chipFeed) chipFeed.textContent = "MODE:DEMO";
|
| 38 |
-
}
|
| 39 |
-
};
|
| 40 |
-
|
| 41 |
-
// Keyframe interpolation for smooth radar movement
|
| 42 |
-
APP.core.demo._keyframeIndices = null;
|
| 43 |
-
|
| 44 |
-
APP.core.demo.getKeyframeIndices = function() {
|
| 45 |
-
if (!APP.core.demo.data || APP.core.demo.data.format !== "keyframes") return null;
|
| 46 |
-
if (APP.core.demo._keyframeIndices) return APP.core.demo._keyframeIndices;
|
| 47 |
-
|
| 48 |
-
const indices = Object.keys(APP.core.demo.data.keyframes)
|
| 49 |
-
.map(k => parseInt(k, 10))
|
| 50 |
-
.sort((a, b) => a - b);
|
| 51 |
-
|
| 52 |
-
APP.core.demo._keyframeIndices = indices;
|
| 53 |
-
return indices;
|
| 54 |
-
};
|
| 55 |
-
|
| 56 |
-
APP.core.demo.interpolateTrack = function(trackA, trackB, t) {
|
| 57 |
-
const { lerp } = APP.core.utils;
|
| 58 |
-
|
| 59 |
-
return {
|
| 60 |
-
id: trackA.id,
|
| 61 |
-
label: trackA.label,
|
| 62 |
-
bbox: {
|
| 63 |
-
x: lerp(trackA.bbox.x, trackB.bbox.x, t),
|
| 64 |
-
y: lerp(trackA.bbox.y, trackB.bbox.y, t),
|
| 65 |
-
w: lerp(trackA.bbox.w, trackB.bbox.w, t),
|
| 66 |
-
h: lerp(trackA.bbox.h, trackB.bbox.h, t)
|
| 67 |
-
},
|
| 68 |
-
angle_deg: trackA.angle_deg,
|
| 69 |
-
speed_kph: lerp(trackA.speed_kph, trackB.speed_kph, t),
|
| 70 |
-
depth_valid: false,
|
| 71 |
-
depth_est_m: null,
|
| 72 |
-
history: [],
|
| 73 |
-
predicted_path: []
|
| 74 |
-
};
|
| 75 |
-
};
|
| 76 |
-
|
| 77 |
-
APP.core.demo.getFrameDataInterpolated = function(currentTime) {
|
| 78 |
-
const data = APP.core.demo.data;
|
| 79 |
-
if (!data || data.format !== "keyframes") return null;
|
| 80 |
-
|
| 81 |
-
const fps = data.fps || 24;
|
| 82 |
-
const frameIdx = Math.floor(currentTime * fps);
|
| 83 |
-
const keyframes = APP.core.demo.getKeyframeIndices();
|
| 84 |
-
|
| 85 |
-
if (!keyframes || keyframes.length === 0) return [];
|
| 86 |
-
|
| 87 |
-
// Find surrounding keyframes
|
| 88 |
-
let beforeIdx = keyframes[0];
|
| 89 |
-
let afterIdx = keyframes[keyframes.length - 1];
|
| 90 |
-
|
| 91 |
-
for (let i = 0; i < keyframes.length; i++) {
|
| 92 |
-
if (keyframes[i] <= frameIdx) beforeIdx = keyframes[i];
|
| 93 |
-
if (keyframes[i] >= frameIdx) { afterIdx = keyframes[i]; break; }
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
// Edge cases
|
| 97 |
-
if (frameIdx <= keyframes[0]) return data.keyframes[keyframes[0]] || [];
|
| 98 |
-
if (frameIdx >= keyframes[keyframes.length - 1]) return data.keyframes[keyframes[keyframes.length - 1]] || [];
|
| 99 |
-
|
| 100 |
-
// Interpolation factor
|
| 101 |
-
const t = (beforeIdx === afterIdx) ? 0 : (frameIdx - beforeIdx) / (afterIdx - beforeIdx);
|
| 102 |
-
|
| 103 |
-
const tracksBefore = data.keyframes[beforeIdx] || [];
|
| 104 |
-
const tracksAfter = data.keyframes[afterIdx] || [];
|
| 105 |
-
|
| 106 |
-
// Match by ID and interpolate
|
| 107 |
-
const result = [];
|
| 108 |
-
for (const trackA of tracksBefore) {
|
| 109 |
-
const trackB = tracksAfter.find(tr => tr.id === trackA.id);
|
| 110 |
-
if (trackB) {
|
| 111 |
-
result.push(APP.core.demo.interpolateTrack(trackA, trackB, t));
|
| 112 |
-
}
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
return result;
|
| 116 |
-
};
|
| 117 |
-
|
| 118 |
-
// Video-specific loading for demo tracks
|
| 119 |
-
APP.core.demo.loadForVideo = async function(videoName) {
|
| 120 |
-
const { log } = APP.ui.logging;
|
| 121 |
-
if (videoName.toLowerCase().includes("enhance_video_movement")) {
|
| 122 |
-
// Use global variable injected by helicopter_demo_data.js script tag (CORS-safe)
|
| 123 |
-
if (window.HELICOPTER_DEMO_DATA) {
|
| 124 |
-
APP.core.demo.data = window.HELICOPTER_DEMO_DATA;
|
| 125 |
-
APP.core.demo._keyframeIndices = null; // Reset cache
|
| 126 |
-
log("Helicopter demo tracks loaded (CORS-safe mode).", "g");
|
| 127 |
-
return;
|
| 128 |
-
}
|
| 129 |
-
// Fallback to fetch (works when served from HTTP server)
|
| 130 |
-
try {
|
| 131 |
-
const resp = await fetch("data/helicopter_demo_tracks.json");
|
| 132 |
-
if (resp.ok) {
|
| 133 |
-
APP.core.demo.data = await resp.json();
|
| 134 |
-
APP.core.demo._keyframeIndices = null; // Reset cache
|
| 135 |
-
log("Helicopter demo tracks loaded.", "g");
|
| 136 |
-
}
|
| 137 |
-
} catch (err) {
|
| 138 |
-
console.warn("Failed to load helicopter demo tracks:", err);
|
| 139 |
-
}
|
| 140 |
-
}
|
| 141 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/gptMapping.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* gptMapping.js — canonical GPT-raw → features field mapping.
|
| 3 |
-
*
|
| 4 |
-
* Replaces 4 identical inline mapping blocks across main.js, client.js,
|
| 5 |
-
* and tracker.js (2 locations).
|
| 6 |
-
*/
|
| 7 |
-
APP.core.gptMapping = {};
|
| 8 |
-
|
| 9 |
-
/** Frozen assessment-status string constants. */
|
| 10 |
-
APP.core.gptMapping.STATUS = Object.freeze({
|
| 11 |
-
ASSESSED: "ASSESSED",
|
| 12 |
-
UNASSESSED: "UNASSESSED",
|
| 13 |
-
STALE: "STALE",
|
| 14 |
-
PENDING_GPT: "PENDING_GPT",
|
| 15 |
-
});
|
| 16 |
-
|
| 17 |
-
/**
|
| 18 |
-
* Build a features object from a gpt_raw payload.
|
| 19 |
-
*
|
| 20 |
-
* @param {Object|null|undefined} gptRaw - The gpt_raw dict from a detection.
|
| 21 |
-
* @returns {Object} Features key-value map (empty object if gptRaw is falsy).
|
| 22 |
-
*/
|
| 23 |
-
APP.core.gptMapping.buildFeatures = function (gptRaw) {
|
| 24 |
-
if (!gptRaw) return {};
|
| 25 |
-
const features = {};
|
| 26 |
-
for (const [k, v] of Object.entries(gptRaw)) {
|
| 27 |
-
if (k === "satisfies" || k === "reason") continue;
|
| 28 |
-
features[k] = String(v);
|
| 29 |
-
}
|
| 30 |
-
if (gptRaw.satisfies !== undefined) {
|
| 31 |
-
features["Satisfies"] = gptRaw.satisfies === true ? "Yes" : gptRaw.satisfies === false ? "No" : "—";
|
| 32 |
-
}
|
| 33 |
-
if (gptRaw.reason) {
|
| 34 |
-
features["Reason"] = gptRaw.reason;
|
| 35 |
-
}
|
| 36 |
-
return features;
|
| 37 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/physics.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
| 1 |
-
APP.core.physics = {};
|
| 2 |
-
|
| 3 |
-
APP.core.physics.defaultAimpoint = function (label) {
|
| 4 |
-
const l = (label || "object").toLowerCase();
|
| 5 |
-
if (l.includes("airplane") || l.includes("drone") || l.includes("uav") || l.includes("kite") || l.includes("bird")) {
|
| 6 |
-
return { relx: 0.62, rely: 0.55, label: "engine" };
|
| 7 |
-
}
|
| 8 |
-
if (l.includes("helicopter")) {
|
| 9 |
-
return { relx: 0.50, rely: 0.45, label: "rotor_hub" };
|
| 10 |
-
}
|
| 11 |
-
if (l.includes("boat") || l.includes("ship")) {
|
| 12 |
-
return { relx: 0.60, rely: 0.55, label: "bridge/engine" };
|
| 13 |
-
}
|
| 14 |
-
if (l.includes("truck") || l.includes("car")) {
|
| 15 |
-
return { relx: 0.55, rely: 0.62, label: "engine_block" };
|
| 16 |
-
}
|
| 17 |
-
return { relx: 0.50, rely: 0.55, label: "center_mass" };
|
| 18 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/state.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
APP.core.state = {
|
| 2 |
-
videoUrl: null,
|
| 3 |
-
videoFile: null,
|
| 4 |
-
videoLoaded: false,
|
| 5 |
-
useProcessedFeed: false,
|
| 6 |
-
useDepthFeed: false, // Flag for depth view (Tab 2 video)
|
| 7 |
-
hasReasoned: false,
|
| 8 |
-
isReasoning: false, // Flag to prevent concurrent Reason executions
|
| 9 |
-
|
| 10 |
-
hf: {
|
| 11 |
-
// Will be properly initialized after CONFIG loads
|
| 12 |
-
baseUrl: (window.API_CONFIG?.BACKEND_BASE || window.API_CONFIG?.BASE_URL || "").replace(/\/$/, "") || window.location.origin,
|
| 13 |
-
detector: "auto",
|
| 14 |
-
asyncJobId: null, // Current job ID from /detect/async
|
| 15 |
-
completedJobId: null, // Preserved job ID for post-completion track sync
|
| 16 |
-
asyncPollInterval: null, // Polling timer handle
|
| 17 |
-
statusUrl: null, // Status polling URL
|
| 18 |
-
videoUrl: null, // Final video URL
|
| 19 |
-
asyncStatus: "idle", // "idle"|"processing"|"completed"|"failed"
|
| 20 |
-
asyncProgress: null, // Progress data from status endpoint
|
| 21 |
-
queries: [], // Mission objective used as query
|
| 22 |
-
processedUrl: null,
|
| 23 |
-
processedBlob: null,
|
| 24 |
-
depthVideoUrl: null, // Depth video URL
|
| 25 |
-
depthBlob: null, // Depth video blob
|
| 26 |
-
summary: null,
|
| 27 |
-
fps: null, // actual video FPS from backend summary
|
| 28 |
-
totalFrames: null, // actual total frame count from backend
|
| 29 |
-
busy: false,
|
| 30 |
-
lastError: null,
|
| 31 |
-
missionSpec: null,
|
| 32 |
-
mode: null // "object_detection"|"segmentation"|"drone_detection"
|
| 33 |
-
},
|
| 34 |
-
|
| 35 |
-
detector: {
|
| 36 |
-
mode: "coco",
|
| 37 |
-
kind: "object",
|
| 38 |
-
loaded: false,
|
| 39 |
-
model: null,
|
| 40 |
-
loading: false,
|
| 41 |
-
cocoBlocked: false,
|
| 42 |
-
hfTrackingWarned: false
|
| 43 |
-
},
|
| 44 |
-
|
| 45 |
-
tracker: {
|
| 46 |
-
mode: "iou",
|
| 47 |
-
tracks: [],
|
| 48 |
-
nextId: 1,
|
| 49 |
-
lastDetTime: 0,
|
| 50 |
-
lastHFSync: 0,
|
| 51 |
-
running: false,
|
| 52 |
-
selectedTrackId: null,
|
| 53 |
-
lastFrameTime: 0,
|
| 54 |
-
frameCount: 0,
|
| 55 |
-
_lastCardRenderFrame: 0, // Frame count at last card render
|
| 56 |
-
heatmap: {}, // { frameIdx: trackCount } for timeline heatmap
|
| 57 |
-
assessmentCache: {} // { track_id: { gpt_raw, satisfies, reason, ... } }
|
| 58 |
-
},
|
| 59 |
-
|
| 60 |
-
frame: {
|
| 61 |
-
w: 1280,
|
| 62 |
-
h: 720,
|
| 63 |
-
bitmap: null
|
| 64 |
-
},
|
| 65 |
-
|
| 66 |
-
detections: [], // from Tab 1
|
| 67 |
-
selectedIds: [],
|
| 68 |
-
|
| 69 |
-
ui: {
|
| 70 |
-
cursorMode: "on",
|
| 71 |
-
agentCursor: { x: 0.65, y: 0.28, vx: 0, vy: 0, visible: false, target: null, mode: "idle", t0: 0 }
|
| 72 |
-
},
|
| 73 |
-
|
| 74 |
-
inspection: {
|
| 75 |
-
visible: false,
|
| 76 |
-
trackId: null,
|
| 77 |
-
frameIdx: null,
|
| 78 |
-
frameImageUrl: null,
|
| 79 |
-
loading: false,
|
| 80 |
-
error: null,
|
| 81 |
-
expandedQuadrant: null, // null | "seg" | "edge" | "depth" | "3d"
|
| 82 |
-
|
| 83 |
-
cache: {
|
| 84 |
-
seg: null,
|
| 85 |
-
edge: null,
|
| 86 |
-
depth: null,
|
| 87 |
-
pointcloud: null
|
| 88 |
-
},
|
| 89 |
-
|
| 90 |
-
explanationCache: {} // track_id -> explanation result (survives frame changes)
|
| 91 |
-
}
|
| 92 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/timeline.js
DELETED
|
@@ -1,167 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* timeline.js — Lazy heatmap timeline bar for Tab 2 (Track & Monitor).
|
| 3 |
-
*
|
| 4 |
-
* Renders a thin canvas bar showing:
|
| 5 |
-
* - Detection density heatmap (colored by track count per frame)
|
| 6 |
-
* - White playhead at current video position
|
| 7 |
-
* - Click-to-seek interaction
|
| 8 |
-
*
|
| 9 |
-
* Heatmap data is populated lazily by tracker.syncWithBackend() writing
|
| 10 |
-
* track counts into state.tracker.heatmap[frameIdx].
|
| 11 |
-
*/
|
| 12 |
-
APP.core.timeline = {};
|
| 13 |
-
|
| 14 |
-
(function () {
|
| 15 |
-
const COLORS = {
|
| 16 |
-
bg: "#111827", // unvisited
|
| 17 |
-
empty: "#1e293b", // visited, 0 tracks
|
| 18 |
-
low: "rgba(59, 130, 246, 0.35)", // 1-2 tracks
|
| 19 |
-
mid: "rgba(59, 130, 246, 0.65)", // 3-4 tracks
|
| 20 |
-
high: "rgba(96, 165, 250, 0.9)", // 5+ tracks
|
| 21 |
-
playhead: "#ffffff",
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
function colorForCount(count) {
|
| 25 |
-
if (count === undefined) return COLORS.bg;
|
| 26 |
-
if (count === 0) return COLORS.empty;
|
| 27 |
-
if (count <= 2) return COLORS.low;
|
| 28 |
-
if (count <= 4) return COLORS.mid;
|
| 29 |
-
return COLORS.high;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
/** Render the timeline heatmap + playhead. Called every RAF frame. */
|
| 33 |
-
APP.core.timeline.render = function () {
|
| 34 |
-
const { state } = APP.core;
|
| 35 |
-
const { $ } = APP.core.utils;
|
| 36 |
-
const canvas = $("#timelineBar");
|
| 37 |
-
const video = $("#videoEngage");
|
| 38 |
-
if (!canvas || !video) return;
|
| 39 |
-
|
| 40 |
-
// Cache duration once metadata loads (video.duration is NaN before that)
|
| 41 |
-
if (video.duration && isFinite(video.duration)) {
|
| 42 |
-
APP.core.timeline._cachedDuration = video.duration;
|
| 43 |
-
}
|
| 44 |
-
const duration = APP.core.timeline._cachedDuration;
|
| 45 |
-
if (!duration) return;
|
| 46 |
-
|
| 47 |
-
const wrap = canvas.parentElement;
|
| 48 |
-
const rect = wrap.getBoundingClientRect();
|
| 49 |
-
const dpr = window.devicePixelRatio || 1;
|
| 50 |
-
const w = Math.floor(rect.width);
|
| 51 |
-
const h = 14;
|
| 52 |
-
|
| 53 |
-
if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
|
| 54 |
-
canvas.width = w * dpr;
|
| 55 |
-
canvas.height = h * dpr;
|
| 56 |
-
canvas.style.width = w + "px";
|
| 57 |
-
canvas.style.height = h + "px";
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
const ctx = canvas.getContext("2d");
|
| 61 |
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
| 62 |
-
|
| 63 |
-
const fps = APP.core.state.hf.fps || 30;
|
| 64 |
-
const totalFrames = Math.ceil(duration * fps);
|
| 65 |
-
if (totalFrames <= 0) return;
|
| 66 |
-
|
| 67 |
-
const heatmap = state.tracker.heatmap;
|
| 68 |
-
|
| 69 |
-
// Draw heatmap segments
|
| 70 |
-
// Group consecutive pixels mapping to same color to reduce draw calls
|
| 71 |
-
const pxPerFrame = w / totalFrames;
|
| 72 |
-
|
| 73 |
-
if (pxPerFrame >= 1) {
|
| 74 |
-
// Enough room to draw per-frame
|
| 75 |
-
for (let f = 0; f < totalFrames; f++) {
|
| 76 |
-
const x = Math.floor(f * pxPerFrame);
|
| 77 |
-
const nx = Math.floor((f + 1) * pxPerFrame);
|
| 78 |
-
ctx.fillStyle = colorForCount(heatmap[f]);
|
| 79 |
-
ctx.fillRect(x, 0, Math.max(nx - x, 1), h);
|
| 80 |
-
}
|
| 81 |
-
} else {
|
| 82 |
-
// More frames than pixels — bucket frames per pixel
|
| 83 |
-
for (let px = 0; px < w; px++) {
|
| 84 |
-
const fStart = Math.floor((px / w) * totalFrames);
|
| 85 |
-
const fEnd = Math.floor(((px + 1) / w) * totalFrames);
|
| 86 |
-
let maxCount;
|
| 87 |
-
for (let f = fStart; f < fEnd; f++) {
|
| 88 |
-
const c = heatmap[f];
|
| 89 |
-
if (c !== undefined && (maxCount === undefined || c > maxCount)) {
|
| 90 |
-
maxCount = c;
|
| 91 |
-
}
|
| 92 |
-
}
|
| 93 |
-
ctx.fillStyle = colorForCount(maxCount);
|
| 94 |
-
ctx.fillRect(px, 0, 1, h);
|
| 95 |
-
}
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
// Draw playhead
|
| 99 |
-
const progress = video.currentTime / duration;
|
| 100 |
-
const px = Math.round(progress * w);
|
| 101 |
-
ctx.fillStyle = COLORS.playhead;
|
| 102 |
-
ctx.fillRect(px - 1, 0, 2, h);
|
| 103 |
-
};
|
| 104 |
-
|
| 105 |
-
/** Initialize click-to-seek on the timeline canvas. */
|
| 106 |
-
APP.core.timeline.init = function () {
|
| 107 |
-
const { $ } = APP.core.utils;
|
| 108 |
-
const canvas = $("#timelineBar");
|
| 109 |
-
const video = $("#videoEngage");
|
| 110 |
-
if (!canvas || !video) return;
|
| 111 |
-
|
| 112 |
-
let dragging = false;
|
| 113 |
-
|
| 114 |
-
function seek(e) {
|
| 115 |
-
const rect = canvas.getBoundingClientRect();
|
| 116 |
-
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
| 117 |
-
const ratio = x / rect.width;
|
| 118 |
-
const dur = APP.core.timeline._cachedDuration || video.duration;
|
| 119 |
-
if (dur && isFinite(dur)) {
|
| 120 |
-
video.currentTime = ratio * dur;
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
canvas.addEventListener("mousedown", function (e) {
|
| 125 |
-
dragging = true;
|
| 126 |
-
seek(e);
|
| 127 |
-
});
|
| 128 |
-
window.addEventListener("mousemove", function (e) {
|
| 129 |
-
if (dragging) seek(e);
|
| 130 |
-
});
|
| 131 |
-
window.addEventListener("mouseup", function () {
|
| 132 |
-
dragging = false;
|
| 133 |
-
});
|
| 134 |
-
};
|
| 135 |
-
|
| 136 |
-
/** Fetch track summary from backend and populate heatmap + duration. */
|
| 137 |
-
APP.core.timeline.loadSummary = async function () {
|
| 138 |
-
const { state } = APP.core;
|
| 139 |
-
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 140 |
-
if (!jobId || !state.hf.baseUrl) return;
|
| 141 |
-
|
| 142 |
-
try {
|
| 143 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/tracks/${jobId}/summary`);
|
| 144 |
-
if (!resp.ok) return;
|
| 145 |
-
|
| 146 |
-
const data = await resp.json();
|
| 147 |
-
const frames = data.frames || {};
|
| 148 |
-
|
| 149 |
-
// Populate heatmap — keys come as strings from JSON, convert to int
|
| 150 |
-
state.tracker.heatmap = {};
|
| 151 |
-
for (const [idx, count] of Object.entries(frames)) {
|
| 152 |
-
state.tracker.heatmap[parseInt(idx, 10)] = count;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
// Cache duration from backend metadata (bypass video.duration dependency)
|
| 156 |
-
if (data.total_frames > 0 && data.fps > 0) {
|
| 157 |
-
APP.core.timeline._cachedDuration = data.total_frames / data.fps;
|
| 158 |
-
state.hf.fps = data.fps;
|
| 159 |
-
state.hf.totalFrames = data.total_frames;
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
console.log(`[timeline] Loaded summary: ${Object.keys(frames).length} frames, duration=${APP.core.timeline._cachedDuration}s`);
|
| 163 |
-
} catch (e) {
|
| 164 |
-
console.warn("[timeline] Failed to load summary", e);
|
| 165 |
-
}
|
| 166 |
-
};
|
| 167 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/tracker.js
DELETED
|
@@ -1,288 +0,0 @@
|
|
| 1 |
-
APP.core.tracker = {};
|
| 2 |
-
|
| 3 |
-
APP.core.tracker.matchAndUpdateTracks = function (dets, dtSec) {
|
| 4 |
-
const { state } = APP.core;
|
| 5 |
-
const { CONFIG } = APP.core;
|
| 6 |
-
const { normBBox, lerp, now, $ } = APP.core.utils;
|
| 7 |
-
const { log } = APP.ui.logging;
|
| 8 |
-
|
| 9 |
-
const videoEngage = $("#videoEngage");
|
| 10 |
-
|
| 11 |
-
if (!videoEngage) return;
|
| 12 |
-
|
| 13 |
-
// IOU helper
|
| 14 |
-
function iou(a, b) {
|
| 15 |
-
const ax2 = a.x + a.w, ay2 = a.y + a.h;
|
| 16 |
-
const bx2 = b.x + b.w, by2 = b.y + b.h;
|
| 17 |
-
const ix1 = Math.max(a.x, b.x), iy1 = Math.max(a.y, b.y);
|
| 18 |
-
const ix2 = Math.min(ax2, bx2), iy2 = Math.min(ay2, by2);
|
| 19 |
-
const iw = Math.max(0, ix2 - ix1), ih = Math.max(0, iy2 - iy1);
|
| 20 |
-
const inter = iw * ih;
|
| 21 |
-
const ua = a.w * a.h + b.w * b.h - inter;
|
| 22 |
-
return ua <= 0 ? 0 : inter / ua;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
// Convert detections to bbox in video coordinates
|
| 26 |
-
const w = videoEngage.videoWidth || state.frame.w;
|
| 27 |
-
const h = videoEngage.videoHeight || state.frame.h;
|
| 28 |
-
|
| 29 |
-
const detObjs = dets.map(d => ({
|
| 30 |
-
bbox: normBBox(d.bbox, w, h),
|
| 31 |
-
label: d.class,
|
| 32 |
-
score: d.score,
|
| 33 |
-
depth_rel: Number.isFinite(d.depth_rel) ? d.depth_rel : null,
|
| 34 |
-
depth_est_m: d.depth_est_m,
|
| 35 |
-
depth_valid: d.depth_valid
|
| 36 |
-
}));
|
| 37 |
-
|
| 38 |
-
// mark all tracks as unmatched
|
| 39 |
-
const tracks = state.tracker.tracks;
|
| 40 |
-
const used = new Set();
|
| 41 |
-
|
| 42 |
-
for (const tr of tracks) {
|
| 43 |
-
let best = null;
|
| 44 |
-
let bestI = 0.0;
|
| 45 |
-
let bestIdx = -1;
|
| 46 |
-
for (let i = 0; i < detObjs.length; i++) {
|
| 47 |
-
if (used.has(i)) continue;
|
| 48 |
-
const IoU = iou(tr.bbox, detObjs[i].bbox);
|
| 49 |
-
if (IoU > bestI) {
|
| 50 |
-
bestI = IoU;
|
| 51 |
-
best = detObjs[i];
|
| 52 |
-
bestIdx = i;
|
| 53 |
-
}
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
// Strict matching threshold
|
| 57 |
-
if (best && bestI >= CONFIG.TRACK_MATCH_THRESHOLD) {
|
| 58 |
-
used.add(bestIdx);
|
| 59 |
-
|
| 60 |
-
// Velocity with Exponential Moving Average (EMA) for smoothing
|
| 61 |
-
const cx0 = tr.bbox.x + tr.bbox.w * 0.5;
|
| 62 |
-
const cy0 = tr.bbox.y + tr.bbox.h * 0.5;
|
| 63 |
-
const cx1 = best.bbox.x + best.bbox.w * 0.5;
|
| 64 |
-
const cy1 = best.bbox.y + best.bbox.h * 0.5;
|
| 65 |
-
|
| 66 |
-
const rawVx = (cx1 - cx0) / Math.max(1e-3, dtSec);
|
| 67 |
-
const rawVy = (cy1 - cy0) / Math.max(1e-3, dtSec);
|
| 68 |
-
|
| 69 |
-
// Alpha of 0.3 means 30% new value, 70% history
|
| 70 |
-
tr.vx = tr.vx * 0.7 + rawVx * 0.3;
|
| 71 |
-
tr.vy = tr.vy * 0.7 + rawVy * 0.3;
|
| 72 |
-
|
| 73 |
-
// smooth bbox update
|
| 74 |
-
tr.bbox.x = lerp(tr.bbox.x, best.bbox.x, 0.7);
|
| 75 |
-
tr.bbox.y = lerp(tr.bbox.y, best.bbox.y, 0.7);
|
| 76 |
-
tr.bbox.w = lerp(tr.bbox.w, best.bbox.w, 0.6);
|
| 77 |
-
tr.bbox.h = lerp(tr.bbox.h, best.bbox.h, 0.6);
|
| 78 |
-
|
| 79 |
-
// Logic: Only update label if the new detection is highly confident
|
| 80 |
-
// AND the current track doesn't have a "premium" label (like 'drone').
|
| 81 |
-
const protectedLabels = ["drone", "uav", "missile"];
|
| 82 |
-
const isProtected = protectedLabels.some(l => (tr.label || "").toLowerCase().includes(l));
|
| 83 |
-
|
| 84 |
-
if (!isProtected || (best.label && protectedLabels.some(l => best.label.toLowerCase().includes(l)))) {
|
| 85 |
-
tr.label = best.label || tr.label;
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
tr.score = best.score || tr.score;
|
| 89 |
-
if (Number.isFinite(best.depth_rel)) {
|
| 90 |
-
tr.depth_rel = best.depth_rel;
|
| 91 |
-
}
|
| 92 |
-
if (best.depth_valid) {
|
| 93 |
-
// EMA Smoothing
|
| 94 |
-
const newD = best.depth_est_m;
|
| 95 |
-
if (tr.depth_est_m == null) tr.depth_est_m = newD;
|
| 96 |
-
else tr.depth_est_m = tr.depth_est_m * 0.7 + newD * 0.3;
|
| 97 |
-
tr.depth_valid = true;
|
| 98 |
-
}
|
| 99 |
-
tr.lastSeen = now();
|
| 100 |
-
} else {
|
| 101 |
-
// Decay velocity
|
| 102 |
-
tr.vx *= 0.9;
|
| 103 |
-
tr.vy *= 0.9;
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
// Limit total tracks
|
| 108 |
-
if (tracks.length < CONFIG.MAX_TRACKS) {
|
| 109 |
-
for (let i = 0; i < detObjs.length; i++) {
|
| 110 |
-
if (used.has(i)) continue;
|
| 111 |
-
// create new track only if big enough
|
| 112 |
-
const a = detObjs[i].bbox.w * detObjs[i].bbox.h;
|
| 113 |
-
if (a < (w * h) * 0.0005) continue;
|
| 114 |
-
|
| 115 |
-
const newId = String(state.tracker.nextId++);
|
| 116 |
-
tracks.push({
|
| 117 |
-
id: newId,
|
| 118 |
-
label: detObjs[i].label,
|
| 119 |
-
bbox: { ...detObjs[i].bbox },
|
| 120 |
-
score: detObjs[i].score,
|
| 121 |
-
depth_rel: detObjs[i].depth_rel,
|
| 122 |
-
depth_est_m: detObjs[i].depth_est_m,
|
| 123 |
-
depth_valid: detObjs[i].depth_valid,
|
| 124 |
-
satisfies: null,
|
| 125 |
-
reason: null,
|
| 126 |
-
lastSeen: now(),
|
| 127 |
-
vx: 0, vy: 0,
|
| 128 |
-
killed: false,
|
| 129 |
-
state: "TRACK",
|
| 130 |
-
});
|
| 131 |
-
log(`New track created: ${newId} (${detObjs[i].label})`, "t");
|
| 132 |
-
}
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
// prune old tracks
|
| 136 |
-
const tNow = now();
|
| 137 |
-
state.tracker.tracks = tracks.filter(tr => (tNow - tr.lastSeen) < CONFIG.TRACK_PRUNE_MS || tr.killed);
|
| 138 |
-
};
|
| 139 |
-
|
| 140 |
-
// Polling for backend tracks
|
| 141 |
-
APP.core.tracker.syncWithBackend = async function (frameIdx) {
|
| 142 |
-
const { state } = APP.core;
|
| 143 |
-
const { $ } = APP.core.utils;
|
| 144 |
-
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 145 |
-
|
| 146 |
-
if (!jobId || !state.hf.baseUrl) return;
|
| 147 |
-
|
| 148 |
-
try {
|
| 149 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/tracks/${jobId}/${frameIdx}`);
|
| 150 |
-
if (!resp.ok) return;
|
| 151 |
-
|
| 152 |
-
const dets = await resp.json();
|
| 153 |
-
if (!dets || !Array.isArray(dets)) return;
|
| 154 |
-
|
| 155 |
-
// If backend returned empty for this frame, keep existing tracks
|
| 156 |
-
if (dets.length === 0) return;
|
| 157 |
-
|
| 158 |
-
// Transform backend format to frontend track format
|
| 159 |
-
// Backend: { bbox: [x1, y1, x2, y2], label: "car", track_id: "T01", angle_deg: 90, ... }
|
| 160 |
-
// Frontend: { id: "T01", bbox: {x,y,w,h}, label: "car", angle_deg: 90, ... }
|
| 161 |
-
|
| 162 |
-
const videoEngage = $("#videoEngage");
|
| 163 |
-
const w = videoEngage ? (videoEngage.videoWidth || state.frame.w) : state.frame.w;
|
| 164 |
-
const h = videoEngage ? (videoEngage.videoHeight || state.frame.h) : state.frame.h;
|
| 165 |
-
|
| 166 |
-
const newTracks = dets.map(d => {
|
| 167 |
-
const x = d.bbox[0], y = d.bbox[1];
|
| 168 |
-
const wBox = d.bbox[2] - d.bbox[0];
|
| 169 |
-
const hBox = d.bbox[3] - d.bbox[1];
|
| 170 |
-
|
| 171 |
-
// Normalize
|
| 172 |
-
const nx = x / w;
|
| 173 |
-
const ny = y / h;
|
| 174 |
-
const nw = wBox / w;
|
| 175 |
-
const nh = hBox / h;
|
| 176 |
-
|
| 177 |
-
return {
|
| 178 |
-
id: d.track_id || String(Math.floor(Math.random() * 1000)),
|
| 179 |
-
label: d.label,
|
| 180 |
-
bbox: { x: nx, y: ny, w: nw, h: nh },
|
| 181 |
-
score: d.score,
|
| 182 |
-
vx: 0,
|
| 183 |
-
vy: 0,
|
| 184 |
-
angle_deg: d.angle_deg,
|
| 185 |
-
speed_kph: d.speed_kph,
|
| 186 |
-
depth_est_m: d.depth_est_m,
|
| 187 |
-
depth_rel: d.depth_rel,
|
| 188 |
-
depth_valid: d.depth_valid,
|
| 189 |
-
// Satisfaction reasoning
|
| 190 |
-
satisfies: d.satisfies ?? null,
|
| 191 |
-
reason: d.reason || null,
|
| 192 |
-
// Mission relevance and assessment status
|
| 193 |
-
mission_relevant: d.mission_relevant ?? null,
|
| 194 |
-
relevance_reason: d.relevance_reason || null,
|
| 195 |
-
assessment_status: d.assessment_status || APP.core.gptMapping.STATUS.UNASSESSED,
|
| 196 |
-
assessment_frame_index: d.assessment_frame_index ?? null,
|
| 197 |
-
// GPT raw data for feature table
|
| 198 |
-
gpt_raw: d.gpt_raw || null,
|
| 199 |
-
features: APP.core.gptMapping.buildFeatures(d.gpt_raw),
|
| 200 |
-
// Keep UI state fields
|
| 201 |
-
lastSeen: Date.now(),
|
| 202 |
-
state: "TRACK"
|
| 203 |
-
};
|
| 204 |
-
});
|
| 205 |
-
|
| 206 |
-
// ── Phase 1: Update assessment cache from tracks that arrived with GPT data ──
|
| 207 |
-
const cache = state.tracker.assessmentCache;
|
| 208 |
-
for (const track of newTracks) {
|
| 209 |
-
if (track.gpt_raw || track.assessment_status === APP.core.gptMapping.STATUS.ASSESSED) {
|
| 210 |
-
cache[track.id] = {
|
| 211 |
-
gpt_raw: track.gpt_raw,
|
| 212 |
-
satisfies: track.satisfies,
|
| 213 |
-
reason: track.reason,
|
| 214 |
-
mission_relevant: track.mission_relevant,
|
| 215 |
-
relevance_reason: track.relevance_reason,
|
| 216 |
-
assessment_status: track.assessment_status,
|
| 217 |
-
assessment_frame_index: track.assessment_frame_index,
|
| 218 |
-
features: track.features,
|
| 219 |
-
};
|
| 220 |
-
}
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
// ── Phase 2: Carry forward cached assessment for tracks missing GPT data ──
|
| 224 |
-
for (const track of newTracks) {
|
| 225 |
-
if (track.gpt_raw) continue;
|
| 226 |
-
const cached = cache[track.id];
|
| 227 |
-
if (!cached) continue;
|
| 228 |
-
track.gpt_raw = cached.gpt_raw;
|
| 229 |
-
track.satisfies = cached.satisfies ?? track.satisfies;
|
| 230 |
-
track.reason = cached.reason || track.reason;
|
| 231 |
-
track.mission_relevant = cached.mission_relevant ?? track.mission_relevant;
|
| 232 |
-
track.relevance_reason = cached.relevance_reason || track.relevance_reason;
|
| 233 |
-
track.assessment_status = cached.assessment_status || APP.core.gptMapping.STATUS.ASSESSED;
|
| 234 |
-
track.assessment_frame_index = cached.assessment_frame_index ?? track.assessment_frame_index;
|
| 235 |
-
track.features = cached.features && Object.keys(cached.features).length > 0
|
| 236 |
-
? cached.features
|
| 237 |
-
: track.features;
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
// Check if mission verdicts exist (used for filtering cards, heatmap, logs)
|
| 241 |
-
const hasVerdicts = newTracks.some(t => t.satisfies === true || t.satisfies === false);
|
| 242 |
-
|
| 243 |
-
// Detect new objects before state update — only log MATCH items when verdicts exist
|
| 244 |
-
const oldIds = new Set(state.tracker.tracks.map(t => t.id));
|
| 245 |
-
const brandNew = newTracks.filter(t => !oldIds.has(t.id));
|
| 246 |
-
const logWorthy = hasVerdicts
|
| 247 |
-
? brandNew.filter(t => t.satisfies === true)
|
| 248 |
-
: brandNew;
|
| 249 |
-
if (logWorthy.length > 0) {
|
| 250 |
-
state.tracker._newObjectDetected = true;
|
| 251 |
-
APP.ui.logging.log(`New objects: ${logWorthy.map(t => t.id).join(", ")}`, "t");
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
// Cache track count for timeline heatmap (only MATCH items when verdicts exist)
|
| 255 |
-
const matchCount = hasVerdicts
|
| 256 |
-
? newTracks.filter(t => t.satisfies === true).length
|
| 257 |
-
: newTracks.length;
|
| 258 |
-
state.tracker.heatmap[frameIdx] = matchCount;
|
| 259 |
-
|
| 260 |
-
// Update state
|
| 261 |
-
state.tracker.tracks = newTracks;
|
| 262 |
-
state.detections = newTracks; // Keep synced
|
| 263 |
-
|
| 264 |
-
// Always trigger card re-render after backend sync (verdicts may have changed)
|
| 265 |
-
state.tracker._newObjectDetected = true;
|
| 266 |
-
|
| 267 |
-
} catch (e) {
|
| 268 |
-
console.warn("Track sync failed", e);
|
| 269 |
-
}
|
| 270 |
-
};
|
| 271 |
-
|
| 272 |
-
APP.core.tracker.predictTracks = function (dtSec) {
|
| 273 |
-
const { state } = APP.core;
|
| 274 |
-
const { $ } = APP.core.utils;
|
| 275 |
-
const videoEngage = $("#videoEngage");
|
| 276 |
-
if (!videoEngage) return;
|
| 277 |
-
const w = videoEngage.videoWidth || state.frame.w;
|
| 278 |
-
const h = videoEngage.videoHeight || state.frame.h;
|
| 279 |
-
|
| 280 |
-
// Simple clamp util locally or imported
|
| 281 |
-
const clamp = (val, min, max) => Math.min(max, Math.max(min, val));
|
| 282 |
-
|
| 283 |
-
state.tracker.tracks.forEach(tr => {
|
| 284 |
-
if (tr.killed) return;
|
| 285 |
-
tr.bbox.x = clamp(tr.bbox.x + (tr.vx || 0) * dtSec * 0.12, 0, w - 1);
|
| 286 |
-
tr.bbox.y = clamp(tr.bbox.y + (tr.vy || 0) * dtSec * 0.12, 0, h - 1);
|
| 287 |
-
});
|
| 288 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/utils.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
APP.core.utils = {};
|
| 2 |
-
|
| 3 |
-
APP.core.utils.$ = (sel, root = document) => root.querySelector(sel);
|
| 4 |
-
APP.core.utils.$$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
| 5 |
-
|
| 6 |
-
APP.core.utils.clamp = (x, a, b) => Math.min(b, Math.max(a, x));
|
| 7 |
-
APP.core.utils.lerp = (a, b, t) => a + (b - a) * t;
|
| 8 |
-
APP.core.utils.now = () => performance.now();
|
| 9 |
-
|
| 10 |
-
APP.core.utils.escapeHtml = function (s) {
|
| 11 |
-
return String(s).replace(/[&<>"']/g, m => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m]));
|
| 12 |
-
};
|
| 13 |
-
|
| 14 |
-
APP.core.utils.canvasToBlob = function (canvas, quality = 0.88) {
|
| 15 |
-
return new Promise((resolve, reject) => {
|
| 16 |
-
if (!canvas.toBlob) { reject(new Error("Canvas.toBlob not supported")); return; }
|
| 17 |
-
canvas.toBlob(blob => {
|
| 18 |
-
if (!blob) { reject(new Error("Canvas toBlob failed")); return; }
|
| 19 |
-
resolve(blob);
|
| 20 |
-
}, "image/jpeg", quality);
|
| 21 |
-
});
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
APP.core.utils.normBBox = function (bbox, w, h) {
|
| 25 |
-
const [x, y, bw, bh] = bbox;
|
| 26 |
-
return {
|
| 27 |
-
x: APP.core.utils.clamp(x, 0, w - 1),
|
| 28 |
-
y: APP.core.utils.clamp(y, 0, h - 1),
|
| 29 |
-
w: APP.core.utils.clamp(bw, 1, w),
|
| 30 |
-
h: APP.core.utils.clamp(bh, 1, h)
|
| 31 |
-
};
|
| 32 |
-
};
|
| 33 |
-
|
| 34 |
-
APP.core.utils.loadedScripts = new Map();
|
| 35 |
-
|
| 36 |
-
APP.core.utils.loadScriptOnce = function (key, src) {
|
| 37 |
-
return new Promise((resolve, reject) => {
|
| 38 |
-
if (APP.core.utils.loadedScripts.get(key) === "loaded") { resolve(); return; }
|
| 39 |
-
if (APP.core.utils.loadedScripts.get(key) === "loading") {
|
| 40 |
-
const iv = setInterval(() => {
|
| 41 |
-
if (APP.core.utils.loadedScripts.get(key) === "loaded") { clearInterval(iv); resolve(); }
|
| 42 |
-
if (APP.core.utils.loadedScripts.get(key) === "failed") { clearInterval(iv); reject(new Error("Script failed earlier")); }
|
| 43 |
-
}, 50);
|
| 44 |
-
return;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
APP.core.utils.loadedScripts.set(key, "loading");
|
| 48 |
-
const s = document.createElement("script");
|
| 49 |
-
s.src = src;
|
| 50 |
-
s.async = true;
|
| 51 |
-
s.onload = () => { APP.core.utils.loadedScripts.set(key, "loaded"); resolve(); };
|
| 52 |
-
s.onerror = () => { APP.core.utils.loadedScripts.set(key, "failed"); reject(new Error(`Failed to load ${src}`)); };
|
| 53 |
-
document.head.appendChild(s);
|
| 54 |
-
});
|
| 55 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/video.js
DELETED
|
@@ -1,367 +0,0 @@
|
|
| 1 |
-
// Video management: loading, unloading, first frame capture, depth handling
|
| 2 |
-
APP.core.video = {};
|
| 3 |
-
|
| 4 |
-
APP.core.video.frameToBitmap = async function (videoEl) {
|
| 5 |
-
const w = videoEl.videoWidth || 1280;
|
| 6 |
-
const h = videoEl.videoHeight || 720;
|
| 7 |
-
const canvas = document.createElement("canvas");
|
| 8 |
-
canvas.width = w;
|
| 9 |
-
canvas.height = h;
|
| 10 |
-
const ctx = canvas.getContext("2d");
|
| 11 |
-
ctx.drawImage(videoEl, 0, 0, w, h);
|
| 12 |
-
return canvas;
|
| 13 |
-
};
|
| 14 |
-
|
| 15 |
-
APP.core.video.seekTo = function (videoEl, time) {
|
| 16 |
-
return new Promise((resolve) => {
|
| 17 |
-
if (!videoEl) { resolve(); return; }
|
| 18 |
-
videoEl.currentTime = Math.max(0, time);
|
| 19 |
-
videoEl.onseeked = () => resolve();
|
| 20 |
-
setTimeout(resolve, 600);
|
| 21 |
-
});
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
APP.core.video.unloadVideo = async function (options = {}) {
|
| 25 |
-
const { state } = APP.core;
|
| 26 |
-
const { $, $$ } = APP.core.utils;
|
| 27 |
-
const { log, setStatus, setHfStatus } = APP.ui.logging;
|
| 28 |
-
const preserveInput = !!options.preserveInput;
|
| 29 |
-
|
| 30 |
-
// Stop polling if running
|
| 31 |
-
if (state.hf.asyncPollInterval) {
|
| 32 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 33 |
-
state.hf.asyncPollInterval = null;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
// Revoke blob URLs
|
| 37 |
-
if (state.videoUrl && state.videoUrl.startsWith("blob:")) {
|
| 38 |
-
URL.revokeObjectURL(state.videoUrl);
|
| 39 |
-
}
|
| 40 |
-
if (state.hf.processedUrl && state.hf.processedUrl.startsWith("blob:")) {
|
| 41 |
-
try { URL.revokeObjectURL(state.hf.processedUrl); } catch (_) { }
|
| 42 |
-
}
|
| 43 |
-
if (state.hf.depthVideoUrl && state.hf.depthVideoUrl.startsWith("blob:")) {
|
| 44 |
-
try { URL.revokeObjectURL(state.hf.depthVideoUrl); } catch (_) { }
|
| 45 |
-
}
|
| 46 |
-
// Reset state
|
| 47 |
-
state.videoUrl = null;
|
| 48 |
-
state.videoFile = null;
|
| 49 |
-
state.videoLoaded = false;
|
| 50 |
-
state.useProcessedFeed = false;
|
| 51 |
-
state.useDepthFeed = false;
|
| 52 |
-
|
| 53 |
-
state.hf.processedUrl = null;
|
| 54 |
-
state.hf.processedBlob = null;
|
| 55 |
-
state.hf.depthVideoUrl = null;
|
| 56 |
-
state.hf.depthBlob = null;
|
| 57 |
-
state.hf.summary = null;
|
| 58 |
-
state.hf.busy = false;
|
| 59 |
-
state.hf.lastError = null;
|
| 60 |
-
state.hf.asyncJobId = null;
|
| 61 |
-
state.hf.completedJobId = null;
|
| 62 |
-
state.hf.asyncStatus = "idle";
|
| 63 |
-
state.hf.videoUrl = null;
|
| 64 |
-
state.hf.fps = null;
|
| 65 |
-
state.hf.totalFrames = null;
|
| 66 |
-
|
| 67 |
-
setHfStatus("idle");
|
| 68 |
-
state.hasReasoned = false;
|
| 69 |
-
state.isReasoning = false;
|
| 70 |
-
|
| 71 |
-
// Reset button states
|
| 72 |
-
const btnReason = $("#btnReason");
|
| 73 |
-
const btnCancelReason = $("#btnCancelReason");
|
| 74 |
-
const btnEngage = $("#btnEngage");
|
| 75 |
-
|
| 76 |
-
if (btnReason) {
|
| 77 |
-
btnReason.disabled = false;
|
| 78 |
-
btnReason.style.opacity = "1";
|
| 79 |
-
btnReason.style.cursor = "pointer";
|
| 80 |
-
}
|
| 81 |
-
if (btnCancelReason) btnCancelReason.style.display = "none";
|
| 82 |
-
if (btnEngage) btnEngage.disabled = true;
|
| 83 |
-
|
| 84 |
-
state.detections = [];
|
| 85 |
-
state.selectedIds = [];
|
| 86 |
-
|
| 87 |
-
state.tracker.tracks = [];
|
| 88 |
-
state.tracker.nextId = 1;
|
| 89 |
-
state.tracker.running = false;
|
| 90 |
-
state.tracker.selectedTrackId = null;
|
| 91 |
-
state.tracker.assessmentCache = {};
|
| 92 |
-
state.inspection.explanationCache = {};
|
| 93 |
-
|
| 94 |
-
// Clear video elements
|
| 95 |
-
const videoEngage = $("#videoEngage");
|
| 96 |
-
const videoFile = $("#videoFile");
|
| 97 |
-
|
| 98 |
-
if (videoEngage) {
|
| 99 |
-
videoEngage.removeAttribute("src");
|
| 100 |
-
videoEngage.load();
|
| 101 |
-
}
|
| 102 |
-
if (!preserveInput && videoFile) {
|
| 103 |
-
videoFile.value = "";
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
// Update UI
|
| 107 |
-
const videoMeta = $("#videoMeta");
|
| 108 |
-
const engageEmpty = $("#engageEmpty");
|
| 109 |
-
const engageNote = $("#engageNote");
|
| 110 |
-
|
| 111 |
-
if (!preserveInput && videoMeta) videoMeta.textContent = "No file";
|
| 112 |
-
if (engageEmpty) engageEmpty.style.display = "flex";
|
| 113 |
-
if (engageNote) engageNote.textContent = "Awaiting video";
|
| 114 |
-
|
| 115 |
-
// Re-render UI components
|
| 116 |
-
if (APP.ui.cards.renderFrameTrackList) APP.ui.cards.renderFrameTrackList();
|
| 117 |
-
|
| 118 |
-
setStatus("warn", "STANDBY · No video loaded");
|
| 119 |
-
log("Video unloaded. Demo reset.", "w");
|
| 120 |
-
};
|
| 121 |
-
|
| 122 |
-
// Depth video handling
|
| 123 |
-
APP.core.video.fetchDepthVideo = async function () {
|
| 124 |
-
const { state } = APP.core;
|
| 125 |
-
const { log } = APP.ui.logging;
|
| 126 |
-
|
| 127 |
-
// Depth is optional - skip silently if no URL
|
| 128 |
-
if (!state.hf.depthVideoUrl) {
|
| 129 |
-
return;
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
try {
|
| 133 |
-
const resp = await fetch(state.hf.depthVideoUrl, { cache: "no-store" });
|
| 134 |
-
|
| 135 |
-
if (!resp.ok) {
|
| 136 |
-
// 404 = depth not enabled/available - this is fine, not an error
|
| 137 |
-
if (resp.status === 404) {
|
| 138 |
-
state.hf.depthVideoUrl = null;
|
| 139 |
-
return;
|
| 140 |
-
}
|
| 141 |
-
// 202 = still processing
|
| 142 |
-
if (resp.status === 202) {
|
| 143 |
-
return;
|
| 144 |
-
}
|
| 145 |
-
throw new Error(`Failed to fetch depth video: ${resp.statusText}`);
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
const nullOrigin = (window.location && window.location.origin) === "null" || (window.location && window.location.protocol === "file:");
|
| 149 |
-
if (nullOrigin) {
|
| 150 |
-
state.hf.depthBlob = null;
|
| 151 |
-
state.hf.depthVideoUrl = `${state.hf.depthVideoUrl}?t=${Date.now()}`;
|
| 152 |
-
log("Depth video ready (streaming URL)");
|
| 153 |
-
return;
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
const blob = await resp.blob();
|
| 157 |
-
state.hf.depthBlob = blob;
|
| 158 |
-
const blobUrl = URL.createObjectURL(blob);
|
| 159 |
-
state.hf.depthVideoUrl = blobUrl;
|
| 160 |
-
|
| 161 |
-
log(`Depth video ready (${(blob.size / 1024 / 1024).toFixed(1)} MB)`, "g");
|
| 162 |
-
APP.core.video.updateDepthChip();
|
| 163 |
-
} catch (err) {
|
| 164 |
-
// Don't log as error - depth is optional
|
| 165 |
-
state.hf.depthVideoUrl = null;
|
| 166 |
-
state.hf.depthBlob = null;
|
| 167 |
-
}
|
| 168 |
-
};
|
| 169 |
-
|
| 170 |
-
APP.core.video.fetchProcessedVideo = async function () {
|
| 171 |
-
const { state } = APP.core;
|
| 172 |
-
const { log } = APP.ui.logging;
|
| 173 |
-
const { $ } = APP.core.utils;
|
| 174 |
-
|
| 175 |
-
const resp = await fetch(state.hf.videoUrl, { cache: "no-store" });
|
| 176 |
-
|
| 177 |
-
if (!resp.ok) {
|
| 178 |
-
if (resp.status === 202) {
|
| 179 |
-
const err = new Error("Video still processing");
|
| 180 |
-
err.code = "VIDEO_PENDING";
|
| 181 |
-
throw err;
|
| 182 |
-
}
|
| 183 |
-
throw new Error(`Failed to fetch video: ${resp.statusText}`);
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
const nullOrigin = (window.location && window.location.origin) === "null" || (window.location && window.location.protocol === "file:");
|
| 187 |
-
if (nullOrigin) {
|
| 188 |
-
state.hf.processedBlob = null;
|
| 189 |
-
state.hf.processedUrl = `${state.hf.videoUrl}?t=${Date.now()}`;
|
| 190 |
-
const btnEngage = $("#btnEngage");
|
| 191 |
-
if (btnEngage) btnEngage.disabled = false;
|
| 192 |
-
log("Processed video ready (streaming URL)");
|
| 193 |
-
return;
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
const blob = await resp.blob();
|
| 197 |
-
|
| 198 |
-
if (state.hf.processedUrl && state.hf.processedUrl.startsWith("blob:")) {
|
| 199 |
-
URL.revokeObjectURL(state.hf.processedUrl);
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
state.hf.processedBlob = blob;
|
| 203 |
-
state.hf.processedUrl = URL.createObjectURL(blob);
|
| 204 |
-
|
| 205 |
-
const btnEngage = $("#btnEngage");
|
| 206 |
-
if (btnEngage) btnEngage.disabled = false;
|
| 207 |
-
log(`Processed video ready (${(blob.size / 1024 / 1024).toFixed(1)} MB)`);
|
| 208 |
-
};
|
| 209 |
-
|
| 210 |
-
APP.core.video.updateDepthChip = function () {
|
| 211 |
-
const { state } = APP.core;
|
| 212 |
-
const { $ } = APP.core.utils;
|
| 213 |
-
|
| 214 |
-
const chipDepth = $("#chipDepth");
|
| 215 |
-
if (!chipDepth) return;
|
| 216 |
-
|
| 217 |
-
if (state.hf.depthVideoUrl || state.hf.depthBlob) {
|
| 218 |
-
chipDepth.style.cursor = "pointer";
|
| 219 |
-
chipDepth.style.opacity = "1";
|
| 220 |
-
} else {
|
| 221 |
-
chipDepth.style.cursor = "not-allowed";
|
| 222 |
-
chipDepth.style.opacity = "0.5";
|
| 223 |
-
}
|
| 224 |
-
};
|
| 225 |
-
|
| 226 |
-
APP.core.video.toggleDepthView = function () {
|
| 227 |
-
const { state } = APP.core;
|
| 228 |
-
const { $, log } = APP.core.utils;
|
| 229 |
-
const { log: uiLog } = APP.ui.logging;
|
| 230 |
-
|
| 231 |
-
if (!state.hf.depthVideoUrl && !state.hf.depthBlob) {
|
| 232 |
-
uiLog("Depth video not available yet. Run Reason and wait for processing.", "w");
|
| 233 |
-
return;
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
state.useDepthFeed = !state.useDepthFeed;
|
| 237 |
-
|
| 238 |
-
const videoEngage = $("#videoEngage");
|
| 239 |
-
const chipDepth = $("#chipDepth");
|
| 240 |
-
|
| 241 |
-
if (state.useDepthFeed) {
|
| 242 |
-
if (chipDepth) chipDepth.textContent = "VIEW:DEPTH";
|
| 243 |
-
if (videoEngage) {
|
| 244 |
-
const currentTime = videoEngage.currentTime;
|
| 245 |
-
const wasPlaying = !videoEngage.paused;
|
| 246 |
-
videoEngage.src = state.hf.depthVideoUrl;
|
| 247 |
-
videoEngage.load();
|
| 248 |
-
videoEngage.currentTime = currentTime;
|
| 249 |
-
if (wasPlaying) videoEngage.play();
|
| 250 |
-
}
|
| 251 |
-
} else {
|
| 252 |
-
if (chipDepth) chipDepth.textContent = "VIEW:DEFAULT";
|
| 253 |
-
if (videoEngage) {
|
| 254 |
-
const currentTime = videoEngage.currentTime;
|
| 255 |
-
const wasPlaying = !videoEngage.paused;
|
| 256 |
-
const feedUrl = state.useProcessedFeed ? state.hf.processedUrl : state.videoUrl;
|
| 257 |
-
videoEngage.src = feedUrl;
|
| 258 |
-
videoEngage.load();
|
| 259 |
-
videoEngage.currentTime = currentTime;
|
| 260 |
-
if (wasPlaying) videoEngage.play();
|
| 261 |
-
}
|
| 262 |
-
}
|
| 263 |
-
};
|
| 264 |
-
|
| 265 |
-
APP.core.video.toggleProcessedFeed = function () {
|
| 266 |
-
const { state } = APP.core;
|
| 267 |
-
const { $ } = APP.core.utils;
|
| 268 |
-
const { log } = APP.ui.logging;
|
| 269 |
-
|
| 270 |
-
if (!state.hf.processedUrl) {
|
| 271 |
-
log("Processed video not available yet", "w");
|
| 272 |
-
return;
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
state.useProcessedFeed = !state.useProcessedFeed;
|
| 276 |
-
state.useDepthFeed = false; // Reset depth view when switching feeds
|
| 277 |
-
|
| 278 |
-
const videoEngage = $("#videoEngage");
|
| 279 |
-
const chipFeed = $("#chipFeed");
|
| 280 |
-
const chipDepth = $("#chipDepth");
|
| 281 |
-
|
| 282 |
-
if (state.useProcessedFeed) {
|
| 283 |
-
if (chipFeed) chipFeed.textContent = "FEED:HF";
|
| 284 |
-
if (videoEngage) {
|
| 285 |
-
const currentTime = videoEngage.currentTime;
|
| 286 |
-
const wasPlaying = !videoEngage.paused;
|
| 287 |
-
videoEngage.src = state.hf.processedUrl;
|
| 288 |
-
videoEngage.load();
|
| 289 |
-
videoEngage.currentTime = currentTime;
|
| 290 |
-
if (wasPlaying) videoEngage.play();
|
| 291 |
-
}
|
| 292 |
-
} else {
|
| 293 |
-
if (chipFeed) chipFeed.textContent = "FEED:RAW";
|
| 294 |
-
if (videoEngage) {
|
| 295 |
-
const currentTime = videoEngage.currentTime;
|
| 296 |
-
const wasPlaying = !videoEngage.paused;
|
| 297 |
-
videoEngage.src = state.videoUrl;
|
| 298 |
-
videoEngage.load();
|
| 299 |
-
videoEngage.currentTime = currentTime;
|
| 300 |
-
if (wasPlaying) videoEngage.play();
|
| 301 |
-
}
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
if (chipDepth) chipDepth.textContent = "VIEW:DEFAULT";
|
| 305 |
-
};
|
| 306 |
-
|
| 307 |
-
// ========= Streaming Mode for Tab 2 (Live Backend Processing) =========
|
| 308 |
-
|
| 309 |
-
APP.core.video.setStreamingMode = function (url) {
|
| 310 |
-
const { $ } = APP.core.utils;
|
| 311 |
-
const videoEngage = $("#videoEngage");
|
| 312 |
-
const engageEmpty = $("#engageEmpty");
|
| 313 |
-
|
| 314 |
-
// Ensure stream image element exists
|
| 315 |
-
let streamView = $("#streamView");
|
| 316 |
-
if (!streamView) {
|
| 317 |
-
streamView = document.createElement("img");
|
| 318 |
-
streamView.id = "streamView";
|
| 319 |
-
streamView.style.width = "100%";
|
| 320 |
-
streamView.style.height = "100%";
|
| 321 |
-
streamView.style.objectFit = "contain";
|
| 322 |
-
streamView.style.position = "absolute";
|
| 323 |
-
streamView.style.top = "0";
|
| 324 |
-
streamView.style.left = "0";
|
| 325 |
-
streamView.style.zIndex = "10"; // Above video
|
| 326 |
-
streamView.style.backgroundColor = "#000";
|
| 327 |
-
|
| 328 |
-
// Insert into the wrapper (parent of videoEngage)
|
| 329 |
-
if (videoEngage && videoEngage.parentNode) {
|
| 330 |
-
videoEngage.parentNode.appendChild(streamView);
|
| 331 |
-
// Ensure container is relative for absolute positioning
|
| 332 |
-
if (getComputedStyle(videoEngage.parentNode).position === "static") {
|
| 333 |
-
videoEngage.parentNode.style.position = "relative";
|
| 334 |
-
}
|
| 335 |
-
}
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
if (streamView) {
|
| 339 |
-
// Reset state
|
| 340 |
-
streamView.style.display = "block";
|
| 341 |
-
streamView.onerror = () => {
|
| 342 |
-
// If stream fails (404 etc), silently revert
|
| 343 |
-
streamView.style.display = "none";
|
| 344 |
-
if (videoEngage) videoEngage.style.display = "block";
|
| 345 |
-
if (engageEmpty && !videoEngage.src) engageEmpty.style.display = "flex";
|
| 346 |
-
};
|
| 347 |
-
streamView.src = url;
|
| 348 |
-
|
| 349 |
-
if (videoEngage) videoEngage.style.display = "none";
|
| 350 |
-
|
| 351 |
-
// Also hide empty state
|
| 352 |
-
if (engageEmpty) engageEmpty.style.display = "none";
|
| 353 |
-
}
|
| 354 |
-
};
|
| 355 |
-
|
| 356 |
-
APP.core.video.stopStreamingMode = function () {
|
| 357 |
-
const { $ } = APP.core.utils;
|
| 358 |
-
const videoEngage = $("#videoEngage");
|
| 359 |
-
|
| 360 |
-
const streamView = $("#streamView");
|
| 361 |
-
if (streamView) {
|
| 362 |
-
streamView.src = ""; // Stop connection
|
| 363 |
-
streamView.style.display = "none";
|
| 364 |
-
}
|
| 365 |
-
if (videoEngage) videoEngage.style.display = "block";
|
| 366 |
-
};
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/init.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
// Initialize Global Namespace
|
| 2 |
-
window.APP = {
|
| 3 |
-
core: {},
|
| 4 |
-
ui: {},
|
| 5 |
-
api: {}
|
| 6 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/main.js
DELETED
|
@@ -1,762 +0,0 @@
|
|
| 1 |
-
// Main Entry Point - Wire up all event handlers and run the application
|
| 2 |
-
document.addEventListener("DOMContentLoaded", () => {
|
| 3 |
-
// Shortcuts
|
| 4 |
-
const { state } = APP.core;
|
| 5 |
-
const { $, $$ } = APP.core.utils;
|
| 6 |
-
const { log, setStatus, setHfStatus } = APP.ui.logging;
|
| 7 |
-
const { hfDetectAsync, checkJobStatus, cancelBackendJob, pollAsyncJob } = APP.api.client;
|
| 8 |
-
|
| 9 |
-
// Core modules
|
| 10 |
-
const { unloadVideo, toggleDepthView, toggleProcessedFeed, setStreamingMode, stopStreamingMode } = APP.core.video;
|
| 11 |
-
const { load: loadDemo, getFrameData: getDemoFrameData, enable: enableDemo } = APP.core.demo;
|
| 12 |
-
|
| 13 |
-
// UI Renderers
|
| 14 |
-
const { renderFrameTrackList } = APP.ui.cards;
|
| 15 |
-
const { render: renderOverlay, init: initOverlay } = APP.ui.overlays;
|
| 16 |
-
const { tickAgentCursor, moveCursorToRect } = APP.ui.cursor;
|
| 17 |
-
const { matchAndUpdateTracks, predictTracks } = APP.core.tracker;
|
| 18 |
-
|
| 19 |
-
// DOM Elements
|
| 20 |
-
const videoEngage = $("#videoEngage");
|
| 21 |
-
const videoFile = $("#videoFile");
|
| 22 |
-
const btnReason = $("#btnReason");
|
| 23 |
-
const btnCancelReason = $("#btnCancelReason");
|
| 24 |
-
const btnEject = $("#btnEject");
|
| 25 |
-
const btnEngage = $("#btnEngage");
|
| 26 |
-
const btnReset = $("#btnReset");
|
| 27 |
-
const btnPause = $("#btnPause");
|
| 28 |
-
const btnToggleSidebar = $("#btnToggleSidebar");
|
| 29 |
-
|
| 30 |
-
const detectorSelect = $("#detectorSelect");
|
| 31 |
-
const missionText = $("#missionText");
|
| 32 |
-
const cursorMode = $("#cursorMode");
|
| 33 |
-
const engageEmpty = $("#engageEmpty");
|
| 34 |
-
const engageNote = $("#engageNote");
|
| 35 |
-
|
| 36 |
-
const chipFeed = $("#chipFeed");
|
| 37 |
-
const chipDepth = $("#chipDepth");
|
| 38 |
-
|
| 39 |
-
// Animation module shortcuts
|
| 40 |
-
const anim = APP.ui.animations;
|
| 41 |
-
|
| 42 |
-
// Initialization
|
| 43 |
-
function init() {
|
| 44 |
-
log("System initializing...", "t");
|
| 45 |
-
|
| 46 |
-
setupFileUpload();
|
| 47 |
-
setupControls();
|
| 48 |
-
setupKnobListeners();
|
| 49 |
-
setupChipToggles();
|
| 50 |
-
|
| 51 |
-
// Initial UI sync
|
| 52 |
-
setHfStatus("idle");
|
| 53 |
-
|
| 54 |
-
// Initialize SVG overlay
|
| 55 |
-
initOverlay();
|
| 56 |
-
|
| 57 |
-
// Initialize chat panel
|
| 58 |
-
APP.ui.chat.init();
|
| 59 |
-
|
| 60 |
-
// Initialize timeline seek interaction
|
| 61 |
-
APP.core.timeline.init();
|
| 62 |
-
|
| 63 |
-
// Initialize inspection panel
|
| 64 |
-
APP.ui.inspection.init();
|
| 65 |
-
|
| 66 |
-
// Initialize animation system
|
| 67 |
-
if (anim && anim.init) anim.init();
|
| 68 |
-
|
| 69 |
-
// Start main loop
|
| 70 |
-
requestAnimationFrame(loop);
|
| 71 |
-
|
| 72 |
-
// Load demo data (if available)
|
| 73 |
-
loadDemo().then(() => {
|
| 74 |
-
// hidden usage: enable if video filename matches "demo" or manually
|
| 75 |
-
// APP.core.demo.enable(true);
|
| 76 |
-
});
|
| 77 |
-
|
| 78 |
-
log("System ready.", "g");
|
| 79 |
-
|
| 80 |
-
// Update footer stat bar with initial model
|
| 81 |
-
if (anim) {
|
| 82 |
-
anim.updateModel(detectorSelect ? detectorSelect.options[detectorSelect.selectedIndex].text : "YOLO11");
|
| 83 |
-
anim.updateStatus("Ready");
|
| 84 |
-
}
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
function setupFileUpload() {
|
| 88 |
-
if (!videoFile) return;
|
| 89 |
-
|
| 90 |
-
videoFile.addEventListener("change", async (e) => {
|
| 91 |
-
const file = e.target.files[0];
|
| 92 |
-
if (!file) return;
|
| 93 |
-
|
| 94 |
-
state.videoFile = file;
|
| 95 |
-
state.videoUrl = URL.createObjectURL(file);
|
| 96 |
-
state.videoLoaded = true;
|
| 97 |
-
|
| 98 |
-
// Show meta
|
| 99 |
-
const videoMeta = $("#videoMeta");
|
| 100 |
-
if (videoMeta) videoMeta.textContent = file.name;
|
| 101 |
-
|
| 102 |
-
// Load video into engage player
|
| 103 |
-
if (videoEngage) {
|
| 104 |
-
videoEngage.src = state.videoUrl;
|
| 105 |
-
videoEngage.load();
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
// Hide empty states
|
| 109 |
-
if (engageEmpty) engageEmpty.style.display = "none";
|
| 110 |
-
if (engageNote) engageNote.textContent = "Ready";
|
| 111 |
-
|
| 112 |
-
setStatus("warn", "Video loaded - ready to detect");
|
| 113 |
-
log(`Video loaded: ${file.name}`, "g");
|
| 114 |
-
|
| 115 |
-
// Toast + stat bar
|
| 116 |
-
if (anim) {
|
| 117 |
-
anim.toast("Video loaded: " + file.name, "success");
|
| 118 |
-
anim.updateStatus("Ready");
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
// Load video-specific demo tracks (e.g., helicopter demo)
|
| 122 |
-
if (APP.core.demo.loadForVideo) {
|
| 123 |
-
await APP.core.demo.loadForVideo(file.name);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
// Auto-enable demo mode if filename contains "demo" or helicopter video
|
| 127 |
-
const shouldEnableDemo = file.name.toLowerCase().includes("demo") ||
|
| 128 |
-
file.name.toLowerCase().includes("enhance_video_movement");
|
| 129 |
-
if (shouldEnableDemo && APP.core.demo.data) {
|
| 130 |
-
enableDemo(true);
|
| 131 |
-
log("Auto-enabled DEMO mode for this video.", "g");
|
| 132 |
-
}
|
| 133 |
-
});
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
function setupControls() {
|
| 137 |
-
// Reason button
|
| 138 |
-
if (btnReason) {
|
| 139 |
-
btnReason.addEventListener("click", runReason);
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
// Cancel Reason button
|
| 143 |
-
if (btnCancelReason) {
|
| 144 |
-
btnCancelReason.addEventListener("click", cancelReasoning);
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
// Eject button
|
| 148 |
-
if (btnEject) {
|
| 149 |
-
btnEject.addEventListener("click", async () => {
|
| 150 |
-
await unloadVideo();
|
| 151 |
-
});
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
// Engage button
|
| 155 |
-
if (btnEngage) {
|
| 156 |
-
btnEngage.addEventListener("click", runEngage);
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
// Pause / Resume toggle button
|
| 160 |
-
if (btnPause) {
|
| 161 |
-
btnPause.addEventListener("click", () => {
|
| 162 |
-
if (!videoEngage) return;
|
| 163 |
-
if (videoEngage.paused) {
|
| 164 |
-
videoEngage.play().catch(err => {
|
| 165 |
-
log(`Video resume failed: ${err.message}`, "e");
|
| 166 |
-
});
|
| 167 |
-
state.tracker.running = true;
|
| 168 |
-
btnPause.textContent = "Resume";
|
| 169 |
-
log("Tracking resumed.", "t");
|
| 170 |
-
} else {
|
| 171 |
-
videoEngage.pause();
|
| 172 |
-
btnPause.textContent = "Pause";
|
| 173 |
-
log("Video paused.", "t");
|
| 174 |
-
}
|
| 175 |
-
});
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
// Reset button
|
| 179 |
-
if (btnReset) {
|
| 180 |
-
btnReset.addEventListener("click", () => {
|
| 181 |
-
if (videoEngage) {
|
| 182 |
-
videoEngage.pause();
|
| 183 |
-
videoEngage.currentTime = 0;
|
| 184 |
-
}
|
| 185 |
-
state.tracker.tracks = [];
|
| 186 |
-
state.tracker.running = false;
|
| 187 |
-
state.tracker.nextId = 1;
|
| 188 |
-
state.tracker.heatmap = {};
|
| 189 |
-
state.tracker.assessmentCache = {};
|
| 190 |
-
state.inspection.explanationCache = {};
|
| 191 |
-
state.hf.fps = null;
|
| 192 |
-
state.hf.totalFrames = null;
|
| 193 |
-
APP.core.timeline._cachedDuration = null;
|
| 194 |
-
if (btnPause) btnPause.textContent = "Pause";
|
| 195 |
-
renderFrameTrackList();
|
| 196 |
-
log("Tracking reset.", "t");
|
| 197 |
-
});
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
// Sidebar toggle (Tab 2)
|
| 201 |
-
if (btnToggleSidebar) {
|
| 202 |
-
btnToggleSidebar.addEventListener("click", () => {
|
| 203 |
-
const engageGrid = $(".engage-grid");
|
| 204 |
-
if (engageGrid) {
|
| 205 |
-
engageGrid.classList.toggle("sidebar-collapsed");
|
| 206 |
-
btnToggleSidebar.textContent = engageGrid.classList.contains("sidebar-collapsed")
|
| 207 |
-
? "Show Panel"
|
| 208 |
-
: "Hide Panel";
|
| 209 |
-
}
|
| 210 |
-
});
|
| 211 |
-
}
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
// Track selection event — multi-select toggle
|
| 217 |
-
document.addEventListener("track-selected", (e) => {
|
| 218 |
-
const clickedId = e.detail.id;
|
| 219 |
-
const vid = $("#videoEngage");
|
| 220 |
-
|
| 221 |
-
if (!clickedId) {
|
| 222 |
-
// Click on empty area — deselect all
|
| 223 |
-
state.selectedIds = [];
|
| 224 |
-
state.tracker.selectedTrackId = null;
|
| 225 |
-
APP.ui.chat.clear();
|
| 226 |
-
APP.ui.inspection.close();
|
| 227 |
-
renderFrameTrackList();
|
| 228 |
-
renderOverlay();
|
| 229 |
-
// Auto-resume playback
|
| 230 |
-
if (vid && vid.paused && state.tracker.running) {
|
| 231 |
-
vid.play().catch(() => {});
|
| 232 |
-
if (btnPause) btnPause.textContent = "Pause";
|
| 233 |
-
}
|
| 234 |
-
return;
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
// Toggle: add or remove from selectedIds
|
| 238 |
-
const idx = state.selectedIds.indexOf(clickedId);
|
| 239 |
-
if (idx >= 0) {
|
| 240 |
-
// Deselect this track
|
| 241 |
-
state.selectedIds.splice(idx, 1);
|
| 242 |
-
APP.ui.chat.removeTrackContext(clickedId);
|
| 243 |
-
// Update selectedTrackId to last remaining, or null
|
| 244 |
-
state.tracker.selectedTrackId = state.selectedIds.length > 0
|
| 245 |
-
? state.selectedIds[state.selectedIds.length - 1]
|
| 246 |
-
: null;
|
| 247 |
-
} else {
|
| 248 |
-
// Select this track
|
| 249 |
-
state.selectedIds.push(clickedId);
|
| 250 |
-
state.tracker.selectedTrackId = clickedId;
|
| 251 |
-
// Pause video to freeze on the selected object
|
| 252 |
-
if (vid && !vid.paused) {
|
| 253 |
-
vid.pause();
|
| 254 |
-
if (btnPause) btnPause.textContent = "Resume";
|
| 255 |
-
}
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
// Sync tracker.tracks from detections if empty
|
| 259 |
-
if (state.tracker.tracks.length === 0 && state.detections.length > 0) {
|
| 260 |
-
state.tracker.tracks = [...state.detections];
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
renderFrameTrackList();
|
| 264 |
-
renderOverlay();
|
| 265 |
-
|
| 266 |
-
// Open inspection for last-clicked track (even if deselecting, show last remaining)
|
| 267 |
-
const inspectId = state.tracker.selectedTrackId;
|
| 268 |
-
if (inspectId) {
|
| 269 |
-
APP.ui.inspection.open(inspectId);
|
| 270 |
-
} else {
|
| 271 |
-
APP.ui.inspection.close();
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
// Inject track context into chat (additive)
|
| 275 |
-
const selectedTrack = (state.detections || []).find(d => d.id === clickedId);
|
| 276 |
-
if (selectedTrack && state.selectedIds.includes(clickedId)) {
|
| 277 |
-
APP.ui.chat.injectTrackContext(selectedTrack);
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
// Scroll selected card into view
|
| 281 |
-
const card = document.getElementById(`card-${clickedId}`);
|
| 282 |
-
if (card) card.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
| 283 |
-
});
|
| 284 |
-
|
| 285 |
-
// Cursor mode toggle
|
| 286 |
-
if (cursorMode) {
|
| 287 |
-
cursorMode.addEventListener("change", () => {
|
| 288 |
-
state.ui.cursorMode = cursorMode.value;
|
| 289 |
-
if (state.ui.cursorMode === "off" && APP.ui.cursor.setCursorVisible) {
|
| 290 |
-
APP.ui.cursor.setCursorVisible(false);
|
| 291 |
-
}
|
| 292 |
-
});
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
function setupKnobListeners() {
|
| 296 |
-
// Listen to all inputs and selects for knob updates
|
| 297 |
-
const inputs = Array.from(document.querySelectorAll("input, select"));
|
| 298 |
-
inputs.forEach(el => {
|
| 299 |
-
el.addEventListener("input", () => {
|
| 300 |
-
// Knob change handler (overlay rendering removed)
|
| 301 |
-
});
|
| 302 |
-
});
|
| 303 |
-
|
| 304 |
-
// Update stat bar when detector changes
|
| 305 |
-
if (detectorSelect) {
|
| 306 |
-
detectorSelect.addEventListener("change", () => {
|
| 307 |
-
if (anim) {
|
| 308 |
-
anim.updateModel(detectorSelect.options[detectorSelect.selectedIndex].text);
|
| 309 |
-
}
|
| 310 |
-
});
|
| 311 |
-
}
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
function setupChipToggles() {
|
| 315 |
-
// Toggle processed/raw feed
|
| 316 |
-
if (chipFeed) {
|
| 317 |
-
chipFeed.style.cursor = "pointer";
|
| 318 |
-
chipFeed.addEventListener("click", () => {
|
| 319 |
-
if (!state.videoLoaded) return;
|
| 320 |
-
toggleProcessedFeed();
|
| 321 |
-
log(`Feed set to: ${state.useProcessedFeed ? "HF" : "RAW"}`, "t");
|
| 322 |
-
});
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
// Toggle depth view (Tab 2)
|
| 326 |
-
if (chipDepth) {
|
| 327 |
-
chipDepth.style.cursor = "pointer";
|
| 328 |
-
chipDepth.addEventListener("click", () => {
|
| 329 |
-
if (!state.videoLoaded) return;
|
| 330 |
-
toggleDepthView();
|
| 331 |
-
log(`Engage view set to: ${state.useDepthFeed ? "DEPTH" : "DEFAULT"}`, "t");
|
| 332 |
-
});
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
async function runReason() {
|
| 338 |
-
if (!state.videoLoaded) {
|
| 339 |
-
log("No video loaded. Upload a video first.", "w");
|
| 340 |
-
setStatus("warn", "Upload a video to begin");
|
| 341 |
-
return;
|
| 342 |
-
}
|
| 343 |
-
|
| 344 |
-
if (state.isReasoning) {
|
| 345 |
-
log("Detection already in progress. Please wait.", "w");
|
| 346 |
-
return;
|
| 347 |
-
}
|
| 348 |
-
|
| 349 |
-
// Lock the Reason process
|
| 350 |
-
state.isReasoning = true;
|
| 351 |
-
if (btnReason) {
|
| 352 |
-
btnReason.disabled = true;
|
| 353 |
-
btnReason.style.opacity = "0.5";
|
| 354 |
-
btnReason.style.cursor = "not-allowed";
|
| 355 |
-
}
|
| 356 |
-
if (btnCancelReason) btnCancelReason.style.display = "inline-block";
|
| 357 |
-
if (btnEngage) btnEngage.disabled = true;
|
| 358 |
-
|
| 359 |
-
// Clear previous detections
|
| 360 |
-
state.detections = [];
|
| 361 |
-
state.selectedIds = [];
|
| 362 |
-
renderFrameTrackList();
|
| 363 |
-
|
| 364 |
-
setStatus("warn", "Running detection pipeline");
|
| 365 |
-
|
| 366 |
-
// Show progress ring + ambient processing state
|
| 367 |
-
if (anim) {
|
| 368 |
-
anim.showProgressRing("Initializing...");
|
| 369 |
-
anim.setBodyState("processing");
|
| 370 |
-
anim.setDotProcessing(true);
|
| 371 |
-
anim.updateStatus("Processing");
|
| 372 |
-
anim.toast("Detection pipeline started", "info");
|
| 373 |
-
// Update model name in stat bar
|
| 374 |
-
const modelText = detectorSelect ? detectorSelect.options[detectorSelect.selectedIndex].text : "YOLO11";
|
| 375 |
-
anim.updateModel(modelText);
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
try {
|
| 379 |
-
const selectedOption = detectorSelect ? detectorSelect.options[detectorSelect.selectedIndex] : null;
|
| 380 |
-
const selectedValue = detectorSelect ? detectorSelect.value : "yolo11";
|
| 381 |
-
const kind = selectedOption ? selectedOption.getAttribute("data-kind") : "object";
|
| 382 |
-
const queries = missionText ? missionText.value.trim() : "";
|
| 383 |
-
const enableDepth = false; // depth mode disabled
|
| 384 |
-
|
| 385 |
-
// Determine mode and model parameter from data-kind attribute
|
| 386 |
-
let mode, detectorParam, segmenterParam;
|
| 387 |
-
if (kind === "segmentation") {
|
| 388 |
-
mode = "segmentation";
|
| 389 |
-
segmenterParam = selectedValue;
|
| 390 |
-
detectorParam = "yolo11"; // default, unused for segmentation
|
| 391 |
-
} else if (kind === "drone") {
|
| 392 |
-
mode = "drone_detection";
|
| 393 |
-
detectorParam = selectedValue;
|
| 394 |
-
segmenterParam = "GSAM2-L";
|
| 395 |
-
} else {
|
| 396 |
-
mode = "object_detection";
|
| 397 |
-
detectorParam = selectedValue;
|
| 398 |
-
segmenterParam = "GSAM2-L";
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
const form = new FormData();
|
| 402 |
-
form.append("video", state.videoFile);
|
| 403 |
-
form.append("mode", mode);
|
| 404 |
-
if (queries) form.append("queries", queries);
|
| 405 |
-
form.append("detector", detectorParam);
|
| 406 |
-
form.append("segmenter", segmenterParam);
|
| 407 |
-
form.append("enable_depth", enableDepth ? "true" : "false");
|
| 408 |
-
|
| 409 |
-
const missionVal = missionText ? missionText.value.trim() : "";
|
| 410 |
-
if (missionVal) form.append("mission", missionVal);
|
| 411 |
-
|
| 412 |
-
log(`Submitting job to ${state.hf.baseUrl}...`, "t");
|
| 413 |
-
setHfStatus("submitting job...");
|
| 414 |
-
|
| 415 |
-
const data = await hfDetectAsync(form);
|
| 416 |
-
|
| 417 |
-
state.hf.asyncJobId = data.job_id;
|
| 418 |
-
state.hf.mode = mode;
|
| 419 |
-
|
| 420 |
-
// Store depth video URL if provided
|
| 421 |
-
if (data.depth_video_url) {
|
| 422 |
-
state.hf.depthVideoUrl = data.depth_video_url.startsWith("http")
|
| 423 |
-
? data.depth_video_url
|
| 424 |
-
: `${state.hf.baseUrl}${data.depth_video_url}`;
|
| 425 |
-
log("Depth video URL received", "t");
|
| 426 |
-
}
|
| 427 |
-
|
| 428 |
-
// Enable streaming mode if stream_url is provided (Tab 2 live view)
|
| 429 |
-
const enableStream = $("#enableStreamToggle")?.checked;
|
| 430 |
-
|
| 431 |
-
if (data.stream_url && enableStream) {
|
| 432 |
-
const streamUrl = data.stream_url.startsWith("http")
|
| 433 |
-
? data.stream_url
|
| 434 |
-
: `${state.hf.baseUrl}${data.stream_url}`;
|
| 435 |
-
log("Activating live stream...", "t");
|
| 436 |
-
setStreamingMode(streamUrl);
|
| 437 |
-
log("Live view available in 'Track' tab.", "g");
|
| 438 |
-
setStatus("warn", "Live processing... View in Track tab");
|
| 439 |
-
|
| 440 |
-
// Poll tracks during live streaming (no video element to derive frameIdx from)
|
| 441 |
-
const streamTrackPoll = setInterval(async () => {
|
| 442 |
-
const jobId = state.hf.asyncJobId;
|
| 443 |
-
if (!jobId || !state.hf.baseUrl) return;
|
| 444 |
-
try {
|
| 445 |
-
// Fetch summary to find latest frame with track data
|
| 446 |
-
const sumResp = await fetch(`${state.hf.baseUrl}/detect/tracks/${jobId}/summary`);
|
| 447 |
-
if (!sumResp.ok) return;
|
| 448 |
-
const summary = await sumResp.json();
|
| 449 |
-
const frames = summary.frames || {};
|
| 450 |
-
const frameIndices = Object.keys(frames).map(Number).filter(n => !isNaN(n));
|
| 451 |
-
if (!frameIndices.length) return;
|
| 452 |
-
|
| 453 |
-
const latestIdx = Math.max(...frameIndices);
|
| 454 |
-
const trackResp = await fetch(`${state.hf.baseUrl}/detect/tracks/${jobId}/${latestIdx}`);
|
| 455 |
-
if (!trackResp.ok) return;
|
| 456 |
-
const dets = await trackResp.json();
|
| 457 |
-
if (!Array.isArray(dets) || !dets.length) return;
|
| 458 |
-
|
| 459 |
-
// Map to frontend format
|
| 460 |
-
const newTracks = dets.map(d => {
|
| 461 |
-
const bw = d.bbox[2] - d.bbox[0];
|
| 462 |
-
const bh = d.bbox[3] - d.bbox[1];
|
| 463 |
-
// Use stream image dimensions or fallback
|
| 464 |
-
const streamView = $("#streamView");
|
| 465 |
-
const fw = streamView ? (streamView.naturalWidth || 1280) : 1280;
|
| 466 |
-
const fh = streamView ? (streamView.naturalHeight || 720) : 720;
|
| 467 |
-
return {
|
| 468 |
-
id: d.track_id || String(Math.floor(Math.random() * 1000)),
|
| 469 |
-
label: d.label,
|
| 470 |
-
bbox: { x: d.bbox[0] / fw, y: d.bbox[1] / fh, w: bw / fw, h: bh / fh },
|
| 471 |
-
score: d.score,
|
| 472 |
-
speed_kph: d.speed_kph,
|
| 473 |
-
vx: 0,
|
| 474 |
-
vy: 0,
|
| 475 |
-
satisfies: d.satisfies ?? null,
|
| 476 |
-
reason: d.reason || null,
|
| 477 |
-
mission_relevant: d.mission_relevant ?? null,
|
| 478 |
-
assessment_status: d.assessment_status || APP.core.gptMapping.STATUS.UNASSESSED,
|
| 479 |
-
gpt_raw: d.gpt_raw || null,
|
| 480 |
-
features: APP.core.gptMapping.buildFeatures(d.gpt_raw),
|
| 481 |
-
lastSeen: Date.now(),
|
| 482 |
-
state: "TRACK"
|
| 483 |
-
};
|
| 484 |
-
});
|
| 485 |
-
|
| 486 |
-
state.detections = newTracks;
|
| 487 |
-
state.tracker.tracks = newTracks;
|
| 488 |
-
renderFrameTrackList();
|
| 489 |
-
} catch (e) { /* ignore stream poll errors */ }
|
| 490 |
-
}, 1000);
|
| 491 |
-
state.hf._streamTrackPoll = streamTrackPoll;
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
// Start polling for completion
|
| 495 |
-
pollAsyncJob().then(() => {
|
| 496 |
-
log("Video processing complete.", "g");
|
| 497 |
-
// Stop streaming track polling and streaming mode
|
| 498 |
-
if (state.hf._streamTrackPoll) {
|
| 499 |
-
clearInterval(state.hf._streamTrackPoll);
|
| 500 |
-
state.hf._streamTrackPoll = null;
|
| 501 |
-
}
|
| 502 |
-
stopStreamingMode();
|
| 503 |
-
|
| 504 |
-
state.hasReasoned = true;
|
| 505 |
-
setStatus("good", "Detection complete - ready to track");
|
| 506 |
-
log("Detection complete. Ready to Track.", "g");
|
| 507 |
-
|
| 508 |
-
// Animation: complete state
|
| 509 |
-
if (anim) {
|
| 510 |
-
anim.updateProgressRing(1.0, "Complete");
|
| 511 |
-
anim.setDotProcessing(false);
|
| 512 |
-
anim.setBodyState("complete");
|
| 513 |
-
anim.updateStatus("Complete");
|
| 514 |
-
anim.toast("Detection complete - " + (state.detections || []).length + " objects found", "success", 5000);
|
| 515 |
-
setTimeout(function() { anim.hideProgressRing(); }, 800);
|
| 516 |
-
}
|
| 517 |
-
|
| 518 |
-
// Seed tracks for Tab 2
|
| 519 |
-
seedTracksFromTab1();
|
| 520 |
-
|
| 521 |
-
// Re-enable engage button
|
| 522 |
-
if (btnEngage) btnEngage.disabled = false;
|
| 523 |
-
}).catch(err => {
|
| 524 |
-
log(`Polling error: ${err.message}`, "e");
|
| 525 |
-
if (state.hf._streamTrackPoll) {
|
| 526 |
-
clearInterval(state.hf._streamTrackPoll);
|
| 527 |
-
state.hf._streamTrackPoll = null;
|
| 528 |
-
}
|
| 529 |
-
stopStreamingMode();
|
| 530 |
-
|
| 531 |
-
// Animation: error state
|
| 532 |
-
if (anim) {
|
| 533 |
-
anim.hideProgressRing();
|
| 534 |
-
anim.setDotProcessing(false);
|
| 535 |
-
anim.setBodyState("error");
|
| 536 |
-
anim.updateStatus("Error");
|
| 537 |
-
anim.toast("Detection failed: " + err.message, "error", 6000);
|
| 538 |
-
}
|
| 539 |
-
});
|
| 540 |
-
|
| 541 |
-
// Initial status (processing in background)
|
| 542 |
-
setStatus("warn", "Processing video...");
|
| 543 |
-
log("Detection started...", "t");
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
} catch (err) {
|
| 548 |
-
setStatus("bad", "Detection failed");
|
| 549 |
-
log(`Detection failed: ${err.message}`, "e");
|
| 550 |
-
console.error(err);
|
| 551 |
-
|
| 552 |
-
if (anim) {
|
| 553 |
-
anim.hideProgressRing();
|
| 554 |
-
anim.setDotProcessing(false);
|
| 555 |
-
anim.setBodyState("error");
|
| 556 |
-
anim.updateStatus("Error");
|
| 557 |
-
anim.toast("Detection failed: " + err.message, "error", 6000);
|
| 558 |
-
}
|
| 559 |
-
} finally {
|
| 560 |
-
state.isReasoning = false;
|
| 561 |
-
if (btnReason) {
|
| 562 |
-
btnReason.disabled = false;
|
| 563 |
-
btnReason.style.opacity = "1";
|
| 564 |
-
btnReason.style.cursor = "pointer";
|
| 565 |
-
}
|
| 566 |
-
if (btnCancelReason) btnCancelReason.style.display = "none";
|
| 567 |
-
// Re-enable engage button in case of failure
|
| 568 |
-
if (btnEngage) btnEngage.disabled = false;
|
| 569 |
-
}
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
function seedTracksFromTab1() {
|
| 573 |
-
// Preserve detections from streaming poll if available
|
| 574 |
-
if (state.detections && state.detections.length > 0) {
|
| 575 |
-
state.tracker.tracks = [...state.detections];
|
| 576 |
-
} else {
|
| 577 |
-
state.tracker.tracks = [];
|
| 578 |
-
}
|
| 579 |
-
state.tracker.nextId = 1;
|
| 580 |
-
log("Tracks initialized.", "t");
|
| 581 |
-
}
|
| 582 |
-
|
| 583 |
-
function cancelReasoning() {
|
| 584 |
-
// Stop HF polling if running
|
| 585 |
-
if (state.hf.asyncPollInterval) {
|
| 586 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 587 |
-
state.hf.asyncPollInterval = null;
|
| 588 |
-
log("HF polling stopped.", "w");
|
| 589 |
-
}
|
| 590 |
-
|
| 591 |
-
// Stop streaming mode
|
| 592 |
-
stopStreamingMode();
|
| 593 |
-
|
| 594 |
-
// Cancel backend job if it exists
|
| 595 |
-
const jobId = state.hf.asyncJobId;
|
| 596 |
-
if (jobId) {
|
| 597 |
-
cancelBackendJob(jobId, "cancel button");
|
| 598 |
-
}
|
| 599 |
-
|
| 600 |
-
// Reset state
|
| 601 |
-
state.isReasoning = false;
|
| 602 |
-
state.hf.busy = false;
|
| 603 |
-
state.hf.asyncJobId = null;
|
| 604 |
-
state.hf.completedJobId = null;
|
| 605 |
-
state.hf.asyncStatus = "cancelled";
|
| 606 |
-
|
| 607 |
-
// Re-enable Reason button
|
| 608 |
-
if (btnReason) {
|
| 609 |
-
btnReason.disabled = false;
|
| 610 |
-
btnReason.style.opacity = "1";
|
| 611 |
-
btnReason.style.cursor = "pointer";
|
| 612 |
-
}
|
| 613 |
-
if (btnCancelReason) btnCancelReason.style.display = "none";
|
| 614 |
-
|
| 615 |
-
setStatus("warn", "Detection cancelled");
|
| 616 |
-
setHfStatus("cancelled (stopped by user)");
|
| 617 |
-
log("Detection cancelled by user.", "w");
|
| 618 |
-
|
| 619 |
-
if (anim) {
|
| 620 |
-
anim.hideProgressRing();
|
| 621 |
-
anim.setDotProcessing(false);
|
| 622 |
-
anim.setBodyState("idle");
|
| 623 |
-
anim.updateStatus("Cancelled");
|
| 624 |
-
anim.toast("Detection cancelled", "warning");
|
| 625 |
-
}
|
| 626 |
-
}
|
| 627 |
-
|
| 628 |
-
function runEngage() {
|
| 629 |
-
if (!state.hasReasoned) {
|
| 630 |
-
log("Please run Detect first.", "w");
|
| 631 |
-
return;
|
| 632 |
-
}
|
| 633 |
-
|
| 634 |
-
if (state.hf.asyncJobId) {
|
| 635 |
-
log("Processing still in progress. Please wait.", "w");
|
| 636 |
-
return;
|
| 637 |
-
}
|
| 638 |
-
|
| 639 |
-
// Set video source
|
| 640 |
-
if (videoEngage) {
|
| 641 |
-
videoEngage.src = state.hf.processedUrl || state.videoUrl;
|
| 642 |
-
videoEngage.play().catch(err => {
|
| 643 |
-
log(`Video playback failed: ${err.message}`, "e");
|
| 644 |
-
});
|
| 645 |
-
}
|
| 646 |
-
|
| 647 |
-
state.tracker.running = true;
|
| 648 |
-
state.tracker.lastFrameTime = APP.core.utils.now();
|
| 649 |
-
if (btnPause) btnPause.textContent = "Pause";
|
| 650 |
-
|
| 651 |
-
// Ensure tracks are seeded
|
| 652 |
-
if (state.tracker.tracks.length === 0) {
|
| 653 |
-
seedTracksFromTab1();
|
| 654 |
-
}
|
| 655 |
-
|
| 656 |
-
log("Tracking started.", "g");
|
| 657 |
-
|
| 658 |
-
if (anim) {
|
| 659 |
-
anim.setBodyState("complete");
|
| 660 |
-
anim.updateStatus("Tracking");
|
| 661 |
-
anim.toast("Real-time tracking active", "success");
|
| 662 |
-
}
|
| 663 |
-
}
|
| 664 |
-
|
| 665 |
-
function loop() {
|
| 666 |
-
const { now } = APP.core.utils;
|
| 667 |
-
const t = now();
|
| 668 |
-
|
| 669 |
-
// Guard against huge dt on first frame
|
| 670 |
-
if (state.tracker.lastFrameTime === 0) state.tracker.lastFrameTime = t;
|
| 671 |
-
|
| 672 |
-
const dt = Math.min((t - state.tracker.lastFrameTime) / 1000, 0.1);
|
| 673 |
-
state.tracker.lastFrameTime = t;
|
| 674 |
-
|
| 675 |
-
// ── Always keep track positions fresh (playing OR paused) ──
|
| 676 |
-
// This ensures bboxes remain clickable regardless of playback state.
|
| 677 |
-
if (state.tracker.running && videoEngage) {
|
| 678 |
-
if (APP.core.demo.active && APP.core.demo.data) {
|
| 679 |
-
// DEMO MODE: sync tracks to current video time (even when paused)
|
| 680 |
-
const demoTracks = getDemoFrameData(videoEngage.currentTime);
|
| 681 |
-
if (demoTracks) {
|
| 682 |
-
const tracksClone = JSON.parse(JSON.stringify(demoTracks));
|
| 683 |
-
|
| 684 |
-
state.tracker.tracks = tracksClone.map(d => ({
|
| 685 |
-
...d,
|
| 686 |
-
lastSeen: t,
|
| 687 |
-
state: "TRACK",
|
| 688 |
-
depth_valid: false,
|
| 689 |
-
depth_est_m: null,
|
| 690 |
-
}));
|
| 691 |
-
|
| 692 |
-
const w = videoEngage.videoWidth || state.frame.w || 1280;
|
| 693 |
-
const h = videoEngage.videoHeight || state.frame.h || 720;
|
| 694 |
-
|
| 695 |
-
state.tracker.tracks.forEach(tr => {
|
| 696 |
-
if (tr.bbox.x > 1 || tr.bbox.w > 1) {
|
| 697 |
-
tr.bbox.x /= w;
|
| 698 |
-
tr.bbox.y /= h;
|
| 699 |
-
tr.bbox.w /= w;
|
| 700 |
-
tr.bbox.h /= h;
|
| 701 |
-
}
|
| 702 |
-
});
|
| 703 |
-
}
|
| 704 |
-
} else {
|
| 705 |
-
// NORMAL MODE: predict positions every frame (only if tracks exist)
|
| 706 |
-
if (state.tracker.tracks.length > 0) {
|
| 707 |
-
predictTracks(dt);
|
| 708 |
-
}
|
| 709 |
-
|
| 710 |
-
// Backend sync every 333ms — always runs, even with zero tracks
|
| 711 |
-
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 712 |
-
if (jobId && (t - state.tracker.lastHFSync > 333)) {
|
| 713 |
-
const fps = state.hf.fps || 30;
|
| 714 |
-
const maxFrame = state.hf.totalFrames ? state.hf.totalFrames - 1 : Infinity;
|
| 715 |
-
const frameIdx = Math.min(Math.floor(videoEngage.currentTime * fps), maxFrame);
|
| 716 |
-
if (isFinite(frameIdx) && frameIdx >= 0) {
|
| 717 |
-
APP.core.tracker.syncWithBackend(frameIdx);
|
| 718 |
-
}
|
| 719 |
-
state.tracker.lastHFSync = t;
|
| 720 |
-
}
|
| 721 |
-
|
| 722 |
-
// Refresh inspection panel if frame changed
|
| 723 |
-
if (APP.ui.inspection.refreshFrame) {
|
| 724 |
-
APP.ui.inspection.refreshFrame();
|
| 725 |
-
}
|
| 726 |
-
}
|
| 727 |
-
}
|
| 728 |
-
|
| 729 |
-
// ── Card rendering ──
|
| 730 |
-
if (state.tracker.running && videoEngage) {
|
| 731 |
-
// Always render immediately when new objects arrive (even if paused)
|
| 732 |
-
if (state.tracker._newObjectDetected) {
|
| 733 |
-
renderFrameTrackList();
|
| 734 |
-
state.tracker._lastCardRenderFrame = state.tracker.frameCount;
|
| 735 |
-
state.tracker._newObjectDetected = false;
|
| 736 |
-
}
|
| 737 |
-
|
| 738 |
-
// Periodic re-render during active playback
|
| 739 |
-
if (!videoEngage.paused) {
|
| 740 |
-
state.tracker.frameCount++;
|
| 741 |
-
const framesSinceRender = state.tracker.frameCount - state.tracker._lastCardRenderFrame;
|
| 742 |
-
if (framesSinceRender >= 40) {
|
| 743 |
-
renderFrameTrackList();
|
| 744 |
-
state.tracker._lastCardRenderFrame = state.tracker.frameCount;
|
| 745 |
-
}
|
| 746 |
-
}
|
| 747 |
-
}
|
| 748 |
-
|
| 749 |
-
// Render UI
|
| 750 |
-
renderOverlay();
|
| 751 |
-
if (tickAgentCursor) tickAgentCursor();
|
| 752 |
-
APP.core.timeline.render();
|
| 753 |
-
|
| 754 |
-
requestAnimationFrame(loop);
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
// Expose state for debugging
|
| 758 |
-
window.__LP_STATE__ = state;
|
| 759 |
-
|
| 760 |
-
// Start
|
| 761 |
-
init();
|
| 762 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/animations.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
| 1 |
-
// Animations Module — Toast notifications, progress ring, stat counters, ambient state
|
| 2 |
-
APP.ui.animations = {};
|
| 3 |
-
|
| 4 |
-
// =========================================
|
| 5 |
-
// Toast Notification System
|
| 6 |
-
// =========================================
|
| 7 |
-
|
| 8 |
-
APP.ui.animations._toastQueue = [];
|
| 9 |
-
APP.ui.animations._toastMax = 4;
|
| 10 |
-
|
| 11 |
-
/**
|
| 12 |
-
* Show a toast notification.
|
| 13 |
-
* @param {string} message - Text to display
|
| 14 |
-
* @param {"info"|"success"|"warning"|"error"} type - Toast type
|
| 15 |
-
* @param {number} duration - Auto-dismiss in ms (default 4000)
|
| 16 |
-
*/
|
| 17 |
-
APP.ui.animations.toast = function (message, type, duration) {
|
| 18 |
-
type = type || "info";
|
| 19 |
-
duration = duration || 4000;
|
| 20 |
-
|
| 21 |
-
var container = document.getElementById("toastContainer");
|
| 22 |
-
if (!container) return;
|
| 23 |
-
|
| 24 |
-
var toast = document.createElement("div");
|
| 25 |
-
toast.className = "toast";
|
| 26 |
-
toast.innerHTML =
|
| 27 |
-
'<span class="toast-icon ' + type + '"></span>' +
|
| 28 |
-
'<span class="toast-text">' + message + '</span>' +
|
| 29 |
-
'<div class="toast-progress" style="animation-duration:' + duration + 'ms"></div>';
|
| 30 |
-
|
| 31 |
-
container.appendChild(toast);
|
| 32 |
-
APP.ui.animations._toastQueue.push(toast);
|
| 33 |
-
|
| 34 |
-
// Enforce max visible
|
| 35 |
-
while (APP.ui.animations._toastQueue.length > APP.ui.animations._toastMax) {
|
| 36 |
-
var old = APP.ui.animations._toastQueue.shift();
|
| 37 |
-
if (old && old.parentNode) old.parentNode.removeChild(old);
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
// Auto-dismiss
|
| 41 |
-
setTimeout(function () {
|
| 42 |
-
toast.classList.add("exiting");
|
| 43 |
-
setTimeout(function () {
|
| 44 |
-
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
| 45 |
-
var idx = APP.ui.animations._toastQueue.indexOf(toast);
|
| 46 |
-
if (idx > -1) APP.ui.animations._toastQueue.splice(idx, 1);
|
| 47 |
-
}, 280);
|
| 48 |
-
}, duration);
|
| 49 |
-
};
|
| 50 |
-
|
| 51 |
-
// =========================================
|
| 52 |
-
// Progress Ring (video overlay)
|
| 53 |
-
// =========================================
|
| 54 |
-
|
| 55 |
-
APP.ui.animations.showProgressRing = function (text) {
|
| 56 |
-
var wrap = document.getElementById("progressRing");
|
| 57 |
-
var ring = document.getElementById("progressRingFg");
|
| 58 |
-
var label = document.getElementById("progressRingText");
|
| 59 |
-
if (!wrap) return;
|
| 60 |
-
|
| 61 |
-
wrap.classList.add("visible");
|
| 62 |
-
if (label) label.textContent = text || "Processing...";
|
| 63 |
-
if (ring) ring.style.strokeDashoffset = "264"; // 0%
|
| 64 |
-
};
|
| 65 |
-
|
| 66 |
-
APP.ui.animations.updateProgressRing = function (fraction, text) {
|
| 67 |
-
var ring = document.getElementById("progressRingFg");
|
| 68 |
-
var label = document.getElementById("progressRingText");
|
| 69 |
-
if (!ring) return;
|
| 70 |
-
|
| 71 |
-
// circumference = 2 * PI * 42 = ~264
|
| 72 |
-
var offset = 264 - (264 * Math.min(Math.max(fraction, 0), 1));
|
| 73 |
-
ring.style.strokeDashoffset = String(offset);
|
| 74 |
-
if (label && text) label.textContent = text;
|
| 75 |
-
};
|
| 76 |
-
|
| 77 |
-
APP.ui.animations.hideProgressRing = function () {
|
| 78 |
-
var wrap = document.getElementById("progressRing");
|
| 79 |
-
if (wrap) wrap.classList.remove("visible");
|
| 80 |
-
};
|
| 81 |
-
|
| 82 |
-
// =========================================
|
| 83 |
-
// Footer Stat Bar (animated counters)
|
| 84 |
-
// =========================================
|
| 85 |
-
|
| 86 |
-
APP.ui.animations._statValues = {};
|
| 87 |
-
|
| 88 |
-
APP.ui.animations.updateStat = function (id, value) {
|
| 89 |
-
var el = document.getElementById(id);
|
| 90 |
-
if (!el) return;
|
| 91 |
-
|
| 92 |
-
var prev = APP.ui.animations._statValues[id];
|
| 93 |
-
if (prev === value) return;
|
| 94 |
-
|
| 95 |
-
APP.ui.animations._statValues[id] = value;
|
| 96 |
-
el.textContent = value;
|
| 97 |
-
|
| 98 |
-
// Flash accent color on change
|
| 99 |
-
el.classList.add("updated");
|
| 100 |
-
setTimeout(function () { el.classList.remove("updated"); }, 600);
|
| 101 |
-
};
|
| 102 |
-
|
| 103 |
-
// Convenience: update track count with animated counter effect
|
| 104 |
-
APP.ui.animations.animateTrackCount = function (count) {
|
| 105 |
-
APP.ui.animations.updateStat("statTracks", String(count));
|
| 106 |
-
};
|
| 107 |
-
|
| 108 |
-
APP.ui.animations.updateModel = function (modelName) {
|
| 109 |
-
APP.ui.animations.updateStat("statModel", modelName);
|
| 110 |
-
};
|
| 111 |
-
|
| 112 |
-
APP.ui.animations.updateStatus = function (statusText) {
|
| 113 |
-
APP.ui.animations.updateStat("statStatus", statusText);
|
| 114 |
-
};
|
| 115 |
-
|
| 116 |
-
// =========================================
|
| 117 |
-
// Ambient State Management
|
| 118 |
-
// =========================================
|
| 119 |
-
|
| 120 |
-
APP.ui.animations.setBodyState = function (state) {
|
| 121 |
-
// Remove all state classes
|
| 122 |
-
document.body.classList.remove("state-processing", "state-complete", "state-error", "state-idle");
|
| 123 |
-
if (state) {
|
| 124 |
-
document.body.classList.add("state-" + state);
|
| 125 |
-
}
|
| 126 |
-
};
|
| 127 |
-
|
| 128 |
-
// Set processing dot to pulse
|
| 129 |
-
APP.ui.animations.setDotProcessing = function (active) {
|
| 130 |
-
var dot = document.getElementById("sys-dot");
|
| 131 |
-
if (!dot) return;
|
| 132 |
-
if (active) {
|
| 133 |
-
dot.classList.add("processing");
|
| 134 |
-
} else {
|
| 135 |
-
dot.classList.remove("processing");
|
| 136 |
-
}
|
| 137 |
-
};
|
| 138 |
-
|
| 139 |
-
// =========================================
|
| 140 |
-
// Init (called from main.js)
|
| 141 |
-
// =========================================
|
| 142 |
-
|
| 143 |
-
APP.ui.animations.init = function () {
|
| 144 |
-
// Set initial state
|
| 145 |
-
APP.ui.animations.setBodyState("idle");
|
| 146 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/cards.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
| 1 |
-
APP.ui.cards = {};
|
| 2 |
-
|
| 3 |
-
APP.ui.cards.renderFrameTrackList = function () {
|
| 4 |
-
const { state } = APP.core;
|
| 5 |
-
const { $ } = APP.core.utils;
|
| 6 |
-
const frameTrackList = $("#frameTrackList");
|
| 7 |
-
const trackList = $("#trackList"); // Tab 2 (Engage) uses the same card logic
|
| 8 |
-
const trackCount = $("#trackCount");
|
| 9 |
-
|
| 10 |
-
if (!frameTrackList && !trackList) return;
|
| 11 |
-
if (frameTrackList) frameTrackList.innerHTML = "";
|
| 12 |
-
if (trackList) trackList.innerHTML = "";
|
| 13 |
-
|
| 14 |
-
// Filter: show only MATCH items when mission assessment is active
|
| 15 |
-
const allDets = state.detections || [];
|
| 16 |
-
const hasVerdicts = allDets.some(d => d.satisfies === true || d.satisfies === false);
|
| 17 |
-
const dets = hasVerdicts ? allDets.filter(d => d.satisfies === true) : allDets;
|
| 18 |
-
|
| 19 |
-
if (trackCount) trackCount.textContent = dets.length;
|
| 20 |
-
|
| 21 |
-
// Update footer stat
|
| 22 |
-
if (APP.ui.animations && APP.ui.animations.animateTrackCount) {
|
| 23 |
-
APP.ui.animations.animateTrackCount(dets.length);
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
if (dets.length === 0) {
|
| 27 |
-
const emptyMsg = '<div style="color:var(--text3); text-align:center; margin-top:20px; font-size:12px;">No objects detected yet.</div>';
|
| 28 |
-
if (frameTrackList) frameTrackList.innerHTML = emptyMsg;
|
| 29 |
-
if (trackList) trackList.innerHTML = emptyMsg;
|
| 30 |
-
return;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
// Sort: mission-relevant first, then assessed (high→low conf), then unassessed, then stale
|
| 34 |
-
const sorted = [...dets].sort((a, b) => {
|
| 35 |
-
// 1. Mission-relevant objects always on top
|
| 36 |
-
const relA = a.mission_relevant === true ? 0 : 1;
|
| 37 |
-
const relB = b.mission_relevant === true ? 0 : 1;
|
| 38 |
-
if (relA !== relB) return relA - relB;
|
| 39 |
-
// 2. Assessed before unassessed before stale
|
| 40 |
-
const S = APP.core.gptMapping.STATUS;
|
| 41 |
-
const statusOrder = { [S.ASSESSED]: 0, [S.UNASSESSED]: 1, [S.STALE]: 2 };
|
| 42 |
-
const statusA = statusOrder[a.assessment_status] ?? 1;
|
| 43 |
-
const statusB = statusOrder[b.assessment_status] ?? 1;
|
| 44 |
-
if (statusA !== statusB) return statusA - statusB;
|
| 45 |
-
// 3. Higher confidence first
|
| 46 |
-
return (b.score || 0) - (a.score || 0);
|
| 47 |
-
});
|
| 48 |
-
|
| 49 |
-
sorted.forEach((det, i) => {
|
| 50 |
-
const id = det.id || String(i + 1);
|
| 51 |
-
const isActive = state.selectedIds.includes(id);
|
| 52 |
-
const isInspected = state.tracker.selectedTrackId === id;
|
| 53 |
-
|
| 54 |
-
const card = document.createElement("div");
|
| 55 |
-
card.className = "track-card" + (isActive ? " active" : "") + (isInspected ? " inspected" : "");
|
| 56 |
-
card.id = `card-${id}`;
|
| 57 |
-
// Staggered entrance animation
|
| 58 |
-
card.style.animationDelay = (i * 40) + "ms";
|
| 59 |
-
if (det.mission_relevant === false) {
|
| 60 |
-
card.style.opacity = "0.4";
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
card.onclick = () => {
|
| 64 |
-
const ev = new CustomEvent("track-selected", { detail: { id } });
|
| 65 |
-
document.dispatchEvent(ev);
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
// Single unified status badge
|
| 69 |
-
const S = APP.core.gptMapping.STATUS;
|
| 70 |
-
const assessStatus = det.assessment_status || S.UNASSESSED;
|
| 71 |
-
let statusBadge = "";
|
| 72 |
-
if (det.satisfies === true) {
|
| 73 |
-
statusBadge = '<span class="badgemini" style="background:rgba(52,211,153,.15); color:#34d399; border:1px solid rgba(52,211,153,.25)">Match</span>';
|
| 74 |
-
} else if (det.satisfies === false) {
|
| 75 |
-
statusBadge = '<span class="badgemini" style="background:rgba(248,113,113,.12); color:#f87171; border:1px solid rgba(248,113,113,.2)">No Match</span>';
|
| 76 |
-
} else if (det.mission_relevant === true) {
|
| 77 |
-
statusBadge = '<span class="badgemini" style="background:rgba(59,130,246,.12); color:#60a5fa; border:1px solid rgba(59,130,246,.2)">Relevant</span>';
|
| 78 |
-
} else if (assessStatus === S.STALE) {
|
| 79 |
-
statusBadge = '<span class="badgemini" style="background:rgba(251,191,36,.1); color:#fbbf24; border:1px solid rgba(251,191,36,.18)">Stale</span>';
|
| 80 |
-
} else if (det.mission_relevant === false) {
|
| 81 |
-
statusBadge = '<span class="badgemini" style="background:rgba(255,255,255,.04); color:rgba(255,255,255,.4); border:1px solid rgba(255,255,255,.08)">N/R</span>';
|
| 82 |
-
} else {
|
| 83 |
-
statusBadge = `<span class="badgemini" style="background:rgba(255,255,255,.05); color:rgba(255,255,255,.6); border:1px solid rgba(255,255,255,.08)">${(det.score * 100).toFixed(0)}%</span>`;
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
// Satisfaction reason (collapsed summary)
|
| 87 |
-
const desc = det.reason
|
| 88 |
-
? `<div class="track-card-body"><span class="gpt-text">${det.reason}</span></div>`
|
| 89 |
-
: "";
|
| 90 |
-
|
| 91 |
-
// Inline features (only shown when active/expanded)
|
| 92 |
-
let featuresHtml = "";
|
| 93 |
-
if (isInspected && det.features && Object.keys(det.features).length > 0) {
|
| 94 |
-
const entries = Object.entries(det.features).slice(0, 12);
|
| 95 |
-
const rows = entries.map(([k, v]) =>
|
| 96 |
-
`<div class="feat-row"><span class="feat-key">${k}</span><span class="feat-val">${String(v)}</span></div>`
|
| 97 |
-
).join("");
|
| 98 |
-
featuresHtml = `<div class="track-card-features">${rows}</div>`;
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
card.innerHTML = `
|
| 102 |
-
<div class="track-card-header">
|
| 103 |
-
<span>${det.label}</span>
|
| 104 |
-
${statusBadge}
|
| 105 |
-
</div>
|
| 106 |
-
${desc}
|
| 107 |
-
${featuresHtml}
|
| 108 |
-
`;
|
| 109 |
-
if (frameTrackList) frameTrackList.appendChild(card);
|
| 110 |
-
if (trackList) trackList.appendChild(card.cloneNode(true));
|
| 111 |
-
});
|
| 112 |
-
|
| 113 |
-
// Wire up click handlers on cloned Tab 2 cards
|
| 114 |
-
if (trackList) {
|
| 115 |
-
trackList.querySelectorAll(".track-card").forEach(card => {
|
| 116 |
-
const id = card.id.replace("card-", "");
|
| 117 |
-
card.onclick = () => {
|
| 118 |
-
document.dispatchEvent(new CustomEvent("track-selected", { detail: { id } }));
|
| 119 |
-
};
|
| 120 |
-
});
|
| 121 |
-
}
|
| 122 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/chat.js
DELETED
|
@@ -1,320 +0,0 @@
|
|
| 1 |
-
// Chat UI Module - Mission Analyst Chatbot (Multi-Object Context)
|
| 2 |
-
APP.ui.chat = {};
|
| 3 |
-
|
| 4 |
-
APP.ui.chat._history = [];
|
| 5 |
-
APP.ui.chat._trackContexts = {}; // { trackId: trackObject }
|
| 6 |
-
APP.ui.chat._msgCounter = 0;
|
| 7 |
-
|
| 8 |
-
APP.ui.chat.init = function () {
|
| 9 |
-
const { $ } = APP.core.utils;
|
| 10 |
-
const input = $("#chatInput");
|
| 11 |
-
const btn = $("#chatSend");
|
| 12 |
-
|
| 13 |
-
if (input) {
|
| 14 |
-
input.addEventListener("keydown", (e) => {
|
| 15 |
-
if (e.key === "Enter" && !e.shiftKey) {
|
| 16 |
-
e.preventDefault();
|
| 17 |
-
APP.ui.chat.send();
|
| 18 |
-
}
|
| 19 |
-
});
|
| 20 |
-
}
|
| 21 |
-
if (btn) {
|
| 22 |
-
btn.addEventListener("click", () => APP.ui.chat.send());
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
const clearBtn = $("#chatClear");
|
| 26 |
-
if (clearBtn) {
|
| 27 |
-
clearBtn.addEventListener("click", () => APP.ui.chat.clear());
|
| 28 |
-
}
|
| 29 |
-
};
|
| 30 |
-
|
| 31 |
-
APP.ui.chat.send = async function () {
|
| 32 |
-
const { $ } = APP.core.utils;
|
| 33 |
-
const input = $("#chatInput");
|
| 34 |
-
if (!input) return;
|
| 35 |
-
|
| 36 |
-
const message = input.value.trim();
|
| 37 |
-
if (!message) return;
|
| 38 |
-
|
| 39 |
-
input.value = "";
|
| 40 |
-
input.disabled = true;
|
| 41 |
-
|
| 42 |
-
const missionText = $("#missionText");
|
| 43 |
-
const mission = missionText ? missionText.value.trim() : "";
|
| 44 |
-
|
| 45 |
-
// Add user message to UI and history
|
| 46 |
-
APP.ui.chat._addMessage("user", message);
|
| 47 |
-
APP.ui.chat._history.push({ role: "user", content: message });
|
| 48 |
-
|
| 49 |
-
// Show loading indicator
|
| 50 |
-
const loadingId = APP.ui.chat._addMessage("assistant", "", true);
|
| 51 |
-
|
| 52 |
-
try {
|
| 53 |
-
const activeObjects = Object.values(APP.ui.chat._trackContexts);
|
| 54 |
-
const response = await APP.api.client.chatSend(
|
| 55 |
-
message,
|
| 56 |
-
mission,
|
| 57 |
-
activeObjects,
|
| 58 |
-
APP.ui.chat._history
|
| 59 |
-
);
|
| 60 |
-
|
| 61 |
-
// Remove loading, add real response
|
| 62 |
-
APP.ui.chat._removeMessage(loadingId);
|
| 63 |
-
APP.ui.chat._addMessage("assistant", response);
|
| 64 |
-
APP.ui.chat._history.push({ role: "assistant", content: response });
|
| 65 |
-
} catch (err) {
|
| 66 |
-
APP.ui.chat._removeMessage(loadingId);
|
| 67 |
-
APP.ui.chat._addMessage("system", "Error: " + err.message);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
input.disabled = false;
|
| 71 |
-
input.focus();
|
| 72 |
-
};
|
| 73 |
-
|
| 74 |
-
/**
|
| 75 |
-
* Add a track to the active context (additive — does not clear).
|
| 76 |
-
*/
|
| 77 |
-
APP.ui.chat.injectTrackContext = function (track) {
|
| 78 |
-
if (!track) return;
|
| 79 |
-
|
| 80 |
-
// Skip if already in context
|
| 81 |
-
if (APP.ui.chat._trackContexts[track.id]) return;
|
| 82 |
-
|
| 83 |
-
APP.ui.chat._trackContexts[track.id] = track;
|
| 84 |
-
|
| 85 |
-
// Build visible context summary
|
| 86 |
-
const parts = [`${track.id} \u00b7 ${track.label}`];
|
| 87 |
-
if (track.assessment_status) parts.push(track.assessment_status);
|
| 88 |
-
if (track.satisfies === true) parts.push("satisfies=YES");
|
| 89 |
-
else if (track.satisfies === false) parts.push("satisfies=NO");
|
| 90 |
-
if (track.mission_relevant === true) parts.push("RELEVANT");
|
| 91 |
-
else if (track.mission_relevant === false) parts.push("NOT RELEVANT");
|
| 92 |
-
if (track.score != null) parts.push(`conf=${(track.score * 100).toFixed(0)}%`);
|
| 93 |
-
if (track.reason) parts.push(track.reason);
|
| 94 |
-
|
| 95 |
-
APP.ui.chat._addMessage("marker", "Context loaded for " + parts.join(" \u00b7 "));
|
| 96 |
-
|
| 97 |
-
// Update header and suggestions
|
| 98 |
-
APP.ui.chat._renderHeader();
|
| 99 |
-
APP.ui.chat._renderSuggestions();
|
| 100 |
-
APP.ui.chat._updatePlaceholder();
|
| 101 |
-
};
|
| 102 |
-
|
| 103 |
-
/**
|
| 104 |
-
* Remove a track from active context.
|
| 105 |
-
*/
|
| 106 |
-
APP.ui.chat.removeTrackContext = function (trackId) {
|
| 107 |
-
if (!APP.ui.chat._trackContexts[trackId]) return;
|
| 108 |
-
|
| 109 |
-
const track = APP.ui.chat._trackContexts[trackId];
|
| 110 |
-
delete APP.ui.chat._trackContexts[trackId];
|
| 111 |
-
|
| 112 |
-
APP.ui.chat._addMessage("marker", "\u2500\u2500 " + track.id + " " + track.label + " removed from context \u2500\u2500");
|
| 113 |
-
|
| 114 |
-
APP.ui.chat._renderHeader();
|
| 115 |
-
APP.ui.chat._renderSuggestions();
|
| 116 |
-
APP.ui.chat._updatePlaceholder();
|
| 117 |
-
};
|
| 118 |
-
|
| 119 |
-
/**
|
| 120 |
-
* Full clear — reset everything.
|
| 121 |
-
*/
|
| 122 |
-
APP.ui.chat.clear = function () {
|
| 123 |
-
const { $ } = APP.core.utils;
|
| 124 |
-
const container = $("#chatMessages");
|
| 125 |
-
if (container) container.innerHTML = "";
|
| 126 |
-
APP.ui.chat._history = [];
|
| 127 |
-
APP.ui.chat._trackContexts = {};
|
| 128 |
-
APP.ui.chat._renderHeader();
|
| 129 |
-
APP.ui.chat._renderSuggestions();
|
| 130 |
-
APP.ui.chat._updatePlaceholder();
|
| 131 |
-
};
|
| 132 |
-
|
| 133 |
-
/**
|
| 134 |
-
* Render the persistent context header with chips.
|
| 135 |
-
*/
|
| 136 |
-
APP.ui.chat._renderHeader = function () {
|
| 137 |
-
const { $ } = APP.core.utils;
|
| 138 |
-
const header = $("#chatContextHeader");
|
| 139 |
-
const chips = $("#chatContextChips");
|
| 140 |
-
if (!header || !chips) return;
|
| 141 |
-
|
| 142 |
-
const contexts = Object.values(APP.ui.chat._trackContexts);
|
| 143 |
-
|
| 144 |
-
if (contexts.length === 0) {
|
| 145 |
-
header.style.display = "none";
|
| 146 |
-
return;
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
header.style.display = "flex";
|
| 150 |
-
const esc = APP.core.utils.escapeHtml;
|
| 151 |
-
chips.innerHTML = contexts.map(t => {
|
| 152 |
-
let statusClass = "unassessed";
|
| 153 |
-
let statusDot = "\u25CF";
|
| 154 |
-
if (t.satisfies === true) statusClass = "match";
|
| 155 |
-
else if (t.satisfies === false) statusClass = "no-match";
|
| 156 |
-
|
| 157 |
-
return `<span class="chat-context-chip">
|
| 158 |
-
${esc(t.id)} <span class="chip-label">${esc(t.label)}</span>
|
| 159 |
-
<span class="chip-status ${statusClass}">${statusDot}</span>
|
| 160 |
-
</span>`;
|
| 161 |
-
}).join("");
|
| 162 |
-
};
|
| 163 |
-
|
| 164 |
-
/**
|
| 165 |
-
* Generate and render suggestion chips (only when 2+ objects).
|
| 166 |
-
*/
|
| 167 |
-
APP.ui.chat._renderSuggestions = function () {
|
| 168 |
-
const { $ } = APP.core.utils;
|
| 169 |
-
const container = $("#chatSuggestions");
|
| 170 |
-
if (!container) return;
|
| 171 |
-
|
| 172 |
-
const contexts = Object.values(APP.ui.chat._trackContexts);
|
| 173 |
-
|
| 174 |
-
if (contexts.length < 2) {
|
| 175 |
-
container.style.display = "none";
|
| 176 |
-
return;
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
const suggestions = APP.ui.chat._generateSuggestions(contexts);
|
| 180 |
-
|
| 181 |
-
if (suggestions.length === 0) {
|
| 182 |
-
container.style.display = "none";
|
| 183 |
-
return;
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
container.style.display = "flex";
|
| 187 |
-
container.innerHTML = suggestions.map(s =>
|
| 188 |
-
`<span class="chat-suggestion-chip">${s}</span>`
|
| 189 |
-
).join("");
|
| 190 |
-
|
| 191 |
-
// Wire click handlers
|
| 192 |
-
container.querySelectorAll(".chat-suggestion-chip").forEach(chip => {
|
| 193 |
-
chip.addEventListener("click", () => {
|
| 194 |
-
const input = $("#chatInput");
|
| 195 |
-
if (input) {
|
| 196 |
-
input.value = chip.textContent;
|
| 197 |
-
APP.ui.chat.send();
|
| 198 |
-
}
|
| 199 |
-
});
|
| 200 |
-
});
|
| 201 |
-
};
|
| 202 |
-
|
| 203 |
-
/**
|
| 204 |
-
* Rule-based suggestion generation from active object set.
|
| 205 |
-
* Returns top 3-4 suggestions.
|
| 206 |
-
*/
|
| 207 |
-
APP.ui.chat._generateSuggestions = function (objects) {
|
| 208 |
-
const suggestions = [];
|
| 209 |
-
|
| 210 |
-
// Rule 1: Mixed satisfaction
|
| 211 |
-
const matches = objects.filter(o => o.satisfies === true);
|
| 212 |
-
const noMatches = objects.filter(o => o.satisfies === false);
|
| 213 |
-
if (matches.length > 0 && noMatches.length > 0) {
|
| 214 |
-
suggestions.push(`Why does ${noMatches[0].id} fail mission?`);
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
// Rule 2: Different classes
|
| 218 |
-
const labels = [...new Set(objects.map(o => o.label))];
|
| 219 |
-
if (labels.length > 1 && objects.length <= 4) {
|
| 220 |
-
const a = objects[0], b = objects.find(o => o.label !== a.label);
|
| 221 |
-
if (b) suggestions.push(`Compare ${a.id} (${a.label}) and ${b.id} (${b.label})`);
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
// Rule 3: Speed delta
|
| 225 |
-
const withSpeed = objects.filter(o => o.speed_kph != null && o.speed_kph > 0);
|
| 226 |
-
if (withSpeed.length >= 2) {
|
| 227 |
-
const speeds = withSpeed.map(o => o.speed_kph);
|
| 228 |
-
const delta = Math.max(...speeds) - Math.min(...speeds);
|
| 229 |
-
if (delta > 15) suggestions.push("Which is moving faster?");
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
// Rule 4: Depth delta
|
| 233 |
-
const withDepth = objects.filter(o => o.depth_est_m != null && o.depth_est_m > 0);
|
| 234 |
-
if (withDepth.length >= 2) {
|
| 235 |
-
const depths = withDepth.map(o => o.depth_est_m);
|
| 236 |
-
const delta = Math.max(...depths) - Math.min(...depths);
|
| 237 |
-
if (delta > 50) suggestions.push("Compare distances");
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
// Rule 5: Both match mission
|
| 241 |
-
if (matches.length >= 2) {
|
| 242 |
-
suggestions.push("Which is higher priority?");
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
// Rule 6: Low confidence
|
| 246 |
-
const lowConf = objects.find(o => o.score != null && o.score < 0.7);
|
| 247 |
-
if (lowConf) {
|
| 248 |
-
suggestions.push(`How confident is ${lowConf.id} classification?`);
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
// Rule 7: 3+ objects — aggregate
|
| 252 |
-
if (objects.length >= 3) {
|
| 253 |
-
suggestions.push("Rank by threat level");
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
// Rule 8: Stale assessment
|
| 257 |
-
const S = APP.core.gptMapping ? APP.core.gptMapping.STATUS : {};
|
| 258 |
-
const stale = objects.find(o => o.assessment_status === (S.STALE || "STALE"));
|
| 259 |
-
if (stale) {
|
| 260 |
-
suggestions.push(`Is ${stale.id} assessment still valid?`);
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
// Rule 9: Mixed motion (stationary vs moving)
|
| 264 |
-
const moving = objects.filter(o => o.speed_kph != null && o.speed_kph > 2);
|
| 265 |
-
const stationary = objects.filter(o => o.speed_kph == null || o.speed_kph <= 2);
|
| 266 |
-
if (moving.length > 0 && stationary.length > 0) {
|
| 267 |
-
suggestions.push("Compare movement patterns");
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
// Return top 4, deduplicated
|
| 271 |
-
return suggestions.slice(0, 4);
|
| 272 |
-
};
|
| 273 |
-
|
| 274 |
-
/**
|
| 275 |
-
* Update input placeholder based on context state.
|
| 276 |
-
*/
|
| 277 |
-
APP.ui.chat._updatePlaceholder = function () {
|
| 278 |
-
const { $ } = APP.core.utils;
|
| 279 |
-
const input = $("#chatInput");
|
| 280 |
-
if (!input) return;
|
| 281 |
-
|
| 282 |
-
const count = Object.keys(APP.ui.chat._trackContexts).length;
|
| 283 |
-
if (count === 0) {
|
| 284 |
-
input.placeholder = "Select a track to start...";
|
| 285 |
-
} else if (count === 1) {
|
| 286 |
-
input.placeholder = "Ask about loaded object...";
|
| 287 |
-
} else {
|
| 288 |
-
input.placeholder = `Ask about ${count} loaded objects...`;
|
| 289 |
-
}
|
| 290 |
-
};
|
| 291 |
-
|
| 292 |
-
APP.ui.chat._addMessage = function (role, content, loading) {
|
| 293 |
-
const { $ } = APP.core.utils;
|
| 294 |
-
const container = $("#chatMessages");
|
| 295 |
-
if (!container) return null;
|
| 296 |
-
|
| 297 |
-
const id = "chat-msg-" + (++APP.ui.chat._msgCounter);
|
| 298 |
-
const div = document.createElement("div");
|
| 299 |
-
div.className = "chat-message chat-" + role + (loading ? " loading" : "");
|
| 300 |
-
div.id = id;
|
| 301 |
-
|
| 302 |
-
const esc = APP.core.utils.escapeHtml;
|
| 303 |
-
if (role === "marker") {
|
| 304 |
-
div.innerHTML = `<span class="chat-content">${esc(content)}</span>`;
|
| 305 |
-
} else {
|
| 306 |
-
const safe = (role === "user") ? esc(content) : content;
|
| 307 |
-
const labels = { user: "You", assistant: "AI", system: "SYS" };
|
| 308 |
-
div.innerHTML = `<span class="chat-icon">${labels[role] || ""}</span><span class="chat-content">${safe}</span>`;
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
container.appendChild(div);
|
| 312 |
-
container.scrollTop = container.scrollHeight;
|
| 313 |
-
return id;
|
| 314 |
-
};
|
| 315 |
-
|
| 316 |
-
APP.ui.chat._removeMessage = function (id) {
|
| 317 |
-
if (!id) return;
|
| 318 |
-
const el = document.getElementById(id);
|
| 319 |
-
if (el) el.remove();
|
| 320 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/cursor.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
| 1 |
-
// Agent Cursor Animation Module
|
| 2 |
-
APP.ui.cursor = {};
|
| 3 |
-
|
| 4 |
-
APP.ui.cursor.ensureAgentCursorOverlay = function () {
|
| 5 |
-
const { $ } = APP.core.utils;
|
| 6 |
-
if ($("#agentCursor")) return;
|
| 7 |
-
|
| 8 |
-
const el = document.createElement("div");
|
| 9 |
-
el.id = "agentCursor";
|
| 10 |
-
el.style.cssText = `
|
| 11 |
-
position: fixed;
|
| 12 |
-
width: 12px;
|
| 13 |
-
height: 12px;
|
| 14 |
-
border-radius: 50%;
|
| 15 |
-
background: linear-gradient(135deg, rgba(59, 130, 246, 0.9), rgba(96, 165, 250, 0.9));
|
| 16 |
-
box-shadow: 0 0 16px rgba(59, 130, 246, 0.5), 0 0 32px rgba(96, 165, 250, 0.3);
|
| 17 |
-
pointer-events: none;
|
| 18 |
-
z-index: 10000;
|
| 19 |
-
opacity: 0;
|
| 20 |
-
display: none;
|
| 21 |
-
transition: opacity 0.3s ease;
|
| 22 |
-
`;
|
| 23 |
-
document.body.appendChild(el);
|
| 24 |
-
};
|
| 25 |
-
|
| 26 |
-
APP.ui.cursor.setCursorVisible = function (visible) {
|
| 27 |
-
const { $ } = APP.core.utils;
|
| 28 |
-
const { state } = APP.core;
|
| 29 |
-
|
| 30 |
-
APP.ui.cursor.ensureAgentCursorOverlay();
|
| 31 |
-
const el = $("#agentCursor");
|
| 32 |
-
|
| 33 |
-
if (!el) return;
|
| 34 |
-
|
| 35 |
-
state.ui.agentCursor.visible = visible;
|
| 36 |
-
el.style.opacity = visible ? "1" : "0";
|
| 37 |
-
el.style.display = visible ? "block" : "none";
|
| 38 |
-
};
|
| 39 |
-
|
| 40 |
-
APP.ui.cursor.moveCursorToRect = function (rect) {
|
| 41 |
-
const { state } = APP.core;
|
| 42 |
-
const { $, now } = APP.core.utils;
|
| 43 |
-
|
| 44 |
-
if (state.ui.cursorMode === "off") return;
|
| 45 |
-
|
| 46 |
-
APP.ui.cursor.ensureAgentCursorOverlay();
|
| 47 |
-
const el = $("#agentCursor");
|
| 48 |
-
|
| 49 |
-
if (!el) return;
|
| 50 |
-
|
| 51 |
-
const c = state.ui.agentCursor;
|
| 52 |
-
c.visible = true;
|
| 53 |
-
c.target = rect;
|
| 54 |
-
c.t0 = now();
|
| 55 |
-
el.style.opacity = "1";
|
| 56 |
-
el.style.display = "block";
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
APP.ui.cursor.tickAgentCursor = function () {
|
| 60 |
-
const { state } = APP.core;
|
| 61 |
-
const { $, clamp, now } = APP.core.utils;
|
| 62 |
-
const el = $("#agentCursor");
|
| 63 |
-
|
| 64 |
-
if (!el || state.ui.cursorMode !== "on" || !state.ui.agentCursor.visible) return;
|
| 65 |
-
|
| 66 |
-
const c = state.ui.agentCursor;
|
| 67 |
-
if (!c.target) return;
|
| 68 |
-
|
| 69 |
-
const tx = c.target.left + c.target.width * 0.72;
|
| 70 |
-
const ty = c.target.top + c.target.height * 0.50;
|
| 71 |
-
|
| 72 |
-
// Smooth spring physics
|
| 73 |
-
const dx = tx - (c.x * window.innerWidth);
|
| 74 |
-
const dy = ty - (c.y * window.innerHeight);
|
| 75 |
-
c.vx = (c.vx + dx * 0.0018) * 0.85;
|
| 76 |
-
c.vy = (c.vy + dy * 0.0018) * 0.85;
|
| 77 |
-
|
| 78 |
-
const px = (c.x * window.innerWidth) + c.vx * 18;
|
| 79 |
-
const py = (c.y * window.innerHeight) + c.vy * 18;
|
| 80 |
-
c.x = clamp(px / window.innerWidth, 0.02, 0.98);
|
| 81 |
-
c.y = clamp(py / window.innerHeight, 0.02, 0.98);
|
| 82 |
-
|
| 83 |
-
el.style.transform = `translate(${c.x * window.innerWidth}px, ${c.y * window.innerHeight}px)`;
|
| 84 |
-
|
| 85 |
-
// Hide after settling
|
| 86 |
-
const settle = Math.hypot(dx, dy);
|
| 87 |
-
if (settle < 6 && (now() - c.t0) > 650) {
|
| 88 |
-
el.style.opacity = "0.75";
|
| 89 |
-
}
|
| 90 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/explainability.js
DELETED
|
@@ -1,367 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* explainability.js — Interactive hierarchical interpretability graph.
|
| 3 |
-
*
|
| 4 |
-
* Renders a D3.js tree showing why an object was classified,
|
| 5 |
-
* with multi-LLM consensus indicators and hover tooltips.
|
| 6 |
-
*/
|
| 7 |
-
APP.ui.explainability = {};
|
| 8 |
-
|
| 9 |
-
/** @type {AbortController|null} */
|
| 10 |
-
APP.ui.explainability._abortCtrl = null;
|
| 11 |
-
|
| 12 |
-
/** Category color fallback */
|
| 13 |
-
APP.ui.explainability._COLORS = {
|
| 14 |
-
Structure: "#3b82f6", Function: "#06b6d4", Material: "#f59e0b",
|
| 15 |
-
Color: "#ef4444", Size: "#10b981", Type: "#8b5cf6",
|
| 16 |
-
Motion: "#ec4899", Context: "#64748b", Shape: "#f97316", Markings: "#a855f7",
|
| 17 |
-
};
|
| 18 |
-
|
| 19 |
-
/**
|
| 20 |
-
* Fetch + render the explainability graph for a track.
|
| 21 |
-
* Aborts any previous in-flight request.
|
| 22 |
-
*/
|
| 23 |
-
APP.ui.explainability.load = async function (jobId, trackId) {
|
| 24 |
-
const { state } = APP.core;
|
| 25 |
-
const container = document.getElementById("interpretabilityGraph");
|
| 26 |
-
if (!container) return;
|
| 27 |
-
|
| 28 |
-
// Check cache first
|
| 29 |
-
const cached = state.inspection.explanationCache[trackId];
|
| 30 |
-
if (cached) {
|
| 31 |
-
APP.ui.explainability.render(cached, container);
|
| 32 |
-
return;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
// Abort previous request
|
| 36 |
-
if (APP.ui.explainability._abortCtrl) {
|
| 37 |
-
APP.ui.explainability._abortCtrl.abort();
|
| 38 |
-
}
|
| 39 |
-
APP.ui.explainability._abortCtrl = new AbortController();
|
| 40 |
-
|
| 41 |
-
// Show loading
|
| 42 |
-
container.innerHTML =
|
| 43 |
-
'<div class="explain-loading">' +
|
| 44 |
-
'<div class="explain-spinner"></div>' +
|
| 45 |
-
'<span>Analyzing with GPT-4o, Claude, and Gemini...</span>' +
|
| 46 |
-
'</div>';
|
| 47 |
-
|
| 48 |
-
try {
|
| 49 |
-
const data = await APP.api.inspection.explainTrack(
|
| 50 |
-
jobId, trackId, APP.ui.explainability._abortCtrl.signal
|
| 51 |
-
);
|
| 52 |
-
state.inspection.explanationCache[trackId] = data;
|
| 53 |
-
APP.ui.explainability.render(data, container);
|
| 54 |
-
} catch (err) {
|
| 55 |
-
if (err.name === "AbortError") return; // Aborted — new track selected
|
| 56 |
-
container.innerHTML =
|
| 57 |
-
'<div class="explain-error">' + _escHtml(err.message) + '</div>';
|
| 58 |
-
}
|
| 59 |
-
};
|
| 60 |
-
|
| 61 |
-
/**
|
| 62 |
-
* Render the hierarchical tree inside a container element.
|
| 63 |
-
*/
|
| 64 |
-
APP.ui.explainability.render = function (data, container) {
|
| 65 |
-
container.innerHTML = "";
|
| 66 |
-
|
| 67 |
-
if (!data || !data.categories || data.categories.length === 0) {
|
| 68 |
-
container.innerHTML = '<div class="explain-error">No explanation data available</div>';
|
| 69 |
-
return;
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
// Build D3 hierarchy data
|
| 73 |
-
const root = {
|
| 74 |
-
name: (data.object || "OBJECT").toUpperCase(),
|
| 75 |
-
confidence: data.confidence,
|
| 76 |
-
satisfies: data.satisfies,
|
| 77 |
-
summary: data.reasoning_summary,
|
| 78 |
-
children: data.categories.map(cat => ({
|
| 79 |
-
name: cat.name,
|
| 80 |
-
color: cat.color || APP.ui.explainability._COLORS[cat.name] || "#64748b",
|
| 81 |
-
isCategory: true,
|
| 82 |
-
children: (cat.features || []).map(f => ({
|
| 83 |
-
name: f.name,
|
| 84 |
-
value: f.value,
|
| 85 |
-
reasoning: f.reasoning,
|
| 86 |
-
validators: f.validators || {},
|
| 87 |
-
consensus: f.consensus || 0,
|
| 88 |
-
color: cat.color || APP.ui.explainability._COLORS[cat.name] || "#64748b",
|
| 89 |
-
isFeature: true,
|
| 90 |
-
})),
|
| 91 |
-
})),
|
| 92 |
-
};
|
| 93 |
-
|
| 94 |
-
// Dimensions
|
| 95 |
-
const margin = { top: 30, right: 20, bottom: 60, left: 20 };
|
| 96 |
-
const width = container.clientWidth || 500;
|
| 97 |
-
const totalLeaves = root.children.reduce((s, c) => s + (c.children ? c.children.length : 1), 0);
|
| 98 |
-
const height = Math.max(280, totalLeaves * 35 + 120);
|
| 99 |
-
|
| 100 |
-
// Create SVG
|
| 101 |
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
| 102 |
-
svg.setAttribute("width", width);
|
| 103 |
-
svg.setAttribute("height", height);
|
| 104 |
-
svg.setAttribute("class", "explain-svg");
|
| 105 |
-
container.appendChild(svg);
|
| 106 |
-
|
| 107 |
-
// Glow filter
|
| 108 |
-
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
| 109 |
-
defs.innerHTML =
|
| 110 |
-
'<filter id="explainGlow"><feGaussianBlur stdDeviation="3" result="blur"/>' +
|
| 111 |
-
'<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>';
|
| 112 |
-
svg.appendChild(defs);
|
| 113 |
-
|
| 114 |
-
// D3 tree layout
|
| 115 |
-
const hierarchy = d3.hierarchy(root);
|
| 116 |
-
const treeLayout = d3.tree().size([
|
| 117 |
-
width - margin.left - margin.right,
|
| 118 |
-
height - margin.top - margin.bottom,
|
| 119 |
-
]);
|
| 120 |
-
treeLayout(hierarchy);
|
| 121 |
-
|
| 122 |
-
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
| 123 |
-
g.setAttribute("transform", `translate(${margin.left},${margin.top})`);
|
| 124 |
-
svg.appendChild(g);
|
| 125 |
-
|
| 126 |
-
// Draw edges (curved paths)
|
| 127 |
-
hierarchy.links().forEach(link => {
|
| 128 |
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
| 129 |
-
const sx = link.source.x, sy = link.source.y;
|
| 130 |
-
const tx = link.target.x, ty = link.target.y;
|
| 131 |
-
const my = (sy + ty) / 2;
|
| 132 |
-
path.setAttribute("d", `M${sx},${sy} C${sx},${my} ${tx},${my} ${tx},${ty}`);
|
| 133 |
-
path.setAttribute("fill", "none");
|
| 134 |
-
path.setAttribute("stroke", link.target.data.color || "#3b82f6");
|
| 135 |
-
path.setAttribute("stroke-width", link.source.depth === 0 ? "2.5" : "1.5");
|
| 136 |
-
path.setAttribute("opacity", "0.5");
|
| 137 |
-
path.setAttribute("filter", "url(#explainGlow)");
|
| 138 |
-
g.appendChild(path);
|
| 139 |
-
});
|
| 140 |
-
|
| 141 |
-
// Draw nodes
|
| 142 |
-
hierarchy.descendants().forEach(node => {
|
| 143 |
-
const d = node.data;
|
| 144 |
-
const ng = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
| 145 |
-
ng.setAttribute("transform", `translate(${node.x},${node.y})`);
|
| 146 |
-
|
| 147 |
-
if (node.depth === 0) {
|
| 148 |
-
// Root node
|
| 149 |
-
_drawRootNode(ng, d);
|
| 150 |
-
} else if (d.isCategory) {
|
| 151 |
-
// Category node
|
| 152 |
-
_drawCategoryNode(ng, d);
|
| 153 |
-
} else if (d.isFeature) {
|
| 154 |
-
// Feature leaf node
|
| 155 |
-
_drawFeatureNode(ng, d);
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
// Tooltip on hover
|
| 159 |
-
ng.addEventListener("mouseenter", (e) => _showTooltip(e, d, container));
|
| 160 |
-
ng.addEventListener("mouseleave", () => _hideTooltip(container));
|
| 161 |
-
|
| 162 |
-
g.appendChild(ng);
|
| 163 |
-
});
|
| 164 |
-
|
| 165 |
-
// Consensus bar
|
| 166 |
-
if (data.consensus_bar) {
|
| 167 |
-
_drawConsensusBar(svg, data.consensus_bar, width, height, margin);
|
| 168 |
-
}
|
| 169 |
-
};
|
| 170 |
-
|
| 171 |
-
/**
|
| 172 |
-
* Clear the graph and remove tooltip.
|
| 173 |
-
*/
|
| 174 |
-
APP.ui.explainability.clear = function () {
|
| 175 |
-
const container = document.getElementById("interpretabilityGraph");
|
| 176 |
-
if (container) container.innerHTML = "";
|
| 177 |
-
if (APP.ui.explainability._abortCtrl) {
|
| 178 |
-
APP.ui.explainability._abortCtrl.abort();
|
| 179 |
-
APP.ui.explainability._abortCtrl = null;
|
| 180 |
-
}
|
| 181 |
-
};
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
// ── Private helpers ──────────────────────────────────────────────
|
| 185 |
-
|
| 186 |
-
function _drawRootNode(g, d) {
|
| 187 |
-
const w = 120, h = 32, rx = 8;
|
| 188 |
-
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
| 189 |
-
rect.setAttribute("x", -w / 2); rect.setAttribute("y", -h / 2);
|
| 190 |
-
rect.setAttribute("width", w); rect.setAttribute("height", h);
|
| 191 |
-
rect.setAttribute("rx", rx); rect.setAttribute("fill", "#7c3aed");
|
| 192 |
-
rect.setAttribute("filter", "url(#explainGlow)");
|
| 193 |
-
g.appendChild(rect);
|
| 194 |
-
|
| 195 |
-
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 196 |
-
text.setAttribute("text-anchor", "middle"); text.setAttribute("dy", "0.35em");
|
| 197 |
-
text.setAttribute("fill", "white"); text.setAttribute("font-size", "11");
|
| 198 |
-
text.setAttribute("font-weight", "700");
|
| 199 |
-
text.textContent = d.name;
|
| 200 |
-
if (d.confidence != null) text.textContent += ` ${Math.round(d.confidence * 100)}%`;
|
| 201 |
-
g.appendChild(text);
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
function _drawCategoryNode(g, d) {
|
| 205 |
-
const w = 90, h = 26, rx = 6;
|
| 206 |
-
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
| 207 |
-
rect.setAttribute("x", -w / 2); rect.setAttribute("y", -h / 2);
|
| 208 |
-
rect.setAttribute("width", w); rect.setAttribute("height", h);
|
| 209 |
-
rect.setAttribute("rx", rx); rect.setAttribute("fill", d.color || "#3b82f6");
|
| 210 |
-
rect.setAttribute("filter", "url(#explainGlow)"); rect.setAttribute("opacity", "0.95");
|
| 211 |
-
g.appendChild(rect);
|
| 212 |
-
|
| 213 |
-
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 214 |
-
text.setAttribute("text-anchor", "middle"); text.setAttribute("dy", "0.35em");
|
| 215 |
-
text.setAttribute("fill", "white"); text.setAttribute("font-size", "9");
|
| 216 |
-
text.setAttribute("font-weight", "600");
|
| 217 |
-
text.textContent = d.name;
|
| 218 |
-
g.appendChild(text);
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
function _drawFeatureNode(g, d) {
|
| 222 |
-
const w = 80, h = 22, rx = 5;
|
| 223 |
-
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
| 224 |
-
rect.setAttribute("x", -w / 2); rect.setAttribute("y", -h / 2);
|
| 225 |
-
rect.setAttribute("width", w); rect.setAttribute("height", h);
|
| 226 |
-
rect.setAttribute("rx", rx); rect.setAttribute("fill", "#0f172a");
|
| 227 |
-
rect.setAttribute("stroke", d.color || "#3b82f6"); rect.setAttribute("stroke-width", "1.5");
|
| 228 |
-
g.appendChild(rect);
|
| 229 |
-
|
| 230 |
-
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 231 |
-
text.setAttribute("text-anchor", "middle"); text.setAttribute("dy", "0.35em");
|
| 232 |
-
text.setAttribute("fill", _lightenColor(d.color || "#3b82f6")); text.setAttribute("font-size", "7.5");
|
| 233 |
-
text.textContent = d.name.length > 12 ? d.name.slice(0, 11) + "\u2026" : d.name;
|
| 234 |
-
g.appendChild(text);
|
| 235 |
-
|
| 236 |
-
// Validator badge
|
| 237 |
-
const total = Object.keys(d.validators || {}).length;
|
| 238 |
-
if (total > 0) {
|
| 239 |
-
const agreed = Object.values(d.validators).filter(v => v.agree).length;
|
| 240 |
-
const badge = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 241 |
-
badge.setAttribute("x", w / 2 - 4); badge.setAttribute("y", -h / 2 - 3);
|
| 242 |
-
badge.setAttribute("text-anchor", "end"); badge.setAttribute("font-size", "7");
|
| 243 |
-
badge.setAttribute("fill", agreed === total ? "#4ade80" : "#f87171");
|
| 244 |
-
badge.textContent = `${agreed}/${total}`;
|
| 245 |
-
g.appendChild(badge);
|
| 246 |
-
}
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
function _drawConsensusBar(svg, bar, width, height, margin) {
|
| 250 |
-
// Positioned at the bottom of the SVG
|
| 251 |
-
const barY = height - 30;
|
| 252 |
-
const barW = width - 40;
|
| 253 |
-
const barX = 20;
|
| 254 |
-
const barH = 6;
|
| 255 |
-
const ratio = bar.total_features > 0 ? bar.agreed / bar.total_features : 0;
|
| 256 |
-
|
| 257 |
-
// Background
|
| 258 |
-
const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
| 259 |
-
bg.setAttribute("x", barX); bg.setAttribute("y", barY);
|
| 260 |
-
bg.setAttribute("width", barW); bg.setAttribute("height", barH);
|
| 261 |
-
bg.setAttribute("rx", 3); bg.setAttribute("fill", "#1e293b");
|
| 262 |
-
svg.appendChild(bg);
|
| 263 |
-
|
| 264 |
-
// Fill
|
| 265 |
-
const fill = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
| 266 |
-
fill.setAttribute("x", barX); fill.setAttribute("y", barY);
|
| 267 |
-
fill.setAttribute("width", barW * ratio); fill.setAttribute("height", barH);
|
| 268 |
-
fill.setAttribute("rx", 3); fill.setAttribute("fill", "#7c3aed");
|
| 269 |
-
fill.setAttribute("opacity", "0.8"); fill.setAttribute("filter", "url(#explainGlow)");
|
| 270 |
-
svg.appendChild(fill);
|
| 271 |
-
|
| 272 |
-
// Label
|
| 273 |
-
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 274 |
-
label.setAttribute("x", barX); label.setAttribute("y", barY + barH + 14);
|
| 275 |
-
label.setAttribute("fill", "#a78bfa"); label.setAttribute("font-size", "9");
|
| 276 |
-
label.textContent = `${bar.agreed}/${bar.total_features} features agreed`;
|
| 277 |
-
svg.appendChild(label);
|
| 278 |
-
|
| 279 |
-
const vLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 280 |
-
vLabel.setAttribute("x", barX + barW); vLabel.setAttribute("y", barY + barH + 14);
|
| 281 |
-
vLabel.setAttribute("text-anchor", "end");
|
| 282 |
-
vLabel.setAttribute("fill", "#6b7280"); vLabel.setAttribute("font-size", "9");
|
| 283 |
-
vLabel.textContent = `${bar.validators_available}/2 validators`;
|
| 284 |
-
svg.appendChild(vLabel);
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
function _showTooltip(event, data, container) {
|
| 288 |
-
_hideTooltip(container);
|
| 289 |
-
if (!data.reasoning && !data.isCategory && !data.summary) return;
|
| 290 |
-
|
| 291 |
-
const tip = document.createElement("div");
|
| 292 |
-
tip.className = "explain-tooltip";
|
| 293 |
-
|
| 294 |
-
if (data.isCategory) {
|
| 295 |
-
// Category summary
|
| 296 |
-
const children = data.children || [];
|
| 297 |
-
const total = children.length;
|
| 298 |
-
const allAgreed = children.filter(c => {
|
| 299 |
-
const vals = Object.values(c.validators || {});
|
| 300 |
-
return vals.length > 0 && vals.every(v => v.agree);
|
| 301 |
-
}).length;
|
| 302 |
-
tip.innerHTML = `<strong>${_escHtml(data.name)}</strong><br>${allAgreed}/${total} features fully validated`;
|
| 303 |
-
} else if (data.isFeature) {
|
| 304 |
-
// Feature detail
|
| 305 |
-
let html = `<strong>${_escHtml(data.name)}</strong>`;
|
| 306 |
-
if (data.reasoning) {
|
| 307 |
-
html += `<div class="tip-section"><span class="tip-label">GPT-4o:</span> ${_escHtml(data.reasoning)}</div>`;
|
| 308 |
-
}
|
| 309 |
-
const validators = data.validators || {};
|
| 310 |
-
if (validators.claude) {
|
| 311 |
-
const icon = validators.claude.agree ? "\u2713" : "\u2717";
|
| 312 |
-
const cls = validators.claude.agree ? "tip-agree" : "tip-disagree";
|
| 313 |
-
html += `<div class="tip-section ${cls}"><span class="tip-label">${icon} Claude:</span> ${_escHtml(validators.claude.note || "")}</div>`;
|
| 314 |
-
}
|
| 315 |
-
if (validators.gemini) {
|
| 316 |
-
const icon = validators.gemini.agree ? "\u2713" : "\u2717";
|
| 317 |
-
const cls = validators.gemini.agree ? "tip-agree" : "tip-disagree";
|
| 318 |
-
html += `<div class="tip-section ${cls}"><span class="tip-label">${icon} Gemini:</span> ${_escHtml(validators.gemini.note || "")}</div>`;
|
| 319 |
-
}
|
| 320 |
-
tip.innerHTML = html;
|
| 321 |
-
} else if (data.summary) {
|
| 322 |
-
tip.innerHTML = `<strong>${_escHtml(data.name)}</strong><br>${_escHtml(data.summary)}`;
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
// Position near the hovered element
|
| 326 |
-
const rect = event.target.closest("g").getBoundingClientRect();
|
| 327 |
-
const containerRect = container.getBoundingClientRect();
|
| 328 |
-
let left = rect.left - containerRect.left + rect.width / 2;
|
| 329 |
-
let top = rect.top - containerRect.top - 8;
|
| 330 |
-
|
| 331 |
-
tip.style.left = left + "px";
|
| 332 |
-
tip.style.top = top + "px";
|
| 333 |
-
tip.style.transform = "translate(-50%, -100%)";
|
| 334 |
-
|
| 335 |
-
container.appendChild(tip);
|
| 336 |
-
|
| 337 |
-
// Clamp to container bounds
|
| 338 |
-
const tipRect = tip.getBoundingClientRect();
|
| 339 |
-
if (tipRect.left < containerRect.left) {
|
| 340 |
-
tip.style.left = (left + (containerRect.left - tipRect.left) + 4) + "px";
|
| 341 |
-
}
|
| 342 |
-
if (tipRect.right > containerRect.right) {
|
| 343 |
-
tip.style.left = (left - (tipRect.right - containerRect.right) - 4) + "px";
|
| 344 |
-
}
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
function _hideTooltip(container) {
|
| 348 |
-
const old = container.querySelector(".explain-tooltip");
|
| 349 |
-
if (old) old.remove();
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
function _escHtml(str) {
|
| 353 |
-
const div = document.createElement("div");
|
| 354 |
-
div.textContent = str || "";
|
| 355 |
-
return div.innerHTML;
|
| 356 |
-
}
|
| 357 |
-
|
| 358 |
-
function _lightenColor(hex) {
|
| 359 |
-
// Return a lighter version for text on dark backgrounds
|
| 360 |
-
const map = {
|
| 361 |
-
"#3b82f6": "#93c5fd", "#06b6d4": "#a5f3fc", "#f59e0b": "#fde68a",
|
| 362 |
-
"#ef4444": "#fca5a5", "#10b981": "#6ee7b7", "#8b5cf6": "#c4b5fd",
|
| 363 |
-
"#ec4899": "#f9a8d4", "#64748b": "#94a3b8", "#f97316": "#fdba74",
|
| 364 |
-
"#a855f7": "#d8b4fe",
|
| 365 |
-
};
|
| 366 |
-
return map[hex] || "#e2e8f0";
|
| 367 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/inspection-3d.js
DELETED
|
@@ -1,474 +0,0 @@
|
|
| 1 |
-
// Inspection 3D Viewer — Three.js mesh + point cloud renderer
|
| 2 |
-
APP.ui.inspection3d = {};
|
| 3 |
-
|
| 4 |
-
// Internal state
|
| 5 |
-
APP.ui.inspection3d._scene = null;
|
| 6 |
-
APP.ui.inspection3d._camera = null;
|
| 7 |
-
APP.ui.inspection3d._renderer = null;
|
| 8 |
-
APP.ui.inspection3d._controls = null;
|
| 9 |
-
APP.ui.inspection3d._animFrameId = null;
|
| 10 |
-
APP.ui.inspection3d._object = null;
|
| 11 |
-
APP.ui.inspection3d._label = null;
|
| 12 |
-
APP.ui.inspection3d._disposed = false;
|
| 13 |
-
|
| 14 |
-
/**
|
| 15 |
-
* Load Three.js and OrbitControls (once).
|
| 16 |
-
*/
|
| 17 |
-
APP.ui.inspection3d._ensureThreeJs = async function () {
|
| 18 |
-
const { loadScriptOnce } = APP.core.utils;
|
| 19 |
-
await loadScriptOnce("three", "/app/vendor/three.min.js");
|
| 20 |
-
await loadScriptOnce("three-orbit", "/app/vendor/OrbitControls.js");
|
| 21 |
-
await loadScriptOnce("three-gltf", "/app/vendor/GLTFLoader.js");
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
/**
|
| 25 |
-
* Render 3D data in the inspection container.
|
| 26 |
-
* Supports both mesh (with indices) and point cloud modes.
|
| 27 |
-
* @param {Object} data — from fetchPointCloud
|
| 28 |
-
*/
|
| 29 |
-
APP.ui.inspection3d.render = async function (data) {
|
| 30 |
-
if (!data || !data.positions || !data.colors) return;
|
| 31 |
-
|
| 32 |
-
const { $ } = APP.core.utils;
|
| 33 |
-
const container = $("#quad3dContainer");
|
| 34 |
-
if (!container) return;
|
| 35 |
-
|
| 36 |
-
try {
|
| 37 |
-
await APP.ui.inspection3d._ensureThreeJs();
|
| 38 |
-
} catch (err) {
|
| 39 |
-
console.error("Failed to load 3D libraries:", err);
|
| 40 |
-
return;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
APP.ui.inspection3d.dispose();
|
| 44 |
-
APP.ui.inspection3d._disposed = false;
|
| 45 |
-
|
| 46 |
-
const rect = container.getBoundingClientRect();
|
| 47 |
-
const width = rect.width || 600;
|
| 48 |
-
const height = rect.height || 350;
|
| 49 |
-
const isMesh = data.renderMode === "mesh" && data.indices && data.indices.length > 0;
|
| 50 |
-
|
| 51 |
-
// ── Scene ──
|
| 52 |
-
const scene = new THREE.Scene();
|
| 53 |
-
scene.background = new THREE.Color(0x0a0f1e);
|
| 54 |
-
|
| 55 |
-
// ── Camera ──
|
| 56 |
-
const camera = new THREE.PerspectiveCamera(50, width / height, 0.01, 500);
|
| 57 |
-
|
| 58 |
-
// ── Renderer ──
|
| 59 |
-
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
| 60 |
-
renderer.setSize(width, height);
|
| 61 |
-
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 62 |
-
container.appendChild(renderer.domElement);
|
| 63 |
-
|
| 64 |
-
// ── OrbitControls ──
|
| 65 |
-
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
| 66 |
-
controls.enableDamping = true;
|
| 67 |
-
controls.dampingFactor = 0.08;
|
| 68 |
-
controls.rotateSpeed = 0.8;
|
| 69 |
-
controls.zoomSpeed = 1.0;
|
| 70 |
-
controls.panSpeed = 0.6;
|
| 71 |
-
controls.minDistance = 0.1;
|
| 72 |
-
controls.maxDistance = 50;
|
| 73 |
-
|
| 74 |
-
// ── Geometry ──
|
| 75 |
-
const geometry = new THREE.BufferGeometry();
|
| 76 |
-
geometry.setAttribute("position", new THREE.Float32BufferAttribute(data.positions, 3));
|
| 77 |
-
|
| 78 |
-
// Normalize colors to [0,1]
|
| 79 |
-
const colorFloats = new Float32Array(data.colors.length);
|
| 80 |
-
for (let i = 0; i < data.colors.length; i++) {
|
| 81 |
-
colorFloats[i] = data.colors[i] / 255.0;
|
| 82 |
-
}
|
| 83 |
-
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colorFloats, 3));
|
| 84 |
-
|
| 85 |
-
// Center geometry
|
| 86 |
-
geometry.computeBoundingBox();
|
| 87 |
-
const bbox = geometry.boundingBox;
|
| 88 |
-
const center = new THREE.Vector3();
|
| 89 |
-
bbox.getCenter(center);
|
| 90 |
-
geometry.translate(-center.x, -center.y, -center.z);
|
| 91 |
-
|
| 92 |
-
// Normalize scale — depth is arbitrary, scale to ~4 units
|
| 93 |
-
const rawSize = new THREE.Vector3();
|
| 94 |
-
bbox.getSize(rawSize);
|
| 95 |
-
const rawMaxDim = Math.max(rawSize.x, rawSize.y, rawSize.z) || 1;
|
| 96 |
-
const scaleFactor = 4.0 / rawMaxDim;
|
| 97 |
-
geometry.scale(scaleFactor, scaleFactor, scaleFactor);
|
| 98 |
-
|
| 99 |
-
let object3d;
|
| 100 |
-
|
| 101 |
-
if (isMesh) {
|
| 102 |
-
// ── Triangle mesh mode ──
|
| 103 |
-
geometry.setIndex(new THREE.BufferAttribute(data.indices, 1));
|
| 104 |
-
geometry.computeVertexNormals();
|
| 105 |
-
|
| 106 |
-
const material = new THREE.MeshPhongMaterial({
|
| 107 |
-
vertexColors: true,
|
| 108 |
-
side: THREE.DoubleSide,
|
| 109 |
-
shininess: 25,
|
| 110 |
-
specular: new THREE.Color(0x222222),
|
| 111 |
-
flatShading: false
|
| 112 |
-
});
|
| 113 |
-
|
| 114 |
-
object3d = new THREE.Mesh(geometry, material);
|
| 115 |
-
|
| 116 |
-
// Better lighting for mesh
|
| 117 |
-
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
|
| 118 |
-
scene.add(ambient);
|
| 119 |
-
|
| 120 |
-
const keyLight = new THREE.DirectionalLight(0xffffff, 0.7);
|
| 121 |
-
keyLight.position.set(5, 5, 5);
|
| 122 |
-
scene.add(keyLight);
|
| 123 |
-
|
| 124 |
-
const fillLight = new THREE.DirectionalLight(0x8888cc, 0.3);
|
| 125 |
-
fillLight.position.set(-4, 2, -3);
|
| 126 |
-
scene.add(fillLight);
|
| 127 |
-
|
| 128 |
-
const rimLight = new THREE.DirectionalLight(0x4444ff, 0.2);
|
| 129 |
-
rimLight.position.set(0, -3, -5);
|
| 130 |
-
scene.add(rimLight);
|
| 131 |
-
} else {
|
| 132 |
-
// ── Point cloud fallback ──
|
| 133 |
-
const numPts = data.numVertices || data.positions.length / 3;
|
| 134 |
-
const ptSize = numPts > 30000 ? 0.02 : numPts > 10000 ? 0.03 : 0.05;
|
| 135 |
-
const material = new THREE.PointsMaterial({
|
| 136 |
-
size: ptSize,
|
| 137 |
-
vertexColors: true,
|
| 138 |
-
sizeAttenuation: true,
|
| 139 |
-
transparent: true,
|
| 140 |
-
opacity: 0.9
|
| 141 |
-
});
|
| 142 |
-
object3d = new THREE.Points(geometry, material);
|
| 143 |
-
|
| 144 |
-
const ambient = new THREE.AmbientLight(0xffffff, 1.0);
|
| 145 |
-
scene.add(ambient);
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
scene.add(object3d);
|
| 149 |
-
|
| 150 |
-
// Subtle fog
|
| 151 |
-
scene.fog = new THREE.FogExp2(0x0a0f1e, 0.03);
|
| 152 |
-
|
| 153 |
-
// ── Camera positioning ──
|
| 154 |
-
geometry.computeBoundingBox();
|
| 155 |
-
const scaledBbox = geometry.boundingBox;
|
| 156 |
-
const scaledSize = new THREE.Vector3();
|
| 157 |
-
scaledBbox.getSize(scaledSize);
|
| 158 |
-
const maxDim = Math.max(scaledSize.x, scaledSize.y, scaledSize.z) || 1;
|
| 159 |
-
|
| 160 |
-
const camDist = maxDim * 1.6;
|
| 161 |
-
camera.position.set(camDist * 0.7, camDist * 0.4, camDist * 0.9);
|
| 162 |
-
camera.lookAt(0, 0, 0);
|
| 163 |
-
controls.target.set(0, 0, 0);
|
| 164 |
-
controls.update();
|
| 165 |
-
|
| 166 |
-
// Subtle grid
|
| 167 |
-
const gridHelper = new THREE.GridHelper(maxDim * 2, 20, 0x333366, 0x1a1a3e);
|
| 168 |
-
gridHelper.position.y = scaledBbox.min.y;
|
| 169 |
-
scene.add(gridHelper);
|
| 170 |
-
|
| 171 |
-
// ── Info labels ──
|
| 172 |
-
const numV = data.numVertices || data.positions.length / 3;
|
| 173 |
-
const modeTag = isMesh ? "MESH" : "POINT CLOUD";
|
| 174 |
-
let labelText = `${modeTag} ${numV.toLocaleString()} ${isMesh ? "verts" : "pts"}`;
|
| 175 |
-
if (isMesh && data.numTriangles) {
|
| 176 |
-
labelText += ` / ${data.numTriangles.toLocaleString()} tris`;
|
| 177 |
-
}
|
| 178 |
-
if (data.bbox3d) {
|
| 179 |
-
const b = data.bbox3d;
|
| 180 |
-
const dims = [
|
| 181 |
-
(b.max[0] - b.min[0]).toFixed(1),
|
| 182 |
-
(b.max[1] - b.min[1]).toFixed(1),
|
| 183 |
-
(b.max[2] - b.min[2]).toFixed(1)
|
| 184 |
-
];
|
| 185 |
-
labelText += ` | ${dims[0]} x ${dims[1]} x ${dims[2]}`;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
const label = document.createElement("div");
|
| 189 |
-
label.style.cssText = [
|
| 190 |
-
"position: absolute", "top: 8px", "left: 8px",
|
| 191 |
-
"padding: 4px 8px", "background: rgba(0,0,0,0.6)",
|
| 192 |
-
"border: 1px solid rgba(255,255,255,0.15)", "border-radius: 6px",
|
| 193 |
-
"font: 10px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace",
|
| 194 |
-
"color: rgba(255,255,255,0.7)", "pointer-events: none", "z-index: 10"
|
| 195 |
-
].join("; ");
|
| 196 |
-
label.textContent = labelText;
|
| 197 |
-
container.appendChild(label);
|
| 198 |
-
|
| 199 |
-
const hint = document.createElement("div");
|
| 200 |
-
hint.style.cssText = [
|
| 201 |
-
"position: absolute", "bottom: 8px", "right: 8px",
|
| 202 |
-
"padding: 4px 8px", "background: rgba(0,0,0,0.5)",
|
| 203 |
-
"border: 1px solid rgba(255,255,255,0.10)", "border-radius: 6px",
|
| 204 |
-
"font: 9px ui-monospace, SFMono-Regular, Menlo, monospace",
|
| 205 |
-
"color: rgba(255,255,255,0.45)", "pointer-events: none", "z-index: 10"
|
| 206 |
-
].join("; ");
|
| 207 |
-
hint.textContent = "Drag to rotate | Scroll to zoom | Right-drag to pan";
|
| 208 |
-
container.appendChild(hint);
|
| 209 |
-
|
| 210 |
-
// ── Store refs ──
|
| 211 |
-
APP.ui.inspection3d._scene = scene;
|
| 212 |
-
APP.ui.inspection3d._camera = camera;
|
| 213 |
-
APP.ui.inspection3d._renderer = renderer;
|
| 214 |
-
APP.ui.inspection3d._controls = controls;
|
| 215 |
-
APP.ui.inspection3d._object = object3d;
|
| 216 |
-
APP.ui.inspection3d._label = label;
|
| 217 |
-
APP.ui.inspection3d._hint = hint;
|
| 218 |
-
|
| 219 |
-
// ── Resize handler ──
|
| 220 |
-
APP.ui.inspection3d._resizeHandler = function () {
|
| 221 |
-
if (APP.ui.inspection3d._disposed) return;
|
| 222 |
-
const r = container.getBoundingClientRect();
|
| 223 |
-
const w = r.width || 600;
|
| 224 |
-
const h = r.height || 350;
|
| 225 |
-
camera.aspect = w / h;
|
| 226 |
-
camera.updateProjectionMatrix();
|
| 227 |
-
renderer.setSize(w, h);
|
| 228 |
-
};
|
| 229 |
-
window.addEventListener("resize", APP.ui.inspection3d._resizeHandler);
|
| 230 |
-
|
| 231 |
-
// ── Animation loop ──
|
| 232 |
-
function animate() {
|
| 233 |
-
if (APP.ui.inspection3d._disposed) return;
|
| 234 |
-
APP.ui.inspection3d._animFrameId = requestAnimationFrame(animate);
|
| 235 |
-
controls.update();
|
| 236 |
-
renderer.render(scene, camera);
|
| 237 |
-
}
|
| 238 |
-
animate();
|
| 239 |
-
};
|
| 240 |
-
|
| 241 |
-
/**
|
| 242 |
-
* Render a GLB model from Tripo3D in the inspection container.
|
| 243 |
-
* @param {ArrayBuffer} arrayBuffer — raw GLB binary
|
| 244 |
-
*/
|
| 245 |
-
APP.ui.inspection3d.renderGLB = async function (arrayBuffer) {
|
| 246 |
-
const { $ } = APP.core.utils;
|
| 247 |
-
const container = $("#quad3dContainer");
|
| 248 |
-
if (!container) return;
|
| 249 |
-
|
| 250 |
-
try {
|
| 251 |
-
await APP.ui.inspection3d._ensureThreeJs();
|
| 252 |
-
} catch (err) {
|
| 253 |
-
console.error("Failed to load 3D libraries:", err);
|
| 254 |
-
return;
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
APP.ui.inspection3d.dispose();
|
| 258 |
-
APP.ui.inspection3d._disposed = false;
|
| 259 |
-
|
| 260 |
-
var rect = container.getBoundingClientRect();
|
| 261 |
-
var width = rect.width || 600;
|
| 262 |
-
var height = rect.height || 350;
|
| 263 |
-
|
| 264 |
-
// ── Scene ──
|
| 265 |
-
var scene = new THREE.Scene();
|
| 266 |
-
scene.background = new THREE.Color(0x0a0f1e);
|
| 267 |
-
|
| 268 |
-
// ── Camera ──
|
| 269 |
-
var camera = new THREE.PerspectiveCamera(50, width / height, 0.01, 500);
|
| 270 |
-
|
| 271 |
-
// ── Renderer ──
|
| 272 |
-
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
| 273 |
-
renderer.setSize(width, height);
|
| 274 |
-
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 275 |
-
container.appendChild(renderer.domElement);
|
| 276 |
-
|
| 277 |
-
// ── OrbitControls ──
|
| 278 |
-
var controls = new THREE.OrbitControls(camera, renderer.domElement);
|
| 279 |
-
controls.enableDamping = true;
|
| 280 |
-
controls.dampingFactor = 0.08;
|
| 281 |
-
controls.rotateSpeed = 0.8;
|
| 282 |
-
controls.zoomSpeed = 1.0;
|
| 283 |
-
controls.panSpeed = 0.6;
|
| 284 |
-
controls.minDistance = 0.1;
|
| 285 |
-
controls.maxDistance = 50;
|
| 286 |
-
|
| 287 |
-
// ── Lights (same 3-light rig as mesh mode) ──
|
| 288 |
-
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
| 289 |
-
var keyLight = new THREE.DirectionalLight(0xffffff, 0.7);
|
| 290 |
-
keyLight.position.set(5, 5, 5);
|
| 291 |
-
scene.add(keyLight);
|
| 292 |
-
var fillLight = new THREE.DirectionalLight(0x8888cc, 0.3);
|
| 293 |
-
fillLight.position.set(-4, 2, -3);
|
| 294 |
-
scene.add(fillLight);
|
| 295 |
-
var rimLight = new THREE.DirectionalLight(0x4444ff, 0.2);
|
| 296 |
-
rimLight.position.set(0, -3, -5);
|
| 297 |
-
scene.add(rimLight);
|
| 298 |
-
|
| 299 |
-
// ── Parse GLB ──
|
| 300 |
-
var loader = new THREE.GLTFLoader();
|
| 301 |
-
var gltf = await new Promise(function (resolve, reject) {
|
| 302 |
-
loader.parse(arrayBuffer, "", resolve, reject);
|
| 303 |
-
});
|
| 304 |
-
|
| 305 |
-
var model = gltf.scene;
|
| 306 |
-
scene.add(model);
|
| 307 |
-
|
| 308 |
-
// ── Center and normalize scale ──
|
| 309 |
-
var box = new THREE.Box3().setFromObject(model);
|
| 310 |
-
var center = new THREE.Vector3();
|
| 311 |
-
box.getCenter(center);
|
| 312 |
-
model.position.sub(center);
|
| 313 |
-
|
| 314 |
-
var size = new THREE.Vector3();
|
| 315 |
-
box.getSize(size);
|
| 316 |
-
var maxDim = Math.max(size.x, size.y, size.z) || 1;
|
| 317 |
-
var scaleFactor = 4.0 / maxDim;
|
| 318 |
-
model.scale.setScalar(scaleFactor);
|
| 319 |
-
|
| 320 |
-
// Recompute after scaling
|
| 321 |
-
var scaledBox = new THREE.Box3().setFromObject(model);
|
| 322 |
-
var scaledSize = new THREE.Vector3();
|
| 323 |
-
scaledBox.getSize(scaledSize);
|
| 324 |
-
var scaledMax = Math.max(scaledSize.x, scaledSize.y, scaledSize.z) || 1;
|
| 325 |
-
|
| 326 |
-
// ── Camera positioning ──
|
| 327 |
-
var camDist = scaledMax * 1.6;
|
| 328 |
-
camera.position.set(camDist * 0.7, camDist * 0.4, camDist * 0.9);
|
| 329 |
-
camera.lookAt(0, 0, 0);
|
| 330 |
-
controls.target.set(0, 0, 0);
|
| 331 |
-
controls.update();
|
| 332 |
-
|
| 333 |
-
// ── Grid ──
|
| 334 |
-
var gridHelper = new THREE.GridHelper(scaledMax * 2, 20, 0x333366, 0x1a1a3e);
|
| 335 |
-
gridHelper.position.y = scaledBox.min.y;
|
| 336 |
-
scene.add(gridHelper);
|
| 337 |
-
|
| 338 |
-
// ── Fog ──
|
| 339 |
-
scene.fog = new THREE.FogExp2(0x0a0f1e, 0.03);
|
| 340 |
-
|
| 341 |
-
// ── Count vertices for label ──
|
| 342 |
-
var totalVerts = 0;
|
| 343 |
-
model.traverse(function (child) {
|
| 344 |
-
if (child.isMesh && child.geometry) {
|
| 345 |
-
var pos = child.geometry.getAttribute("position");
|
| 346 |
-
if (pos) totalVerts += pos.count;
|
| 347 |
-
}
|
| 348 |
-
});
|
| 349 |
-
|
| 350 |
-
// ── Label ──
|
| 351 |
-
var label = document.createElement("div");
|
| 352 |
-
label.style.cssText = [
|
| 353 |
-
"position: absolute", "top: 8px", "left: 8px",
|
| 354 |
-
"padding: 4px 8px", "background: rgba(0,0,0,0.6)",
|
| 355 |
-
"border: 1px solid rgba(255,255,255,0.15)", "border-radius: 6px",
|
| 356 |
-
"font: 10px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace",
|
| 357 |
-
"color: rgba(255,255,255,0.7)", "pointer-events: none", "z-index: 10"
|
| 358 |
-
].join("; ");
|
| 359 |
-
label.textContent = "GENERATIVE 3D " + totalVerts.toLocaleString() + " verts";
|
| 360 |
-
container.appendChild(label);
|
| 361 |
-
|
| 362 |
-
// ── Hint ──
|
| 363 |
-
var hint = document.createElement("div");
|
| 364 |
-
hint.style.cssText = [
|
| 365 |
-
"position: absolute", "bottom: 8px", "right: 8px",
|
| 366 |
-
"padding: 4px 8px", "background: rgba(0,0,0,0.5)",
|
| 367 |
-
"border: 1px solid rgba(255,255,255,0.10)", "border-radius: 6px",
|
| 368 |
-
"font: 9px ui-monospace, SFMono-Regular, Menlo, monospace",
|
| 369 |
-
"color: rgba(255,255,255,0.45)", "pointer-events: none", "z-index: 10"
|
| 370 |
-
].join("; ");
|
| 371 |
-
hint.textContent = "Drag to rotate | Scroll to zoom | Right-drag to pan";
|
| 372 |
-
container.appendChild(hint);
|
| 373 |
-
|
| 374 |
-
// ── Store refs ──
|
| 375 |
-
APP.ui.inspection3d._scene = scene;
|
| 376 |
-
APP.ui.inspection3d._camera = camera;
|
| 377 |
-
APP.ui.inspection3d._renderer = renderer;
|
| 378 |
-
APP.ui.inspection3d._controls = controls;
|
| 379 |
-
APP.ui.inspection3d._object = model;
|
| 380 |
-
APP.ui.inspection3d._label = label;
|
| 381 |
-
APP.ui.inspection3d._hint = hint;
|
| 382 |
-
|
| 383 |
-
// ── Resize handler ──
|
| 384 |
-
APP.ui.inspection3d._resizeHandler = function () {
|
| 385 |
-
if (APP.ui.inspection3d._disposed) return;
|
| 386 |
-
var r = container.getBoundingClientRect();
|
| 387 |
-
var w = r.width || 600;
|
| 388 |
-
var h = r.height || 350;
|
| 389 |
-
camera.aspect = w / h;
|
| 390 |
-
camera.updateProjectionMatrix();
|
| 391 |
-
renderer.setSize(w, h);
|
| 392 |
-
};
|
| 393 |
-
window.addEventListener("resize", APP.ui.inspection3d._resizeHandler);
|
| 394 |
-
|
| 395 |
-
// ── Animation loop ──
|
| 396 |
-
function animate() {
|
| 397 |
-
if (APP.ui.inspection3d._disposed) return;
|
| 398 |
-
APP.ui.inspection3d._animFrameId = requestAnimationFrame(animate);
|
| 399 |
-
controls.update();
|
| 400 |
-
renderer.render(scene, camera);
|
| 401 |
-
}
|
| 402 |
-
animate();
|
| 403 |
-
};
|
| 404 |
-
|
| 405 |
-
/**
|
| 406 |
-
* Dispose of all Three.js resources and clean up DOM.
|
| 407 |
-
*/
|
| 408 |
-
APP.ui.inspection3d.dispose = function () {
|
| 409 |
-
APP.ui.inspection3d._disposed = true;
|
| 410 |
-
|
| 411 |
-
if (APP.ui.inspection3d._animFrameId) {
|
| 412 |
-
cancelAnimationFrame(APP.ui.inspection3d._animFrameId);
|
| 413 |
-
APP.ui.inspection3d._animFrameId = null;
|
| 414 |
-
}
|
| 415 |
-
|
| 416 |
-
if (APP.ui.inspection3d._resizeHandler) {
|
| 417 |
-
window.removeEventListener("resize", APP.ui.inspection3d._resizeHandler);
|
| 418 |
-
APP.ui.inspection3d._resizeHandler = null;
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
if (APP.ui.inspection3d._controls) {
|
| 422 |
-
APP.ui.inspection3d._controls.dispose();
|
| 423 |
-
APP.ui.inspection3d._controls = null;
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
if (APP.ui.inspection3d._object) {
|
| 427 |
-
if (APP.ui.inspection3d._object.geometry) {
|
| 428 |
-
APP.ui.inspection3d._object.geometry.dispose();
|
| 429 |
-
}
|
| 430 |
-
if (APP.ui.inspection3d._object.material) {
|
| 431 |
-
APP.ui.inspection3d._object.material.dispose();
|
| 432 |
-
}
|
| 433 |
-
APP.ui.inspection3d._object = null;
|
| 434 |
-
}
|
| 435 |
-
|
| 436 |
-
if (APP.ui.inspection3d._renderer) {
|
| 437 |
-
APP.ui.inspection3d._renderer.dispose();
|
| 438 |
-
const canvas = APP.ui.inspection3d._renderer.domElement;
|
| 439 |
-
if (canvas && canvas.parentElement) {
|
| 440 |
-
canvas.parentElement.removeChild(canvas);
|
| 441 |
-
}
|
| 442 |
-
APP.ui.inspection3d._renderer = null;
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
if (APP.ui.inspection3d._label && APP.ui.inspection3d._label.parentElement) {
|
| 446 |
-
APP.ui.inspection3d._label.parentElement.removeChild(APP.ui.inspection3d._label);
|
| 447 |
-
}
|
| 448 |
-
APP.ui.inspection3d._label = null;
|
| 449 |
-
|
| 450 |
-
if (APP.ui.inspection3d._hint && APP.ui.inspection3d._hint.parentElement) {
|
| 451 |
-
APP.ui.inspection3d._hint.parentElement.removeChild(APP.ui.inspection3d._hint);
|
| 452 |
-
}
|
| 453 |
-
APP.ui.inspection3d._hint = null;
|
| 454 |
-
|
| 455 |
-
if (APP.ui.inspection3d._scene) {
|
| 456 |
-
APP.ui.inspection3d._scene.traverse(function (obj) {
|
| 457 |
-
if (obj.geometry) obj.geometry.dispose();
|
| 458 |
-
if (obj.material) {
|
| 459 |
-
var mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
| 460 |
-
mats.forEach(function (m) {
|
| 461 |
-
if (m.map) m.map.dispose();
|
| 462 |
-
if (m.normalMap) m.normalMap.dispose();
|
| 463 |
-
if (m.roughnessMap) m.roughnessMap.dispose();
|
| 464 |
-
if (m.metalnessMap) m.metalnessMap.dispose();
|
| 465 |
-
if (m.emissiveMap) m.emissiveMap.dispose();
|
| 466 |
-
m.dispose();
|
| 467 |
-
});
|
| 468 |
-
}
|
| 469 |
-
});
|
| 470 |
-
APP.ui.inspection3d._scene = null;
|
| 471 |
-
}
|
| 472 |
-
|
| 473 |
-
APP.ui.inspection3d._camera = null;
|
| 474 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/inspection-renders.js
DELETED
|
@@ -1,446 +0,0 @@
|
|
| 1 |
-
// Inspection Renderers — per-mode canvas drawing functions
|
| 2 |
-
APP.ui.inspectionRenders = {};
|
| 3 |
-
|
| 4 |
-
/**
|
| 5 |
-
* Render segmentation mask overlay on the frame.
|
| 6 |
-
* Shows the full frame dimmed, with the selected object's mask highlighted.
|
| 7 |
-
*/
|
| 8 |
-
APP.ui.inspectionRenders._renderSeg = function (canvas, frameImg, maskData, track) {
|
| 9 |
-
if (!frameImg) return;
|
| 10 |
-
|
| 11 |
-
const w = frameImg.naturalWidth || frameImg.width;
|
| 12 |
-
const h = frameImg.naturalHeight || frameImg.height;
|
| 13 |
-
canvas.width = w;
|
| 14 |
-
canvas.height = h;
|
| 15 |
-
const ctx = canvas.getContext("2d");
|
| 16 |
-
|
| 17 |
-
// Draw base frame (dimmed)
|
| 18 |
-
ctx.drawImage(frameImg, 0, 0);
|
| 19 |
-
ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
|
| 20 |
-
ctx.fillRect(0, 0, w, h);
|
| 21 |
-
|
| 22 |
-
if (!maskData) return;
|
| 23 |
-
|
| 24 |
-
// Compute the crop region matching backend's crop_frame(bbox, padding=0.20)
|
| 25 |
-
// The frame image is cropped, but the mask is in full-frame coordinates.
|
| 26 |
-
const _cropRegion = (fullW, fullH) => {
|
| 27 |
-
const padding = 0.20;
|
| 28 |
-
let bx1, by1, bx2, by2;
|
| 29 |
-
if (maskData.bbox) {
|
| 30 |
-
[bx1, by1, bx2, by2] = maskData.bbox;
|
| 31 |
-
} else if (track && track.bbox) {
|
| 32 |
-
bx1 = track.bbox.x * fullW;
|
| 33 |
-
by1 = track.bbox.y * fullH;
|
| 34 |
-
bx2 = (track.bbox.x + track.bbox.w) * fullW;
|
| 35 |
-
by2 = (track.bbox.y + track.bbox.h) * fullH;
|
| 36 |
-
} else {
|
| 37 |
-
return null;
|
| 38 |
-
}
|
| 39 |
-
const bw = bx2 - bx1, bh = by2 - by1;
|
| 40 |
-
const padX = Math.floor(bw * padding), padY = Math.floor(bh * padding);
|
| 41 |
-
return {
|
| 42 |
-
cx1: Math.max(0, bx1 - padX), cy1: Math.max(0, by1 - padY),
|
| 43 |
-
cx2: Math.min(fullW, bx2 + padX), cy2: Math.min(fullH, by2 + padY),
|
| 44 |
-
};
|
| 45 |
-
};
|
| 46 |
-
|
| 47 |
-
const color = maskData.color || [59, 130, 246];
|
| 48 |
-
|
| 49 |
-
if (maskData.rle) {
|
| 50 |
-
const rle = maskData.rle;
|
| 51 |
-
const maskH = rle.size[0];
|
| 52 |
-
const maskW = rle.size[1];
|
| 53 |
-
|
| 54 |
-
// Decode RLE to flat boolean array (column-major, COCO format)
|
| 55 |
-
const mask = new Uint8Array(maskH * maskW);
|
| 56 |
-
let pos = 0;
|
| 57 |
-
let val = 0;
|
| 58 |
-
for (const count of rle.counts) {
|
| 59 |
-
for (let i = 0; i < count && pos < mask.length; i++) {
|
| 60 |
-
mask[pos++] = val;
|
| 61 |
-
}
|
| 62 |
-
val = 1 - val;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
const crop = _cropRegion(maskW, maskH);
|
| 66 |
-
|
| 67 |
-
const frameCanvas = document.createElement("canvas");
|
| 68 |
-
frameCanvas.width = w;
|
| 69 |
-
frameCanvas.height = h;
|
| 70 |
-
const fctx = frameCanvas.getContext("2d");
|
| 71 |
-
fctx.drawImage(frameImg, 0, 0);
|
| 72 |
-
const framePixels = fctx.getImageData(0, 0, w, h);
|
| 73 |
-
|
| 74 |
-
const outPixels = ctx.getImageData(0, 0, w, h);
|
| 75 |
-
const out = outPixels.data;
|
| 76 |
-
const fd = framePixels.data;
|
| 77 |
-
|
| 78 |
-
for (let py = 0; py < h; py++) {
|
| 79 |
-
for (let px = 0; px < w; px++) {
|
| 80 |
-
let mx, my;
|
| 81 |
-
if (crop) {
|
| 82 |
-
// Map cropped canvas pixel back to full-frame coordinates
|
| 83 |
-
mx = Math.floor(crop.cx1 + px * (crop.cx2 - crop.cx1) / w);
|
| 84 |
-
my = Math.floor(crop.cy1 + py * (crop.cy2 - crop.cy1) / h);
|
| 85 |
-
} else {
|
| 86 |
-
mx = Math.floor(px * maskW / w);
|
| 87 |
-
my = Math.floor(py * maskH / h);
|
| 88 |
-
}
|
| 89 |
-
// COCO RLE is column-major: index = x * height + y
|
| 90 |
-
const maskIdx = mx * maskH + my;
|
| 91 |
-
|
| 92 |
-
if (maskIdx >= 0 && maskIdx < mask.length && mask[maskIdx]) {
|
| 93 |
-
const i = (py * w + px) * 4;
|
| 94 |
-
out[i] = Math.round(fd[i] * 0.5 + color[0] * 0.5);
|
| 95 |
-
out[i + 1] = Math.round(fd[i + 1] * 0.5 + color[1] * 0.5);
|
| 96 |
-
out[i + 2] = Math.round(fd[i + 2] * 0.5 + color[2] * 0.5);
|
| 97 |
-
out[i + 3] = 255;
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
}
|
| 101 |
-
ctx.putImageData(outPixels, 0, 0);
|
| 102 |
-
|
| 103 |
-
} else if (maskData._maskImage) {
|
| 104 |
-
// PNG mask: full-frame sized, must map through crop region
|
| 105 |
-
const maskImgW = maskData._maskImage.naturalWidth || maskData._maskImage.width;
|
| 106 |
-
const maskImgH = maskData._maskImage.naturalHeight || maskData._maskImage.height;
|
| 107 |
-
|
| 108 |
-
const maskCanvas = document.createElement("canvas");
|
| 109 |
-
maskCanvas.width = maskImgW;
|
| 110 |
-
maskCanvas.height = maskImgH;
|
| 111 |
-
const mctx = maskCanvas.getContext("2d");
|
| 112 |
-
mctx.drawImage(maskData._maskImage, 0, 0);
|
| 113 |
-
const maskPixels = mctx.getImageData(0, 0, maskImgW, maskImgH);
|
| 114 |
-
|
| 115 |
-
const crop = _cropRegion(maskImgW, maskImgH);
|
| 116 |
-
|
| 117 |
-
const frameCanvas = document.createElement("canvas");
|
| 118 |
-
frameCanvas.width = w;
|
| 119 |
-
frameCanvas.height = h;
|
| 120 |
-
const fctx = frameCanvas.getContext("2d");
|
| 121 |
-
fctx.drawImage(frameImg, 0, 0);
|
| 122 |
-
const framePixels = fctx.getImageData(0, 0, w, h);
|
| 123 |
-
|
| 124 |
-
const outPixels = ctx.getImageData(0, 0, w, h);
|
| 125 |
-
const out = outPixels.data;
|
| 126 |
-
const md = maskPixels.data;
|
| 127 |
-
const fd = framePixels.data;
|
| 128 |
-
|
| 129 |
-
for (let py = 0; py < h; py++) {
|
| 130 |
-
for (let px = 0; px < w; px++) {
|
| 131 |
-
let fullX, fullY;
|
| 132 |
-
if (crop) {
|
| 133 |
-
fullX = Math.floor(crop.cx1 + px * (crop.cx2 - crop.cx1) / w);
|
| 134 |
-
fullY = Math.floor(crop.cy1 + py * (crop.cy2 - crop.cy1) / h);
|
| 135 |
-
} else {
|
| 136 |
-
fullX = Math.floor(px * maskImgW / w);
|
| 137 |
-
fullY = Math.floor(py * maskImgH / h);
|
| 138 |
-
}
|
| 139 |
-
if (fullX >= 0 && fullX < maskImgW && fullY >= 0 && fullY < maskImgH) {
|
| 140 |
-
const mi = (fullY * maskImgW + fullX) * 4;
|
| 141 |
-
const maskVal = md[mi + 3] > 0 ? 1 : (md[mi] > 128 ? 1 : 0);
|
| 142 |
-
if (maskVal) {
|
| 143 |
-
const i = (py * w + px) * 4;
|
| 144 |
-
out[i] = Math.round(fd[i] * 0.5 + color[0] * 0.5);
|
| 145 |
-
out[i + 1] = Math.round(fd[i + 1] * 0.5 + color[1] * 0.5);
|
| 146 |
-
out[i + 2] = Math.round(fd[i + 2] * 0.5 + color[2] * 0.5);
|
| 147 |
-
out[i + 3] = 255;
|
| 148 |
-
}
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
-
}
|
| 152 |
-
ctx.putImageData(outPixels, 0, 0);
|
| 153 |
-
}
|
| 154 |
-
};
|
| 155 |
-
|
| 156 |
-
/**
|
| 157 |
-
* Compute Sobel edge detection from a frame image (pure frontend).
|
| 158 |
-
* @param {HTMLImageElement} frameImg
|
| 159 |
-
* @returns {ImageData} the edge-detected image
|
| 160 |
-
*/
|
| 161 |
-
APP.ui.inspectionRenders.computeEdge = function (frameImg) {
|
| 162 |
-
const w = frameImg.naturalWidth || frameImg.width;
|
| 163 |
-
const h = frameImg.naturalHeight || frameImg.height;
|
| 164 |
-
|
| 165 |
-
const tmpCanvas = document.createElement("canvas");
|
| 166 |
-
tmpCanvas.width = w;
|
| 167 |
-
tmpCanvas.height = h;
|
| 168 |
-
const tmpCtx = tmpCanvas.getContext("2d");
|
| 169 |
-
tmpCtx.drawImage(frameImg, 0, 0);
|
| 170 |
-
const src = tmpCtx.getImageData(0, 0, w, h);
|
| 171 |
-
const d = src.data;
|
| 172 |
-
|
| 173 |
-
// Convert to grayscale
|
| 174 |
-
const gray = new Float32Array(w * h);
|
| 175 |
-
for (let i = 0; i < w * h; i++) {
|
| 176 |
-
const idx = i * 4;
|
| 177 |
-
gray[i] = 0.299 * d[idx] + 0.587 * d[idx + 1] + 0.114 * d[idx + 2];
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
// Sobel kernels
|
| 181 |
-
const out = new ImageData(w, h);
|
| 182 |
-
const od = out.data;
|
| 183 |
-
|
| 184 |
-
for (let y = 1; y < h - 1; y++) {
|
| 185 |
-
for (let x = 1; x < w - 1; x++) {
|
| 186 |
-
const idx = y * w + x;
|
| 187 |
-
|
| 188 |
-
// Gx = Sobel horizontal
|
| 189 |
-
const gx =
|
| 190 |
-
-gray[(y - 1) * w + (x - 1)] + gray[(y - 1) * w + (x + 1)]
|
| 191 |
-
- 2 * gray[y * w + (x - 1)] + 2 * gray[y * w + (x + 1)]
|
| 192 |
-
- gray[(y + 1) * w + (x - 1)] + gray[(y + 1) * w + (x + 1)];
|
| 193 |
-
|
| 194 |
-
// Gy = Sobel vertical
|
| 195 |
-
const gy =
|
| 196 |
-
-gray[(y - 1) * w + (x - 1)] - 2 * gray[(y - 1) * w + x] - gray[(y - 1) * w + (x + 1)]
|
| 197 |
-
+ gray[(y + 1) * w + (x - 1)] + 2 * gray[(y + 1) * w + x] + gray[(y + 1) * w + (x + 1)];
|
| 198 |
-
|
| 199 |
-
const mag = Math.min(255, Math.sqrt(gx * gx + gy * gy));
|
| 200 |
-
|
| 201 |
-
const oi = idx * 4;
|
| 202 |
-
// Render edges as cyan on black (matches design system)
|
| 203 |
-
od[oi] = Math.round(mag * 0.13); // R (slight tint)
|
| 204 |
-
od[oi + 1] = Math.round(mag * 0.83); // G
|
| 205 |
-
od[oi + 2] = Math.round(mag * 0.93); // B (cyan)
|
| 206 |
-
od[oi + 3] = 255;
|
| 207 |
-
}
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
return out;
|
| 211 |
-
};
|
| 212 |
-
|
| 213 |
-
/**
|
| 214 |
-
* Render edge detection result.
|
| 215 |
-
*/
|
| 216 |
-
APP.ui.inspectionRenders._renderEdge = function (canvas, frameImg, edgeData, track) {
|
| 217 |
-
if (!frameImg) return;
|
| 218 |
-
|
| 219 |
-
const w = frameImg.naturalWidth || frameImg.width;
|
| 220 |
-
const h = frameImg.naturalHeight || frameImg.height;
|
| 221 |
-
canvas.width = w;
|
| 222 |
-
canvas.height = h;
|
| 223 |
-
const ctx = canvas.getContext("2d");
|
| 224 |
-
|
| 225 |
-
if (edgeData) {
|
| 226 |
-
ctx.putImageData(edgeData, 0, 0);
|
| 227 |
-
} else {
|
| 228 |
-
ctx.drawImage(frameImg, 0, 0);
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
APP.ui.inspectionRenders._drawBBoxHighlight(ctx, track, w, h);
|
| 232 |
-
};
|
| 233 |
-
|
| 234 |
-
/**
|
| 235 |
-
* Render depth colormap (viridis-like palette) with scale legend.
|
| 236 |
-
*/
|
| 237 |
-
APP.ui.inspectionRenders._renderDepth = function (canvas, frameImg, depthData, track) {
|
| 238 |
-
if (!frameImg) return;
|
| 239 |
-
|
| 240 |
-
const w = frameImg.naturalWidth || frameImg.width;
|
| 241 |
-
const h = frameImg.naturalHeight || frameImg.height;
|
| 242 |
-
canvas.width = w;
|
| 243 |
-
canvas.height = h;
|
| 244 |
-
const ctx = canvas.getContext("2d");
|
| 245 |
-
|
| 246 |
-
if (!depthData || !depthData.data) {
|
| 247 |
-
// No depth — show frame with message
|
| 248 |
-
ctx.drawImage(frameImg, 0, 0);
|
| 249 |
-
ctx.fillStyle = "rgba(0,0,0,0.6)";
|
| 250 |
-
ctx.fillRect(0, 0, w, h);
|
| 251 |
-
ctx.fillStyle = "#aaa";
|
| 252 |
-
ctx.font = "14px monospace";
|
| 253 |
-
ctx.textAlign = "center";
|
| 254 |
-
ctx.fillText("Depth data not available", w / 2, h / 2);
|
| 255 |
-
return;
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
// Apply viridis-like colormap to depth values
|
| 259 |
-
const dw = depthData.width;
|
| 260 |
-
const dh = depthData.height;
|
| 261 |
-
const dd = depthData.data;
|
| 262 |
-
const minD = depthData.min;
|
| 263 |
-
const maxD = depthData.max;
|
| 264 |
-
const range = maxD - minD || 1;
|
| 265 |
-
|
| 266 |
-
const depthCanvas = document.createElement("canvas");
|
| 267 |
-
depthCanvas.width = dw;
|
| 268 |
-
depthCanvas.height = dh;
|
| 269 |
-
const dctx = depthCanvas.getContext("2d");
|
| 270 |
-
const img = dctx.createImageData(dw, dh);
|
| 271 |
-
const id = img.data;
|
| 272 |
-
|
| 273 |
-
for (let i = 0; i < dd.length; i++) {
|
| 274 |
-
const val = dd[i];
|
| 275 |
-
// Handle NaN/Inf values — render as black
|
| 276 |
-
if (!isFinite(val)) {
|
| 277 |
-
const oi = i * 4;
|
| 278 |
-
id[oi] = 0; id[oi + 1] = 0; id[oi + 2] = 0; id[oi + 3] = 255;
|
| 279 |
-
continue;
|
| 280 |
-
}
|
| 281 |
-
const t = APP.core.utils.clamp((val - minD) / range, 0, 1);
|
| 282 |
-
const rgb = APP.ui.inspectionRenders._viridis(t);
|
| 283 |
-
const oi = i * 4;
|
| 284 |
-
id[oi] = rgb[0];
|
| 285 |
-
id[oi + 1] = rgb[1];
|
| 286 |
-
id[oi + 2] = rgb[2];
|
| 287 |
-
id[oi + 3] = 255;
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
dctx.putImageData(img, 0, 0);
|
| 291 |
-
|
| 292 |
-
// Draw scaled to canvas size
|
| 293 |
-
ctx.drawImage(depthCanvas, 0, 0, w, h);
|
| 294 |
-
|
| 295 |
-
// Draw bbox highlight for the selected track
|
| 296 |
-
APP.ui.inspectionRenders._drawBBoxHighlight(ctx, track, w, h);
|
| 297 |
-
|
| 298 |
-
// Draw depth scale legend (vertical gradient bar on the right)
|
| 299 |
-
APP.ui.inspectionRenders._drawDepthLegend(ctx, w, h, minD, maxD);
|
| 300 |
-
|
| 301 |
-
// If track is selected, compute and show average depth in the bbox region
|
| 302 |
-
if (track && track.bbox && dw > 0 && dh > 0) {
|
| 303 |
-
APP.ui.inspectionRenders._drawTrackDepthStats(ctx, track, w, h, depthData);
|
| 304 |
-
}
|
| 305 |
-
};
|
| 306 |
-
|
| 307 |
-
/**
|
| 308 |
-
* Draw a vertical depth scale legend on the right side of the canvas.
|
| 309 |
-
*/
|
| 310 |
-
APP.ui.inspectionRenders._drawDepthLegend = function (ctx, canvasW, canvasH, minD, maxD) {
|
| 311 |
-
const barW = 16;
|
| 312 |
-
const barH = Math.min(180, canvasH - 60);
|
| 313 |
-
const x = canvasW - barW - 30;
|
| 314 |
-
const y = 30;
|
| 315 |
-
|
| 316 |
-
// Background panel
|
| 317 |
-
ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
|
| 318 |
-
ctx.fillRect(x - 6, y - 20, barW + 52, barH + 40);
|
| 319 |
-
ctx.strokeStyle = "rgba(255, 255, 255, 0.15)";
|
| 320 |
-
ctx.lineWidth = 1;
|
| 321 |
-
ctx.strokeRect(x - 6, y - 20, barW + 52, barH + 40);
|
| 322 |
-
|
| 323 |
-
// Draw gradient bar
|
| 324 |
-
for (let py = 0; py < barH; py++) {
|
| 325 |
-
const t = py / (barH - 1); // 0 = top (near/min), 1 = bottom (far/max)
|
| 326 |
-
const rgb = APP.ui.inspectionRenders._viridis(t);
|
| 327 |
-
ctx.fillStyle = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
| 328 |
-
ctx.fillRect(x, y + py, barW, 1);
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
// Border around bar
|
| 332 |
-
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
|
| 333 |
-
ctx.lineWidth = 1;
|
| 334 |
-
ctx.strokeRect(x, y, barW, barH);
|
| 335 |
-
|
| 336 |
-
// Labels
|
| 337 |
-
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
|
| 338 |
-
ctx.font = "10px monospace";
|
| 339 |
-
ctx.textAlign = "left";
|
| 340 |
-
ctx.fillText(`${minD.toFixed(1)}m`, x + barW + 4, y + 10);
|
| 341 |
-
ctx.fillText(`${maxD.toFixed(1)}m`, x + barW + 4, y + barH);
|
| 342 |
-
|
| 343 |
-
// Title
|
| 344 |
-
ctx.fillStyle = "rgba(255, 255, 255, 0.6)";
|
| 345 |
-
ctx.font = "9px monospace";
|
| 346 |
-
ctx.textAlign = "center";
|
| 347 |
-
ctx.fillText("DEPTH", x + barW / 2, y - 6);
|
| 348 |
-
};
|
| 349 |
-
|
| 350 |
-
/**
|
| 351 |
-
* Compute and display average depth within the selected track's bounding box.
|
| 352 |
-
*/
|
| 353 |
-
APP.ui.inspectionRenders._drawTrackDepthStats = function (ctx, track, canvasW, canvasH, depthData) {
|
| 354 |
-
const b = track.bbox;
|
| 355 |
-
const dw = depthData.width;
|
| 356 |
-
const dh = depthData.height;
|
| 357 |
-
const dd = depthData.data;
|
| 358 |
-
|
| 359 |
-
// Map normalized bbox to depth map coordinates
|
| 360 |
-
const x1 = Math.max(0, Math.floor(b.x * dw));
|
| 361 |
-
const y1 = Math.max(0, Math.floor(b.y * dh));
|
| 362 |
-
const x2 = Math.min(dw - 1, Math.floor((b.x + b.w) * dw));
|
| 363 |
-
const y2 = Math.min(dh - 1, Math.floor((b.y + b.h) * dh));
|
| 364 |
-
|
| 365 |
-
let sum = 0;
|
| 366 |
-
let count = 0;
|
| 367 |
-
let localMin = Infinity;
|
| 368 |
-
let localMax = -Infinity;
|
| 369 |
-
|
| 370 |
-
for (let py = y1; py <= y2; py++) {
|
| 371 |
-
for (let px = x1; px <= x2; px++) {
|
| 372 |
-
const val = dd[py * dw + px];
|
| 373 |
-
if (isFinite(val)) {
|
| 374 |
-
sum += val;
|
| 375 |
-
count++;
|
| 376 |
-
if (val < localMin) localMin = val;
|
| 377 |
-
if (val > localMax) localMax = val;
|
| 378 |
-
}
|
| 379 |
-
}
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
if (count === 0) return;
|
| 383 |
-
|
| 384 |
-
const avgDepth = sum / count;
|
| 385 |
-
|
| 386 |
-
// Draw stats label below the bbox
|
| 387 |
-
const bx = b.x * canvasW;
|
| 388 |
-
const by = (b.y + b.h) * canvasH;
|
| 389 |
-
|
| 390 |
-
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
|
| 391 |
-
ctx.fillRect(bx, by + 4, 160, 18);
|
| 392 |
-
|
| 393 |
-
ctx.fillStyle = "rgba(253, 231, 37, 0.9)"; // viridis yellow for contrast
|
| 394 |
-
ctx.font = "bold 11px monospace";
|
| 395 |
-
ctx.textAlign = "left";
|
| 396 |
-
ctx.fillText(`Depth: ${avgDepth.toFixed(1)}m (${localMin.toFixed(1)}-${localMax.toFixed(1)})`, bx + 4, by + 16);
|
| 397 |
-
};
|
| 398 |
-
|
| 399 |
-
/**
|
| 400 |
-
* Draw a highlighted bounding box for the selected track.
|
| 401 |
-
*/
|
| 402 |
-
APP.ui.inspectionRenders._drawBBoxHighlight = function () {
|
| 403 |
-
// No-op — inspection shows the cropped object only, no bbox overlay
|
| 404 |
-
};
|
| 405 |
-
|
| 406 |
-
/**
|
| 407 |
-
* Viridis-like colormap: t in [0,1] -> [R, G, B] in [0,255].
|
| 408 |
-
* Simplified 5-stop linear interpolation.
|
| 409 |
-
*/
|
| 410 |
-
APP.ui.inspectionRenders._viridis = function (t) {
|
| 411 |
-
const stops = [
|
| 412 |
-
[0.0, 68, 1, 84],
|
| 413 |
-
[0.25, 59, 82, 139],
|
| 414 |
-
[0.5, 33, 145, 140],
|
| 415 |
-
[0.75, 94, 201, 98],
|
| 416 |
-
[1.0, 253, 231, 37]
|
| 417 |
-
];
|
| 418 |
-
return APP.ui.inspectionRenders._interpolateColormap(t, stops);
|
| 419 |
-
};
|
| 420 |
-
|
| 421 |
-
/**
|
| 422 |
-
* Linearly interpolate a colormap defined by stops.
|
| 423 |
-
* @param {number} t — value in [0,1]
|
| 424 |
-
* @param {Array} stops — [[t, r, g, b], ...] sorted by t
|
| 425 |
-
* @returns {number[]} [r, g, b]
|
| 426 |
-
*/
|
| 427 |
-
APP.ui.inspectionRenders._interpolateColormap = function (t, stops) {
|
| 428 |
-
if (t <= stops[0][0]) return [stops[0][1], stops[0][2], stops[0][3]];
|
| 429 |
-
if (t >= stops[stops.length - 1][0]) {
|
| 430 |
-
const s = stops[stops.length - 1];
|
| 431 |
-
return [s[1], s[2], s[3]];
|
| 432 |
-
}
|
| 433 |
-
|
| 434 |
-
for (let i = 0; i < stops.length - 1; i++) {
|
| 435 |
-
if (t >= stops[i][0] && t <= stops[i + 1][0]) {
|
| 436 |
-
const f = (t - stops[i][0]) / (stops[i + 1][0] - stops[i][0]);
|
| 437 |
-
return [
|
| 438 |
-
Math.round(stops[i][1] + f * (stops[i + 1][1] - stops[i][1])),
|
| 439 |
-
Math.round(stops[i][2] + f * (stops[i + 1][2] - stops[i][2])),
|
| 440 |
-
Math.round(stops[i][3] + f * (stops[i + 1][3] - stops[i][3]))
|
| 441 |
-
];
|
| 442 |
-
}
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
return [0, 0, 0];
|
| 446 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/inspection.js
DELETED
|
@@ -1,520 +0,0 @@
|
|
| 1 |
-
// Inspection Panel Controller — quad view: seg, edge, depth, 3D simultaneously
|
| 2 |
-
APP.ui.inspection = {};
|
| 3 |
-
|
| 4 |
-
/**
|
| 5 |
-
* Initialize: wire close button, quad expand/collapse, Escape key.
|
| 6 |
-
* Called once from main.js init().
|
| 7 |
-
*/
|
| 8 |
-
APP.ui.inspection.init = function () {
|
| 9 |
-
const { $ } = APP.core.utils;
|
| 10 |
-
|
| 11 |
-
// Close button
|
| 12 |
-
const closeBtn = $("#btnCloseInspection");
|
| 13 |
-
if (closeBtn) {
|
| 14 |
-
closeBtn.addEventListener("click", () => APP.ui.inspection.close());
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
// Quad expand/collapse (event delegation)
|
| 18 |
-
const quad = $("#inspectionQuad");
|
| 19 |
-
if (quad) {
|
| 20 |
-
quad.addEventListener("click", (e) => {
|
| 21 |
-
const quadrant = e.target.closest(".inspection-quadrant");
|
| 22 |
-
if (!quadrant) return;
|
| 23 |
-
const mode = quadrant.getAttribute("data-mode");
|
| 24 |
-
APP.ui.inspection._toggleExpand(mode);
|
| 25 |
-
});
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
// Escape to collapse
|
| 29 |
-
document.addEventListener("keydown", (e) => {
|
| 30 |
-
if (e.key === "Escape" && APP.core.state.inspection.expandedQuadrant) {
|
| 31 |
-
APP.ui.inspection._collapseExpanded();
|
| 32 |
-
}
|
| 33 |
-
});
|
| 34 |
-
};
|
| 35 |
-
|
| 36 |
-
/**
|
| 37 |
-
* Open the inspection panel for a specific track.
|
| 38 |
-
* @param {string} trackId
|
| 39 |
-
*/
|
| 40 |
-
APP.ui.inspection.open = function (trackId) {
|
| 41 |
-
const { state } = APP.core;
|
| 42 |
-
const { $ } = APP.core.utils;
|
| 43 |
-
|
| 44 |
-
if (!trackId) return;
|
| 45 |
-
|
| 46 |
-
const panel = $("#inspectionPanel");
|
| 47 |
-
if (!panel) return;
|
| 48 |
-
|
| 49 |
-
// Determine current frame index
|
| 50 |
-
const video = $("#videoEngage");
|
| 51 |
-
const fps = state.hf.fps || 30;
|
| 52 |
-
const maxFrame = state.hf.totalFrames ? state.hf.totalFrames - 1 : Infinity;
|
| 53 |
-
let frameIdx = 0;
|
| 54 |
-
if (video && isFinite(video.currentTime)) {
|
| 55 |
-
frameIdx = Math.min(Math.floor(video.currentTime * fps), maxFrame);
|
| 56 |
-
}
|
| 57 |
-
if (!isFinite(frameIdx) || frameIdx < 0) frameIdx = 0;
|
| 58 |
-
|
| 59 |
-
// Collapse any expanded quadrant
|
| 60 |
-
APP.ui.inspection._collapseExpanded();
|
| 61 |
-
|
| 62 |
-
// Update state
|
| 63 |
-
state.inspection.trackId = trackId;
|
| 64 |
-
state.inspection.frameIdx = frameIdx;
|
| 65 |
-
state.inspection.visible = true;
|
| 66 |
-
state.inspection.error = null;
|
| 67 |
-
|
| 68 |
-
// Clear caches
|
| 69 |
-
APP.ui.inspection._clearCaches();
|
| 70 |
-
|
| 71 |
-
// Show panel
|
| 72 |
-
panel.style.display = "flex";
|
| 73 |
-
|
| 74 |
-
// Hide empty state
|
| 75 |
-
const empty = $("#inspectionEmpty");
|
| 76 |
-
if (empty) empty.style.display = "none";
|
| 77 |
-
|
| 78 |
-
// Update header
|
| 79 |
-
const track = (state.detections || []).find(d => d.id === trackId);
|
| 80 |
-
APP.ui.inspection._updateHeader(track, frameIdx);
|
| 81 |
-
|
| 82 |
-
// Load all quadrants
|
| 83 |
-
APP.ui.inspection._loadAllQuadrants();
|
| 84 |
-
|
| 85 |
-
// Load explainability graph
|
| 86 |
-
const expJobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 87 |
-
if (expJobId && APP.ui.explainability) {
|
| 88 |
-
APP.ui.explainability.load(expJobId, trackId);
|
| 89 |
-
}
|
| 90 |
-
};
|
| 91 |
-
|
| 92 |
-
/**
|
| 93 |
-
* Close the inspection panel.
|
| 94 |
-
*/
|
| 95 |
-
APP.ui.inspection.close = function () {
|
| 96 |
-
const { state } = APP.core;
|
| 97 |
-
const { $ } = APP.core.utils;
|
| 98 |
-
|
| 99 |
-
state.inspection.visible = false;
|
| 100 |
-
state.inspection.trackId = null;
|
| 101 |
-
state.inspection.loading = false;
|
| 102 |
-
state.inspection.error = null;
|
| 103 |
-
|
| 104 |
-
APP.ui.inspection._collapseExpanded();
|
| 105 |
-
APP.ui.inspection._clearCaches();
|
| 106 |
-
|
| 107 |
-
// Clear explainability graph (but NOT the cache — it's track-scoped)
|
| 108 |
-
if (APP.ui.explainability) APP.ui.explainability.clear();
|
| 109 |
-
|
| 110 |
-
const panel = $("#inspectionPanel");
|
| 111 |
-
if (panel) panel.style.display = "none";
|
| 112 |
-
|
| 113 |
-
// Clean up 3D scene
|
| 114 |
-
if (APP.ui.inspection3d && APP.ui.inspection3d.dispose) {
|
| 115 |
-
APP.ui.inspection3d.dispose();
|
| 116 |
-
}
|
| 117 |
-
};
|
| 118 |
-
|
| 119 |
-
/**
|
| 120 |
-
* Refresh inspection for the current video time (called when user seeks).
|
| 121 |
-
*/
|
| 122 |
-
APP.ui.inspection.refreshFrame = function () {
|
| 123 |
-
const { state } = APP.core;
|
| 124 |
-
if (!state.inspection.visible || !state.inspection.trackId) return;
|
| 125 |
-
|
| 126 |
-
const { $ } = APP.core.utils;
|
| 127 |
-
const video = $("#videoEngage");
|
| 128 |
-
const fps = state.hf.fps || 30;
|
| 129 |
-
const maxFrame = state.hf.totalFrames ? state.hf.totalFrames - 1 : Infinity;
|
| 130 |
-
let frameIdx = 0;
|
| 131 |
-
if (video && isFinite(video.currentTime)) {
|
| 132 |
-
frameIdx = Math.min(Math.floor(video.currentTime * fps), maxFrame);
|
| 133 |
-
}
|
| 134 |
-
if (!isFinite(frameIdx) || frameIdx < 0) frameIdx = 0;
|
| 135 |
-
|
| 136 |
-
if (frameIdx === state.inspection.frameIdx) return;
|
| 137 |
-
|
| 138 |
-
state.inspection.frameIdx = frameIdx;
|
| 139 |
-
APP.ui.inspection._clearCaches();
|
| 140 |
-
|
| 141 |
-
// Update header frame counter
|
| 142 |
-
const track = (state.detections || []).find(d => d.id === state.inspection.trackId);
|
| 143 |
-
APP.ui.inspection._updateHeader(track, frameIdx);
|
| 144 |
-
|
| 145 |
-
APP.ui.inspection._loadAllQuadrants();
|
| 146 |
-
};
|
| 147 |
-
|
| 148 |
-
/**
|
| 149 |
-
* Update the inspection header bar.
|
| 150 |
-
*/
|
| 151 |
-
APP.ui.inspection._updateHeader = function (track, frameIdx) {
|
| 152 |
-
const { $ } = APP.core.utils;
|
| 153 |
-
const { state } = APP.core;
|
| 154 |
-
const totalFrames = state.hf.totalFrames || "--";
|
| 155 |
-
|
| 156 |
-
const nameEl = $("#inspectionObjName");
|
| 157 |
-
const confEl = $("#inspectionConf");
|
| 158 |
-
const statusEl = $("#inspectionStatus");
|
| 159 |
-
const frameEl = $("#inspectionFrame");
|
| 160 |
-
|
| 161 |
-
if (nameEl) nameEl.textContent = (track && track.label || "UNKNOWN").toUpperCase();
|
| 162 |
-
if (confEl) confEl.textContent = track ? (track.score || 0).toFixed(2) : "--";
|
| 163 |
-
if (frameEl) frameEl.textContent = `FRM ${frameIdx} / ${totalFrames}`;
|
| 164 |
-
|
| 165 |
-
if (statusEl) {
|
| 166 |
-
statusEl.className = "inspection-status";
|
| 167 |
-
if (track && track.satisfies === true) {
|
| 168 |
-
statusEl.textContent = "MISSION MATCH";
|
| 169 |
-
statusEl.classList.add("match");
|
| 170 |
-
} else if (track && track.satisfies === false) {
|
| 171 |
-
statusEl.textContent = "NO MATCH";
|
| 172 |
-
statusEl.classList.add("no-match");
|
| 173 |
-
} else {
|
| 174 |
-
statusEl.textContent = "PENDING";
|
| 175 |
-
statusEl.classList.add("pending");
|
| 176 |
-
}
|
| 177 |
-
}
|
| 178 |
-
};
|
| 179 |
-
|
| 180 |
-
/**
|
| 181 |
-
* Clear all cached visualization data.
|
| 182 |
-
*/
|
| 183 |
-
APP.ui.inspection._clearCaches = function () {
|
| 184 |
-
const { state } = APP.core;
|
| 185 |
-
const cache = state.inspection.cache;
|
| 186 |
-
|
| 187 |
-
if (state.inspection._frameImg && state.inspection._frameImg._blobUrl) {
|
| 188 |
-
URL.revokeObjectURL(state.inspection._frameImg._blobUrl);
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
state.inspection.frameImageUrl = null;
|
| 192 |
-
state.inspection._frameImg = null;
|
| 193 |
-
cache.seg = null;
|
| 194 |
-
cache.edge = null;
|
| 195 |
-
cache.depth = null;
|
| 196 |
-
cache.pointcloud = null;
|
| 197 |
-
};
|
| 198 |
-
|
| 199 |
-
/**
|
| 200 |
-
* Load and render all four quadrants in parallel.
|
| 201 |
-
*/
|
| 202 |
-
APP.ui.inspection._loadAllQuadrants = async function () {
|
| 203 |
-
const { state } = APP.core;
|
| 204 |
-
const { $ } = APP.core.utils;
|
| 205 |
-
const api = APP.api.inspection;
|
| 206 |
-
const renders = APP.ui.inspectionRenders;
|
| 207 |
-
const { trackId, frameIdx, cache } = state.inspection;
|
| 208 |
-
|
| 209 |
-
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 210 |
-
if (!jobId || !trackId || frameIdx == null) {
|
| 211 |
-
const msg = !jobId ? "No active job — run detection first"
|
| 212 |
-
: !trackId ? "No track selected"
|
| 213 |
-
: "Frame index unavailable";
|
| 214 |
-
APP.ui.inspection._showQuadError("seg", msg);
|
| 215 |
-
APP.ui.inspection._showQuadError("edge", msg);
|
| 216 |
-
APP.ui.inspection._showQuadError("depth", msg);
|
| 217 |
-
APP.ui.inspection._showQuadError("3d", msg);
|
| 218 |
-
return;
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
const track = (state.detections || []).find(d => d.id === trackId);
|
| 222 |
-
if (!track || !track.bbox) {
|
| 223 |
-
const msg = "Track not found in current frame";
|
| 224 |
-
APP.ui.inspection._showQuadError("seg", msg);
|
| 225 |
-
APP.ui.inspection._showQuadError("edge", msg);
|
| 226 |
-
APP.ui.inspection._showQuadError("depth", msg);
|
| 227 |
-
APP.ui.inspection._showQuadError("3d", msg);
|
| 228 |
-
return;
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
try {
|
| 232 |
-
// Step 1: Fetch shared frame image
|
| 233 |
-
if (!state.inspection._frameImg) {
|
| 234 |
-
const frameImg = await api.fetchFrame(jobId, frameIdx, trackId, 0.20);
|
| 235 |
-
state.inspection._frameImg = frameImg;
|
| 236 |
-
state.inspection.frameImageUrl = frameImg.src;
|
| 237 |
-
}
|
| 238 |
-
} catch (err) {
|
| 239 |
-
APP.ui.inspection._showQuadError("seg", "Frame load failed");
|
| 240 |
-
APP.ui.inspection._showQuadError("edge", "Frame load failed");
|
| 241 |
-
APP.ui.inspection._showQuadError("depth", "Frame load failed");
|
| 242 |
-
APP.ui.inspection._showQuadError("3d", "Frame load failed");
|
| 243 |
-
return;
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
const frameImg = state.inspection._frameImg;
|
| 247 |
-
|
| 248 |
-
// Step 2: Fire seg, edge, depth in parallel; 3D is on-demand
|
| 249 |
-
const segPromise = (async () => {
|
| 250 |
-
APP.ui.inspection._showQuadLoading("seg", true);
|
| 251 |
-
try {
|
| 252 |
-
if (!cache.seg) cache.seg = await api.fetchMask(jobId, frameIdx, trackId);
|
| 253 |
-
const canvas = $("#quadCanvasSeg");
|
| 254 |
-
if (canvas && renders) renders._renderSeg(canvas, frameImg, cache.seg, track);
|
| 255 |
-
APP.ui.inspection._updateQuadMetric("seg", cache.seg);
|
| 256 |
-
} catch (e) {
|
| 257 |
-
APP.ui.inspection._showQuadError("seg", e.message);
|
| 258 |
-
}
|
| 259 |
-
APP.ui.inspection._showQuadLoading("seg", false);
|
| 260 |
-
})();
|
| 261 |
-
|
| 262 |
-
const edgePromise = (async () => {
|
| 263 |
-
try {
|
| 264 |
-
if (!cache.edge && frameImg && renders) {
|
| 265 |
-
cache.edge = renders.computeEdge(frameImg);
|
| 266 |
-
}
|
| 267 |
-
const canvas = $("#quadCanvasEdge");
|
| 268 |
-
if (canvas && renders) renders._renderEdge(canvas, frameImg, cache.edge, track);
|
| 269 |
-
} catch (e) {
|
| 270 |
-
console.error("Edge render error:", e);
|
| 271 |
-
}
|
| 272 |
-
})();
|
| 273 |
-
|
| 274 |
-
const depthPromise = (async () => {
|
| 275 |
-
APP.ui.inspection._showQuadLoading("depth", true);
|
| 276 |
-
try {
|
| 277 |
-
if (!cache.depth) cache.depth = await api.fetchDepth(jobId, frameIdx, trackId);
|
| 278 |
-
const canvas = $("#quadCanvasDepth");
|
| 279 |
-
if (canvas && renders) renders._renderDepth(canvas, frameImg, cache.depth, track);
|
| 280 |
-
APP.ui.inspection._updateQuadMetric("depth", cache.depth);
|
| 281 |
-
} catch (e) {
|
| 282 |
-
APP.ui.inspection._showQuadError("depth", e.message);
|
| 283 |
-
}
|
| 284 |
-
APP.ui.inspection._showQuadLoading("depth", false);
|
| 285 |
-
})();
|
| 286 |
-
|
| 287 |
-
await Promise.allSettled([segPromise, edgePromise, depthPromise]);
|
| 288 |
-
|
| 289 |
-
// Step 3: Show 3D on-demand prompt (no auto-loading)
|
| 290 |
-
APP.ui.inspection._show3dPrompt();
|
| 291 |
-
|
| 292 |
-
// Step 4: Update bottom metrics
|
| 293 |
-
APP.ui.inspection._updateBottomMetrics(track, cache);
|
| 294 |
-
};
|
| 295 |
-
|
| 296 |
-
/**
|
| 297 |
-
* Show on-demand 3D generation prompt in the 3D quadrant.
|
| 298 |
-
*/
|
| 299 |
-
APP.ui.inspection._show3dPrompt = function () {
|
| 300 |
-
const container = document.getElementById("quad3dContainer");
|
| 301 |
-
if (!container) return;
|
| 302 |
-
|
| 303 |
-
// Clean up existing 3D scene
|
| 304 |
-
if (APP.ui.inspection3d && APP.ui.inspection3d.dispose) {
|
| 305 |
-
APP.ui.inspection3d.dispose();
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
// Clear container
|
| 309 |
-
container.innerHTML = "";
|
| 310 |
-
|
| 311 |
-
const prompt = document.createElement("div");
|
| 312 |
-
prompt.className = "quad-3d-prompt";
|
| 313 |
-
prompt.innerHTML = `
|
| 314 |
-
<div class="quad-3d-prompt-icon">
|
| 315 |
-
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
| 316 |
-
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
| 317 |
-
<path d="M2 17l10 5 10-5"/>
|
| 318 |
-
<path d="M2 12l10 5 10-5"/>
|
| 319 |
-
</svg>
|
| 320 |
-
</div>
|
| 321 |
-
<div class="quad-3d-prompt-label">3D MODEL</div>
|
| 322 |
-
<button class="quad-3d-btn" data-mode="depth">COMPUTE 3D</button>
|
| 323 |
-
<button class="quad-3d-btn quad-3d-btn-gen" data-mode="generative">GENERATE AI 3D</button>
|
| 324 |
-
<div class="quad-3d-prompt-hint">AI 3D uses Tripo API (external)</div>
|
| 325 |
-
`;
|
| 326 |
-
|
| 327 |
-
// Wire button handlers
|
| 328 |
-
prompt.querySelector('[data-mode="depth"]').addEventListener("click", (e) => {
|
| 329 |
-
e.stopPropagation();
|
| 330 |
-
APP.ui.inspection._trigger3d(false);
|
| 331 |
-
});
|
| 332 |
-
prompt.querySelector('[data-mode="generative"]').addEventListener("click", (e) => {
|
| 333 |
-
e.stopPropagation();
|
| 334 |
-
APP.ui.inspection._trigger3d(true);
|
| 335 |
-
});
|
| 336 |
-
|
| 337 |
-
container.appendChild(prompt);
|
| 338 |
-
};
|
| 339 |
-
|
| 340 |
-
/**
|
| 341 |
-
* Trigger 3D model generation (on-demand).
|
| 342 |
-
* @param {boolean} generative — if true, use Tripo3D (API cost)
|
| 343 |
-
*/
|
| 344 |
-
APP.ui.inspection._trigger3d = async function (generative) {
|
| 345 |
-
const { state } = APP.core;
|
| 346 |
-
const api = APP.api.inspection;
|
| 347 |
-
const { trackId, frameIdx, cache } = state.inspection;
|
| 348 |
-
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 349 |
-
|
| 350 |
-
if (!jobId || !trackId || frameIdx == null) return;
|
| 351 |
-
|
| 352 |
-
// Clear prompt and show loading with status
|
| 353 |
-
const container = document.getElementById("quad3dContainer");
|
| 354 |
-
if (container) container.innerHTML = "";
|
| 355 |
-
|
| 356 |
-
const statusEl = document.createElement("div");
|
| 357 |
-
statusEl.className = "quad-3d-status";
|
| 358 |
-
statusEl.innerHTML = generative
|
| 359 |
-
? '<div class="inspection-spinner"></div><div class="quad-3d-status-text">Uploading to Tripo3D...</div>'
|
| 360 |
-
: '<div class="inspection-spinner"></div><div class="quad-3d-status-text">Computing depth mesh...</div>';
|
| 361 |
-
if (container) container.appendChild(statusEl);
|
| 362 |
-
|
| 363 |
-
APP.ui.inspection._showQuadLoading("3d", true);
|
| 364 |
-
APP.ui.inspection._showQuadError("3d", "");
|
| 365 |
-
|
| 366 |
-
// Update metric to show status
|
| 367 |
-
const metricEl = document.getElementById("quadMetric3d");
|
| 368 |
-
if (metricEl) metricEl.textContent = generative ? "TRIPO3D GENERATING..." : "COMPUTING...";
|
| 369 |
-
|
| 370 |
-
try {
|
| 371 |
-
cache.pointcloud = await api.fetchPointCloud(jobId, frameIdx, trackId, generative);
|
| 372 |
-
if (container) container.innerHTML = "";
|
| 373 |
-
|
| 374 |
-
if (cache.pointcloud && APP.ui.inspection3d) {
|
| 375 |
-
if (cache.pointcloud.type === "glb") {
|
| 376 |
-
await APP.ui.inspection3d.renderGLB(cache.pointcloud.buffer);
|
| 377 |
-
} else {
|
| 378 |
-
APP.ui.inspection3d.render(cache.pointcloud);
|
| 379 |
-
}
|
| 380 |
-
}
|
| 381 |
-
APP.ui.inspection._updateQuadMetric("3d", cache.pointcloud);
|
| 382 |
-
} catch (e) {
|
| 383 |
-
if (container) container.innerHTML = "";
|
| 384 |
-
APP.ui.inspection._showQuadError("3d", e.message);
|
| 385 |
-
if (metricEl) metricEl.textContent = "FAILED";
|
| 386 |
-
}
|
| 387 |
-
APP.ui.inspection._showQuadLoading("3d", false);
|
| 388 |
-
};
|
| 389 |
-
|
| 390 |
-
/**
|
| 391 |
-
* Toggle per-quadrant loading spinner.
|
| 392 |
-
*/
|
| 393 |
-
APP.ui.inspection._showQuadLoading = function (mode, show) {
|
| 394 |
-
const modeToId = { seg: "quadSeg", edge: "quadEdge", depth: "quadDepth", "3d": "quad3d" };
|
| 395 |
-
const el = document.getElementById(modeToId[mode]);
|
| 396 |
-
if (!el) return;
|
| 397 |
-
const loader = el.querySelector(".quad-loading");
|
| 398 |
-
if (loader) loader.style.display = show ? "flex" : "none";
|
| 399 |
-
};
|
| 400 |
-
|
| 401 |
-
/**
|
| 402 |
-
* Show per-quadrant error message.
|
| 403 |
-
*/
|
| 404 |
-
APP.ui.inspection._showQuadError = function (mode, msg) {
|
| 405 |
-
const modeToId = { seg: "quadSeg", edge: "quadEdge", depth: "quadDepth", "3d": "quad3d" };
|
| 406 |
-
const el = document.getElementById(modeToId[mode]);
|
| 407 |
-
if (!el) return;
|
| 408 |
-
const errEl = el.querySelector(".quad-error");
|
| 409 |
-
if (errEl) {
|
| 410 |
-
errEl.textContent = msg || "";
|
| 411 |
-
errEl.style.display = msg ? "flex" : "none";
|
| 412 |
-
}
|
| 413 |
-
};
|
| 414 |
-
|
| 415 |
-
/**
|
| 416 |
-
* Update per-quadrant metric overlay text.
|
| 417 |
-
*/
|
| 418 |
-
APP.ui.inspection._updateQuadMetric = function (mode, data) {
|
| 419 |
-
const { $ } = APP.core.utils;
|
| 420 |
-
if (!data) return;
|
| 421 |
-
|
| 422 |
-
if (mode === "seg") {
|
| 423 |
-
// Count mask pixels from RLE or mask image
|
| 424 |
-
let area = 0;
|
| 425 |
-
if (data.rle && data.rle.counts) {
|
| 426 |
-
let val = 0;
|
| 427 |
-
for (const count of data.rle.counts) {
|
| 428 |
-
if (val) area += count;
|
| 429 |
-
val = 1 - val;
|
| 430 |
-
}
|
| 431 |
-
}
|
| 432 |
-
const el = $("#quadMetricSeg");
|
| 433 |
-
if (el) el.textContent = area > 0 ? `AREA ${area.toLocaleString()} px` : "";
|
| 434 |
-
} else if (mode === "depth") {
|
| 435 |
-
const el = $("#quadMetricDepth");
|
| 436 |
-
if (el && data.min != null && data.max != null) {
|
| 437 |
-
const avg = ((data.min + data.max) / 2).toFixed(1);
|
| 438 |
-
el.textContent = `${avg}m`;
|
| 439 |
-
}
|
| 440 |
-
} else if (mode === "3d") {
|
| 441 |
-
const el = $("#quadMetric3d");
|
| 442 |
-
if (el) {
|
| 443 |
-
if (data.type === "glb") {
|
| 444 |
-
el.textContent = "GENERATIVE 3D";
|
| 445 |
-
} else {
|
| 446 |
-
const n = data.numVertices || (data.positions ? data.positions.length / 3 : 0);
|
| 447 |
-
const label = data.renderMode === "mesh" ? "verts" : "pts";
|
| 448 |
-
el.textContent = `${n.toLocaleString()} ${label}`;
|
| 449 |
-
}
|
| 450 |
-
}
|
| 451 |
-
}
|
| 452 |
-
};
|
| 453 |
-
|
| 454 |
-
/**
|
| 455 |
-
* Update the bottom metrics strip.
|
| 456 |
-
*/
|
| 457 |
-
APP.ui.inspection._updateBottomMetrics = function (track, cache) {
|
| 458 |
-
const { $ } = APP.core.utils;
|
| 459 |
-
const el = $("#inspectionMetrics");
|
| 460 |
-
if (!el || !track) return;
|
| 461 |
-
|
| 462 |
-
const speed = track.speed_kph != null ? `${Math.round(track.speed_kph)} kph` : "N/A";
|
| 463 |
-
const depth = track.depth_est_m != null ? `${track.depth_est_m.toFixed(1)}m` : "N/A";
|
| 464 |
-
|
| 465 |
-
let area = "N/A";
|
| 466 |
-
if (cache.seg && cache.seg.rle && cache.seg.rle.counts) {
|
| 467 |
-
let px = 0, val = 0;
|
| 468 |
-
for (const count of cache.seg.rle.counts) {
|
| 469 |
-
if (val) px += count;
|
| 470 |
-
val = 1 - val;
|
| 471 |
-
}
|
| 472 |
-
if (px > 0) area = `${px.toLocaleString()} px`;
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
const frames = track.frameCount || "--";
|
| 476 |
-
|
| 477 |
-
el.innerHTML =
|
| 478 |
-
`VEL <span>${speed}</span>` +
|
| 479 |
-
`DEPTH <span>${depth}</span>` +
|
| 480 |
-
`AREA <span>${area}</span>` +
|
| 481 |
-
`TRACKED <span>${frames} frm</span>`;
|
| 482 |
-
};
|
| 483 |
-
|
| 484 |
-
/**
|
| 485 |
-
* Toggle expand/collapse of a quadrant.
|
| 486 |
-
*/
|
| 487 |
-
APP.ui.inspection._toggleExpand = function (mode) {
|
| 488 |
-
const { state } = APP.core;
|
| 489 |
-
if (state.inspection.expandedQuadrant === mode) {
|
| 490 |
-
APP.ui.inspection._collapseExpanded();
|
| 491 |
-
} else {
|
| 492 |
-
APP.ui.inspection._collapseExpanded();
|
| 493 |
-
const modeToId = { seg: "quadSeg", edge: "quadEdge", depth: "quadDepth", "3d": "quad3d" };
|
| 494 |
-
const el = document.getElementById(modeToId[mode]);
|
| 495 |
-
if (el) {
|
| 496 |
-
el.classList.add("expanded");
|
| 497 |
-
state.inspection.expandedQuadrant = mode;
|
| 498 |
-
// Trigger resize for 3D renderer
|
| 499 |
-
if (mode === "3d") {
|
| 500 |
-
setTimeout(() => window.dispatchEvent(new Event("resize")), 50);
|
| 501 |
-
}
|
| 502 |
-
}
|
| 503 |
-
}
|
| 504 |
-
};
|
| 505 |
-
|
| 506 |
-
/**
|
| 507 |
-
* Collapse any currently expanded quadrant.
|
| 508 |
-
*/
|
| 509 |
-
APP.ui.inspection._collapseExpanded = function () {
|
| 510 |
-
const { state } = APP.core;
|
| 511 |
-
const was3d = state.inspection.expandedQuadrant === "3d";
|
| 512 |
-
|
| 513 |
-
const expanded = document.querySelector(".inspection-quadrant.expanded");
|
| 514 |
-
if (expanded) expanded.classList.remove("expanded");
|
| 515 |
-
state.inspection.expandedQuadrant = null;
|
| 516 |
-
|
| 517 |
-
if (was3d) {
|
| 518 |
-
setTimeout(() => window.dispatchEvent(new Event("resize")), 50);
|
| 519 |
-
}
|
| 520 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/logging.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
APP.ui.logging = {};
|
| 2 |
-
|
| 3 |
-
APP.ui.logging.log = function (msg, type = "i") {
|
| 4 |
-
const { $ } = APP.core.utils;
|
| 5 |
-
const consoleHook = $("#sysLog"); // Fixed ID: sysLog from html
|
| 6 |
-
if (!consoleHook) return;
|
| 7 |
-
const div = document.createElement("div");
|
| 8 |
-
div.className = "log-line";
|
| 9 |
-
const ts = new Date().toISOString().split("T")[1].slice(0, 8);
|
| 10 |
-
let color = "#94a3b8"; // dim
|
| 11 |
-
if (type === "g") color = "var(--success)";
|
| 12 |
-
if (type === "w") color = "var(--warning)";
|
| 13 |
-
if (type === "e") color = "var(--danger)";
|
| 14 |
-
if (type === "t") color = "var(--accent2)";
|
| 15 |
-
|
| 16 |
-
div.innerHTML = `<span class="ts">[${ts}]</span> <span style="color:${color}">${msg}</span>`;
|
| 17 |
-
consoleHook.appendChild(div);
|
| 18 |
-
consoleHook.scrollTop = consoleHook.scrollHeight;
|
| 19 |
-
|
| 20 |
-
// Fire toast for error messages
|
| 21 |
-
if (type === "e" && APP.ui.animations && APP.ui.animations.toast) {
|
| 22 |
-
APP.ui.animations.toast(msg, "error", 5000);
|
| 23 |
-
}
|
| 24 |
-
};
|
| 25 |
-
|
| 26 |
-
APP.ui.logging.setStatus = function (level, text) {
|
| 27 |
-
const { $ } = APP.core.utils;
|
| 28 |
-
const sysDot = $("#sys-dot");
|
| 29 |
-
const sysStatus = $("#sys-status");
|
| 30 |
-
if (!sysDot || !sysStatus) return;
|
| 31 |
-
let color = "var(--text3)";
|
| 32 |
-
if (level === "good") color = "var(--success)";
|
| 33 |
-
if (level === "warn") color = "var(--warning)";
|
| 34 |
-
if (level === "bad") color = "var(--danger)";
|
| 35 |
-
|
| 36 |
-
// Update dot CSS class for state-aware styling
|
| 37 |
-
sysDot.classList.remove("warn", "bad");
|
| 38 |
-
if (level === "warn") sysDot.classList.add("warn");
|
| 39 |
-
if (level === "bad") sysDot.classList.add("bad");
|
| 40 |
-
|
| 41 |
-
sysDot.style.background = color;
|
| 42 |
-
sysDot.style.boxShadow = `0 0 8px ${color}`;
|
| 43 |
-
sysStatus.textContent = text;
|
| 44 |
-
sysStatus.style.color = color === "var(--text3)" ? "var(--text)" : color;
|
| 45 |
-
};
|
| 46 |
-
|
| 47 |
-
APP.ui.logging.setHfStatus = function (msg) {
|
| 48 |
-
const { $ } = APP.core.utils;
|
| 49 |
-
const el = $("#hfBackendStatus");
|
| 50 |
-
if (el) el.textContent = `Backend: ${msg}`;
|
| 51 |
-
|
| 52 |
-
if (msg && (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("failed"))) {
|
| 53 |
-
APP.ui.logging.log(msg, "e");
|
| 54 |
-
}
|
| 55 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/overlays.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
| 1 |
-
// SVG-based overlay for bounding box rendering and click-to-select
|
| 2 |
-
APP.ui.overlays = {};
|
| 3 |
-
|
| 4 |
-
const SVG_NS = "http://www.w3.org/2000/svg";
|
| 5 |
-
|
| 6 |
-
/**
|
| 7 |
-
* Get current tracks from state (detections preferred, then tracker.tracks).
|
| 8 |
-
*/
|
| 9 |
-
APP.ui.overlays._getTracks = function () {
|
| 10 |
-
const { state } = APP.core;
|
| 11 |
-
if (state.detections && state.detections.length > 0) return state.detections;
|
| 12 |
-
if (state.tracker.tracks && state.tracker.tracks.length > 0) return state.tracker.tracks;
|
| 13 |
-
return [];
|
| 14 |
-
};
|
| 15 |
-
|
| 16 |
-
/**
|
| 17 |
-
* Ensure the SVG element exists inside the viewbox, create if missing.
|
| 18 |
-
* Uses viewBox="0 0 1 1" so all coordinates are normalized 0-1.
|
| 19 |
-
*/
|
| 20 |
-
APP.ui.overlays._ensureSVG = function () {
|
| 21 |
-
const { $ } = APP.core.utils;
|
| 22 |
-
let svg = $("#engageOverlay");
|
| 23 |
-
if (svg) return svg;
|
| 24 |
-
|
| 25 |
-
const viewbox = $(".viewbox");
|
| 26 |
-
if (!viewbox) return null;
|
| 27 |
-
|
| 28 |
-
svg = document.createElementNS(SVG_NS, "svg");
|
| 29 |
-
svg.id = "engageOverlay";
|
| 30 |
-
svg.setAttribute("viewBox", "0 0 1 1");
|
| 31 |
-
svg.setAttribute("preserveAspectRatio", "none");
|
| 32 |
-
|
| 33 |
-
// Insert after video element
|
| 34 |
-
const video = $("#videoEngage");
|
| 35 |
-
if (video && video.nextSibling) {
|
| 36 |
-
viewbox.insertBefore(svg, video.nextSibling);
|
| 37 |
-
} else {
|
| 38 |
-
viewbox.appendChild(svg);
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
return svg;
|
| 42 |
-
};
|
| 43 |
-
|
| 44 |
-
/**
|
| 45 |
-
* Sync SVG overlay position/size to match the video's actual rendered area
|
| 46 |
-
* (accounting for object-fit: contain letterboxing).
|
| 47 |
-
*/
|
| 48 |
-
APP.ui.overlays._syncToVideo = function (svg) {
|
| 49 |
-
const { $ } = APP.core.utils;
|
| 50 |
-
const video = $("#videoEngage");
|
| 51 |
-
if (!video || !video.videoWidth || !video.videoHeight) return;
|
| 52 |
-
|
| 53 |
-
const container = svg.parentElement;
|
| 54 |
-
if (!container) return;
|
| 55 |
-
|
| 56 |
-
const cw = container.clientWidth;
|
| 57 |
-
const ch = container.clientHeight;
|
| 58 |
-
const vw = video.videoWidth;
|
| 59 |
-
const vh = video.videoHeight;
|
| 60 |
-
|
| 61 |
-
// Calculate rendered video area within container (object-fit: contain)
|
| 62 |
-
const containerRatio = cw / ch;
|
| 63 |
-
const videoRatio = vw / vh;
|
| 64 |
-
|
| 65 |
-
let renderW, renderH, offsetX, offsetY;
|
| 66 |
-
if (videoRatio > containerRatio) {
|
| 67 |
-
// Video wider than container — letterbox top/bottom
|
| 68 |
-
renderW = cw;
|
| 69 |
-
renderH = cw / videoRatio;
|
| 70 |
-
offsetX = 0;
|
| 71 |
-
offsetY = (ch - renderH) / 2;
|
| 72 |
-
} else {
|
| 73 |
-
// Video taller than container — letterbox left/right
|
| 74 |
-
renderH = ch;
|
| 75 |
-
renderW = ch * videoRatio;
|
| 76 |
-
offsetX = (cw - renderW) / 2;
|
| 77 |
-
offsetY = 0;
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
svg.style.left = offsetX + "px";
|
| 81 |
-
svg.style.top = offsetY + "px";
|
| 82 |
-
svg.style.width = renderW + "px";
|
| 83 |
-
svg.style.height = renderH + "px";
|
| 84 |
-
};
|
| 85 |
-
|
| 86 |
-
/**
|
| 87 |
-
* Main render: draws all track boxes with mission-status colors,
|
| 88 |
-
* and highlights the selected track.
|
| 89 |
-
*/
|
| 90 |
-
APP.ui.overlays.render = function () {
|
| 91 |
-
const { state } = APP.core;
|
| 92 |
-
const svg = APP.ui.overlays._ensureSVG();
|
| 93 |
-
if (!svg) return;
|
| 94 |
-
|
| 95 |
-
// Sync SVG position to video's rendered area
|
| 96 |
-
APP.ui.overlays._syncToVideo(svg);
|
| 97 |
-
|
| 98 |
-
// Clear previous content
|
| 99 |
-
svg.innerHTML = "";
|
| 100 |
-
|
| 101 |
-
// In segmentation mode, masks are already rendered in the video by the backend —
|
| 102 |
-
// skip drawing bounding box overlays to avoid clutter.
|
| 103 |
-
if (state.hf.mode === "segmentation") return;
|
| 104 |
-
|
| 105 |
-
const tracks = APP.ui.overlays._getTracks();
|
| 106 |
-
const selIds = state.selectedIds || [];
|
| 107 |
-
const inspectedId = state.tracker.selectedTrackId;
|
| 108 |
-
|
| 109 |
-
if (!tracks.length) return;
|
| 110 |
-
|
| 111 |
-
for (const t of tracks) {
|
| 112 |
-
if (!t.bbox) continue;
|
| 113 |
-
const b = t.bbox;
|
| 114 |
-
const isSelected = selIds.includes(t.id);
|
| 115 |
-
const isInspected = t.id === inspectedId;
|
| 116 |
-
|
| 117 |
-
// Skip tracks with invalid bbox
|
| 118 |
-
if (!isFinite(b.x) || !isFinite(b.y) || !isFinite(b.w) || !isFinite(b.h)) continue;
|
| 119 |
-
|
| 120 |
-
const group = document.createElementNS(SVG_NS, "g");
|
| 121 |
-
group.setAttribute("data-track-id", t.id);
|
| 122 |
-
group.classList.add("track-box");
|
| 123 |
-
if (isSelected) group.classList.add("selected");
|
| 124 |
-
if (isInspected) group.classList.add("inspected");
|
| 125 |
-
|
| 126 |
-
// Mission-status border (green/red) or default neutral
|
| 127 |
-
let strokeColor = "rgba(255,255,255,0.3)";
|
| 128 |
-
if (t.satisfies === true) {
|
| 129 |
-
strokeColor = "rgba(40, 167, 69, 0.8)"; // green
|
| 130 |
-
} else if (t.satisfies === false) {
|
| 131 |
-
strokeColor = "rgba(220, 53, 69, 0.8)"; // red
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
// Main bbox rect
|
| 135 |
-
const rect = document.createElementNS(SVG_NS, "rect");
|
| 136 |
-
rect.setAttribute("x", b.x);
|
| 137 |
-
rect.setAttribute("y", b.y);
|
| 138 |
-
rect.setAttribute("width", b.w);
|
| 139 |
-
rect.setAttribute("height", b.h);
|
| 140 |
-
rect.setAttribute("fill", isSelected ? "rgba(59, 130, 246, 0.12)" : "none");
|
| 141 |
-
rect.setAttribute("stroke", isSelected ? "rgba(59, 130, 246, 0.7)" : strokeColor);
|
| 142 |
-
rect.setAttribute("stroke-width", isInspected ? "0.004" : isSelected ? "0.003" : "0.002");
|
| 143 |
-
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
| 144 |
-
group.appendChild(rect);
|
| 145 |
-
|
| 146 |
-
// Selected glow border
|
| 147 |
-
if (isSelected) {
|
| 148 |
-
const glow = document.createElementNS(SVG_NS, "rect");
|
| 149 |
-
glow.setAttribute("x", b.x);
|
| 150 |
-
glow.setAttribute("y", b.y);
|
| 151 |
-
glow.setAttribute("width", b.w);
|
| 152 |
-
glow.setAttribute("height", b.h);
|
| 153 |
-
glow.setAttribute("fill", "none");
|
| 154 |
-
glow.setAttribute("stroke", isInspected ? "rgba(96, 165, 250, 0.6)" : "rgba(96, 165, 250, 0.25)");
|
| 155 |
-
glow.setAttribute("stroke-width", isInspected ? "2" : "1");
|
| 156 |
-
glow.setAttribute("vector-effect", "non-scaling-stroke");
|
| 157 |
-
group.appendChild(glow);
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
// No label tag — backend-rendered labels are sufficient
|
| 161 |
-
|
| 162 |
-
// Click handler for this track group
|
| 163 |
-
group.addEventListener("click", (e) => {
|
| 164 |
-
e.stopPropagation();
|
| 165 |
-
document.dispatchEvent(new CustomEvent("track-selected", { detail: { id: t.id } }));
|
| 166 |
-
});
|
| 167 |
-
|
| 168 |
-
svg.appendChild(group);
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
};
|
| 172 |
-
|
| 173 |
-
/**
|
| 174 |
-
* Initialize the SVG overlay (creates element if needed).
|
| 175 |
-
* Attaches the empty-area click handler once.
|
| 176 |
-
*/
|
| 177 |
-
APP.ui.overlays.init = function () {
|
| 178 |
-
const svg = APP.ui.overlays._ensureSVG();
|
| 179 |
-
if (svg && !svg._deselectBound) {
|
| 180 |
-
svg.addEventListener("click", (e) => {
|
| 181 |
-
if (e.target === svg) {
|
| 182 |
-
document.dispatchEvent(new CustomEvent("track-selected", { detail: { id: null } }));
|
| 183 |
-
}
|
| 184 |
-
});
|
| 185 |
-
svg._deselectBound = true;
|
| 186 |
-
}
|
| 187 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/style.css
DELETED
|
@@ -1,2028 +0,0 @@
|
|
| 1 |
-
/* =========================================
|
| 2 |
-
ISR Analytics — Investor Demo
|
| 3 |
-
Floating Glass HUD Design System
|
| 4 |
-
========================================= */
|
| 5 |
-
|
| 6 |
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 7 |
-
|
| 8 |
-
:root {
|
| 9 |
-
/* --- Surface --- */
|
| 10 |
-
--bg: #06080f;
|
| 11 |
-
--surface: rgba(12, 17, 30, .72);
|
| 12 |
-
--surface2: rgba(16, 22, 38, .65);
|
| 13 |
-
--surface3: rgba(22, 30, 50, .60);
|
| 14 |
-
--glass: rgba(12, 17, 30, .55);
|
| 15 |
-
|
| 16 |
-
/* --- Border --- */
|
| 17 |
-
--border: rgba(255, 255, 255, .06);
|
| 18 |
-
--border2: rgba(255, 255, 255, .09);
|
| 19 |
-
--border3: rgba(255, 255, 255, .14);
|
| 20 |
-
|
| 21 |
-
/* --- Text --- */
|
| 22 |
-
--text: rgba(255, 255, 255, .92);
|
| 23 |
-
--text2: rgba(255, 255, 255, .58);
|
| 24 |
-
--text3: rgba(255, 255, 255, .34);
|
| 25 |
-
|
| 26 |
-
/* --- Semantic --- */
|
| 27 |
-
--success: #34d399;
|
| 28 |
-
--warning: #fbbf24;
|
| 29 |
-
--danger: #f87171;
|
| 30 |
-
|
| 31 |
-
/* --- Accent --- */
|
| 32 |
-
--accent: #3b82f6;
|
| 33 |
-
--accent2: #60a5fa;
|
| 34 |
-
--accent-glow: rgba(59, 130, 246, .15);
|
| 35 |
-
|
| 36 |
-
/* --- Effects --- */
|
| 37 |
-
--blur: blur(24px);
|
| 38 |
-
--blur-heavy: blur(40px);
|
| 39 |
-
--shadow-sm: 0 1px 3px rgba(0,0,0,.3), 0 1px 2px rgba(0,0,0,.2);
|
| 40 |
-
--shadow-md: 0 4px 20px rgba(0,0,0,.35);
|
| 41 |
-
--shadow-lg: 0 12px 48px rgba(0,0,0,.5);
|
| 42 |
-
--shadow-glow: 0 0 40px rgba(59, 130, 246, .08);
|
| 43 |
-
|
| 44 |
-
--radius-sm: 8px;
|
| 45 |
-
--radius-md: 12px;
|
| 46 |
-
--radius-lg: 16px;
|
| 47 |
-
--radius-xl: 20px;
|
| 48 |
-
|
| 49 |
-
/* --- Typography --- */
|
| 50 |
-
--font: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 51 |
-
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
| 52 |
-
--sans: var(--font);
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
/* ---- Reset ---- */
|
| 56 |
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 57 |
-
html, body { height: 100%; }
|
| 58 |
-
|
| 59 |
-
body {
|
| 60 |
-
background: var(--bg);
|
| 61 |
-
color: var(--text);
|
| 62 |
-
font-family: var(--font);
|
| 63 |
-
font-size: 13px;
|
| 64 |
-
line-height: 1.5;
|
| 65 |
-
overflow: hidden;
|
| 66 |
-
-webkit-font-smoothing: antialiased;
|
| 67 |
-
-moz-osx-font-smoothing: grayscale;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
/* =========================================
|
| 71 |
-
Ambient Background
|
| 72 |
-
========================================= */
|
| 73 |
-
|
| 74 |
-
#ambientBg {
|
| 75 |
-
position: fixed;
|
| 76 |
-
inset: 0;
|
| 77 |
-
z-index: 0;
|
| 78 |
-
pointer-events: none;
|
| 79 |
-
overflow: hidden;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
#ambientBg .orb {
|
| 83 |
-
position: absolute;
|
| 84 |
-
border-radius: 50%;
|
| 85 |
-
filter: blur(100px);
|
| 86 |
-
opacity: 0.4;
|
| 87 |
-
will-change: transform;
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
#ambientBg .orb-1 {
|
| 91 |
-
width: 600px; height: 600px;
|
| 92 |
-
background: radial-gradient(circle, rgba(59, 130, 246, .18), transparent 70%);
|
| 93 |
-
top: -10%; left: -5%;
|
| 94 |
-
animation: orbFloat1 25s ease-in-out infinite;
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
#ambientBg .orb-2 {
|
| 98 |
-
width: 500px; height: 500px;
|
| 99 |
-
background: radial-gradient(circle, rgba(99, 102, 241, .12), transparent 70%);
|
| 100 |
-
bottom: -15%; right: -5%;
|
| 101 |
-
animation: orbFloat2 30s ease-in-out infinite;
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
#ambientBg .orb-3 {
|
| 105 |
-
width: 400px; height: 400px;
|
| 106 |
-
background: radial-gradient(circle, rgba(14, 165, 233, .10), transparent 70%);
|
| 107 |
-
top: 40%; left: 50%;
|
| 108 |
-
animation: orbFloat3 20s ease-in-out infinite;
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
@keyframes orbFloat1 {
|
| 112 |
-
0%, 100% { transform: translate(0, 0) scale(1); }
|
| 113 |
-
33% { transform: translate(80px, 40px) scale(1.1); }
|
| 114 |
-
66% { transform: translate(-40px, 80px) scale(0.95); }
|
| 115 |
-
}
|
| 116 |
-
@keyframes orbFloat2 {
|
| 117 |
-
0%, 100% { transform: translate(0, 0) scale(1); }
|
| 118 |
-
33% { transform: translate(-60px, -30px) scale(1.05); }
|
| 119 |
-
66% { transform: translate(40px, -60px) scale(0.9); }
|
| 120 |
-
}
|
| 121 |
-
@keyframes orbFloat3 {
|
| 122 |
-
0%, 100% { transform: translate(0, 0) scale(1); }
|
| 123 |
-
50% { transform: translate(60px, -40px) scale(1.15); }
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
/* State-aware ambient: processing = pulse */
|
| 127 |
-
body.state-processing #ambientBg .orb-1 {
|
| 128 |
-
animation: orbPulse 3s ease-in-out infinite;
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
-
@keyframes orbPulse {
|
| 132 |
-
0%, 100% { opacity: 0.3; transform: scale(1); }
|
| 133 |
-
50% { opacity: 0.6; transform: scale(1.15); }
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
/* =========================================
|
| 137 |
-
Layout Shell
|
| 138 |
-
========================================= */
|
| 139 |
-
|
| 140 |
-
#app {
|
| 141 |
-
position: relative;
|
| 142 |
-
z-index: 1;
|
| 143 |
-
height: 100%;
|
| 144 |
-
display: flex;
|
| 145 |
-
flex-direction: column;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
/* ---- Header ---- */
|
| 149 |
-
header {
|
| 150 |
-
display: flex;
|
| 151 |
-
align-items: center;
|
| 152 |
-
justify-content: space-between;
|
| 153 |
-
padding: 0 20px;
|
| 154 |
-
height: 52px;
|
| 155 |
-
background: var(--glass);
|
| 156 |
-
backdrop-filter: var(--blur);
|
| 157 |
-
-webkit-backdrop-filter: var(--blur);
|
| 158 |
-
border-bottom: 1px solid var(--border);
|
| 159 |
-
flex-shrink: 0;
|
| 160 |
-
z-index: 100;
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
/* ---- Workspace ---- */
|
| 164 |
-
.workspace {
|
| 165 |
-
flex: 1;
|
| 166 |
-
display: grid;
|
| 167 |
-
grid-template-columns: 272px 1fr;
|
| 168 |
-
gap: 0;
|
| 169 |
-
min-height: 0;
|
| 170 |
-
transition: grid-template-columns 0.4s cubic-bezier(.4, 0, .2, 1);
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
/* ---- Sidebar ---- */
|
| 174 |
-
aside {
|
| 175 |
-
background: var(--glass);
|
| 176 |
-
backdrop-filter: var(--blur);
|
| 177 |
-
-webkit-backdrop-filter: var(--blur);
|
| 178 |
-
border-right: 1px solid var(--border);
|
| 179 |
-
overflow-y: auto;
|
| 180 |
-
overflow-x: hidden;
|
| 181 |
-
display: flex;
|
| 182 |
-
flex-direction: column;
|
| 183 |
-
min-height: 0;
|
| 184 |
-
z-index: 10;
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
/* ---- Main ---- */
|
| 188 |
-
main {
|
| 189 |
-
background: transparent;
|
| 190 |
-
overflow: hidden;
|
| 191 |
-
display: flex;
|
| 192 |
-
flex-direction: column;
|
| 193 |
-
min-height: 0;
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
/* ---- Footer / Stat Bar ---- */
|
| 197 |
-
footer {
|
| 198 |
-
display: flex;
|
| 199 |
-
align-items: center;
|
| 200 |
-
justify-content: space-between;
|
| 201 |
-
padding: 0 20px;
|
| 202 |
-
height: 40px;
|
| 203 |
-
background: var(--glass);
|
| 204 |
-
backdrop-filter: var(--blur);
|
| 205 |
-
-webkit-backdrop-filter: var(--blur);
|
| 206 |
-
border-top: 1px solid var(--border);
|
| 207 |
-
flex-shrink: 0;
|
| 208 |
-
z-index: 100;
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
footer .mono {
|
| 212 |
-
font-family: var(--mono);
|
| 213 |
-
font-size: 11px;
|
| 214 |
-
color: var(--text3);
|
| 215 |
-
letter-spacing: .02em;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
.footer-left {
|
| 219 |
-
font-size: 11px;
|
| 220 |
-
color: var(--text3);
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
/* ---- Stat Chips in footer ---- */
|
| 224 |
-
.stat-bar {
|
| 225 |
-
display: flex;
|
| 226 |
-
gap: 16px;
|
| 227 |
-
align-items: center;
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
.stat-item {
|
| 231 |
-
display: flex;
|
| 232 |
-
align-items: baseline;
|
| 233 |
-
gap: 6px;
|
| 234 |
-
font-size: 11px;
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
.stat-item .stat-label {
|
| 238 |
-
color: var(--text3);
|
| 239 |
-
font-weight: 500;
|
| 240 |
-
text-transform: uppercase;
|
| 241 |
-
letter-spacing: .05em;
|
| 242 |
-
font-size: 10px;
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
.stat-item .stat-value {
|
| 246 |
-
font-family: var(--mono);
|
| 247 |
-
font-size: 12px;
|
| 248 |
-
color: var(--text);
|
| 249 |
-
font-weight: 600;
|
| 250 |
-
font-variant-numeric: tabular-nums;
|
| 251 |
-
transition: color 0.3s ease;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
.stat-item .stat-value.updated {
|
| 255 |
-
color: var(--accent2);
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
/* =========================================
|
| 259 |
-
Brand / Header
|
| 260 |
-
========================================= */
|
| 261 |
-
|
| 262 |
-
.brand {
|
| 263 |
-
display: flex;
|
| 264 |
-
gap: 10px;
|
| 265 |
-
align-items: center;
|
| 266 |
-
min-width: 0;
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
.logo {
|
| 270 |
-
width: 28px;
|
| 271 |
-
height: 28px;
|
| 272 |
-
border-radius: 7px;
|
| 273 |
-
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
| 274 |
-
position: relative;
|
| 275 |
-
flex-shrink: 0;
|
| 276 |
-
box-shadow: 0 0 20px rgba(59, 130, 246, .2);
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
.logo::after {
|
| 280 |
-
content: "";
|
| 281 |
-
position: absolute;
|
| 282 |
-
inset: 6px;
|
| 283 |
-
border: 1.5px solid rgba(255,255,255,.8);
|
| 284 |
-
border-radius: 3px;
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
.brand h1 {
|
| 288 |
-
font-size: 14px;
|
| 289 |
-
font-weight: 600;
|
| 290 |
-
letter-spacing: -.01em;
|
| 291 |
-
color: var(--text);
|
| 292 |
-
}
|
| 293 |
-
|
| 294 |
-
.brand .sub {
|
| 295 |
-
font-size: 11px;
|
| 296 |
-
color: var(--text3);
|
| 297 |
-
margin-top: 0;
|
| 298 |
-
line-height: 1.2;
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
.status-row {
|
| 302 |
-
display: flex;
|
| 303 |
-
gap: 8px;
|
| 304 |
-
align-items: center;
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
/* =========================================
|
| 308 |
-
Glass Pill / Indicators
|
| 309 |
-
========================================= */
|
| 310 |
-
|
| 311 |
-
.pill {
|
| 312 |
-
display: flex;
|
| 313 |
-
align-items: center;
|
| 314 |
-
gap: 7px;
|
| 315 |
-
padding: 5px 12px;
|
| 316 |
-
border-radius: var(--radius-md);
|
| 317 |
-
border: 1px solid var(--border2);
|
| 318 |
-
background: var(--surface);
|
| 319 |
-
backdrop-filter: var(--blur);
|
| 320 |
-
font-size: 12px;
|
| 321 |
-
color: var(--text2);
|
| 322 |
-
white-space: nowrap;
|
| 323 |
-
transition: all 0.3s ease;
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
.dot {
|
| 327 |
-
width: 6px;
|
| 328 |
-
height: 6px;
|
| 329 |
-
border-radius: 50%;
|
| 330 |
-
background: var(--success);
|
| 331 |
-
box-shadow: 0 0 8px rgba(52, 211, 153, .5);
|
| 332 |
-
flex-shrink: 0;
|
| 333 |
-
transition: all 0.4s ease;
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
.dot.warn {
|
| 337 |
-
background: var(--warning);
|
| 338 |
-
box-shadow: 0 0 8px rgba(251, 191, 36, .5);
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
.dot.bad {
|
| 342 |
-
background: var(--danger);
|
| 343 |
-
box-shadow: 0 0 8px rgba(248, 113, 113, .5);
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
/* Processing pulse on dot */
|
| 347 |
-
.dot.processing {
|
| 348 |
-
animation: dotPulse 1.5s ease-in-out infinite;
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
@keyframes dotPulse {
|
| 352 |
-
0%, 100% { transform: scale(1); opacity: 1; }
|
| 353 |
-
50% { transform: scale(1.6); opacity: .6; }
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
.kbd {
|
| 357 |
-
font-family: var(--mono);
|
| 358 |
-
font-size: 10px;
|
| 359 |
-
font-weight: 600;
|
| 360 |
-
padding: 2px 6px;
|
| 361 |
-
border: 1px solid var(--border2);
|
| 362 |
-
background: rgba(255,255,255,.04);
|
| 363 |
-
border-radius: 4px;
|
| 364 |
-
color: var(--text2);
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
.badge {
|
| 368 |
-
display: inline-flex;
|
| 369 |
-
align-items: center;
|
| 370 |
-
gap: 5px;
|
| 371 |
-
padding: 3px 8px;
|
| 372 |
-
border-radius: var(--radius-sm);
|
| 373 |
-
border: 1px solid var(--border2);
|
| 374 |
-
background: var(--surface2);
|
| 375 |
-
font-family: var(--mono);
|
| 376 |
-
font-size: 11px;
|
| 377 |
-
color: var(--text2);
|
| 378 |
-
}
|
| 379 |
-
|
| 380 |
-
/* =========================================
|
| 381 |
-
Cards / Panels (Glass)
|
| 382 |
-
========================================= */
|
| 383 |
-
|
| 384 |
-
.card {
|
| 385 |
-
padding: 14px 16px;
|
| 386 |
-
border-bottom: 1px solid var(--border);
|
| 387 |
-
position: relative;
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
.card:last-child { border-bottom: none; }
|
| 391 |
-
|
| 392 |
-
.card h2 {
|
| 393 |
-
font-size: 10px;
|
| 394 |
-
font-weight: 600;
|
| 395 |
-
letter-spacing: .08em;
|
| 396 |
-
text-transform: uppercase;
|
| 397 |
-
color: var(--text3);
|
| 398 |
-
margin-bottom: 2px;
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
.card small { color: var(--text2); }
|
| 402 |
-
|
| 403 |
-
.card .hint {
|
| 404 |
-
color: var(--text3);
|
| 405 |
-
font-size: 11px;
|
| 406 |
-
line-height: 1.4;
|
| 407 |
-
margin-top: 5px;
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
.panel {
|
| 411 |
-
background: var(--glass);
|
| 412 |
-
backdrop-filter: var(--blur);
|
| 413 |
-
-webkit-backdrop-filter: var(--blur);
|
| 414 |
-
border: 1px solid var(--border);
|
| 415 |
-
border-radius: var(--radius-lg);
|
| 416 |
-
padding: 14px;
|
| 417 |
-
box-shadow: var(--shadow-md), var(--shadow-glow);
|
| 418 |
-
overflow: hidden;
|
| 419 |
-
position: relative;
|
| 420 |
-
transition: box-shadow 0.3s ease;
|
| 421 |
-
}
|
| 422 |
-
|
| 423 |
-
.panel:hover {
|
| 424 |
-
box-shadow: var(--shadow-lg), 0 0 60px rgba(59, 130, 246, .06);
|
| 425 |
-
}
|
| 426 |
-
|
| 427 |
-
.panel h3 {
|
| 428 |
-
margin: 0 0 10px;
|
| 429 |
-
font-size: 10px;
|
| 430 |
-
font-weight: 600;
|
| 431 |
-
letter-spacing: .08em;
|
| 432 |
-
text-transform: uppercase;
|
| 433 |
-
color: var(--text3);
|
| 434 |
-
display: flex;
|
| 435 |
-
align-items: center;
|
| 436 |
-
justify-content: space-between;
|
| 437 |
-
gap: 8px;
|
| 438 |
-
}
|
| 439 |
-
|
| 440 |
-
.panel h3 .rightnote {
|
| 441 |
-
font-size: 11px;
|
| 442 |
-
color: var(--text3);
|
| 443 |
-
font-family: var(--mono);
|
| 444 |
-
letter-spacing: 0;
|
| 445 |
-
text-transform: none;
|
| 446 |
-
font-weight: 400;
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
.collapse-btn {
|
| 450 |
-
background: rgba(255,255,255,.04);
|
| 451 |
-
border: 1px solid var(--border2);
|
| 452 |
-
border-radius: var(--radius-sm);
|
| 453 |
-
padding: 4px 10px;
|
| 454 |
-
color: var(--text3);
|
| 455 |
-
cursor: pointer;
|
| 456 |
-
font-size: 11px;
|
| 457 |
-
font-family: var(--font);
|
| 458 |
-
font-weight: 500;
|
| 459 |
-
transition: all 0.2s ease;
|
| 460 |
-
}
|
| 461 |
-
|
| 462 |
-
.collapse-btn:hover {
|
| 463 |
-
background: rgba(255,255,255,.08);
|
| 464 |
-
color: var(--text2);
|
| 465 |
-
border-color: var(--border3);
|
| 466 |
-
}
|
| 467 |
-
|
| 468 |
-
/* =========================================
|
| 469 |
-
Inputs & Controls
|
| 470 |
-
========================================= */
|
| 471 |
-
|
| 472 |
-
.grid2 {
|
| 473 |
-
display: grid;
|
| 474 |
-
grid-template-columns: 1fr 1fr;
|
| 475 |
-
gap: 8px;
|
| 476 |
-
margin-top: 10px;
|
| 477 |
-
}
|
| 478 |
-
|
| 479 |
-
.row {
|
| 480 |
-
display: flex;
|
| 481 |
-
gap: 8px;
|
| 482 |
-
align-items: center;
|
| 483 |
-
justify-content: space-between;
|
| 484 |
-
margin-top: 8px;
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
label {
|
| 488 |
-
font-size: 11px;
|
| 489 |
-
font-weight: 500;
|
| 490 |
-
color: var(--text3);
|
| 491 |
-
}
|
| 492 |
-
|
| 493 |
-
input[type="range"] { width: 100%; }
|
| 494 |
-
|
| 495 |
-
select,
|
| 496 |
-
textarea,
|
| 497 |
-
input[type="text"],
|
| 498 |
-
input[type="number"] {
|
| 499 |
-
width: 100%;
|
| 500 |
-
background: rgba(255,255,255,.03);
|
| 501 |
-
border: 1px solid var(--border2);
|
| 502 |
-
border-radius: var(--radius-sm);
|
| 503 |
-
padding: 8px 10px;
|
| 504 |
-
color: var(--text);
|
| 505 |
-
outline: none;
|
| 506 |
-
font-family: var(--font);
|
| 507 |
-
font-size: 13px;
|
| 508 |
-
transition: all 0.2s ease;
|
| 509 |
-
}
|
| 510 |
-
|
| 511 |
-
select:focus,
|
| 512 |
-
textarea:focus,
|
| 513 |
-
input[type="text"]:focus,
|
| 514 |
-
input[type="number"]:focus {
|
| 515 |
-
border-color: rgba(59, 130, 246, .5);
|
| 516 |
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, .1), 0 0 20px rgba(59, 130, 246, .05);
|
| 517 |
-
background: rgba(255,255,255,.05);
|
| 518 |
-
}
|
| 519 |
-
|
| 520 |
-
textarea {
|
| 521 |
-
resize: vertical;
|
| 522 |
-
line-height: 1.4;
|
| 523 |
-
}
|
| 524 |
-
|
| 525 |
-
/* ---- Buttons ---- */
|
| 526 |
-
.btn {
|
| 527 |
-
user-select: none;
|
| 528 |
-
cursor: pointer;
|
| 529 |
-
border: none;
|
| 530 |
-
border-radius: var(--radius-sm);
|
| 531 |
-
padding: 9px 16px;
|
| 532 |
-
font-weight: 600;
|
| 533 |
-
font-size: 12px;
|
| 534 |
-
font-family: var(--font);
|
| 535 |
-
letter-spacing: .01em;
|
| 536 |
-
color: #fff;
|
| 537 |
-
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
| 538 |
-
box-shadow: 0 2px 8px rgba(59, 130, 246, .25), inset 0 1px 0 rgba(255,255,255,.1);
|
| 539 |
-
transition: all 0.2s cubic-bezier(.4, 0, .2, 1);
|
| 540 |
-
position: relative;
|
| 541 |
-
overflow: hidden;
|
| 542 |
-
}
|
| 543 |
-
|
| 544 |
-
.btn::after {
|
| 545 |
-
content: "";
|
| 546 |
-
position: absolute;
|
| 547 |
-
inset: 0;
|
| 548 |
-
background: linear-gradient(180deg, rgba(255,255,255,.08), transparent);
|
| 549 |
-
pointer-events: none;
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
-
.btn:hover {
|
| 553 |
-
transform: translateY(-1px);
|
| 554 |
-
box-shadow: 0 4px 16px rgba(59, 130, 246, .35), inset 0 1px 0 rgba(255,255,255,.15);
|
| 555 |
-
}
|
| 556 |
-
|
| 557 |
-
.btn:active {
|
| 558 |
-
transform: translateY(0);
|
| 559 |
-
box-shadow: 0 1px 4px rgba(59, 130, 246, .2);
|
| 560 |
-
}
|
| 561 |
-
|
| 562 |
-
.btn.secondary {
|
| 563 |
-
background: rgba(255,255,255,.05);
|
| 564 |
-
border: 1px solid var(--border2);
|
| 565 |
-
box-shadow: none;
|
| 566 |
-
color: var(--text2);
|
| 567 |
-
font-weight: 500;
|
| 568 |
-
}
|
| 569 |
-
|
| 570 |
-
.btn.secondary::after { display: none; }
|
| 571 |
-
|
| 572 |
-
.btn.secondary:hover {
|
| 573 |
-
background: rgba(255,255,255,.08);
|
| 574 |
-
color: var(--text);
|
| 575 |
-
border-color: var(--border3);
|
| 576 |
-
transform: translateY(-1px);
|
| 577 |
-
box-shadow: var(--shadow-sm);
|
| 578 |
-
}
|
| 579 |
-
|
| 580 |
-
.btn.danger {
|
| 581 |
-
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
| 582 |
-
box-shadow: 0 2px 8px rgba(220, 38, 38, .25);
|
| 583 |
-
}
|
| 584 |
-
|
| 585 |
-
.btn.danger:hover {
|
| 586 |
-
box-shadow: 0 4px 16px rgba(220, 38, 38, .35);
|
| 587 |
-
}
|
| 588 |
-
|
| 589 |
-
.btnrow {
|
| 590 |
-
display: flex;
|
| 591 |
-
gap: 6px;
|
| 592 |
-
margin-top: 10px;
|
| 593 |
-
}
|
| 594 |
-
|
| 595 |
-
.btnrow .btn { flex: 1; }
|
| 596 |
-
|
| 597 |
-
/* =========================================
|
| 598 |
-
Tabs
|
| 599 |
-
========================================= */
|
| 600 |
-
|
| 601 |
-
.tabs {
|
| 602 |
-
display: flex;
|
| 603 |
-
gap: 2px;
|
| 604 |
-
padding: 8px 12px;
|
| 605 |
-
border-bottom: 1px solid var(--border);
|
| 606 |
-
background: var(--glass);
|
| 607 |
-
}
|
| 608 |
-
|
| 609 |
-
.tabbtn {
|
| 610 |
-
cursor: pointer;
|
| 611 |
-
border: none;
|
| 612 |
-
border-radius: var(--radius-sm);
|
| 613 |
-
padding: 6px 14px;
|
| 614 |
-
font-size: 12px;
|
| 615 |
-
font-family: var(--font);
|
| 616 |
-
font-weight: 500;
|
| 617 |
-
color: var(--text3);
|
| 618 |
-
background: transparent;
|
| 619 |
-
transition: all 0.2s ease;
|
| 620 |
-
}
|
| 621 |
-
|
| 622 |
-
.tabbtn:hover {
|
| 623 |
-
color: var(--text2);
|
| 624 |
-
background: rgba(255,255,255,.04);
|
| 625 |
-
}
|
| 626 |
-
|
| 627 |
-
.tabbtn.active {
|
| 628 |
-
color: var(--text);
|
| 629 |
-
background: rgba(255,255,255,.06);
|
| 630 |
-
}
|
| 631 |
-
|
| 632 |
-
.tab {
|
| 633 |
-
display: none;
|
| 634 |
-
flex: 1;
|
| 635 |
-
min-height: 0;
|
| 636 |
-
overflow: auto;
|
| 637 |
-
padding: 14px;
|
| 638 |
-
}
|
| 639 |
-
|
| 640 |
-
.tab.active { display: block; }
|
| 641 |
-
|
| 642 |
-
/* =========================================
|
| 643 |
-
Video Viewport (Cinematic)
|
| 644 |
-
========================================= */
|
| 645 |
-
|
| 646 |
-
.viewbox {
|
| 647 |
-
position: relative;
|
| 648 |
-
border-radius: var(--radius-lg);
|
| 649 |
-
overflow: hidden;
|
| 650 |
-
background: #000;
|
| 651 |
-
border: 1px solid var(--border);
|
| 652 |
-
min-height: 360px;
|
| 653 |
-
box-shadow: var(--shadow-lg), 0 0 80px rgba(0,0,0,.4);
|
| 654 |
-
transition: box-shadow 0.4s ease;
|
| 655 |
-
}
|
| 656 |
-
|
| 657 |
-
/* Ambient glow behind video during processing */
|
| 658 |
-
body.state-processing .viewbox {
|
| 659 |
-
box-shadow: var(--shadow-lg), 0 0 60px rgba(59, 130, 246, .12), 0 0 120px rgba(59, 130, 246, .06);
|
| 660 |
-
}
|
| 661 |
-
|
| 662 |
-
body.state-complete .viewbox {
|
| 663 |
-
box-shadow: var(--shadow-lg), 0 0 60px rgba(52, 211, 153, .08);
|
| 664 |
-
}
|
| 665 |
-
|
| 666 |
-
.viewbox canvas,
|
| 667 |
-
.viewbox video {
|
| 668 |
-
width: 100%;
|
| 669 |
-
height: 100%;
|
| 670 |
-
display: block;
|
| 671 |
-
object-fit: contain;
|
| 672 |
-
}
|
| 673 |
-
|
| 674 |
-
#videoEngage { display: block; opacity: 1; }
|
| 675 |
-
|
| 676 |
-
/* SVG overlay */
|
| 677 |
-
#engageOverlay {
|
| 678 |
-
position: absolute;
|
| 679 |
-
top: 0; left: 0;
|
| 680 |
-
width: 100%; height: 100%;
|
| 681 |
-
pointer-events: auto;
|
| 682 |
-
cursor: crosshair;
|
| 683 |
-
z-index: 5;
|
| 684 |
-
}
|
| 685 |
-
|
| 686 |
-
#engageOverlay .track-box {
|
| 687 |
-
cursor: pointer;
|
| 688 |
-
transition: opacity 0.15s ease;
|
| 689 |
-
}
|
| 690 |
-
|
| 691 |
-
#engageOverlay .track-box:hover rect:first-child {
|
| 692 |
-
fill: rgba(59, 130, 246, 0.08);
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
/* ---- Watermark ---- */
|
| 696 |
-
.viewbox .watermark {
|
| 697 |
-
position: absolute;
|
| 698 |
-
left: 10px; bottom: 10px;
|
| 699 |
-
font-family: var(--mono);
|
| 700 |
-
font-size: 10px;
|
| 701 |
-
color: rgba(255,255,255,.25);
|
| 702 |
-
background: rgba(0,0,0,.5);
|
| 703 |
-
backdrop-filter: blur(8px);
|
| 704 |
-
padding: 4px 10px;
|
| 705 |
-
border-radius: 6px;
|
| 706 |
-
letter-spacing: .04em;
|
| 707 |
-
border: 1px solid rgba(255,255,255,.06);
|
| 708 |
-
}
|
| 709 |
-
|
| 710 |
-
/* ---- Empty State ---- */
|
| 711 |
-
.viewbox .empty {
|
| 712 |
-
position: absolute;
|
| 713 |
-
inset: 0;
|
| 714 |
-
display: flex;
|
| 715 |
-
flex-direction: column;
|
| 716 |
-
align-items: center;
|
| 717 |
-
justify-content: center;
|
| 718 |
-
gap: 16px;
|
| 719 |
-
color: var(--text3);
|
| 720 |
-
text-align: center;
|
| 721 |
-
padding: 40px;
|
| 722 |
-
}
|
| 723 |
-
|
| 724 |
-
.viewbox .empty .big {
|
| 725 |
-
font-size: 16px;
|
| 726 |
-
font-weight: 500;
|
| 727 |
-
color: var(--text2);
|
| 728 |
-
}
|
| 729 |
-
|
| 730 |
-
.viewbox .empty .small {
|
| 731 |
-
color: var(--text3);
|
| 732 |
-
font-size: 13px;
|
| 733 |
-
max-width: 420px;
|
| 734 |
-
line-height: 1.6;
|
| 735 |
-
}
|
| 736 |
-
|
| 737 |
-
/* ---- Progress Ring (overlaid on video) ---- */
|
| 738 |
-
.progress-ring-wrap {
|
| 739 |
-
position: absolute;
|
| 740 |
-
top: 50%; left: 50%;
|
| 741 |
-
transform: translate(-50%, -50%);
|
| 742 |
-
z-index: 20;
|
| 743 |
-
display: none;
|
| 744 |
-
flex-direction: column;
|
| 745 |
-
align-items: center;
|
| 746 |
-
gap: 12px;
|
| 747 |
-
pointer-events: none;
|
| 748 |
-
}
|
| 749 |
-
|
| 750 |
-
.progress-ring-wrap.visible {
|
| 751 |
-
display: flex;
|
| 752 |
-
animation: fadeInScale 0.4s cubic-bezier(.4, 0, .2, 1);
|
| 753 |
-
}
|
| 754 |
-
|
| 755 |
-
@keyframes fadeInScale {
|
| 756 |
-
from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
|
| 757 |
-
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
| 758 |
-
}
|
| 759 |
-
|
| 760 |
-
.progress-ring-wrap svg {
|
| 761 |
-
width: 100px;
|
| 762 |
-
height: 100px;
|
| 763 |
-
transform: rotate(-90deg);
|
| 764 |
-
filter: drop-shadow(0 0 12px rgba(59, 130, 246, .3));
|
| 765 |
-
}
|
| 766 |
-
|
| 767 |
-
.progress-ring-bg {
|
| 768 |
-
fill: none;
|
| 769 |
-
stroke: rgba(255,255,255,.06);
|
| 770 |
-
stroke-width: 4;
|
| 771 |
-
}
|
| 772 |
-
|
| 773 |
-
.progress-ring-fg {
|
| 774 |
-
fill: none;
|
| 775 |
-
stroke: var(--accent);
|
| 776 |
-
stroke-width: 4;
|
| 777 |
-
stroke-linecap: round;
|
| 778 |
-
stroke-dasharray: 264;
|
| 779 |
-
stroke-dashoffset: 264;
|
| 780 |
-
transition: stroke-dashoffset 0.5s cubic-bezier(.4, 0, .2, 1);
|
| 781 |
-
}
|
| 782 |
-
|
| 783 |
-
.progress-ring-text {
|
| 784 |
-
font-family: var(--mono);
|
| 785 |
-
font-size: 13px;
|
| 786 |
-
font-weight: 600;
|
| 787 |
-
color: var(--text);
|
| 788 |
-
background: rgba(0,0,0,.5);
|
| 789 |
-
backdrop-filter: blur(8px);
|
| 790 |
-
padding: 4px 14px;
|
| 791 |
-
border-radius: 20px;
|
| 792 |
-
border: 1px solid var(--border2);
|
| 793 |
-
letter-spacing: .02em;
|
| 794 |
-
}
|
| 795 |
-
|
| 796 |
-
/* =========================================
|
| 797 |
-
Timeline
|
| 798 |
-
========================================= */
|
| 799 |
-
|
| 800 |
-
.timeline-wrap {
|
| 801 |
-
width: 100%;
|
| 802 |
-
margin-top: 8px;
|
| 803 |
-
border-radius: 4px;
|
| 804 |
-
overflow: hidden;
|
| 805 |
-
background: rgba(255,255,255,.03);
|
| 806 |
-
cursor: pointer;
|
| 807 |
-
position: relative;
|
| 808 |
-
}
|
| 809 |
-
|
| 810 |
-
.timeline-wrap canvas {
|
| 811 |
-
display: block;
|
| 812 |
-
width: 100%;
|
| 813 |
-
height: 14px;
|
| 814 |
-
}
|
| 815 |
-
|
| 816 |
-
/* =========================================
|
| 817 |
-
Track Cards (with animation)
|
| 818 |
-
========================================= */
|
| 819 |
-
|
| 820 |
-
.list {
|
| 821 |
-
display: flex;
|
| 822 |
-
flex-direction: column;
|
| 823 |
-
gap: 4px;
|
| 824 |
-
min-height: 160px;
|
| 825 |
-
max-height: none;
|
| 826 |
-
overflow-y: auto;
|
| 827 |
-
padding-right: 4px;
|
| 828 |
-
}
|
| 829 |
-
|
| 830 |
-
.obj {
|
| 831 |
-
padding: 10px 12px;
|
| 832 |
-
border-radius: var(--radius-sm);
|
| 833 |
-
border: 1px solid var(--border);
|
| 834 |
-
background: rgba(255,255,255,.02);
|
| 835 |
-
cursor: pointer;
|
| 836 |
-
transition: all 0.15s ease;
|
| 837 |
-
}
|
| 838 |
-
|
| 839 |
-
.obj:hover {
|
| 840 |
-
background: rgba(255,255,255,.05);
|
| 841 |
-
border-color: var(--border2);
|
| 842 |
-
}
|
| 843 |
-
|
| 844 |
-
.obj.active {
|
| 845 |
-
border-color: var(--accent);
|
| 846 |
-
box-shadow: inset 3px 0 0 var(--accent), 0 0 20px rgba(59,130,246,.06);
|
| 847 |
-
background: rgba(59, 130, 246, .04);
|
| 848 |
-
}
|
| 849 |
-
|
| 850 |
-
.obj .top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
| 851 |
-
.obj .id { font-family: var(--mono); font-size: 12px; color: var(--text); }
|
| 852 |
-
.obj .cls { font-size: 12px; color: var(--text2); }
|
| 853 |
-
.obj .meta { margin-top: 4px; display: flex; gap: 10px; flex-wrap: wrap; font-size: 11px; color: var(--text3); }
|
| 854 |
-
|
| 855 |
-
/* ---- Track Cards ---- */
|
| 856 |
-
.track-card {
|
| 857 |
-
background: rgba(255,255,255,.02);
|
| 858 |
-
border: 1px solid var(--border);
|
| 859 |
-
border-radius: var(--radius-sm);
|
| 860 |
-
padding: 10px 12px;
|
| 861 |
-
cursor: pointer;
|
| 862 |
-
transition: all 0.2s cubic-bezier(.4, 0, .2, 1);
|
| 863 |
-
/* Staggered entrance */
|
| 864 |
-
opacity: 0;
|
| 865 |
-
transform: translateY(8px);
|
| 866 |
-
animation: cardEnter 0.35s cubic-bezier(.4, 0, .2, 1) forwards;
|
| 867 |
-
}
|
| 868 |
-
|
| 869 |
-
@keyframes cardEnter {
|
| 870 |
-
to { opacity: 1; transform: translateY(0); }
|
| 871 |
-
}
|
| 872 |
-
|
| 873 |
-
/* Stagger delays applied via JS: style="animation-delay: Xms" */
|
| 874 |
-
|
| 875 |
-
.track-card:hover {
|
| 876 |
-
background: rgba(255,255,255,.05);
|
| 877 |
-
border-color: var(--border2);
|
| 878 |
-
transform: translateY(-1px);
|
| 879 |
-
box-shadow: var(--shadow-sm);
|
| 880 |
-
}
|
| 881 |
-
|
| 882 |
-
.track-card.active {
|
| 883 |
-
border-color: var(--accent);
|
| 884 |
-
background: rgba(59, 130, 246, .05);
|
| 885 |
-
box-shadow: inset 3px 0 0 var(--accent), 0 0 24px rgba(59, 130, 246, .06);
|
| 886 |
-
transform: translateY(0);
|
| 887 |
-
}
|
| 888 |
-
|
| 889 |
-
.track-card.inspected {
|
| 890 |
-
border-left-color: rgba(96, 165, 250, 0.8);
|
| 891 |
-
box-shadow: inset 3px 0 0 rgba(96, 165, 250, 0.8);
|
| 892 |
-
}
|
| 893 |
-
|
| 894 |
-
.track-card-header {
|
| 895 |
-
display: flex;
|
| 896 |
-
justify-content: space-between;
|
| 897 |
-
align-items: center;
|
| 898 |
-
font-weight: 600;
|
| 899 |
-
margin-bottom: 4px;
|
| 900 |
-
font-size: 13px;
|
| 901 |
-
color: var(--text);
|
| 902 |
-
}
|
| 903 |
-
|
| 904 |
-
.track-card-meta {
|
| 905 |
-
font-size: 11px;
|
| 906 |
-
font-family: var(--mono);
|
| 907 |
-
color: var(--text3);
|
| 908 |
-
margin-bottom: 6px;
|
| 909 |
-
letter-spacing: .02em;
|
| 910 |
-
}
|
| 911 |
-
|
| 912 |
-
.track-card-body {
|
| 913 |
-
font-size: 12px;
|
| 914 |
-
line-height: 1.45;
|
| 915 |
-
color: var(--text2);
|
| 916 |
-
background: rgba(0,0,0,.2);
|
| 917 |
-
padding: 8px 10px;
|
| 918 |
-
border-radius: var(--radius-sm);
|
| 919 |
-
border: 1px solid var(--border);
|
| 920 |
-
}
|
| 921 |
-
|
| 922 |
-
.badgemini {
|
| 923 |
-
font-size: 10px;
|
| 924 |
-
font-weight: 600;
|
| 925 |
-
padding: 2px 7px;
|
| 926 |
-
border-radius: 4px;
|
| 927 |
-
letter-spacing: .02em;
|
| 928 |
-
line-height: 1;
|
| 929 |
-
}
|
| 930 |
-
|
| 931 |
-
.track-card-features {
|
| 932 |
-
margin-top: 8px;
|
| 933 |
-
border-top: 1px solid var(--border);
|
| 934 |
-
padding-top: 8px;
|
| 935 |
-
display: grid;
|
| 936 |
-
grid-template-columns: 1fr 1fr;
|
| 937 |
-
gap: 3px 12px;
|
| 938 |
-
}
|
| 939 |
-
|
| 940 |
-
.track-card-features .feat-row {
|
| 941 |
-
display: flex;
|
| 942 |
-
justify-content: space-between;
|
| 943 |
-
align-items: baseline;
|
| 944 |
-
font-size: 11px;
|
| 945 |
-
padding: 2px 0;
|
| 946 |
-
}
|
| 947 |
-
|
| 948 |
-
.track-card-features .feat-key {
|
| 949 |
-
font-family: var(--mono);
|
| 950 |
-
color: var(--text3);
|
| 951 |
-
white-space: nowrap;
|
| 952 |
-
margin-right: 6px;
|
| 953 |
-
}
|
| 954 |
-
|
| 955 |
-
.track-card-features .feat-val {
|
| 956 |
-
color: var(--text);
|
| 957 |
-
text-align: right;
|
| 958 |
-
font-size: 11px;
|
| 959 |
-
font-variant-numeric: tabular-nums;
|
| 960 |
-
}
|
| 961 |
-
|
| 962 |
-
.gpt-badge {
|
| 963 |
-
color: var(--accent2);
|
| 964 |
-
font-size: 10px;
|
| 965 |
-
border: 1px solid rgba(59,130,246,.3);
|
| 966 |
-
border-radius: 3px;
|
| 967 |
-
padding: 1px 4px;
|
| 968 |
-
margin-left: 6px;
|
| 969 |
-
}
|
| 970 |
-
|
| 971 |
-
.gpt-text { color: var(--text2); }
|
| 972 |
-
|
| 973 |
-
/* =========================================
|
| 974 |
-
Tables
|
| 975 |
-
========================================= */
|
| 976 |
-
|
| 977 |
-
.table {
|
| 978 |
-
width: 100%;
|
| 979 |
-
border-collapse: separate;
|
| 980 |
-
border-spacing: 0;
|
| 981 |
-
border-radius: var(--radius-md);
|
| 982 |
-
border: 1px solid var(--border);
|
| 983 |
-
overflow: hidden;
|
| 984 |
-
}
|
| 985 |
-
|
| 986 |
-
.table th, .table td {
|
| 987 |
-
padding: 8px 10px;
|
| 988 |
-
font-size: 12px;
|
| 989 |
-
border-bottom: 1px solid var(--border);
|
| 990 |
-
vertical-align: top;
|
| 991 |
-
}
|
| 992 |
-
|
| 993 |
-
.table th {
|
| 994 |
-
background: rgba(255,255,255,.03);
|
| 995 |
-
color: var(--text3);
|
| 996 |
-
letter-spacing: .04em;
|
| 997 |
-
text-transform: uppercase;
|
| 998 |
-
font-size: 10px;
|
| 999 |
-
font-weight: 600;
|
| 1000 |
-
}
|
| 1001 |
-
|
| 1002 |
-
.table tr:last-child td { border-bottom: none; }
|
| 1003 |
-
|
| 1004 |
-
.k { font-family: var(--mono); color: var(--text); }
|
| 1005 |
-
.mini { font-size: 11px; color: var(--text3); line-height: 1.4; }
|
| 1006 |
-
|
| 1007 |
-
/* =========================================
|
| 1008 |
-
Metrics & Log
|
| 1009 |
-
========================================= */
|
| 1010 |
-
|
| 1011 |
-
.metricgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
| 1012 |
-
|
| 1013 |
-
.metric {
|
| 1014 |
-
border: 1px solid var(--border);
|
| 1015 |
-
background: rgba(255,255,255,.02);
|
| 1016 |
-
border-radius: var(--radius-md);
|
| 1017 |
-
padding: 12px;
|
| 1018 |
-
}
|
| 1019 |
-
|
| 1020 |
-
.metric .label {
|
| 1021 |
-
font-size: 10px;
|
| 1022 |
-
color: var(--text3);
|
| 1023 |
-
font-weight: 600;
|
| 1024 |
-
letter-spacing: .05em;
|
| 1025 |
-
text-transform: uppercase;
|
| 1026 |
-
}
|
| 1027 |
-
|
| 1028 |
-
.metric .value {
|
| 1029 |
-
margin-top: 4px;
|
| 1030 |
-
font-family: var(--mono);
|
| 1031 |
-
font-size: 20px;
|
| 1032 |
-
font-weight: 600;
|
| 1033 |
-
color: var(--text);
|
| 1034 |
-
font-variant-numeric: tabular-nums;
|
| 1035 |
-
}
|
| 1036 |
-
|
| 1037 |
-
.metric .sub {
|
| 1038 |
-
margin-top: 4px;
|
| 1039 |
-
font-size: 11px;
|
| 1040 |
-
color: var(--text3);
|
| 1041 |
-
}
|
| 1042 |
-
|
| 1043 |
-
.log {
|
| 1044 |
-
font-family: var(--mono);
|
| 1045 |
-
font-size: 11px;
|
| 1046 |
-
color: var(--text2);
|
| 1047 |
-
line-height: 1.5;
|
| 1048 |
-
background: rgba(0,0,0,.3);
|
| 1049 |
-
border: 1px solid var(--border);
|
| 1050 |
-
border-radius: var(--radius-sm);
|
| 1051 |
-
padding: 10px;
|
| 1052 |
-
height: 210px;
|
| 1053 |
-
overflow: auto;
|
| 1054 |
-
white-space: pre-wrap;
|
| 1055 |
-
}
|
| 1056 |
-
|
| 1057 |
-
.log .t { color: var(--accent2); }
|
| 1058 |
-
.log .w { color: var(--warning); }
|
| 1059 |
-
.log .e { color: var(--danger); }
|
| 1060 |
-
.log .g { color: var(--success); }
|
| 1061 |
-
|
| 1062 |
-
/* =========================================
|
| 1063 |
-
Engage Grid
|
| 1064 |
-
========================================= */
|
| 1065 |
-
|
| 1066 |
-
.engage-grid {
|
| 1067 |
-
display: grid;
|
| 1068 |
-
grid-template-columns: 1.6fr .9fr;
|
| 1069 |
-
gap: 12px;
|
| 1070 |
-
min-height: 0;
|
| 1071 |
-
transition: grid-template-columns 0.4s cubic-bezier(.4, 0, .2, 1);
|
| 1072 |
-
}
|
| 1073 |
-
|
| 1074 |
-
.engage-grid.sidebar-collapsed {
|
| 1075 |
-
grid-template-columns: 1fr 0fr;
|
| 1076 |
-
}
|
| 1077 |
-
|
| 1078 |
-
.engage-grid.sidebar-collapsed .engage-right { display: none; }
|
| 1079 |
-
|
| 1080 |
-
.engage-right {
|
| 1081 |
-
display: flex;
|
| 1082 |
-
flex-direction: column;
|
| 1083 |
-
gap: 12px;
|
| 1084 |
-
min-height: 0;
|
| 1085 |
-
}
|
| 1086 |
-
|
| 1087 |
-
/* =========================================
|
| 1088 |
-
Chips / Strip
|
| 1089 |
-
========================================= */
|
| 1090 |
-
|
| 1091 |
-
.strip {
|
| 1092 |
-
display: flex;
|
| 1093 |
-
gap: 6px;
|
| 1094 |
-
flex-wrap: wrap;
|
| 1095 |
-
align-items: center;
|
| 1096 |
-
}
|
| 1097 |
-
|
| 1098 |
-
.strip .chip {
|
| 1099 |
-
padding: 4px 10px;
|
| 1100 |
-
border-radius: 6px;
|
| 1101 |
-
border: 1px solid var(--border);
|
| 1102 |
-
background: rgba(255,255,255,.03);
|
| 1103 |
-
font-family: var(--mono);
|
| 1104 |
-
font-size: 11px;
|
| 1105 |
-
color: var(--text2);
|
| 1106 |
-
transition: all 0.2s ease;
|
| 1107 |
-
cursor: default;
|
| 1108 |
-
}
|
| 1109 |
-
|
| 1110 |
-
.strip .chip:hover {
|
| 1111 |
-
background: rgba(255,255,255,.06);
|
| 1112 |
-
border-color: var(--border2);
|
| 1113 |
-
}
|
| 1114 |
-
|
| 1115 |
-
/* =========================================
|
| 1116 |
-
Sidebar Checkbox
|
| 1117 |
-
========================================= */
|
| 1118 |
-
|
| 1119 |
-
.checkbox-row {
|
| 1120 |
-
grid-column: span 2;
|
| 1121 |
-
margin-top: 8px;
|
| 1122 |
-
border-top: 1px solid var(--border);
|
| 1123 |
-
padding-top: 8px;
|
| 1124 |
-
display: flex;
|
| 1125 |
-
align-items: center;
|
| 1126 |
-
gap: 8px;
|
| 1127 |
-
cursor: pointer;
|
| 1128 |
-
font-size: 12px;
|
| 1129 |
-
}
|
| 1130 |
-
|
| 1131 |
-
.checkbox-row input[type="checkbox"] {
|
| 1132 |
-
width: auto;
|
| 1133 |
-
margin: 0;
|
| 1134 |
-
accent-color: var(--accent);
|
| 1135 |
-
}
|
| 1136 |
-
|
| 1137 |
-
/* =========================================
|
| 1138 |
-
Progress Bar
|
| 1139 |
-
========================================= */
|
| 1140 |
-
|
| 1141 |
-
.bar {
|
| 1142 |
-
height: 4px;
|
| 1143 |
-
border-radius: 2px;
|
| 1144 |
-
background: rgba(255,255,255,.04);
|
| 1145 |
-
overflow: hidden;
|
| 1146 |
-
}
|
| 1147 |
-
|
| 1148 |
-
.bar > div {
|
| 1149 |
-
height: 100%;
|
| 1150 |
-
width: 0%;
|
| 1151 |
-
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
| 1152 |
-
transition: width .3s ease;
|
| 1153 |
-
border-radius: 2px;
|
| 1154 |
-
}
|
| 1155 |
-
|
| 1156 |
-
/* =========================================
|
| 1157 |
-
Frame / Intel Grid
|
| 1158 |
-
========================================= */
|
| 1159 |
-
|
| 1160 |
-
.frame-grid {
|
| 1161 |
-
display: grid;
|
| 1162 |
-
grid-template-columns: 1.6fr 0.9fr;
|
| 1163 |
-
grid-template-rows: auto auto;
|
| 1164 |
-
gap: 12px;
|
| 1165 |
-
min-height: 0;
|
| 1166 |
-
}
|
| 1167 |
-
|
| 1168 |
-
.frame-grid .panel-monitor { grid-column: 1; grid-row: 1 / 3; }
|
| 1169 |
-
.frame-grid .panel-summary { grid-column: 2; grid-row: 1 / 3; }
|
| 1170 |
-
|
| 1171 |
-
.intel { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
| 1172 |
-
.intel-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
| 1173 |
-
|
| 1174 |
-
.thumbrow { display: flex; gap: 8px; }
|
| 1175 |
-
.thumbrow img {
|
| 1176 |
-
flex: 1;
|
| 1177 |
-
height: 86px;
|
| 1178 |
-
object-fit: cover;
|
| 1179 |
-
border-radius: var(--radius-sm);
|
| 1180 |
-
border: 1px solid var(--border);
|
| 1181 |
-
background: rgba(0,0,0,.2);
|
| 1182 |
-
}
|
| 1183 |
-
|
| 1184 |
-
.intelbox {
|
| 1185 |
-
font-size: 12px;
|
| 1186 |
-
line-height: 1.45;
|
| 1187 |
-
color: var(--text2);
|
| 1188 |
-
background: rgba(0,0,0,.2);
|
| 1189 |
-
border: 1px solid var(--border);
|
| 1190 |
-
border-radius: var(--radius-sm);
|
| 1191 |
-
padding: 10px;
|
| 1192 |
-
min-height: 72px;
|
| 1193 |
-
}
|
| 1194 |
-
|
| 1195 |
-
/* =========================================
|
| 1196 |
-
Radar / Trade Space
|
| 1197 |
-
========================================= */
|
| 1198 |
-
|
| 1199 |
-
.radar { height: 540px; display: flex; flex-direction: column; }
|
| 1200 |
-
.radar canvas { flex: 1; width: 100%; height: 100%; display: block; }
|
| 1201 |
-
|
| 1202 |
-
.trade-grid {
|
| 1203 |
-
display: grid;
|
| 1204 |
-
grid-template-columns: 1.35fr .65fr;
|
| 1205 |
-
gap: 12px;
|
| 1206 |
-
min-height: 0;
|
| 1207 |
-
}
|
| 1208 |
-
|
| 1209 |
-
.plot { height: 420px; }
|
| 1210 |
-
|
| 1211 |
-
/* =========================================
|
| 1212 |
-
Chat Panel (Glass)
|
| 1213 |
-
========================================= */
|
| 1214 |
-
|
| 1215 |
-
.panel-chat {
|
| 1216 |
-
grid-column: 2;
|
| 1217 |
-
grid-row: 2;
|
| 1218 |
-
max-height: 480px;
|
| 1219 |
-
min-height: 320px;
|
| 1220 |
-
display: flex;
|
| 1221 |
-
flex-direction: column;
|
| 1222 |
-
}
|
| 1223 |
-
|
| 1224 |
-
.panel-chat.collapsed .chat-container { display: none; }
|
| 1225 |
-
.panel-chat.collapsed { max-height: 44px; min-height: 0; }
|
| 1226 |
-
|
| 1227 |
-
.chat-container {
|
| 1228 |
-
display: flex;
|
| 1229 |
-
flex-direction: column;
|
| 1230 |
-
flex: 1;
|
| 1231 |
-
min-height: 0;
|
| 1232 |
-
gap: 8px;
|
| 1233 |
-
}
|
| 1234 |
-
|
| 1235 |
-
.chat-messages {
|
| 1236 |
-
flex: 1;
|
| 1237 |
-
overflow-y: auto;
|
| 1238 |
-
background: rgba(0,0,0,.2);
|
| 1239 |
-
border: 1px solid var(--border);
|
| 1240 |
-
border-radius: var(--radius-sm);
|
| 1241 |
-
padding: 10px;
|
| 1242 |
-
display: flex;
|
| 1243 |
-
flex-direction: column;
|
| 1244 |
-
gap: 6px;
|
| 1245 |
-
min-height: 100px;
|
| 1246 |
-
}
|
| 1247 |
-
|
| 1248 |
-
.chat-message {
|
| 1249 |
-
display: flex;
|
| 1250 |
-
gap: 8px;
|
| 1251 |
-
padding: 8px 12px;
|
| 1252 |
-
border-radius: var(--radius-sm);
|
| 1253 |
-
font-size: 13px;
|
| 1254 |
-
line-height: 1.45;
|
| 1255 |
-
animation: msgEnter 0.25s cubic-bezier(.4, 0, .2, 1);
|
| 1256 |
-
}
|
| 1257 |
-
|
| 1258 |
-
@keyframes msgEnter {
|
| 1259 |
-
from { opacity: 0; transform: translateY(6px); }
|
| 1260 |
-
to { opacity: 1; transform: translateY(0); }
|
| 1261 |
-
}
|
| 1262 |
-
|
| 1263 |
-
.chat-message.chat-user {
|
| 1264 |
-
background: rgba(59, 130, 246, .08);
|
| 1265 |
-
border: 1px solid rgba(59, 130, 246, .15);
|
| 1266 |
-
margin-left: 16px;
|
| 1267 |
-
}
|
| 1268 |
-
|
| 1269 |
-
.chat-message.chat-assistant {
|
| 1270 |
-
background: rgba(255,255,255,.03);
|
| 1271 |
-
border: 1px solid var(--border);
|
| 1272 |
-
margin-right: 16px;
|
| 1273 |
-
}
|
| 1274 |
-
|
| 1275 |
-
.chat-message.chat-system {
|
| 1276 |
-
background: rgba(251, 191, 36, .06);
|
| 1277 |
-
border: 1px solid rgba(251, 191, 36, .12);
|
| 1278 |
-
color: var(--warning);
|
| 1279 |
-
font-size: 12px;
|
| 1280 |
-
}
|
| 1281 |
-
|
| 1282 |
-
.chat-message.loading .chat-content::after {
|
| 1283 |
-
content: "";
|
| 1284 |
-
display: inline-block;
|
| 1285 |
-
width: 12px; height: 12px;
|
| 1286 |
-
border: 2px solid rgba(255,255,255,.1);
|
| 1287 |
-
border-top-color: var(--accent);
|
| 1288 |
-
border-radius: 50%;
|
| 1289 |
-
animation: spin 0.7s linear infinite;
|
| 1290 |
-
margin-left: 8px;
|
| 1291 |
-
vertical-align: middle;
|
| 1292 |
-
}
|
| 1293 |
-
|
| 1294 |
-
@keyframes spin { to { transform: rotate(360deg); } }
|
| 1295 |
-
|
| 1296 |
-
.chat-icon {
|
| 1297 |
-
flex-shrink: 0;
|
| 1298 |
-
font-size: 11px;
|
| 1299 |
-
font-weight: 600;
|
| 1300 |
-
color: var(--text3);
|
| 1301 |
-
}
|
| 1302 |
-
|
| 1303 |
-
.chat-content {
|
| 1304 |
-
flex: 1;
|
| 1305 |
-
color: var(--text);
|
| 1306 |
-
word-break: break-word;
|
| 1307 |
-
}
|
| 1308 |
-
|
| 1309 |
-
.chat-input-row {
|
| 1310 |
-
display: flex;
|
| 1311 |
-
gap: 6px;
|
| 1312 |
-
}
|
| 1313 |
-
|
| 1314 |
-
.chat-input-row input {
|
| 1315 |
-
flex: 1;
|
| 1316 |
-
background: rgba(255,255,255,.03);
|
| 1317 |
-
border: 1px solid var(--border2);
|
| 1318 |
-
border-radius: var(--radius-sm);
|
| 1319 |
-
padding: 10px 12px;
|
| 1320 |
-
color: var(--text);
|
| 1321 |
-
font-family: var(--font);
|
| 1322 |
-
font-size: 13px;
|
| 1323 |
-
outline: none;
|
| 1324 |
-
transition: all 0.2s ease;
|
| 1325 |
-
}
|
| 1326 |
-
|
| 1327 |
-
.chat-input-row input:focus {
|
| 1328 |
-
border-color: rgba(59, 130, 246, .5);
|
| 1329 |
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, .1);
|
| 1330 |
-
background: rgba(255,255,255,.05);
|
| 1331 |
-
}
|
| 1332 |
-
|
| 1333 |
-
.chat-input-row input:disabled {
|
| 1334 |
-
opacity: 0.3;
|
| 1335 |
-
cursor: not-allowed;
|
| 1336 |
-
}
|
| 1337 |
-
|
| 1338 |
-
.chat-input-row .btn {
|
| 1339 |
-
padding: 10px 18px;
|
| 1340 |
-
min-width: 70px;
|
| 1341 |
-
}
|
| 1342 |
-
|
| 1343 |
-
/* Chat Context Header */
|
| 1344 |
-
.chat-context-header {
|
| 1345 |
-
display: flex;
|
| 1346 |
-
align-items: center;
|
| 1347 |
-
gap: 6px;
|
| 1348 |
-
padding: 6px 12px;
|
| 1349 |
-
background: rgba(0,0,0,.25);
|
| 1350 |
-
border-bottom: 1px solid var(--border);
|
| 1351 |
-
flex-wrap: wrap;
|
| 1352 |
-
flex-shrink: 0;
|
| 1353 |
-
}
|
| 1354 |
-
|
| 1355 |
-
.chat-context-label {
|
| 1356 |
-
color: var(--text3);
|
| 1357 |
-
font-size: 10px;
|
| 1358 |
-
text-transform: uppercase;
|
| 1359 |
-
letter-spacing: 0.5px;
|
| 1360 |
-
flex-shrink: 0;
|
| 1361 |
-
}
|
| 1362 |
-
|
| 1363 |
-
.chat-context-chips {
|
| 1364 |
-
display: flex;
|
| 1365 |
-
flex-wrap: wrap;
|
| 1366 |
-
gap: 4px;
|
| 1367 |
-
}
|
| 1368 |
-
|
| 1369 |
-
.chat-context-chip {
|
| 1370 |
-
display: inline-flex;
|
| 1371 |
-
align-items: center;
|
| 1372 |
-
gap: 4px;
|
| 1373 |
-
background: rgba(255,255,255,.04);
|
| 1374 |
-
border: 1px solid var(--border2);
|
| 1375 |
-
color: var(--text);
|
| 1376 |
-
padding: 2px 8px;
|
| 1377 |
-
border-radius: 4px;
|
| 1378 |
-
font-size: 11px;
|
| 1379 |
-
font-family: var(--mono);
|
| 1380 |
-
animation: msgEnter 0.2s ease;
|
| 1381 |
-
}
|
| 1382 |
-
|
| 1383 |
-
.chat-context-chip .chip-label {
|
| 1384 |
-
color: var(--text2);
|
| 1385 |
-
}
|
| 1386 |
-
|
| 1387 |
-
.chat-context-chip .chip-status {
|
| 1388 |
-
font-size: 9px;
|
| 1389 |
-
line-height: 1;
|
| 1390 |
-
}
|
| 1391 |
-
|
| 1392 |
-
.chat-context-chip .chip-status.match { color: #34d399; }
|
| 1393 |
-
.chat-context-chip .chip-status.no-match { color: #f87171; }
|
| 1394 |
-
.chat-context-chip .chip-status.unassessed { color: var(--text3); }
|
| 1395 |
-
|
| 1396 |
-
/* Chat Suggestions */
|
| 1397 |
-
.chat-suggestions {
|
| 1398 |
-
display: flex;
|
| 1399 |
-
gap: 5px;
|
| 1400 |
-
padding: 6px 12px;
|
| 1401 |
-
border-bottom: 1px solid var(--border);
|
| 1402 |
-
flex-wrap: wrap;
|
| 1403 |
-
flex-shrink: 0;
|
| 1404 |
-
overflow-x: auto;
|
| 1405 |
-
}
|
| 1406 |
-
|
| 1407 |
-
.chat-suggestion-chip {
|
| 1408 |
-
background: rgba(255,255,255,.03);
|
| 1409 |
-
border: 1px solid var(--border2);
|
| 1410 |
-
color: var(--text2);
|
| 1411 |
-
padding: 3px 10px;
|
| 1412 |
-
border-radius: 12px;
|
| 1413 |
-
font-size: 10px;
|
| 1414 |
-
cursor: pointer;
|
| 1415 |
-
white-space: nowrap;
|
| 1416 |
-
transition: all 0.15s ease;
|
| 1417 |
-
animation: msgEnter 0.2s ease;
|
| 1418 |
-
}
|
| 1419 |
-
|
| 1420 |
-
.chat-suggestion-chip:hover {
|
| 1421 |
-
background: rgba(59, 130, 246, .1);
|
| 1422 |
-
border-color: rgba(59, 130, 246, .3);
|
| 1423 |
-
color: var(--text);
|
| 1424 |
-
}
|
| 1425 |
-
|
| 1426 |
-
/* Chat context marker (join/leave) */
|
| 1427 |
-
.chat-message.chat-marker {
|
| 1428 |
-
background: none;
|
| 1429 |
-
border: none;
|
| 1430 |
-
justify-content: center;
|
| 1431 |
-
color: var(--text3);
|
| 1432 |
-
font-size: 11px;
|
| 1433 |
-
padding: 4px 0;
|
| 1434 |
-
animation: msgEnter 0.2s ease;
|
| 1435 |
-
}
|
| 1436 |
-
|
| 1437 |
-
/* =========================================
|
| 1438 |
-
Inspection Panel — Quad View
|
| 1439 |
-
========================================= */
|
| 1440 |
-
|
| 1441 |
-
.panel-inspection {
|
| 1442 |
-
grid-column: 1;
|
| 1443 |
-
grid-row: 2;
|
| 1444 |
-
max-height: 520px;
|
| 1445 |
-
min-height: 360px;
|
| 1446 |
-
display: flex;
|
| 1447 |
-
flex-direction: column;
|
| 1448 |
-
animation: panelSlideUp 0.3s cubic-bezier(.4, 0, .2, 1);
|
| 1449 |
-
}
|
| 1450 |
-
|
| 1451 |
-
@keyframes panelSlideUp {
|
| 1452 |
-
from { opacity: 0; transform: translateY(12px); }
|
| 1453 |
-
to { opacity: 1; transform: translateY(0); }
|
| 1454 |
-
}
|
| 1455 |
-
|
| 1456 |
-
/* --- Header --- */
|
| 1457 |
-
|
| 1458 |
-
.inspection-header {
|
| 1459 |
-
display: flex;
|
| 1460 |
-
justify-content: space-between;
|
| 1461 |
-
align-items: center;
|
| 1462 |
-
padding: 10px 12px;
|
| 1463 |
-
border-bottom: 1px solid rgba(255,255,255,0.04);
|
| 1464 |
-
}
|
| 1465 |
-
|
| 1466 |
-
.inspection-header-left {
|
| 1467 |
-
display: flex;
|
| 1468 |
-
align-items: center;
|
| 1469 |
-
gap: 12px;
|
| 1470 |
-
}
|
| 1471 |
-
|
| 1472 |
-
.inspection-obj-name {
|
| 1473 |
-
font-size: 14px;
|
| 1474 |
-
font-weight: 700;
|
| 1475 |
-
color: #e2e8f0;
|
| 1476 |
-
letter-spacing: 0.5px;
|
| 1477 |
-
font-family: var(--mono);
|
| 1478 |
-
}
|
| 1479 |
-
|
| 1480 |
-
.inspection-conf {
|
| 1481 |
-
font-size: 10px;
|
| 1482 |
-
color: #64748b;
|
| 1483 |
-
font-family: var(--mono);
|
| 1484 |
-
}
|
| 1485 |
-
|
| 1486 |
-
.inspection-status {
|
| 1487 |
-
font-size: 8px;
|
| 1488 |
-
padding: 2px 8px;
|
| 1489 |
-
border-radius: 2px;
|
| 1490 |
-
letter-spacing: 0.5px;
|
| 1491 |
-
font-family: var(--mono);
|
| 1492 |
-
font-weight: 600;
|
| 1493 |
-
}
|
| 1494 |
-
.inspection-status.match {
|
| 1495 |
-
background: rgba(34,197,94,0.12);
|
| 1496 |
-
border: 1px solid rgba(34,197,94,0.2);
|
| 1497 |
-
color: #22c55e;
|
| 1498 |
-
}
|
| 1499 |
-
.inspection-status.no-match {
|
| 1500 |
-
background: rgba(239,68,68,0.12);
|
| 1501 |
-
border: 1px solid rgba(239,68,68,0.2);
|
| 1502 |
-
color: #ef4444;
|
| 1503 |
-
}
|
| 1504 |
-
.inspection-status.pending {
|
| 1505 |
-
background: rgba(255,255,255,0.05);
|
| 1506 |
-
border: 1px solid rgba(255,255,255,0.08);
|
| 1507 |
-
color: #64748b;
|
| 1508 |
-
}
|
| 1509 |
-
|
| 1510 |
-
.inspection-header-right {
|
| 1511 |
-
display: flex;
|
| 1512 |
-
align-items: center;
|
| 1513 |
-
gap: 10px;
|
| 1514 |
-
}
|
| 1515 |
-
|
| 1516 |
-
.inspection-frame {
|
| 1517 |
-
font-size: 9px;
|
| 1518 |
-
color: #64748b;
|
| 1519 |
-
letter-spacing: 1px;
|
| 1520 |
-
font-family: var(--mono);
|
| 1521 |
-
}
|
| 1522 |
-
|
| 1523 |
-
/* --- Quad Grid --- */
|
| 1524 |
-
|
| 1525 |
-
.inspection-quad {
|
| 1526 |
-
display: grid;
|
| 1527 |
-
grid-template-columns: 1fr 1fr;
|
| 1528 |
-
grid-template-rows: 1fr 1fr;
|
| 1529 |
-
gap: 8px;
|
| 1530 |
-
flex: 1;
|
| 1531 |
-
min-height: 300px;
|
| 1532 |
-
position: relative;
|
| 1533 |
-
padding: 8px 12px;
|
| 1534 |
-
}
|
| 1535 |
-
|
| 1536 |
-
.inspection-quadrant {
|
| 1537 |
-
background: rgba(0,0,0,0.5);
|
| 1538 |
-
border: 1px solid rgba(74,158,255,0.1);
|
| 1539 |
-
border-radius: 5px;
|
| 1540 |
-
position: relative;
|
| 1541 |
-
overflow: hidden;
|
| 1542 |
-
cursor: pointer;
|
| 1543 |
-
transition: all 0.25s ease;
|
| 1544 |
-
}
|
| 1545 |
-
|
| 1546 |
-
.inspection-quadrant:hover {
|
| 1547 |
-
border-color: rgba(74,158,255,0.2);
|
| 1548 |
-
}
|
| 1549 |
-
|
| 1550 |
-
.inspection-quadrant.expanded {
|
| 1551 |
-
position: absolute;
|
| 1552 |
-
inset: 8px;
|
| 1553 |
-
z-index: 10;
|
| 1554 |
-
border-radius: 5px;
|
| 1555 |
-
}
|
| 1556 |
-
|
| 1557 |
-
/* --- Corner Brackets --- */
|
| 1558 |
-
|
| 1559 |
-
.inspection-quadrant::before {
|
| 1560 |
-
content: '';
|
| 1561 |
-
position: absolute;
|
| 1562 |
-
top: 6px;
|
| 1563 |
-
right: 6px;
|
| 1564 |
-
width: 14px;
|
| 1565 |
-
height: 14px;
|
| 1566 |
-
border-top: 1.5px solid rgba(74,158,255,0.25);
|
| 1567 |
-
border-right: 1.5px solid rgba(74,158,255,0.25);
|
| 1568 |
-
pointer-events: none;
|
| 1569 |
-
z-index: 1;
|
| 1570 |
-
}
|
| 1571 |
-
|
| 1572 |
-
.inspection-quadrant::after {
|
| 1573 |
-
content: '';
|
| 1574 |
-
position: absolute;
|
| 1575 |
-
bottom: 6px;
|
| 1576 |
-
left: 6px;
|
| 1577 |
-
width: 14px;
|
| 1578 |
-
height: 14px;
|
| 1579 |
-
border-bottom: 1.5px solid rgba(74,158,255,0.25);
|
| 1580 |
-
border-left: 1.5px solid rgba(74,158,255,0.25);
|
| 1581 |
-
pointer-events: none;
|
| 1582 |
-
z-index: 1;
|
| 1583 |
-
}
|
| 1584 |
-
|
| 1585 |
-
#quadSeg::before, #quadSeg::after { border-color: rgba(74,158,255,0.25); }
|
| 1586 |
-
#quadEdge::before, #quadEdge::after { border-color: rgba(148,163,184,0.2); }
|
| 1587 |
-
#quadDepth::before, #quadDepth::after { border-color: rgba(245,158,11,0.2); }
|
| 1588 |
-
#quad3d::before, #quad3d::after { border-color: rgba(34,197,94,0.2); }
|
| 1589 |
-
|
| 1590 |
-
/* --- Quadrant Labels & Metrics --- */
|
| 1591 |
-
|
| 1592 |
-
.quad-label {
|
| 1593 |
-
position: absolute;
|
| 1594 |
-
top: 8px;
|
| 1595 |
-
left: 10px;
|
| 1596 |
-
z-index: 1;
|
| 1597 |
-
display: flex;
|
| 1598 |
-
align-items: center;
|
| 1599 |
-
gap: 6px;
|
| 1600 |
-
font-size: 8px;
|
| 1601 |
-
letter-spacing: 2px;
|
| 1602 |
-
font-family: var(--mono);
|
| 1603 |
-
pointer-events: none;
|
| 1604 |
-
}
|
| 1605 |
-
|
| 1606 |
-
.quad-dot {
|
| 1607 |
-
width: 5px;
|
| 1608 |
-
height: 5px;
|
| 1609 |
-
border-radius: 50%;
|
| 1610 |
-
}
|
| 1611 |
-
|
| 1612 |
-
.quad-dot-seg { background: #4a9eff; box-shadow: 0 0 4px rgba(74,158,255,0.5); }
|
| 1613 |
-
.quad-dot-edge { background: #94a3b8; box-shadow: 0 0 4px rgba(148,163,184,0.5); }
|
| 1614 |
-
.quad-dot-depth { background: #f59e0b; box-shadow: 0 0 4px rgba(245,158,11,0.5); }
|
| 1615 |
-
.quad-dot-3d { background: #22c55e; box-shadow: 0 0 4px rgba(34,197,94,0.5); }
|
| 1616 |
-
|
| 1617 |
-
#quadSeg .quad-label { color: rgba(74,158,255,0.6); }
|
| 1618 |
-
#quadEdge .quad-label { color: rgba(148,163,184,0.6); }
|
| 1619 |
-
#quadDepth .quad-label { color: rgba(245,158,11,0.6); }
|
| 1620 |
-
#quad3d .quad-label { color: rgba(34,197,94,0.6); }
|
| 1621 |
-
|
| 1622 |
-
.quad-metric {
|
| 1623 |
-
position: absolute;
|
| 1624 |
-
bottom: 8px;
|
| 1625 |
-
right: 10px;
|
| 1626 |
-
font-size: 8px;
|
| 1627 |
-
font-family: var(--mono);
|
| 1628 |
-
pointer-events: none;
|
| 1629 |
-
}
|
| 1630 |
-
|
| 1631 |
-
#quadSeg .quad-metric { color: rgba(74,158,255,0.35); }
|
| 1632 |
-
#quadEdge .quad-metric { color: rgba(148,163,184,0.35); }
|
| 1633 |
-
#quadDepth .quad-metric { color: rgba(245,158,11,0.35); }
|
| 1634 |
-
#quad3d .quad-metric { color: rgba(34,197,94,0.35); }
|
| 1635 |
-
|
| 1636 |
-
.quad-canvas {
|
| 1637 |
-
width: 100%;
|
| 1638 |
-
height: 100%;
|
| 1639 |
-
display: block;
|
| 1640 |
-
object-fit: contain;
|
| 1641 |
-
}
|
| 1642 |
-
|
| 1643 |
-
.quad-3d-container {
|
| 1644 |
-
position: absolute;
|
| 1645 |
-
inset: 0;
|
| 1646 |
-
z-index: 2;
|
| 1647 |
-
}
|
| 1648 |
-
|
| 1649 |
-
.quad-3d-container canvas {
|
| 1650 |
-
width: 100% !important;
|
| 1651 |
-
height: 100% !important;
|
| 1652 |
-
}
|
| 1653 |
-
|
| 1654 |
-
/* --- 3D On-Demand Prompt --- */
|
| 1655 |
-
|
| 1656 |
-
.quad-3d-prompt {
|
| 1657 |
-
position: absolute;
|
| 1658 |
-
inset: 0;
|
| 1659 |
-
display: flex;
|
| 1660 |
-
flex-direction: column;
|
| 1661 |
-
align-items: center;
|
| 1662 |
-
justify-content: center;
|
| 1663 |
-
gap: 8px;
|
| 1664 |
-
z-index: 3;
|
| 1665 |
-
}
|
| 1666 |
-
|
| 1667 |
-
.quad-3d-prompt-icon {
|
| 1668 |
-
color: rgba(34,197,94,0.4);
|
| 1669 |
-
margin-bottom: 4px;
|
| 1670 |
-
}
|
| 1671 |
-
|
| 1672 |
-
.quad-3d-prompt-label {
|
| 1673 |
-
font: 10px/1 var(--mono);
|
| 1674 |
-
color: rgba(255,255,255,0.4);
|
| 1675 |
-
letter-spacing: 1px;
|
| 1676 |
-
}
|
| 1677 |
-
|
| 1678 |
-
.quad-3d-btn {
|
| 1679 |
-
padding: 6px 16px;
|
| 1680 |
-
border: 1px solid rgba(34,197,94,0.3);
|
| 1681 |
-
border-radius: 4px;
|
| 1682 |
-
background: rgba(34,197,94,0.08);
|
| 1683 |
-
color: rgba(34,197,94,0.8);
|
| 1684 |
-
font: bold 10px/1 var(--mono);
|
| 1685 |
-
letter-spacing: 0.5px;
|
| 1686 |
-
cursor: pointer;
|
| 1687 |
-
transition: all 0.15s ease;
|
| 1688 |
-
}
|
| 1689 |
-
|
| 1690 |
-
.quad-3d-btn:hover {
|
| 1691 |
-
background: rgba(34,197,94,0.18);
|
| 1692 |
-
border-color: rgba(34,197,94,0.5);
|
| 1693 |
-
color: #22c55e;
|
| 1694 |
-
}
|
| 1695 |
-
|
| 1696 |
-
.quad-3d-btn-gen {
|
| 1697 |
-
border-color: rgba(168,85,247,0.3);
|
| 1698 |
-
background: rgba(168,85,247,0.08);
|
| 1699 |
-
color: rgba(168,85,247,0.7);
|
| 1700 |
-
}
|
| 1701 |
-
|
| 1702 |
-
.quad-3d-btn-gen:hover {
|
| 1703 |
-
background: rgba(168,85,247,0.18);
|
| 1704 |
-
border-color: rgba(168,85,247,0.5);
|
| 1705 |
-
color: #a855f7;
|
| 1706 |
-
}
|
| 1707 |
-
|
| 1708 |
-
.quad-3d-prompt-hint {
|
| 1709 |
-
font: 8px/1 var(--mono);
|
| 1710 |
-
color: rgba(255,255,255,0.2);
|
| 1711 |
-
margin-top: 2px;
|
| 1712 |
-
}
|
| 1713 |
-
|
| 1714 |
-
.quad-3d-status {
|
| 1715 |
-
position: absolute;
|
| 1716 |
-
inset: 0;
|
| 1717 |
-
display: flex;
|
| 1718 |
-
flex-direction: column;
|
| 1719 |
-
align-items: center;
|
| 1720 |
-
justify-content: center;
|
| 1721 |
-
gap: 12px;
|
| 1722 |
-
z-index: 3;
|
| 1723 |
-
}
|
| 1724 |
-
|
| 1725 |
-
.quad-3d-status-text {
|
| 1726 |
-
font: 10px/1.4 var(--mono);
|
| 1727 |
-
color: rgba(255,255,255,0.5);
|
| 1728 |
-
letter-spacing: 0.5px;
|
| 1729 |
-
}
|
| 1730 |
-
|
| 1731 |
-
/* --- Per-Quadrant Loading/Error --- */
|
| 1732 |
-
|
| 1733 |
-
.quad-loading {
|
| 1734 |
-
position: absolute;
|
| 1735 |
-
inset: 0;
|
| 1736 |
-
display: flex;
|
| 1737 |
-
align-items: center;
|
| 1738 |
-
justify-content: center;
|
| 1739 |
-
background: rgba(0,0,0,0.5);
|
| 1740 |
-
backdrop-filter: blur(2px);
|
| 1741 |
-
z-index: 5;
|
| 1742 |
-
}
|
| 1743 |
-
|
| 1744 |
-
.quad-error {
|
| 1745 |
-
position: absolute;
|
| 1746 |
-
inset: 0;
|
| 1747 |
-
display: flex;
|
| 1748 |
-
align-items: center;
|
| 1749 |
-
justify-content: center;
|
| 1750 |
-
background: rgba(0,0,0,0.5);
|
| 1751 |
-
z-index: 5;
|
| 1752 |
-
color: var(--danger);
|
| 1753 |
-
font-size: 10px;
|
| 1754 |
-
font-family: var(--mono);
|
| 1755 |
-
padding: 10px;
|
| 1756 |
-
text-align: center;
|
| 1757 |
-
}
|
| 1758 |
-
|
| 1759 |
-
/* --- Bottom Metrics Strip --- */
|
| 1760 |
-
|
| 1761 |
-
.inspection-metrics {
|
| 1762 |
-
display: flex;
|
| 1763 |
-
gap: 24px;
|
| 1764 |
-
padding: 8px 12px;
|
| 1765 |
-
border-top: 1px solid rgba(255,255,255,0.04);
|
| 1766 |
-
font-size: 9px;
|
| 1767 |
-
color: #64748b;
|
| 1768 |
-
font-family: var(--mono);
|
| 1769 |
-
}
|
| 1770 |
-
|
| 1771 |
-
.inspection-metrics span {
|
| 1772 |
-
color: #e2e8f0;
|
| 1773 |
-
}
|
| 1774 |
-
|
| 1775 |
-
/* --- Retained --- */
|
| 1776 |
-
|
| 1777 |
-
.inspection-spinner {
|
| 1778 |
-
width: 20px; height: 20px;
|
| 1779 |
-
border: 2px solid rgba(255,255,255,.08);
|
| 1780 |
-
border-top-color: var(--accent);
|
| 1781 |
-
border-radius: 50%;
|
| 1782 |
-
animation: spin 0.7s linear infinite;
|
| 1783 |
-
}
|
| 1784 |
-
|
| 1785 |
-
.inspection-empty {
|
| 1786 |
-
position: absolute; inset: 0;
|
| 1787 |
-
display: flex; align-items: center; justify-content: center;
|
| 1788 |
-
color: var(--text3);
|
| 1789 |
-
font-size: 12px;
|
| 1790 |
-
text-align: center;
|
| 1791 |
-
padding: 20px;
|
| 1792 |
-
}
|
| 1793 |
-
|
| 1794 |
-
.engage-grid.sidebar-collapsed .panel-inspection { grid-column: 1; }
|
| 1795 |
-
.engage-grid.sidebar-collapsed .panel-chat { grid-column: 2; }
|
| 1796 |
-
|
| 1797 |
-
/* =========================================
|
| 1798 |
-
Toast Notification System
|
| 1799 |
-
========================================= */
|
| 1800 |
-
|
| 1801 |
-
.toast-container {
|
| 1802 |
-
position: fixed;
|
| 1803 |
-
bottom: 56px;
|
| 1804 |
-
right: 20px;
|
| 1805 |
-
z-index: 9999;
|
| 1806 |
-
display: flex;
|
| 1807 |
-
flex-direction: column-reverse;
|
| 1808 |
-
gap: 8px;
|
| 1809 |
-
pointer-events: none;
|
| 1810 |
-
max-width: 360px;
|
| 1811 |
-
}
|
| 1812 |
-
|
| 1813 |
-
.toast {
|
| 1814 |
-
pointer-events: auto;
|
| 1815 |
-
display: flex;
|
| 1816 |
-
align-items: center;
|
| 1817 |
-
gap: 10px;
|
| 1818 |
-
padding: 10px 16px;
|
| 1819 |
-
background: var(--glass);
|
| 1820 |
-
backdrop-filter: var(--blur);
|
| 1821 |
-
-webkit-backdrop-filter: var(--blur);
|
| 1822 |
-
border: 1px solid var(--border2);
|
| 1823 |
-
border-radius: var(--radius-md);
|
| 1824 |
-
box-shadow: var(--shadow-lg);
|
| 1825 |
-
font-size: 12px;
|
| 1826 |
-
color: var(--text);
|
| 1827 |
-
animation: toastIn 0.35s cubic-bezier(.4, 0, .2, 1);
|
| 1828 |
-
transition: all 0.3s ease;
|
| 1829 |
-
overflow: hidden;
|
| 1830 |
-
}
|
| 1831 |
-
|
| 1832 |
-
.toast.exiting {
|
| 1833 |
-
animation: toastOut 0.25s ease forwards;
|
| 1834 |
-
}
|
| 1835 |
-
|
| 1836 |
-
@keyframes toastIn {
|
| 1837 |
-
from { opacity: 0; transform: translateX(40px) scale(0.95); }
|
| 1838 |
-
to { opacity: 1; transform: translateX(0) scale(1); }
|
| 1839 |
-
}
|
| 1840 |
-
|
| 1841 |
-
@keyframes toastOut {
|
| 1842 |
-
to { opacity: 0; transform: translateX(40px) scale(0.95); }
|
| 1843 |
-
}
|
| 1844 |
-
|
| 1845 |
-
.toast-icon {
|
| 1846 |
-
width: 6px; height: 6px;
|
| 1847 |
-
border-radius: 50%;
|
| 1848 |
-
flex-shrink: 0;
|
| 1849 |
-
}
|
| 1850 |
-
|
| 1851 |
-
.toast-icon.success { background: var(--success); box-shadow: 0 0 8px rgba(52,211,153,.5); }
|
| 1852 |
-
.toast-icon.warning { background: var(--warning); box-shadow: 0 0 8px rgba(251,191,36,.5); }
|
| 1853 |
-
.toast-icon.error { background: var(--danger); box-shadow: 0 0 8px rgba(248,113,113,.5); }
|
| 1854 |
-
.toast-icon.info { background: var(--accent); box-shadow: 0 0 8px rgba(59,130,246,.5); }
|
| 1855 |
-
|
| 1856 |
-
.toast-text { flex: 1; line-height: 1.4; }
|
| 1857 |
-
|
| 1858 |
-
.toast-progress {
|
| 1859 |
-
position: absolute;
|
| 1860 |
-
bottom: 0; left: 0;
|
| 1861 |
-
height: 2px;
|
| 1862 |
-
background: var(--accent);
|
| 1863 |
-
border-radius: 0 0 0 var(--radius-md);
|
| 1864 |
-
animation: toastProgress 4s linear forwards;
|
| 1865 |
-
}
|
| 1866 |
-
|
| 1867 |
-
@keyframes toastProgress {
|
| 1868 |
-
from { width: 100%; }
|
| 1869 |
-
to { width: 0; }
|
| 1870 |
-
}
|
| 1871 |
-
|
| 1872 |
-
/* =========================================
|
| 1873 |
-
Scrollbars
|
| 1874 |
-
========================================= */
|
| 1875 |
-
|
| 1876 |
-
::-webkit-scrollbar { width: 5px; height: 5px; }
|
| 1877 |
-
::-webkit-scrollbar-track { background: transparent; }
|
| 1878 |
-
::-webkit-scrollbar-thumb { background: rgba(255,255,255,.06); border-radius: 3px; }
|
| 1879 |
-
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.12); }
|
| 1880 |
-
|
| 1881 |
-
/* =========================================
|
| 1882 |
-
Utility
|
| 1883 |
-
========================================= */
|
| 1884 |
-
|
| 1885 |
-
.mt-xs { margin-top: 4px; }
|
| 1886 |
-
.mt-sm { margin-top: 6px; }
|
| 1887 |
-
.mt-md { margin-top: 12px; }
|
| 1888 |
-
|
| 1889 |
-
/* Skeleton loading shimmer */
|
| 1890 |
-
.skeleton {
|
| 1891 |
-
background: linear-gradient(90deg, rgba(255,255,255,.03) 25%, rgba(255,255,255,.06) 50%, rgba(255,255,255,.03) 75%);
|
| 1892 |
-
background-size: 200% 100%;
|
| 1893 |
-
animation: shimmer 1.5s ease-in-out infinite;
|
| 1894 |
-
border-radius: var(--radius-sm);
|
| 1895 |
-
}
|
| 1896 |
-
|
| 1897 |
-
@keyframes shimmer {
|
| 1898 |
-
0% { background-position: 200% 0; }
|
| 1899 |
-
100% { background-position: -200% 0; }
|
| 1900 |
-
}
|
| 1901 |
-
|
| 1902 |
-
/* Number transition for animated counters */
|
| 1903 |
-
.counter-value {
|
| 1904 |
-
font-variant-numeric: tabular-nums;
|
| 1905 |
-
transition: color 0.3s ease;
|
| 1906 |
-
display: inline-block;
|
| 1907 |
-
}
|
| 1908 |
-
|
| 1909 |
-
.counter-value.tick {
|
| 1910 |
-
color: var(--accent2);
|
| 1911 |
-
}
|
| 1912 |
-
|
| 1913 |
-
/* Focus visible ring */
|
| 1914 |
-
:focus-visible {
|
| 1915 |
-
outline: 2px solid var(--accent);
|
| 1916 |
-
outline-offset: 2px;
|
| 1917 |
-
}
|
| 1918 |
-
|
| 1919 |
-
/* =========================================
|
| 1920 |
-
Selection
|
| 1921 |
-
========================================= */
|
| 1922 |
-
|
| 1923 |
-
::selection {
|
| 1924 |
-
background: rgba(59, 130, 246, .25);
|
| 1925 |
-
color: #fff;
|
| 1926 |
-
}
|
| 1927 |
-
|
| 1928 |
-
/* ── Explainability Graph ─────────────────────────────────────── */
|
| 1929 |
-
|
| 1930 |
-
.interpretability-graph {
|
| 1931 |
-
position: relative;
|
| 1932 |
-
width: 100%;
|
| 1933 |
-
min-height: 60px;
|
| 1934 |
-
background: #0a0e1a;
|
| 1935 |
-
border-top: 1px solid #1e293b;
|
| 1936 |
-
overflow: visible;
|
| 1937 |
-
}
|
| 1938 |
-
|
| 1939 |
-
.explain-svg {
|
| 1940 |
-
display: block;
|
| 1941 |
-
width: 100%;
|
| 1942 |
-
font-family: 'Inter', -apple-system, sans-serif;
|
| 1943 |
-
}
|
| 1944 |
-
|
| 1945 |
-
.explain-svg text {
|
| 1946 |
-
pointer-events: none;
|
| 1947 |
-
user-select: none;
|
| 1948 |
-
}
|
| 1949 |
-
|
| 1950 |
-
.explain-svg g {
|
| 1951 |
-
cursor: pointer;
|
| 1952 |
-
}
|
| 1953 |
-
|
| 1954 |
-
/* Loading state */
|
| 1955 |
-
.explain-loading {
|
| 1956 |
-
display: flex;
|
| 1957 |
-
align-items: center;
|
| 1958 |
-
justify-content: center;
|
| 1959 |
-
gap: 12px;
|
| 1960 |
-
padding: 32px 16px;
|
| 1961 |
-
color: #94a3b8;
|
| 1962 |
-
font-size: 12px;
|
| 1963 |
-
}
|
| 1964 |
-
|
| 1965 |
-
.explain-spinner {
|
| 1966 |
-
width: 18px;
|
| 1967 |
-
height: 18px;
|
| 1968 |
-
border: 2px solid #334155;
|
| 1969 |
-
border-top-color: #7c3aed;
|
| 1970 |
-
border-radius: 50%;
|
| 1971 |
-
animation: explain-spin 0.8s linear infinite;
|
| 1972 |
-
}
|
| 1973 |
-
|
| 1974 |
-
@keyframes explain-spin {
|
| 1975 |
-
to { transform: rotate(360deg); }
|
| 1976 |
-
}
|
| 1977 |
-
|
| 1978 |
-
.explain-loading span {
|
| 1979 |
-
animation: explain-pulse 2s ease-in-out infinite;
|
| 1980 |
-
}
|
| 1981 |
-
|
| 1982 |
-
@keyframes explain-pulse {
|
| 1983 |
-
0%, 100% { opacity: 0.6; }
|
| 1984 |
-
50% { opacity: 1; }
|
| 1985 |
-
}
|
| 1986 |
-
|
| 1987 |
-
/* Error state */
|
| 1988 |
-
.explain-error {
|
| 1989 |
-
padding: 24px 16px;
|
| 1990 |
-
color: #f87171;
|
| 1991 |
-
font-size: 11px;
|
| 1992 |
-
text-align: center;
|
| 1993 |
-
}
|
| 1994 |
-
|
| 1995 |
-
/* Tooltip */
|
| 1996 |
-
.explain-tooltip {
|
| 1997 |
-
position: absolute;
|
| 1998 |
-
z-index: 1000;
|
| 1999 |
-
background: #1e293b;
|
| 2000 |
-
border: 1px solid #334155;
|
| 2001 |
-
border-radius: 6px;
|
| 2002 |
-
padding: 10px 12px;
|
| 2003 |
-
max-width: 280px;
|
| 2004 |
-
font-size: 11px;
|
| 2005 |
-
color: #e2e8f0;
|
| 2006 |
-
line-height: 1.5;
|
| 2007 |
-
pointer-events: none;
|
| 2008 |
-
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
| 2009 |
-
}
|
| 2010 |
-
|
| 2011 |
-
.explain-tooltip strong {
|
| 2012 |
-
color: #f8fafc;
|
| 2013 |
-
font-size: 12px;
|
| 2014 |
-
}
|
| 2015 |
-
|
| 2016 |
-
.tip-section {
|
| 2017 |
-
margin-top: 6px;
|
| 2018 |
-
padding-top: 4px;
|
| 2019 |
-
border-top: 1px solid #334155;
|
| 2020 |
-
}
|
| 2021 |
-
|
| 2022 |
-
.tip-label {
|
| 2023 |
-
font-weight: 600;
|
| 2024 |
-
color: #94a3b8;
|
| 2025 |
-
}
|
| 2026 |
-
|
| 2027 |
-
.tip-agree .tip-label { color: #4ade80; }
|
| 2028 |
-
.tip-disagree .tip-label { color: #f87171; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/vendor/GLTFLoader.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/vendor/OrbitControls.js
DELETED
|
@@ -1,1101 +0,0 @@
|
|
| 1 |
-
( function () {
|
| 2 |
-
|
| 3 |
-
// This set of controls performs orbiting, dollying (zooming), and panning.
|
| 4 |
-
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
|
| 5 |
-
//
|
| 6 |
-
// Orbit - left mouse / touch: one-finger move
|
| 7 |
-
// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
|
| 8 |
-
// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
|
| 9 |
-
|
| 10 |
-
const _changeEvent = {
|
| 11 |
-
type: 'change'
|
| 12 |
-
};
|
| 13 |
-
const _startEvent = {
|
| 14 |
-
type: 'start'
|
| 15 |
-
};
|
| 16 |
-
const _endEvent = {
|
| 17 |
-
type: 'end'
|
| 18 |
-
};
|
| 19 |
-
class OrbitControls extends THREE.EventDispatcher {
|
| 20 |
-
|
| 21 |
-
constructor( object, domElement ) {
|
| 22 |
-
|
| 23 |
-
super();
|
| 24 |
-
this.object = object;
|
| 25 |
-
this.domElement = domElement;
|
| 26 |
-
this.domElement.style.touchAction = 'none'; // disable touch scroll
|
| 27 |
-
|
| 28 |
-
// Set to false to disable this control
|
| 29 |
-
this.enabled = true;
|
| 30 |
-
|
| 31 |
-
// "target" sets the location of focus, where the object orbits around
|
| 32 |
-
this.target = new THREE.Vector3();
|
| 33 |
-
|
| 34 |
-
// How far you can dolly in and out ( PerspectiveCamera only )
|
| 35 |
-
this.minDistance = 0;
|
| 36 |
-
this.maxDistance = Infinity;
|
| 37 |
-
|
| 38 |
-
// How far you can zoom in and out ( OrthographicCamera only )
|
| 39 |
-
this.minZoom = 0;
|
| 40 |
-
this.maxZoom = Infinity;
|
| 41 |
-
|
| 42 |
-
// How far you can orbit vertically, upper and lower limits.
|
| 43 |
-
// Range is 0 to Math.PI radians.
|
| 44 |
-
this.minPolarAngle = 0; // radians
|
| 45 |
-
this.maxPolarAngle = Math.PI; // radians
|
| 46 |
-
|
| 47 |
-
// How far you can orbit horizontally, upper and lower limits.
|
| 48 |
-
// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
|
| 49 |
-
this.minAzimuthAngle = - Infinity; // radians
|
| 50 |
-
this.maxAzimuthAngle = Infinity; // radians
|
| 51 |
-
|
| 52 |
-
// Set to true to enable damping (inertia)
|
| 53 |
-
// If damping is enabled, you must call controls.update() in your animation loop
|
| 54 |
-
this.enableDamping = false;
|
| 55 |
-
this.dampingFactor = 0.05;
|
| 56 |
-
|
| 57 |
-
// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
|
| 58 |
-
// Set to false to disable zooming
|
| 59 |
-
this.enableZoom = true;
|
| 60 |
-
this.zoomSpeed = 1.0;
|
| 61 |
-
|
| 62 |
-
// Set to false to disable rotating
|
| 63 |
-
this.enableRotate = true;
|
| 64 |
-
this.rotateSpeed = 1.0;
|
| 65 |
-
|
| 66 |
-
// Set to false to disable panning
|
| 67 |
-
this.enablePan = true;
|
| 68 |
-
this.panSpeed = 1.0;
|
| 69 |
-
this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
|
| 70 |
-
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
|
| 71 |
-
|
| 72 |
-
// Set to true to automatically rotate around the target
|
| 73 |
-
// If auto-rotate is enabled, you must call controls.update() in your animation loop
|
| 74 |
-
this.autoRotate = false;
|
| 75 |
-
this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60
|
| 76 |
-
|
| 77 |
-
// The four arrow keys
|
| 78 |
-
this.keys = {
|
| 79 |
-
LEFT: 'ArrowLeft',
|
| 80 |
-
UP: 'ArrowUp',
|
| 81 |
-
RIGHT: 'ArrowRight',
|
| 82 |
-
BOTTOM: 'ArrowDown'
|
| 83 |
-
};
|
| 84 |
-
|
| 85 |
-
// Mouse buttons
|
| 86 |
-
this.mouseButtons = {
|
| 87 |
-
LEFT: THREE.MOUSE.ROTATE,
|
| 88 |
-
MIDDLE: THREE.MOUSE.DOLLY,
|
| 89 |
-
RIGHT: THREE.MOUSE.PAN
|
| 90 |
-
};
|
| 91 |
-
|
| 92 |
-
// Touch fingers
|
| 93 |
-
this.touches = {
|
| 94 |
-
ONE: THREE.TOUCH.ROTATE,
|
| 95 |
-
TWO: THREE.TOUCH.DOLLY_PAN
|
| 96 |
-
};
|
| 97 |
-
|
| 98 |
-
// for reset
|
| 99 |
-
this.target0 = this.target.clone();
|
| 100 |
-
this.position0 = this.object.position.clone();
|
| 101 |
-
this.zoom0 = this.object.zoom;
|
| 102 |
-
|
| 103 |
-
// the target DOM element for key events
|
| 104 |
-
this._domElementKeyEvents = null;
|
| 105 |
-
|
| 106 |
-
//
|
| 107 |
-
// public methods
|
| 108 |
-
//
|
| 109 |
-
|
| 110 |
-
this.getPolarAngle = function () {
|
| 111 |
-
|
| 112 |
-
return spherical.phi;
|
| 113 |
-
|
| 114 |
-
};
|
| 115 |
-
|
| 116 |
-
this.getAzimuthalAngle = function () {
|
| 117 |
-
|
| 118 |
-
return spherical.theta;
|
| 119 |
-
|
| 120 |
-
};
|
| 121 |
-
|
| 122 |
-
this.getDistance = function () {
|
| 123 |
-
|
| 124 |
-
return this.object.position.distanceTo( this.target );
|
| 125 |
-
|
| 126 |
-
};
|
| 127 |
-
|
| 128 |
-
this.listenToKeyEvents = function ( domElement ) {
|
| 129 |
-
|
| 130 |
-
domElement.addEventListener( 'keydown', onKeyDown );
|
| 131 |
-
this._domElementKeyEvents = domElement;
|
| 132 |
-
|
| 133 |
-
};
|
| 134 |
-
|
| 135 |
-
this.saveState = function () {
|
| 136 |
-
|
| 137 |
-
scope.target0.copy( scope.target );
|
| 138 |
-
scope.position0.copy( scope.object.position );
|
| 139 |
-
scope.zoom0 = scope.object.zoom;
|
| 140 |
-
|
| 141 |
-
};
|
| 142 |
-
|
| 143 |
-
this.reset = function () {
|
| 144 |
-
|
| 145 |
-
scope.target.copy( scope.target0 );
|
| 146 |
-
scope.object.position.copy( scope.position0 );
|
| 147 |
-
scope.object.zoom = scope.zoom0;
|
| 148 |
-
scope.object.updateProjectionMatrix();
|
| 149 |
-
scope.dispatchEvent( _changeEvent );
|
| 150 |
-
scope.update();
|
| 151 |
-
state = STATE.NONE;
|
| 152 |
-
|
| 153 |
-
};
|
| 154 |
-
|
| 155 |
-
// this method is exposed, but perhaps it would be better if we can make it private...
|
| 156 |
-
this.update = function () {
|
| 157 |
-
|
| 158 |
-
const offset = new THREE.Vector3();
|
| 159 |
-
|
| 160 |
-
// so camera.up is the orbit axis
|
| 161 |
-
const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
|
| 162 |
-
const quatInverse = quat.clone().invert();
|
| 163 |
-
const lastPosition = new THREE.Vector3();
|
| 164 |
-
const lastQuaternion = new THREE.Quaternion();
|
| 165 |
-
const twoPI = 2 * Math.PI;
|
| 166 |
-
return function update() {
|
| 167 |
-
|
| 168 |
-
const position = scope.object.position;
|
| 169 |
-
offset.copy( position ).sub( scope.target );
|
| 170 |
-
|
| 171 |
-
// rotate offset to "y-axis-is-up" space
|
| 172 |
-
offset.applyQuaternion( quat );
|
| 173 |
-
|
| 174 |
-
// angle from z-axis around y-axis
|
| 175 |
-
spherical.setFromVector3( offset );
|
| 176 |
-
if ( scope.autoRotate && state === STATE.NONE ) {
|
| 177 |
-
|
| 178 |
-
rotateLeft( getAutoRotationAngle() );
|
| 179 |
-
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
if ( scope.enableDamping ) {
|
| 183 |
-
|
| 184 |
-
spherical.theta += sphericalDelta.theta * scope.dampingFactor;
|
| 185 |
-
spherical.phi += sphericalDelta.phi * scope.dampingFactor;
|
| 186 |
-
|
| 187 |
-
} else {
|
| 188 |
-
|
| 189 |
-
spherical.theta += sphericalDelta.theta;
|
| 190 |
-
spherical.phi += sphericalDelta.phi;
|
| 191 |
-
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
// restrict theta to be between desired limits
|
| 195 |
-
|
| 196 |
-
let min = scope.minAzimuthAngle;
|
| 197 |
-
let max = scope.maxAzimuthAngle;
|
| 198 |
-
if ( isFinite( min ) && isFinite( max ) ) {
|
| 199 |
-
|
| 200 |
-
if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
|
| 201 |
-
if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
|
| 202 |
-
if ( min <= max ) {
|
| 203 |
-
|
| 204 |
-
spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
|
| 205 |
-
|
| 206 |
-
} else {
|
| 207 |
-
|
| 208 |
-
spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta );
|
| 209 |
-
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
// restrict phi to be between desired limits
|
| 215 |
-
spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
|
| 216 |
-
spherical.makeSafe();
|
| 217 |
-
spherical.radius *= scale;
|
| 218 |
-
|
| 219 |
-
// restrict radius to be between desired limits
|
| 220 |
-
spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
|
| 221 |
-
|
| 222 |
-
// move target to panned location
|
| 223 |
-
|
| 224 |
-
if ( scope.enableDamping === true ) {
|
| 225 |
-
|
| 226 |
-
scope.target.addScaledVector( panOffset, scope.dampingFactor );
|
| 227 |
-
|
| 228 |
-
} else {
|
| 229 |
-
|
| 230 |
-
scope.target.add( panOffset );
|
| 231 |
-
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
offset.setFromSpherical( spherical );
|
| 235 |
-
|
| 236 |
-
// rotate offset back to "camera-up-vector-is-up" space
|
| 237 |
-
offset.applyQuaternion( quatInverse );
|
| 238 |
-
position.copy( scope.target ).add( offset );
|
| 239 |
-
scope.object.lookAt( scope.target );
|
| 240 |
-
if ( scope.enableDamping === true ) {
|
| 241 |
-
|
| 242 |
-
sphericalDelta.theta *= 1 - scope.dampingFactor;
|
| 243 |
-
sphericalDelta.phi *= 1 - scope.dampingFactor;
|
| 244 |
-
panOffset.multiplyScalar( 1 - scope.dampingFactor );
|
| 245 |
-
|
| 246 |
-
} else {
|
| 247 |
-
|
| 248 |
-
sphericalDelta.set( 0, 0, 0 );
|
| 249 |
-
panOffset.set( 0, 0, 0 );
|
| 250 |
-
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
scale = 1;
|
| 254 |
-
|
| 255 |
-
// update condition is:
|
| 256 |
-
// min(camera displacement, camera rotation in radians)^2 > EPS
|
| 257 |
-
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
|
| 258 |
-
|
| 259 |
-
if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
|
| 260 |
-
|
| 261 |
-
scope.dispatchEvent( _changeEvent );
|
| 262 |
-
lastPosition.copy( scope.object.position );
|
| 263 |
-
lastQuaternion.copy( scope.object.quaternion );
|
| 264 |
-
zoomChanged = false;
|
| 265 |
-
return true;
|
| 266 |
-
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
return false;
|
| 270 |
-
|
| 271 |
-
};
|
| 272 |
-
|
| 273 |
-
}();
|
| 274 |
-
this.dispose = function () {
|
| 275 |
-
|
| 276 |
-
scope.domElement.removeEventListener( 'contextmenu', onContextMenu );
|
| 277 |
-
scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
|
| 278 |
-
scope.domElement.removeEventListener( 'pointercancel', onPointerCancel );
|
| 279 |
-
scope.domElement.removeEventListener( 'wheel', onMouseWheel );
|
| 280 |
-
scope.domElement.removeEventListener( 'pointermove', onPointerMove );
|
| 281 |
-
scope.domElement.removeEventListener( 'pointerup', onPointerUp );
|
| 282 |
-
if ( scope._domElementKeyEvents !== null ) {
|
| 283 |
-
|
| 284 |
-
scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
|
| 285 |
-
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
|
| 289 |
-
|
| 290 |
-
};
|
| 291 |
-
|
| 292 |
-
//
|
| 293 |
-
// internals
|
| 294 |
-
//
|
| 295 |
-
|
| 296 |
-
const scope = this;
|
| 297 |
-
const STATE = {
|
| 298 |
-
NONE: - 1,
|
| 299 |
-
ROTATE: 0,
|
| 300 |
-
DOLLY: 1,
|
| 301 |
-
PAN: 2,
|
| 302 |
-
TOUCH_ROTATE: 3,
|
| 303 |
-
TOUCH_PAN: 4,
|
| 304 |
-
TOUCH_DOLLY_PAN: 5,
|
| 305 |
-
TOUCH_DOLLY_ROTATE: 6
|
| 306 |
-
};
|
| 307 |
-
let state = STATE.NONE;
|
| 308 |
-
const EPS = 0.000001;
|
| 309 |
-
|
| 310 |
-
// current position in spherical coordinates
|
| 311 |
-
const spherical = new THREE.Spherical();
|
| 312 |
-
const sphericalDelta = new THREE.Spherical();
|
| 313 |
-
let scale = 1;
|
| 314 |
-
const panOffset = new THREE.Vector3();
|
| 315 |
-
let zoomChanged = false;
|
| 316 |
-
const rotateStart = new THREE.Vector2();
|
| 317 |
-
const rotateEnd = new THREE.Vector2();
|
| 318 |
-
const rotateDelta = new THREE.Vector2();
|
| 319 |
-
const panStart = new THREE.Vector2();
|
| 320 |
-
const panEnd = new THREE.Vector2();
|
| 321 |
-
const panDelta = new THREE.Vector2();
|
| 322 |
-
const dollyStart = new THREE.Vector2();
|
| 323 |
-
const dollyEnd = new THREE.Vector2();
|
| 324 |
-
const dollyDelta = new THREE.Vector2();
|
| 325 |
-
const pointers = [];
|
| 326 |
-
const pointerPositions = {};
|
| 327 |
-
function getAutoRotationAngle() {
|
| 328 |
-
|
| 329 |
-
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
|
| 330 |
-
|
| 331 |
-
}
|
| 332 |
-
|
| 333 |
-
function getZoomScale() {
|
| 334 |
-
|
| 335 |
-
return Math.pow( 0.95, scope.zoomSpeed );
|
| 336 |
-
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
function rotateLeft( angle ) {
|
| 340 |
-
|
| 341 |
-
sphericalDelta.theta -= angle;
|
| 342 |
-
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
function rotateUp( angle ) {
|
| 346 |
-
|
| 347 |
-
sphericalDelta.phi -= angle;
|
| 348 |
-
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
const panLeft = function () {
|
| 352 |
-
|
| 353 |
-
const v = new THREE.Vector3();
|
| 354 |
-
return function panLeft( distance, objectMatrix ) {
|
| 355 |
-
|
| 356 |
-
v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
|
| 357 |
-
v.multiplyScalar( - distance );
|
| 358 |
-
panOffset.add( v );
|
| 359 |
-
|
| 360 |
-
};
|
| 361 |
-
|
| 362 |
-
}();
|
| 363 |
-
const panUp = function () {
|
| 364 |
-
|
| 365 |
-
const v = new THREE.Vector3();
|
| 366 |
-
return function panUp( distance, objectMatrix ) {
|
| 367 |
-
|
| 368 |
-
if ( scope.screenSpacePanning === true ) {
|
| 369 |
-
|
| 370 |
-
v.setFromMatrixColumn( objectMatrix, 1 );
|
| 371 |
-
|
| 372 |
-
} else {
|
| 373 |
-
|
| 374 |
-
v.setFromMatrixColumn( objectMatrix, 0 );
|
| 375 |
-
v.crossVectors( scope.object.up, v );
|
| 376 |
-
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
v.multiplyScalar( distance );
|
| 380 |
-
panOffset.add( v );
|
| 381 |
-
|
| 382 |
-
};
|
| 383 |
-
|
| 384 |
-
}();
|
| 385 |
-
|
| 386 |
-
// deltaX and deltaY are in pixels; right and down are positive
|
| 387 |
-
const pan = function () {
|
| 388 |
-
|
| 389 |
-
const offset = new THREE.Vector3();
|
| 390 |
-
return function pan( deltaX, deltaY ) {
|
| 391 |
-
|
| 392 |
-
const element = scope.domElement;
|
| 393 |
-
if ( scope.object.isPerspectiveCamera ) {
|
| 394 |
-
|
| 395 |
-
// perspective
|
| 396 |
-
const position = scope.object.position;
|
| 397 |
-
offset.copy( position ).sub( scope.target );
|
| 398 |
-
let targetDistance = offset.length();
|
| 399 |
-
|
| 400 |
-
// half of the fov is center to top of screen
|
| 401 |
-
targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 );
|
| 402 |
-
|
| 403 |
-
// we use only clientHeight here so aspect ratio does not distort speed
|
| 404 |
-
panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
|
| 405 |
-
panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
|
| 406 |
-
|
| 407 |
-
} else if ( scope.object.isOrthographicCamera ) {
|
| 408 |
-
|
| 409 |
-
// orthographic
|
| 410 |
-
panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
|
| 411 |
-
panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
|
| 412 |
-
|
| 413 |
-
} else {
|
| 414 |
-
|
| 415 |
-
// camera neither orthographic nor perspective
|
| 416 |
-
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
|
| 417 |
-
scope.enablePan = false;
|
| 418 |
-
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
};
|
| 422 |
-
|
| 423 |
-
}();
|
| 424 |
-
function dollyOut( dollyScale ) {
|
| 425 |
-
|
| 426 |
-
if ( scope.object.isPerspectiveCamera ) {
|
| 427 |
-
|
| 428 |
-
scale /= dollyScale;
|
| 429 |
-
|
| 430 |
-
} else if ( scope.object.isOrthographicCamera ) {
|
| 431 |
-
|
| 432 |
-
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
|
| 433 |
-
scope.object.updateProjectionMatrix();
|
| 434 |
-
zoomChanged = true;
|
| 435 |
-
|
| 436 |
-
} else {
|
| 437 |
-
|
| 438 |
-
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
|
| 439 |
-
scope.enableZoom = false;
|
| 440 |
-
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
function dollyIn( dollyScale ) {
|
| 446 |
-
|
| 447 |
-
if ( scope.object.isPerspectiveCamera ) {
|
| 448 |
-
|
| 449 |
-
scale *= dollyScale;
|
| 450 |
-
|
| 451 |
-
} else if ( scope.object.isOrthographicCamera ) {
|
| 452 |
-
|
| 453 |
-
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
|
| 454 |
-
scope.object.updateProjectionMatrix();
|
| 455 |
-
zoomChanged = true;
|
| 456 |
-
|
| 457 |
-
} else {
|
| 458 |
-
|
| 459 |
-
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
|
| 460 |
-
scope.enableZoom = false;
|
| 461 |
-
|
| 462 |
-
}
|
| 463 |
-
|
| 464 |
-
}
|
| 465 |
-
|
| 466 |
-
//
|
| 467 |
-
// event callbacks - update the object state
|
| 468 |
-
//
|
| 469 |
-
|
| 470 |
-
function handleMouseDownRotate( event ) {
|
| 471 |
-
|
| 472 |
-
rotateStart.set( event.clientX, event.clientY );
|
| 473 |
-
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
function handleMouseDownDolly( event ) {
|
| 477 |
-
|
| 478 |
-
dollyStart.set( event.clientX, event.clientY );
|
| 479 |
-
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
function handleMouseDownPan( event ) {
|
| 483 |
-
|
| 484 |
-
panStart.set( event.clientX, event.clientY );
|
| 485 |
-
|
| 486 |
-
}
|
| 487 |
-
|
| 488 |
-
function handleMouseMoveRotate( event ) {
|
| 489 |
-
|
| 490 |
-
rotateEnd.set( event.clientX, event.clientY );
|
| 491 |
-
rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
|
| 492 |
-
const element = scope.domElement;
|
| 493 |
-
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
|
| 494 |
-
|
| 495 |
-
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
|
| 496 |
-
rotateStart.copy( rotateEnd );
|
| 497 |
-
scope.update();
|
| 498 |
-
|
| 499 |
-
}
|
| 500 |
-
|
| 501 |
-
function handleMouseMoveDolly( event ) {
|
| 502 |
-
|
| 503 |
-
dollyEnd.set( event.clientX, event.clientY );
|
| 504 |
-
dollyDelta.subVectors( dollyEnd, dollyStart );
|
| 505 |
-
if ( dollyDelta.y > 0 ) {
|
| 506 |
-
|
| 507 |
-
dollyOut( getZoomScale() );
|
| 508 |
-
|
| 509 |
-
} else if ( dollyDelta.y < 0 ) {
|
| 510 |
-
|
| 511 |
-
dollyIn( getZoomScale() );
|
| 512 |
-
|
| 513 |
-
}
|
| 514 |
-
|
| 515 |
-
dollyStart.copy( dollyEnd );
|
| 516 |
-
scope.update();
|
| 517 |
-
|
| 518 |
-
}
|
| 519 |
-
|
| 520 |
-
function handleMouseMovePan( event ) {
|
| 521 |
-
|
| 522 |
-
panEnd.set( event.clientX, event.clientY );
|
| 523 |
-
panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
|
| 524 |
-
pan( panDelta.x, panDelta.y );
|
| 525 |
-
panStart.copy( panEnd );
|
| 526 |
-
scope.update();
|
| 527 |
-
|
| 528 |
-
}
|
| 529 |
-
|
| 530 |
-
function handleMouseWheel( event ) {
|
| 531 |
-
|
| 532 |
-
if ( event.deltaY < 0 ) {
|
| 533 |
-
|
| 534 |
-
dollyIn( getZoomScale() );
|
| 535 |
-
|
| 536 |
-
} else if ( event.deltaY > 0 ) {
|
| 537 |
-
|
| 538 |
-
dollyOut( getZoomScale() );
|
| 539 |
-
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
-
scope.update();
|
| 543 |
-
|
| 544 |
-
}
|
| 545 |
-
|
| 546 |
-
function handleKeyDown( event ) {
|
| 547 |
-
|
| 548 |
-
let needsUpdate = false;
|
| 549 |
-
switch ( event.code ) {
|
| 550 |
-
|
| 551 |
-
case scope.keys.UP:
|
| 552 |
-
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
|
| 553 |
-
|
| 554 |
-
rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
|
| 555 |
-
|
| 556 |
-
} else {
|
| 557 |
-
|
| 558 |
-
pan( 0, scope.keyPanSpeed );
|
| 559 |
-
|
| 560 |
-
}
|
| 561 |
-
|
| 562 |
-
needsUpdate = true;
|
| 563 |
-
break;
|
| 564 |
-
case scope.keys.BOTTOM:
|
| 565 |
-
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
|
| 566 |
-
|
| 567 |
-
rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
|
| 568 |
-
|
| 569 |
-
} else {
|
| 570 |
-
|
| 571 |
-
pan( 0, - scope.keyPanSpeed );
|
| 572 |
-
|
| 573 |
-
}
|
| 574 |
-
|
| 575 |
-
needsUpdate = true;
|
| 576 |
-
break;
|
| 577 |
-
case scope.keys.LEFT:
|
| 578 |
-
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
|
| 579 |
-
|
| 580 |
-
rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
|
| 581 |
-
|
| 582 |
-
} else {
|
| 583 |
-
|
| 584 |
-
pan( scope.keyPanSpeed, 0 );
|
| 585 |
-
|
| 586 |
-
}
|
| 587 |
-
|
| 588 |
-
needsUpdate = true;
|
| 589 |
-
break;
|
| 590 |
-
case scope.keys.RIGHT:
|
| 591 |
-
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
|
| 592 |
-
|
| 593 |
-
rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
|
| 594 |
-
|
| 595 |
-
} else {
|
| 596 |
-
|
| 597 |
-
pan( - scope.keyPanSpeed, 0 );
|
| 598 |
-
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
-
needsUpdate = true;
|
| 602 |
-
break;
|
| 603 |
-
|
| 604 |
-
}
|
| 605 |
-
|
| 606 |
-
if ( needsUpdate ) {
|
| 607 |
-
|
| 608 |
-
// prevent the browser from scrolling on cursor keys
|
| 609 |
-
event.preventDefault();
|
| 610 |
-
scope.update();
|
| 611 |
-
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
}
|
| 615 |
-
|
| 616 |
-
function handleTouchStartRotate() {
|
| 617 |
-
|
| 618 |
-
if ( pointers.length === 1 ) {
|
| 619 |
-
|
| 620 |
-
rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );
|
| 621 |
-
|
| 622 |
-
} else {
|
| 623 |
-
|
| 624 |
-
const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
|
| 625 |
-
const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );
|
| 626 |
-
rotateStart.set( x, y );
|
| 627 |
-
|
| 628 |
-
}
|
| 629 |
-
|
| 630 |
-
}
|
| 631 |
-
|
| 632 |
-
function handleTouchStartPan() {
|
| 633 |
-
|
| 634 |
-
if ( pointers.length === 1 ) {
|
| 635 |
-
|
| 636 |
-
panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );
|
| 637 |
-
|
| 638 |
-
} else {
|
| 639 |
-
|
| 640 |
-
const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
|
| 641 |
-
const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );
|
| 642 |
-
panStart.set( x, y );
|
| 643 |
-
|
| 644 |
-
}
|
| 645 |
-
|
| 646 |
-
}
|
| 647 |
-
|
| 648 |
-
function handleTouchStartDolly() {
|
| 649 |
-
|
| 650 |
-
const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX;
|
| 651 |
-
const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY;
|
| 652 |
-
const distance = Math.sqrt( dx * dx + dy * dy );
|
| 653 |
-
dollyStart.set( 0, distance );
|
| 654 |
-
|
| 655 |
-
}
|
| 656 |
-
|
| 657 |
-
function handleTouchStartDollyPan() {
|
| 658 |
-
|
| 659 |
-
if ( scope.enableZoom ) handleTouchStartDolly();
|
| 660 |
-
if ( scope.enablePan ) handleTouchStartPan();
|
| 661 |
-
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
function handleTouchStartDollyRotate() {
|
| 665 |
-
|
| 666 |
-
if ( scope.enableZoom ) handleTouchStartDolly();
|
| 667 |
-
if ( scope.enableRotate ) handleTouchStartRotate();
|
| 668 |
-
|
| 669 |
-
}
|
| 670 |
-
|
| 671 |
-
function handleTouchMoveRotate( event ) {
|
| 672 |
-
|
| 673 |
-
if ( pointers.length == 1 ) {
|
| 674 |
-
|
| 675 |
-
rotateEnd.set( event.pageX, event.pageY );
|
| 676 |
-
|
| 677 |
-
} else {
|
| 678 |
-
|
| 679 |
-
const position = getSecondPointerPosition( event );
|
| 680 |
-
const x = 0.5 * ( event.pageX + position.x );
|
| 681 |
-
const y = 0.5 * ( event.pageY + position.y );
|
| 682 |
-
rotateEnd.set( x, y );
|
| 683 |
-
|
| 684 |
-
}
|
| 685 |
-
|
| 686 |
-
rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
|
| 687 |
-
const element = scope.domElement;
|
| 688 |
-
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
|
| 689 |
-
|
| 690 |
-
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
|
| 691 |
-
rotateStart.copy( rotateEnd );
|
| 692 |
-
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
function handleTouchMovePan( event ) {
|
| 696 |
-
|
| 697 |
-
if ( pointers.length === 1 ) {
|
| 698 |
-
|
| 699 |
-
panEnd.set( event.pageX, event.pageY );
|
| 700 |
-
|
| 701 |
-
} else {
|
| 702 |
-
|
| 703 |
-
const position = getSecondPointerPosition( event );
|
| 704 |
-
const x = 0.5 * ( event.pageX + position.x );
|
| 705 |
-
const y = 0.5 * ( event.pageY + position.y );
|
| 706 |
-
panEnd.set( x, y );
|
| 707 |
-
|
| 708 |
-
}
|
| 709 |
-
|
| 710 |
-
panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
|
| 711 |
-
pan( panDelta.x, panDelta.y );
|
| 712 |
-
panStart.copy( panEnd );
|
| 713 |
-
|
| 714 |
-
}
|
| 715 |
-
|
| 716 |
-
function handleTouchMoveDolly( event ) {
|
| 717 |
-
|
| 718 |
-
const position = getSecondPointerPosition( event );
|
| 719 |
-
const dx = event.pageX - position.x;
|
| 720 |
-
const dy = event.pageY - position.y;
|
| 721 |
-
const distance = Math.sqrt( dx * dx + dy * dy );
|
| 722 |
-
dollyEnd.set( 0, distance );
|
| 723 |
-
dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
|
| 724 |
-
dollyOut( dollyDelta.y );
|
| 725 |
-
dollyStart.copy( dollyEnd );
|
| 726 |
-
|
| 727 |
-
}
|
| 728 |
-
|
| 729 |
-
function handleTouchMoveDollyPan( event ) {
|
| 730 |
-
|
| 731 |
-
if ( scope.enableZoom ) handleTouchMoveDolly( event );
|
| 732 |
-
if ( scope.enablePan ) handleTouchMovePan( event );
|
| 733 |
-
|
| 734 |
-
}
|
| 735 |
-
|
| 736 |
-
function handleTouchMoveDollyRotate( event ) {
|
| 737 |
-
|
| 738 |
-
if ( scope.enableZoom ) handleTouchMoveDolly( event );
|
| 739 |
-
if ( scope.enableRotate ) handleTouchMoveRotate( event );
|
| 740 |
-
|
| 741 |
-
}
|
| 742 |
-
|
| 743 |
-
//
|
| 744 |
-
// event handlers - FSM: listen for events and reset state
|
| 745 |
-
//
|
| 746 |
-
|
| 747 |
-
function onPointerDown( event ) {
|
| 748 |
-
|
| 749 |
-
if ( scope.enabled === false ) return;
|
| 750 |
-
if ( pointers.length === 0 ) {
|
| 751 |
-
|
| 752 |
-
scope.domElement.setPointerCapture( event.pointerId );
|
| 753 |
-
scope.domElement.addEventListener( 'pointermove', onPointerMove );
|
| 754 |
-
scope.domElement.addEventListener( 'pointerup', onPointerUp );
|
| 755 |
-
|
| 756 |
-
}
|
| 757 |
-
|
| 758 |
-
//
|
| 759 |
-
|
| 760 |
-
addPointer( event );
|
| 761 |
-
if ( event.pointerType === 'touch' ) {
|
| 762 |
-
|
| 763 |
-
onTouchStart( event );
|
| 764 |
-
|
| 765 |
-
} else {
|
| 766 |
-
|
| 767 |
-
onMouseDown( event );
|
| 768 |
-
|
| 769 |
-
}
|
| 770 |
-
|
| 771 |
-
}
|
| 772 |
-
|
| 773 |
-
function onPointerMove( event ) {
|
| 774 |
-
|
| 775 |
-
if ( scope.enabled === false ) return;
|
| 776 |
-
if ( event.pointerType === 'touch' ) {
|
| 777 |
-
|
| 778 |
-
onTouchMove( event );
|
| 779 |
-
|
| 780 |
-
} else {
|
| 781 |
-
|
| 782 |
-
onMouseMove( event );
|
| 783 |
-
|
| 784 |
-
}
|
| 785 |
-
|
| 786 |
-
}
|
| 787 |
-
|
| 788 |
-
function onPointerUp( event ) {
|
| 789 |
-
|
| 790 |
-
removePointer( event );
|
| 791 |
-
if ( pointers.length === 0 ) {
|
| 792 |
-
|
| 793 |
-
scope.domElement.releasePointerCapture( event.pointerId );
|
| 794 |
-
scope.domElement.removeEventListener( 'pointermove', onPointerMove );
|
| 795 |
-
scope.domElement.removeEventListener( 'pointerup', onPointerUp );
|
| 796 |
-
|
| 797 |
-
}
|
| 798 |
-
|
| 799 |
-
scope.dispatchEvent( _endEvent );
|
| 800 |
-
state = STATE.NONE;
|
| 801 |
-
|
| 802 |
-
}
|
| 803 |
-
|
| 804 |
-
function onPointerCancel( event ) {
|
| 805 |
-
|
| 806 |
-
removePointer( event );
|
| 807 |
-
|
| 808 |
-
}
|
| 809 |
-
|
| 810 |
-
function onMouseDown( event ) {
|
| 811 |
-
|
| 812 |
-
let mouseAction;
|
| 813 |
-
switch ( event.button ) {
|
| 814 |
-
|
| 815 |
-
case 0:
|
| 816 |
-
mouseAction = scope.mouseButtons.LEFT;
|
| 817 |
-
break;
|
| 818 |
-
case 1:
|
| 819 |
-
mouseAction = scope.mouseButtons.MIDDLE;
|
| 820 |
-
break;
|
| 821 |
-
case 2:
|
| 822 |
-
mouseAction = scope.mouseButtons.RIGHT;
|
| 823 |
-
break;
|
| 824 |
-
default:
|
| 825 |
-
mouseAction = - 1;
|
| 826 |
-
|
| 827 |
-
}
|
| 828 |
-
|
| 829 |
-
switch ( mouseAction ) {
|
| 830 |
-
|
| 831 |
-
case THREE.MOUSE.DOLLY:
|
| 832 |
-
if ( scope.enableZoom === false ) return;
|
| 833 |
-
handleMouseDownDolly( event );
|
| 834 |
-
state = STATE.DOLLY;
|
| 835 |
-
break;
|
| 836 |
-
case THREE.MOUSE.ROTATE:
|
| 837 |
-
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
|
| 838 |
-
|
| 839 |
-
if ( scope.enablePan === false ) return;
|
| 840 |
-
handleMouseDownPan( event );
|
| 841 |
-
state = STATE.PAN;
|
| 842 |
-
|
| 843 |
-
} else {
|
| 844 |
-
|
| 845 |
-
if ( scope.enableRotate === false ) return;
|
| 846 |
-
handleMouseDownRotate( event );
|
| 847 |
-
state = STATE.ROTATE;
|
| 848 |
-
|
| 849 |
-
}
|
| 850 |
-
|
| 851 |
-
break;
|
| 852 |
-
case THREE.MOUSE.PAN:
|
| 853 |
-
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
|
| 854 |
-
|
| 855 |
-
if ( scope.enableRotate === false ) return;
|
| 856 |
-
handleMouseDownRotate( event );
|
| 857 |
-
state = STATE.ROTATE;
|
| 858 |
-
|
| 859 |
-
} else {
|
| 860 |
-
|
| 861 |
-
if ( scope.enablePan === false ) return;
|
| 862 |
-
handleMouseDownPan( event );
|
| 863 |
-
state = STATE.PAN;
|
| 864 |
-
|
| 865 |
-
}
|
| 866 |
-
|
| 867 |
-
break;
|
| 868 |
-
default:
|
| 869 |
-
state = STATE.NONE;
|
| 870 |
-
|
| 871 |
-
}
|
| 872 |
-
|
| 873 |
-
if ( state !== STATE.NONE ) {
|
| 874 |
-
|
| 875 |
-
scope.dispatchEvent( _startEvent );
|
| 876 |
-
|
| 877 |
-
}
|
| 878 |
-
|
| 879 |
-
}
|
| 880 |
-
|
| 881 |
-
function onMouseMove( event ) {
|
| 882 |
-
|
| 883 |
-
switch ( state ) {
|
| 884 |
-
|
| 885 |
-
case STATE.ROTATE:
|
| 886 |
-
if ( scope.enableRotate === false ) return;
|
| 887 |
-
handleMouseMoveRotate( event );
|
| 888 |
-
break;
|
| 889 |
-
case STATE.DOLLY:
|
| 890 |
-
if ( scope.enableZoom === false ) return;
|
| 891 |
-
handleMouseMoveDolly( event );
|
| 892 |
-
break;
|
| 893 |
-
case STATE.PAN:
|
| 894 |
-
if ( scope.enablePan === false ) return;
|
| 895 |
-
handleMouseMovePan( event );
|
| 896 |
-
break;
|
| 897 |
-
|
| 898 |
-
}
|
| 899 |
-
|
| 900 |
-
}
|
| 901 |
-
|
| 902 |
-
function onMouseWheel( event ) {
|
| 903 |
-
|
| 904 |
-
if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;
|
| 905 |
-
event.preventDefault();
|
| 906 |
-
scope.dispatchEvent( _startEvent );
|
| 907 |
-
handleMouseWheel( event );
|
| 908 |
-
scope.dispatchEvent( _endEvent );
|
| 909 |
-
|
| 910 |
-
}
|
| 911 |
-
|
| 912 |
-
function onKeyDown( event ) {
|
| 913 |
-
|
| 914 |
-
if ( scope.enabled === false || scope.enablePan === false ) return;
|
| 915 |
-
handleKeyDown( event );
|
| 916 |
-
|
| 917 |
-
}
|
| 918 |
-
|
| 919 |
-
function onTouchStart( event ) {
|
| 920 |
-
|
| 921 |
-
trackPointer( event );
|
| 922 |
-
switch ( pointers.length ) {
|
| 923 |
-
|
| 924 |
-
case 1:
|
| 925 |
-
switch ( scope.touches.ONE ) {
|
| 926 |
-
|
| 927 |
-
case THREE.TOUCH.ROTATE:
|
| 928 |
-
if ( scope.enableRotate === false ) return;
|
| 929 |
-
handleTouchStartRotate();
|
| 930 |
-
state = STATE.TOUCH_ROTATE;
|
| 931 |
-
break;
|
| 932 |
-
case THREE.TOUCH.PAN:
|
| 933 |
-
if ( scope.enablePan === false ) return;
|
| 934 |
-
handleTouchStartPan();
|
| 935 |
-
state = STATE.TOUCH_PAN;
|
| 936 |
-
break;
|
| 937 |
-
default:
|
| 938 |
-
state = STATE.NONE;
|
| 939 |
-
|
| 940 |
-
}
|
| 941 |
-
|
| 942 |
-
break;
|
| 943 |
-
case 2:
|
| 944 |
-
switch ( scope.touches.TWO ) {
|
| 945 |
-
|
| 946 |
-
case THREE.TOUCH.DOLLY_PAN:
|
| 947 |
-
if ( scope.enableZoom === false && scope.enablePan === false ) return;
|
| 948 |
-
handleTouchStartDollyPan();
|
| 949 |
-
state = STATE.TOUCH_DOLLY_PAN;
|
| 950 |
-
break;
|
| 951 |
-
case THREE.TOUCH.DOLLY_ROTATE:
|
| 952 |
-
if ( scope.enableZoom === false && scope.enableRotate === false ) return;
|
| 953 |
-
handleTouchStartDollyRotate();
|
| 954 |
-
state = STATE.TOUCH_DOLLY_ROTATE;
|
| 955 |
-
break;
|
| 956 |
-
default:
|
| 957 |
-
state = STATE.NONE;
|
| 958 |
-
|
| 959 |
-
}
|
| 960 |
-
|
| 961 |
-
break;
|
| 962 |
-
default:
|
| 963 |
-
state = STATE.NONE;
|
| 964 |
-
|
| 965 |
-
}
|
| 966 |
-
|
| 967 |
-
if ( state !== STATE.NONE ) {
|
| 968 |
-
|
| 969 |
-
scope.dispatchEvent( _startEvent );
|
| 970 |
-
|
| 971 |
-
}
|
| 972 |
-
|
| 973 |
-
}
|
| 974 |
-
|
| 975 |
-
function onTouchMove( event ) {
|
| 976 |
-
|
| 977 |
-
trackPointer( event );
|
| 978 |
-
switch ( state ) {
|
| 979 |
-
|
| 980 |
-
case STATE.TOUCH_ROTATE:
|
| 981 |
-
if ( scope.enableRotate === false ) return;
|
| 982 |
-
handleTouchMoveRotate( event );
|
| 983 |
-
scope.update();
|
| 984 |
-
break;
|
| 985 |
-
case STATE.TOUCH_PAN:
|
| 986 |
-
if ( scope.enablePan === false ) return;
|
| 987 |
-
handleTouchMovePan( event );
|
| 988 |
-
scope.update();
|
| 989 |
-
break;
|
| 990 |
-
case STATE.TOUCH_DOLLY_PAN:
|
| 991 |
-
if ( scope.enableZoom === false && scope.enablePan === false ) return;
|
| 992 |
-
handleTouchMoveDollyPan( event );
|
| 993 |
-
scope.update();
|
| 994 |
-
break;
|
| 995 |
-
case STATE.TOUCH_DOLLY_ROTATE:
|
| 996 |
-
if ( scope.enableZoom === false && scope.enableRotate === false ) return;
|
| 997 |
-
handleTouchMoveDollyRotate( event );
|
| 998 |
-
scope.update();
|
| 999 |
-
break;
|
| 1000 |
-
default:
|
| 1001 |
-
state = STATE.NONE;
|
| 1002 |
-
|
| 1003 |
-
}
|
| 1004 |
-
|
| 1005 |
-
}
|
| 1006 |
-
|
| 1007 |
-
function onContextMenu( event ) {
|
| 1008 |
-
|
| 1009 |
-
if ( scope.enabled === false ) return;
|
| 1010 |
-
event.preventDefault();
|
| 1011 |
-
|
| 1012 |
-
}
|
| 1013 |
-
|
| 1014 |
-
function addPointer( event ) {
|
| 1015 |
-
|
| 1016 |
-
pointers.push( event );
|
| 1017 |
-
|
| 1018 |
-
}
|
| 1019 |
-
|
| 1020 |
-
function removePointer( event ) {
|
| 1021 |
-
|
| 1022 |
-
delete pointerPositions[ event.pointerId ];
|
| 1023 |
-
for ( let i = 0; i < pointers.length; i ++ ) {
|
| 1024 |
-
|
| 1025 |
-
if ( pointers[ i ].pointerId == event.pointerId ) {
|
| 1026 |
-
|
| 1027 |
-
pointers.splice( i, 1 );
|
| 1028 |
-
return;
|
| 1029 |
-
|
| 1030 |
-
}
|
| 1031 |
-
|
| 1032 |
-
}
|
| 1033 |
-
|
| 1034 |
-
}
|
| 1035 |
-
|
| 1036 |
-
function trackPointer( event ) {
|
| 1037 |
-
|
| 1038 |
-
let position = pointerPositions[ event.pointerId ];
|
| 1039 |
-
if ( position === undefined ) {
|
| 1040 |
-
|
| 1041 |
-
position = new THREE.Vector2();
|
| 1042 |
-
pointerPositions[ event.pointerId ] = position;
|
| 1043 |
-
|
| 1044 |
-
}
|
| 1045 |
-
|
| 1046 |
-
position.set( event.pageX, event.pageY );
|
| 1047 |
-
|
| 1048 |
-
}
|
| 1049 |
-
|
| 1050 |
-
function getSecondPointerPosition( event ) {
|
| 1051 |
-
|
| 1052 |
-
const pointer = event.pointerId === pointers[ 0 ].pointerId ? pointers[ 1 ] : pointers[ 0 ];
|
| 1053 |
-
return pointerPositions[ pointer.pointerId ];
|
| 1054 |
-
|
| 1055 |
-
}
|
| 1056 |
-
|
| 1057 |
-
//
|
| 1058 |
-
|
| 1059 |
-
scope.domElement.addEventListener( 'contextmenu', onContextMenu );
|
| 1060 |
-
scope.domElement.addEventListener( 'pointerdown', onPointerDown );
|
| 1061 |
-
scope.domElement.addEventListener( 'pointercancel', onPointerCancel );
|
| 1062 |
-
scope.domElement.addEventListener( 'wheel', onMouseWheel, {
|
| 1063 |
-
passive: false
|
| 1064 |
-
} );
|
| 1065 |
-
|
| 1066 |
-
// force an update at start
|
| 1067 |
-
|
| 1068 |
-
this.update();
|
| 1069 |
-
|
| 1070 |
-
}
|
| 1071 |
-
|
| 1072 |
-
}
|
| 1073 |
-
|
| 1074 |
-
// This set of controls performs orbiting, dollying (zooming), and panning.
|
| 1075 |
-
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
|
| 1076 |
-
// This is very similar to OrbitControls, another set of touch behavior
|
| 1077 |
-
//
|
| 1078 |
-
// Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
|
| 1079 |
-
// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
|
| 1080 |
-
// Pan - left mouse, or arrow keys / touch: one-finger move
|
| 1081 |
-
|
| 1082 |
-
class MapControls extends OrbitControls {
|
| 1083 |
-
|
| 1084 |
-
constructor( object, domElement ) {
|
| 1085 |
-
|
| 1086 |
-
super( object, domElement );
|
| 1087 |
-
this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up
|
| 1088 |
-
|
| 1089 |
-
this.mouseButtons.LEFT = THREE.MOUSE.PAN;
|
| 1090 |
-
this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE;
|
| 1091 |
-
this.touches.ONE = THREE.TOUCH.PAN;
|
| 1092 |
-
this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE;
|
| 1093 |
-
|
| 1094 |
-
}
|
| 1095 |
-
|
| 1096 |
-
}
|
| 1097 |
-
|
| 1098 |
-
THREE.MapControls = MapControls;
|
| 1099 |
-
THREE.OrbitControls = OrbitControls;
|
| 1100 |
-
|
| 1101 |
-
} )();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/vendor/d3-hierarchy.v3.min.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// https://d3js.org/d3-hierarchy/ v3.1.2 Copyright 2010-2021 Mike Bostock
|
| 2 |
-
!function(n,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r((n="undefined"!=typeof globalThis?globalThis:n||self).d3=n.d3||{})}(this,(function(n){"use strict";function r(n,r){return n.parent===r.parent?1:2}function t(n,r){return n+r.x}function e(n,r){return Math.max(n,r.y)}function i(n){var r=0,t=n.children,e=t&&t.length;if(e)for(;--e>=0;)r+=t[e].value;else r=1;n.value=r}function u(n,r){n instanceof Map?(n=[void 0,n],void 0===r&&(r=f)):void 0===r&&(r=o);for(var t,e,i,u,a,l=new h(n),p=[l];t=p.pop();)if((i=r(t.data))&&(a=(i=Array.from(i)).length))for(t.children=i,u=a-1;u>=0;--u)p.push(e=i[u]=new h(i[u])),e.parent=t,e.depth=t.depth+1;return l.eachBefore(c)}function o(n){return n.children}function f(n){return Array.isArray(n)?n[1]:null}function a(n){void 0!==n.data.value&&(n.value=n.data.value),n.data=n.data.data}function c(n){var r=0;do{n.height=r}while((n=n.parent)&&n.height<++r)}function h(n){this.data=n,this.depth=this.height=0,this.parent=null}function l(n){return null==n?null:p(n)}function p(n){if("function"!=typeof n)throw new Error;return n}function d(){return 0}function s(n){return function(){return n}}h.prototype=u.prototype={constructor:h,count:function(){return this.eachAfter(i)},each:function(n,r){let t=-1;for(const e of this)n.call(r,e,++t,this);return this},eachAfter:function(n,r){for(var t,e,i,u=this,o=[u],f=[],a=-1;u=o.pop();)if(f.push(u),t=u.children)for(e=0,i=t.length;e<i;++e)o.push(t[e]);for(;u=f.pop();)n.call(r,u,++a,this);return this},eachBefore:function(n,r){for(var t,e,i=this,u=[i],o=-1;i=u.pop();)if(n.call(r,i,++o,this),t=i.children)for(e=t.length-1;e>=0;--e)u.push(t[e]);return this},find:function(n,r){let t=-1;for(const e of this)if(n.call(r,e,++t,this))return e},sum:function(n){return this.eachAfter((function(r){for(var t=+n(r.data)||0,e=r.children,i=e&&e.length;--i>=0;)t+=e[i].value;r.value=t}))},sort:function(n){return this.eachBefore((function(r){r.children&&r.children.sort(n)}))},path:function(n){for(var r=this,t=function(n,r){if(n===r)return n;var t=n.ancestors(),e=r.ancestors(),i=null;n=t.pop(),r=e.pop();for(;n===r;)i=n,n=t.pop(),r=e.pop();return i}(r,n),e=[r];r!==t;)r=r.parent,e.push(r);for(var i=e.length;n!==t;)e.splice(i,0,n),n=n.parent;return e},ancestors:function(){for(var n=this,r=[n];n=n.parent;)r.push(n);return r},descendants:function(){return Array.from(this)},leaves:function(){var n=[];return this.eachBefore((function(r){r.children||n.push(r)})),n},links:function(){var n=this,r=[];return n.each((function(t){t!==n&&r.push({source:t.parent,target:t})})),r},copy:function(){return u(this).eachBefore(a)},[Symbol.iterator]:function*(){var n,r,t,e,i=this,u=[i];do{for(n=u.reverse(),u=[];i=n.pop();)if(yield i,r=i.children)for(t=0,e=r.length;t<e;++t)u.push(r[t])}while(u.length)}};const v=4294967296;function x(){let n=1;return()=>(n=(1664525*n+1013904223)%v)/v}function y(n,r){for(var t,e,i=0,u=(n=function(n,r){let t,e,i=n.length;for(;i;)e=r()*i--|0,t=n[i],n[i]=n[e],n[e]=t;return n}(Array.from(n),r)).length,o=[];i<u;)t=n[i],e&&w(e,t)?++i:(e=M(o=g(o,t)),i=0);return e}function g(n,r){var t,e;if(_(r,n))return[r];for(t=0;t<n.length;++t)if(m(r,n[t])&&_(z(n[t],r),n))return[n[t],r];for(t=0;t<n.length-1;++t)for(e=t+1;e<n.length;++e)if(m(z(n[t],n[e]),r)&&m(z(n[t],r),n[e])&&m(z(n[e],r),n[t])&&_(B(n[t],n[e],r),n))return[n[t],n[e],r];throw new Error}function m(n,r){var t=n.r-r.r,e=r.x-n.x,i=r.y-n.y;return t<0||t*t<e*e+i*i}function w(n,r){var t=n.r-r.r+1e-9*Math.max(n.r,r.r,1),e=r.x-n.x,i=r.y-n.y;return t>0&&t*t>e*e+i*i}function _(n,r){for(var t=0;t<r.length;++t)if(!w(n,r[t]))return!1;return!0}function M(n){switch(n.length){case 1:return function(n){return{x:n.x,y:n.y,r:n.r}}(n[0]);case 2:return z(n[0],n[1]);case 3:return B(n[0],n[1],n[2])}}function z(n,r){var t=n.x,e=n.y,i=n.r,u=r.x,o=r.y,f=r.r,a=u-t,c=o-e,h=f-i,l=Math.sqrt(a*a+c*c);return{x:(t+u+a/l*h)/2,y:(e+o+c/l*h)/2,r:(l+i+f)/2}}function B(n,r,t){var e=n.x,i=n.y,u=n.r,o=r.x,f=r.y,a=r.r,c=t.x,h=t.y,l=t.r,p=e-o,d=e-c,s=i-f,v=i-h,x=a-u,y=l-u,g=e*e+i*i-u*u,m=g-o*o-f*f+a*a,w=g-c*c-h*h+l*l,_=d*s-p*v,M=(s*w-v*m)/(2*_)-e,z=(v*x-s*y)/_,B=(d*m-p*w)/(2*_)-i,A=(p*y-d*x)/_,b=z*z+A*A-1,q=2*(u+M*z+B*A),E=M*M+B*B-u*u,S=-(Math.abs(b)>1e-6?(q+Math.sqrt(q*q-4*b*E))/(2*b):E/q);return{x:e+M+z*S,y:i+B+A*S,r:S}}function A(n,r,t){var e,i,u,o,f=n.x-r.x,a=n.y-r.y,c=f*f+a*a;c?(i=r.r+t.r,i*=i,o=n.r+t.r,i>(o*=o)?(e=(c+o-i)/(2*c),u=Math.sqrt(Math.max(0,o/c-e*e)),t.x=n.x-e*f-u*a,t.y=n.y-e*a+u*f):(e=(c+i-o)/(2*c),u=Math.sqrt(Math.max(0,i/c-e*e)),t.x=r.x+e*f-u*a,t.y=r.y+e*a+u*f)):(t.x=r.x+t.r,t.y=r.y)}function b(n,r){var t=n.r+r.r-1e-6,e=r.x-n.x,i=r.y-n.y;return t>0&&t*t>e*e+i*i}function q(n){var r=n._,t=n.next._,e=r.r+t.r,i=(r.x*t.r+t.x*r.r)/e,u=(r.y*t.r+t.y*r.r)/e;return i*i+u*u}function Node(n){this._=n,this.next=null,this.previous=null}function E(n,r){if(!(o=(t=n,n="object"==typeof t&&"length"in t?t:Array.from(t)).length))return 0;var t,e,i,u,o,f,a,c,h,l,p,d;if((e=n[0]).x=0,e.y=0,!(o>1))return e.r;if(i=n[1],e.x=-i.r,i.x=e.r,i.y=0,!(o>2))return e.r+i.r;A(i,e,u=n[2]),e=new Node(e),i=new Node(i),u=new Node(u),e.next=u.previous=i,i.next=e.previous=u,u.next=i.previous=e;n:for(c=3;c<o;++c){A(e._,i._,u=n[c]),u=new Node(u),h=i.next,l=e.previous,p=i._.r,d=e._.r;do{if(p<=d){if(b(h._,u._)){i=h,e.next=i,i.previous=e,--c;continue n}p+=h._.r,h=h.next}else{if(b(l._,u._)){(e=l).next=i,i.previous=e,--c;continue n}d+=l._.r,l=l.previous}}while(h!==l.next);for(u.previous=e,u.next=i,e.next=i.previous=i=u,f=q(e);(u=u.next)!==i;)(a=q(u))<f&&(e=u,f=a);i=e.next}for(e=[i._],u=i;(u=u.next)!==i;)e.push(u._);for(u=y(e,r),c=0;c<o;++c)(e=n[c]).x-=u.x,e.y-=u.y;return u.r}function S(n){return Math.sqrt(n.value)}function k(n){return function(r){r.children||(r.r=Math.max(0,+n(r)||0))}}function I(n,r,t){return function(e){if(i=e.children){var i,u,o,f=i.length,a=n(e)*r||0;if(a)for(u=0;u<f;++u)i[u].r+=a;if(o=E(i,t),a)for(u=0;u<f;++u)i[u].r-=a;e.r=o+a}}}function T(n){return function(r){var t=r.parent;r.r*=n,t&&(r.x=t.x+n*r.x,r.y=t.y+n*r.y)}}function j(n){n.x0=Math.round(n.x0),n.y0=Math.round(n.y0),n.x1=Math.round(n.x1),n.y1=Math.round(n.y1)}function O(n,r,t,e,i){for(var u,o=n.children,f=-1,a=o.length,c=n.value&&(e-r)/n.value;++f<a;)(u=o[f]).y0=t,u.y1=i,u.x0=r,u.x1=r+=u.value*c}var R={depth:-1},D={},L={};function $(n){return n.id}function N(n){return n.parentId}function P(n){let r=n.length;if(r<2)return"";for(;--r>1&&!C(n,r););return n.slice(0,r)}function C(n,r){if("/"===n[r]){let t=0;for(;r>0&&"\\"===n[--r];)++t;if(0==(1&t))return!0}return!1}function F(n,r){return n.parent===r.parent?1:2}function G(n){var r=n.children;return r?r[0]:n.t}function H(n){var r=n.children;return r?r[r.length-1]:n.t}function J(n,r,t){var e=t/(r.i-n.i);r.c-=e,r.s+=t,n.c+=e,r.z+=t,r.m+=t}function K(n,r,t){return n.a.parent===r.parent?n.a:t}function Q(n,r){this._=n,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=r}function U(n,r,t,e,i){for(var u,o=n.children,f=-1,a=o.length,c=n.value&&(i-t)/n.value;++f<a;)(u=o[f]).x0=r,u.x1=e,u.y0=t,u.y1=t+=u.value*c}Q.prototype=Object.create(h.prototype);var V=(1+Math.sqrt(5))/2;function W(n,r,t,e,i,u){for(var o,f,a,c,h,l,p,d,s,v,x,y=[],g=r.children,m=0,w=0,_=g.length,M=r.value;m<_;){a=i-t,c=u-e;do{h=g[w++].value}while(!h&&w<_);for(l=p=h,x=h*h*(v=Math.max(c/a,a/c)/(M*n)),s=Math.max(p/x,x/l);w<_;++w){if(h+=f=g[w].value,f<l&&(l=f),f>p&&(p=f),x=h*h*v,(d=Math.max(p/x,x/l))>s){h-=f;break}s=d}y.push(o={value:h,dice:a<c,children:g.slice(m,w)}),o.dice?O(o,t,e,i,M?e+=c*h/M:u):U(o,t,e,M?t+=a*h/M:i,u),M-=h,m=w}return y}var X=function n(r){function t(n,t,e,i,u){W(r,n,t,e,i,u)}return t.ratio=function(r){return n((r=+r)>1?r:1)},t}(V);var Y=function n(r){function t(n,t,e,i,u){if((o=n._squarify)&&o.ratio===r)for(var o,f,a,c,h,l=-1,p=o.length,d=n.value;++l<p;){for(a=(f=o[l]).children,c=f.value=0,h=a.length;c<h;++c)f.value+=a[c].value;f.dice?O(f,t,e,i,d?e+=(u-e)*f.value/d:u):U(f,t,e,d?t+=(i-t)*f.value/d:i,u),d-=f.value}else n._squarify=o=W(r,n,t,e,i,u),o.ratio=r}return t.ratio=function(r){return n((r=+r)>1?r:1)},t}(V);n.Node=h,n.cluster=function(){var n=r,i=1,u=1,o=!1;function f(r){var f,a=0;r.eachAfter((function(r){var i=r.children;i?(r.x=function(n){return n.reduce(t,0)/n.length}(i),r.y=function(n){return 1+n.reduce(e,0)}(i)):(r.x=f?a+=n(r,f):0,r.y=0,f=r)}));var c=function(n){for(var r;r=n.children;)n=r[0];return n}(r),h=function(n){for(var r;r=n.children;)n=r[r.length-1];return n}(r),l=c.x-n(c,h)/2,p=h.x+n(h,c)/2;return r.eachAfter(o?function(n){n.x=(n.x-r.x)*i,n.y=(r.y-n.y)*u}:function(n){n.x=(n.x-l)/(p-l)*i,n.y=(1-(r.y?n.y/r.y:1))*u})}return f.separation=function(r){return arguments.length?(n=r,f):n},f.size=function(n){return arguments.length?(o=!1,i=+n[0],u=+n[1],f):o?null:[i,u]},f.nodeSize=function(n){return arguments.length?(o=!0,i=+n[0],u=+n[1],f):o?[i,u]:null},f},n.hierarchy=u,n.pack=function(){var n=null,r=1,t=1,e=d;function i(i){const u=x();return i.x=r/2,i.y=t/2,n?i.eachBefore(k(n)).eachAfter(I(e,.5,u)).eachBefore(T(1)):i.eachBefore(k(S)).eachAfter(I(d,1,u)).eachAfter(I(e,i.r/Math.min(r,t),u)).eachBefore(T(Math.min(r,t)/(2*i.r))),i}return i.radius=function(r){return arguments.length?(n=l(r),i):n},i.size=function(n){return arguments.length?(r=+n[0],t=+n[1],i):[r,t]},i.padding=function(n){return arguments.length?(e="function"==typeof n?n:s(+n),i):e},i},n.packEnclose=function(n){return y(n,x())},n.packSiblings=function(n){return E(n,x()),n},n.partition=function(){var n=1,r=1,t=0,e=!1;function i(i){var u=i.height+1;return i.x0=i.y0=t,i.x1=n,i.y1=r/u,i.eachBefore(function(n,r){return function(e){e.children&&O(e,e.x0,n*(e.depth+1)/r,e.x1,n*(e.depth+2)/r);var i=e.x0,u=e.y0,o=e.x1-t,f=e.y1-t;o<i&&(i=o=(i+o)/2),f<u&&(u=f=(u+f)/2),e.x0=i,e.y0=u,e.x1=o,e.y1=f}}(r,u)),e&&i.eachBefore(j),i}return i.round=function(n){return arguments.length?(e=!!n,i):e},i.size=function(t){return arguments.length?(n=+t[0],r=+t[1],i):[n,r]},i.padding=function(n){return arguments.length?(t=+n,i):t},i},n.stratify=function(){var n,r=$,t=N;function e(e){var i,u,o,f,a,l,p,d,s=Array.from(e),v=r,x=t,y=new Map;if(null!=n){const r=s.map(((r,t)=>function(n){let r=(n=`${n}`).length;C(n,r-1)&&!C(n,r-2)&&(n=n.slice(0,-1));return"/"===n[0]?n:`/${n}`}(n(r,t,e)))),t=r.map(P),i=new Set(r).add("");for(const n of t)i.has(n)||(i.add(n),r.push(n),t.push(P(n)),s.push(L));v=(n,t)=>r[t],x=(n,r)=>t[r]}for(o=0,i=s.length;o<i;++o)u=s[o],l=s[o]=new h(u),null!=(p=v(u,o,e))&&(p+="")&&(d=l.id=p,y.set(d,y.has(d)?D:l)),null!=(p=x(u,o,e))&&(p+="")&&(l.parent=p);for(o=0;o<i;++o)if(p=(l=s[o]).parent){if(!(a=y.get(p)))throw new Error("missing: "+p);if(a===D)throw new Error("ambiguous: "+p);a.children?a.children.push(l):a.children=[l],l.parent=a}else{if(f)throw new Error("multiple roots");f=l}if(!f)throw new Error("no root");if(null!=n){for(;f.data===L&&1===f.children.length;)f=f.children[0],--i;for(let n=s.length-1;n>=0&&(l=s[n],l.data===L);--n)l.data=null}if(f.parent=R,f.eachBefore((function(n){n.depth=n.parent.depth+1,--i})).eachBefore(c),f.parent=null,i>0)throw new Error("cycle");return f}return e.id=function(n){return arguments.length?(r=l(n),e):r},e.parentId=function(n){return arguments.length?(t=l(n),e):t},e.path=function(r){return arguments.length?(n=l(r),e):n},e},n.tree=function(){var n=F,r=1,t=1,e=null;function i(i){var a=function(n){for(var r,t,e,i,u,o=new Q(n,0),f=[o];r=f.pop();)if(e=r._.children)for(r.children=new Array(u=e.length),i=u-1;i>=0;--i)f.push(t=r.children[i]=new Q(e[i],i)),t.parent=r;return(o.parent=new Q(null,0)).children=[o],o}(i);if(a.eachAfter(u),a.parent.m=-a.z,a.eachBefore(o),e)i.eachBefore(f);else{var c=i,h=i,l=i;i.eachBefore((function(n){n.x<c.x&&(c=n),n.x>h.x&&(h=n),n.depth>l.depth&&(l=n)}));var p=c===h?1:n(c,h)/2,d=p-c.x,s=r/(h.x+p+d),v=t/(l.depth||1);i.eachBefore((function(n){n.x=(n.x+d)*s,n.y=n.depth*v}))}return i}function u(r){var t=r.children,e=r.parent.children,i=r.i?e[r.i-1]:null;if(t){!function(n){for(var r,t=0,e=0,i=n.children,u=i.length;--u>=0;)(r=i[u]).z+=t,r.m+=t,t+=r.s+(e+=r.c)}(r);var u=(t[0].z+t[t.length-1].z)/2;i?(r.z=i.z+n(r._,i._),r.m=r.z-u):r.z=u}else i&&(r.z=i.z+n(r._,i._));r.parent.A=function(r,t,e){if(t){for(var i,u=r,o=r,f=t,a=u.parent.children[0],c=u.m,h=o.m,l=f.m,p=a.m;f=H(f),u=G(u),f&&u;)a=G(a),(o=H(o)).a=r,(i=f.z+l-u.z-c+n(f._,u._))>0&&(J(K(f,r,e),r,i),c+=i,h+=i),l+=f.m,c+=u.m,p+=a.m,h+=o.m;f&&!H(o)&&(o.t=f,o.m+=l-h),u&&!G(a)&&(a.t=u,a.m+=c-p,e=r)}return e}(r,i,r.parent.A||e[0])}function o(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function f(n){n.x*=r,n.y=n.depth*t}return i.separation=function(r){return arguments.length?(n=r,i):n},i.size=function(n){return arguments.length?(e=!1,r=+n[0],t=+n[1],i):e?null:[r,t]},i.nodeSize=function(n){return arguments.length?(e=!0,r=+n[0],t=+n[1],i):e?[r,t]:null},i},n.treemap=function(){var n=X,r=!1,t=1,e=1,i=[0],u=d,o=d,f=d,a=d,c=d;function h(n){return n.x0=n.y0=0,n.x1=t,n.y1=e,n.eachBefore(l),i=[0],r&&n.eachBefore(j),n}function l(r){var t=i[r.depth],e=r.x0+t,h=r.y0+t,l=r.x1-t,p=r.y1-t;l<e&&(e=l=(e+l)/2),p<h&&(h=p=(h+p)/2),r.x0=e,r.y0=h,r.x1=l,r.y1=p,r.children&&(t=i[r.depth+1]=u(r)/2,e+=c(r)-t,h+=o(r)-t,(l-=f(r)-t)<e&&(e=l=(e+l)/2),(p-=a(r)-t)<h&&(h=p=(h+p)/2),n(r,e,h,l,p))}return h.round=function(n){return arguments.length?(r=!!n,h):r},h.size=function(n){return arguments.length?(t=+n[0],e=+n[1],h):[t,e]},h.tile=function(r){return arguments.length?(n=p(r),h):n},h.padding=function(n){return arguments.length?h.paddingInner(n).paddingOuter(n):h.paddingInner()},h.paddingInner=function(n){return arguments.length?(u="function"==typeof n?n:s(+n),h):u},h.paddingOuter=function(n){return arguments.length?h.paddingTop(n).paddingRight(n).paddingBottom(n).paddingLeft(n):h.paddingTop()},h.paddingTop=function(n){return arguments.length?(o="function"==typeof n?n:s(+n),h):o},h.paddingRight=function(n){return arguments.length?(f="function"==typeof n?n:s(+n),h):f},h.paddingBottom=function(n){return arguments.length?(a="function"==typeof n?n:s(+n),h):a},h.paddingLeft=function(n){return arguments.length?(c="function"==typeof n?n:s(+n),h):c},h},n.treemapBinary=function(n,r,t,e,i){var u,o,f=n.children,a=f.length,c=new Array(a+1);for(c[0]=o=u=0;u<a;++u)c[u+1]=o+=f[u].value;!function n(r,t,e,i,u,o,a){if(r>=t-1){var h=f[r];return h.x0=i,h.y0=u,h.x1=o,void(h.y1=a)}var l=c[r],p=e/2+l,d=r+1,s=t-1;for(;d<s;){var v=d+s>>>1;c[v]<p?d=v+1:s=v}p-c[d-1]<c[d]-p&&r+1<d&&--d;var x=c[d]-l,y=e-x;if(o-i>a-u){var g=e?(i*y+o*x)/e:o;n(r,d,x,i,u,g,a),n(d,t,y,g,u,o,a)}else{var m=e?(u*y+a*x)/e:a;n(r,d,x,i,u,o,m),n(d,t,y,i,m,o,a)}}(0,a,n.value,r,t,e,i)},n.treemapDice=O,n.treemapResquarify=Y,n.treemapSlice=U,n.treemapSliceDice=function(n,r,t,e,i){(1&n.depth?U:O)(n,r,t,e,i)},n.treemapSquarify=X,Object.defineProperty(n,"__esModule",{value:!0})}));
|
|
|
|
|
|
|
|
|
frontend/vendor/d3-path.v3.min.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// https://d3js.org/d3-path/ v3.1.0 Copyright 2015-2022 Mike Bostock
|
| 2 |
-
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";const i=Math.PI,s=2*i,h=1e-6,e=s-h;function n(t){this._+=t[0];for(let i=1,s=t.length;i<s;++i)this._+=arguments[i]+t[i]}class _{constructor(t){this._x0=this._y0=this._x1=this._y1=null,this._="",this._append=null==t?n:function(t){let i=Math.floor(t);if(!(i>=0))throw new Error(`invalid digits: ${t}`);if(i>15)return n;const s=10**i;return function(t){this._+=t[0];for(let i=1,h=t.length;i<h;++i)this._+=Math.round(arguments[i]*s)/s+t[i]}}(t)}moveTo(t,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+i}`}closePath(){null!==this._x1&&(this._x1=this._x0,this._y1=this._y0,this._append`Z`)}lineTo(t,i){this._append`L${this._x1=+t},${this._y1=+i}`}quadraticCurveTo(t,i,s,h){this._append`Q${+t},${+i},${this._x1=+s},${this._y1=+h}`}bezierCurveTo(t,i,s,h,e,n){this._append`C${+t},${+i},${+s},${+h},${this._x1=+e},${this._y1=+n}`}arcTo(t,s,e,n,_){if(t=+t,s=+s,e=+e,n=+n,(_=+_)<0)throw new Error(`negative radius: ${_}`);let a=this._x1,$=this._y1,o=e-t,r=n-s,p=a-t,d=$-s,l=p*p+d*d;if(null===this._x1)this._append`M${this._x1=t},${this._y1=s}`;else if(l>h)if(Math.abs(d*o-r*p)>h&&_){let u=e-a,f=n-$,x=o*o+r*r,y=u*u+f*f,c=Math.sqrt(x),M=Math.sqrt(l),b=_*Math.tan((i-Math.acos((x+l-y)/(2*c*M)))/2),g=b/M,w=b/c;Math.abs(g-1)>h&&this._append`L${t+g*p},${s+g*d}`,this._append`A${_},${_},0,0,${+(d*u>p*f)},${this._x1=t+w*o},${this._y1=s+w*r}`}else this._append`L${this._x1=t},${this._y1=s}`;else;}arc(t,n,_,a,$,o){if(t=+t,n=+n,o=!!o,(_=+_)<0)throw new Error(`negative radius: ${_}`);let r=_*Math.cos(a),p=_*Math.sin(a),d=t+r,l=n+p,u=1^o,f=o?a-$:$-a;null===this._x1?this._append`M${d},${l}`:(Math.abs(this._x1-d)>h||Math.abs(this._y1-l)>h)&&this._append`L${d},${l}`,_&&(f<0&&(f=f%s+s),f>e?this._append`A${_},${_},0,1,${u},${t-r},${n-p}A${_},${_},0,1,${u},${this._x1=d},${this._y1=l}`:f>h&&this._append`A${_},${_},0,${+(f>=i)},${u},${this._x1=t+_*Math.cos($)},${this._y1=n+_*Math.sin($)}`)}rect(t,i,s,h){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+i}h${s=+s}v${+h}h${-s}Z`}toString(){return this._}}function a(){return new _}a.prototype=_.prototype,t.Path=_,t.path=a,t.pathRound=function(t=3){return new _(+t)}}));
|
|
|
|
|
|
|
|
|
frontend/vendor/d3-shape.v3.min.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// https://d3js.org/d3-shape/ v3.2.0 Copyright 2010-2022 Mike Bostock
|
| 2 |
-
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("d3-path")):"function"==typeof define&&define.amd?define(["exports","d3-path"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{},t.d3)}(this,(function(t,n){"use strict";function i(t){return function(){return t}}const e=Math.abs,s=Math.atan2,o=Math.cos,h=Math.max,_=Math.min,r=Math.sin,a=Math.sqrt,l=1e-12,c=Math.PI,u=c/2,f=2*c;function y(t){return t>1?0:t<-1?c:Math.acos(t)}function x(t){return t>=1?u:t<=-1?-u:Math.asin(t)}function p(t){let i=3;return t.digits=function(n){if(!arguments.length)return i;if(null==n)i=null;else{const t=Math.floor(n);if(!(t>=0))throw new RangeError(`invalid digits: ${n}`);i=t}return t},()=>new n.Path(i)}function v(t){return t.innerRadius}function d(t){return t.outerRadius}function T(t){return t.startAngle}function g(t){return t.endAngle}function m(t){return t&&t.padAngle}function b(t,n,i,e,s,o,h,_){var r=i-t,a=e-n,c=h-s,u=_-o,f=u*r-c*a;if(!(f*f<l))return[t+(f=(c*(n-o)-u*(t-s))/f)*r,n+f*a]}function w(t,n,i,e,s,o,_){var r=t-i,l=n-e,c=(_?o:-o)/a(r*r+l*l),u=c*l,f=-c*r,y=t+u,x=n+f,p=i+u,v=e+f,d=(y+p)/2,T=(x+v)/2,g=p-y,m=v-x,b=g*g+m*m,w=s-o,k=y*v-p*x,N=(m<0?-1:1)*a(h(0,w*w*b-k*k)),S=(k*m-g*N)/b,E=(-k*g-m*N)/b,A=(k*m+g*N)/b,P=(-k*g+m*N)/b,M=S-d,C=E-T,R=A-d,O=P-T;return M*M+C*C>R*R+O*O&&(S=A,E=P),{cx:S,cy:E,x01:-u,y01:-f,x11:S*(s/w-1),y11:E*(s/w-1)}}var k=Array.prototype.slice;function N(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}function S(t){this._context=t}function E(t){return new S(t)}function A(t){return t[0]}function P(t){return t[1]}function M(t,n){var e=i(!0),s=null,o=E,h=null,_=p(r);function r(i){var r,a,l,c=(i=N(i)).length,u=!1;for(null==s&&(h=o(l=_())),r=0;r<=c;++r)!(r<c&&e(a=i[r],r,i))===u&&((u=!u)?h.lineStart():h.lineEnd()),u&&h.point(+t(a,r,i),+n(a,r,i));if(l)return h=null,l+""||null}return t="function"==typeof t?t:void 0===t?A:i(t),n="function"==typeof n?n:void 0===n?P:i(n),r.x=function(n){return arguments.length?(t="function"==typeof n?n:i(+n),r):t},r.y=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),r):n},r.defined=function(t){return arguments.length?(e="function"==typeof t?t:i(!!t),r):e},r.curve=function(t){return arguments.length?(o=t,null!=s&&(h=o(s)),r):o},r.context=function(t){return arguments.length?(null==t?s=h=null:h=o(s=t),r):s},r}function C(t,n,e){var s=null,o=i(!0),h=null,_=E,r=null,a=p(l);function l(i){var l,c,u,f,y,x=(i=N(i)).length,p=!1,v=new Array(x),d=new Array(x);for(null==h&&(r=_(y=a())),l=0;l<=x;++l){if(!(l<x&&o(f=i[l],l,i))===p)if(p=!p)c=l,r.areaStart(),r.lineStart();else{for(r.lineEnd(),r.lineStart(),u=l-1;u>=c;--u)r.point(v[u],d[u]);r.lineEnd(),r.areaEnd()}p&&(v[l]=+t(f,l,i),d[l]=+n(f,l,i),r.point(s?+s(f,l,i):v[l],e?+e(f,l,i):d[l]))}if(y)return r=null,y+""||null}function c(){return M().defined(o).curve(_).context(h)}return t="function"==typeof t?t:void 0===t?A:i(+t),n="function"==typeof n?n:i(void 0===n?0:+n),e="function"==typeof e?e:void 0===e?P:i(+e),l.x=function(n){return arguments.length?(t="function"==typeof n?n:i(+n),s=null,l):t},l.x0=function(n){return arguments.length?(t="function"==typeof n?n:i(+n),l):t},l.x1=function(t){return arguments.length?(s=null==t?null:"function"==typeof t?t:i(+t),l):s},l.y=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),e=null,l):n},l.y0=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),l):n},l.y1=function(t){return arguments.length?(e=null==t?null:"function"==typeof t?t:i(+t),l):e},l.lineX0=l.lineY0=function(){return c().x(t).y(n)},l.lineY1=function(){return c().x(t).y(e)},l.lineX1=function(){return c().x(s).y(n)},l.defined=function(t){return arguments.length?(o="function"==typeof t?t:i(!!t),l):o},l.curve=function(t){return arguments.length?(_=t,null!=h&&(r=_(h)),l):_},l.context=function(t){return arguments.length?(null==t?h=r=null:r=_(h=t),l):h},l}function R(t,n){return n<t?-1:n>t?1:n>=t?0:NaN}function O(t){return t}S.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var z=Y(E);function X(t){this._curve=t}function Y(t){function n(n){return new X(t(n))}return n._curve=t,n}function q(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Y(t)):n()._curve},t}function B(){return q(M().curve(z))}function D(){var t=C().curve(z),n=t.curve,i=t.lineX0,e=t.lineX1,s=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return q(i())},delete t.lineX0,t.lineEndAngle=function(){return q(e())},delete t.lineX1,t.lineInnerRadius=function(){return q(s())},delete t.lineY0,t.lineOuterRadius=function(){return q(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Y(t)):n()._curve},t}function I(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}X.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};class j{constructor(t,n){this._context=t,this._x=n}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line}point(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,n,t,n):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+n)/2,t,this._y0,t,n)}this._x0=t,this._y0=n}}class L{constructor(t){this._context=t}lineStart(){this._point=0}lineEnd(){}point(t,n){if(t=+t,n=+n,0===this._point)this._point=1;else{const i=I(this._x0,this._y0),e=I(this._x0,this._y0=(this._y0+n)/2),s=I(t,this._y0),o=I(t,n);this._context.moveTo(...i),this._context.bezierCurveTo(...e,...s,...o)}this._x0=t,this._y0=n}}function V(t){return new j(t,!0)}function W(t){return new j(t,!1)}function F(t){return new L(t)}function H(t){return t.source}function $(t){return t.target}function G(t){let n=H,e=$,s=A,o=P,h=null,_=null,r=p(a);function a(){let i;const a=k.call(arguments),l=n.apply(this,a),c=e.apply(this,a);if(null==h&&(_=t(i=r())),_.lineStart(),a[0]=l,_.point(+s.apply(this,a),+o.apply(this,a)),a[0]=c,_.point(+s.apply(this,a),+o.apply(this,a)),_.lineEnd(),i)return _=null,i+""||null}return a.source=function(t){return arguments.length?(n=t,a):n},a.target=function(t){return arguments.length?(e=t,a):e},a.x=function(t){return arguments.length?(s="function"==typeof t?t:i(+t),a):s},a.y=function(t){return arguments.length?(o="function"==typeof t?t:i(+t),a):o},a.context=function(n){return arguments.length?(null==n?h=_=null:_=t(h=n),a):h},a}const J=a(3);var K={draw(t,n){const i=.59436*a(n+_(n/28,.75)),e=i/2,s=e*J;t.moveTo(0,i),t.lineTo(0,-i),t.moveTo(-s,-e),t.lineTo(s,e),t.moveTo(-s,e),t.lineTo(s,-e)}},Q={draw(t,n){const i=a(n/c);t.moveTo(i,0),t.arc(0,0,i,0,f)}},U={draw(t,n){const i=a(n/5)/2;t.moveTo(-3*i,-i),t.lineTo(-i,-i),t.lineTo(-i,-3*i),t.lineTo(i,-3*i),t.lineTo(i,-i),t.lineTo(3*i,-i),t.lineTo(3*i,i),t.lineTo(i,i),t.lineTo(i,3*i),t.lineTo(-i,3*i),t.lineTo(-i,i),t.lineTo(-3*i,i),t.closePath()}};const Z=a(1/3),tt=2*Z;var nt={draw(t,n){const i=a(n/tt),e=i*Z;t.moveTo(0,-i),t.lineTo(e,0),t.lineTo(0,i),t.lineTo(-e,0),t.closePath()}},it={draw(t,n){const i=.62625*a(n);t.moveTo(0,-i),t.lineTo(i,0),t.lineTo(0,i),t.lineTo(-i,0),t.closePath()}},et={draw(t,n){const i=.87559*a(n-_(n/7,2));t.moveTo(-i,0),t.lineTo(i,0),t.moveTo(0,i),t.lineTo(0,-i)}},st={draw(t,n){const i=a(n),e=-i/2;t.rect(e,e,i,i)}},ot={draw(t,n){const i=.4431*a(n);t.moveTo(i,i),t.lineTo(i,-i),t.lineTo(-i,-i),t.lineTo(-i,i),t.closePath()}};const ht=r(c/10)/r(7*c/10),_t=r(f/10)*ht,rt=-o(f/10)*ht;var at={draw(t,n){const i=a(.8908130915292852*n),e=_t*i,s=rt*i;t.moveTo(0,-i),t.lineTo(e,s);for(let n=1;n<5;++n){const h=f*n/5,_=o(h),a=r(h);t.lineTo(a*i,-_*i),t.lineTo(_*e-a*s,a*e+_*s)}t.closePath()}};const lt=a(3);var ct={draw(t,n){const i=-a(n/(3*lt));t.moveTo(0,2*i),t.lineTo(-lt*i,-i),t.lineTo(lt*i,-i),t.closePath()}};const ut=a(3);var ft={draw(t,n){const i=.6824*a(n),e=i/2,s=i*ut/2;t.moveTo(0,-i),t.lineTo(s,e),t.lineTo(-s,e),t.closePath()}};const yt=-.5,xt=a(3)/2,pt=1/a(12),vt=3*(pt/2+1);var dt={draw(t,n){const i=a(n/vt),e=i/2,s=i*pt,o=e,h=i*pt+i,_=-o,r=h;t.moveTo(e,s),t.lineTo(o,h),t.lineTo(_,r),t.lineTo(yt*e-xt*s,xt*e+yt*s),t.lineTo(yt*o-xt*h,xt*o+yt*h),t.lineTo(yt*_-xt*r,xt*_+yt*r),t.lineTo(yt*e+xt*s,yt*s-xt*e),t.lineTo(yt*o+xt*h,yt*h-xt*o),t.lineTo(yt*_+xt*r,yt*r-xt*_),t.closePath()}},Tt={draw(t,n){const i=.6189*a(n-_(n/6,1.7));t.moveTo(-i,-i),t.lineTo(i,i),t.moveTo(-i,i),t.lineTo(i,-i)}};const gt=[Q,U,nt,st,at,ct,dt],mt=[Q,et,Tt,ft,K,ot,it];function bt(){}function wt(t,n,i){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+i)/6)}function kt(t){this._context=t}function Nt(t){this._context=t}function St(t){this._context=t}function Et(t,n){this._basis=new kt(t),this._beta=n}kt.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:wt(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:wt(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Nt.prototype={areaStart:bt,areaEnd:bt,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:wt(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},St.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var i=(this._x0+4*this._x1+t)/6,e=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(i,e):this._context.moveTo(i,e);break;case 3:this._point=4;default:wt(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Et.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,i=t.length-1;if(i>0)for(var e,s=t[0],o=n[0],h=t[i]-s,_=n[i]-o,r=-1;++r<=i;)e=r/i,this._basis.point(this._beta*t[r]+(1-this._beta)*(s+e*h),this._beta*n[r]+(1-this._beta)*(o+e*_));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var At=function t(n){function i(t){return 1===n?new kt(t):new Et(t,n)}return i.beta=function(n){return t(+n)},i}(.85);function Pt(t,n,i){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-i),t._x2,t._y2)}function Mt(t,n){this._context=t,this._k=(1-n)/6}Mt.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Pt(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Pt(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Ct=function t(n){function i(t){return new Mt(t,n)}return i.tension=function(n){return t(+n)},i}(0);function Rt(t,n){this._context=t,this._k=(1-n)/6}Rt.prototype={areaStart:bt,areaEnd:bt,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Pt(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Ot=function t(n){function i(t){return new Rt(t,n)}return i.tension=function(n){return t(+n)},i}(0);function zt(t,n){this._context=t,this._k=(1-n)/6}zt.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Pt(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Xt=function t(n){function i(t){return new zt(t,n)}return i.tension=function(n){return t(+n)},i}(0);function Yt(t,n,i){var e=t._x1,s=t._y1,o=t._x2,h=t._y2;if(t._l01_a>l){var _=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,r=3*t._l01_a*(t._l01_a+t._l12_a);e=(e*_-t._x0*t._l12_2a+t._x2*t._l01_2a)/r,s=(s*_-t._y0*t._l12_2a+t._y2*t._l01_2a)/r}if(t._l23_a>l){var a=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,c=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*a+t._x1*t._l23_2a-n*t._l12_2a)/c,h=(h*a+t._y1*t._l23_2a-i*t._l12_2a)/c}t._context.bezierCurveTo(e,s,o,h,t._x2,t._y2)}function qt(t,n){this._context=t,this._alpha=n}qt.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var i=this._x2-t,e=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(i*i+e*e,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Yt(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Bt=function t(n){function i(t){return n?new qt(t,n):new Mt(t,0)}return i.alpha=function(n){return t(+n)},i}(.5);function Dt(t,n){this._context=t,this._alpha=n}Dt.prototype={areaStart:bt,areaEnd:bt,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var i=this._x2-t,e=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(i*i+e*e,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Yt(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var It=function t(n){function i(t){return n?new Dt(t,n):new Rt(t,0)}return i.alpha=function(n){return t(+n)},i}(.5);function jt(t,n){this._context=t,this._alpha=n}jt.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var i=this._x2-t,e=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(i*i+e*e,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Yt(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Lt=function t(n){function i(t){return n?new jt(t,n):new zt(t,0)}return i.alpha=function(n){return t(+n)},i}(.5);function Vt(t){this._context=t}function Wt(t){return t<0?-1:1}function Ft(t,n,i){var e=t._x1-t._x0,s=n-t._x1,o=(t._y1-t._y0)/(e||s<0&&-0),h=(i-t._y1)/(s||e<0&&-0),_=(o*s+h*e)/(e+s);return(Wt(o)+Wt(h))*Math.min(Math.abs(o),Math.abs(h),.5*Math.abs(_))||0}function Ht(t,n){var i=t._x1-t._x0;return i?(3*(t._y1-t._y0)/i-n)/2:n}function $t(t,n,i){var e=t._x0,s=t._y0,o=t._x1,h=t._y1,_=(o-e)/3;t._context.bezierCurveTo(e+_,s+_*n,o-_,h-_*i,o,h)}function Gt(t){this._context=t}function Jt(t){this._context=new Kt(t)}function Kt(t){this._context=t}function Qt(t){this._context=t}function Ut(t){var n,i,e=t.length-1,s=new Array(e),o=new Array(e),h=new Array(e);for(s[0]=0,o[0]=2,h[0]=t[0]+2*t[1],n=1;n<e-1;++n)s[n]=1,o[n]=4,h[n]=4*t[n]+2*t[n+1];for(s[e-1]=2,o[e-1]=7,h[e-1]=8*t[e-1]+t[e],n=1;n<e;++n)i=s[n]/o[n-1],o[n]-=i,h[n]-=i*h[n-1];for(s[e-1]=h[e-1]/o[e-1],n=e-2;n>=0;--n)s[n]=(h[n]-s[n+1])/o[n];for(o[e-1]=(t[e]+s[e-1])/2,n=0;n<e-1;++n)o[n]=2*t[n+1]-s[n+1];return[s,o]}function Zt(t,n){this._context=t,this._t=n}function tn(t,n){if((s=t.length)>1)for(var i,e,s,o=1,h=t[n[0]],_=h.length;o<s;++o)for(e=h,h=t[n[o]],i=0;i<_;++i)h[i][1]+=h[i][0]=isNaN(e[i][1])?e[i][0]:e[i][1]}function nn(t){for(var n=t.length,i=new Array(n);--n>=0;)i[n]=n;return i}function en(t,n){return t[n]}function sn(t){const n=[];return n.key=t,n}function on(t){var n=t.map(hn);return nn(t).sort((function(t,i){return n[t]-n[i]}))}function hn(t){for(var n,i=-1,e=0,s=t.length,o=-1/0;++i<s;)(n=+t[i][1])>o&&(o=n,e=i);return e}function _n(t){var n=t.map(rn);return nn(t).sort((function(t,i){return n[t]-n[i]}))}function rn(t){for(var n,i=0,e=-1,s=t.length;++e<s;)(n=+t[e][1])&&(i+=n);return i}Vt.prototype={areaStart:bt,areaEnd:bt,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}},Gt.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:$t(this,this._t0,Ht(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var i=NaN;if(n=+n,(t=+t)!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,$t(this,Ht(this,i=Ft(this,t,n)),i);break;default:$t(this,this._t0,i=Ft(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=i}}},(Jt.prototype=Object.create(Gt.prototype)).point=function(t,n){Gt.prototype.point.call(this,n,t)},Kt.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,i,e,s,o){this._context.bezierCurveTo(n,t,e,i,o,s)}},Qt.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,i=t.length;if(i)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===i)this._context.lineTo(t[1],n[1]);else for(var e=Ut(t),s=Ut(n),o=0,h=1;h<i;++o,++h)this._context.bezierCurveTo(e[0][o],s[0][o],e[1][o],s[1][o],t[h],n[h]);(this._line||0!==this._line&&1===i)&&this._context.closePath(),this._line=1-this._line,this._x=this._y=null},point:function(t,n){this._x.push(+t),this._y.push(+n)}},Zt.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=this._y=NaN,this._point=0},lineEnd:function(){0<this._t&&this._t<1&&2===this._point&&this._context.lineTo(this._x,this._y),(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line>=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var i=this._x*(1-this._t)+t*this._t;this._context.lineTo(i,this._y),this._context.lineTo(i,n)}}this._x=t,this._y=n}},t.arc=function(){var t=v,n=d,h=i(0),k=null,N=T,S=g,E=m,A=null,P=p(M);function M(){var i,p,v=+t.apply(this,arguments),d=+n.apply(this,arguments),T=N.apply(this,arguments)-u,g=S.apply(this,arguments)-u,m=e(g-T),M=g>T;if(A||(A=i=P()),d<v&&(p=d,d=v,v=p),d>l)if(m>f-l)A.moveTo(d*o(T),d*r(T)),A.arc(0,0,d,T,g,!M),v>l&&(A.moveTo(v*o(g),v*r(g)),A.arc(0,0,v,g,T,M));else{var C,R,O=T,z=g,X=T,Y=g,q=m,B=m,D=E.apply(this,arguments)/2,I=D>l&&(k?+k.apply(this,arguments):a(v*v+d*d)),j=_(e(d-v)/2,+h.apply(this,arguments)),L=j,V=j;if(I>l){var W=x(I/v*r(D)),F=x(I/d*r(D));(q-=2*W)>l?(X+=W*=M?1:-1,Y-=W):(q=0,X=Y=(T+g)/2),(B-=2*F)>l?(O+=F*=M?1:-1,z-=F):(B=0,O=z=(T+g)/2)}var H=d*o(O),$=d*r(O),G=v*o(Y),J=v*r(Y);if(j>l){var K,Q=d*o(z),U=d*r(z),Z=v*o(X),tt=v*r(X);if(m<c)if(K=b(H,$,Z,tt,Q,U,G,J)){var nt=H-K[0],it=$-K[1],et=Q-K[0],st=U-K[1],ot=1/r(y((nt*et+it*st)/(a(nt*nt+it*it)*a(et*et+st*st)))/2),ht=a(K[0]*K[0]+K[1]*K[1]);L=_(j,(v-ht)/(ot-1)),V=_(j,(d-ht)/(ot+1))}else L=V=0}B>l?V>l?(C=w(Z,tt,H,$,d,V,M),R=w(Q,U,G,J,d,V,M),A.moveTo(C.cx+C.x01,C.cy+C.y01),V<j?A.arc(C.cx,C.cy,V,s(C.y01,C.x01),s(R.y01,R.x01),!M):(A.arc(C.cx,C.cy,V,s(C.y01,C.x01),s(C.y11,C.x11),!M),A.arc(0,0,d,s(C.cy+C.y11,C.cx+C.x11),s(R.cy+R.y11,R.cx+R.x11),!M),A.arc(R.cx,R.cy,V,s(R.y11,R.x11),s(R.y01,R.x01),!M))):(A.moveTo(H,$),A.arc(0,0,d,O,z,!M)):A.moveTo(H,$),v>l&&q>l?L>l?(C=w(G,J,Q,U,v,-L,M),R=w(H,$,Z,tt,v,-L,M),A.lineTo(C.cx+C.x01,C.cy+C.y01),L<j?A.arc(C.cx,C.cy,L,s(C.y01,C.x01),s(R.y01,R.x01),!M):(A.arc(C.cx,C.cy,L,s(C.y01,C.x01),s(C.y11,C.x11),!M),A.arc(0,0,v,s(C.cy+C.y11,C.cx+C.x11),s(R.cy+R.y11,R.cx+R.x11),M),A.arc(R.cx,R.cy,L,s(R.y11,R.x11),s(R.y01,R.x01),!M))):A.arc(0,0,v,Y,X,M):A.lineTo(G,J)}else A.moveTo(0,0);if(A.closePath(),i)return A=null,i+""||null}return M.centroid=function(){var i=(+t.apply(this,arguments)+ +n.apply(this,arguments))/2,e=(+N.apply(this,arguments)+ +S.apply(this,arguments))/2-c/2;return[o(e)*i,r(e)*i]},M.innerRadius=function(n){return arguments.length?(t="function"==typeof n?n:i(+n),M):t},M.outerRadius=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),M):n},M.cornerRadius=function(t){return arguments.length?(h="function"==typeof t?t:i(+t),M):h},M.padRadius=function(t){return arguments.length?(k=null==t?null:"function"==typeof t?t:i(+t),M):k},M.startAngle=function(t){return arguments.length?(N="function"==typeof t?t:i(+t),M):N},M.endAngle=function(t){return arguments.length?(S="function"==typeof t?t:i(+t),M):S},M.padAngle=function(t){return arguments.length?(E="function"==typeof t?t:i(+t),M):E},M.context=function(t){return arguments.length?(A=null==t?null:t,M):A},M},t.area=C,t.areaRadial=D,t.curveBasis=function(t){return new kt(t)},t.curveBasisClosed=function(t){return new Nt(t)},t.curveBasisOpen=function(t){return new St(t)},t.curveBumpX=V,t.curveBumpY=W,t.curveBundle=At,t.curveCardinal=Ct,t.curveCardinalClosed=Ot,t.curveCardinalOpen=Xt,t.curveCatmullRom=Bt,t.curveCatmullRomClosed=It,t.curveCatmullRomOpen=Lt,t.curveLinear=E,t.curveLinearClosed=function(t){return new Vt(t)},t.curveMonotoneX=function(t){return new Gt(t)},t.curveMonotoneY=function(t){return new Jt(t)},t.curveNatural=function(t){return new Qt(t)},t.curveStep=function(t){return new Zt(t,.5)},t.curveStepAfter=function(t){return new Zt(t,1)},t.curveStepBefore=function(t){return new Zt(t,0)},t.line=M,t.lineRadial=B,t.link=G,t.linkHorizontal=function(){return G(V)},t.linkRadial=function(){const t=G(F);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.linkVertical=function(){return G(W)},t.pie=function(){var t=O,n=R,e=null,s=i(0),o=i(f),h=i(0);function _(i){var _,r,a,l,c,u=(i=N(i)).length,y=0,x=new Array(u),p=new Array(u),v=+s.apply(this,arguments),d=Math.min(f,Math.max(-f,o.apply(this,arguments)-v)),T=Math.min(Math.abs(d)/u,h.apply(this,arguments)),g=T*(d<0?-1:1);for(_=0;_<u;++_)(c=p[x[_]=_]=+t(i[_],_,i))>0&&(y+=c);for(null!=n?x.sort((function(t,i){return n(p[t],p[i])})):null!=e&&x.sort((function(t,n){return e(i[t],i[n])})),_=0,a=y?(d-u*g)/y:0;_<u;++_,v=l)r=x[_],l=v+((c=p[r])>0?c*a:0)+g,p[r]={data:i[r],index:_,value:c,startAngle:v,endAngle:l,padAngle:T};return p}return _.value=function(n){return arguments.length?(t="function"==typeof n?n:i(+n),_):t},_.sortValues=function(t){return arguments.length?(n=t,e=null,_):n},_.sort=function(t){return arguments.length?(e=t,n=null,_):e},_.startAngle=function(t){return arguments.length?(s="function"==typeof t?t:i(+t),_):s},_.endAngle=function(t){return arguments.length?(o="function"==typeof t?t:i(+t),_):o},_.padAngle=function(t){return arguments.length?(h="function"==typeof t?t:i(+t),_):h},_},t.pointRadial=I,t.radialArea=D,t.radialLine=B,t.stack=function(){var t=i([]),n=nn,e=tn,s=en;function o(i){var o,h,_=Array.from(t.apply(this,arguments),sn),r=_.length,a=-1;for(const t of i)for(o=0,++a;o<r;++o)(_[o][a]=[0,+s(t,_[o].key,a,i)]).data=t;for(o=0,h=N(n(_));o<r;++o)_[h[o]].index=o;return e(_,h),_}return o.keys=function(n){return arguments.length?(t="function"==typeof n?n:i(Array.from(n)),o):t},o.value=function(t){return arguments.length?(s="function"==typeof t?t:i(+t),o):s},o.order=function(t){return arguments.length?(n=null==t?nn:"function"==typeof t?t:i(Array.from(t)),o):n},o.offset=function(t){return arguments.length?(e=null==t?tn:t,o):e},o},t.stackOffsetDiverging=function(t,n){if((_=t.length)>0)for(var i,e,s,o,h,_,r=0,a=t[n[0]].length;r<a;++r)for(o=h=0,i=0;i<_;++i)(s=(e=t[n[i]][r])[1]-e[0])>0?(e[0]=o,e[1]=o+=s):s<0?(e[1]=h,e[0]=h+=s):(e[0]=0,e[1]=s)},t.stackOffsetExpand=function(t,n){if((e=t.length)>0){for(var i,e,s,o=0,h=t[0].length;o<h;++o){for(s=i=0;i<e;++i)s+=t[i][o][1]||0;if(s)for(i=0;i<e;++i)t[i][o][1]/=s}tn(t,n)}},t.stackOffsetNone=tn,t.stackOffsetSilhouette=function(t,n){if((i=t.length)>0){for(var i,e=0,s=t[n[0]],o=s.length;e<o;++e){for(var h=0,_=0;h<i;++h)_+=t[h][e][1]||0;s[e][1]+=s[e][0]=-_/2}tn(t,n)}},t.stackOffsetWiggle=function(t,n){if((s=t.length)>0&&(e=(i=t[n[0]]).length)>0){for(var i,e,s,o=0,h=1;h<e;++h){for(var _=0,r=0,a=0;_<s;++_){for(var l=t[n[_]],c=l[h][1]||0,u=(c-(l[h-1][1]||0))/2,f=0;f<_;++f){var y=t[n[f]];u+=(y[h][1]||0)-(y[h-1][1]||0)}r+=c,a+=u*c}i[h-1][1]+=i[h-1][0]=o,r&&(o-=a/r)}i[h-1][1]+=i[h-1][0]=o,tn(t,n)}},t.stackOrderAppearance=on,t.stackOrderAscending=_n,t.stackOrderDescending=function(t){return _n(t).reverse()},t.stackOrderInsideOut=function(t){var n,i,e=t.length,s=t.map(rn),o=on(t),h=0,_=0,r=[],a=[];for(n=0;n<e;++n)i=o[n],h<_?(h+=s[i],r.push(i)):(_+=s[i],a.push(i));return a.reverse().concat(r)},t.stackOrderNone=nn,t.stackOrderReverse=function(t){return nn(t).reverse()},t.symbol=function(t,n){let e=null,s=p(o);function o(){let i;if(e||(e=i=s()),t.apply(this,arguments).draw(e,+n.apply(this,arguments)),i)return e=null,i+""||null}return t="function"==typeof t?t:i(t||Q),n="function"==typeof n?n:i(void 0===n?64:+n),o.type=function(n){return arguments.length?(t="function"==typeof n?n:i(n),o):t},o.size=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),o):n},o.context=function(t){return arguments.length?(e=null==t?null:t,o):e},o},t.symbolAsterisk=K,t.symbolCircle=Q,t.symbolCross=U,t.symbolDiamond=nt,t.symbolDiamond2=it,t.symbolPlus=et,t.symbolSquare=st,t.symbolSquare2=ot,t.symbolStar=at,t.symbolTimes=Tt,t.symbolTriangle=ct,t.symbolTriangle2=ft,t.symbolWye=dt,t.symbolX=Tt,t.symbols=gt,t.symbolsFill=gt,t.symbolsStroke=mt}));
|
|
|
|
|
|
|
|
|
frontend/vendor/three.min.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|