sceneview-mcp 3.4.12 → 3.4.13

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.
@@ -0,0 +1,551 @@
1
+ /**
2
+ * debug-issue.ts
3
+ *
4
+ * Targeted debugging guide for common SceneView issues.
5
+ * Given a symptom, returns a focused diagnostic checklist.
6
+ */
7
+ export const DEBUG_CATEGORIES = [
8
+ "model-not-showing",
9
+ "ar-not-working",
10
+ "crash",
11
+ "performance",
12
+ "build-error",
13
+ "black-screen",
14
+ "lighting",
15
+ "gestures",
16
+ "ios",
17
+ ];
18
+ const DEBUG_GUIDES = {
19
+ "model-not-showing": {
20
+ title: "Model Not Showing / Invisible",
21
+ guide: `## Debugging: Model Not Showing
22
+
23
+ ### Quick Diagnostic Checklist
24
+
25
+ 1. **Is \`rememberModelInstance\` returning null?**
26
+ - It returns \`null\` while loading AND if the file fails to load.
27
+ - Add a log: \`Log.d("SV", "model: \$modelInstance")\`
28
+ - Show a loading indicator while null.
29
+
30
+ 2. **Is the asset path correct?**
31
+ - Assets must be in \`src/main/assets/\` (not \`res/\`)
32
+ - Path is relative to assets root: \`"models/chair.glb"\` (no leading slash)
33
+ - Check file extension: \`.glb\` or \`.gltf\` (case-sensitive on Android)
34
+
35
+ 3. **Is there a light in the scene?**
36
+ - Without light, PBR models appear black (not invisible — but very dark).
37
+ - Add a directional light:
38
+ \`\`\`kotlin
39
+ LightNode(
40
+ engine = engine,
41
+ type = LightManager.Type.DIRECTIONAL,
42
+ apply = {
43
+ intensity(100_000f)
44
+ castShadows(true)
45
+ }
46
+ )
47
+ \`\`\`
48
+ - Or load an HDR environment for IBL.
49
+
50
+ 4. **Is the model too small or too large?**
51
+ - Default scale = 1.0 in model units. Some models are in millimeters (1000x too small) or centimeters (10x).
52
+ - Try \`scaleToUnits = 1.0f\` to normalize.
53
+ - Check in a 3D editor (Blender) what units the model uses.
54
+
55
+ 5. **Is the camera pointing at the model?**
56
+ - Default camera is at origin looking -Z.
57
+ - Model at (0, 0, 0) may be inside or behind the camera.
58
+ - Try \`centerOrigin = Position(0f, 0f, 0f)\` on ModelNode.
59
+ - Or move camera: \`cameraNode = rememberCameraNode(engine) { lookAt(Position(0f, 1f, 3f), Position(0f, 0f, 0f)) }\`
60
+
61
+ 6. **Is the Scene composable actually visible?**
62
+ - Check Modifier: \`Modifier.fillMaxSize()\` — not \`Modifier.size(0.dp)\` or hidden behind another composable.
63
+
64
+ 7. **Is the GLB file valid?**
65
+ - Test in https://gltf-viewer.donmccurdy.com/
66
+ - Corrupt or incompatible GLB files silently fail to load.
67
+
68
+ ### Common Fixes
69
+
70
+ \`\`\`kotlin
71
+ // Minimal working example — if this shows the model, the issue is in your code
72
+ @Composable
73
+ fun DebugModelViewer() {
74
+ val engine = rememberEngine()
75
+ val modelLoader = rememberModelLoader(engine)
76
+
77
+ val modelInstance = rememberModelInstance(modelLoader, "models/YOUR_FILE.glb")
78
+ Log.d("SceneView", "Model loaded: \${modelInstance != null}")
79
+
80
+ Scene(
81
+ modifier = Modifier.fillMaxSize(),
82
+ engine = engine,
83
+ modelLoader = modelLoader
84
+ ) {
85
+ // Light is essential!
86
+ LightNode(
87
+ engine = engine,
88
+ type = LightManager.Type.DIRECTIONAL,
89
+ apply = { intensity(100_000f) }
90
+ )
91
+
92
+ modelInstance?.let { instance ->
93
+ ModelNode(
94
+ modelInstance = instance,
95
+ scaleToUnits = 1.0f,
96
+ centerOrigin = Position(0f, 0f, 0f)
97
+ )
98
+ }
99
+ }
100
+ }
101
+ \`\`\``,
102
+ },
103
+ "ar-not-working": {
104
+ title: "AR Not Working",
105
+ guide: `## Debugging: AR Not Working
106
+
107
+ ### Quick Diagnostic Checklist
108
+
109
+ 1. **Camera permission granted?**
110
+ - AR requires \`CAMERA\` permission at runtime (not just manifest).
111
+ - Request it BEFORE showing ARScene:
112
+ \`\`\`kotlin
113
+ val launcher = rememberLauncherForActivityResult(
114
+ ActivityResultContracts.RequestPermission()
115
+ ) { granted -> showAR = granted }
116
+ LaunchedEffect(Unit) { launcher.launch(Manifest.permission.CAMERA) }
117
+ \`\`\`
118
+
119
+ 2. **ARCore installed?**
120
+ - Check: \`ArCoreApk.getInstance().checkAvailability(context)\`
121
+ - Emulators need Google Play Services for AR manually installed.
122
+ - Real devices auto-install if manifest has \`com.google.ar.core\` meta-data.
123
+
124
+ 3. **Manifest correct?**
125
+ \`\`\`xml
126
+ <uses-permission android:name="android.permission.CAMERA" />
127
+ <uses-feature android:name="android.hardware.camera.ar" android:required="true" />
128
+ <application>
129
+ <meta-data android:name="com.google.ar.core" android:value="required" />
130
+ </application>
131
+ \`\`\`
132
+
133
+ 4. **Planes not detecting?**
134
+ - Point at a **textured surface** (not plain white walls).
135
+ - Wait 2-3 seconds for ARCore to initialize.
136
+ - Check \`planeFindingMode\` is not \`DISABLED\`.
137
+ - Ensure good lighting — ARCore struggles in dim conditions.
138
+
139
+ 5. **Objects not appearing on tap?**
140
+ - Is \`onTouchEvent\` wired up?
141
+ - Is \`hitResult\` null? Log it: \`Log.d("AR", "hitResult: \$hitResult")\`
142
+ - Are you creating an anchor? \`hitResult.createAnchor()\` must be called.
143
+ - Is the model loaded? Check \`rememberModelInstance\` is not null.
144
+
145
+ 6. **Anchor drift / objects moving?**
146
+ - ALWAYS use \`AnchorNode(anchor = ...)\` — never set \`worldPosition\` manually.
147
+ - ARCore's coordinate system shifts during tracking; anchors compensate.
148
+
149
+ 7. **Testing on emulator?**
150
+ - ARCore emulator support is limited. Test on a real device.
151
+ - If using emulator: Extended Controls > Virtual Sensors > enable AR.
152
+
153
+ ### Common AR Crash Causes
154
+
155
+ | Symptom | Cause | Fix |
156
+ |---------|-------|-----|
157
+ | Crash on ARScene open | No camera permission | Request at runtime |
158
+ | "ARCore not installed" | Missing Play Services | Add manifest meta-data |
159
+ | Session create fails | Device not supported | Check \`checkAvailability()\` |
160
+ | Black camera feed | Permission denied | Check runtime permission result |`,
161
+ },
162
+ crash: {
163
+ title: "Crash / SIGABRT / Native Error",
164
+ guide: `## Debugging: Crashes
165
+
166
+ ### SIGABRT / Native Crash
167
+
168
+ **Most common cause:** Filament resource lifecycle violation.
169
+
170
+ #### 1. Wrong thread
171
+ - ALL Filament JNI calls must be on the **main thread**.
172
+ - Check for \`Dispatchers.IO\` or \`Dispatchers.Default\` near Filament calls.
173
+ - Fix: wrap in \`withContext(Dispatchers.Main) { ... }\`
174
+
175
+ #### 2. Wrong destroy order
176
+ - Materials MUST be destroyed before Textures.
177
+ - Textures MUST be destroyed before Engine.
178
+ - \`rememberEngine()\` handles this automatically — avoid manual destroy.
179
+ - If imperative: \`materialLoader.destroyMaterialInstance(mi)\` then \`engine.safeDestroyTexture(tex)\`
180
+
181
+ #### 3. Double destroy
182
+ - Calling \`engine.destroy()\` alongside \`rememberEngine()\` → SIGABRT.
183
+ - \`rememberEngine()\` destroys on composition disposal — remove manual calls.
184
+ - Same for nodes: don't call \`node.destroy()\` on composable nodes.
185
+
186
+ #### 4. Accessing destroyed resources
187
+ - If a composable leaves the tree, its nodes are destroyed.
188
+ - Accessing a node/model after removal → native crash.
189
+ - Fix: null-check before access, use Compose state properly.
190
+
191
+ ### NullPointerException
192
+
193
+ - \`rememberModelInstance\` returns null while loading — always null-check.
194
+ - \`hitResult\` can be null in \`onTouchEvent\` if no surface is hit.
195
+
196
+ ### Out of Memory
197
+
198
+ - Multiple \`Engine\` instances waste GPU memory → use single \`rememberEngine()\`.
199
+ - Large models (>100K triangles) on old devices → reduce poly count.
200
+ - Multiple 4K HDR environments → use 2K or smaller.
201
+
202
+ ### Diagnostic Steps
203
+
204
+ 1. Enable Filament debug logging:
205
+ \`\`\`kotlin
206
+ // In Application.onCreate()
207
+ System.setProperty("filament.backend.debug", "true")
208
+ \`\`\`
209
+
210
+ 2. Check logcat for Filament errors:
211
+ \`\`\`
212
+ adb logcat -s Filament
213
+ \`\`\`
214
+
215
+ 3. Run with Android Studio memory profiler to detect leaks.`,
216
+ },
217
+ performance: {
218
+ title: "Performance Problems",
219
+ guide: `## Debugging: Performance
220
+
221
+ ### Measuring Performance
222
+
223
+ 1. **Enable FPS overlay:**
224
+ \`\`\`kotlin
225
+ Scene(
226
+ engine = engine,
227
+ // Check frame time in onFrame callback
228
+ onFrame = { frameTimeNanos ->
229
+ val fps = 1_000_000_000.0 / frameTimeNanos
230
+ Log.d("FPS", "%.1f".format(fps))
231
+ }
232
+ )
233
+ \`\`\`
234
+
235
+ 2. **Android GPU Inspector** — shows exactly where GPU time goes.
236
+ 3. **Android Studio Profiler** — CPU and memory usage.
237
+
238
+ ### Common Performance Issues
239
+
240
+ #### Low FPS (<30)
241
+
242
+ | Cause | Fix |
243
+ |-------|-----|
244
+ | High poly model (>100K tris) | Reduce in Blender, use Draco/Meshopt compression |
245
+ | Uncompressed textures | Use KTX2 with Basis Universal, max 1024x1024 |
246
+ | Too many draw calls | Merge meshes in 3D editor (1 material = 1 draw call) |
247
+ | Per-frame allocations | Reuse objects in \`onFrame\`, avoid creating Position/Rotation each frame |
248
+ | Multiple engines | Use single \`rememberEngine()\`, never create multiple |
249
+ | Post-processing enabled | Disable if not needed: \`Scene(postProcessing = false)\` |
250
+ | Shadow-casting lights | Each shadow light = extra depth pass. Limit to 1-2. |
251
+
252
+ #### High Memory (>500MB)
253
+
254
+ | Cause | Fix |
255
+ |-------|-----|
256
+ | 4K HDR environments | Use 2K (\`sky_2k.hdr\`) |
257
+ | Multiple scenes | Each \`Scene\` = separate Filament View + Renderer |
258
+ | Unreleased models | Let unused models leave composition (auto-cleanup) |
259
+ | Bitmap texture leaks | Recycle bitmaps after Filament consumes them |
260
+ | Concurrent model loads | Max 3-4 simultaneous \`rememberModelInstance\` calls |
261
+
262
+ #### Model Optimization Checklist
263
+
264
+ - [ ] GLB format (not glTF multi-file)
265
+ - [ ] <100K triangles per model
266
+ - [ ] Textures <=1024x1024
267
+ - [ ] KTX2 compressed textures (Basis Universal)
268
+ - [ ] Draco or Meshopt mesh compression
269
+ - [ ] Single material per mesh where possible
270
+ - [ ] Remove unused animations / morph targets`,
271
+ },
272
+ "build-error": {
273
+ title: "Build / Gradle Errors",
274
+ guide: `## Debugging: Build Errors
275
+
276
+ ### "Cannot resolve io.github.sceneview:sceneview:3.3.0"
277
+
278
+ 1. Check repositories in \`settings.gradle.kts\`:
279
+ \`\`\`kotlin
280
+ dependencyResolutionManagement {
281
+ repositories {
282
+ google()
283
+ mavenCentral()
284
+ }
285
+ }
286
+ \`\`\`
287
+ 2. Check internet connectivity and proxy settings.
288
+ 3. Try: \`./gradlew --refresh-dependencies\`
289
+
290
+ ### Java Version Mismatch
291
+
292
+ SceneView requires **Java 17**.
293
+ \`\`\`kotlin
294
+ android {
295
+ compileOptions {
296
+ sourceCompatibility = JavaVersion.VERSION_17
297
+ targetCompatibility = JavaVersion.VERSION_17
298
+ }
299
+ kotlinOptions {
300
+ jvmTarget = "17"
301
+ }
302
+ }
303
+ \`\`\`
304
+
305
+ ### AGP / Gradle Version Compatibility
306
+
307
+ | AGP | Gradle | Status |
308
+ |-----|--------|--------|
309
+ | 8.7+ | 8.11.1+ | Recommended |
310
+ | 8.4-8.6 | 8.6-8.10 | Works |
311
+ | <8.4 | <8.6 | May have issues |
312
+
313
+ ### "Duplicate class" Errors
314
+
315
+ SceneView bundles Filament. If you also depend on Filament directly:
316
+ \`\`\`kotlin
317
+ // Remove direct Filament dependency — SceneView includes it
318
+ // implementation("com.google.android.filament:filament-android:1.x.x") // REMOVE
319
+ implementation("io.github.sceneview:sceneview:3.3.0") // This includes Filament
320
+ \`\`\`
321
+
322
+ ### "Cannot find Filament material"
323
+
324
+ - Pre-compiled materials live in \`src/main/assets/materials/\`
325
+ - Don't delete them when cleaning
326
+ - If \`filamentPluginEnabled=true\` in gradle.properties, you need Filament desktop tools
327
+
328
+ ### Gradle Clean
329
+
330
+ \`\`\`bash
331
+ ./gradlew clean
332
+ rm -rf ~/.gradle/caches
333
+ rm -rf .gradle
334
+ ./gradlew --refresh-dependencies
335
+ \`\`\``,
336
+ },
337
+ "black-screen": {
338
+ title: "Black Screen / No Rendering",
339
+ guide: `## Debugging: Black Screen
340
+
341
+ ### AR Black Screen
342
+
343
+ 1. **Camera permission not granted** — most common cause.
344
+ - Check logcat for permission denial.
345
+ - Request permission before showing ARScene.
346
+
347
+ 2. **ARCore not initialized** — takes 1-2 seconds.
348
+ - Show a loading overlay until first frame.
349
+
350
+ 3. **Device camera in use** — another app or the system camera is using it.
351
+ - Close other camera apps.
352
+
353
+ ### 3D Black Screen
354
+
355
+ 1. **No light source** — PBR models need light to be visible.
356
+ - Add \`LightNode\` or HDR environment.
357
+
358
+ 2. **Camera inside model** — default camera at origin, model also at origin.
359
+ - Set camera position: \`cameraNode = rememberCameraNode(engine) { lookAt(...) }\`
360
+ - Or set \`centerOrigin\` on ModelNode.
361
+
362
+ 3. **Environment not loaded** — HDR file path wrong or file missing.
363
+ - Check logcat for "Environment not found" messages.
364
+ - Test without environment first.
365
+
366
+ 4. **Scene composable has zero size** — \`Modifier.fillMaxSize()\` missing.
367
+
368
+ 5. **OpenGL ES version** — Filament requires OpenGL ES 3.0+.
369
+ - Check: \`GLES30.glGetString(GLES30.GL_VERSION)\`
370
+ - Very old devices (<2015) may not support it.
371
+
372
+ ### Diagnostic Code
373
+
374
+ \`\`\`kotlin
375
+ Scene(
376
+ modifier = Modifier
377
+ .fillMaxSize()
378
+ .background(Color.Red), // Should see red if Scene has zero size
379
+ engine = engine,
380
+ onFrame = { Log.d("SV", "Frame rendered") } // If not logged, Scene isn't rendering
381
+ ) {
382
+ // Minimal visible content
383
+ CubeNode(engine = engine, size = 1.0f)
384
+ LightNode(engine = engine, type = LightManager.Type.DIRECTIONAL, apply = { intensity(100_000f) })
385
+ }
386
+ \`\`\``,
387
+ },
388
+ lighting: {
389
+ title: "Lighting Issues",
390
+ guide: `## Debugging: Lighting Issues
391
+
392
+ ### Model Too Dark / Black
393
+
394
+ - **No light source** — add a directional light or HDR environment.
395
+ - **Intensity too low** — directional lights typically need 100,000+ lux.
396
+ - **Wrong light type** — \`POINT\` lights need to be near the model; \`DIRECTIONAL\` lights work everywhere.
397
+
398
+ ### Model Too Bright / Overexposed in AR
399
+
400
+ - **Tone mapping** — AR scenes use the camera feed; default tone mapping enhances contrast.
401
+ - Fix: set linear tone mapping on the AR view.
402
+ - **Double lighting** — if you add lights AND use \`ENVIRONMENTAL_HDR\`, the model gets double-lit.
403
+ - Fix: use one lighting method, not both.
404
+
405
+ ### Flat / No Shadows
406
+
407
+ - Shadows disabled by default in \`Scene\` (enabled in \`ARScene\`).
408
+ - Enable: \`Scene(view = rememberView(engine).also { it.setShadowingEnabled(true) })\`
409
+ - Light must have \`castShadows(true)\` in its \`apply\` block.
410
+
411
+ ### Environment / IBL Not Working
412
+
413
+ \`\`\`kotlin
414
+ val environmentLoader = rememberEnvironmentLoader(engine)
415
+ Scene(
416
+ environment = rememberEnvironment(environmentLoader) {
417
+ environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
418
+ ?: createEnvironment(environmentLoader)
419
+ }
420
+ ) { ... }
421
+ \`\`\`
422
+ - Check HDR file exists in \`src/main/assets/environments/\`.
423
+ - Use 2K HDR files (not 4K — wastes memory on mobile).`,
424
+ },
425
+ gestures: {
426
+ title: "Gesture / Interaction Issues",
427
+ guide: `## Debugging: Gesture Issues
428
+
429
+ ### Model Not Responding to Touch
430
+
431
+ 1. **\`isEditable\` not set:**
432
+ \`\`\`kotlin
433
+ ModelNode(
434
+ modelInstance = instance,
435
+ isEditable = true // Enables pinch-to-scale + drag-to-rotate
436
+ )
437
+ \`\`\`
438
+
439
+ 2. **\`onTouchEvent\` consuming events:**
440
+ - If \`onTouchEvent\` returns \`true\`, it consumes the event before nodes see it.
441
+ - Return \`false\` for events you don't handle.
442
+
443
+ 3. **Node has no collision shape:**
444
+ - By default, ModelNode uses its bounding box for hit testing.
445
+ - Geometry nodes (CubeNode, SphereNode) have built-in collision.
446
+
447
+ ### Tap-to-Place Not Working in AR
448
+
449
+ 1. Check \`hitResult\` is not null — log it.
450
+ 2. Ensure plane detection is enabled: \`planeFindingMode = HORIZONTAL_AND_VERTICAL\`.
451
+ 3. Plane renderer helps user see where planes are: \`planeRenderer = true\`.
452
+ 4. Create anchor from hit: \`hitResult.createAnchor()\`.
453
+
454
+ ### Camera Orbit Not Working
455
+
456
+ - Default Scene has orbit camera. If overridden:
457
+ \`\`\`kotlin
458
+ Scene(
459
+ cameraManipulator = rememberCameraManipulator() // Enables orbit
460
+ )
461
+ \`\`\`
462
+ - In AR, camera is controlled by ARCore — orbit is disabled.`,
463
+ },
464
+ ios: {
465
+ title: "iOS / SceneViewSwift Issues",
466
+ guide: `## Debugging: iOS Issues
467
+
468
+ ### Model Not Loading
469
+
470
+ 1. **Wrong format** — RealityKit uses USDZ or .reality, NOT GLB/glTF.
471
+ 2. **Missing \`try await\`** — \`ModelNode.load()\` is \`async throws\`:
472
+ \`\`\`swift
473
+ .task {
474
+ do {
475
+ model = try await ModelNode.load("models/car.usdz")
476
+ } catch {
477
+ print("Load failed: \\(error)")
478
+ }
479
+ }
480
+ \`\`\`
481
+ 3. **File not in bundle** — check Xcode: target > Build Phases > Copy Bundle Resources.
482
+ 4. **Using \`addChild(model)\` instead of \`addChild(model.entity)\`** — node wrappers aren't Entity subclasses.
483
+
484
+ ### AR Camera Black Screen (iOS)
485
+
486
+ - **Missing Info.plist entry:**
487
+ \`\`\`xml
488
+ <key>NSCameraUsageDescription</key>
489
+ <string>Camera needed for AR</string>
490
+ \`\`\`
491
+ - **Simulator** — ARKit doesn't work on simulators. Use a real device.
492
+ - **Device unsupported** — check \`ARWorldTrackingConfiguration.isSupported\`.
493
+
494
+ ### ARSceneView Crash on macOS / visionOS
495
+
496
+ - \`ARSceneView\` uses \`ARView\` which is iOS-only.
497
+ - macOS: use \`SceneView\` (3D only).
498
+ - visionOS: use \`RealityView\` with \`ARKitSession\` directly.
499
+
500
+ ### SPM Package Resolution Fails
501
+
502
+ - Require Xcode 15.0+ (iOS 17 / visionOS targets).
503
+ - Clean: Xcode > Product > Clean Build Folder.
504
+ - Reset packages: File > Packages > Reset Package Caches.
505
+ - URL must be exactly: \`https://github.com/SceneView/sceneview\`
506
+
507
+ ### Swift Concurrency Warnings
508
+
509
+ - RealityKit entities are main-actor-bound.
510
+ - Always load models in \`.task { }\` (inherits @MainActor) or annotate functions with \`@MainActor\`.
511
+ - SceneViewSwift nodes are \`@unchecked Sendable\` — the warning is expected.`,
512
+ },
513
+ };
514
+ export function getDebugGuide(category) {
515
+ const entry = DEBUG_GUIDES[category];
516
+ if (!entry) {
517
+ return `Unknown debug category "${category}". Available: ${DEBUG_CATEGORIES.join(", ")}`;
518
+ }
519
+ return `# ${entry.title}\n\n${entry.guide}`;
520
+ }
521
+ export function autoDetectIssue(description) {
522
+ const lower = description.toLowerCase();
523
+ if (lower.includes("not showing") || lower.includes("invisible") || lower.includes("can't see") || lower.includes("model doesn't appear")) {
524
+ return "model-not-showing";
525
+ }
526
+ if (lower.includes("ar not") || lower.includes("ar doesn't") || lower.includes("arcore") || lower.includes("plane") || lower.includes("anchor")) {
527
+ return "ar-not-working";
528
+ }
529
+ if (lower.includes("crash") || lower.includes("sigabrt") || lower.includes("native") || lower.includes("fatal") || lower.includes("exception")) {
530
+ return "crash";
531
+ }
532
+ if (lower.includes("slow") || lower.includes("fps") || lower.includes("lag") || lower.includes("jank") || lower.includes("performance") || lower.includes("memory")) {
533
+ return "performance";
534
+ }
535
+ if (lower.includes("build") || lower.includes("gradle") || lower.includes("compile") || lower.includes("dependency") || lower.includes("cannot resolve")) {
536
+ return "build-error";
537
+ }
538
+ if (lower.includes("black screen") || lower.includes("blank") || lower.includes("nothing renders")) {
539
+ return "black-screen";
540
+ }
541
+ if (lower.includes("dark") || lower.includes("bright") || lower.includes("light") || lower.includes("shadow") || lower.includes("overexposed")) {
542
+ return "lighting";
543
+ }
544
+ if (lower.includes("touch") || lower.includes("gesture") || lower.includes("tap") || lower.includes("drag") || lower.includes("rotate") || lower.includes("interact")) {
545
+ return "gestures";
546
+ }
547
+ if (lower.includes("ios") || lower.includes("swift") || lower.includes("xcode") || lower.includes("spm") || lower.includes("realitykit") || lower.includes("usdz")) {
548
+ return "ios";
549
+ }
550
+ return null;
551
+ }