sceneview-mcp 3.4.7 → 3.4.8
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 +1 -1
- package/README.md +10 -2
- package/dist/index.js +33 -22
- package/dist/samples.js +564 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/sceneview-mcp)
|
|
4
4
|
[](https://www.npmjs.com/package/sceneview-mcp)
|
|
5
5
|
[](https://modelcontextprotocol.io/)
|
|
6
|
-
[](./LICENSE)
|
|
7
7
|
[](https://nodejs.org/)
|
|
8
8
|
|
|
9
|
+
> **Disclaimer:** This tool generates code suggestions for the SceneView SDK. Generated code is provided "as is" without warranty. Always review generated code before use in production. This is not a substitute for professional software engineering review. See [TERMS.md](./TERMS.md) and [PRIVACY.md](./PRIVACY.md).
|
|
10
|
+
|
|
9
11
|
The official [Model Context Protocol](https://modelcontextprotocol.io/) server for **SceneView** — giving AI assistants deep knowledge of the SceneView 3D/AR SDK so they generate correct, compilable Kotlin code.
|
|
10
12
|
|
|
11
13
|
---
|
|
@@ -265,6 +267,12 @@ Published to npm on each SceneView release:
|
|
|
265
267
|
npm publish --access public
|
|
266
268
|
```
|
|
267
269
|
|
|
270
|
+
## Legal
|
|
271
|
+
|
|
272
|
+
- [LICENSE](./LICENSE) — MIT License
|
|
273
|
+
- [TERMS.md](./TERMS.md) — Terms of Service
|
|
274
|
+
- [PRIVACY.md](./PRIVACY.md) — Privacy Policy (no data collected)
|
|
275
|
+
|
|
268
276
|
## License
|
|
269
277
|
|
|
270
|
-
|
|
278
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,17 @@ import { parseNodeSections, findNodeSection, listNodeTypes } from "./node-refere
|
|
|
13
13
|
import { PLATFORM_ROADMAP, BEST_PRACTICES, AR_SETUP_GUIDE, TROUBLESHOOTING_GUIDE } from "./guides.js";
|
|
14
14
|
import { buildPreviewUrl, validatePreviewInput, formatPreviewResponse } from "./preview.js";
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
// ─── Legal disclaimer ─────────────────────────────────────────────────────────
|
|
17
|
+
const DISCLAIMER = '\n\n---\n*Generated code suggestion. Review before use in production. See [TERMS.md](https://github.com/SceneView/sceneview/blob/main/mcp/TERMS.md).*';
|
|
18
|
+
function withDisclaimer(content) {
|
|
19
|
+
if (content.length === 0)
|
|
20
|
+
return content;
|
|
21
|
+
const last = content[content.length - 1];
|
|
22
|
+
return [
|
|
23
|
+
...content.slice(0, -1),
|
|
24
|
+
{ ...last, text: last.text + DISCLAIMER },
|
|
25
|
+
];
|
|
26
|
+
}
|
|
16
27
|
let API_DOCS;
|
|
17
28
|
try {
|
|
18
29
|
API_DOCS = readFileSync(resolve(__dirname, "../llms.txt"), "utf-8");
|
|
@@ -271,7 +282,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
271
282
|
const codeLang = isIos ? "swift" : "kotlin";
|
|
272
283
|
const codeLabel = isIos ? "**Swift (SwiftUI):**" : "**Kotlin (Jetpack Compose):**";
|
|
273
284
|
return {
|
|
274
|
-
content: [
|
|
285
|
+
content: withDisclaimer([
|
|
275
286
|
{
|
|
276
287
|
type: "text",
|
|
277
288
|
text: [
|
|
@@ -290,7 +301,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
290
301
|
`> ${sample.prompt}`,
|
|
291
302
|
].join("\n"),
|
|
292
303
|
},
|
|
293
|
-
],
|
|
304
|
+
]),
|
|
294
305
|
};
|
|
295
306
|
}
|
|
296
307
|
// ── list_samples ──────────────────────────────────────────────────────────
|
|
@@ -316,14 +327,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
316
327
|
return `### \`${s.id}\`\n**${s.title}**${s.language === "swift" ? " (Swift/iOS)" : ""}\n${s.description}\n*Tags:* ${s.tags.join(", ")}\n${depLabel} \`${s.dependency}\`\n\nCall \`get_sample("${s.id}")\` for the full code.`;
|
|
317
328
|
})
|
|
318
329
|
.join("\n\n---\n\n");
|
|
319
|
-
return { content: [{ type: "text", text: header + rows }] };
|
|
330
|
+
return { content: withDisclaimer([{ type: "text", text: header + rows }]) };
|
|
320
331
|
}
|
|
321
332
|
// ── get_setup ─────────────────────────────────────────────────────────────
|
|
322
333
|
case "get_setup": {
|
|
323
334
|
const type = request.params.arguments?.type;
|
|
324
335
|
if (type === "3d") {
|
|
325
336
|
return {
|
|
326
|
-
content: [
|
|
337
|
+
content: withDisclaimer([
|
|
327
338
|
{
|
|
328
339
|
type: "text",
|
|
329
340
|
text: [
|
|
@@ -339,12 +350,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
339
350
|
`No manifest changes required for 3D-only scenes.`,
|
|
340
351
|
].join("\n"),
|
|
341
352
|
},
|
|
342
|
-
],
|
|
353
|
+
]),
|
|
343
354
|
};
|
|
344
355
|
}
|
|
345
356
|
if (type === "ar") {
|
|
346
357
|
return {
|
|
347
|
-
content: [
|
|
358
|
+
content: withDisclaimer([
|
|
348
359
|
{
|
|
349
360
|
type: "text",
|
|
350
361
|
text: [
|
|
@@ -367,7 +378,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
367
378
|
`\`\`\``,
|
|
368
379
|
].join("\n"),
|
|
369
380
|
},
|
|
370
|
-
],
|
|
381
|
+
]),
|
|
371
382
|
};
|
|
372
383
|
}
|
|
373
384
|
return {
|
|
@@ -386,11 +397,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
386
397
|
}
|
|
387
398
|
const issues = validateCode(code);
|
|
388
399
|
const report = formatValidationReport(issues);
|
|
389
|
-
return { content: [{ type: "text", text: report }] };
|
|
400
|
+
return { content: withDisclaimer([{ type: "text", text: report }]) };
|
|
390
401
|
}
|
|
391
402
|
// ── get_migration_guide ───────────────────────────────────────────────────
|
|
392
403
|
case "get_migration_guide": {
|
|
393
|
-
return { content: [{ type: "text", text: MIGRATION_GUIDE }] };
|
|
404
|
+
return { content: withDisclaimer([{ type: "text", text: MIGRATION_GUIDE }]) };
|
|
394
405
|
}
|
|
395
406
|
// ── get_node_reference ────────────────────────────────────────────────────
|
|
396
407
|
case "get_node_reference": {
|
|
@@ -419,7 +430,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
419
430
|
};
|
|
420
431
|
}
|
|
421
432
|
return {
|
|
422
|
-
content: [
|
|
433
|
+
content: withDisclaimer([
|
|
423
434
|
{
|
|
424
435
|
type: "text",
|
|
425
436
|
text: [
|
|
@@ -428,33 +439,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
428
439
|
section.content,
|
|
429
440
|
].join("\n"),
|
|
430
441
|
},
|
|
431
|
-
],
|
|
442
|
+
]),
|
|
432
443
|
};
|
|
433
444
|
}
|
|
434
445
|
// ── get_platform_roadmap ────────────────────────────────────────────────
|
|
435
446
|
case "get_platform_roadmap": {
|
|
436
|
-
return { content: [{ type: "text", text: PLATFORM_ROADMAP }] };
|
|
447
|
+
return { content: withDisclaimer([{ type: "text", text: PLATFORM_ROADMAP }]) };
|
|
437
448
|
}
|
|
438
449
|
// ── get_best_practices ───────────────────────────────────────────────────
|
|
439
450
|
case "get_best_practices": {
|
|
440
451
|
const category = request.params.arguments?.category || "all";
|
|
441
452
|
const text = BEST_PRACTICES[category] ?? BEST_PRACTICES["all"];
|
|
442
|
-
return { content: [{ type: "text", text }] };
|
|
453
|
+
return { content: withDisclaimer([{ type: "text", text }]) };
|
|
443
454
|
}
|
|
444
455
|
// ── get_ar_setup ─────────────────────────────────────────────────────────
|
|
445
456
|
case "get_ar_setup": {
|
|
446
|
-
return { content: [{ type: "text", text: AR_SETUP_GUIDE }] };
|
|
457
|
+
return { content: withDisclaimer([{ type: "text", text: AR_SETUP_GUIDE }]) };
|
|
447
458
|
}
|
|
448
459
|
// ── get_troubleshooting ──────────────────────────────────────────────────
|
|
449
460
|
case "get_troubleshooting": {
|
|
450
|
-
return { content: [{ type: "text", text: TROUBLESHOOTING_GUIDE }] };
|
|
461
|
+
return { content: withDisclaimer([{ type: "text", text: TROUBLESHOOTING_GUIDE }]) };
|
|
451
462
|
}
|
|
452
463
|
// ── get_ios_setup ─────────────────────────────────────────────────────────
|
|
453
464
|
case "get_ios_setup": {
|
|
454
465
|
const iosType = request.params.arguments?.type;
|
|
455
466
|
if (iosType === "3d") {
|
|
456
467
|
return {
|
|
457
|
-
content: [
|
|
468
|
+
content: withDisclaimer([
|
|
458
469
|
{
|
|
459
470
|
type: "text",
|
|
460
471
|
text: [
|
|
@@ -534,12 +545,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
534
545
|
`No manifest or permission changes needed for 3D-only scenes.`,
|
|
535
546
|
].join("\n"),
|
|
536
547
|
},
|
|
537
|
-
],
|
|
548
|
+
]),
|
|
538
549
|
};
|
|
539
550
|
}
|
|
540
551
|
if (iosType === "ar") {
|
|
541
552
|
return {
|
|
542
|
-
content: [
|
|
553
|
+
content: withDisclaimer([
|
|
543
554
|
{
|
|
544
555
|
type: "text",
|
|
545
556
|
text: [
|
|
@@ -625,7 +636,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
625
636
|
`\`\`\``,
|
|
626
637
|
].join("\n"),
|
|
627
638
|
},
|
|
628
|
-
],
|
|
639
|
+
]),
|
|
629
640
|
};
|
|
630
641
|
}
|
|
631
642
|
return {
|
|
@@ -636,7 +647,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
636
647
|
// ── get_web_setup ────────────────────────────────────────────────────────
|
|
637
648
|
case "get_web_setup": {
|
|
638
649
|
return {
|
|
639
|
-
content: [
|
|
650
|
+
content: withDisclaimer([
|
|
640
651
|
{
|
|
641
652
|
type: "text",
|
|
642
653
|
text: [
|
|
@@ -721,7 +732,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
721
732
|
`- glTF/GLB format only (same as Android)`,
|
|
722
733
|
].join("\n"),
|
|
723
734
|
},
|
|
724
|
-
],
|
|
735
|
+
]),
|
|
725
736
|
};
|
|
726
737
|
}
|
|
727
738
|
// ── render_3d_preview ──────────────────────────────────────────────────
|
|
@@ -740,7 +751,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
740
751
|
}
|
|
741
752
|
const result = buildPreviewUrl({ modelUrl, codeSnippet, autoRotate, ar, title });
|
|
742
753
|
const text = formatPreviewResponse(result);
|
|
743
|
-
return { content: [{ type: "text", text }] };
|
|
754
|
+
return { content: withDisclaimer([{ type: "text", text }]) };
|
|
744
755
|
}
|
|
745
756
|
default:
|
|
746
757
|
return {
|
package/dist/samples.js
CHANGED
|
@@ -181,6 +181,50 @@ fun PointCloudScreen() {
|
|
|
181
181
|
) {
|
|
182
182
|
// Render point cloud model instances at detected positions
|
|
183
183
|
}
|
|
184
|
+
}`,
|
|
185
|
+
},
|
|
186
|
+
"ar-face-mesh": {
|
|
187
|
+
id: "ar-face-mesh",
|
|
188
|
+
title: "AR Face Mesh",
|
|
189
|
+
description: "AR face tracking with AugmentedFaceNode — applies a textured mesh overlay to detected faces using the front camera.",
|
|
190
|
+
tags: ["ar", "face-tracking", "model"],
|
|
191
|
+
dependency: "io.github.sceneview:arsceneview:3.3.0",
|
|
192
|
+
prompt: "Create an AR screen that uses the front camera to detect faces and overlay a 3D mesh on them. Use SceneView `io.github.sceneview:arsceneview:3.3.0`.",
|
|
193
|
+
code: `@Composable
|
|
194
|
+
fun ARFaceMeshScreen() {
|
|
195
|
+
val engine = rememberEngine()
|
|
196
|
+
val modelLoader = rememberModelLoader(engine)
|
|
197
|
+
val materialLoader = rememberMaterialLoader(engine)
|
|
198
|
+
var trackedFaces by remember { mutableStateOf(listOf<AugmentedFace>()) }
|
|
199
|
+
|
|
200
|
+
val faceMaterial = remember(materialLoader) {
|
|
201
|
+
materialLoader.createColorInstance(
|
|
202
|
+
color = Color(0.8f, 0.6f, 0.4f, 0.5f),
|
|
203
|
+
metallic = 0f,
|
|
204
|
+
roughness = 0.9f
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
ARScene(
|
|
209
|
+
modifier = Modifier.fillMaxSize(),
|
|
210
|
+
engine = engine,
|
|
211
|
+
modelLoader = modelLoader,
|
|
212
|
+
sessionFeatures = setOf(Session.Feature.FRONT_CAMERA),
|
|
213
|
+
sessionConfiguration = { _, config ->
|
|
214
|
+
config.augmentedFaceMode = Config.AugmentedFaceMode.MESH3D
|
|
215
|
+
},
|
|
216
|
+
onSessionUpdated = { session, _ ->
|
|
217
|
+
trackedFaces = session.getAllTrackables(AugmentedFace::class.java)
|
|
218
|
+
.filter { it.trackingState == TrackingState.TRACKING }
|
|
219
|
+
}
|
|
220
|
+
) {
|
|
221
|
+
trackedFaces.forEach { face ->
|
|
222
|
+
AugmentedFaceNode(
|
|
223
|
+
augmentedFace = face,
|
|
224
|
+
meshMaterialInstance = faceMaterial
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
184
228
|
}`,
|
|
185
229
|
},
|
|
186
230
|
"gltf-camera": {
|
|
@@ -242,6 +286,67 @@ fun CameraManipulatorScreen() {
|
|
|
242
286
|
ModelNode(modelInstance = instance, scaleToUnits = 1.0f)
|
|
243
287
|
}
|
|
244
288
|
}
|
|
289
|
+
}`,
|
|
290
|
+
},
|
|
291
|
+
"camera-animation": {
|
|
292
|
+
id: "camera-animation",
|
|
293
|
+
title: "Camera Animation",
|
|
294
|
+
description: "Animated camera flythrough around a 3D model — smooth orbit using LaunchedEffect and trigonometric interpolation.",
|
|
295
|
+
tags: ["3d", "camera", "animation", "model"],
|
|
296
|
+
dependency: "io.github.sceneview:sceneview:3.3.0",
|
|
297
|
+
prompt: "Create a 3D scene with a camera that automatically orbits around a model in a smooth circle. Include a play/pause button. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
|
|
298
|
+
code: `@Composable
|
|
299
|
+
fun CameraAnimationScreen() {
|
|
300
|
+
val engine = rememberEngine()
|
|
301
|
+
val modelLoader = rememberModelLoader(engine)
|
|
302
|
+
val environmentLoader = rememberEnvironmentLoader(engine)
|
|
303
|
+
var isOrbiting by remember { mutableStateOf(true) }
|
|
304
|
+
var angle by remember { mutableFloatStateOf(0f) }
|
|
305
|
+
|
|
306
|
+
val cameraNode = rememberCameraNode(engine) {
|
|
307
|
+
position = Position(x = 0f, y = 1.5f, z = 4f)
|
|
308
|
+
lookAt(Position(0f, 0f, 0f))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Animate camera orbit
|
|
312
|
+
LaunchedEffect(isOrbiting) {
|
|
313
|
+
while (isOrbiting) {
|
|
314
|
+
withFrameNanos { _ ->
|
|
315
|
+
angle += 0.5f
|
|
316
|
+
val radians = Math.toRadians(angle.toDouble())
|
|
317
|
+
cameraNode.position = Position(
|
|
318
|
+
x = (4f * sin(radians)).toFloat(),
|
|
319
|
+
y = 1.5f,
|
|
320
|
+
z = (4f * cos(radians)).toFloat()
|
|
321
|
+
)
|
|
322
|
+
cameraNode.lookAt(Position(0f, 0f, 0f))
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
Column {
|
|
328
|
+
Scene(
|
|
329
|
+
modifier = Modifier.weight(1f).fillMaxWidth(),
|
|
330
|
+
engine = engine,
|
|
331
|
+
modelLoader = modelLoader,
|
|
332
|
+
cameraNode = cameraNode,
|
|
333
|
+
environment = rememberEnvironment(environmentLoader) {
|
|
334
|
+
environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
|
|
335
|
+
?: createEnvironment(environmentLoader)
|
|
336
|
+
},
|
|
337
|
+
mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f }
|
|
338
|
+
) {
|
|
339
|
+
rememberModelInstance(modelLoader, "models/damaged_helmet.glb")?.let { instance ->
|
|
340
|
+
ModelNode(modelInstance = instance, scaleToUnits = 1.0f)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
Button(
|
|
344
|
+
onClick = { isOrbiting = !isOrbiting },
|
|
345
|
+
modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp)
|
|
346
|
+
) {
|
|
347
|
+
Text(if (isOrbiting) "Stop Orbit" else "Start Orbit")
|
|
348
|
+
}
|
|
349
|
+
}
|
|
245
350
|
}`,
|
|
246
351
|
},
|
|
247
352
|
"autopilot-demo": {
|
|
@@ -466,6 +571,465 @@ fun PostProcessingScreen() {
|
|
|
466
571
|
}
|
|
467
572
|
// Configure view.bloomOptions, view.vignetteOptions, etc.
|
|
468
573
|
// See samples/post-processing for full interactive controls
|
|
574
|
+
}`,
|
|
575
|
+
},
|
|
576
|
+
"video-texture": {
|
|
577
|
+
id: "video-texture",
|
|
578
|
+
title: "Video Texture",
|
|
579
|
+
description: "Video playback on a 3D plane using VideoNode with MediaPlayer — supports looping, chroma-key, and auto-sizing.",
|
|
580
|
+
tags: ["3d", "video", "model"],
|
|
581
|
+
dependency: "io.github.sceneview:sceneview:3.3.0",
|
|
582
|
+
prompt: "Create a 3D scene with a video playing on a floating 3D plane. Include play/pause controls and chroma-key support. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
|
|
583
|
+
code: `@Composable
|
|
584
|
+
fun VideoTextureScreen() {
|
|
585
|
+
val context = LocalContext.current
|
|
586
|
+
val engine = rememberEngine()
|
|
587
|
+
var isPlaying by remember { mutableStateOf(true) }
|
|
588
|
+
|
|
589
|
+
val player = remember {
|
|
590
|
+
MediaPlayer().apply {
|
|
591
|
+
setDataSource(context, Uri.parse("android.resource://\${context.packageName}/raw/video"))
|
|
592
|
+
isLooping = true
|
|
593
|
+
prepare()
|
|
594
|
+
start()
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
DisposableEffect(Unit) { onDispose { player.release() } }
|
|
598
|
+
|
|
599
|
+
Column {
|
|
600
|
+
Scene(
|
|
601
|
+
modifier = Modifier.weight(1f).fillMaxWidth(),
|
|
602
|
+
engine = engine
|
|
603
|
+
) {
|
|
604
|
+
VideoNode(
|
|
605
|
+
player = player,
|
|
606
|
+
// size = null auto-sizes from video aspect ratio (longer edge = 1 unit)
|
|
607
|
+
position = Position(z = -2f),
|
|
608
|
+
chromaKeyColor = null // set to android.graphics.Color.GREEN for green-screen
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
Row(
|
|
612
|
+
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
|
613
|
+
horizontalArrangement = Arrangement.Center,
|
|
614
|
+
verticalAlignment = Alignment.CenterVertically
|
|
615
|
+
) {
|
|
616
|
+
Button(onClick = {
|
|
617
|
+
if (isPlaying) player.pause() else player.start()
|
|
618
|
+
isPlaying = !isPlaying
|
|
619
|
+
}) {
|
|
620
|
+
Text(if (isPlaying) "Pause" else "Play")
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}`,
|
|
625
|
+
},
|
|
626
|
+
"multi-model-scene": {
|
|
627
|
+
id: "multi-model-scene",
|
|
628
|
+
title: "Multi-Model Scene",
|
|
629
|
+
description: "Scene with multiple 3D models loaded independently, positioned and scaled to create a complete environment.",
|
|
630
|
+
tags: ["3d", "model", "multi-model", "environment"],
|
|
631
|
+
dependency: "io.github.sceneview:sceneview:3.3.0",
|
|
632
|
+
prompt: "Create a 3D scene that loads multiple GLB models (a car, a building, and trees) and positions them to form a street scene. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
|
|
633
|
+
code: `@Composable
|
|
634
|
+
fun MultiModelScreen() {
|
|
635
|
+
val engine = rememberEngine()
|
|
636
|
+
val modelLoader = rememberModelLoader(engine)
|
|
637
|
+
val materialLoader = rememberMaterialLoader(engine)
|
|
638
|
+
val environmentLoader = rememberEnvironmentLoader(engine)
|
|
639
|
+
|
|
640
|
+
Scene(
|
|
641
|
+
modifier = Modifier.fillMaxSize(),
|
|
642
|
+
engine = engine,
|
|
643
|
+
modelLoader = modelLoader,
|
|
644
|
+
cameraManipulator = rememberCameraManipulator(
|
|
645
|
+
orbitHomePosition = Position(x = 0f, y = 3f, z = 8f),
|
|
646
|
+
targetPosition = Position(0f, 0f, 0f)
|
|
647
|
+
),
|
|
648
|
+
environment = rememberEnvironment(environmentLoader) {
|
|
649
|
+
environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
|
|
650
|
+
?: createEnvironment(environmentLoader)
|
|
651
|
+
},
|
|
652
|
+
mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f }
|
|
653
|
+
) {
|
|
654
|
+
// Ground plane
|
|
655
|
+
val groundMat = remember(materialLoader) {
|
|
656
|
+
materialLoader.createColorInstance(Color.DarkGray, roughness = 0.9f)
|
|
657
|
+
}
|
|
658
|
+
PlaneNode(size = Size(20f, 20f), materialInstance = groundMat)
|
|
659
|
+
|
|
660
|
+
// Car in the center
|
|
661
|
+
rememberModelInstance(modelLoader, "models/car.glb")?.let { car ->
|
|
662
|
+
ModelNode(
|
|
663
|
+
modelInstance = car,
|
|
664
|
+
scaleToUnits = 2.0f,
|
|
665
|
+
position = Position(x = 0f, y = 0f, z = 0f),
|
|
666
|
+
autoAnimate = true
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Building on the left
|
|
671
|
+
rememberModelInstance(modelLoader, "models/building.glb")?.let { building ->
|
|
672
|
+
ModelNode(
|
|
673
|
+
modelInstance = building,
|
|
674
|
+
scaleToUnits = 5.0f,
|
|
675
|
+
position = Position(x = -6f, y = 0f, z = -3f)
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Trees along the right side
|
|
680
|
+
for (i in 0..2) {
|
|
681
|
+
rememberModelInstance(modelLoader, "models/tree.glb")?.let { tree ->
|
|
682
|
+
ModelNode(
|
|
683
|
+
modelInstance = tree,
|
|
684
|
+
scaleToUnits = 3.0f,
|
|
685
|
+
position = Position(x = 5f, y = 0f, z = i * -3f)
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}`,
|
|
691
|
+
},
|
|
692
|
+
"gesture-interaction": {
|
|
693
|
+
id: "gesture-interaction",
|
|
694
|
+
title: "Gesture Interaction",
|
|
695
|
+
description: "Full gesture handling — tap to select, double-tap to scale, long-press for info, pinch-to-scale, drag-to-move on editable nodes.",
|
|
696
|
+
tags: ["3d", "gestures", "model"],
|
|
697
|
+
dependency: "io.github.sceneview:sceneview:3.3.0",
|
|
698
|
+
prompt: "Create a 3D scene with a model that responds to tap (select), double-tap (scale up), long-press (show info), and supports pinch-to-scale and drag-to-move. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
|
|
699
|
+
code: `@Composable
|
|
700
|
+
fun GestureInteractionScreen() {
|
|
701
|
+
val engine = rememberEngine()
|
|
702
|
+
val modelLoader = rememberModelLoader(engine)
|
|
703
|
+
val environmentLoader = rememberEnvironmentLoader(engine)
|
|
704
|
+
var selectedNode by remember { mutableStateOf<String?>(null) }
|
|
705
|
+
var infoText by remember { mutableStateOf("Tap a model to select it") }
|
|
706
|
+
|
|
707
|
+
Box(modifier = Modifier.fillMaxSize()) {
|
|
708
|
+
Scene(
|
|
709
|
+
modifier = Modifier.fillMaxSize(),
|
|
710
|
+
engine = engine,
|
|
711
|
+
modelLoader = modelLoader,
|
|
712
|
+
cameraManipulator = rememberCameraManipulator(),
|
|
713
|
+
environment = rememberEnvironment(environmentLoader) {
|
|
714
|
+
environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
|
|
715
|
+
?: createEnvironment(environmentLoader)
|
|
716
|
+
},
|
|
717
|
+
onGestureListener = rememberOnGestureListener(
|
|
718
|
+
onSingleTapConfirmed = { event, node ->
|
|
719
|
+
selectedNode = node?.name
|
|
720
|
+
infoText = if (node != null) "Selected: \${node.name}" else "Tap a model to select it"
|
|
721
|
+
},
|
|
722
|
+
onDoubleTap = { event, node ->
|
|
723
|
+
node?.let {
|
|
724
|
+
it.scale = if (it.scale.x > 1.5f) Scale(1f) else Scale(2f)
|
|
725
|
+
infoText = "Double-tap: toggled scale"
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
onLongPress = { event, node ->
|
|
729
|
+
node?.let {
|
|
730
|
+
infoText = "Position: \${it.worldPosition}, Scale: \${it.scale}"
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
)
|
|
734
|
+
) {
|
|
735
|
+
rememberModelInstance(modelLoader, "models/damaged_helmet.glb")?.let { instance ->
|
|
736
|
+
ModelNode(
|
|
737
|
+
modelInstance = instance,
|
|
738
|
+
scaleToUnits = 1.0f,
|
|
739
|
+
isEditable = true, // enables pinch-to-scale and drag-to-move
|
|
740
|
+
autoAnimate = true
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Info overlay
|
|
746
|
+
Text(
|
|
747
|
+
text = infoText,
|
|
748
|
+
modifier = Modifier.align(Alignment.TopCenter).padding(24.dp)
|
|
749
|
+
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), RoundedCornerShape(8.dp))
|
|
750
|
+
.padding(12.dp),
|
|
751
|
+
style = MaterialTheme.typography.bodyMedium
|
|
752
|
+
)
|
|
753
|
+
}
|
|
754
|
+
}`,
|
|
755
|
+
},
|
|
756
|
+
"environment-lighting": {
|
|
757
|
+
id: "environment-lighting",
|
|
758
|
+
title: "Environment & Lighting",
|
|
759
|
+
description: "Complete lighting setup — HDR environment (IBL + skybox), main directional light, point light, and spot light with LightNode.",
|
|
760
|
+
tags: ["3d", "environment", "lighting", "model"],
|
|
761
|
+
dependency: "io.github.sceneview:sceneview:3.3.0",
|
|
762
|
+
prompt: "Create a 3D scene with full HDR environment lighting (IBL + skybox), a directional sun light, a red point light, and a blue spot light. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
|
|
763
|
+
code: `@Composable
|
|
764
|
+
fun EnvironmentLightingScreen() {
|
|
765
|
+
val engine = rememberEngine()
|
|
766
|
+
val modelLoader = rememberModelLoader(engine)
|
|
767
|
+
val materialLoader = rememberMaterialLoader(engine)
|
|
768
|
+
val environmentLoader = rememberEnvironmentLoader(engine)
|
|
769
|
+
|
|
770
|
+
Scene(
|
|
771
|
+
modifier = Modifier.fillMaxSize(),
|
|
772
|
+
engine = engine,
|
|
773
|
+
modelLoader = modelLoader,
|
|
774
|
+
cameraManipulator = rememberCameraManipulator(
|
|
775
|
+
orbitHomePosition = Position(x = 0f, y = 2f, z = 5f),
|
|
776
|
+
targetPosition = Position(0f, 0f, 0f)
|
|
777
|
+
),
|
|
778
|
+
// HDR environment provides both IBL (indirect lighting) and skybox (background)
|
|
779
|
+
environment = rememberEnvironment(environmentLoader) {
|
|
780
|
+
environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
|
|
781
|
+
?: createEnvironment(environmentLoader)
|
|
782
|
+
},
|
|
783
|
+
// Main directional light (sun)
|
|
784
|
+
mainLightNode = rememberMainLightNode(engine) {
|
|
785
|
+
intensity = 100_000f
|
|
786
|
+
// castShadows is true by default for the main light
|
|
787
|
+
}
|
|
788
|
+
) {
|
|
789
|
+
// Floor to receive shadows
|
|
790
|
+
val floorMat = remember(materialLoader) {
|
|
791
|
+
materialLoader.createColorInstance(Color.LightGray, roughness = 0.8f)
|
|
792
|
+
}
|
|
793
|
+
PlaneNode(size = Size(10f, 10f), materialInstance = floorMat)
|
|
794
|
+
|
|
795
|
+
// Model
|
|
796
|
+
rememberModelInstance(modelLoader, "models/damaged_helmet.glb")?.let { instance ->
|
|
797
|
+
ModelNode(modelInstance = instance, scaleToUnits = 1.0f, position = Position(y = 0.5f))
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Red point light on the left
|
|
801
|
+
LightNode(
|
|
802
|
+
type = LightManager.Type.POINT,
|
|
803
|
+
apply = {
|
|
804
|
+
color(1.0f, 0.2f, 0.2f)
|
|
805
|
+
intensity(200_000f)
|
|
806
|
+
falloff(5.0f)
|
|
807
|
+
},
|
|
808
|
+
position = Position(x = -2f, y = 2f, z = 1f)
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
// Blue spot light on the right
|
|
812
|
+
LightNode(
|
|
813
|
+
type = LightManager.Type.SPOT,
|
|
814
|
+
apply = {
|
|
815
|
+
color(0.2f, 0.4f, 1.0f)
|
|
816
|
+
intensity(300_000f)
|
|
817
|
+
falloff(8.0f)
|
|
818
|
+
castShadows(true)
|
|
819
|
+
},
|
|
820
|
+
position = Position(x = 2f, y = 3f, z = 1f)
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
}`,
|
|
824
|
+
},
|
|
825
|
+
"procedural-geometry": {
|
|
826
|
+
id: "procedural-geometry",
|
|
827
|
+
title: "Procedural Geometry",
|
|
828
|
+
description: "Procedural shapes — CubeNode, SphereNode, CylinderNode, PlaneNode — with PBR materials (metallic, roughness, color).",
|
|
829
|
+
tags: ["3d", "geometry", "model"],
|
|
830
|
+
dependency: "io.github.sceneview:sceneview:3.3.0",
|
|
831
|
+
prompt: "Create a 3D scene showing procedural geometry shapes (cube, sphere, cylinder, plane) with different PBR materials. No model files needed. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
|
|
832
|
+
code: `@Composable
|
|
833
|
+
fun ProceduralGeometryScreen() {
|
|
834
|
+
val engine = rememberEngine()
|
|
835
|
+
val materialLoader = rememberMaterialLoader(engine)
|
|
836
|
+
val environmentLoader = rememberEnvironmentLoader(engine)
|
|
837
|
+
|
|
838
|
+
Scene(
|
|
839
|
+
modifier = Modifier.fillMaxSize(),
|
|
840
|
+
engine = engine,
|
|
841
|
+
cameraManipulator = rememberCameraManipulator(
|
|
842
|
+
orbitHomePosition = Position(x = 0f, y = 2f, z = 6f),
|
|
843
|
+
targetPosition = Position(0f, 0.5f, 0f)
|
|
844
|
+
),
|
|
845
|
+
environment = rememberEnvironment(environmentLoader) {
|
|
846
|
+
environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
|
|
847
|
+
?: createEnvironment(environmentLoader)
|
|
848
|
+
},
|
|
849
|
+
mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f }
|
|
850
|
+
) {
|
|
851
|
+
// Floor
|
|
852
|
+
val floorMat = remember(materialLoader) {
|
|
853
|
+
materialLoader.createColorInstance(Color.DarkGray, roughness = 0.9f)
|
|
854
|
+
}
|
|
855
|
+
PlaneNode(size = Size(8f, 8f), materialInstance = floorMat)
|
|
856
|
+
|
|
857
|
+
// Red matte cube
|
|
858
|
+
val redMat = remember(materialLoader) {
|
|
859
|
+
materialLoader.createColorInstance(Color.Red, metallic = 0f, roughness = 0.6f)
|
|
860
|
+
}
|
|
861
|
+
CubeNode(
|
|
862
|
+
size = Size(0.6f),
|
|
863
|
+
center = Position(0f, 0.3f, 0f),
|
|
864
|
+
materialInstance = redMat,
|
|
865
|
+
position = Position(x = -2f)
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
// Chrome sphere
|
|
869
|
+
val chromeMat = remember(materialLoader) {
|
|
870
|
+
materialLoader.createColorInstance(Color.Gray, metallic = 1f, roughness = 0.05f, reflectance = 0.9f)
|
|
871
|
+
}
|
|
872
|
+
SphereNode(
|
|
873
|
+
radius = 0.4f,
|
|
874
|
+
materialInstance = chromeMat,
|
|
875
|
+
position = Position(x = -0.7f, y = 0.4f)
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
// Green cylinder
|
|
879
|
+
val greenMat = remember(materialLoader) {
|
|
880
|
+
materialLoader.createColorInstance(Color.Green, metallic = 0.2f, roughness = 0.4f)
|
|
881
|
+
}
|
|
882
|
+
CylinderNode(
|
|
883
|
+
radius = 0.25f,
|
|
884
|
+
height = 0.8f,
|
|
885
|
+
materialInstance = greenMat,
|
|
886
|
+
position = Position(x = 0.7f, y = 0.4f)
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
// Gold sphere
|
|
890
|
+
val goldMat = remember(materialLoader) {
|
|
891
|
+
materialLoader.createColorInstance(
|
|
892
|
+
Color(1f, 0.84f, 0f),
|
|
893
|
+
metallic = 1f,
|
|
894
|
+
roughness = 0.3f
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
SphereNode(
|
|
898
|
+
radius = 0.35f,
|
|
899
|
+
materialInstance = goldMat,
|
|
900
|
+
position = Position(x = 2f, y = 0.35f)
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
}`,
|
|
904
|
+
},
|
|
905
|
+
"compose-ui-3d": {
|
|
906
|
+
id: "compose-ui-3d",
|
|
907
|
+
title: "Compose UI in 3D",
|
|
908
|
+
description: "Embed interactive Jetpack Compose UI (Cards, Buttons, Text) inside 3D space using ViewNode.",
|
|
909
|
+
tags: ["3d", "compose-ui", "text"],
|
|
910
|
+
dependency: "io.github.sceneview:sceneview:3.3.0",
|
|
911
|
+
prompt: "Create a 3D scene with interactive Compose UI elements (Card with text and a button) floating in 3D space using ViewNode. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
|
|
912
|
+
code: `@Composable
|
|
913
|
+
fun ComposeUI3DScreen() {
|
|
914
|
+
val engine = rememberEngine()
|
|
915
|
+
val modelLoader = rememberModelLoader(engine)
|
|
916
|
+
val windowManager = rememberViewNodeManager()
|
|
917
|
+
var clickCount by remember { mutableIntStateOf(0) }
|
|
918
|
+
|
|
919
|
+
Scene(
|
|
920
|
+
modifier = Modifier.fillMaxSize(),
|
|
921
|
+
engine = engine,
|
|
922
|
+
modelLoader = modelLoader,
|
|
923
|
+
cameraManipulator = rememberCameraManipulator(),
|
|
924
|
+
viewNodeWindowManager = windowManager
|
|
925
|
+
) {
|
|
926
|
+
// 3D model behind the UI
|
|
927
|
+
rememberModelInstance(modelLoader, "models/damaged_helmet.glb")?.let { instance ->
|
|
928
|
+
ModelNode(modelInstance = instance, scaleToUnits = 1.0f, position = Position(z = -1f))
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Floating Compose Card in 3D space
|
|
932
|
+
ViewNode(
|
|
933
|
+
windowManager = windowManager,
|
|
934
|
+
position = Position(x = 0f, y = 1.2f, z = 0.5f)
|
|
935
|
+
) {
|
|
936
|
+
Card(
|
|
937
|
+
modifier = Modifier.width(200.dp).padding(8.dp),
|
|
938
|
+
colors = CardDefaults.cardColors(
|
|
939
|
+
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
|
|
940
|
+
)
|
|
941
|
+
) {
|
|
942
|
+
Column(modifier = Modifier.padding(16.dp)) {
|
|
943
|
+
Text("Hello 3D World!", style = MaterialTheme.typography.titleMedium)
|
|
944
|
+
Text("Clicks: \$clickCount", style = MaterialTheme.typography.bodySmall)
|
|
945
|
+
Spacer(Modifier.height(8.dp))
|
|
946
|
+
Button(onClick = { clickCount++ }) {
|
|
947
|
+
Text("Click Me")
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}`,
|
|
954
|
+
},
|
|
955
|
+
"node-hierarchy": {
|
|
956
|
+
id: "node-hierarchy",
|
|
957
|
+
title: "Node Hierarchy",
|
|
958
|
+
description: "Parent-child node relationships — a spinning solar system with planet groups orbiting a central sun.",
|
|
959
|
+
tags: ["3d", "hierarchy", "geometry", "animation"],
|
|
960
|
+
dependency: "io.github.sceneview:sceneview:3.3.0",
|
|
961
|
+
prompt: "Create a 3D solar system where planets orbit a sun using parent-child node hierarchies. Each planet group rotates independently. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
|
|
962
|
+
code: `@Composable
|
|
963
|
+
fun NodeHierarchyScreen() {
|
|
964
|
+
val engine = rememberEngine()
|
|
965
|
+
val materialLoader = rememberMaterialLoader(engine)
|
|
966
|
+
val environmentLoader = rememberEnvironmentLoader(engine)
|
|
967
|
+
var earthAngle by remember { mutableFloatStateOf(0f) }
|
|
968
|
+
var marsAngle by remember { mutableFloatStateOf(0f) }
|
|
969
|
+
|
|
970
|
+
// Animate planet orbits
|
|
971
|
+
LaunchedEffect(Unit) {
|
|
972
|
+
while (true) {
|
|
973
|
+
withFrameNanos { _ ->
|
|
974
|
+
earthAngle += 0.3f
|
|
975
|
+
marsAngle += 0.18f
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
Scene(
|
|
981
|
+
modifier = Modifier.fillMaxSize(),
|
|
982
|
+
engine = engine,
|
|
983
|
+
cameraManipulator = rememberCameraManipulator(
|
|
984
|
+
orbitHomePosition = Position(x = 0f, y = 4f, z = 8f),
|
|
985
|
+
targetPosition = Position(0f, 0f, 0f)
|
|
986
|
+
),
|
|
987
|
+
environment = rememberEnvironment(environmentLoader) {
|
|
988
|
+
environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
|
|
989
|
+
?: createEnvironment(environmentLoader)
|
|
990
|
+
}
|
|
991
|
+
) {
|
|
992
|
+
// Sun (center)
|
|
993
|
+
val sunMat = remember(materialLoader) {
|
|
994
|
+
materialLoader.createColorInstance(Color.Yellow, metallic = 0f, roughness = 1f)
|
|
995
|
+
}
|
|
996
|
+
SphereNode(radius = 0.5f, materialInstance = sunMat)
|
|
997
|
+
|
|
998
|
+
// Earth orbit group — parent node rotates, child offset creates orbit
|
|
999
|
+
Node(rotation = Rotation(y = earthAngle)) {
|
|
1000
|
+
// Earth sphere
|
|
1001
|
+
val earthMat = remember(materialLoader) {
|
|
1002
|
+
materialLoader.createColorInstance(Color.Blue, metallic = 0f, roughness = 0.7f)
|
|
1003
|
+
}
|
|
1004
|
+
SphereNode(radius = 0.2f, materialInstance = earthMat, position = Position(x = 2.5f))
|
|
1005
|
+
|
|
1006
|
+
// Moon orbits Earth (nested hierarchy)
|
|
1007
|
+
Node(position = Position(x = 2.5f), rotation = Rotation(y = earthAngle * 3f)) {
|
|
1008
|
+
val moonMat = remember(materialLoader) {
|
|
1009
|
+
materialLoader.createColorInstance(Color.LightGray, metallic = 0f, roughness = 0.9f)
|
|
1010
|
+
}
|
|
1011
|
+
SphereNode(radius = 0.06f, materialInstance = moonMat, position = Position(x = 0.4f))
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Mars orbit group
|
|
1016
|
+
Node(rotation = Rotation(y = marsAngle)) {
|
|
1017
|
+
val marsMat = remember(materialLoader) {
|
|
1018
|
+
materialLoader.createColorInstance(Color.Red, metallic = 0f, roughness = 0.8f)
|
|
1019
|
+
}
|
|
1020
|
+
SphereNode(radius = 0.15f, materialInstance = marsMat, position = Position(x = 4f))
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Sun light
|
|
1024
|
+
LightNode(
|
|
1025
|
+
type = LightManager.Type.POINT,
|
|
1026
|
+
apply = {
|
|
1027
|
+
color(1.0f, 0.95f, 0.8f)
|
|
1028
|
+
intensity(500_000f)
|
|
1029
|
+
falloff(15.0f)
|
|
1030
|
+
}
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
469
1033
|
}`,
|
|
470
1034
|
},
|
|
471
1035
|
// ─── iOS Samples ────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sceneview-mcp",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.8",
|
|
4
4
|
"mcpName": "io.github.sceneview/mcp",
|
|
5
5
|
"description": "MCP server for SceneView — cross-platform 3D & AR SDK for Android and iOS. Give Claude the full SceneView SDK so it writes correct, compilable code.",
|
|
6
6
|
"keywords": [
|