lens-inspector 0.1.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/LICENSE +21 -0
- package/README.md +93 -0
- package/SceneInspector.ts +294 -0
- package/example.js +339 -0
- package/index.html +661 -0
- package/package.json +34 -0
- package/server.js +148 -0
package/index.html
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Lens Inspector</title>
|
|
6
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
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
|
+
<style>
|
|
9
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #f4f4f7;
|
|
13
|
+
--panel: #fff;
|
|
14
|
+
--surface: #eaeaef;
|
|
15
|
+
--border: #d0d0d8;
|
|
16
|
+
--border-bright: #b0b0ba;
|
|
17
|
+
--text: #101018;
|
|
18
|
+
--text-dim: #48485a;
|
|
19
|
+
--text-muted: #70707e;
|
|
20
|
+
--white: #fff;
|
|
21
|
+
|
|
22
|
+
/* LS component categories — no green, higher saturation */
|
|
23
|
+
--c-camera: #d08020;
|
|
24
|
+
--c-visual: #3088c0;
|
|
25
|
+
--c-text: #3070d0;
|
|
26
|
+
--c-script: #5060c0;
|
|
27
|
+
--c-audio: #1090b0;
|
|
28
|
+
--c-light: #c09010;
|
|
29
|
+
--c-physics: #d05030;
|
|
30
|
+
--c-vfx: #1098c0;
|
|
31
|
+
--c-interaction: #4060c0;
|
|
32
|
+
--c-tracking: #1090b0;
|
|
33
|
+
--c-animation: #c07020;
|
|
34
|
+
--c-ui: #3878d0;
|
|
35
|
+
--c-ml: #3070c0;
|
|
36
|
+
--c-empty: #909098;
|
|
37
|
+
--c-prefab: #4080d0;
|
|
38
|
+
|
|
39
|
+
--c-runtime: #3088c0;
|
|
40
|
+
--c-selected: #1850d0;
|
|
41
|
+
--c-enabled: #3088c0;
|
|
42
|
+
--c-disabled: #d04040;
|
|
43
|
+
|
|
44
|
+
--font: 'IBM Plex Sans', -apple-system, system-ui, sans-serif;
|
|
45
|
+
--mono: 'IBM Plex Mono', 'JetBrains Mono', monospace;
|
|
46
|
+
|
|
47
|
+
--transition: 0.15s ease;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
body { background: var(--bg); color: var(--text); font-family: var(--font); height: 100vh; display: flex; flex-direction: column; -webkit-font-smoothing: antialiased; }
|
|
51
|
+
|
|
52
|
+
/* --- Toolbar --- */
|
|
53
|
+
#toolbar {
|
|
54
|
+
height: 32px; background: var(--panel); border-bottom: 2px solid var(--border);
|
|
55
|
+
display: flex; align-items: center; padding: 0 10px; gap: 6px; flex-shrink: 0;
|
|
56
|
+
}
|
|
57
|
+
.toolbar-brand { font-family: var(--mono); font-size: 11px; font-weight: 600; color: var(--text-dim); letter-spacing: 0.3px; }
|
|
58
|
+
.toolbar-brand b { color: var(--text); }
|
|
59
|
+
.sep { width: 1px; height: 14px; background: var(--border); }
|
|
60
|
+
#toolbar input {
|
|
61
|
+
background: var(--surface); color: var(--text); border: 1px solid var(--border); padding: 2px 6px;
|
|
62
|
+
font-family: var(--mono); font-size: 10px; border-radius: 3px; outline: none; transition: border-color var(--transition);
|
|
63
|
+
}
|
|
64
|
+
#toolbar input:focus { border-color: var(--c-selected); }
|
|
65
|
+
#toolbar button {
|
|
66
|
+
background: var(--surface); color: var(--text-dim); border: 1px solid var(--border); padding: 2px 8px;
|
|
67
|
+
font-family: var(--font); font-size: 10px; font-weight: 500; border-radius: 3px; cursor: pointer; transition: all var(--transition);
|
|
68
|
+
}
|
|
69
|
+
#toolbar button:hover { border-color: var(--border-bright); color: var(--text); background: var(--border); }
|
|
70
|
+
.status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; transition: all 0.3s ease; }
|
|
71
|
+
.dot-off { background: #bbb; }
|
|
72
|
+
.dot-live { background: var(--c-selected); box-shadow: 0 0 6px rgba(24,80,208,0.5); }
|
|
73
|
+
.dot-mcp { background: var(--c-text); box-shadow: 0 0 6px rgba(48,112,208,0.5); }
|
|
74
|
+
#status-text { font-family: var(--mono); font-size: 10px; color: var(--text-dim); }
|
|
75
|
+
|
|
76
|
+
/* --- Main layout --- */
|
|
77
|
+
#main { flex: 1; display: flex; min-height: 0; }
|
|
78
|
+
|
|
79
|
+
/* --- Tree panel (left) --- */
|
|
80
|
+
#tree-panel {
|
|
81
|
+
width: 300px; background: var(--panel); border-right: 2px solid var(--border);
|
|
82
|
+
display: flex; flex-direction: column; flex-shrink: 0;
|
|
83
|
+
}
|
|
84
|
+
#tree-header {
|
|
85
|
+
padding: 4px 10px; border-bottom: 1px solid var(--border);
|
|
86
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
87
|
+
}
|
|
88
|
+
#tree-header h2 { font-family: var(--mono); font-size: 9px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
|
|
89
|
+
#tree-count { font-family: var(--mono); font-size: 9px; color: var(--text-dim); }
|
|
90
|
+
#tree-search {
|
|
91
|
+
margin: 3px 6px; padding: 3px 7px; background: var(--surface); border: 1px solid var(--border);
|
|
92
|
+
color: var(--text); font-family: var(--font); font-size: 11px; border-radius: 3px; outline: none; transition: border-color var(--transition);
|
|
93
|
+
}
|
|
94
|
+
#tree-search:focus { border-color: var(--c-selected); }
|
|
95
|
+
#tree-search::placeholder { color: var(--border-bright); }
|
|
96
|
+
#tree-list { flex: 1; overflow-y: auto; padding: 1px 0; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
|
|
97
|
+
#tree-list::-webkit-scrollbar { width: 5px; }
|
|
98
|
+
#tree-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
99
|
+
|
|
100
|
+
/* Tree nodes */
|
|
101
|
+
.tree-node {
|
|
102
|
+
display: flex; align-items: center; gap: 2px; padding: 0 6px; cursor: pointer;
|
|
103
|
+
border-left: 3px solid transparent; font-size: 11px; min-height: 20px;
|
|
104
|
+
transition: background var(--transition), border-color var(--transition);
|
|
105
|
+
}
|
|
106
|
+
.tree-node:hover { background: var(--surface); }
|
|
107
|
+
.tree-node.selected { background: #dde4f4; border-left-color: var(--c-selected); }
|
|
108
|
+
.tree-node.disabled { opacity: 0.3; }
|
|
109
|
+
.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
|
+
.tree-arrow:hover { color: var(--text); }
|
|
111
|
+
.tree-icon { width: 13px; height: 13px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
|
112
|
+
.tree-icon svg { width: 11px; height: 11px; }
|
|
113
|
+
.tree-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; }
|
|
114
|
+
.tree-name-runtime { color: var(--c-runtime); }
|
|
115
|
+
.tree-text-preview { color: var(--text-dim); font-family: var(--mono); font-size: 9px; max-width: 70px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0; }
|
|
116
|
+
.tree-comp-count { font-family: var(--mono); font-size: 8px; color: var(--text-dim); flex-shrink: 0; padding: 0 2px; }
|
|
117
|
+
.tree-badge { font-family: var(--mono); font-size: 7px; padding: 0 4px; border-radius: 2px; flex-shrink: 0; font-weight: 500; }
|
|
118
|
+
.badge-prefab { background: #d8e4f4; color: var(--c-prefab); }
|
|
119
|
+
|
|
120
|
+
/* --- Viewport (center, single view) --- */
|
|
121
|
+
#viewport { flex: 1; position: relative; background: var(--panel); overflow: hidden; min-width: 0; }
|
|
122
|
+
#viewport canvas { width: 100%; height: 100%; cursor: grab; }
|
|
123
|
+
#viewport canvas:active { cursor: grabbing; }
|
|
124
|
+
#view-bar {
|
|
125
|
+
position: absolute; top: 6px; left: 8px; display: flex; align-items: center; gap: 0; z-index: 2;
|
|
126
|
+
background: rgba(255,255,255,0.92); border: 2px solid var(--border); border-radius: 4px; overflow: hidden;
|
|
127
|
+
}
|
|
128
|
+
.view-btn {
|
|
129
|
+
font-family: var(--mono); font-size: 10px; font-weight: 600; padding: 3px 10px;
|
|
130
|
+
border: none; background: none; color: var(--text-dim); cursor: pointer; transition: all var(--transition);
|
|
131
|
+
letter-spacing: 0.3px;
|
|
132
|
+
}
|
|
133
|
+
.view-btn:hover { color: var(--text); background: var(--surface); }
|
|
134
|
+
.view-btn.active { color: var(--c-selected); background: #dde4f4; }
|
|
135
|
+
.view-btn + .view-btn { border-left: 1px solid var(--border); }
|
|
136
|
+
#view-info {
|
|
137
|
+
position: absolute; bottom: 6px; right: 8px; font-family: var(--mono); font-size: 9px;
|
|
138
|
+
color: var(--text-dim); pointer-events: none; background: rgba(255,255,255,0.85); padding: 1px 6px; border-radius: 3px;
|
|
139
|
+
}
|
|
140
|
+
#view-stats {
|
|
141
|
+
position: absolute; bottom: 6px; left: 8px; display: flex; gap: 12px; z-index: 2;
|
|
142
|
+
font-family: var(--mono); font-size: 10px; background: rgba(255,255,255,0.92); padding: 3px 10px; border-radius: 3px; border: 1px solid var(--border);
|
|
143
|
+
}
|
|
144
|
+
.stat-item { display: flex; align-items: baseline; gap: 4px; }
|
|
145
|
+
.stat-num { font-size: 13px; font-weight: 700; color: var(--text); }
|
|
146
|
+
.stat-label { font-size: 9px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
147
|
+
|
|
148
|
+
/* --- Properties panel (right) --- */
|
|
149
|
+
#props-panel {
|
|
150
|
+
width: 240px; background: var(--panel); border-left: 2px solid var(--border);
|
|
151
|
+
display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto;
|
|
152
|
+
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
|
153
|
+
}
|
|
154
|
+
#props-panel::-webkit-scrollbar { width: 5px; }
|
|
155
|
+
#props-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
156
|
+
#props-header { padding: 4px 10px; border-bottom: 1px solid var(--border); }
|
|
157
|
+
#props-header h2 { font-family: var(--mono); font-size: 9px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
|
|
158
|
+
#props-content { padding: 6px 10px; }
|
|
159
|
+
.prop-section { margin-bottom: 8px; animation: fadeIn 0.2s ease; }
|
|
160
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
161
|
+
.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
|
+
.prop-row { display: flex; justify-content: space-between; align-items: center; padding: 1px 0; border-bottom: 1px solid var(--surface); }
|
|
163
|
+
.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; max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
165
|
+
.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
|
+
.prop-hint { font-size: 11px; color: var(--text-dim); padding: 16px 0; text-align: center; }
|
|
167
|
+
|
|
168
|
+
.comp-item { padding: 2px 0; border-bottom: 1px solid var(--surface); display: flex; align-items: center; gap: 4px; transition: background var(--transition); }
|
|
169
|
+
.comp-type { font-family: var(--mono); font-size: 10px; }
|
|
170
|
+
.comp-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
|
171
|
+
.comp-on { background: var(--c-enabled); }
|
|
172
|
+
.comp-off { background: var(--c-disabled); }
|
|
173
|
+
.comp-detail { font-family: var(--mono); font-size: 9px; color: var(--text-dim); margin-left: auto; max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
174
|
+
|
|
175
|
+
/* --- Welcome --- */
|
|
176
|
+
#welcome {
|
|
177
|
+
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
178
|
+
z-index: 10; background: var(--bg); animation: fadeIn 0.3s ease;
|
|
179
|
+
}
|
|
180
|
+
#welcome.hidden { display: none; }
|
|
181
|
+
.welcome-box {
|
|
182
|
+
max-width: 420px; padding: 28px 36px; background: var(--panel); border: 1px solid var(--border);
|
|
183
|
+
border-radius: 6px; box-shadow: 0 2px 12px rgba(0,0,0,0.04);
|
|
184
|
+
}
|
|
185
|
+
.welcome-box h1 { font-family: var(--font); font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 18px; }
|
|
186
|
+
.welcome-box .step { display: flex; gap: 10px; margin-bottom: 12px; font-size: 12px; line-height: 1.6; }
|
|
187
|
+
.welcome-box .step-num {
|
|
188
|
+
width: 18px; height: 18px; background: var(--surface); color: var(--text-dim);
|
|
189
|
+
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
190
|
+
font-family: var(--mono); font-size: 9px; font-weight: 600; flex-shrink: 0; margin-top: 2px;
|
|
191
|
+
}
|
|
192
|
+
.welcome-box .step-text { color: var(--text-dim); }
|
|
193
|
+
.welcome-box .step-text code { background: var(--surface); padding: 1px 5px; border-radius: 3px; color: var(--text); font-family: var(--mono); font-size: 10px; }
|
|
194
|
+
.welcome-box .step-check {
|
|
195
|
+
width: 18px; height: 18px; background: #e0eaf4; color: var(--c-selected);
|
|
196
|
+
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
197
|
+
font-size: 11px; font-weight: 700; flex-shrink: 0; margin-top: 2px;
|
|
198
|
+
}
|
|
199
|
+
.welcome-status {
|
|
200
|
+
margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border);
|
|
201
|
+
font-family: var(--mono); font-size: 10px; color: var(--text-muted); display: flex; align-items: center; gap: 8px;
|
|
202
|
+
}
|
|
203
|
+
.welcome-spinner {
|
|
204
|
+
width: 10px; height: 10px; border: 1.5px solid var(--border); border-top-color: var(--c-selected);
|
|
205
|
+
border-radius: 50%; animation: spin 0.7s linear infinite;
|
|
206
|
+
}
|
|
207
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
208
|
+
</style>
|
|
209
|
+
</head>
|
|
210
|
+
<body>
|
|
211
|
+
|
|
212
|
+
<div id="welcome">
|
|
213
|
+
<div class="welcome-box">
|
|
214
|
+
<h1>Lens Inspector</h1>
|
|
215
|
+
<div class="step">
|
|
216
|
+
<span class="step-check">✓</span>
|
|
217
|
+
<span class="step-text">Server running</span>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="step">
|
|
220
|
+
<span class="step-num">2</span>
|
|
221
|
+
<span class="step-text">
|
|
222
|
+
In your Lens Studio project, add <code>SceneInspector.ts</code> to Assets and attach it to any object.
|
|
223
|
+
Then go to <strong>Project Settings > General</strong> and enable <strong>Experimental APIs</strong>.
|
|
224
|
+
</span>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="step">
|
|
227
|
+
<span class="step-num">3</span>
|
|
228
|
+
<span class="step-text">Hit <strong>Play</strong> in Lens Studio.</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="welcome-status">
|
|
231
|
+
<div class="welcome-spinner"></div>
|
|
232
|
+
<span>Waiting for Lens Studio to connect...</span>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div id="toolbar">
|
|
238
|
+
<span class="toolbar-brand"><b>lens</b>-inspector</span>
|
|
239
|
+
<div class="sep"></div>
|
|
240
|
+
<span class="status-dot dot-off" id="ws-dot"></span>
|
|
241
|
+
<span id="status-text">disconnected</span>
|
|
242
|
+
<div class="sep"></div>
|
|
243
|
+
<label style="font-size:10px;color:var(--text-muted)">Server:</label>
|
|
244
|
+
<input id="ws-url" value="ws://localhost:8200" style="width:140px">
|
|
245
|
+
<button id="ws-btn">Connect</button>
|
|
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>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div id="main">
|
|
254
|
+
<div id="tree-panel">
|
|
255
|
+
<div id="tree-header">
|
|
256
|
+
<h2>Hierarchy</h2>
|
|
257
|
+
<span id="tree-count"></span>
|
|
258
|
+
</div>
|
|
259
|
+
<input type="text" id="tree-search" placeholder="Filter...">
|
|
260
|
+
<div id="tree-list"></div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div id="viewport">
|
|
264
|
+
<canvas id="scene-canvas"></canvas>
|
|
265
|
+
<div id="view-bar">
|
|
266
|
+
<button class="view-btn active" data-view="xz">TOP</button>
|
|
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>
|
|
271
|
+
<div id="view-stats">
|
|
272
|
+
<div class="stat-item"><span class="stat-num" id="stat-objects">0</span><span class="stat-label">objects</span></div>
|
|
273
|
+
<div class="stat-item"><span class="stat-num" id="stat-components">0</span><span class="stat-label">components</span></div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div id="props-panel">
|
|
278
|
+
<div id="props-header"><h2>Inspector</h2></div>
|
|
279
|
+
<div id="props-content"><p class="prop-hint">Select a node</p></div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<script>
|
|
284
|
+
// --- Icons (no green) ---
|
|
285
|
+
const ICONS = {
|
|
286
|
+
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
|
+
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>`,
|
|
288
|
+
text: `<svg viewBox="0 0 12 12" fill="none"><text x="3" y="9.5" font-size="9" font-weight="700" fill="currentColor" font-family="sans-serif">T</text></svg>`,
|
|
289
|
+
script: `<svg viewBox="0 0 12 12" fill="none"><path d="M3 3l3 3-3 3M7 9h3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>`,
|
|
290
|
+
audio: `<svg viewBox="0 0 12 12" fill="none"><rect x="4" y="1" width="2" height="10" rx="1" fill="currentColor" opacity="0.5"/><rect x="1" y="3" width="2" height="6" rx="1" fill="currentColor" opacity="0.3"/><rect x="7" y="3" width="2" height="6" rx="1" fill="currentColor" opacity="0.3"/></svg>`,
|
|
291
|
+
light: `<svg viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="2.5" stroke="currentColor" stroke-width="1.2"/><path d="M6 1v1.5M6 9.5V11M1 6h1.5M9.5 6H11M2.5 2.5l1 1M8.5 8.5l1 1M2.5 9.5l1-1M8.5 3.5l1-1" stroke="currentColor" stroke-width="0.8"/></svg>`,
|
|
292
|
+
physics: `<svg viewBox="0 0 12 12" fill="none"><path d="M6 1L1 4v4l5 3 5-3V4L6 1z" stroke="currentColor" stroke-width="1.1"/></svg>`,
|
|
293
|
+
vfx: `<svg viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="1.5" fill="currentColor"/><circle cx="3" cy="3" r="1" fill="currentColor" opacity="0.4"/><circle cx="9" cy="4" r="0.8" fill="currentColor" opacity="0.3"/><circle cx="4" cy="9" r="0.7" fill="currentColor" opacity="0.25"/></svg>`,
|
|
294
|
+
interaction: `<svg viewBox="0 0 12 12" fill="none"><path d="M4 1v7l2-2 1.5 3.5 1.5-.5-1.5-3.5H10L4 1z" fill="currentColor" opacity="0.6"/></svg>`,
|
|
295
|
+
tracking: `<svg viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="2" stroke="currentColor" stroke-width="1.2"/><circle cx="6" cy="6" r="0.8" fill="currentColor"/><path d="M6 1v2M6 9v2M1 6h2M9 6h2" stroke="currentColor" stroke-width="0.8"/></svg>`,
|
|
296
|
+
animation: `<svg viewBox="0 0 12 12" fill="none"><path d="M2 9l2-4 2 2 2-5 2 3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
297
|
+
ui: `<svg viewBox="0 0 12 12" fill="none"><rect x="1.5" y="2" width="9" height="8" rx="1" stroke="currentColor" stroke-width="1"/><line x1="1.5" y1="5" x2="10.5" y2="5" stroke="currentColor" stroke-width="0.7"/></svg>`,
|
|
298
|
+
ml: `<svg viewBox="0 0 12 12" fill="none"><rect x="3" y="1" width="6" height="4" rx="1" stroke="currentColor" stroke-width="1"/><rect x="3" y="7" width="6" height="4" rx="1" stroke="currentColor" stroke-width="1"/><path d="M6 5v2M4 5v2M8 5v2" stroke="currentColor" stroke-width="0.8"/></svg>`,
|
|
299
|
+
utility: `<svg viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="4" stroke="currentColor" stroke-width="1"/><circle cx="6" cy="6" r="1.5" stroke="currentColor" stroke-width="0.8"/></svg>`,
|
|
300
|
+
empty: `<svg viewBox="0 0 12 12" fill="none"><rect x="2" y="2" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1" stroke-dasharray="2 1.5"/></svg>`,
|
|
301
|
+
prefab: `<svg viewBox="0 0 12 12" fill="none"><rect x="1.5" y="1.5" width="9" height="9" rx="1.5" stroke="currentColor" stroke-width="1"/><path d="M4 4h4M4 6h4M4 8h2" stroke="currentColor" stroke-width="0.8" opacity="0.5"/></svg>`,
|
|
302
|
+
unknown: `<svg viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="4" stroke="currentColor" stroke-width="1"/></svg>`,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const CAT_COLORS = {
|
|
306
|
+
camera: '#d08020', visual: '#3088c0', text: '#3070d0', script: '#5060c0',
|
|
307
|
+
audio: '#1090b0', light: '#c09010', physics: '#d05030', vfx: '#1098c0',
|
|
308
|
+
interaction: '#4060c0', tracking: '#1090b0', animation: '#c07020', ui: '#3878d0',
|
|
309
|
+
ml: '#3070c0', utility: '#70707e', empty: '#909098', prefab: '#4080d0', unknown: '#909098',
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// --- State ---
|
|
313
|
+
let liveTree = null, mcpTree = null, flatNodes = [], selectedId = null;
|
|
314
|
+
let collapsedPaths = new Set(), searchFilter = '', ws = null;
|
|
315
|
+
|
|
316
|
+
// Single view with switchable plane
|
|
317
|
+
let activeView = 'xz';
|
|
318
|
+
const viewDefs = {
|
|
319
|
+
xz: { offset: {x:0,y:0}, scale: 14, axes: [0,2], label: ['X','Z'] },
|
|
320
|
+
xy: { offset: {x:0,y:0}, scale: 14, axes: [0,1], label: ['X','Y'] },
|
|
321
|
+
yz: { offset: {x:0,y:0}, scale: 14, axes: [2,1], label: ['Z','Y'] },
|
|
322
|
+
};
|
|
323
|
+
let canvas = null, ctx = null, dragging = false;
|
|
324
|
+
|
|
325
|
+
// --- Flatten ---
|
|
326
|
+
function flattenTree(roots, source) {
|
|
327
|
+
const result = [];
|
|
328
|
+
function walk(node, depth, path) {
|
|
329
|
+
const id = source + ':' + path;
|
|
330
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
331
|
+
const collapsed = collapsedPaths.has(id);
|
|
332
|
+
let category = node.category || 'empty';
|
|
333
|
+
if (category === 'empty' && node.components && node.components.length > 0) {
|
|
334
|
+
const types = node.components.map(c => c.type || '');
|
|
335
|
+
if (types.some(t => t.includes('Camera'))) category = 'camera';
|
|
336
|
+
else if (types.some(t => t.includes('Script') || t.includes('Inspector') || t.includes('Spawner'))) category = 'script';
|
|
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';
|
|
348
|
+
}
|
|
349
|
+
const isPrefab = node.isPrefab || (node.name && node.name.startsWith('[Prefab]'));
|
|
350
|
+
result.push({
|
|
351
|
+
id, name: node.name || '(unnamed)', depth, hasChildren, collapsed,
|
|
352
|
+
enabled: node.enabled !== false, category, source, isPrefab,
|
|
353
|
+
text: node.text || null,
|
|
354
|
+
pos: node.pos || [0,0,0], scale: node.scale || [1,1,1], rot: node.rot || [0,0,0,1],
|
|
355
|
+
components: node.components || [], hasVisual: node.hasVisual || false,
|
|
356
|
+
color: node.color || null, layer: node.layer || null,
|
|
357
|
+
childCount: node.children ? node.children.length : 0,
|
|
358
|
+
});
|
|
359
|
+
if (hasChildren && !collapsed) {
|
|
360
|
+
for (let i = 0; i < node.children.length; i++) walk(node.children[i], depth+1, path+'/'+(node.children[i].name||i));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (let i = 0; i < roots.length; i++) walk(roots[i], 0, roots[i].name || String(i));
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function rebuildFlat() {
|
|
368
|
+
const tree = liveTree || mcpTree;
|
|
369
|
+
if (!tree) { flatNodes = []; return; }
|
|
370
|
+
flatNodes = flattenTree(tree, liveTree ? 'live' : 'mcp');
|
|
371
|
+
if (searchFilter) {
|
|
372
|
+
const q = searchFilter.toLowerCase();
|
|
373
|
+
flatNodes = flatNodes.filter(n =>
|
|
374
|
+
n.name.toLowerCase().includes(q) || (n.text && n.text.toLowerCase().includes(q)) ||
|
|
375
|
+
n.components.some(c => (c.type||'').toLowerCase().includes(q)) || n.category.includes(q)
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
document.getElementById('tree-count').textContent = flatNodes.length;
|
|
379
|
+
// Update stats
|
|
380
|
+
let totalComp = 0;
|
|
381
|
+
flatNodes.forEach(n => totalComp += n.components.length);
|
|
382
|
+
document.getElementById('stat-objects').textContent = flatNodes.length;
|
|
383
|
+
document.getElementById('stat-components').textContent = totalComp;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// --- Tree ---
|
|
387
|
+
function renderTree() {
|
|
388
|
+
const list = document.getElementById('tree-list');
|
|
389
|
+
const html = flatNodes.map(n => {
|
|
390
|
+
const indent = n.depth * 16;
|
|
391
|
+
const arrowChar = n.hasChildren ? (n.collapsed ? '▶' : '▼') : '';
|
|
392
|
+
const cat = n.isPrefab ? 'prefab' : n.category;
|
|
393
|
+
const iconColor = CAT_COLORS[cat] || CAT_COLORS.empty;
|
|
394
|
+
const nameClass = n.source === 'live' && n.name.startsWith('[') ? 'tree-name tree-name-runtime' : 'tree-name';
|
|
395
|
+
const textPreview = n.text ? `<span class="tree-text-preview">"${esc(n.text.substring(0,14))}"</span>` : '';
|
|
396
|
+
const sel = n.id === selectedId ? ' selected' : '';
|
|
397
|
+
const dis = n.enabled ? '' : ' disabled';
|
|
398
|
+
const cc = n.components.length > 1 ? `<span class="tree-comp-count">${n.components.length}</span>` : '';
|
|
399
|
+
return `<div class="tree-node${sel}${dis}" data-id="${esc(n.id)}" style="padding-left:${indent+4}px">
|
|
400
|
+
<span class="tree-arrow" data-toggle="${esc(n.id)}">${arrowChar}</span>
|
|
401
|
+
<span class="tree-icon" style="color:${iconColor}">${ICONS[cat]||ICONS.empty}</span>
|
|
402
|
+
<span class="${nameClass}">${esc(n.name)}</span>${textPreview}${cc}
|
|
403
|
+
</div>`;
|
|
404
|
+
}).join('');
|
|
405
|
+
list.innerHTML = html || '<p class="prop-hint">No scene data</p>';
|
|
406
|
+
list.onclick = (e) => {
|
|
407
|
+
const toggle = e.target.closest('[data-toggle]');
|
|
408
|
+
if (toggle) { const id = toggle.dataset.toggle; if (collapsedPaths.has(id)) collapsedPaths.delete(id); else collapsedPaths.add(id); rebuildFlat(); renderTree(); renderAllViews(); return; }
|
|
409
|
+
const row = e.target.closest('.tree-node');
|
|
410
|
+
if (row) { selectedId = row.dataset.id; renderTree(); renderProps(); renderAllViews(); }
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// --- Properties ---
|
|
415
|
+
function renderProps() {
|
|
416
|
+
const el = document.getElementById('props-content');
|
|
417
|
+
const node = flatNodes.find(n => n.id === selectedId);
|
|
418
|
+
if (!node) { el.innerHTML = '<p class="prop-hint">Select a node</p>'; return; }
|
|
419
|
+
const cat = node.isPrefab ? 'prefab' : node.category;
|
|
420
|
+
const cc = CAT_COLORS[cat] || CAT_COLORS.empty;
|
|
421
|
+
let h = `<div style="margin-bottom:10px;display:flex;align-items:center;gap:6px;animation:fadeIn 0.2s ease">
|
|
422
|
+
<span class="tree-icon" style="color:${cc}">${ICONS[cat]||ICONS.empty}</span>
|
|
423
|
+
<span style="font-weight:600;font-size:12px">${esc(node.name)}</span>
|
|
424
|
+
</div>`;
|
|
425
|
+
h += `<div class="prop-section"><div class="prop-section-title">Transform</div>
|
|
426
|
+
<div class="prop-row"><span class="prop-key">position</span><span class="prop-val">${node.pos.map(v=>v.toFixed(2)).join(', ')}</span></div>
|
|
427
|
+
<div class="prop-row"><span class="prop-key">scale</span><span class="prop-val">${node.scale.map(v=>v.toFixed(2)).join(', ')}</span></div>
|
|
428
|
+
<div class="prop-row"><span class="prop-key">rotation</span><span class="prop-val">${node.rot.map(v=>v.toFixed(2)).join(', ')}</span></div>
|
|
429
|
+
</div>`;
|
|
430
|
+
h += `<div class="prop-section"><div class="prop-section-title">Object</div>
|
|
431
|
+
<div class="prop-row"><span class="prop-key">enabled</span><span class="prop-val">${node.enabled?'yes':'no'}</span></div>
|
|
432
|
+
<div class="prop-row"><span class="prop-key">children</span><span class="prop-val">${node.childCount}</span></div>
|
|
433
|
+
<div class="prop-row"><span class="prop-key">category</span><span class="prop-val" style="color:${cc}">${cat}</span></div>`;
|
|
434
|
+
if (node.layer) h += `<div class="prop-row"><span class="prop-key">layer</span><span class="prop-val">${esc(node.layer)}</span></div>`;
|
|
435
|
+
if (node.text) h += `<div class="prop-row"><span class="prop-key">text</span><span class="prop-val" title="${esc(node.text)}">${esc(node.text)}</span></div>`;
|
|
436
|
+
if (node.color) { const [r,g,b,a]=node.color; h += `<div class="prop-row"><span class="prop-key">color</span><span class="prop-val"><span class="prop-val-color" style="background:rgba(${r},${g},${b},${(a||255)/255})"></span>${r},${g},${b}</span></div>`; }
|
|
437
|
+
h += `</div>`;
|
|
438
|
+
if (node.components.length > 0) {
|
|
439
|
+
h += `<div class="prop-section"><div class="prop-section-title">Components (${node.components.length})</div>`;
|
|
440
|
+
for (const c of node.components) {
|
|
441
|
+
const compCat = c.category || 'unknown';
|
|
442
|
+
const compC = CAT_COLORS[compCat] || CAT_COLORS.empty;
|
|
443
|
+
let detail = c.text ? `"${esc(c.text.substring(0,14))}"` : c.scriptName || c.meshName || c.materialName || c.cameraType || c.trackName || '';
|
|
444
|
+
h += `<div class="comp-item"><span class="comp-dot ${c.enabled!==false?'comp-on':'comp-off'}"></span><span class="comp-type" style="color:${compC}">${esc(c.type||'Unknown')}</span>${detail?`<span class="comp-detail">${detail}</span>`:''}</div>`;
|
|
445
|
+
}
|
|
446
|
+
h += `</div>`;
|
|
447
|
+
}
|
|
448
|
+
el.innerHTML = h;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// --- Scene View ---
|
|
452
|
+
function renderScene() {
|
|
453
|
+
const v = viewDefs[activeView];
|
|
454
|
+
const dpr = window.devicePixelRatio || 1;
|
|
455
|
+
const w = canvas.clientWidth, h = canvas.clientHeight;
|
|
456
|
+
if (w <= 0 || h <= 0) return;
|
|
457
|
+
canvas.width = w * dpr; canvas.height = h * dpr;
|
|
458
|
+
ctx.scale(dpr, dpr);
|
|
459
|
+
|
|
460
|
+
const cx = w/2 + v.offset.x, cy = h/2 + v.offset.y;
|
|
461
|
+
const [ax, ay] = v.axes;
|
|
462
|
+
const flipY = ay === 1;
|
|
463
|
+
|
|
464
|
+
// Background
|
|
465
|
+
ctx.fillStyle = '#f8f8fa';
|
|
466
|
+
ctx.fillRect(0, 0, w, h);
|
|
467
|
+
|
|
468
|
+
// Grid
|
|
469
|
+
const gs = v.scale * 5;
|
|
470
|
+
if (gs > 3) {
|
|
471
|
+
ctx.strokeStyle = '#e4e4ea';
|
|
472
|
+
ctx.lineWidth = 1;
|
|
473
|
+
for (let x = cx % gs; x < w; x += gs) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,h); ctx.stroke(); }
|
|
474
|
+
for (let y = cy % gs; y < h; y += gs) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(w,y); ctx.stroke(); }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Origin axes
|
|
478
|
+
ctx.lineWidth = 1.5;
|
|
479
|
+
ctx.strokeStyle = '#d0d0d8'; ctx.beginPath(); ctx.moveTo(0,cy); ctx.lineTo(w,cy); ctx.stroke();
|
|
480
|
+
ctx.strokeStyle = '#d0d0d8'; ctx.beginPath(); ctx.moveTo(cx,0); ctx.lineTo(cx,h); ctx.stroke();
|
|
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([]);
|
|
516
|
+
|
|
517
|
+
if (sw > 12 || sel) nodeRects.push({ node, px, py, sw, sh, sel, cat, catColor });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Labels: collision-aware placement
|
|
521
|
+
const placedLabels = []; // [{x,y,w,h}]
|
|
522
|
+
function labelsOverlap(lx, ly, lw, lh) {
|
|
523
|
+
for (const p of placedLabels) {
|
|
524
|
+
if (lx < p.x + p.w && lx + lw > p.x && ly < p.y + p.h && ly + lh > p.y) return true;
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
// Selected nodes get priority
|
|
529
|
+
nodeRects.sort((a, b) => (b.sel ? 1 : 0) - (a.sel ? 1 : 0));
|
|
530
|
+
for (const { node, px, py, sw, sh, sel, cat, catColor } of nodeRects) {
|
|
531
|
+
ctx.font = sel ? '600 8px "IBM Plex Mono", monospace' : '8px "IBM Plex Mono", monospace';
|
|
532
|
+
const tw = ctx.measureText(node.name).width;
|
|
533
|
+
const th = 10;
|
|
534
|
+
// Try positions: above-left, above-right, below-left, below-right
|
|
535
|
+
const candidates = [
|
|
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
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
document.getElementById('view-info').textContent = `${v.scale.toFixed(1)}x`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function renderAllViews() { renderScene(); }
|
|
566
|
+
|
|
567
|
+
// --- Pan/Zoom ---
|
|
568
|
+
function setupViewport() {
|
|
569
|
+
canvas = document.getElementById('scene-canvas');
|
|
570
|
+
ctx = canvas.getContext('2d');
|
|
571
|
+
let dragStart = {x:0,y:0};
|
|
572
|
+
|
|
573
|
+
canvas.addEventListener('mousedown', e => { dragging = true; const v = viewDefs[activeView]; dragStart = {x: e.clientX - v.offset.x, y: e.clientY - v.offset.y}; });
|
|
574
|
+
window.addEventListener('mousemove', e => { if (dragging) { const v = viewDefs[activeView]; v.offset.x = e.clientX - dragStart.x; v.offset.y = e.clientY - dragStart.y; renderScene(); }});
|
|
575
|
+
window.addEventListener('mouseup', () => { dragging = false; });
|
|
576
|
+
canvas.addEventListener('wheel', e => { e.preventDefault(); const v = viewDefs[activeView]; v.scale = Math.max(1, Math.min(100, v.scale * (1 - e.deltaY*0.002))); renderScene(); }, {passive:false});
|
|
577
|
+
|
|
578
|
+
// View toggle
|
|
579
|
+
document.getElementById('view-bar').addEventListener('click', e => {
|
|
580
|
+
const btn = e.target.closest('.view-btn');
|
|
581
|
+
if (!btn) return;
|
|
582
|
+
activeView = btn.dataset.view;
|
|
583
|
+
document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b === btn));
|
|
584
|
+
renderScene();
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// --- WebSocket ---
|
|
589
|
+
function connectWS() {
|
|
590
|
+
const url = document.getElementById('ws-url').value.trim();
|
|
591
|
+
if (ws) { ws.close(); ws = null; }
|
|
592
|
+
ws = new WebSocket(url);
|
|
593
|
+
ws.onopen = () => { document.getElementById('ws-dot').className = 'status-dot dot-live'; document.getElementById('status-text').textContent = 'live'; };
|
|
594
|
+
ws.onmessage = (e) => {
|
|
595
|
+
try {
|
|
596
|
+
const msg = JSON.parse(e.data);
|
|
597
|
+
if (msg.event === 'scene_snapshot' && msg.roots) {
|
|
598
|
+
liveTree = msg.roots;
|
|
599
|
+
rebuildFlat(); renderTree(); renderAllViews();
|
|
600
|
+
if (selectedId) renderProps();
|
|
601
|
+
hideWelcome();
|
|
602
|
+
}
|
|
603
|
+
} catch (err) {}
|
|
604
|
+
};
|
|
605
|
+
ws.onclose = () => {
|
|
606
|
+
document.getElementById('ws-dot').className = 'status-dot dot-off';
|
|
607
|
+
document.getElementById('status-text').textContent = 'reconnecting...';
|
|
608
|
+
ws = null; setTimeout(connectWS, 2000);
|
|
609
|
+
};
|
|
610
|
+
}
|
|
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
|
+
|
|
645
|
+
// --- Search ---
|
|
646
|
+
document.getElementById('tree-search').addEventListener('input', (e) => {
|
|
647
|
+
searchFilter = e.target.value.trim(); rebuildFlat(); renderTree();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// --- Utils ---
|
|
651
|
+
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
652
|
+
function hideWelcome() { document.getElementById('welcome').classList.add('hidden'); }
|
|
653
|
+
|
|
654
|
+
// --- Init ---
|
|
655
|
+
window.addEventListener('resize', renderScene);
|
|
656
|
+
setupViewport();
|
|
657
|
+
renderScene();
|
|
658
|
+
connectWS();
|
|
659
|
+
</script>
|
|
660
|
+
</body>
|
|
661
|
+
</html>
|