aaurelions commited on
Commit
d795b5e
·
verified ·
1 Parent(s): 0e9ae0d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +879 -19
index.html CHANGED
@@ -1,19 +1,879 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Syntho-Animator Suite: Production Data Factory</title>
7
+ <!--
8
+ PRODUCTION DEPLOYMENT ADVISORY:
9
+ This project uses the Tailwind CSS CDN and ES Module imports from CDNs for ease of use
10
+ and demonstration purposes. This is ideal for development and quick testing.
11
+
12
+ For a production environment, it is STRONGLY RECOMMENDED to set up a build process:
13
+ 1. Install Tailwind CSS via npm/yarn and generate a static, purged CSS file. This will
14
+ significantly reduce the CSS file size and improve load performance.
15
+ 2. Bundle JavaScript modules using a tool like Vite, Webpack, or Rollup. This optimizes
16
+ dependencies and reduces the number of network requests.
17
+
18
+ See Tailwind's official guide for setup: https://tailwindcss.com/docs/installation
19
+ -->
20
+ <script src="https://cdn.tailwindcss.com"></script>
21
+ <script src="https://unpkg.com/feather-icons"></script>
22
+ <style>
23
+ @import url('https://rsms.me/inter/inter.css');
24
+ html { font-family: 'Inter', sans-serif; }
25
+ body { background-color: #0c0a09; color: #e7e5e4; }
26
+ .glass-ui { background-color: rgba(28, 25, 23, 0.7); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.1); }
27
+ .modal-overlay { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
28
+ .hidden { display: none !important; }
29
+ .side-panel { transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
30
+ .side-panel.left { transform: translateX(-100%); }
31
+ .side-panel.right { transform: translateX(100%); }
32
+ .side-panel.is-open { transform: translateX(0); }
33
+ .panel-trigger { transition: background-color 0.2s ease; }
34
+ .panel-trigger:hover { background-color: rgba(59, 130, 246, 0.5); }
35
+ .asset-card { transition: all 0.2s ease-in-out; border: 2px solid transparent; }
36
+ .asset-card:hover { transform: scale(1.03); background-color: rgba(59, 130, 246, 0.1); }
37
+ .asset-card.active { background-color: rgba(59, 130, 246, 0.2); border-color: #3b82f6; }
38
+ .panel-content::-webkit-scrollbar { width: 6px; }
39
+ .panel-content::-webkit-scrollbar-track { background: transparent; }
40
+ .panel-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 10px; }
41
+ .drop-zone { border: 2px dashed #44403c; transition: background-color 0.2s, border-color 0.2s; }
42
+ .drop-zone.drag-over { background-color: rgba(59, 130, 246, 0.15); border-color: #3b82f6; }
43
+ .loader-spinner { width: 48px; height: 48px; border: 4px solid #44403c; border-top-color: #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; }
44
+ .notification-spinner { width: 20px; height: 20px; border: 2px solid #e7e5e4; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
45
+ @keyframes spin { to { transform: rotate(360deg); } }
46
+ .toggle-bg:after { content: ''; position: absolute; top: 2px; left: 2px; background: white; border-radius: 9999px; height: 1.25rem; width: 1.25rem; transition: all .3s; }
47
+ input:checked + .toggle-bg:after { transform: translateX(100%); }
48
+ input:checked + .toggle-bg { background-color: #3b82f6; }
49
+ #main-canvas { transition: filter 0.5s cubic-bezier(0.4, 0, 0.2, 1); }
50
+ #main-canvas.loading { filter: blur(8px) brightness(0.6); }
51
+ .notification { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); transform: translateY(150%) scale(0.9); opacity: 0; }
52
+ .notification.show { transform: translateY(0) scale(1); opacity: 1; }
53
+ .control-section { transition: opacity 0.3s ease; }
54
+ .sliders-container { transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s, padding-top 0.4s, margin-top 0.4s; overflow: hidden; }
55
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: #3b82f6; border-radius: 50%; cursor: pointer; margin-top: -6px; transition: background-color 0.2s; }
56
+ input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; background: #3b82f6; border-radius: 50%; cursor: pointer; }
57
+ input[type="range"]:hover::-webkit-slider-thumb { background: #2563eb; }
58
+ .tooltip-trigger { position: relative; cursor: help; }
59
+ .tooltip-trigger .tooltip-content { visibility: hidden; opacity: 0; position: absolute; bottom: 125%; left: 50%; transform: translateX(-50%); background-color: #1c1917; color: #e7e5e4; text-align: center; padding: 8px 12px; border-radius: 6px; z-index: 10; width: 220px; font-size: 0.8rem; line-height: 1.4; transition: opacity 0.2s; pointer-events: none; }
60
+ .tooltip-trigger:hover .tooltip-content { visibility: visible; opacity: 1; }
61
+ </style>
62
+ </head>
63
+ <body class="select-none overflow-hidden">
64
+
65
+ <canvas id="main-canvas" class="absolute top-0 left-0 w-full h-full outline-none z-0"></canvas>
66
+
67
+ <!-- OVERLAYS -->
68
+ <div id="loader-overlay" class="absolute inset-0 bg-stone-950 flex flex-col items-center justify-center z-50 transition-opacity duration-500">
69
+ <div class="loader-spinner"></div>
70
+ <p id="loader-text" class="mt-4 text-xl tracking-wider text-stone-300">Initializing...</p>
71
+ </div>
72
+ <div id="notification-container" class="fixed bottom-5 left-1/2 -translate-x-1/2 z-[60] flex flex-col items-center gap-2 w-[90vw] max-w-md"></div>
73
+
74
+ <!-- MODALS -->
75
+ <div id="dataset-modal" class="modal-overlay fixed inset-0 z-40 hidden items-center justify-center p-4">
76
+ <div class="glass-ui rounded-lg w-full max-w-2xl mx-auto flex flex-col max-h-[90vh]">
77
+ <header class="p-6 pb-4 flex-shrink-0">
78
+ <h3 class="text-2xl font-bold text-center">Generate Synthetic Dataset</h3>
79
+ </header>
80
+
81
+ <div class="overflow-y-auto px-6 pb-6 space-y-6 flex-grow">
82
+
83
+ <!-- SECTION 1: CORE SETTINGS -->
84
+ <div class="space-y-4">
85
+ <h4 class="text-lg font-semibold text-stone-300 border-b border-white/10 pb-2">1. Core Settings</h4>
86
+ <div>
87
+ <label for="dataset-task" class="block text-sm font-medium text-stone-300 mb-1">Task Type</label>
88
+ <select id="dataset-task" class="w-full bg-stone-800/70 p-2 rounded-md border-2 border-stone-600 focus:border-blue-500 outline-none">
89
+ <option value="detection">Object Detection (YOLO)</option>
90
+ <option value="segmentation">Instance Segmentation (YOLO)</option>
91
+ <option value="classification">Image Classification</option>
92
+ </select>
93
+ </div>
94
+ <div>
95
+ <label for="dataset-samples" class="block text-sm font-medium text-stone-300 mb-1">Number of Samples</label>
96
+ <input type="number" id="dataset-samples" value="100" min="1" max="10000" class="w-full bg-stone-800/70 p-2 rounded-md border-2 border-stone-600 focus:border-blue-500 outline-none">
97
+ </div>
98
+ <div>
99
+ <label for="dataset-name" class="block text-sm font-medium text-stone-300 mb-1">Dataset Name</label>
100
+ <input type="text" id="dataset-name" value="my_synthetic_dataset" class="w-full bg-stone-800/70 p-2 rounded-md border-2 border-stone-600 focus:border-blue-500 outline-none">
101
+ </div>
102
+ </div>
103
+
104
+ <!-- SECTION 2: SCENE COMPOSITION -->
105
+ <div class="space-y-4">
106
+ <h4 class="text-lg font-semibold text-stone-300 border-b border-white/10 pb-2">2. Scene Composition</h4>
107
+
108
+ <!-- MODEL CONFIGURATION -->
109
+ <div class="bg-stone-900/50 p-4 rounded-lg space-y-3">
110
+ <label class="font-medium text-stone-200 block">3D Model Source</label>
111
+ <div class="flex flex-col sm:flex-row gap-2 sm:gap-4">
112
+ <label class="flex items-center gap-2 p-2 rounded-md bg-stone-800/50 hover:bg-stone-700/50 cursor-pointer flex-1"><input type="radio" name="model-source" value="current" class="h-4 w-4 rounded-full text-blue-500 bg-stone-700 border-stone-500 focus:ring-blue-600"> Use Current Model</label>
113
+ <label class="flex items-center gap-2 p-2 rounded-md bg-stone-800/50 hover:bg-stone-700/50 cursor-pointer flex-1"><input type="radio" name="model-source" value="random" checked class="h-4 w-4 rounded-full text-blue-500 bg-stone-700 border-stone-500 focus:ring-blue-600"> Randomize from Library</label>
114
+ </div>
115
+ <div id="model-source-current-info" class="text-sm text-blue-400 pl-2 hidden"></div>
116
+ </div>
117
+
118
+ <!-- PANORAMA CONFIGURATION -->
119
+ <div class="bg-stone-900/50 p-4 rounded-lg space-y-3">
120
+ <label class="font-medium text-stone-200 block">Background Source</label>
121
+ <div class="flex flex-col sm:flex-row gap-2 sm:gap-4">
122
+ <label class="flex items-center gap-2 p-2 rounded-md bg-stone-800/50 hover:bg-stone-700/50 cursor-pointer flex-1"><input type="radio" name="pano-source" value="current" class="h-4 w-4 rounded-full text-blue-500 bg-stone-700 border-stone-500 focus:ring-blue-600"> Use Current Panorama</label>
123
+ <label class="flex items-center gap-2 p-2 rounded-md bg-stone-800/50 hover:bg-stone-700/50 cursor-pointer flex-1"><input type="radio" name="pano-source" value="random" checked class="h-4 w-4 rounded-full text-blue-500 bg-stone-700 border-stone-500 focus:ring-blue-600"> Randomize from Library</label>
124
+ </div>
125
+ <div id="pano-source-current-info" class="text-sm text-blue-400 pl-2 hidden"></div>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- SECTION 3: RANDOMIZATION CONTROLS -->
130
+ <div class="space-y-4">
131
+ <h4 class="text-lg font-semibold text-stone-300 border-b border-white/10 pb-2">3. Randomization Controls</h4>
132
+
133
+ <!-- Model Transformation Controls -->
134
+ <div class="control-section space-y-3 bg-stone-900/50 p-4 rounded-lg">
135
+ <div class="flex items-center justify-between">
136
+ <label for="randomize-model-toggle" class="font-medium text-stone-300 select-none cursor-pointer flex items-center gap-2">Randomize Model Transform
137
+ <span class="tooltip-trigger"><i data-feather="help-circle" class="w-4 h-4 text-stone-400"></i><span class="tooltip-content">Applies random position, rotation, and scale. If 'Use Current Model' is selected, randomization is relative to its current state. Otherwise, it's fully random.</span></span>
138
+ </label>
139
+ <input type="checkbox" id="randomize-model-toggle" checked class="h-5 w-5 rounded text-blue-500 bg-stone-700 border-stone-500 focus:ring-blue-600 cursor-pointer">
140
+ </div>
141
+ <div id="model-sliders" class="sliders-container pl-4 border-l-2 border-stone-700 space-y-3">
142
+ <div>
143
+ <label for="position-variance" class="text-sm flex justify-between">Position Variance <span id="position-variance-value">90%</span></label>
144
+ <input type="range" id="position-variance" min="0" max="100" value="90" class="w-full h-2 bg-stone-700 rounded-lg appearance-none cursor-pointer">
145
+ </div>
146
+ <div>
147
+ <label for="rotation-variance" class="text-sm flex justify-between">Rotation Variance <span id="rotation-variance-value">100%</span></label>
148
+ <input type="range" id="rotation-variance" min="0" max="100" value="100" class="w-full h-2 bg-stone-700 rounded-lg appearance-none cursor-pointer">
149
+ </div>
150
+ <div>
151
+ <label for="scale-variance" class="text-sm flex justify-between">Scale Variance <span id="scale-variance-value">10%</span></label>
152
+ <input type="range" id="scale-variance" min="0" max="100" value="10" class="w-full h-2 bg-stone-700 rounded-lg appearance-none cursor-pointer">
153
+ </div>
154
+ </div>
155
+ </div>
156
+ <!-- Camera Angle Controls -->
157
+ <div id="camera-randomization-section" class="control-section space-y-3 bg-stone-900/50 p-4 rounded-lg transition-opacity duration-300">
158
+ <div class="flex items-center justify-between">
159
+ <label for="randomize-camera-toggle" class="font-medium text-stone-300 select-none cursor-pointer flex items-center gap-2">Randomize Camera Viewpoint
160
+ <span class="tooltip-trigger"><i data-feather="help-circle" class="w-4 h-4 text-stone-400"></i><span class="tooltip-content">Generate images from viewpoints similar to the current camera angle. This option is only available when 'Use Current Panorama' is selected.</span></span>
161
+ </label>
162
+ <input type="checkbox" id="randomize-camera-toggle" class="h-5 w-5 rounded text-blue-500 bg-stone-700 border-stone-500 focus:ring-blue-600 cursor-pointer">
163
+ </div>
164
+ <div id="camera-sliders" class="sliders-container pl-4 border-l-2 border-stone-700 space-y-3">
165
+ <div>
166
+ <label for="horizontal-variance" class="text-sm flex justify-between">Horizontal Variance <span id="horizontal-variance-value">20°</span></label>
167
+ <input type="range" id="horizontal-variance" min="0" max="180" value="20" class="w-full h-2 bg-stone-700 rounded-lg appearance-none cursor-pointer">
168
+ </div>
169
+ <div>
170
+ <label for="vertical-variance" class="text-sm flex justify-between">Vertical Variance <span id="vertical-variance-value">0°</span></label>
171
+ <input type="range" id="vertical-variance" min="0" max="90" value="0" class="w-full h-2 bg-stone-700 rounded-lg appearance-none cursor-pointer">
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
+ </div>
178
+
179
+ <!-- FOOTER -->
180
+ <div class="flex-shrink-0 flex justify-end gap-3 p-4 border-t border-white/10 bg-stone-900/30">
181
+ <button id="cancel-dataset" class="bg-stone-600 px-4 py-2 rounded-md font-semibold hover:bg-stone-700 transition-colors">Cancel</button>
182
+ <button id="start-dataset" class="bg-blue-600 px-4 py-2 rounded-md font-semibold hover:bg-blue-700 min-w-[80px] flex justify-center items-center transition-colors">Generate</button>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ <div id="progress-container" class="modal-overlay fixed inset-0 z-40 hidden items-center justify-center p-4">
187
+ <div class="glass-ui w-full max-w-sm p-6 rounded-lg">
188
+ <h3 id="progress-title" class="text-lg font-bold mb-4">Generating Dataset</h3>
189
+ <div id="progress-label" class="mb-2 text-sm text-stone-300">Processing...</div>
190
+ <div class="w-full bg-stone-700 rounded-full h-2.5"><div id="progress-bar" class="bg-blue-600 h-2.5 rounded-full transition-all" style="width: 0%"></div></div>
191
+ </div>
192
+ </div>
193
+ <div id="upload-modal" class="modal-overlay fixed inset-0 z-40 hidden items-center justify-center p-4">
194
+ <div class="glass-ui p-6 rounded-lg w-full max-w-md mx-auto flex flex-col gap-4">
195
+ <h3 id="upload-title" class="text-2xl font-bold text-center">Upload New Asset</h3>
196
+ <div id="upload-drop-zone" class="drop-zone p-6 rounded-lg text-center cursor-pointer">
197
+ <p class="text-stone-300 pointer-events-none">Drag & Drop File</p>
198
+ <p class="text-stone-400 text-sm pointer-events-none">or click to browse</p>
199
+ <input type="file" id="upload-file-input" class="hidden">
200
+ </div>
201
+ <div class="flex items-center text-stone-400"><hr class="flex-grow border-white/10"><span class="mx-2 text-sm">OR</span><hr class="flex-grow border-white/10"></div>
202
+ <input type="text" id="upload-url-input" class="w-full bg-stone-900/50 p-2 rounded-md border-2 border-stone-600 focus:border-blue-500 outline-none" placeholder="Enter Asset URL">
203
+ <div class="flex justify-end gap-3 mt-2">
204
+ <button id="upload-cancel-btn" class="bg-stone-600 px-4 py-2 rounded-md font-semibold hover:bg-stone-700 transition-colors">Cancel</button>
205
+ <button id="upload-load-btn" class="bg-blue-600 px-4 py-2 rounded-md font-semibold hover:bg-blue-700 min-w-[80px] flex justify-center items-center transition-colors">Load</button>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <!-- MAIN UI -->
211
+ <div id="controls-bar" class="glass-ui fixed top-4 left-1/2 -translate-x-1/2 z-20 p-2 rounded-lg flex items-center gap-2">
212
+ <button id="translate-btn" class="control-btn p-2 rounded hover:bg-white/10" data-mode="translate" title="Translate (W)"><i data-feather="move"></i></button>
213
+ <button id="rotate-btn" class="control-btn p-2 rounded hover:bg-white/10" data-mode="rotate" title="Rotate (E)"><i data-feather="rotate-cw"></i></button>
214
+ <button id="scale-btn" class="control-btn p-2 rounded hover:bg-white/10" data-mode="scale" title="Scale (R)"><i data-feather="maximize"></i></button>
215
+ <div class="w-px h-6 bg-white/10 mx-1"></div>
216
+ <div class="flex items-center gap-2 px-2" title="Toggle Day/Night">
217
+ <i data-feather="sun" class="w-5 h-5 text-stone-400"></i>
218
+ <label class="relative inline-flex items-center cursor-pointer">
219
+ <input type="checkbox" id="day-night-toggle" class="sr-only peer">
220
+ <div class="w-11 h-6 bg-stone-700 rounded-full peer peer-checked:bg-blue-600 toggle-bg"></div>
221
+ </label>
222
+ <i data-feather="moon" class="w-5 h-5 text-stone-400"></i>
223
+ </div>
224
+ <div class="w-px h-6 bg-white/10 mx-1"></div>
225
+ <button id="generate-dataset-btn" class="p-2 rounded hover:bg-blue-600/50" title="Generate Dataset (G)"><i data-feather="database"></i></button>
226
+ </div>
227
+
228
+ <button id="model-panel-trigger" class="panel-trigger glass-ui absolute top-1/2 -translate-y-1/2 left-0 z-20 rounded-r-lg p-3"><i data-feather="box"></i></button>
229
+ <button id="panorama-panel-trigger" class="panel-trigger glass-ui absolute top-1/2 -translate-y-1/2 right-0 z-20 rounded-l-lg p-3"><i data-feather="image"></i></button>
230
+
231
+ <aside id="model-panel" class="side-panel left glass-ui fixed top-0 left-0 h-full w-[80vw] sm:w-80 z-30 flex flex-col">
232
+ <header class="flex-shrink-0 flex justify-between items-center p-4 border-b border-white/10">
233
+ <h2 class="text-xl font-bold">Models</h2>
234
+ <div>
235
+ <button id="add-model-btn" class="p-2 rounded-full hover:bg-blue-600/50" title="Add new model"><i data-feather="plus"></i></button>
236
+ <button id="close-model-panel" class="p-2 rounded-full hover:bg-stone-700"><i data-feather="x"></i></button>
237
+ </div>
238
+ </header>
239
+ <div id="model-selector" class="panel-content flex-grow overflow-y-auto p-4 grid grid-cols-1 md:grid-cols-2 gap-4"></div>
240
+ </aside>
241
+
242
+ <aside id="panorama-panel" class="side-panel right glass-ui fixed top-0 right-0 h-full w-[80vw] sm:w-80 z-30 flex flex-col">
243
+ <header class="flex-shrink-0 flex justify-between items-center p-4 border-b border-white/10">
244
+ <h2 class="text-xl font-bold">Panoramas</h2>
245
+ <div>
246
+ <button id="add-panorama-btn" class="p-2 rounded-full hover:bg-blue-600/50" title="Add new panorama"><i data-feather="plus"></i></button>
247
+ <button id="close-panorama-panel" class="p-2 rounded-full hover:bg-stone-700"><i data-feather="x"></i></button>
248
+ </div>
249
+ </header>
250
+ <div id="panorama-gallery" class="panel-content flex-grow overflow-y-auto p-4 grid grid-cols-1 md:grid-cols-2 gap-4"></div>
251
+ </aside>
252
+
253
+ <script type="importmap">{ "imports": {
254
+ "three": "https://unpkg.com/[email protected]/build/three.module.js",
255
+ "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
256
+ } }</script>
257
+
258
+ <script type="module">
259
+ import * as THREE from 'three';
260
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
261
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
262
+ import { TransformControls } from 'three/addons/controls/TransformControls.js';
263
+ import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
264
+ import JSZip from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
265
+ import gsap from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
266
+
267
+ // --- CONFIG & STATE ---
268
+ const ASSET_BASE_URL = 'https://huggingface.co/spaces/aaurelions/drones/resolve/main';
269
+ const defaultPanoramas = Array.from({ length: 10 }, (_, i) => ({ name: `bg${i + 1}.jpg`, url: `${ASSET_BASE_URL}/jpg/bg${i+1}.jpg`, thumb: `${ASSET_BASE_URL}/jpg/thumbnails/bg${i+1}.jpg` }));
270
+ const defaultModels = [ 'gerbera.glb', 'shahed1.glb', 'shahed2.glb', 'shahed3.glb', 'supercam.glb', 'zala.glb', 'beaver.glb' ].map(n => ({ name: n, url: `${ASSET_BASE_URL}/glb/${n}`, thumb: `${ASSET_BASE_URL}/glb/thumbnails/${n.replace('.glb','.png')}` }));
271
+
272
+ let panoramaAssets = [...defaultPanoramas];
273
+ let modelAssets = [...defaultModels];
274
+
275
+ let scene, camera, renderer, orbitControls, transformControls, pmremGenerator;
276
+ let selectedObject = null;
277
+ let activeControlMode = null;
278
+ let activePanorama = null;
279
+
280
+ const loaderOverlay = document.getElementById('loader-overlay');
281
+ const loaderText = document.getElementById('loader-text');
282
+ const canvas = document.getElementById('main-canvas');
283
+
284
+ // --- INITIALIZATION ---
285
+ async function init() {
286
+ loaderText.textContent = 'Setting up 3D scene...';
287
+ scene = new THREE.Scene();
288
+ camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 2000);
289
+ camera.position.set(0, 1.5, 8);
290
+
291
+ renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true });
292
+ renderer.setPixelRatio(window.devicePixelRatio);
293
+ renderer.setSize(window.innerWidth, window.innerHeight);
294
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
295
+ renderer.toneMappingExposure = 1.0;
296
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
297
+
298
+ pmremGenerator = new THREE.PMREMGenerator(renderer);
299
+ pmremGenerator.compileEquirectangularShader();
300
+
301
+ orbitControls = new OrbitControls(camera, renderer.domElement);
302
+ orbitControls.enableDamping = true;
303
+ orbitControls.target.set(0, 1, 0);
304
+
305
+ transformControls = new TransformControls(camera, renderer.domElement);
306
+ transformControls.enabled = false;
307
+ scene.add(transformControls);
308
+
309
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
310
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
311
+ dirLight.position.set(8, 15, 10);
312
+ scene.add(ambientLight, dirLight);
313
+
314
+ setupUI();
315
+ setupEventListeners();
316
+ animate();
317
+
318
+ // Initial asset loading sequence
319
+ try {
320
+ await loadAsset(panoramaAssets[0], 'panorama');
321
+ await loadAsset(modelAssets[0], 'model');
322
+ setControlMode('translate');
323
+ } catch (error) {
324
+ showNotification({ text: `Initialization failed: ${error.message}`, type: 'error' });
325
+ console.error("Initialization failed:", error);
326
+ } finally {
327
+ gsap.to(loaderOverlay, { opacity: 0, duration: 0.5, onComplete: () => loaderOverlay.classList.add('hidden') });
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Generic asset loading execution wrapper.
333
+ * @param {object} assetData - The asset data object {name, url, ...}.
334
+ * @param {'model'|'panorama'} assetType - The type of the asset.
335
+ */
336
+ async function loadAsset(assetData, assetType) {
337
+ const isInitialLoad = !loaderOverlay.classList.contains('hidden');
338
+ if (!isInitialLoad) {
339
+ loaderText.textContent = `Loading ${assetType}...`;
340
+ loaderOverlay.classList.remove('hidden');
341
+ gsap.to(loaderOverlay, { opacity: 1, duration: 0.3 });
342
+ canvas.classList.add('loading');
343
+ } else {
344
+ loaderText.textContent = `Loading ${assetType}: ${assetData.name}`;
345
+ }
346
+
347
+ try {
348
+ if (assetType === 'panorama') {
349
+ await new Promise((resolve, reject) => {
350
+ const isHDR = assetData.url.endsWith('.hdr');
351
+ const loader = isHDR ? new RGBELoader() : new THREE.TextureLoader();
352
+ loader.load(assetData.url, (texture) => {
353
+ texture.mapping = THREE.EquirectangularReflectionMapping;
354
+ if (!isHDR) texture.colorSpace = THREE.SRGBColorSpace;
355
+ scene.background = texture;
356
+ scene.environment = pmremGenerator.fromEquirectangular(texture).texture;
357
+ pmremGenerator.dispose();
358
+ texture.dispose();
359
+ activePanorama = assetData;
360
+ setActiveCard('panorama-gallery', assetData.name);
361
+ resolve();
362
+ }, undefined, reject);
363
+ });
364
+ } else { // model
365
+ await new Promise((resolve, reject) => {
366
+ if (selectedObject) transformControls.detach();
367
+
368
+ const existingAsset = modelAssets.find(m => m.name === assetData.name && m.mesh);
369
+ if (existingAsset) {
370
+ modelAssets.forEach(m => { if(m.mesh) m.mesh.visible = false; });
371
+ existingAsset.mesh.visible = true;
372
+ selectedObject = existingAsset.mesh;
373
+ if (activeControlMode) transformControls.attach(selectedObject);
374
+ setActiveCard('model-selector', assetData.name);
375
+ return resolve();
376
+ }
377
+
378
+ const loader = new GLTFLoader();
379
+ loader.load(assetData.url, (gltf) => {
380
+ modelAssets.forEach(m => { if(m.mesh) m.mesh.visible = false; });
381
+ const model = gltf.scene;
382
+ model.name = assetData.name;
383
+ const box = new THREE.Box3().setFromObject(model);
384
+ const size = box.getSize(new THREE.Vector3());
385
+ const center = box.getCenter(new THREE.Vector3());
386
+ model.position.sub(center);
387
+ const maxDim = Math.max(size.x, size.y, size.z);
388
+ model.scale.setScalar(4.0 / maxDim);
389
+ scene.add(model);
390
+
391
+ const assetToUpdate = modelAssets.find(m => m.name === assetData.name);
392
+ if (assetToUpdate) assetToUpdate.mesh = model;
393
+ else modelAssets.push({ ...assetData, mesh: model });
394
+
395
+ selectedObject = model;
396
+ if (activeControlMode) transformControls.attach(selectedObject);
397
+ setActiveCard('model-selector', assetData.name);
398
+ resolve();
399
+ }, undefined, reject);
400
+ });
401
+ }
402
+ } catch (error) {
403
+ showNotification({ text: `Failed to load ${assetType}: ${assetData.name}`, type: 'error', duration: 4000 });
404
+ console.error(`Failed to load ${assetType}:`, error);
405
+ } finally {
406
+ if (!isInitialLoad) {
407
+ gsap.to(loaderOverlay, { opacity: 0, duration: 0.5, onComplete: () => loaderOverlay.classList.add('hidden') });
408
+ canvas.classList.remove('loading');
409
+ }
410
+ }
411
+ }
412
+
413
+ function createAssetCard(type, assetData) {
414
+ const container = document.createElement('div');
415
+ container.className = 'asset-card flex flex-col rounded-lg cursor-pointer overflow-hidden bg-stone-900/50';
416
+ container.dataset.name = assetData.name;
417
+ const thumbWrapper = document.createElement('div');
418
+ thumbWrapper.className = 'w-full h-28 flex items-center justify-center';
419
+ const thumb = document.createElement('img');
420
+ thumb.className = 'max-w-full max-h-full object-contain';
421
+
422
+ container.onclick = () => loadAsset(assetData, type);
423
+
424
+ if (type === 'model') {
425
+ thumbWrapper.classList.add('p-2');
426
+ thumb.src = assetData.thumb;
427
+ const nameWrapper = document.createElement('div');
428
+ nameWrapper.textContent = assetData.name.replace(/\.[^/.]+$/, "").substring(0, 15);
429
+ nameWrapper.className = 'text-white text-xs font-semibold text-center block truncate p-2 bg-black/20 mt-auto';
430
+ thumbWrapper.appendChild(thumb);
431
+ container.append(thumbWrapper, nameWrapper);
432
+ } else { // Panorama
433
+ thumb.className = 'w-full h-full object-cover';
434
+ thumb.src = assetData.thumb;
435
+ thumbWrapper.appendChild(thumb);
436
+ container.appendChild(thumbWrapper);
437
+ }
438
+
439
+ thumb.onerror = () => {
440
+ thumbWrapper.innerHTML = `<i data-feather="package" class="w-12 h-12 text-stone-500"></i>`;
441
+ feather.replace();
442
+ thumbWrapper.parentElement.classList.add('bg-stone-800');
443
+ }
444
+ document.getElementById(type === 'model' ? 'model-selector' : 'panorama-gallery').appendChild(container);
445
+ }
446
+
447
+ // --- UI & EVENT LISTENERS ---
448
+ function setupUI() {
449
+ feather.replace();
450
+ modelAssets.forEach(m => createAssetCard('model', m));
451
+ panoramaAssets.forEach(p => createAssetCard('panorama', p));
452
+ }
453
+
454
+ function setControlMode(mode) {
455
+ if (mode === activeControlMode) {
456
+ activeControlMode = null;
457
+ transformControls.detach();
458
+ transformControls.enabled = false;
459
+ } else {
460
+ activeControlMode = mode;
461
+ transformControls.setMode(activeControlMode);
462
+ transformControls.enabled = true;
463
+ if (selectedObject) transformControls.attach(selectedObject);
464
+ }
465
+ document.querySelectorAll('.control-btn').forEach(b => b.classList.toggle('bg-blue-600', b.dataset.mode === activeControlMode));
466
+ }
467
+
468
+ function setupEventListeners() {
469
+ ['model', 'panorama'].forEach(type => {
470
+ document.getElementById(`${type}-panel-trigger`).addEventListener('click', e => { e.stopPropagation(); document.getElementById(`${type}-panel`).classList.add('is-open'); });
471
+ document.getElementById(`close-${type}-panel`).addEventListener('click', () => document.getElementById(`${type}-panel`).classList.remove('is-open'));
472
+ });
473
+
474
+ document.getElementById('day-night-toggle').addEventListener('change', (e) => {
475
+ const isNight = e.target.checked;
476
+ gsap.to(renderer, { toneMappingExposure: isNight ? 0.3 : 1.0, duration: 0.5 });
477
+ gsap.to(scene.children.find(c => c.isDirectionalLight), { intensity: isNight ? 0.2 : 1.0, duration: 0.5 });
478
+ });
479
+
480
+ document.querySelectorAll('.control-btn').forEach(btn => btn.addEventListener('click', () => setControlMode(btn.dataset.mode)));
481
+ transformControls.addEventListener('dragging-changed', e => { orbitControls.enabled = !e.value; });
482
+
483
+ canvas.addEventListener('click', (e) => {
484
+ if (transformControls.dragging) return;
485
+ const rect = renderer.domElement.getBoundingClientRect();
486
+ const mouse = new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
487
+ const raycaster = new THREE.Raycaster();
488
+ raycaster.setFromCamera(mouse, camera);
489
+ const visibleMeshObjects = modelAssets.map(m => m.mesh).filter(m => m && m.visible);
490
+ if (visibleMeshObjects.length === 0) return;
491
+ const intersects = raycaster.intersectObjects(visibleMeshObjects, true);
492
+
493
+ if (intersects.length > 0) {
494
+ const object = findTopLevelGroup(intersects[0].object);
495
+ if (object && object.isGroup && object !== selectedObject) {
496
+ const modelData = modelAssets.find(m => m.name === object.name);
497
+ if (modelData) loadAsset(modelData, 'model');
498
+ }
499
+ }
500
+ });
501
+
502
+ window.addEventListener('keydown', (e) => {
503
+ if(document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'SELECT') return;
504
+ const key = e.key.toLowerCase();
505
+ if (key === 'w' || key === 'e' || key === 'r') { e.preventDefault(); setControlMode(key === 'w' ? 'translate' : key === 'e' ? 'rotate' : 'scale'); }
506
+ if (key === 'g') { e.preventDefault(); document.getElementById('generate-dataset-btn').click(); }
507
+ if (key === 'escape') { document.querySelectorAll('.side-panel.is-open').forEach(p => p.classList.remove('is-open')); }
508
+ });
509
+
510
+ setupUploadModal();
511
+ setupDatasetModal();
512
+ }
513
+
514
+ function setupUploadModal() {
515
+ const modal = document.getElementById('upload-modal');
516
+ const title = document.getElementById('upload-title');
517
+ const fileInput = document.getElementById('upload-file-input');
518
+ const urlInput = document.getElementById('upload-url-input');
519
+ const dropZone = document.getElementById('upload-drop-zone');
520
+ let currentType, currentHandler;
521
+ const openModal = (type, titleText, accept, placeholder, handler) => {
522
+ currentType = type;
523
+ title.textContent = titleText;
524
+ fileInput.value = ''; urlInput.value = '';
525
+ fileInput.accept = accept;
526
+ urlInput.placeholder = placeholder;
527
+ currentHandler = handler;
528
+ modal.classList.remove('hidden'); modal.classList.add('flex');
529
+ };
530
+ document.getElementById('add-model-btn').addEventListener('click', () => openModal('model', 'Upload New Model', '.glb,.gltf', 'Enter .glb URL', handleNewAsset));
531
+ document.getElementById('add-panorama-btn').addEventListener('click', () => openModal('panorama', 'Upload New Panorama', 'image/*,.hdr', 'Enter image URL', handleNewAsset));
532
+ document.getElementById('upload-cancel-btn').addEventListener('click', () => modal.classList.add('hidden'));
533
+ document.getElementById('upload-load-btn').addEventListener('click', () => { const url = urlInput.value.trim(); if(url) currentHandler(currentType, url); });
534
+ dropZone.addEventListener('click', () => fileInput.click());
535
+ fileInput.addEventListener('change', e => { if (e.target.files.length) currentHandler(currentType, e.target.files[0]) });
536
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
537
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
538
+ dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); if (e.dataTransfer.files.length) currentHandler(currentType, e.dataTransfer.files[0]); });
539
+ }
540
+
541
+ async function handleNewAsset(type, fileOrUrl) {
542
+ document.getElementById('upload-modal').classList.add('hidden');
543
+ const isUrl = typeof fileOrUrl === 'string';
544
+ const url = isUrl ? fileOrUrl : URL.createObjectURL(fileOrUrl);
545
+ const name = (isUrl ? new URL(url).pathname.split('/').pop() : fileOrUrl.name).substring(0,25);
546
+
547
+ const newAssetData = { name, url, mesh: null };
548
+ if (type === 'model') {
549
+ newAssetData.thumb = 'placeholder'; // Placeholder, will be replaced by icon
550
+ modelAssets.push(newAssetData);
551
+ createAssetCard('model', newAssetData);
552
+ await loadAsset(newAssetData, 'model');
553
+ } else {
554
+ newAssetData.thumb = url; // For panoramas, the URL itself is the thumb
555
+ panoramaAssets.push(newAssetData);
556
+ createAssetCard('panorama', newAssetData);
557
+ await loadAsset(newAssetData, 'panorama');
558
+ }
559
+ }
560
+
561
+ function setupDatasetModal() {
562
+ const modal = document.getElementById('dataset-modal');
563
+ document.getElementById('generate-dataset-btn').addEventListener('click', () => {
564
+ modal.classList.remove('hidden');
565
+ modal.classList.add('flex');
566
+ feather.replace();
567
+ updateDatasetModalUI();
568
+ });
569
+ document.getElementById('cancel-dataset').addEventListener('click', () => modal.classList.add('hidden'));
570
+ document.getElementById('start-dataset').addEventListener('click', handleDatasetGeneration);
571
+
572
+ document.querySelectorAll('#dataset-modal input[type="radio"], #dataset-modal input[type="checkbox"]').forEach(input => {
573
+ input.addEventListener('change', updateDatasetModalUI);
574
+ });
575
+
576
+ const setupSlider = (sliderId, displayId, unit) => {
577
+ const slider = document.getElementById(sliderId);
578
+ const display = document.getElementById(displayId);
579
+ if(slider && display) slider.addEventListener('input', () => { display.textContent = slider.value + unit; });
580
+ };
581
+ setupSlider('position-variance', 'position-variance-value', '%');
582
+ setupSlider('rotation-variance', 'rotation-variance-value', '%');
583
+ setupSlider('scale-variance', 'scale-variance-value', '%');
584
+ setupSlider('horizontal-variance', 'horizontal-variance-value', '°');
585
+ setupSlider('vertical-variance', 'vertical-variance-value', '°');
586
+ }
587
+
588
+ function updateDatasetModalUI() {
589
+ // Model Source UI
590
+ const useCurrentModel = document.querySelector('input[name="model-source"]:checked').value === 'current';
591
+ const modelInfo = document.getElementById('model-source-current-info');
592
+ if (useCurrentModel) {
593
+ modelInfo.textContent = `Using: ${selectedObject ? selectedObject.name : 'None'}`;
594
+ modelInfo.classList.remove('hidden');
595
+ } else {
596
+ modelInfo.classList.add('hidden');
597
+ }
598
+
599
+ // Panorama Source UI
600
+ const useCurrentPano = document.querySelector('input[name="pano-source"]:checked').value === 'current';
601
+ const panoInfo = document.getElementById('pano-source-current-info');
602
+ if (useCurrentPano) {
603
+ panoInfo.textContent = `Using: ${activePanorama ? activePanorama.name : 'None'}`;
604
+ panoInfo.classList.remove('hidden');
605
+ } else {
606
+ panoInfo.classList.add('hidden');
607
+ }
608
+
609
+ // Model Randomization Sliders
610
+ const randomizeModel = document.getElementById('randomize-model-toggle').checked;
611
+ const modelSliders = document.getElementById('model-sliders');
612
+ gsap.to(modelSliders, {
613
+ maxHeight: randomizeModel ? 200 : 0,
614
+ opacity: randomizeModel ? 1 : 0,
615
+ paddingTop: randomizeModel ? '0.75rem' : 0,
616
+ marginTop: randomizeModel ? '0.75rem' : 0,
617
+ duration: 0.4,
618
+ ease: 'power2.inOut'
619
+ });
620
+
621
+ // Camera Randomization Controls
622
+ const cameraSection = document.getElementById('camera-randomization-section');
623
+ const cameraToggle = document.getElementById('randomize-camera-toggle');
624
+
625
+ cameraSection.style.opacity = useCurrentPano ? '1' : '0.5';
626
+ cameraToggle.disabled = !useCurrentPano;
627
+ cameraToggle.closest('label').style.cursor = useCurrentPano ? 'pointer' : 'not-allowed';
628
+
629
+ if (!useCurrentPano) cameraToggle.checked = false;
630
+
631
+ const randomizeCamera = useCurrentPano && cameraToggle.checked;
632
+ const cameraSliders = document.getElementById('camera-sliders');
633
+ gsap.to(cameraSliders, {
634
+ maxHeight: randomizeCamera ? 150 : 0,
635
+ opacity: randomizeCamera ? 1 : 0,
636
+ paddingTop: randomizeCamera ? '0.75rem' : 0,
637
+ marginTop: randomizeCamera ? '0.75rem' : 0,
638
+ duration: 0.4,
639
+ ease: 'power2.inOut'
640
+ });
641
+ }
642
+
643
+ async function handleDatasetGeneration() {
644
+ const modal = document.getElementById('dataset-modal');
645
+ const options = {
646
+ task: document.getElementById('dataset-task').value,
647
+ samples: parseInt(document.getElementById('dataset-samples').value),
648
+ name: document.getElementById('dataset-name').value.trim(),
649
+ useCurrentModel: document.querySelector('input[name="model-source"]:checked').value === 'current',
650
+ randomizeModel: document.getElementById('randomize-model-toggle').checked,
651
+ posVar: parseInt(document.getElementById('position-variance').value) / 100,
652
+ rotVar: parseInt(document.getElementById('rotation-variance').value) / 100,
653
+ scaleVar: parseInt(document.getElementById('scale-variance').value) / 100,
654
+ useCurrentPanorama: document.querySelector('input[name="pano-source"]:checked').value === 'current',
655
+ randomizeCamera: document.getElementById('randomize-camera-toggle').checked,
656
+ hVar: THREE.MathUtils.degToRad(parseInt(document.getElementById('horizontal-variance').value)),
657
+ vVar: THREE.MathUtils.degToRad(parseInt(document.getElementById('vertical-variance').value)),
658
+ initial: {
659
+ model: {
660
+ position: selectedObject ? selectedObject.position.clone() : new THREE.Vector3(),
661
+ rotation: selectedObject ? selectedObject.rotation.clone() : new THREE.Euler(),
662
+ scale: selectedObject ? selectedObject.scale.x : 1
663
+ },
664
+ camera: { position: camera.position.clone() }
665
+ }
666
+ };
667
+ if (!options.name) { showNotification({ text: "Dataset name cannot be empty.", type: "error" }); return; }
668
+ modal.classList.add('hidden');
669
+ const progressContainer = document.getElementById('progress-container');
670
+ progressContainer.classList.remove('hidden'); progressContainer.classList.add('flex');
671
+
672
+ const zip = new JSZip();
673
+ const originalModelAsset = selectedObject ? modelAssets.find(m => m.name === selectedObject.name) : null;
674
+ const originalPanoramaAsset = activePanorama;
675
+
676
+ const transformWasVisible = transformControls.visible;
677
+ if (transformWasVisible) transformControls.visible = false;
678
+
679
+ for (let i = 0; i < options.samples; i++) {
680
+ updateProgress(i + 1, options.samples, `Generating sample ${i + 1} of ${options.samples}`);
681
+ await randomizeSceneForGeneration(options);
682
+ renderer.render(scene, camera);
683
+ const imageName = `sample_${String(i).padStart(5, '0')}.jpg`;
684
+ const labelName = imageName.replace('.jpg', '.txt');
685
+ const { imageBlob, labelData } = await generateSampleData(options.task);
686
+
687
+ if (imageBlob && selectedObject) {
688
+ const classFolder = selectedObject.visible ? selectedObject.name.replace(/\.[^/.]+$/, "") : 'background_only';
689
+ if (options.task === 'classification') {
690
+ zip.folder(options.name).folder('train').folder(classFolder).file(imageName, imageBlob);
691
+ } else {
692
+ zip.folder(options.name).folder('images').folder('train').file(imageName, imageBlob);
693
+ if (labelData) { zip.folder(options.name).folder('labels').folder('train').file(labelName, labelData); }
694
+ }
695
+ }
696
+ await new Promise(resolve => setTimeout(resolve, 5));
697
+ }
698
+
699
+ if (transformWasVisible) transformControls.visible = true;
700
+
701
+ updateProgress(options.samples, options.samples, "Compressing ZIP file...");
702
+ const content = await zip.generateAsync({ type: "blob" });
703
+ const link = document.createElement('a');
704
+ link.href = URL.createObjectURL(content);
705
+ link.download = `${options.name}.zip`;
706
+ link.click();
707
+ URL.revokeObjectURL(link.href);
708
+ progressContainer.classList.add('hidden');
709
+ showNotification({ text: 'Dataset generation complete!', type: 'success' });
710
+
711
+ // Restore original scene
712
+ if (originalPanoramaAsset) await loadAsset(originalPanoramaAsset, 'panorama');
713
+ if (originalModelAsset) await loadAsset(originalModelAsset, 'model');
714
+ }
715
+
716
+ async function randomizeSceneForGeneration(options) {
717
+ // Select and load assets
718
+ if (!options.useCurrentPanorama) {
719
+ const randomPano = panoramaAssets[Math.floor(Math.random() * panoramaAssets.length)];
720
+ await loadAsset(randomPano, 'panorama');
721
+ }
722
+ if (!options.useCurrentModel) {
723
+ const randomModel = modelAssets[Math.floor(Math.random() * modelAssets.length)];
724
+ await loadAsset(randomModel, 'model');
725
+ }
726
+
727
+ if (!selectedObject) return;
728
+
729
+ // Handle classification case with background-only images
730
+ selectedObject.visible = !(options.task === 'classification' && Math.random() < 0.2); // 20% chance of background only
731
+ if (!selectedObject.visible) return;
732
+
733
+ // Randomize Model
734
+ if (options.randomizeModel) {
735
+ if(options.useCurrentModel) { // Relative randomization
736
+ const { position: basePos, rotation: baseRot, scale: baseScale } = options.initial.model;
737
+ selectedObject.position.copy(basePos).add(new THREE.Vector3().randomDirection().multiplyScalar(Math.random() * options.posVar * 5));
738
+
739
+ const baseQuaternion = new THREE.Quaternion().setFromEuler(baseRot);
740
+ const rotOffset = new THREE.Euler(
741
+ (Math.random() - 0.5) * 2 * Math.PI * options.rotVar,
742
+ (Math.random() - 0.5) * 2 * Math.PI * options.rotVar,
743
+ (Math.random() - 0.5) * 2 * Math.PI * options.rotVar
744
+ );
745
+ const offsetQuaternion = new THREE.Quaternion().setFromEuler(rotOffset);
746
+ baseQuaternion.multiply(offsetQuaternion);
747
+ selectedObject.quaternion.copy(baseQuaternion);
748
+
749
+ const scaleFactor = 1 + (Math.random() - 0.5) * 2 * options.scaleVar;
750
+ selectedObject.scale.setScalar(baseScale * scaleFactor);
751
+ } else { // Absolute randomization
752
+ selectedObject.position.set((Math.random() - 0.5) * 10 * options.posVar, Math.random() * 5 * options.posVar, (Math.random() - 0.5) * 10 * options.posVar);
753
+ selectedObject.rotation.set(Math.random() * Math.PI * 2 * options.rotVar, Math.random() * Math.PI * 2 * options.rotVar, Math.random() * Math.PI * 2 * options.rotVar);
754
+ const baseScale = selectedObject.scale.x; // Use its default scale as a base
755
+ const scaleFactor = 0.5 + Math.random() * 1.5 * (1 + options.scaleVar);
756
+ selectedObject.scale.setScalar(baseScale * scaleFactor);
757
+ }
758
+ }
759
+
760
+ // Randomize Camera
761
+ if (options.useCurrentPanorama && options.randomizeCamera) { // Relative randomization
762
+ const baseCamPos = options.initial.camera.position;
763
+ const spherical = new THREE.Spherical().setFromCartesianCoords(baseCamPos.x, baseCamPos.y, baseCamPos.z);
764
+ spherical.theta += (Math.random() - 0.5) * 2 * options.hVar;
765
+ spherical.phi += (Math.random() - 0.5) * 2 * options.vVar;
766
+ spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
767
+ camera.position.setFromSpherical(spherical);
768
+ } else if (!options.useCurrentPanorama) { // Absolute randomization
769
+ camera.position.set((Math.random() - 0.5) * 20, Math.random() * 8 + 1, (Math.random() - 0.5) * 15 + 5);
770
+ }
771
+ camera.lookAt(selectedObject.position);
772
+ selectedObject.updateMatrixWorld(true);
773
+ }
774
+
775
+ async function generateSampleData(task) {
776
+ let imageBlob = null, labelData = null;
777
+ if (task === 'segmentation') {
778
+ const { colorBlob, segLabel } = await getSegmentationLabelAndImage();
779
+ imageBlob = colorBlob; labelData = segLabel;
780
+ } else {
781
+ imageBlob = await getCanvasBlob();
782
+ if (task === 'detection') { labelData = getDetectionLabel(); }
783
+ }
784
+ return { imageBlob, labelData };
785
+ }
786
+
787
+ function getDetectionLabel() {
788
+ if (!selectedObject || !selectedObject.visible) return null;
789
+ const box3 = new THREE.Box3().setFromObject(selectedObject);
790
+ if (box3.isEmpty()) return null;
791
+ const corners = [ new THREE.Vector3(box3.min.x, box3.min.y, box3.min.z), new THREE.Vector3(box3.min.x, box3.min.y, box3.max.z), new THREE.Vector3(box3.min.x, box3.max.y, box3.min.z), new THREE.Vector3(box3.min.x, box3.max.y, box3.max.z), new THREE.Vector3(box3.max.x, box3.min.y, box3.min.z), new THREE.Vector3(box3.max.x, box3.min.y, box3.max.z), new THREE.Vector3(box3.max.x, box3.max.y, box3.min.z), new THREE.Vector3(box3.max.x, box3.max.y, box3.max.z) ];
792
+ let minX = 1, maxX = -1, minY = 1, maxY = -1;
793
+ let visibleCorners = 0;
794
+ corners.forEach(corner => {
795
+ corner.project(camera);
796
+ if (corner.z < 1) { // Check if the corner is in front of the camera's near plane
797
+ minX = Math.min(minX, corner.x); maxX = Math.max(maxX, corner.x);
798
+ minY = Math.min(minY, corner.y); maxY = Math.max(maxY, corner.y);
799
+ visibleCorners++;
800
+ }
801
+ });
802
+ if (visibleCorners === 0 || maxX < -1 || minX > 1 || maxY < -1 || minY > 1) return null;
803
+ minX = Math.max(-1, minX); maxX = Math.min(1, maxX); minY = Math.max(-1, minY); maxY = Math.min(1, maxY);
804
+ const normCenterX = ((minX + maxX) / 2 + 1) / 2; const normCenterY = (-(minY + maxY) / 2 + 1) / 2;
805
+ const normWidth = (maxX - minX) / 2; const normHeight = (maxY - minY) / 2;
806
+ if (normWidth < 0.01 || normHeight < 0.01) return null; // Filter out tiny boxes
807
+ return `0 ${normCenterX.toFixed(6)} ${normCenterY.toFixed(6)} ${normWidth.toFixed(6)} ${normHeight.toFixed(6)}`;
808
+ }
809
+
810
+ async function getSegmentationLabelAndImage() {
811
+ if (!selectedObject || !selectedObject.visible) {
812
+ const colorBlob = await getCanvasBlob();
813
+ return { colorBlob, segLabel: null };
814
+ }
815
+ const originalBackground = scene.background;
816
+ renderer.render(scene, camera);
817
+ const colorBlob = await getCanvasBlob();
818
+ const maskMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
819
+ const originalMaterials = new Map();
820
+ selectedObject.traverse(node => { if (node.isMesh) { originalMaterials.set(node, node.material); node.material = maskMaterial; } });
821
+ scene.background = new THREE.Color(0x000000);
822
+ renderer.render(scene, camera);
823
+ const maskCanvas = document.createElement('canvas');
824
+ maskCanvas.width = renderer.domElement.width; maskCanvas.height = renderer.domElement.height;
825
+ const maskCtx = maskCanvas.getContext('2d');
826
+ maskCtx.drawImage(renderer.domElement, 0, 0);
827
+ const imageData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height);
828
+ scene.background = originalBackground;
829
+ selectedObject.traverse(node => { if (node.isMesh && originalMaterials.has(node)) { node.material = originalMaterials.get(node); } });
830
+ const contours = findContours(imageData);
831
+ if (contours.length === 0) return { colorBlob, segLabel: null };
832
+ contours.sort((a, b) => b.length - a.length);
833
+ const simplificationRate = Math.max(1, Math.floor(contours[0].length / 100)); // Simplify based on point count
834
+ const simplifiedContour = contours[0].filter((_, i) => i % simplificationRate === 0);
835
+ if (simplifiedContour.length < 3) return { colorBlob, segLabel: null };
836
+ const yoloPoints = simplifiedContour.map(p => `${(p.x/maskCanvas.width).toFixed(6)} ${(p.y/maskCanvas.height).toFixed(6)}`).join(' ');
837
+ return { colorBlob, segLabel: `0 ${yoloPoints}` };
838
+ }
839
+
840
+ // --- UTILS ---
841
+ function findTopLevelGroup(object) { let current = object; while (current.parent && current.parent !== scene) { current = current.parent; } return current; }
842
+ function findContours(imageData) { const { data, width, height } = imageData; const points = []; const step = 4; for (let y = 0; y < height; y+=step) { for (let x = 0; x < width; x+=step) { const i = (y * width + x) * 4; if (data[i] > 128) { const isEdge = x <= step || x >= width - step || y <= step || y >= height - step || data[i - (step*4)] === 0 || data[i + (step*4)] === 0 || data[i - width * (step*4)] === 0 || data[i + width * (step*4)] === 0; if (isEdge) points.push({ x, y }); } } } return points.length > 0 ? [points] : []; }
843
+ function getCanvasBlob() { return new Promise(resolve => renderer.domElement.toBlob(resolve, 'image/jpeg')); }
844
+ function updateProgress(current, total, text) { document.getElementById('progress-bar').style.width = `${total > 0 ? (current / total) * 100 : 0}%`; document.getElementById('progress-label').textContent = text; }
845
+ function setActiveCard(containerId, name) { document.querySelectorAll(`#${containerId} .asset-card`).forEach(c => c.classList.toggle('active', c.dataset.name === name)); }
846
+
847
+ function showNotification({ text, type = 'success', duration = 3000, id = null }) {
848
+ const container = document.getElementById('notification-container');
849
+ if (id) { const existing = document.getElementById(id); if (existing) { existing.querySelector('span').textContent = text; return; } }
850
+ const el = document.createElement('div');
851
+ el.id = id || `notif-${Date.now()}`;
852
+ el.className = `notification glass-ui p-3 px-4 rounded-lg flex items-center gap-3`;
853
+ const colors = { success: 'bg-green-500/50', error: 'bg-red-500/50', info: 'bg-blue-500/50', loading: 'bg-stone-500/50' };
854
+ const icons = { success: 'check-circle', error: 'alert-triangle', info: 'info', loading: null };
855
+ el.classList.add(colors[type]);
856
+ let iconHtml = type === 'loading' ? `<div class="notification-spinner"></div>` : `<i data-feather="${icons[type]}" class="w-5 h-5"></i>`;
857
+ el.innerHTML = `${iconHtml}<span>${text}</span>`;
858
+ container.appendChild(el);
859
+ if (icons[type]) feather.replace();
860
+ setTimeout(() => el.classList.add('show'), 10);
861
+ if (type !== 'loading') { setTimeout(() => hideNotification(el.id), duration); }
862
+ }
863
+
864
+ function hideNotification(id) {
865
+ const el = document.getElementById(id);
866
+ if (el) { el.classList.remove('show'); setTimeout(() => el.remove(), 500); }
867
+ }
868
+
869
+ function animate() {
870
+ requestAnimationFrame(animate);
871
+ orbitControls.update();
872
+ renderer.render(scene, camera);
873
+ }
874
+ window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });
875
+
876
+ init();
877
+ </script>
878
+ </body>
879
+ </html>