lens-inspector 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -11
- package/SceneInspector.ts +171 -92
- package/example.js +8 -2
- package/index.html +1138 -295
- package/package.json +5 -1
- package/server.js +54 -14
package/index.html
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
<title>Lens Inspector</title>
|
|
6
6
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
7
7
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.171.0/build/three.module.js",
|
|
12
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.171.0/examples/jsm/"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
8
16
|
<style>
|
|
9
17
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
18
|
|
|
@@ -19,27 +27,27 @@
|
|
|
19
27
|
--text-muted: #70707e;
|
|
20
28
|
--white: #fff;
|
|
21
29
|
|
|
22
|
-
/*
|
|
23
|
-
--c-camera: #
|
|
24
|
-
--c-visual: #
|
|
25
|
-
--c-text: #
|
|
26
|
-
--c-script: #
|
|
27
|
-
--c-audio: #
|
|
28
|
-
--c-light: #
|
|
29
|
-
--c-physics: #
|
|
30
|
-
--c-vfx: #
|
|
31
|
-
--c-interaction: #
|
|
32
|
-
--c-tracking: #
|
|
33
|
-
--c-animation: #
|
|
34
|
-
--c-ui: #
|
|
35
|
-
--c-ml: #
|
|
36
|
-
--c-empty: #
|
|
37
|
-
--c-prefab: #
|
|
38
|
-
|
|
39
|
-
--c-runtime: #
|
|
40
|
-
--c-selected: #
|
|
41
|
-
--c-enabled: #
|
|
42
|
-
--c-disabled: #
|
|
30
|
+
/* BRIGHT, PUNCHY. No purple, green, cyan, or washed-out steel blue. */
|
|
31
|
+
--c-camera: #ff9500;
|
|
32
|
+
--c-visual: #0090ff;
|
|
33
|
+
--c-text: #0080ff;
|
|
34
|
+
--c-script: #ff2d20;
|
|
35
|
+
--c-audio: #ff6b00;
|
|
36
|
+
--c-light: #ffc800;
|
|
37
|
+
--c-physics: #ff1a1a;
|
|
38
|
+
--c-vfx: #00a0ff;
|
|
39
|
+
--c-interaction: #ff5500;
|
|
40
|
+
--c-tracking: #99a;
|
|
41
|
+
--c-animation: #ff8800;
|
|
42
|
+
--c-ui: #0070ff;
|
|
43
|
+
--c-ml: #5060ff;
|
|
44
|
+
--c-empty: #99a;
|
|
45
|
+
--c-prefab: #00aaff;
|
|
46
|
+
|
|
47
|
+
--c-runtime: #0090ff;
|
|
48
|
+
--c-selected: #0070ff;
|
|
49
|
+
--c-enabled: #0090ff;
|
|
50
|
+
--c-disabled: #ff2d20;
|
|
43
51
|
|
|
44
52
|
--font: 'IBM Plex Sans', -apple-system, system-ui, sans-serif;
|
|
45
53
|
--mono: 'IBM Plex Mono', 'JetBrains Mono', monospace;
|
|
@@ -67,19 +75,45 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
67
75
|
font-family: var(--font); font-size: 10px; font-weight: 500; border-radius: 3px; cursor: pointer; transition: all var(--transition);
|
|
68
76
|
}
|
|
69
77
|
#toolbar button:hover { border-color: var(--border-bright); color: var(--text); background: var(--border); }
|
|
70
|
-
.status-dot { width:
|
|
71
|
-
.dot-off { background: #
|
|
72
|
-
.dot-live { background: var(--c-selected);
|
|
73
|
-
.dot-
|
|
78
|
+
.status-dot { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; transition: background 0.2s ease; }
|
|
79
|
+
.dot-off { background: #101018; }
|
|
80
|
+
.dot-live { background: var(--c-selected); }
|
|
81
|
+
.dot-example { background: var(--c-camera); }
|
|
74
82
|
#status-text { font-family: var(--mono); font-size: 10px; color: var(--text-dim); }
|
|
83
|
+
#source-badge {
|
|
84
|
+
font-family: var(--mono); font-size: 9px; font-weight: 600; padding: 1px 7px;
|
|
85
|
+
border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
86
|
+
}
|
|
87
|
+
.badge-example { background: #fff3e0; color: #c06000; border: 1px solid #f0c880; }
|
|
88
|
+
.badge-live { background: #e0f0ff; color: #0060c0; border: 1px solid #80c0f0; }
|
|
89
|
+
.badge-off { display: none; }
|
|
90
|
+
#project-name { font-family: var(--mono); font-size: 10px; color: var(--text); font-weight: 500; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
91
|
+
.toolbar-spacer { flex: 1; }
|
|
92
|
+
#setup-btn { font-size: 9px; }
|
|
75
93
|
|
|
76
94
|
/* --- Main layout --- */
|
|
77
95
|
#main { flex: 1; display: flex; min-height: 0; }
|
|
78
96
|
|
|
97
|
+
/* --- Resize handles --- */
|
|
98
|
+
.resize-handle {
|
|
99
|
+
width: 5px; flex-shrink: 0; cursor: col-resize; background: transparent;
|
|
100
|
+
position: relative; z-index: 5; margin: 0 -2px;
|
|
101
|
+
transition: background 0.15s;
|
|
102
|
+
}
|
|
103
|
+
.resize-handle:hover, .resize-handle.active { background: var(--c-selected); }
|
|
104
|
+
.resize-handle::after {
|
|
105
|
+
content: ''; position: absolute; inset: 0; width: 1px; margin: auto;
|
|
106
|
+
background: var(--border);
|
|
107
|
+
}
|
|
108
|
+
.resize-handle:hover::after, .resize-handle.active::after { background: transparent; }
|
|
109
|
+
body.resizing { cursor: col-resize !important; user-select: none !important; }
|
|
110
|
+
body.resizing * { cursor: col-resize !important; user-select: none !important; pointer-events: none !important; }
|
|
111
|
+
body.resizing .resize-handle { pointer-events: auto !important; }
|
|
112
|
+
|
|
79
113
|
/* --- Tree panel (left) --- */
|
|
80
114
|
#tree-panel {
|
|
81
|
-
width: 300px; background: var(--panel);
|
|
82
|
-
display: flex; flex-direction: column; flex-shrink: 0;
|
|
115
|
+
width: 300px; background: var(--panel);
|
|
116
|
+
display: flex; flex-direction: column; flex-shrink: 0; min-width: 150px;
|
|
83
117
|
}
|
|
84
118
|
#tree-header {
|
|
85
119
|
padding: 4px 10px; border-bottom: 1px solid var(--border);
|
|
@@ -93,6 +127,17 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
93
127
|
}
|
|
94
128
|
#tree-search:focus { border-color: var(--c-selected); }
|
|
95
129
|
#tree-search::placeholder { color: var(--border-bright); }
|
|
130
|
+
|
|
131
|
+
/* --- Category filter chips --- */
|
|
132
|
+
#tree-filters { display: flex; flex-wrap: wrap; gap: 2px; padding: 2px 6px 3px; }
|
|
133
|
+
.filter-chip {
|
|
134
|
+
font-family: var(--mono); font-size: 8px; padding: 1px 5px; border-radius: 3px;
|
|
135
|
+
border: 1px solid var(--border); background: transparent; color: var(--text-dim);
|
|
136
|
+
cursor: pointer; transition: all var(--transition); line-height: 1.4;
|
|
137
|
+
}
|
|
138
|
+
.filter-chip:hover { border-color: var(--border-bright); color: var(--text); }
|
|
139
|
+
.filter-chip.active { background: var(--text); color: var(--panel); border-color: var(--text); }
|
|
140
|
+
|
|
96
141
|
#tree-list { flex: 1; overflow-y: auto; padding: 1px 0; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
|
|
97
142
|
#tree-list::-webkit-scrollbar { width: 5px; }
|
|
98
143
|
#tree-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
@@ -106,6 +151,8 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
106
151
|
.tree-node:hover { background: var(--surface); }
|
|
107
152
|
.tree-node.selected { background: #dde4f4; border-left-color: var(--c-selected); }
|
|
108
153
|
.tree-node.disabled { opacity: 0.3; }
|
|
154
|
+
.tree-node.hidden-vis { opacity: 0.35; }
|
|
155
|
+
.tree-node.hidden-vis .tree-name { text-decoration: line-through; }
|
|
109
156
|
.tree-arrow { width: 12px; font-size: 7px; color: var(--border-bright); text-align: center; flex-shrink: 0; cursor: pointer; user-select: none; transition: color var(--transition); }
|
|
110
157
|
.tree-arrow:hover { color: var(--text); }
|
|
111
158
|
.tree-icon { width: 13px; height: 13px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
|
@@ -117,29 +164,60 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
117
164
|
.tree-badge { font-family: var(--mono); font-size: 7px; padding: 0 4px; border-radius: 2px; flex-shrink: 0; font-weight: 500; }
|
|
118
165
|
.badge-prefab { background: #d8e4f4; color: var(--c-prefab); }
|
|
119
166
|
|
|
120
|
-
/*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
167
|
+
/* Eye (visibility) toggle */
|
|
168
|
+
.tree-vis {
|
|
169
|
+
width: 14px; height: 14px; flex-shrink: 0; display: flex; align-items: center;
|
|
170
|
+
justify-content: center; cursor: pointer; color: var(--border-bright);
|
|
171
|
+
opacity: 0; transition: opacity var(--transition), color var(--transition);
|
|
172
|
+
}
|
|
173
|
+
.tree-vis svg { width: 11px; height: 11px; }
|
|
174
|
+
.tree-node:hover .tree-vis { opacity: 0.6; }
|
|
175
|
+
.tree-vis:hover { color: var(--text); opacity: 1 !important; }
|
|
176
|
+
.tree-vis.vis-off { opacity: 0.7; color: var(--c-disabled); }
|
|
177
|
+
.tree-node:hover .tree-vis.vis-off { opacity: 1; }
|
|
178
|
+
|
|
179
|
+
/* --- 3D Viewport (center) --- */
|
|
180
|
+
#viewport { flex: 1; position: relative; background: #f0f0f4; overflow: hidden; min-width: 0; user-select: none; -webkit-user-select: none; }
|
|
181
|
+
#viewport canvas { width: 100%; height: 100%; display: block; -webkit-user-drag: none; user-select: none; -webkit-user-select: none; touch-action: none; }
|
|
182
|
+
#scene-labels {
|
|
183
|
+
position: absolute; inset: 0; pointer-events: none; overflow: hidden;
|
|
184
|
+
}
|
|
185
|
+
.scene-label {
|
|
186
|
+
position: absolute; font-family: var(--mono); font-size: 9px; font-weight: 500;
|
|
187
|
+
white-space: nowrap; pointer-events: auto; cursor: pointer;
|
|
188
|
+
transform: translate(-50%, -100%) translateY(-8px);
|
|
189
|
+
text-shadow: 0 0 3px rgba(255,255,255,0.9), 0 0 6px rgba(255,255,255,0.5);
|
|
190
|
+
opacity: 0.7; transition: opacity 0.15s;
|
|
191
|
+
}
|
|
192
|
+
.scene-label:hover { opacity: 1; }
|
|
193
|
+
.scene-label.selected { opacity: 1; font-weight: 600; font-size: 10px; }
|
|
194
|
+
.grad-label {
|
|
195
|
+
position: absolute; font-family: var(--mono); font-size: 9px; font-weight: 500;
|
|
196
|
+
white-space: nowrap; pointer-events: none;
|
|
197
|
+
transform: translate(-50%, -50%);
|
|
198
|
+
opacity: 0.6;
|
|
199
|
+
}
|
|
200
|
+
.grad-label-unit { font-weight: 600; font-size: 10px; opacity: 0.7; }
|
|
201
|
+
|
|
202
|
+
/* --- Gizmo --- */
|
|
203
|
+
#gizmo {
|
|
204
|
+
position: absolute; top: 8px; right: 8px; width: 90px; height: 90px; z-index: 3;
|
|
205
|
+
}
|
|
206
|
+
#gizmo canvas { width: 100%; height: 100%; }
|
|
207
|
+
.gizmo-label {
|
|
208
|
+
position: absolute; font-family: var(--mono); font-size: 11px; font-weight: 700;
|
|
209
|
+
pointer-events: auto; cursor: pointer; user-select: none;
|
|
210
|
+
transform: translate(-50%, -50%);
|
|
211
|
+
width: 18px; height: 18px; line-height: 18px; text-align: center;
|
|
212
|
+
border-radius: 50%;
|
|
213
|
+
}
|
|
214
|
+
.gizmo-label:hover { text-decoration: underline; }
|
|
215
|
+
.gizmo-back {
|
|
216
|
+
font-size: 8px; font-weight: 500; opacity: 0.3;
|
|
139
217
|
}
|
|
140
218
|
#view-stats {
|
|
141
|
-
position: absolute; bottom:
|
|
142
|
-
font-family: var(--mono); font-size: 10px; background: rgba(255,255,255,0.
|
|
219
|
+
position: absolute; bottom: 8px; left: 10px; display: flex; gap: 12px; z-index: 2;
|
|
220
|
+
font-family: var(--mono); font-size: 10px; background: rgba(255,255,255,0.85); padding: 3px 10px; border-radius: 3px; border: 1px solid var(--border);
|
|
143
221
|
}
|
|
144
222
|
.stat-item { display: flex; align-items: baseline; gap: 4px; }
|
|
145
223
|
.stat-num { font-size: 13px; font-weight: 700; color: var(--text); }
|
|
@@ -147,8 +225,8 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
147
225
|
|
|
148
226
|
/* --- Properties panel (right) --- */
|
|
149
227
|
#props-panel {
|
|
150
|
-
width:
|
|
151
|
-
display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto;
|
|
228
|
+
width: 320px; background: var(--panel);
|
|
229
|
+
display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto; min-width: 180px;
|
|
152
230
|
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
|
153
231
|
}
|
|
154
232
|
#props-panel::-webkit-scrollbar { width: 5px; }
|
|
@@ -156,12 +234,12 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
156
234
|
#props-header { padding: 4px 10px; border-bottom: 1px solid var(--border); }
|
|
157
235
|
#props-header h2 { font-family: var(--mono); font-size: 9px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
|
|
158
236
|
#props-content { padding: 6px 10px; }
|
|
159
|
-
.prop-section { margin-bottom: 8px;
|
|
237
|
+
.prop-section { margin-bottom: 8px; }
|
|
160
238
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
161
239
|
.prop-section-title { font-family: var(--mono); font-size: 8px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3px; }
|
|
162
240
|
.prop-row { display: flex; justify-content: space-between; align-items: center; padding: 1px 0; border-bottom: 1px solid var(--surface); }
|
|
163
241
|
.prop-key { font-family: var(--mono); font-size: 10px; color: var(--text-dim); }
|
|
164
|
-
.prop-val { font-family: var(--mono); font-size: 10px; color: var(--text); text-align: right;
|
|
242
|
+
.prop-val { font-family: var(--mono); font-size: 10px; color: var(--text); text-align: right; word-break: break-all; }
|
|
165
243
|
.prop-val-color { display: inline-block; width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--border); vertical-align: middle; margin-right: 4px; }
|
|
166
244
|
.prop-hint { font-size: 11px; color: var(--text-dim); padding: 16px 0; text-align: center; }
|
|
167
245
|
|
|
@@ -176,6 +254,7 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
176
254
|
#welcome {
|
|
177
255
|
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
178
256
|
z-index: 10; background: var(--bg); animation: fadeIn 0.3s ease;
|
|
257
|
+
overflow-y: auto; padding: 24px 0;
|
|
179
258
|
}
|
|
180
259
|
#welcome.hidden { display: none; }
|
|
181
260
|
.welcome-box {
|
|
@@ -211,25 +290,50 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
211
290
|
|
|
212
291
|
<div id="welcome">
|
|
213
292
|
<div class="welcome-box">
|
|
214
|
-
<h1>Lens
|
|
293
|
+
<h1>Connect to Lens Studio</h1>
|
|
215
294
|
<div class="step">
|
|
216
295
|
<span class="step-check">✓</span>
|
|
217
|
-
<span class="step-text">
|
|
296
|
+
<span class="step-text">Start the inspector: <code>npx lens-inspector</code></span>
|
|
218
297
|
</div>
|
|
219
298
|
<div class="step">
|
|
220
299
|
<span class="step-num">2</span>
|
|
221
300
|
<span class="step-text">
|
|
222
|
-
Download <a href="/SceneInspector.ts" style="color: var(--
|
|
223
|
-
|
|
301
|
+
Download <a href="/SceneInspector.ts" style="color: var(--c-selected);">SceneInspector.ts</a> and drag it into your Lens Studio project's <strong>Assets</strong> panel.
|
|
302
|
+
Attach it to any SceneObject (e.g. Camera).
|
|
224
303
|
</span>
|
|
225
304
|
</div>
|
|
226
305
|
<div class="step">
|
|
227
306
|
<span class="step-num">3</span>
|
|
228
|
-
<span class="step-text">
|
|
307
|
+
<span class="step-text">
|
|
308
|
+
In Lens Studio: <strong>Project Settings > General > Experimental APIs</strong> → enable.
|
|
309
|
+
</span>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="step">
|
|
312
|
+
<span class="step-num">4</span>
|
|
313
|
+
<span class="step-text">Hit <strong>Play</strong> in Lens Studio. The scene appears here automatically. Component details require LS MCP (AI Assistant > MCP, port 8733).</span>
|
|
314
|
+
</div>
|
|
315
|
+
<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border);">
|
|
316
|
+
<div style="font-family:var(--mono);font-size:9px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;">Server</div>
|
|
317
|
+
<div style="display:flex;gap:6px;align-items:center;">
|
|
318
|
+
<input type="text" id="welcome-url" value="localhost:8200"
|
|
319
|
+
style="flex:1;padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:3px;font-family:var(--mono);font-size:11px;color:var(--text);outline:none;">
|
|
320
|
+
</div>
|
|
321
|
+
<div style="font-size:9px;color:var(--text-muted);margin-top:4px;">Change this if your inspector runs on a different port or machine.</div>
|
|
322
|
+
<div style="margin-top:8px;">
|
|
323
|
+
<div style="font-family:var(--mono);font-size:9px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;">MCP Config <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--text-muted)">(optional, for component details)</span></div>
|
|
324
|
+
<textarea id="mcp-config" rows="3" placeholder='Paste MCP config JSON from Lens Studio (AI Assistant > MCP)'
|
|
325
|
+
style="width:100%;padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:3px;font-family:var(--mono);font-size:10px;color:var(--text);outline:none;resize:vertical;line-height:1.4;"></textarea>
|
|
326
|
+
<div id="mcp-config-status" style="font-size:9px;color:var(--text-muted);margin-top:3px;">Copy from Lens Studio: AI Assistant > MCP config.</div>
|
|
327
|
+
</div>
|
|
328
|
+
<button id="server-connect-btn" style="margin-top:14px;width:100%;padding:8px 0;background:var(--c-selected);border:none;border-radius:4px;font-family:var(--font);font-size:12px;font-weight:600;cursor:pointer;color:#fff;">Connect</button>
|
|
229
329
|
</div>
|
|
230
|
-
<div class="welcome-status">
|
|
330
|
+
<div class="welcome-status" id="welcome-status">
|
|
231
331
|
<div class="welcome-spinner"></div>
|
|
232
|
-
<span>Waiting for Lens Studio
|
|
332
|
+
<span>Waiting for Lens Studio...</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="welcome-status" id="welcome-example" style="display:none">
|
|
335
|
+
<span style="color:var(--c-camera);font-weight:500">Viewing example scene.</span>
|
|
336
|
+
<span>Connect Lens Studio to see your real project.</span>
|
|
233
337
|
</div>
|
|
234
338
|
</div>
|
|
235
339
|
</div>
|
|
@@ -238,17 +342,14 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
238
342
|
<span class="toolbar-brand"><b>lens</b>-inspector</span>
|
|
239
343
|
<div class="sep"></div>
|
|
240
344
|
<span class="status-dot dot-off" id="ws-dot"></span>
|
|
241
|
-
<span id="status-text">
|
|
242
|
-
<
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
<button id="
|
|
246
|
-
<div class="sep"></div>
|
|
247
|
-
<label style="font-size:10px;color:var(--text-muted)">MCP:</label>
|
|
248
|
-
<input id="mcp-token" value="" placeholder="token" style="width:90px">
|
|
249
|
-
<button id="mcp-btn">Design-Time</button>
|
|
250
|
-
<span class="status-dot dot-off" id="mcp-dot"></span>
|
|
345
|
+
<span id="status-text">connecting...</span>
|
|
346
|
+
<span id="source-badge" class="badge-off"></span>
|
|
347
|
+
<span id="project-name"></span>
|
|
348
|
+
<div class="toolbar-spacer"></div>
|
|
349
|
+
<button id="setup-btn">How to connect</button>
|
|
251
350
|
</div>
|
|
351
|
+
<input type="hidden" id="ws-url" value="ws://localhost:8200">
|
|
352
|
+
<input type="hidden" id="mcp-base" value="http://localhost:8200">
|
|
252
353
|
|
|
253
354
|
<div id="main">
|
|
254
355
|
<div id="tree-panel">
|
|
@@ -257,22 +358,33 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
257
358
|
<span id="tree-count"></span>
|
|
258
359
|
</div>
|
|
259
360
|
<input type="text" id="tree-search" placeholder="Filter...">
|
|
361
|
+
<div id="tree-filters">
|
|
362
|
+
<button class="filter-chip active" data-cat="all">All</button>
|
|
363
|
+
<button class="filter-chip" data-cat="camera">Camera</button>
|
|
364
|
+
<button class="filter-chip" data-cat="visual">Visual</button>
|
|
365
|
+
<button class="filter-chip" data-cat="script">Script</button>
|
|
366
|
+
<button class="filter-chip" data-cat="light">Light</button>
|
|
367
|
+
<button class="filter-chip" data-cat="physics">Physics</button>
|
|
368
|
+
<button class="filter-chip" data-cat="audio">Audio</button>
|
|
369
|
+
<button class="filter-chip" data-cat="animation">Anim</button>
|
|
370
|
+
<button class="filter-chip" data-cat="ui">UI</button>
|
|
371
|
+
<button class="filter-chip" data-cat="tracking">Track</button>
|
|
372
|
+
<button class="filter-chip" data-cat="empty">Empty</button>
|
|
373
|
+
</div>
|
|
260
374
|
<div id="tree-list"></div>
|
|
261
375
|
</div>
|
|
376
|
+
<div class="resize-handle" id="resize-left"></div>
|
|
262
377
|
|
|
263
378
|
<div id="viewport">
|
|
264
379
|
<canvas id="scene-canvas"></canvas>
|
|
265
|
-
<div id="
|
|
266
|
-
|
|
267
|
-
<button class="view-btn" data-view="xy">FRONT</button>
|
|
268
|
-
<button class="view-btn" data-view="yz">SIDE</button>
|
|
269
|
-
</div>
|
|
270
|
-
<span id="view-info"></span>
|
|
380
|
+
<div id="scene-labels"></div>
|
|
381
|
+
<div id="gizmo"><canvas width="180" height="180"></canvas></div>
|
|
271
382
|
<div id="view-stats">
|
|
272
383
|
<div class="stat-item"><span class="stat-num" id="stat-objects">0</span><span class="stat-label">objects</span></div>
|
|
273
384
|
<div class="stat-item"><span class="stat-num" id="stat-components">0</span><span class="stat-label">components</span></div>
|
|
274
385
|
</div>
|
|
275
386
|
</div>
|
|
387
|
+
<div class="resize-handle" id="resize-right"></div>
|
|
276
388
|
|
|
277
389
|
<div id="props-panel">
|
|
278
390
|
<div id="props-header"><h2>Inspector</h2></div>
|
|
@@ -280,8 +392,11 @@ body { background: var(--bg); color: var(--text); font-family: var(--font); heig
|
|
|
280
392
|
</div>
|
|
281
393
|
</div>
|
|
282
394
|
|
|
283
|
-
<script>
|
|
284
|
-
|
|
395
|
+
<script type="module">
|
|
396
|
+
import * as THREE from 'three';
|
|
397
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
398
|
+
|
|
399
|
+
// --- Icons ---
|
|
285
400
|
const ICONS = {
|
|
286
401
|
camera: `<svg viewBox="0 0 12 12" fill="none"><rect x="1" y="3" width="7" height="6" rx="1" stroke="currentColor" stroke-width="1.2"/><path d="M8 5l3-1.5v5L8 7" stroke="currentColor" stroke-width="1.2"/></svg>`,
|
|
287
402
|
visual: `<svg viewBox="0 0 12 12" fill="none"><rect x="2" y="2" width="8" height="8" rx="1" fill="currentColor" opacity="0.6"/></svg>`,
|
|
@@ -302,61 +417,630 @@ const ICONS = {
|
|
|
302
417
|
unknown: `<svg viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="4" stroke="currentColor" stroke-width="1"/></svg>`,
|
|
303
418
|
};
|
|
304
419
|
|
|
420
|
+
const ICON_EYE = `<svg viewBox="0 0 12 12" fill="none"><path d="M1 6s2-3.5 5-3.5S11 6 11 6s-2 3.5-5 3.5S1 6 1 6z" stroke="currentColor" stroke-width="1" fill="none"/><circle cx="6" cy="6" r="1.5" fill="currentColor"/></svg>`;
|
|
421
|
+
const ICON_EYE_OFF = `<svg viewBox="0 0 12 12" fill="none"><path d="M1 6s2-3.5 5-3.5S11 6 11 6s-2 3.5-5 3.5S1 6 1 6z" stroke="currentColor" stroke-width="1" fill="none"/><line x1="2" y1="10" x2="10" y2="2" stroke="currentColor" stroke-width="1.2"/></svg>`;
|
|
422
|
+
|
|
305
423
|
const CAT_COLORS = {
|
|
306
|
-
camera: '#
|
|
307
|
-
audio: '#
|
|
308
|
-
interaction: '#
|
|
309
|
-
ml: '#
|
|
424
|
+
camera: '#ff9500', visual: '#0090ff', text: '#0080ff', script: '#ff2d20',
|
|
425
|
+
audio: '#ff6b00', light: '#ffc800', physics: '#ff1a1a', vfx: '#00a0ff',
|
|
426
|
+
interaction: '#ff5500', tracking: '#99a', animation: '#ff8800', ui: '#0070ff',
|
|
427
|
+
ml: '#5060ff', utility: '#88889a', empty: '#99a', prefab: '#00aaff', unknown: '#99a',
|
|
310
428
|
};
|
|
311
429
|
|
|
312
430
|
// --- State ---
|
|
313
|
-
let liveTree = null, mcpTree = null, flatNodes = [], selectedId = null;
|
|
314
|
-
let collapsedPaths = new Set(), searchFilter = '', ws = null;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
431
|
+
let liveTree = null, mcpTree = null, flatNodes = [], sceneNodes = [], selectedId = null;
|
|
432
|
+
let collapsedPaths = new Set(), hiddenPaths = new Set(), searchFilter = '', activeCategoryFilter = 'all', ws = null;
|
|
433
|
+
let dataSource = null; // 'example' | 'live' | null
|
|
434
|
+
|
|
435
|
+
// --- Three.js ---
|
|
436
|
+
let scene3d, camera3d, renderer3d, controls3d, raycaster, pointer, gridMat;
|
|
437
|
+
let sceneMeshes = []; // { group, boxMesh, node }
|
|
438
|
+
let labelEls = []; // { el, node }
|
|
439
|
+
const labelContainer = document.getElementById('scene-labels');
|
|
440
|
+
const tmpVec = new THREE.Vector3();
|
|
441
|
+
|
|
442
|
+
function initScene3D() {
|
|
443
|
+
const container = document.getElementById('viewport');
|
|
444
|
+
const canvas = document.getElementById('scene-canvas');
|
|
445
|
+
|
|
446
|
+
scene3d = new THREE.Scene();
|
|
447
|
+
|
|
448
|
+
// Gradient background (top: white, bottom: soft blue-gray)
|
|
449
|
+
const bgCanvas = document.createElement('canvas');
|
|
450
|
+
bgCanvas.width = 2; bgCanvas.height = 512;
|
|
451
|
+
const bgCtx = bgCanvas.getContext('2d');
|
|
452
|
+
const grad = bgCtx.createLinearGradient(0, 0, 0, 512);
|
|
453
|
+
grad.addColorStop(0, '#ffffff');
|
|
454
|
+
grad.addColorStop(0.5, '#f0f2f8');
|
|
455
|
+
grad.addColorStop(1, '#d8dce8');
|
|
456
|
+
bgCtx.fillStyle = grad;
|
|
457
|
+
bgCtx.fillRect(0, 0, 2, 512);
|
|
458
|
+
const bgTex = new THREE.CanvasTexture(bgCanvas);
|
|
459
|
+
bgTex.mapping = THREE.EquirectangularReflectionMapping;
|
|
460
|
+
scene3d.background = bgTex;
|
|
461
|
+
|
|
462
|
+
camera3d = new THREE.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.1, 2000);
|
|
463
|
+
camera3d.position.set(30, 25, 10);
|
|
464
|
+
|
|
465
|
+
renderer3d = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
466
|
+
renderer3d.setSize(container.clientWidth, container.clientHeight);
|
|
467
|
+
renderer3d.setPixelRatio(window.devicePixelRatio);
|
|
468
|
+
|
|
469
|
+
controls3d = new OrbitControls(camera3d, canvas);
|
|
470
|
+
controls3d.enableDamping = true;
|
|
471
|
+
controls3d.dampingFactor = 0.08;
|
|
472
|
+
controls3d.enablePan = true;
|
|
473
|
+
controls3d.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN };
|
|
474
|
+
controls3d.target.set(0, 0, 40);
|
|
475
|
+
|
|
476
|
+
// Infinite grid (multi-level shader, smooth transitions)
|
|
477
|
+
gridMat = new THREE.ShaderMaterial({
|
|
478
|
+
transparent: true, depthWrite: false, side: THREE.DoubleSide,
|
|
479
|
+
uniforms: { uCamDist: { value: 50.0 } },
|
|
480
|
+
vertexShader: `
|
|
481
|
+
varying vec3 vWorld;
|
|
482
|
+
void main() {
|
|
483
|
+
vWorld = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
484
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
485
|
+
}
|
|
486
|
+
`,
|
|
487
|
+
fragmentShader: `
|
|
488
|
+
varying vec3 vWorld;
|
|
489
|
+
uniform float uCamDist;
|
|
490
|
+
float gridLine(vec2 pos, float spacing) {
|
|
491
|
+
vec2 coord = pos / spacing;
|
|
492
|
+
vec2 g = abs(fract(coord - 0.5) - 0.5);
|
|
493
|
+
vec2 line = g / fwidth(coord);
|
|
494
|
+
return 1.0 - min(min(line.x, line.y), 1.0);
|
|
495
|
+
}
|
|
496
|
+
void main() {
|
|
497
|
+
float dist = length(vWorld.xz);
|
|
498
|
+
float fadeR = uCamDist * 3.0;
|
|
499
|
+
float fade = 1.0 - smoothstep(fadeR * 0.5, fadeR, dist);
|
|
500
|
+
if (fade < 0.01) discard;
|
|
501
|
+
|
|
502
|
+
float logD = log(uCamDist) / log(10.0);
|
|
503
|
+
float level = floor(logD);
|
|
504
|
+
float frac = logD - level;
|
|
505
|
+
|
|
506
|
+
float s0 = pow(10.0, level - 1.0);
|
|
507
|
+
float s1 = pow(10.0, level);
|
|
508
|
+
float s2 = pow(10.0, level + 1.0);
|
|
509
|
+
|
|
510
|
+
float g0 = gridLine(vWorld.xz, s0);
|
|
511
|
+
float g1 = gridLine(vWorld.xz, s1);
|
|
512
|
+
float g2 = gridLine(vWorld.xz, s2);
|
|
513
|
+
|
|
514
|
+
float a0 = g0 * 0.06 * (1.0 - frac);
|
|
515
|
+
float a1 = g1 * 0.18;
|
|
516
|
+
float a2 = g2 * (0.28 + 0.1 * frac);
|
|
517
|
+
|
|
518
|
+
float a = max(max(a0, a1), a2) * fade;
|
|
519
|
+
if (a < 0.01) discard;
|
|
520
|
+
gl_FragColor = vec4(0.55, 0.57, 0.63, a);
|
|
521
|
+
}
|
|
522
|
+
`,
|
|
523
|
+
});
|
|
524
|
+
const gridPlane = new THREE.Mesh(new THREE.PlaneGeometry(20000, 20000), gridMat);
|
|
525
|
+
gridPlane.rotation.x = -Math.PI / 2;
|
|
526
|
+
scene3d.add(gridPlane);
|
|
527
|
+
|
|
528
|
+
// Subtle axes at origin
|
|
529
|
+
const axLen = 8;
|
|
530
|
+
const axMat = (c) => new THREE.LineBasicMaterial({ color: c, transparent: true, opacity: 0.8 });
|
|
531
|
+
const mkAx = (dir, color) => {
|
|
532
|
+
const g = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), dir.clone().multiplyScalar(axLen)]);
|
|
533
|
+
return new THREE.Line(g, axMat(color));
|
|
534
|
+
};
|
|
535
|
+
scene3d.add(mkAx(new THREE.Vector3(1,0,0), 0xe03020));
|
|
536
|
+
scene3d.add(mkAx(new THREE.Vector3(0,1,0), 0xe0b010));
|
|
537
|
+
scene3d.add(mkAx(new THREE.Vector3(0,0,1), 0x1878e0));
|
|
538
|
+
|
|
539
|
+
raycaster = new THREE.Raycaster();
|
|
540
|
+
pointer = new THREE.Vector2();
|
|
541
|
+
|
|
542
|
+
// Click to select
|
|
543
|
+
canvas.addEventListener('pointerdown', onPointerDown);
|
|
544
|
+
canvas.addEventListener('pointermove', (e) => {
|
|
545
|
+
const rect = canvas.getBoundingClientRect();
|
|
546
|
+
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
547
|
+
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
window.addEventListener('resize', onResize);
|
|
551
|
+
onResize();
|
|
552
|
+
|
|
553
|
+
animate();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
let pointerDownPos = { x: 0, y: 0 };
|
|
557
|
+
function onPointerDown(e) {
|
|
558
|
+
pointerDownPos = { x: e.clientX, y: e.clientY };
|
|
559
|
+
const canvas = renderer3d.domElement;
|
|
560
|
+
canvas.addEventListener('pointerup', onPointerUp, { once: true });
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function onPointerUp(e) {
|
|
564
|
+
const dx = e.clientX - pointerDownPos.x, dy = e.clientY - pointerDownPos.y;
|
|
565
|
+
if (dx * dx + dy * dy > 25) return;
|
|
566
|
+
|
|
567
|
+
const rect = renderer3d.domElement.getBoundingClientRect();
|
|
568
|
+
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
569
|
+
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
570
|
+
|
|
571
|
+
raycaster.setFromCamera(pointer, camera3d);
|
|
572
|
+
const meshes = sceneMeshes.map(m => m.boxMesh);
|
|
573
|
+
const hits = raycaster.intersectObjects(meshes);
|
|
574
|
+
if (hits.length > 0) {
|
|
575
|
+
const hit = sceneMeshes.find(m => m.boxMesh === hits[0].object);
|
|
576
|
+
if (hit) {
|
|
577
|
+
selectedId = hit.node.id;
|
|
578
|
+
updateMeshStyles();
|
|
579
|
+
renderTree();
|
|
580
|
+
renderProps();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function onResize() {
|
|
586
|
+
const container = document.getElementById('viewport');
|
|
587
|
+
const w = container.clientWidth, h = container.clientHeight;
|
|
588
|
+
if (w <= 0 || h <= 0) return;
|
|
589
|
+
camera3d.aspect = w / h;
|
|
590
|
+
camera3d.updateProjectionMatrix();
|
|
591
|
+
renderer3d.setSize(w, h);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// --- Gizmo (navigation gnomon) ---
|
|
595
|
+
const gizmoEl = document.getElementById('gizmo');
|
|
596
|
+
const gizmoCanvas = gizmoEl.querySelector('canvas');
|
|
597
|
+
const gizmoCtx = gizmoCanvas.getContext('2d');
|
|
598
|
+
const gizmoDirs = [
|
|
599
|
+
{ label: 'X', dir: new THREE.Vector3(1,0,0), color: '#e03020' },
|
|
600
|
+
{ label: 'Y', dir: new THREE.Vector3(0,1,0), color: '#e0b010' },
|
|
601
|
+
{ label: 'Z', dir: new THREE.Vector3(0,0,1), color: '#1878e0' },
|
|
602
|
+
];
|
|
603
|
+
const gizmoLabels = [];
|
|
604
|
+
|
|
605
|
+
for (const d of gizmoDirs) {
|
|
606
|
+
const pos = document.createElement('div');
|
|
607
|
+
pos.className = 'gizmo-label';
|
|
608
|
+
pos.style.color = d.color;
|
|
609
|
+
pos.textContent = d.label;
|
|
610
|
+
pos.addEventListener('click', () => snapToView(d.dir.clone()));
|
|
611
|
+
gizmoEl.appendChild(pos);
|
|
612
|
+
gizmoLabels.push({ el: pos, dir: d.dir, color: d.color, back: false });
|
|
613
|
+
|
|
614
|
+
const neg = document.createElement('div');
|
|
615
|
+
neg.className = 'gizmo-label gizmo-back';
|
|
616
|
+
neg.style.color = d.color;
|
|
617
|
+
neg.textContent = '-' + d.label;
|
|
618
|
+
neg.addEventListener('click', () => snapToView(d.dir.clone().negate()));
|
|
619
|
+
gizmoEl.appendChild(neg);
|
|
620
|
+
gizmoLabels.push({ el: neg, dir: d.dir.clone().negate(), color: d.color, back: true });
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let snapAnimId = null;
|
|
624
|
+
function snapToView(dir) {
|
|
625
|
+
const dist = camera3d.position.distanceTo(controls3d.target);
|
|
626
|
+
const target = controls3d.target.clone();
|
|
627
|
+
const endPos = target.clone().add(dir.clone().multiplyScalar(dist));
|
|
628
|
+
const startPos = camera3d.position.clone();
|
|
629
|
+
const up = Math.abs(dir.y) > 0.9 ? new THREE.Vector3(0, 0, dir.y > 0 ? -1 : 1) : new THREE.Vector3(0, 1, 0);
|
|
630
|
+
const startTime = performance.now();
|
|
631
|
+
if (snapAnimId) cancelAnimationFrame(snapAnimId);
|
|
632
|
+
function step() {
|
|
633
|
+
const t = Math.min((performance.now() - startTime) / 350, 1);
|
|
634
|
+
const ease = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
635
|
+
camera3d.position.lerpVectors(startPos, endPos, ease);
|
|
636
|
+
camera3d.up.lerp(up, ease * 0.5 + 0.5);
|
|
637
|
+
camera3d.lookAt(target);
|
|
638
|
+
controls3d.update();
|
|
639
|
+
if (t < 1) snapAnimId = requestAnimationFrame(step);
|
|
640
|
+
else snapAnimId = null;
|
|
641
|
+
}
|
|
642
|
+
step();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function updateGizmo() {
|
|
646
|
+
const cx = 90, cy = 90, r = 32;
|
|
647
|
+
gizmoCtx.clearRect(0, 0, 180, 180);
|
|
648
|
+
const quat = camera3d.quaternion.clone().invert();
|
|
649
|
+
|
|
650
|
+
for (const d of gizmoDirs) {
|
|
651
|
+
const p = d.dir.clone().applyQuaternion(quat);
|
|
652
|
+
const n = d.dir.clone().negate().applyQuaternion(quat);
|
|
653
|
+
gizmoCtx.beginPath();
|
|
654
|
+
gizmoCtx.moveTo(cx + n.x * r, cy - n.y * r);
|
|
655
|
+
gizmoCtx.lineTo(cx + p.x * r, cy - p.y * r);
|
|
656
|
+
gizmoCtx.strokeStyle = d.color;
|
|
657
|
+
gizmoCtx.globalAlpha = 0.4;
|
|
658
|
+
gizmoCtx.lineWidth = 2;
|
|
659
|
+
gizmoCtx.stroke();
|
|
660
|
+
gizmoCtx.globalAlpha = 1;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
for (const gl of gizmoLabels) {
|
|
664
|
+
const p = gl.dir.clone().applyQuaternion(quat);
|
|
665
|
+
const x = 45 + p.x * 34;
|
|
666
|
+
const y = 45 - p.y * 34;
|
|
667
|
+
gl.el.style.left = x + 'px';
|
|
668
|
+
gl.el.style.top = y + 'px';
|
|
669
|
+
gl.el.style.opacity = gl.back ? (p.z > 0 ? '0.15' : '0.4') : (p.z > 0 ? '0.4' : '0.9');
|
|
670
|
+
gl.el.style.zIndex = p.z > 0 ? '0' : '1';
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function animate() {
|
|
675
|
+
requestAnimationFrame(animate);
|
|
676
|
+
updateFlyMovement();
|
|
677
|
+
controls3d.update();
|
|
678
|
+
rebuildGraduations();
|
|
679
|
+
renderer3d.render(scene3d, camera3d);
|
|
680
|
+
updateLabelPositions();
|
|
681
|
+
updateGradLabels();
|
|
682
|
+
updateGizmo();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let hasAutoFit = false;
|
|
686
|
+
|
|
687
|
+
function autoFitCamera() {
|
|
688
|
+
const nodes = sceneNodes.length ? sceneNodes : flatNodes;
|
|
689
|
+
if (nodes.length === 0) return;
|
|
690
|
+
const box = new THREE.Box3();
|
|
691
|
+
for (const node of nodes) {
|
|
692
|
+
const p = new THREE.Vector3(node.pos[0], node.pos[1], node.pos[2]);
|
|
693
|
+
const s = new THREE.Vector3(
|
|
694
|
+
Math.max(Math.abs(node.scale[0]), 0.3),
|
|
695
|
+
Math.max(Math.abs(node.scale[1]), 0.3),
|
|
696
|
+
Math.max(Math.abs(node.scale[2]), 0.3)
|
|
697
|
+
);
|
|
698
|
+
box.expandByPoint(p.clone().sub(s.clone().multiplyScalar(0.5)));
|
|
699
|
+
box.expandByPoint(p.clone().add(s.clone().multiplyScalar(0.5)));
|
|
700
|
+
}
|
|
701
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
702
|
+
const size = box.getSize(new THREE.Vector3());
|
|
703
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
704
|
+
const dist = maxDim * 1.2;
|
|
705
|
+
|
|
706
|
+
controls3d.target.copy(center);
|
|
707
|
+
camera3d.position.set(center.x + dist * 0.6, center.y + dist * 0.4, center.z - dist * 0.6);
|
|
708
|
+
controls3d.update();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// --- Build 3D scene from scene nodes (independent of tree collapse) ---
|
|
712
|
+
function buildScene3D() {
|
|
713
|
+
for (const m of sceneMeshes) {
|
|
714
|
+
scene3d.remove(m.group);
|
|
715
|
+
m.group.traverse(child => {
|
|
716
|
+
if (child.geometry) child.geometry.dispose();
|
|
717
|
+
if (child.material) child.material.dispose();
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
sceneMeshes = [];
|
|
721
|
+
|
|
722
|
+
for (const l of labelEls) l.el.remove();
|
|
723
|
+
labelEls = [];
|
|
724
|
+
|
|
725
|
+
const visibleNodes = sceneNodes.filter(n => !hiddenPaths.has(n.id));
|
|
726
|
+
|
|
727
|
+
for (const node of visibleNodes) {
|
|
728
|
+
const cat = node.isPrefab ? 'prefab' : node.category;
|
|
729
|
+
const hexColor = CAT_COLORS[cat] || '#909098';
|
|
730
|
+
const color = new THREE.Color(hexColor);
|
|
731
|
+
const sel = node.id === selectedId;
|
|
732
|
+
|
|
733
|
+
const group = new THREE.Group();
|
|
734
|
+
group.position.set(node.pos[0], node.pos[1], node.pos[2]);
|
|
735
|
+
|
|
736
|
+
const sx = Math.max(Math.abs(node.scale[0]), 0.3);
|
|
737
|
+
const sy = Math.max(Math.abs(node.scale[1]), 0.3);
|
|
738
|
+
const sz = Math.max(Math.abs(node.scale[2]), 0.3);
|
|
739
|
+
|
|
740
|
+
const geom = new THREE.BoxGeometry(sx, sy, sz);
|
|
741
|
+
|
|
742
|
+
const fillMat = new THREE.MeshBasicMaterial({
|
|
743
|
+
color, transparent: true, opacity: sel ? 0.3 : 0.15, depthWrite: false, side: THREE.DoubleSide,
|
|
744
|
+
});
|
|
745
|
+
const boxMesh = new THREE.Mesh(geom, fillMat);
|
|
746
|
+
group.add(boxMesh);
|
|
747
|
+
|
|
748
|
+
const edges = new THREE.EdgesGeometry(geom);
|
|
749
|
+
const edgeMat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: sel ? 1.0 : 0.65 });
|
|
750
|
+
const wire = new THREE.LineSegments(edges, edgeMat);
|
|
751
|
+
group.add(wire);
|
|
752
|
+
|
|
753
|
+
scene3d.add(group);
|
|
754
|
+
sceneMeshes.push({ group, boxMesh, wire, fillMat, edgeMat, node });
|
|
755
|
+
|
|
756
|
+
const label = document.createElement('div');
|
|
757
|
+
label.className = 'scene-label' + (sel ? ' selected' : '');
|
|
758
|
+
label.style.color = hexColor;
|
|
759
|
+
label.textContent = node.name;
|
|
760
|
+
label.dataset.id = node.id;
|
|
761
|
+
label.addEventListener('click', () => {
|
|
762
|
+
selectedId = node.id;
|
|
763
|
+
updateMeshStyles();
|
|
764
|
+
renderTree();
|
|
765
|
+
renderProps();
|
|
766
|
+
});
|
|
767
|
+
labelContainer.appendChild(label);
|
|
768
|
+
labelEls.push({ el: label, node });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (!hasAutoFit && sceneNodes.length > 0) {
|
|
772
|
+
hasAutoFit = true;
|
|
773
|
+
autoFitCamera();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function updateMeshStyles() {
|
|
778
|
+
for (const m of sceneMeshes) {
|
|
779
|
+
const sel = m.node.id === selectedId;
|
|
780
|
+
m.fillMat.opacity = sel ? 0.25 : 0.1;
|
|
781
|
+
m.edgeMat.opacity = sel ? 1.0 : 0.5;
|
|
782
|
+
}
|
|
783
|
+
for (const l of labelEls) {
|
|
784
|
+
l.el.classList.toggle('selected', l.node.id === selectedId);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function updateLabelPositions() {
|
|
789
|
+
if (!camera3d || !renderer3d) return;
|
|
790
|
+
const w = renderer3d.domElement.clientWidth;
|
|
791
|
+
const h = renderer3d.domElement.clientHeight;
|
|
792
|
+
|
|
793
|
+
const labelData = [];
|
|
794
|
+
for (const { el, node } of labelEls) {
|
|
795
|
+
const m = sceneMeshes.find(m => m.node.id === node.id);
|
|
796
|
+
if (!m) { el.style.display = 'none'; continue; }
|
|
797
|
+
|
|
798
|
+
tmpVec.copy(m.group.position);
|
|
799
|
+
const sy = Math.max(Math.abs(node.scale[1]), 0.3);
|
|
800
|
+
tmpVec.y += sy / 2;
|
|
801
|
+
tmpVec.project(camera3d);
|
|
802
|
+
|
|
803
|
+
if (tmpVec.z > 1) { el.style.display = 'none'; continue; }
|
|
804
|
+
|
|
805
|
+
const x = (tmpVec.x * 0.5 + 0.5) * w;
|
|
806
|
+
const y = (-tmpVec.y * 0.5 + 0.5) * h;
|
|
807
|
+
const dist = camera3d.position.distanceTo(m.group.position);
|
|
808
|
+
const maxDist = 150;
|
|
809
|
+
|
|
810
|
+
if (dist > maxDist && node.id !== selectedId) { el.style.display = 'none'; continue; }
|
|
811
|
+
|
|
812
|
+
labelData.push({ el, node, x, y, dist, sel: node.id === selectedId });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
labelData.sort((a, b) => {
|
|
816
|
+
if (a.sel !== b.sel) return a.sel ? -1 : 1;
|
|
817
|
+
return a.dist - b.dist;
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const placed = [];
|
|
821
|
+
const labelH = 14;
|
|
822
|
+
const pad = 4;
|
|
823
|
+
|
|
824
|
+
for (const d of labelData) {
|
|
825
|
+
const labelW = d.node.name.length * 6.5 + 8;
|
|
826
|
+
const candidates = [
|
|
827
|
+
{ x: d.x - labelW / 2, y: d.y - labelH - 8 },
|
|
828
|
+
{ x: d.x - labelW - 4, y: d.y - labelH - 4 },
|
|
829
|
+
{ x: d.x + 4, y: d.y - labelH - 4 },
|
|
830
|
+
{ x: d.x - labelW / 2, y: d.y + 4 },
|
|
831
|
+
];
|
|
324
832
|
|
|
325
|
-
|
|
326
|
-
|
|
833
|
+
let ok = false;
|
|
834
|
+
for (const c of candidates) {
|
|
835
|
+
const overlaps = placed.some(p =>
|
|
836
|
+
c.x < p.x + p.w + pad && c.x + labelW + pad > p.x &&
|
|
837
|
+
c.y < p.y + p.h + pad && c.y + labelH + pad > p.y
|
|
838
|
+
);
|
|
839
|
+
if (!overlaps || d.sel) {
|
|
840
|
+
d.el.style.display = '';
|
|
841
|
+
d.el.style.left = (c.x + labelW / 2) + 'px';
|
|
842
|
+
d.el.style.top = (c.y + labelH) + 'px';
|
|
843
|
+
placed.push({ x: c.x, y: c.y, w: labelW, h: labelH });
|
|
844
|
+
ok = true;
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (!ok) {
|
|
850
|
+
d.el.style.display = d.sel ? '' : 'none';
|
|
851
|
+
if (d.sel) {
|
|
852
|
+
d.el.style.left = d.x + 'px';
|
|
853
|
+
d.el.style.top = (d.y - labelH - 8) + 'px';
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// --- Adaptive Graduations ---
|
|
860
|
+
let gradLabelEls = [];
|
|
861
|
+
let gradGroup = null;
|
|
862
|
+
let lastGradSpacing = 0;
|
|
863
|
+
|
|
864
|
+
const GRAD_AXES = [
|
|
865
|
+
{ name: 'X', color: 0xe03020, css: '#e03020', dir: new THREE.Vector3(1,0,0), perp: new THREE.Vector3(0,1,0) },
|
|
866
|
+
{ name: 'Y', color: 0xe0b010, css: '#e0b010', dir: new THREE.Vector3(0,1,0), perp: new THREE.Vector3(1,0,0) },
|
|
867
|
+
{ name: 'Z', color: 0x1878e0, css: '#1878e0', dir: new THREE.Vector3(0,0,1), perp: new THREE.Vector3(0,1,0) },
|
|
868
|
+
];
|
|
869
|
+
|
|
870
|
+
function niceNum(v) {
|
|
871
|
+
const exp = Math.floor(Math.log10(v));
|
|
872
|
+
const frac = v / Math.pow(10, exp);
|
|
873
|
+
const nice = frac < 1.5 ? 1 : frac < 3.5 ? 2 : frac < 7.5 ? 5 : 10;
|
|
874
|
+
return nice * Math.pow(10, exp);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function formatCm(v, spacing) {
|
|
878
|
+
if (v === 0) return '0';
|
|
879
|
+
if (spacing >= 100) return (v / 100) + ' m';
|
|
880
|
+
if (spacing < 1) return Math.round(v * 10) + ' mm';
|
|
881
|
+
if (Number.isInteger(v)) return v + ' cm';
|
|
882
|
+
return +v.toFixed(1) + ' cm';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function rebuildGraduations() {
|
|
886
|
+
if (!camera3d || !controls3d) return;
|
|
887
|
+
const dist = camera3d.position.distanceTo(controls3d.target);
|
|
888
|
+
const majorSpacing = niceNum(dist * 0.25);
|
|
889
|
+
|
|
890
|
+
if (gridMat) gridMat.uniforms.uCamDist.value = dist;
|
|
891
|
+
|
|
892
|
+
if (majorSpacing === lastGradSpacing) return;
|
|
893
|
+
lastGradSpacing = majorSpacing;
|
|
894
|
+
|
|
895
|
+
if (gradGroup) {
|
|
896
|
+
scene3d.remove(gradGroup);
|
|
897
|
+
gradGroup.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); });
|
|
898
|
+
}
|
|
899
|
+
for (const gl of gradLabelEls) gl.el.remove();
|
|
900
|
+
gradLabelEls = [];
|
|
901
|
+
gradGroup = new THREE.Group();
|
|
902
|
+
|
|
903
|
+
const minorSpacing = majorSpacing / 5;
|
|
904
|
+
const range = majorSpacing * 10;
|
|
905
|
+
const nTicks = Math.round(range / minorSpacing);
|
|
906
|
+
const majorTickSize = Math.max(majorSpacing * 0.06, 0.5);
|
|
907
|
+
const minorTickSize = majorTickSize * 0.5;
|
|
908
|
+
|
|
909
|
+
for (const ax of GRAD_AXES) {
|
|
910
|
+
const lineMat = new THREE.LineBasicMaterial({ color: ax.color, transparent: true, opacity: 0.2 });
|
|
911
|
+
const majorTickMat = new THREE.LineBasicMaterial({ color: ax.color, transparent: true, opacity: 0.5 });
|
|
912
|
+
const minorTickMat = new THREE.LineBasicMaterial({ color: ax.color, transparent: true, opacity: 0.2 });
|
|
913
|
+
|
|
914
|
+
gradGroup.add(new THREE.Line(
|
|
915
|
+
new THREE.BufferGeometry().setFromPoints([ax.dir.clone().multiplyScalar(-range), ax.dir.clone().multiplyScalar(range)]),
|
|
916
|
+
lineMat
|
|
917
|
+
));
|
|
918
|
+
|
|
919
|
+
const majorPts = [], minorPts = [];
|
|
920
|
+
for (let i = -nTicks; i <= nTicks; i++) {
|
|
921
|
+
if (i === 0) continue;
|
|
922
|
+
const v = i * minorSpacing;
|
|
923
|
+
const isMajor = (i % 5) === 0;
|
|
924
|
+
const ts = isMajor ? majorTickSize : minorTickSize;
|
|
925
|
+
const center = ax.dir.clone().multiplyScalar(v);
|
|
926
|
+
majorPts.push(
|
|
927
|
+
...(isMajor ? [center.clone().add(ax.perp.clone().multiplyScalar(ts)), center.clone().sub(ax.perp.clone().multiplyScalar(ts))] : [])
|
|
928
|
+
);
|
|
929
|
+
if (!isMajor) {
|
|
930
|
+
minorPts.push(center.clone().add(ax.perp.clone().multiplyScalar(ts)), center.clone().sub(ax.perp.clone().multiplyScalar(ts)));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (isMajor) {
|
|
934
|
+
const label = document.createElement('div');
|
|
935
|
+
label.className = 'grad-label';
|
|
936
|
+
label.style.color = ax.css;
|
|
937
|
+
label.textContent = formatCm(v, majorSpacing);
|
|
938
|
+
labelContainer.appendChild(label);
|
|
939
|
+
const lPos = center.clone().add(ax.perp.clone().multiplyScalar(ts + Math.max(majorSpacing * 0.04, 0.8)));
|
|
940
|
+
gradLabelEls.push({ el: label, pos: lPos });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (majorPts.length) gradGroup.add(new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(majorPts), majorTickMat));
|
|
945
|
+
if (minorPts.length) gradGroup.add(new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(minorPts), minorTickMat));
|
|
946
|
+
|
|
947
|
+
const endLabel = document.createElement('div');
|
|
948
|
+
endLabel.className = 'grad-label grad-label-unit';
|
|
949
|
+
endLabel.style.color = ax.css;
|
|
950
|
+
endLabel.textContent = ax.name;
|
|
951
|
+
labelContainer.appendChild(endLabel);
|
|
952
|
+
gradLabelEls.push({ el: endLabel, pos: ax.dir.clone().multiplyScalar(range + majorSpacing * 0.5) });
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
scene3d.add(gradGroup);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function updateGradLabels() {
|
|
959
|
+
if (!camera3d || !renderer3d) return;
|
|
960
|
+
const w = renderer3d.domElement.clientWidth;
|
|
961
|
+
const h = renderer3d.domElement.clientHeight;
|
|
962
|
+
|
|
963
|
+
const items = [];
|
|
964
|
+
for (const gl of gradLabelEls) {
|
|
965
|
+
tmpVec.copy(gl.pos);
|
|
966
|
+
tmpVec.project(camera3d);
|
|
967
|
+
if (tmpVec.z > 1) { gl.el.style.display = 'none'; continue; }
|
|
968
|
+
const x = (tmpVec.x * 0.5 + 0.5) * w;
|
|
969
|
+
const y = (-tmpVec.y * 0.5 + 0.5) * h;
|
|
970
|
+
if (x < -30 || x > w + 30 || y < -30 || y > h + 30) { gl.el.style.display = 'none'; continue; }
|
|
971
|
+
const dist = camera3d.position.distanceTo(gl.pos);
|
|
972
|
+
items.push({ gl, x, y, dist });
|
|
973
|
+
}
|
|
974
|
+
items.sort((a, b) => a.dist - b.dist);
|
|
975
|
+
|
|
976
|
+
const placed = [];
|
|
977
|
+
const lh = 12, pad = 3;
|
|
978
|
+
for (const it of items) {
|
|
979
|
+
const lw = it.gl.el.textContent.length * 6 + 4;
|
|
980
|
+
const rx = it.x - lw / 2, ry = it.y - lh / 2;
|
|
981
|
+
const overlaps = placed.some(p =>
|
|
982
|
+
rx < p.x + p.w + pad && rx + lw + pad > p.x &&
|
|
983
|
+
ry < p.y + p.h + pad && ry + lh + pad > p.y
|
|
984
|
+
);
|
|
985
|
+
if (overlaps) { it.gl.el.style.display = 'none'; continue; }
|
|
986
|
+
it.gl.el.style.display = '';
|
|
987
|
+
it.gl.el.style.left = it.x + 'px';
|
|
988
|
+
it.gl.el.style.top = it.y + 'px';
|
|
989
|
+
placed.push({ x: rx, y: ry, w: lw, h: lh });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// --- Classify a node from its components ---
|
|
994
|
+
function classifyNode(node, components) {
|
|
995
|
+
let category = node.category || 'empty';
|
|
996
|
+
let hasVisual = node.hasVisual || false;
|
|
997
|
+
if (components.length > 0 && (category === 'empty' || category === 'unknown')) {
|
|
998
|
+
const types = components.map(c => c.type || '');
|
|
999
|
+
if (types.some(t => t.includes('Camera'))) category = 'camera';
|
|
1000
|
+
else if (types.some(t => t.includes('Script') || t.includes('Inspector') || t.includes('Spawner'))) category = 'script';
|
|
1001
|
+
else if (types.some(t => t.includes('Light') || t === 'LightSource' || t.includes('DirectionalLight') || t.includes('PointLight') || t.includes('AmbientLight'))) category = 'light';
|
|
1002
|
+
else if (types.some(t => t.includes('Audio'))) category = 'audio';
|
|
1003
|
+
else if (types.some(t => t.includes('Animation') || t.includes('BlendShapes'))) category = 'animation';
|
|
1004
|
+
else if (types.some(t => t.includes('Body') || t.includes('Collider') || t.includes('Physics'))) category = 'physics';
|
|
1005
|
+
else if (types.some(t => t.includes('VFX') || t.includes('Particle'))) category = 'vfx';
|
|
1006
|
+
else if (types.some(t => t.includes('Interaction') || t.includes('Manipulate'))) category = 'interaction';
|
|
1007
|
+
else if (types.some(t => t.includes('Tracking') || t.includes('DeviceTracking') || t.includes('ObjectTracking') || t.includes('Head'))) category = 'tracking';
|
|
1008
|
+
else if (types.some(t => t.includes('ScreenTransform') || t.includes('Canvas') || t.includes('ScreenRegion'))) category = 'ui';
|
|
1009
|
+
else if (types.some(t => t.includes('ML') || t.includes('SnapML'))) category = 'ml';
|
|
1010
|
+
else if (types.some(t => t.includes('RenderMeshVisual') || t.includes('Image') || t.includes('FaceMask') || t.includes('SpriteVisual'))) { category = 'visual'; hasVisual = true; }
|
|
1011
|
+
else if (types.some(t => t.includes('Text'))) category = 'text';
|
|
1012
|
+
}
|
|
1013
|
+
return { category, hasVisual };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// --- Flatten tree ---
|
|
1017
|
+
// respectCollapse=true for tree display, false for 3D scene (shows everything)
|
|
1018
|
+
function flattenTree(roots, source, respectCollapse) {
|
|
327
1019
|
const result = [];
|
|
328
1020
|
function walk(node, depth, path) {
|
|
329
1021
|
const id = source + ':' + path;
|
|
330
1022
|
const hasChildren = node.children && node.children.length > 0;
|
|
331
1023
|
const collapsed = collapsedPaths.has(id);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
else if (types.some(t => t.includes('Light'))) category = 'light';
|
|
338
|
-
else if (types.some(t => t.includes('Audio'))) category = 'audio';
|
|
339
|
-
else if (types.some(t => t.includes('Animation'))) category = 'animation';
|
|
340
|
-
else if (types.some(t => t.includes('Body') || t.includes('Collider') || t.includes('Physics'))) category = 'physics';
|
|
341
|
-
else if (types.some(t => t.includes('VFX') || t.includes('Particle'))) category = 'vfx';
|
|
342
|
-
else if (types.some(t => t.includes('Interaction') || t.includes('Manipulate'))) category = 'interaction';
|
|
343
|
-
else if (types.some(t => t.includes('Tracking') || t.includes('DeviceTracking') || t.includes('Head'))) category = 'tracking';
|
|
344
|
-
else if (types.some(t => t.includes('ScreenTransform') || t.includes('Canvas') || t.includes('ScreenRegion'))) category = 'ui';
|
|
345
|
-
else if (types.some(t => t.includes('ML'))) category = 'ml';
|
|
346
|
-
else if (node.hasVisual) category = 'visual';
|
|
347
|
-
else if (node.text) category = 'text';
|
|
1024
|
+
|
|
1025
|
+
// Get components: live data first, MCP enrichment if empty
|
|
1026
|
+
let components = node.components || [];
|
|
1027
|
+
if (components.length === 0 && mcpComponentMap && mcpComponentMap[node.name]) {
|
|
1028
|
+
components = mcpComponentMap[node.name].components;
|
|
348
1029
|
}
|
|
1030
|
+
|
|
1031
|
+
const { category, hasVisual } = classifyNode(node, components);
|
|
1032
|
+
|
|
349
1033
|
const isPrefab = node.isPrefab || (node.name && node.name.startsWith('[Prefab]'));
|
|
350
1034
|
result.push({
|
|
351
1035
|
id, name: node.name || '(unnamed)', depth, hasChildren, collapsed,
|
|
352
1036
|
enabled: node.enabled !== false, category, source, isPrefab,
|
|
353
1037
|
text: node.text || null,
|
|
354
1038
|
pos: node.pos || [0,0,0], scale: node.scale || [1,1,1], rot: node.rot || [0,0,0,1],
|
|
355
|
-
components
|
|
1039
|
+
components, hasVisual,
|
|
356
1040
|
color: node.color || null, layer: node.layer || null,
|
|
357
1041
|
childCount: node.children ? node.children.length : 0,
|
|
358
1042
|
});
|
|
359
|
-
if (hasChildren && !collapsed) {
|
|
1043
|
+
if (hasChildren && (!respectCollapse || !collapsed)) {
|
|
360
1044
|
for (let i = 0; i < node.children.length; i++) walk(node.children[i], depth+1, path+'/'+(node.children[i].name||i));
|
|
361
1045
|
}
|
|
362
1046
|
}
|
|
@@ -366,20 +1050,31 @@ function flattenTree(roots, source) {
|
|
|
366
1050
|
|
|
367
1051
|
function rebuildFlat() {
|
|
368
1052
|
const tree = liveTree || mcpTree;
|
|
369
|
-
if (!tree) { flatNodes = []; return; }
|
|
370
|
-
|
|
371
|
-
|
|
1053
|
+
if (!tree) { flatNodes = []; sceneNodes = []; return; }
|
|
1054
|
+
const source = liveTree ? 'live' : 'mcp';
|
|
1055
|
+
|
|
1056
|
+
// Full walk for 3D scene (ignores tree collapse state)
|
|
1057
|
+
sceneNodes = flattenTree(tree, source, false);
|
|
1058
|
+
|
|
1059
|
+
// Tree walk (respects collapse for display)
|
|
1060
|
+
flatNodes = flattenTree(tree, source, true);
|
|
1061
|
+
|
|
1062
|
+
// Apply text search + category filter to tree display only
|
|
1063
|
+
if (searchFilter || activeCategoryFilter !== 'all') {
|
|
372
1064
|
const q = searchFilter.toLowerCase();
|
|
373
|
-
flatNodes = flatNodes.filter(n =>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1065
|
+
flatNodes = flatNodes.filter(n => {
|
|
1066
|
+
const matchSearch = !searchFilter ||
|
|
1067
|
+
n.name.toLowerCase().includes(q) || (n.text && n.text.toLowerCase().includes(q)) ||
|
|
1068
|
+
n.components.some(c => (c.type||'').toLowerCase().includes(q)) || n.category.includes(q);
|
|
1069
|
+
const matchCat = activeCategoryFilter === 'all' || n.category === activeCategoryFilter;
|
|
1070
|
+
return matchSearch && matchCat;
|
|
1071
|
+
});
|
|
377
1072
|
}
|
|
378
|
-
|
|
379
|
-
|
|
1073
|
+
|
|
1074
|
+
document.getElementById('tree-count').textContent = flatNodes.length + ' / ' + sceneNodes.length;
|
|
380
1075
|
let totalComp = 0;
|
|
381
|
-
|
|
382
|
-
document.getElementById('stat-objects').textContent =
|
|
1076
|
+
sceneNodes.forEach(n => totalComp += n.components.length);
|
|
1077
|
+
document.getElementById('stat-objects').textContent = sceneNodes.length;
|
|
383
1078
|
document.getElementById('stat-components').textContent = totalComp;
|
|
384
1079
|
}
|
|
385
1080
|
|
|
@@ -395,30 +1090,51 @@ function renderTree() {
|
|
|
395
1090
|
const textPreview = n.text ? `<span class="tree-text-preview">"${esc(n.text.substring(0,14))}"</span>` : '';
|
|
396
1091
|
const sel = n.id === selectedId ? ' selected' : '';
|
|
397
1092
|
const dis = n.enabled ? '' : ' disabled';
|
|
1093
|
+
const visHidden = hiddenPaths.has(n.id);
|
|
1094
|
+
const visClass = visHidden ? ' hidden-vis' : '';
|
|
398
1095
|
const cc = n.components.length > 1 ? `<span class="tree-comp-count">${n.components.length}</span>` : '';
|
|
399
|
-
|
|
1096
|
+
const eye = `<span class="tree-vis${visHidden ? ' vis-off' : ''}" data-vis="${esc(n.id)}">${visHidden ? ICON_EYE_OFF : ICON_EYE}</span>`;
|
|
1097
|
+
return `<div class="tree-node${sel}${dis}${visClass}" data-id="${esc(n.id)}" style="padding-left:${indent+4}px">
|
|
400
1098
|
<span class="tree-arrow" data-toggle="${esc(n.id)}">${arrowChar}</span>
|
|
401
1099
|
<span class="tree-icon" style="color:${iconColor}">${ICONS[cat]||ICONS.empty}</span>
|
|
402
|
-
<span class="${nameClass}">${esc(n.name)}</span>${textPreview}${cc}
|
|
1100
|
+
<span class="${nameClass}">${esc(n.name)}</span>${textPreview}${cc}${eye}
|
|
403
1101
|
</div>`;
|
|
404
1102
|
}).join('');
|
|
405
1103
|
list.innerHTML = html || '<p class="prop-hint">No scene data</p>';
|
|
406
1104
|
list.onclick = (e) => {
|
|
1105
|
+
// Eye toggle (visibility)
|
|
1106
|
+
const vis = e.target.closest('[data-vis]');
|
|
1107
|
+
if (vis) {
|
|
1108
|
+
const id = vis.dataset.vis;
|
|
1109
|
+
if (hiddenPaths.has(id)) hiddenPaths.delete(id); else hiddenPaths.add(id);
|
|
1110
|
+
renderTree();
|
|
1111
|
+
buildScene3D();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
// Arrow toggle (collapse)
|
|
407
1115
|
const toggle = e.target.closest('[data-toggle]');
|
|
408
|
-
if (toggle) {
|
|
1116
|
+
if (toggle) {
|
|
1117
|
+
const id = toggle.dataset.toggle;
|
|
1118
|
+
if (collapsedPaths.has(id)) collapsedPaths.delete(id); else collapsedPaths.add(id);
|
|
1119
|
+
rebuildFlat(); renderTree();
|
|
1120
|
+
// NOTE: collapsing no longer affects 3D scene
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
// Row click (select)
|
|
409
1124
|
const row = e.target.closest('.tree-node');
|
|
410
|
-
if (row) { selectedId = row.dataset.id;
|
|
1125
|
+
if (row) { selectedId = row.dataset.id; updateMeshStyles(); renderTree(); renderProps(); }
|
|
411
1126
|
};
|
|
412
1127
|
}
|
|
413
1128
|
|
|
414
1129
|
// --- Properties ---
|
|
415
1130
|
function renderProps() {
|
|
416
1131
|
const el = document.getElementById('props-content');
|
|
417
|
-
|
|
1132
|
+
// Look in both flatNodes (tree) and sceneNodes (full) since tree might be filtered
|
|
1133
|
+
const node = flatNodes.find(n => n.id === selectedId) || sceneNodes.find(n => n.id === selectedId);
|
|
418
1134
|
if (!node) { el.innerHTML = '<p class="prop-hint">Select a node</p>'; return; }
|
|
419
1135
|
const cat = node.isPrefab ? 'prefab' : node.category;
|
|
420
1136
|
const cc = CAT_COLORS[cat] || CAT_COLORS.empty;
|
|
421
|
-
let h = `<div style="margin-bottom:10px;display:flex;align-items:center;gap:6px
|
|
1137
|
+
let h = `<div style="margin-bottom:10px;display:flex;align-items:center;gap:6px">
|
|
422
1138
|
<span class="tree-icon" style="color:${cc}">${ICONS[cat]||ICONS.empty}</span>
|
|
423
1139
|
<span style="font-weight:600;font-size:12px">${esc(node.name)}</span>
|
|
424
1140
|
</div>`;
|
|
@@ -448,213 +1164,340 @@ function renderProps() {
|
|
|
448
1164
|
el.innerHTML = h;
|
|
449
1165
|
}
|
|
450
1166
|
|
|
451
|
-
// ---
|
|
452
|
-
function
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1167
|
+
// --- Server URL ---
|
|
1168
|
+
function getServerHost() {
|
|
1169
|
+
return localStorage.getItem('lens-inspector-host') || location.host || 'localhost:8200';
|
|
1170
|
+
}
|
|
1171
|
+
function applyServerUrl() {
|
|
1172
|
+
const input = document.getElementById('welcome-url');
|
|
1173
|
+
let host = input.value.trim().replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
1174
|
+
if (!host) host = 'localhost:8200';
|
|
1175
|
+
localStorage.setItem('lens-inspector-host', host);
|
|
1176
|
+
applyMcpConfig();
|
|
1177
|
+
input.value = host;
|
|
1178
|
+
document.getElementById('ws-url').value = 'ws://' + host;
|
|
1179
|
+
document.getElementById('mcp-base').value = 'http://' + host;
|
|
1180
|
+
liveTree = null; mcpTree = null; mcpComponentMap = null; hasFetchedMCP = false;
|
|
1181
|
+
dataSource = null;
|
|
1182
|
+
connectWS();
|
|
1183
|
+
}
|
|
1184
|
+
// Set initial values from storage
|
|
1185
|
+
{
|
|
1186
|
+
const h = getServerHost();
|
|
1187
|
+
document.getElementById('ws-url').value = 'ws://' + h;
|
|
1188
|
+
document.getElementById('mcp-base').value = 'http://' + h;
|
|
1189
|
+
const welcomeInput = document.getElementById('welcome-url');
|
|
1190
|
+
if (welcomeInput) welcomeInput.value = h;
|
|
1191
|
+
const savedConfig = localStorage.getItem('lens-inspector-mcp-config');
|
|
1192
|
+
const configArea = document.getElementById('mcp-config');
|
|
1193
|
+
if (savedConfig && configArea) {
|
|
1194
|
+
configArea.value = savedConfig;
|
|
1195
|
+
applyMcpConfig();
|
|
475
1196
|
}
|
|
1197
|
+
}
|
|
476
1198
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// Axis labels
|
|
483
|
-
ctx.font = '600 9px "IBM Plex Mono", monospace'; ctx.fillStyle = '#a0a0aa';
|
|
484
|
-
ctx.textAlign = 'right'; ctx.textBaseline = 'bottom';
|
|
485
|
-
ctx.fillText('+' + v.label[0], w-6, cy-4);
|
|
486
|
-
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
|
487
|
-
ctx.fillText('+' + v.label[1], cx+4, 4);
|
|
488
|
-
|
|
489
|
-
// Nodes: first pass draws boxes, second pass draws labels with collision avoidance
|
|
490
|
-
const nodeRects = [];
|
|
491
|
-
for (const node of flatNodes) {
|
|
492
|
-
const px = cx + node.pos[ax] * v.scale;
|
|
493
|
-
const py = flipY ? cy - node.pos[ay] * v.scale : cy + node.pos[ay] * v.scale;
|
|
494
|
-
const sw = Math.max(Math.abs(node.scale[ax]), 0.4) * v.scale;
|
|
495
|
-
const sh = Math.max(Math.abs(node.scale[ay]), 0.4) * v.scale;
|
|
496
|
-
const sel = node.id === selectedId;
|
|
497
|
-
const cat = node.isPrefab ? 'prefab' : node.category;
|
|
498
|
-
const catColor = CAT_COLORS[cat] || '#909098';
|
|
499
|
-
|
|
500
|
-
// Fill
|
|
501
|
-
if (node.hasVisual && node.color) {
|
|
502
|
-
const [r,g,b,a] = node.color;
|
|
503
|
-
ctx.fillStyle = `rgba(${r},${g},${b},${Math.max((a||255)/255*0.2, 0.05)})`;
|
|
504
|
-
ctx.fillRect(px-sw/2, py-sh/2, sw, sh);
|
|
505
|
-
} else if (node.hasVisual || (cat !== 'empty' && cat !== 'unknown')) {
|
|
506
|
-
ctx.fillStyle = catColor + '12';
|
|
507
|
-
ctx.fillRect(px-sw/2, py-sh/2, sw, sh);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Outline
|
|
511
|
-
if (sel) { ctx.strokeStyle = '#1850d0'; ctx.lineWidth = 2.5; ctx.setLineDash([]); }
|
|
512
|
-
else if (node.hasVisual || (cat !== 'empty' && cat !== 'unknown')) { ctx.strokeStyle = catColor + '70'; ctx.lineWidth = 1.5; ctx.setLineDash([]); }
|
|
513
|
-
else { ctx.strokeStyle = '#c8c8d0'; ctx.lineWidth = 1; ctx.setLineDash([3,3]); }
|
|
514
|
-
ctx.strokeRect(px-sw/2, py-sh/2, sw, sh);
|
|
515
|
-
ctx.setLineDash([]);
|
|
1199
|
+
// --- MCP enrichment ---
|
|
1200
|
+
let mcpComponentMap = null;
|
|
1201
|
+
let hasFetchedMCP = false;
|
|
1202
|
+
let parsedMcpConfig = null;
|
|
516
1203
|
|
|
517
|
-
|
|
1204
|
+
function parseMcpConfig(raw) {
|
|
1205
|
+
try {
|
|
1206
|
+
const obj = JSON.parse(raw);
|
|
1207
|
+
const servers = obj.servers || obj;
|
|
1208
|
+
const key = Object.keys(servers)[0];
|
|
1209
|
+
if (!key) return null;
|
|
1210
|
+
const srv = servers[key];
|
|
1211
|
+
if (!srv.url) return null;
|
|
1212
|
+
return { url: srv.url, headers: srv.headers || {}, name: key };
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
return null;
|
|
518
1215
|
}
|
|
1216
|
+
}
|
|
519
1217
|
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
{ x: px - sw/2, y: py - sh/2 - th - 2 },
|
|
537
|
-
{ x: px + sw/2 - tw, y: py - sh/2 - th - 2 },
|
|
538
|
-
{ x: px - sw/2, y: py + sh/2 + 2 },
|
|
539
|
-
{ x: px + sw/2 - tw, y: py + sh/2 + 2 },
|
|
540
|
-
];
|
|
541
|
-
let placed = false;
|
|
542
|
-
for (const c of candidates) {
|
|
543
|
-
if (!labelsOverlap(c.x, c.y, tw, th)) {
|
|
544
|
-
ctx.fillStyle = sel ? catColor : '#80808c';
|
|
545
|
-
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
|
546
|
-
ctx.fillText(node.name, c.x, c.y);
|
|
547
|
-
placedLabels.push({ x: c.x, y: c.y, w: tw, h: th });
|
|
548
|
-
placed = true;
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
// If all positions overlap, selected nodes always draw (force top-left)
|
|
553
|
-
if (!placed && sel) {
|
|
554
|
-
const c = candidates[0];
|
|
555
|
-
ctx.fillStyle = catColor;
|
|
556
|
-
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
|
557
|
-
ctx.fillText(node.name, c.x, c.y);
|
|
558
|
-
placedLabels.push({ x: c.x, y: c.y, w: tw, h: th });
|
|
559
|
-
}
|
|
1218
|
+
function applyMcpConfig() {
|
|
1219
|
+
const textarea = document.getElementById('mcp-config');
|
|
1220
|
+
const status = document.getElementById('mcp-config-status');
|
|
1221
|
+
const raw = textarea.value.trim();
|
|
1222
|
+
if (!raw) { parsedMcpConfig = null; status.style.color = 'var(--text-muted)'; status.textContent = 'Copy from Lens Studio: AI Assistant > MCP config.'; return; }
|
|
1223
|
+
const parsed = parseMcpConfig(raw);
|
|
1224
|
+
if (parsed) {
|
|
1225
|
+
parsedMcpConfig = parsed;
|
|
1226
|
+
localStorage.setItem('lens-inspector-mcp-config', raw);
|
|
1227
|
+
status.style.color = '#0a8020';
|
|
1228
|
+
status.textContent = 'MCP configured: ' + parsed.url;
|
|
1229
|
+
if (liveTree && hasFetchedMCP) { hasFetchedMCP = false; fetchMCPComponents(); }
|
|
1230
|
+
} else {
|
|
1231
|
+
parsedMcpConfig = null;
|
|
1232
|
+
status.style.color = '#c03020';
|
|
1233
|
+
status.textContent = 'Invalid config. Paste the full JSON from Lens Studio.';
|
|
560
1234
|
}
|
|
561
|
-
|
|
562
|
-
document.getElementById('view-info').textContent = `${v.scale.toFixed(1)}x`;
|
|
563
1235
|
}
|
|
564
1236
|
|
|
565
|
-
function
|
|
1237
|
+
async function fetchMCPComponents() {
|
|
1238
|
+
if (hasFetchedMCP) return;
|
|
1239
|
+
hasFetchedMCP = true;
|
|
1240
|
+
|
|
1241
|
+
let fetchUrl, fetchHeaders;
|
|
1242
|
+
if (parsedMcpConfig) {
|
|
1243
|
+
const base = document.getElementById('mcp-base').value;
|
|
1244
|
+
const params = new URLSearchParams();
|
|
1245
|
+
params.set('url', parsedMcpConfig.url);
|
|
1246
|
+
const authHeader = parsedMcpConfig.headers['Authorization'] || parsedMcpConfig.headers['authorization'] || '';
|
|
1247
|
+
const token = authHeader.replace(/^Bearer\s+/i, '');
|
|
1248
|
+
if (token) params.set('token', token);
|
|
1249
|
+
fetchUrl = base + '/mcp?' + params.toString();
|
|
1250
|
+
fetchHeaders = { 'Content-Type': 'application/json' };
|
|
1251
|
+
} else {
|
|
1252
|
+
const base = document.getElementById('mcp-base').value;
|
|
1253
|
+
fetchUrl = base + '/mcp';
|
|
1254
|
+
fetchHeaders = { 'Content-Type': 'application/json' };
|
|
1255
|
+
}
|
|
566
1256
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1257
|
+
try {
|
|
1258
|
+
const resp = await fetch(fetchUrl, {
|
|
1259
|
+
method: 'POST',
|
|
1260
|
+
headers: fetchHeaders,
|
|
1261
|
+
body: JSON.stringify({
|
|
1262
|
+
jsonrpc: '2.0', id: 1, method: 'tools/call',
|
|
1263
|
+
params: { name: 'GetLensStudioSceneGraph', arguments: {} }
|
|
1264
|
+
})
|
|
1265
|
+
});
|
|
1266
|
+
const data = await resp.json();
|
|
1267
|
+
const sceneText = data.result?.content?.[0]?.text;
|
|
1268
|
+
if (!sceneText) { console.log('[inspector] MCP returned no scene data'); return; }
|
|
1269
|
+
const scene = JSON.parse(sceneText);
|
|
1270
|
+
const roots = scene.sceneTree?.children || scene.children || [];
|
|
572
1271
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
1272
|
+
// Extract project name from scene data
|
|
1273
|
+
const projName = scene.projectName || scene.name || (parsedMcpConfig ? parsedMcpConfig.name : '');
|
|
1274
|
+
if (projName) {
|
|
1275
|
+
document.getElementById('project-name').textContent = projName;
|
|
1276
|
+
}
|
|
577
1277
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
1278
|
+
mcpComponentMap = {};
|
|
1279
|
+
function walkMcp(obj, path) {
|
|
1280
|
+
const name = obj.name || '';
|
|
1281
|
+
const key = path + '/' + name;
|
|
1282
|
+
const components = (obj.components || []).map(c => ({
|
|
1283
|
+
type: c.name || c.type || 'Unknown',
|
|
1284
|
+
enabled: c.enabled !== false,
|
|
1285
|
+
category: null,
|
|
1286
|
+
scriptName: c.properties?.scriptAsset?.name || null,
|
|
1287
|
+
meshName: c.properties?.mesh?.name || null,
|
|
1288
|
+
materialName: c.properties?.mainMaterial?.name || null,
|
|
1289
|
+
text: c.properties?.text || null,
|
|
1290
|
+
}));
|
|
1291
|
+
if (components.length > 0) {
|
|
1292
|
+
mcpComponentMap[name] = { components, path: key };
|
|
1293
|
+
}
|
|
1294
|
+
for (const child of (obj.children || [])) walkMcp(child, key);
|
|
1295
|
+
}
|
|
1296
|
+
for (const r of roots) walkMcp(r, '');
|
|
1297
|
+
console.log('[inspector] MCP enrichment: got components for', Object.keys(mcpComponentMap).length, 'objects');
|
|
1298
|
+
if (liveTree) { rebuildFlat(); renderTree(); buildScene3D(); if (selectedId) renderProps(); }
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
console.log('[inspector] MCP enrichment failed:', e.message);
|
|
1301
|
+
}
|
|
586
1302
|
}
|
|
587
1303
|
|
|
588
1304
|
// --- WebSocket ---
|
|
1305
|
+
function updateSourceUI() {
|
|
1306
|
+
const dot = document.getElementById('ws-dot');
|
|
1307
|
+
const text = document.getElementById('status-text');
|
|
1308
|
+
const badge = document.getElementById('source-badge');
|
|
1309
|
+
if (dataSource === 'example') {
|
|
1310
|
+
dot.className = 'status-dot dot-example';
|
|
1311
|
+
text.textContent = 'connected';
|
|
1312
|
+
badge.className = 'badge-example';
|
|
1313
|
+
badge.textContent = 'Example scene';
|
|
1314
|
+
} else if (dataSource === 'live') {
|
|
1315
|
+
dot.className = 'status-dot dot-live';
|
|
1316
|
+
text.textContent = 'live';
|
|
1317
|
+
badge.className = 'badge-live';
|
|
1318
|
+
badge.textContent = 'Lens Studio';
|
|
1319
|
+
} else {
|
|
1320
|
+
dot.className = 'status-dot dot-off';
|
|
1321
|
+
text.textContent = 'connecting...';
|
|
1322
|
+
badge.className = 'badge-off';
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
589
1326
|
function connectWS() {
|
|
590
1327
|
const url = document.getElementById('ws-url').value.trim();
|
|
591
1328
|
if (ws) { ws.close(); ws = null; }
|
|
592
1329
|
ws = new WebSocket(url);
|
|
593
|
-
ws.onopen = () =>
|
|
1330
|
+
ws.onopen = () => updateSourceUI();
|
|
594
1331
|
ws.onmessage = (e) => {
|
|
595
1332
|
try {
|
|
596
1333
|
const msg = JSON.parse(e.data);
|
|
1334
|
+
if (msg.event === 'reload') { location.reload(); return; }
|
|
597
1335
|
if (msg.event === 'scene_snapshot' && msg.roots) {
|
|
1336
|
+
const wasEmpty = !liveTree;
|
|
1337
|
+
const oldCount = sceneNodes.length;
|
|
1338
|
+
const newSource = msg.source === 'example' ? 'example' : 'live';
|
|
1339
|
+
|
|
1340
|
+
if (dataSource === 'example' && newSource === 'live') {
|
|
1341
|
+
selectedId = null;
|
|
1342
|
+
}
|
|
1343
|
+
dataSource = newSource;
|
|
598
1344
|
liveTree = msg.roots;
|
|
599
|
-
rebuildFlat(); renderTree();
|
|
1345
|
+
rebuildFlat(); renderTree(); buildScene3D();
|
|
1346
|
+
if (wasEmpty || Math.abs(sceneNodes.length - oldCount) > oldCount * 0.5) {
|
|
1347
|
+
hasAutoFit = false;
|
|
1348
|
+
autoFitCamera();
|
|
1349
|
+
hasAutoFit = true;
|
|
1350
|
+
lastGradSpacing = 0;
|
|
1351
|
+
}
|
|
600
1352
|
if (selectedId) renderProps();
|
|
601
|
-
|
|
1353
|
+
updateSourceUI();
|
|
1354
|
+
if (wasEmpty) hideWelcome();
|
|
1355
|
+
if (newSource === 'live' && !hasFetchedMCP) fetchMCPComponents();
|
|
602
1356
|
}
|
|
603
1357
|
} catch (err) {}
|
|
604
1358
|
};
|
|
605
1359
|
ws.onclose = () => {
|
|
606
|
-
|
|
607
|
-
|
|
1360
|
+
dataSource = null;
|
|
1361
|
+
updateSourceUI();
|
|
608
1362
|
ws = null; setTimeout(connectWS, 2000);
|
|
609
1363
|
};
|
|
610
1364
|
}
|
|
611
|
-
document.getElementById('ws-btn').addEventListener('click', connectWS);
|
|
612
|
-
|
|
613
|
-
// --- MCP ---
|
|
614
|
-
async function fetchMCP() {
|
|
615
|
-
const token = document.getElementById('mcp-token').value.trim();
|
|
616
|
-
const dot = document.getElementById('mcp-dot');
|
|
617
|
-
dot.className = 'status-dot dot-off';
|
|
618
|
-
try {
|
|
619
|
-
const proxyUrl = `http://${location.host}/mcp` + (token ? `?token=${encodeURIComponent(token)}` : '');
|
|
620
|
-
const resp = await fetch(proxyUrl, { method:'POST', headers:{'Content-Type':'application/json'},
|
|
621
|
-
body: JSON.stringify({jsonrpc:'2.0',id:1,method:'tools/call',params:{name:'GetLensStudioSceneGraph',arguments:{}}})});
|
|
622
|
-
const data = await resp.json();
|
|
623
|
-
const sceneText = data.result?.content?.[0]?.text;
|
|
624
|
-
if (!sceneText) throw new Error('No scene data');
|
|
625
|
-
const scene = JSON.parse(sceneText);
|
|
626
|
-
const roots = scene.sceneTree?.children || scene.children || [];
|
|
627
|
-
function convertMcp(obj) {
|
|
628
|
-
const pos = obj.localTransform?.position||{x:0,y:0,z:0};
|
|
629
|
-
const scl = obj.localTransform?.scale||{x:1,y:1,z:1};
|
|
630
|
-
const rot = obj.localTransform?.rotation||{x:0,y:0,z:0,w:1};
|
|
631
|
-
const components = (obj.components||[]).map(c => ({type:c.name||c.type||'Unknown',enabled:c.enabled!==false,text:c.properties?.text||null,category:null}));
|
|
632
|
-
return { name:obj.name, enabled:obj.enabled!==false, category:'empty',
|
|
633
|
-
pos:[pos.x,pos.y,pos.z], scale:[scl.x,scl.y,scl.z], rot:[rot.x,rot.y,rot.z,rot.w],
|
|
634
|
-
components, text:components.find(c=>c.text)?.text||null,
|
|
635
|
-
hasVisual:components.some(c=>c.type==='RenderMeshVisual'||c.type==='Image'), color:null,
|
|
636
|
-
children:(obj.children||[]).map(convertMcp) };
|
|
637
|
-
}
|
|
638
|
-
mcpTree = roots.map(convertMcp);
|
|
639
|
-
if (!liveTree) { rebuildFlat(); renderTree(); renderAllViews(); }
|
|
640
|
-
dot.className = 'status-dot dot-mcp'; hideWelcome();
|
|
641
|
-
} catch (e) { dot.className = 'status-dot dot-off'; alert('MCP error: '+e.message); }
|
|
642
|
-
}
|
|
643
|
-
document.getElementById('mcp-btn').addEventListener('click', fetchMCP);
|
|
644
1365
|
|
|
645
1366
|
// --- Search ---
|
|
646
1367
|
document.getElementById('tree-search').addEventListener('input', (e) => {
|
|
647
1368
|
searchFilter = e.target.value.trim(); rebuildFlat(); renderTree();
|
|
648
1369
|
});
|
|
649
1370
|
|
|
1371
|
+
// --- Category filter chips ---
|
|
1372
|
+
document.getElementById('tree-filters').addEventListener('click', (e) => {
|
|
1373
|
+
const chip = e.target.closest('.filter-chip');
|
|
1374
|
+
if (!chip) return;
|
|
1375
|
+
activeCategoryFilter = chip.dataset.cat;
|
|
1376
|
+
document.querySelectorAll('.filter-chip').forEach(c => c.classList.toggle('active', c.dataset.cat === activeCategoryFilter));
|
|
1377
|
+
rebuildFlat(); renderTree();
|
|
1378
|
+
});
|
|
1379
|
+
|
|
650
1380
|
// --- Utils ---
|
|
651
1381
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
652
|
-
function hideWelcome() {
|
|
1382
|
+
function hideWelcome() {
|
|
1383
|
+
const w = document.getElementById('welcome');
|
|
1384
|
+
if (dataSource === 'example') {
|
|
1385
|
+
document.getElementById('welcome-status').style.display = 'none';
|
|
1386
|
+
document.getElementById('welcome-example').style.display = 'flex';
|
|
1387
|
+
}
|
|
1388
|
+
w.classList.add('hidden');
|
|
1389
|
+
}
|
|
1390
|
+
function showWelcome() {
|
|
1391
|
+
const w = document.getElementById('welcome');
|
|
1392
|
+
if (dataSource === 'example') {
|
|
1393
|
+
document.getElementById('welcome-status').style.display = 'none';
|
|
1394
|
+
document.getElementById('welcome-example').style.display = 'flex';
|
|
1395
|
+
} else if (dataSource === 'live') {
|
|
1396
|
+
document.getElementById('welcome-status').style.display = 'none';
|
|
1397
|
+
document.getElementById('welcome-example').style.display = 'none';
|
|
1398
|
+
} else {
|
|
1399
|
+
document.getElementById('welcome-status').style.display = 'flex';
|
|
1400
|
+
document.getElementById('welcome-example').style.display = 'none';
|
|
1401
|
+
}
|
|
1402
|
+
w.classList.remove('hidden');
|
|
1403
|
+
}
|
|
1404
|
+
document.getElementById('setup-btn').addEventListener('click', showWelcome);
|
|
1405
|
+
document.getElementById('server-connect-btn').addEventListener('click', applyServerUrl);
|
|
1406
|
+
document.getElementById('welcome-url').addEventListener('keydown', (e) => { if (e.key === 'Enter') applyServerUrl(); });
|
|
1407
|
+
document.getElementById('mcp-config').addEventListener('input', applyMcpConfig);
|
|
1408
|
+
document.getElementById('welcome').addEventListener('click', (e) => {
|
|
1409
|
+
if (e.target === document.getElementById('welcome') && dataSource) hideWelcome();
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
// --- Resizable panes ---
|
|
1413
|
+
function initResize() {
|
|
1414
|
+
const treePanel = document.getElementById('tree-panel');
|
|
1415
|
+
const propsPanel = document.getElementById('props-panel');
|
|
1416
|
+
const handleL = document.getElementById('resize-left');
|
|
1417
|
+
const handleR = document.getElementById('resize-right');
|
|
1418
|
+
|
|
1419
|
+
function makeDraggable(handle, getTarget, isLeft) {
|
|
1420
|
+
let startX, startW;
|
|
1421
|
+
function onDown(e) {
|
|
1422
|
+
e.preventDefault();
|
|
1423
|
+
const target = getTarget();
|
|
1424
|
+
startX = e.clientX;
|
|
1425
|
+
startW = target.offsetWidth;
|
|
1426
|
+
handle.classList.add('active');
|
|
1427
|
+
document.body.classList.add('resizing');
|
|
1428
|
+
document.addEventListener('mousemove', onMove);
|
|
1429
|
+
document.addEventListener('mouseup', onUp);
|
|
1430
|
+
}
|
|
1431
|
+
function onMove(e) {
|
|
1432
|
+
const dx = e.clientX - startX;
|
|
1433
|
+
const target = getTarget();
|
|
1434
|
+
const newW = isLeft ? startW + dx : startW - dx;
|
|
1435
|
+
target.style.width = Math.max(150, newW) + 'px';
|
|
1436
|
+
onResize();
|
|
1437
|
+
}
|
|
1438
|
+
function onUp() {
|
|
1439
|
+
handle.classList.remove('active');
|
|
1440
|
+
document.body.classList.remove('resizing');
|
|
1441
|
+
document.removeEventListener('mousemove', onMove);
|
|
1442
|
+
document.removeEventListener('mouseup', onUp);
|
|
1443
|
+
}
|
|
1444
|
+
handle.addEventListener('mousedown', onDown);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
makeDraggable(handleL, () => treePanel, true);
|
|
1448
|
+
makeDraggable(handleR, () => propsPanel, false);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// --- WASD fly controls (uses e.code for keyboard-layout independence) ---
|
|
1452
|
+
const keysDown = new Set();
|
|
1453
|
+
const flySpeed = { base: 0.5, shift: 2.5 };
|
|
1454
|
+
const FLY_CODES = new Set(['KeyW','KeyA','KeyS','KeyD','KeyQ','KeyE','ShiftLeft','ShiftRight']);
|
|
1455
|
+
|
|
1456
|
+
function initFlyControls() {
|
|
1457
|
+
const canvas = document.getElementById('scene-canvas');
|
|
1458
|
+
canvas.tabIndex = 0;
|
|
1459
|
+
canvas.style.outline = 'none';
|
|
1460
|
+
canvas.addEventListener('mousedown', () => canvas.focus());
|
|
1461
|
+
|
|
1462
|
+
canvas.addEventListener('keydown', (e) => {
|
|
1463
|
+
if (FLY_CODES.has(e.code)) {
|
|
1464
|
+
keysDown.add(e.code);
|
|
1465
|
+
e.preventDefault();
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
canvas.addEventListener('keyup', (e) => {
|
|
1469
|
+
keysDown.delete(e.code);
|
|
1470
|
+
});
|
|
1471
|
+
canvas.addEventListener('blur', () => keysDown.clear());
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function updateFlyMovement() {
|
|
1475
|
+
if (keysDown.size === 0) return;
|
|
1476
|
+
const speed = (keysDown.has('ShiftLeft') || keysDown.has('ShiftRight')) ? flySpeed.shift : flySpeed.base;
|
|
1477
|
+
const dist = camera3d.position.distanceTo(controls3d.target);
|
|
1478
|
+
const step = speed * dist * 0.01;
|
|
1479
|
+
|
|
1480
|
+
const forward = new THREE.Vector3();
|
|
1481
|
+
camera3d.getWorldDirection(forward);
|
|
1482
|
+
const right = new THREE.Vector3().crossVectors(forward, camera3d.up).normalize();
|
|
1483
|
+
const up = new THREE.Vector3(0, 1, 0);
|
|
1484
|
+
|
|
1485
|
+
const move = new THREE.Vector3();
|
|
1486
|
+
if (keysDown.has('KeyW')) move.add(forward.clone().multiplyScalar(step));
|
|
1487
|
+
if (keysDown.has('KeyS')) move.add(forward.clone().multiplyScalar(-step));
|
|
1488
|
+
if (keysDown.has('KeyD')) move.add(right.clone().multiplyScalar(step));
|
|
1489
|
+
if (keysDown.has('KeyA')) move.add(right.clone().multiplyScalar(-step));
|
|
1490
|
+
if (keysDown.has('KeyE')) move.add(up.clone().multiplyScalar(step));
|
|
1491
|
+
if (keysDown.has('KeyQ')) move.add(up.clone().multiplyScalar(-step));
|
|
1492
|
+
|
|
1493
|
+
camera3d.position.add(move);
|
|
1494
|
+
controls3d.target.add(move);
|
|
1495
|
+
}
|
|
653
1496
|
|
|
654
1497
|
// --- Init ---
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1498
|
+
initScene3D();
|
|
1499
|
+
initResize();
|
|
1500
|
+
initFlyControls();
|
|
658
1501
|
connectWS();
|
|
659
1502
|
</script>
|
|
660
1503
|
</body>
|