lens-inspector 0.1.1 → 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 +52 -7
- 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/README.md
CHANGED
|
@@ -42,6 +42,35 @@ If you want to see it working right away, open the included `LSProject/` folder
|
|
|
42
42
|
|
|
43
43
|

|
|
44
44
|
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
### 3D Viewport with WASD Navigation
|
|
48
|
+
|
|
49
|
+
The viewer renders every SceneObject as a labeled wireframe box in a 3D viewport. Orbit with left-click drag, pan with middle-click or right-click, zoom with scroll wheel. Fly through the scene with WASD (forward/left/back/right), Q/E (down/up), and hold Shift for speed. Navigation keys use physical key positions so they work correctly on AZERTY, QWERTZ, and other keyboard layouts.
|
|
50
|
+
|
|
51
|
+
### Hierarchy Panel
|
|
52
|
+
|
|
53
|
+
The left panel shows a collapsible tree matching your scene structure. Each node displays a color-coded icon by component category (camera, visual, script, light, physics, audio, VFX, etc.) and a component count badge. Runtime-spawned objects appear in blue.
|
|
54
|
+
|
|
55
|
+
- **Search**: type to filter nodes by name, text content, component type, or category
|
|
56
|
+
- **Category chips**: click Camera, Visual, Script, Light, Physics, Audio, Anim, UI, Track, or Empty to show only that type
|
|
57
|
+
- **Expand/Collapse**: click the arrow to fold tree branches without affecting 3D visibility
|
|
58
|
+
- **Eye toggle**: hover a node and click the eye icon to hide or show it in the 3D viewport independently of the tree
|
|
59
|
+
|
|
60
|
+
### Inspector Panel
|
|
61
|
+
|
|
62
|
+
Click any node in the tree or 3D viewport to see its transform (position, scale, rotation), object properties (enabled, children count, layer, category), color, text content, and a full component list with type-specific details like mesh name, material, script asset, camera FOV, audio track, and physics mass.
|
|
63
|
+
|
|
64
|
+
### MCP Component Enrichment
|
|
65
|
+
|
|
66
|
+
The runtime scene graph walker detects components by probing 40+ known Lens Studio types individually (Camera, ScriptComponent, RenderMeshVisual, LightSource, etc.). When the generic `getComponentCount("Component")` returns 0 for certain LS versions or object types, the fallback catches what would otherwise show as empty nodes.
|
|
67
|
+
|
|
68
|
+
For additional component detail, paste your Lens Studio MCP config JSON into the welcome screen. The viewer proxies MCP calls through the inspector server to avoid CORS, fetching the design-time scene graph and merging component data into live nodes. Copy the config from **AI Assistant > MCP** in Lens Studio.
|
|
69
|
+
|
|
70
|
+
### Configurable Server
|
|
71
|
+
|
|
72
|
+
Change the server address in the welcome screen if you run the inspector on a different port or machine. The setting persists in localStorage.
|
|
73
|
+
|
|
45
74
|
## Two modes
|
|
46
75
|
|
|
47
76
|
**Runtime**: A Script component inside your lens walks the full scene graph using the SceneObject API, serializes the hierarchy into JSON, and sends it over WebSocket to the browser. This captures everything: both the objects you placed in the Scene Hierarchy and anything created by Scripts at runtime.
|
|
@@ -50,9 +79,23 @@ If you want to see it working right away, open the included `LSProject/` folder
|
|
|
50
79
|
|
|
51
80
|
**Design-time**: When your lens isn't running, the viewer can fetch the static scene structure through Lens Studio's MCP server. This is useful for inspecting your Scene Hierarchy without entering preview mode.
|
|
52
81
|
|
|
82
|
+
To use design-time mode:
|
|
83
|
+
|
|
84
|
+
1. In Lens Studio, go to **AI Assistant > MCP > Configure Server** and set a port
|
|
85
|
+
2. Click **Copy MCP Config** to get the JSON with server URL and auth token
|
|
86
|
+
3. In the inspector welcome screen, paste the full MCP config JSON into the MCP Config field
|
|
87
|
+
|
|
88
|
+
If your MCP server runs on a non-default port, you can also pass a token on the command line:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx lens-inspector --mcp http://localhost:8733/mcp --mcp-token YOUR_TOKEN
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The MCP config from Lens Studio also works with AI coding assistants like Claude. Paste it into your assistant's MCP server config to let it read and modify your scene programmatically.
|
|
95
|
+
|
|
53
96
|
## The viewer
|
|
54
97
|
|
|
55
|
-
The browser shows three synchronized panels. A
|
|
98
|
+
The browser shows three synchronized panels. A hierarchy tree on the left with search, category filters, and independent visibility toggles. A 3D viewport in the center with an infinite multi-level grid, axis graduations in centimeters/meters, a navigation gizmo, and WASD fly controls. And an inspector panel on the right showing transform data, component details, text content, and colors for the selected node. All three panels are resizable by dragging the borders between them.
|
|
56
99
|
|
|
57
100
|

|
|
58
101
|
|
|
@@ -82,17 +125,19 @@ The SceneInspector component exposes three inputs in Lens Studio's Inspector pan
|
|
|
82
125
|
The server accepts these flags:
|
|
83
126
|
|
|
84
127
|
```bash
|
|
85
|
-
node server.js --port 9000
|
|
86
|
-
node server.js --no-open
|
|
128
|
+
node server.js --port 9000 # custom port
|
|
129
|
+
node server.js --no-open # don't auto-open browser
|
|
130
|
+
node server.js --mcp http://localhost:8733/mcp # custom MCP URL
|
|
131
|
+
node server.js --mcp-token YOUR_TOKEN # MCP auth token
|
|
87
132
|
```
|
|
88
133
|
|
|
89
134
|
## Architecture
|
|
90
135
|
|
|
91
|
-
The entire tool is three files with
|
|
136
|
+
The entire tool is three files with no build step or external framework:
|
|
92
137
|
|
|
93
|
-
- **SceneInspector.ts** (~
|
|
94
|
-
- **server.js** (~
|
|
95
|
-
- **index.html** (single file): the entire browser viewer with
|
|
138
|
+
- **SceneInspector.ts** (~370 lines): LS component that walks and serializes the scene graph, with fallback type-specific component probing
|
|
139
|
+
- **server.js** (~200 lines): WebSocket relay between LS and browser, with an MCP proxy for design-time queries and live reload
|
|
140
|
+
- **index.html** (single file): the entire browser viewer with Three.js 3D viewport, hierarchy, and inspector
|
|
96
141
|
|
|
97
142
|
The code is meant to be read and modified. If you want to add custom component introspection or change the viewer layout, everything is right there in plain files.
|
|
98
143
|
|
package/SceneInspector.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
@component
|
|
20
20
|
export class SceneInspector extends BaseScriptComponent {
|
|
21
21
|
@input
|
|
22
|
-
@hint("Inspector server
|
|
22
|
+
@hint("Inspector server URL. Change port if you ran: npx lens-inspector --port XXXX")
|
|
23
23
|
public wsUrl: string = "ws://localhost:8200?role=ls";
|
|
24
24
|
|
|
25
25
|
@input
|
|
@@ -145,13 +145,134 @@ export class SceneInspector extends BaseScriptComponent {
|
|
|
145
145
|
return "unknown";
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
private processComponent(comp: any, typeName: string, components: any[]) {
|
|
149
|
+
var category = this.classifyComponent(typeName);
|
|
150
|
+
var compInfo: any = {
|
|
151
|
+
type: typeName,
|
|
152
|
+
enabled: comp.enabled !== false,
|
|
153
|
+
category: category,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Extract text content
|
|
157
|
+
if (typeName === "Text" || typeName === "Component.Text") {
|
|
158
|
+
compInfo.text = comp.text || null;
|
|
159
|
+
if (comp.size !== undefined) compInfo.textSize = comp.size;
|
|
160
|
+
}
|
|
161
|
+
if (typeName === "Text3D" || typeName === "Component.Text3D") {
|
|
162
|
+
compInfo.text = comp.text || null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Extract visual info
|
|
166
|
+
if (category === "visual") {
|
|
167
|
+
try {
|
|
168
|
+
var mat = comp.mainMaterial;
|
|
169
|
+
if (mat && mat.mainPass) {
|
|
170
|
+
var bc = mat.mainPass["baseColor"];
|
|
171
|
+
if (bc) compInfo._color = [
|
|
172
|
+
Math.round(bc.x * 255),
|
|
173
|
+
Math.round(bc.y * 255),
|
|
174
|
+
Math.round(bc.z * 255),
|
|
175
|
+
Math.round(bc.w * 255),
|
|
176
|
+
];
|
|
177
|
+
compInfo.materialName = mat.name || null;
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
try {
|
|
181
|
+
if (comp.mesh) compInfo.meshName = comp.mesh.name || null;
|
|
182
|
+
} catch (e) {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Extract script info
|
|
186
|
+
if (category === "script") {
|
|
187
|
+
try {
|
|
188
|
+
if (comp.scriptAsset) compInfo.scriptName = comp.scriptAsset.name || null;
|
|
189
|
+
} catch (e) {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Extract camera info
|
|
193
|
+
if (category === "camera") {
|
|
194
|
+
try {
|
|
195
|
+
compInfo.cameraType = comp.cameraType === 1 ? "orthographic" : "perspective";
|
|
196
|
+
if (comp.fov !== undefined) compInfo.fov = comp.fov;
|
|
197
|
+
if (comp.size !== undefined) compInfo.orthoSize = comp.size;
|
|
198
|
+
if (comp.near !== undefined) compInfo.near = comp.near;
|
|
199
|
+
if (comp.far !== undefined) compInfo.far = comp.far;
|
|
200
|
+
compInfo.renderOrder = comp.renderOrder || 0;
|
|
201
|
+
} catch (e) {}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Extract physics info
|
|
205
|
+
if (category === "physics") {
|
|
206
|
+
try {
|
|
207
|
+
if (comp.dynamic !== undefined) compInfo.dynamic = comp.dynamic;
|
|
208
|
+
if (comp.mass !== undefined) compInfo.mass = comp.mass;
|
|
209
|
+
} catch (e) {}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Extract audio info
|
|
213
|
+
if (category === "audio") {
|
|
214
|
+
try {
|
|
215
|
+
if (comp.volume !== undefined) compInfo.volume = comp.volume;
|
|
216
|
+
if (comp.audioTrack) compInfo.trackName = comp.audioTrack.name || null;
|
|
217
|
+
} catch (e) {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
components.push(compInfo);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Known LS component types to probe individually when generic lookup fails
|
|
224
|
+
private static KNOWN_TYPES: string[] = [
|
|
225
|
+
"Camera",
|
|
226
|
+
"ScriptComponent",
|
|
227
|
+
"RenderMeshVisual",
|
|
228
|
+
"Image",
|
|
229
|
+
"Text",
|
|
230
|
+
"Text3D",
|
|
231
|
+
"LightSource",
|
|
232
|
+
"ScreenTransform",
|
|
233
|
+
"ScreenRegionComponent",
|
|
234
|
+
"Canvas",
|
|
235
|
+
"AnimationMixer",
|
|
236
|
+
"AnimationPlayer",
|
|
237
|
+
"AudioComponent",
|
|
238
|
+
"AudioListenerComponent",
|
|
239
|
+
"VFXComponent",
|
|
240
|
+
"BodyComponent",
|
|
241
|
+
"ColliderComponent",
|
|
242
|
+
"WorldComponent",
|
|
243
|
+
"InteractionComponent",
|
|
244
|
+
"ManipulateComponent",
|
|
245
|
+
"DeviceTracking",
|
|
246
|
+
"ObjectTracking3D",
|
|
247
|
+
"MarkerTrackingComponent",
|
|
248
|
+
"Head",
|
|
249
|
+
"BlendShapes",
|
|
250
|
+
"SpriteVisual",
|
|
251
|
+
"PostEffectVisual",
|
|
252
|
+
"FaceMaskVisual",
|
|
253
|
+
"FaceInsetVisual",
|
|
254
|
+
"RectangleSetter",
|
|
255
|
+
"PinToMeshComponent",
|
|
256
|
+
"LookAtComponent",
|
|
257
|
+
"ClothVisual",
|
|
258
|
+
"HintsComponent",
|
|
259
|
+
"SkinComponent",
|
|
260
|
+
"MLComponent",
|
|
261
|
+
"RetouchVisual",
|
|
262
|
+
"EyeColorVisual",
|
|
263
|
+
"HairVisual",
|
|
264
|
+
"LiquifyVisual",
|
|
265
|
+
"ClearScreenComponent",
|
|
266
|
+
"ParticlesVisual",
|
|
267
|
+
];
|
|
268
|
+
|
|
148
269
|
private walkObject(obj: SceneObject, depth: number): any {
|
|
149
270
|
if (depth > this.maxDepth) return null;
|
|
150
271
|
|
|
151
272
|
var t = obj.getTransform();
|
|
152
|
-
var pos = t.
|
|
153
|
-
var scl = t.
|
|
154
|
-
var rot = t.
|
|
273
|
+
var pos = t.getWorldPosition();
|
|
274
|
+
var scl = t.getWorldScale();
|
|
275
|
+
var rot = t.getWorldRotation();
|
|
155
276
|
|
|
156
277
|
// Gather components
|
|
157
278
|
var components: any[] = [];
|
|
@@ -160,96 +281,54 @@ export class SceneInspector extends BaseScriptComponent {
|
|
|
160
281
|
var color: number[] | null = null;
|
|
161
282
|
var primaryCategory = "empty";
|
|
162
283
|
|
|
284
|
+
// Try generic component lookup first
|
|
163
285
|
var compCount = obj.getComponentCount("Component");
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
var mat = comp.mainMaterial;
|
|
191
|
-
if (mat && mat.mainPass) {
|
|
192
|
-
var bc = mat.mainPass["baseColor"];
|
|
193
|
-
if (bc) color = [
|
|
194
|
-
Math.round(bc.x * 255),
|
|
195
|
-
Math.round(bc.y * 255),
|
|
196
|
-
Math.round(bc.z * 255),
|
|
197
|
-
Math.round(bc.w * 255),
|
|
198
|
-
];
|
|
199
|
-
compInfo.materialName = mat.name || null;
|
|
200
|
-
}
|
|
201
|
-
} catch (e) {}
|
|
202
|
-
try {
|
|
203
|
-
if (comp.mesh) compInfo.meshName = comp.mesh.name || null;
|
|
204
|
-
} catch (e) {}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Extract script info
|
|
208
|
-
if (category === "script") {
|
|
209
|
-
try {
|
|
210
|
-
if (comp.scriptAsset) compInfo.scriptName = comp.scriptAsset.name || null;
|
|
211
|
-
} catch (e) {}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Extract camera info
|
|
215
|
-
if (category === "camera") {
|
|
216
|
-
try {
|
|
217
|
-
compInfo.cameraType = comp.cameraType === 1 ? "orthographic" : "perspective";
|
|
218
|
-
if (comp.fov !== undefined) compInfo.fov = comp.fov;
|
|
219
|
-
if (comp.size !== undefined) compInfo.orthoSize = comp.size;
|
|
220
|
-
if (comp.near !== undefined) compInfo.near = comp.near;
|
|
221
|
-
if (comp.far !== undefined) compInfo.far = comp.far;
|
|
222
|
-
compInfo.renderOrder = comp.renderOrder || 0;
|
|
223
|
-
} catch (e) {}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Extract physics info
|
|
227
|
-
if (category === "physics") {
|
|
228
|
-
try {
|
|
229
|
-
if (comp.dynamic !== undefined) compInfo.dynamic = comp.dynamic;
|
|
230
|
-
if (comp.mass !== undefined) compInfo.mass = comp.mass;
|
|
231
|
-
} catch (e) {}
|
|
232
|
-
}
|
|
286
|
+
if (compCount > 0) {
|
|
287
|
+
for (var ci = 0; ci < compCount; ci++) {
|
|
288
|
+
try {
|
|
289
|
+
var comp = obj.getComponentByIndex("Component", ci) as any;
|
|
290
|
+
var typeName = comp.getTypeName ? comp.getTypeName() : "Unknown";
|
|
291
|
+
this.processComponent(comp, typeName, components);
|
|
292
|
+
} catch (e) {}
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
// Fallback: probe each known type individually.
|
|
296
|
+
// getComponentCount("Component") returns 0 in some LS versions
|
|
297
|
+
// but type-specific lookups still work.
|
|
298
|
+
for (var ti = 0; ti < SceneInspector.KNOWN_TYPES.length; ti++) {
|
|
299
|
+
var probeName = SceneInspector.KNOWN_TYPES[ti];
|
|
300
|
+
try {
|
|
301
|
+
var probeCount = obj.getComponentCount(probeName);
|
|
302
|
+
for (var pi = 0; pi < probeCount; pi++) {
|
|
303
|
+
try {
|
|
304
|
+
var probeComp = obj.getComponentByIndex(probeName, pi) as any;
|
|
305
|
+
var probeTypeName = probeComp.getTypeName ? probeComp.getTypeName() : probeName;
|
|
306
|
+
this.processComponent(probeComp, probeTypeName, components);
|
|
307
|
+
} catch (e2) {}
|
|
308
|
+
}
|
|
309
|
+
} catch (e3) {}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
233
312
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
313
|
+
// Derive category and visual flags from collected components
|
|
314
|
+
for (var ci2 = 0; ci2 < components.length; ci2++) {
|
|
315
|
+
var compInfo = components[ci2];
|
|
316
|
+
var category = compInfo.category;
|
|
317
|
+
if (compInfo.text) text = compInfo.text;
|
|
318
|
+
if (category === "visual") hasVisual = true;
|
|
319
|
+
if (compInfo._color) color = compInfo._color;
|
|
241
320
|
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
321
|
+
if (primaryCategory === "empty" || primaryCategory === "unknown") {
|
|
322
|
+
if (category !== "unknown") primaryCategory = category;
|
|
323
|
+
}
|
|
324
|
+
if (category === "camera" || category === "script" || category === "visual" || category === "text") {
|
|
325
|
+
if (primaryCategory !== "camera") primaryCategory = category;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
250
328
|
|
|
251
|
-
|
|
252
|
-
|
|
329
|
+
// Clean internal fields
|
|
330
|
+
for (var ci3 = 0; ci3 < components.length; ci3++) {
|
|
331
|
+
delete components[ci3]._color;
|
|
253
332
|
}
|
|
254
333
|
|
|
255
334
|
// Get layer info
|
|
@@ -262,8 +341,8 @@ export class SceneInspector extends BaseScriptComponent {
|
|
|
262
341
|
// Walk children
|
|
263
342
|
var childNodes: any[] = [];
|
|
264
343
|
var childCount = obj.getChildrenCount();
|
|
265
|
-
for (var
|
|
266
|
-
var child = obj.getChild(
|
|
344
|
+
for (var ki = 0; ki < childCount; ki++) {
|
|
345
|
+
var child = obj.getChild(ki);
|
|
267
346
|
var childNode = this.walkObject(child, depth + 1);
|
|
268
347
|
if (childNode) childNodes.push(childNode);
|
|
269
348
|
}
|
package/example.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import WebSocket from "ws";
|
|
15
15
|
|
|
16
|
-
const URL = process.argv[2] || "ws://localhost:8200?role=ls";
|
|
16
|
+
const URL = process.argv[2] || "ws://localhost:8200?role=ls&source=example";
|
|
17
17
|
|
|
18
18
|
let frame = 0;
|
|
19
19
|
let spawned = [];
|
|
@@ -84,6 +84,7 @@ function buildScene() {
|
|
|
84
84
|
|
|
85
85
|
const scene = {
|
|
86
86
|
event: "scene_snapshot",
|
|
87
|
+
source: "example",
|
|
87
88
|
ts: Date.now(),
|
|
88
89
|
roots: [
|
|
89
90
|
// Perspective Camera with scripts
|
|
@@ -326,7 +327,12 @@ function connect() {
|
|
|
326
327
|
}, 1000 / 2);
|
|
327
328
|
});
|
|
328
329
|
|
|
329
|
-
ws.on("close", () => {
|
|
330
|
+
ws.on("close", (code, reason) => {
|
|
331
|
+
const msg = reason?.toString() || '';
|
|
332
|
+
if (code === 1000 && msg === 'replaced') {
|
|
333
|
+
console.log("Real Lens Studio connected. Example exiting.");
|
|
334
|
+
process.exit(0);
|
|
335
|
+
}
|
|
330
336
|
console.log("Disconnected, reconnecting in 2s...");
|
|
331
337
|
setTimeout(connect, 2000);
|
|
332
338
|
});
|