sceneview-mcp 3.1.1 → 3.3.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 +208 -66
- package/dist/guides.js +603 -0
- package/dist/index.js +346 -15
- package/dist/issues.js +2 -2
- package/dist/migration.js +3 -3
- package/dist/node-reference.js +73 -0
- package/dist/samples.js +784 -158
- package/dist/validator.js +287 -3
- package/llms.txt +1746 -19
- package/package.json +12 -5
package/dist/validator.js
CHANGED
|
@@ -196,6 +196,132 @@ const RULES = [
|
|
|
196
196
|
return issues;
|
|
197
197
|
},
|
|
198
198
|
},
|
|
199
|
+
// ─── FogNode missing view parameter ──────────────────────────────────────
|
|
200
|
+
{
|
|
201
|
+
id: "api/fog-node-missing-view",
|
|
202
|
+
severity: "error",
|
|
203
|
+
check(code, lines) {
|
|
204
|
+
const issues = [];
|
|
205
|
+
if (!code.includes("FogNode"))
|
|
206
|
+
return issues;
|
|
207
|
+
const fogLines = findLines(lines, /FogNode\s*\(/);
|
|
208
|
+
fogLines.forEach((line) => {
|
|
209
|
+
const block = lines.slice(line - 1, line + 5).join("\n");
|
|
210
|
+
if (!block.includes("view")) {
|
|
211
|
+
issues.push({
|
|
212
|
+
severity: "error",
|
|
213
|
+
rule: "api/fog-node-missing-view",
|
|
214
|
+
message: "`FogNode` requires a `view` parameter — the same Filament View passed to `Scene(view = view)`. Create one with `val view = rememberView(engine)` and pass it to both `Scene` and `FogNode`.",
|
|
215
|
+
line,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
return issues;
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
// ─── ReflectionProbeNode missing cameraPosition ────────────────────────
|
|
223
|
+
{
|
|
224
|
+
id: "api/reflection-probe-missing-camera",
|
|
225
|
+
severity: "warning",
|
|
226
|
+
check(code, lines) {
|
|
227
|
+
const issues = [];
|
|
228
|
+
if (!code.includes("ReflectionProbeNode"))
|
|
229
|
+
return issues;
|
|
230
|
+
const probeLines = findLines(lines, /ReflectionProbeNode\s*\(/);
|
|
231
|
+
probeLines.forEach((line) => {
|
|
232
|
+
const block = lines.slice(line - 1, line + 8).join("\n");
|
|
233
|
+
if (!block.includes("cameraPosition")) {
|
|
234
|
+
issues.push({
|
|
235
|
+
severity: "warning",
|
|
236
|
+
rule: "api/reflection-probe-missing-camera",
|
|
237
|
+
message: "`ReflectionProbeNode` needs `cameraPosition` to detect when the camera enters its zone. Track it in `onFrame`: `onFrame = { cameraPosition = cameraNode.worldPosition }` and pass it to the probe.",
|
|
238
|
+
line,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return issues;
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
// ─── PhysicsNode without radius offset ──────────────────────────────────
|
|
246
|
+
{
|
|
247
|
+
id: "api/physics-node-missing-radius",
|
|
248
|
+
severity: "info",
|
|
249
|
+
check(code, lines) {
|
|
250
|
+
const issues = [];
|
|
251
|
+
if (!code.includes("PhysicsNode"))
|
|
252
|
+
return issues;
|
|
253
|
+
const physicsLines = findLines(lines, /PhysicsNode\s*\(/);
|
|
254
|
+
physicsLines.forEach((line) => {
|
|
255
|
+
const block = lines.slice(line - 1, line + 8).join("\n");
|
|
256
|
+
if (!block.includes("radius")) {
|
|
257
|
+
issues.push({
|
|
258
|
+
severity: "info",
|
|
259
|
+
rule: "api/physics-node-missing-radius",
|
|
260
|
+
message: "`PhysicsNode` defaults to `radius = 0f` — the collision point is the node centre, not the surface. For spheres, set `radius` to match the sphere radius so the surface touches the floor.",
|
|
261
|
+
line,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return issues;
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
// ─── DynamicSkyNode outside SceneScope ──────────────────────────────────
|
|
269
|
+
{
|
|
270
|
+
id: "api/dynamic-sky-outside-scene",
|
|
271
|
+
severity: "warning",
|
|
272
|
+
check(code, lines) {
|
|
273
|
+
const issues = [];
|
|
274
|
+
if (!code.includes("DynamicSkyNode"))
|
|
275
|
+
return issues;
|
|
276
|
+
// If DynamicSkyNode appears but Scene { } doesn't, it's likely wrong
|
|
277
|
+
if (!code.includes("Scene(") && !code.includes("Scene {")) {
|
|
278
|
+
findLines(lines, /DynamicSkyNode\s*\(/).forEach((line) => issues.push({
|
|
279
|
+
severity: "warning",
|
|
280
|
+
rule: "api/dynamic-sky-outside-scene",
|
|
281
|
+
message: "`DynamicSkyNode` is a `SceneScope` extension composable — it must be declared inside a `Scene { }` content block, not at the top level.",
|
|
282
|
+
line,
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
return issues;
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
// ─── Deprecated Sceneform imports ───────────────────────────────────────
|
|
289
|
+
{
|
|
290
|
+
id: "migration/sceneform-import",
|
|
291
|
+
severity: "error",
|
|
292
|
+
check(code, lines) {
|
|
293
|
+
const issues = [];
|
|
294
|
+
findLines(lines, /import\s+com\.google\.ar\.sceneform/).forEach((line) => issues.push({
|
|
295
|
+
severity: "error",
|
|
296
|
+
rule: "migration/sceneform-import",
|
|
297
|
+
message: "Sceneform imports detected (`com.google.ar.sceneform.*`). Sceneform was deprecated by Google in 2021. Use `io.github.sceneview.*` imports instead — SceneView is the official successor.",
|
|
298
|
+
line,
|
|
299
|
+
}));
|
|
300
|
+
return issues;
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
// ─── Node.destroy() called manually ────────────────────────────────────
|
|
304
|
+
{
|
|
305
|
+
id: "lifecycle/manual-node-destroy",
|
|
306
|
+
severity: "warning",
|
|
307
|
+
check(code, lines) {
|
|
308
|
+
const issues = [];
|
|
309
|
+
if (!code.includes("Scene(") && !code.includes("Scene {"))
|
|
310
|
+
return issues;
|
|
311
|
+
findLines(lines, /\bnode\w*\.destroy\(\)|\.\bdestroy\(\)/).forEach((line) => {
|
|
312
|
+
const lineContent = lines[line - 1];
|
|
313
|
+
if (lineContent && !lineContent.includes("engine") && !lineContent.includes("Engine")) {
|
|
314
|
+
issues.push({
|
|
315
|
+
severity: "warning",
|
|
316
|
+
rule: "lifecycle/manual-node-destroy",
|
|
317
|
+
message: "Manual `destroy()` on a node inside a composable Scene. Compose manages node lifecycle automatically — nodes are destroyed when they leave composition. Remove the manual call to avoid double-destroy crashes.",
|
|
318
|
+
line,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
return issues;
|
|
323
|
+
},
|
|
324
|
+
},
|
|
199
325
|
// ─── Scene missing engine param ───────────────────────────────────────────
|
|
200
326
|
{
|
|
201
327
|
id: "api/scene-missing-engine",
|
|
@@ -220,9 +346,167 @@ const RULES = [
|
|
|
220
346
|
},
|
|
221
347
|
},
|
|
222
348
|
];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
349
|
+
// ─── Swift Validation Rules ──────────────────────────────────────────────────
|
|
350
|
+
const SWIFT_RULES = [
|
|
351
|
+
// ─── Missing @MainActor for async model loading ────────────────────────────
|
|
352
|
+
{
|
|
353
|
+
id: "swift/missing-main-actor",
|
|
354
|
+
severity: "warning",
|
|
355
|
+
check(code, lines) {
|
|
356
|
+
const issues = [];
|
|
357
|
+
// If there's an async function that loads models but no @MainActor annotation
|
|
358
|
+
if (/func\s+\w+.*async/.test(code) && /ModelNode\.load\(/.test(code)) {
|
|
359
|
+
if (!code.includes("@MainActor")) {
|
|
360
|
+
findLines(lines, /func\s+\w+.*async/).forEach((line) => issues.push({
|
|
361
|
+
severity: "warning",
|
|
362
|
+
rule: "swift/missing-main-actor",
|
|
363
|
+
message: "Async function that loads RealityKit models should be annotated with `@MainActor` or called from a `@MainActor` context. RealityKit entity operations are main-thread-bound. Using `.task { }` in SwiftUI is already `@MainActor`-isolated, but standalone functions should be annotated.",
|
|
364
|
+
line,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return issues;
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
// ─── Missing async/await for ModelNode.load ────────────────────────────────
|
|
372
|
+
{
|
|
373
|
+
id: "swift/model-load-not-async",
|
|
374
|
+
severity: "error",
|
|
375
|
+
check(code, lines) {
|
|
376
|
+
const issues = [];
|
|
377
|
+
// ModelNode.load is async throws — must use try await
|
|
378
|
+
if (/ModelNode\.load\(/.test(code)) {
|
|
379
|
+
findLines(lines, /ModelNode\.load\(/).forEach((line) => {
|
|
380
|
+
const lineContent = lines[line - 1];
|
|
381
|
+
if (lineContent && !lineContent.includes("await") && !lineContent.includes("try")) {
|
|
382
|
+
issues.push({
|
|
383
|
+
severity: "error",
|
|
384
|
+
rule: "swift/model-load-not-async",
|
|
385
|
+
message: "`ModelNode.load()` is `async throws` — you must call it with `try await ModelNode.load(\"model.usdz\")` inside a `.task { }` block or async function.",
|
|
386
|
+
line,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return issues;
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
// ─── Missing import statements ─────────────────────────────────────────────
|
|
395
|
+
{
|
|
396
|
+
id: "swift/missing-imports",
|
|
397
|
+
severity: "warning",
|
|
398
|
+
check(code, lines) {
|
|
399
|
+
const issues = [];
|
|
400
|
+
// Check if SceneViewSwift types are used but not imported
|
|
401
|
+
const svTypes = ["SceneView", "ARSceneView", "ModelNode", "GeometryNode", "LightNode", "TextNode", "VideoNode", "PhysicsNode", "BillboardNode", "AnchorNode", "AugmentedImageNode", "ImageNode", "CameraNode", "PathNode", "LineNode", "MeshNode", "FogNode", "DynamicSkyNode", "ReflectionProbeNode"];
|
|
402
|
+
const usesSceneView = svTypes.some((t) => code.includes(t));
|
|
403
|
+
if (usesSceneView && !code.includes("import SceneViewSwift")) {
|
|
404
|
+
issues.push({
|
|
405
|
+
severity: "warning",
|
|
406
|
+
rule: "swift/missing-imports",
|
|
407
|
+
message: "SceneViewSwift types used but `import SceneViewSwift` not found. Add `import SceneViewSwift` at the top of the file.",
|
|
408
|
+
line: 1,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
// Check for RealityKit types without import
|
|
412
|
+
const rkTypes = ["Entity", "ModelEntity", "AnchorEntity", "RealityView", "MeshResource", "SimpleMaterial"];
|
|
413
|
+
const usesRK = rkTypes.some((t) => new RegExp(`\\b${t}\\b`).test(code));
|
|
414
|
+
if (usesRK && !code.includes("import RealityKit")) {
|
|
415
|
+
issues.push({
|
|
416
|
+
severity: "warning",
|
|
417
|
+
rule: "swift/missing-imports",
|
|
418
|
+
message: "RealityKit types used but `import RealityKit` not found. Add `import RealityKit` at the top of the file.",
|
|
419
|
+
line: 1,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return issues;
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
// ─── Common RealityKit mistakes: using addChild on non-Entity ──────────────
|
|
426
|
+
{
|
|
427
|
+
id: "swift/add-child-wrong-type",
|
|
428
|
+
severity: "error",
|
|
429
|
+
check(code, lines) {
|
|
430
|
+
const issues = [];
|
|
431
|
+
// Common mistake: passing ModelNode instead of ModelNode.entity
|
|
432
|
+
const patterns = [
|
|
433
|
+
[/addChild\(\s*model\s*\)/, "model"],
|
|
434
|
+
[/addChild\(\s*cube\s*\)/, "cube"],
|
|
435
|
+
[/addChild\(\s*sphere\s*\)/, "sphere"],
|
|
436
|
+
[/addChild\(\s*light\s*\)/, "light"],
|
|
437
|
+
[/addChild\(\s*text\s*\)/, "text"],
|
|
438
|
+
];
|
|
439
|
+
for (const [pat, name] of patterns) {
|
|
440
|
+
// Only flag if the variable is likely a SceneViewSwift node wrapper, not a raw Entity
|
|
441
|
+
if (pat.test(code) && new RegExp(`(ModelNode|GeometryNode|LightNode|TextNode|VideoNode|BillboardNode).*\\b${name}\\b`).test(code)) {
|
|
442
|
+
findLines(lines, pat).forEach((line) => issues.push({
|
|
443
|
+
severity: "error",
|
|
444
|
+
rule: "swift/add-child-wrong-type",
|
|
445
|
+
message: `\`addChild(${name})\` — SceneViewSwift node wrappers are not \`Entity\` subclasses. Use \`addChild(${name}.entity)\` to add the underlying RealityKit entity.`,
|
|
446
|
+
line,
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return issues;
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
// ─── ARSceneView used on macOS or visionOS ─────────────────────────────────
|
|
454
|
+
{
|
|
455
|
+
id: "swift/ar-platform-check",
|
|
456
|
+
severity: "info",
|
|
457
|
+
check(code, lines) {
|
|
458
|
+
const issues = [];
|
|
459
|
+
if (code.includes("ARSceneView")) {
|
|
460
|
+
if (code.includes("macOS") || code.includes("visionOS")) {
|
|
461
|
+
findLines(lines, /ARSceneView/).forEach((line) => issues.push({
|
|
462
|
+
severity: "info",
|
|
463
|
+
rule: "swift/ar-platform-check",
|
|
464
|
+
message: "`ARSceneView` uses `ARView` which is only available on iOS. For macOS use `SceneView` (3D only). For visionOS, use RealityKit's `RealityView` with `ARKitSession` directly.",
|
|
465
|
+
line,
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return issues;
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
// ─── Missing error handling on model load ──────────────────────────────────
|
|
473
|
+
{
|
|
474
|
+
id: "swift/unhandled-model-load-error",
|
|
475
|
+
severity: "warning",
|
|
476
|
+
check(code, lines) {
|
|
477
|
+
const issues = [];
|
|
478
|
+
// Using try? silently swallows errors — warning but not error
|
|
479
|
+
if (/try\?\s+await\s+ModelNode\.load/.test(code)) {
|
|
480
|
+
findLines(lines, /try\?\s+await\s+ModelNode\.load/).forEach((line) => issues.push({
|
|
481
|
+
severity: "warning",
|
|
482
|
+
rule: "swift/unhandled-model-load-error",
|
|
483
|
+
message: "`try?` on `ModelNode.load()` silently swallows load failures. Consider using `do { try await ... } catch { print(error) }` to at least log failures, or show a loading indicator.",
|
|
484
|
+
line,
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
return issues;
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
function detectLanguage(code) {
|
|
492
|
+
// Heuristic: if the code has Swift markers, validate as Swift
|
|
493
|
+
if (code.includes("import SwiftUI") ||
|
|
494
|
+
code.includes("import RealityKit") ||
|
|
495
|
+
code.includes("import SceneViewSwift") ||
|
|
496
|
+
code.includes("struct ") && code.includes(": View") ||
|
|
497
|
+
code.includes("@State private var") ||
|
|
498
|
+
/SceneView\s*\{.*\bin\b/.test(code)) {
|
|
499
|
+
return "swift";
|
|
500
|
+
}
|
|
501
|
+
return "kotlin";
|
|
502
|
+
}
|
|
503
|
+
export function validateCode(code) {
|
|
504
|
+
const lines = code.split("\n");
|
|
505
|
+
const lang = detectLanguage(code);
|
|
506
|
+
if (lang === "swift") {
|
|
507
|
+
return SWIFT_RULES.flatMap((rule) => rule.check(code, lines));
|
|
508
|
+
}
|
|
509
|
+
return RULES.flatMap((rule) => rule.check(code, lines));
|
|
226
510
|
}
|
|
227
511
|
export function formatValidationReport(issues) {
|
|
228
512
|
if (issues.length === 0) {
|