sceneview-mcp 3.6.2 → 3.6.4

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,570 @@
1
+ /**
2
+ * generate-physics.ts
3
+ *
4
+ * Generates compilable physics simulation code for SceneView.
5
+ */
6
+ export const PHYSICS_TYPES = [
7
+ "gravity-drop",
8
+ "collision-detection",
9
+ "spring-physics",
10
+ "projectile",
11
+ "ragdoll-basic",
12
+ "rigid-body",
13
+ ];
14
+ const PHYSICS_TEMPLATES = {
15
+ "gravity-drop": {
16
+ title: "Gravity Drop Simulation",
17
+ description: "Simulates objects falling under gravity using onFrame callback with velocity accumulation.",
18
+ android: `@Composable
19
+ fun GravityDropScene() {
20
+ val engine = rememberEngine()
21
+ val modelLoader = rememberModelLoader(engine)
22
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
23
+
24
+ var posY by remember { mutableFloatStateOf(3.0f) }
25
+ var velocityY by remember { mutableFloatStateOf(0f) }
26
+ val gravity = -9.81f
27
+ val groundY = 0f
28
+ val bounceFactor = 0.6f
29
+
30
+ SceneView(
31
+ modifier = Modifier.fillMaxSize(),
32
+ engine = engine,
33
+ modelLoader = modelLoader,
34
+ onFrame = { frameTimeNanos ->
35
+ val dt = 1f / 60f // Fixed timestep for stability
36
+ velocityY += gravity * dt
37
+ posY += velocityY * dt
38
+
39
+ // Bounce off ground
40
+ if (posY <= groundY) {
41
+ posY = groundY
42
+ velocityY = -velocityY * bounceFactor
43
+ // Stop bouncing when velocity is negligible
44
+ if (kotlin.math.abs(velocityY) < 0.1f) {
45
+ velocityY = 0f
46
+ posY = groundY
47
+ }
48
+ }
49
+ }
50
+ ) {
51
+ modelInstance?.let { instance ->
52
+ ModelNode(
53
+ modelInstance = instance,
54
+ scaleToUnits = 0.5f,
55
+ centerOrigin = Position(0f, 0f, 0f),
56
+ position = Position(0f, posY, 0f)
57
+ )
58
+ }
59
+
60
+ // Ground plane
61
+ CubeNode(
62
+ engine = engine,
63
+ size = Size(10f, 0.01f, 10f),
64
+ center = Position(0f, 0f, 0f),
65
+ materialInstance = rememberMaterialInstance(engine) {
66
+ baseColor(0.3f, 0.3f, 0.3f)
67
+ roughness(0.8f)
68
+ }
69
+ )
70
+
71
+ LightNode(
72
+ engine = engine,
73
+ type = LightManager.Type.DIRECTIONAL,
74
+ apply = {
75
+ intensity(100_000f)
76
+ castShadows(true)
77
+ direction(0f, -1f, -0.5f)
78
+ }
79
+ )
80
+ }
81
+ }`,
82
+ ios: `import SwiftUI
83
+ import SceneViewSwift
84
+ import RealityKit
85
+
86
+ struct GravityDropScene: View {
87
+ @State private var posY: Float = 3.0
88
+ @State private var velocityY: Float = 0
89
+
90
+ let gravity: Float = -9.81
91
+ let bounceFactor: Float = 0.6
92
+ let timer = Timer.publish(every: 1.0/60.0, on: .main, in: .common).autoconnect()
93
+
94
+ var body: some View {
95
+ SceneView { root in
96
+ // Add ground plane
97
+ let ground = ModelEntity(
98
+ mesh: .generatePlane(width: 10, depth: 10),
99
+ materials: [SimpleMaterial(color: .gray, isMetallic: false)]
100
+ )
101
+ root.addChild(ground)
102
+ }
103
+ .onReceive(timer) { _ in
104
+ let dt: Float = 1.0 / 60.0
105
+ velocityY += gravity * dt
106
+ posY += velocityY * dt
107
+ if posY <= 0 {
108
+ posY = 0
109
+ velocityY = -velocityY * bounceFactor
110
+ if abs(velocityY) < 0.1 {
111
+ velocityY = 0
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }`,
117
+ },
118
+ "collision-detection": {
119
+ title: "Collision Detection",
120
+ description: "Detects collisions between objects using bounding box intersection from sceneview-core KMP.",
121
+ android: `@Composable
122
+ fun CollisionDetectionScene() {
123
+ val engine = rememberEngine()
124
+ val modelLoader = rememberModelLoader(engine)
125
+ val modelA = rememberModelInstance(modelLoader, "models/sphere.glb")
126
+ val modelB = rememberModelInstance(modelLoader, "models/cube.glb")
127
+
128
+ var posAx by remember { mutableFloatStateOf(-2f) }
129
+ var isColliding by remember { mutableStateOf(false) }
130
+ val speed = 1.5f
131
+
132
+ SceneView(
133
+ modifier = Modifier.fillMaxSize(),
134
+ engine = engine,
135
+ modelLoader = modelLoader,
136
+ onFrame = { _ ->
137
+ // Move sphere toward cube
138
+ if (posAx < 2f && !isColliding) {
139
+ posAx += speed * (1f / 60f)
140
+ }
141
+
142
+ // Simple AABB collision check
143
+ // Object A center at (posAx, 0, 0) with radius ~0.5
144
+ // Object B center at (1, 0, 0) with radius ~0.5
145
+ val distance = kotlin.math.abs(posAx - 1f)
146
+ isColliding = distance < 1.0f // Combined radii
147
+ }
148
+ ) {
149
+ // Moving sphere
150
+ modelA?.let { instance ->
151
+ ModelNode(
152
+ modelInstance = instance,
153
+ scaleToUnits = 0.5f,
154
+ position = Position(posAx, 0.5f, 0f)
155
+ )
156
+ }
157
+
158
+ // Static cube
159
+ modelB?.let { instance ->
160
+ ModelNode(
161
+ modelInstance = instance,
162
+ scaleToUnits = 0.5f,
163
+ position = Position(1f, 0.5f, 0f)
164
+ )
165
+ }
166
+
167
+ LightNode(
168
+ engine = engine,
169
+ type = LightManager.Type.DIRECTIONAL,
170
+ apply = {
171
+ intensity(100_000f)
172
+ direction(0f, -1f, -0.5f)
173
+ }
174
+ )
175
+ }
176
+
177
+ // Collision feedback
178
+ if (isColliding) {
179
+ Text(
180
+ text = "COLLISION!",
181
+ modifier = Modifier.padding(16.dp),
182
+ color = Color.Red,
183
+ style = MaterialTheme.typography.headlineMedium
184
+ )
185
+ }
186
+ }`,
187
+ },
188
+ "spring-physics": {
189
+ title: "Spring Physics (KMP Core)",
190
+ description: "Uses Spring animation from sceneview-core KMP for physically-based bounce and wobble.",
191
+ android: `@Composable
192
+ fun SpringPhysicsScene() {
193
+ val engine = rememberEngine()
194
+ val modelLoader = rememberModelLoader(engine)
195
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
196
+
197
+ // Spring parameters
198
+ var targetY by remember { mutableFloatStateOf(1.0f) }
199
+ var currentY by remember { mutableFloatStateOf(0f) }
200
+ var velocityY by remember { mutableFloatStateOf(0f) }
201
+ val stiffness = 200f // Higher = snappier
202
+ val damping = 10f // Higher = less bounce
203
+ val mass = 1f
204
+
205
+ // Toggle target on tap
206
+ var raised by remember { mutableStateOf(false) }
207
+
208
+ SceneView(
209
+ modifier = Modifier
210
+ .fillMaxSize()
211
+ .clickable {
212
+ raised = !raised
213
+ targetY = if (raised) 2.0f else 1.0f
214
+ },
215
+ engine = engine,
216
+ modelLoader = modelLoader,
217
+ onFrame = { _ ->
218
+ val dt = 1f / 60f
219
+ // Spring force: F = -k * (x - target) - d * v
220
+ val springForce = -stiffness * (currentY - targetY) - damping * velocityY
221
+ val acceleration = springForce / mass
222
+ velocityY += acceleration * dt
223
+ currentY += velocityY * dt
224
+ }
225
+ ) {
226
+ modelInstance?.let { instance ->
227
+ ModelNode(
228
+ modelInstance = instance,
229
+ scaleToUnits = 0.5f,
230
+ centerOrigin = Position(0f, 0f, 0f),
231
+ position = Position(0f, currentY, 0f)
232
+ )
233
+ }
234
+
235
+ // Ground
236
+ CubeNode(
237
+ engine = engine,
238
+ size = Size(5f, 0.01f, 5f),
239
+ center = Position(0f, 0f, 0f),
240
+ materialInstance = rememberMaterialInstance(engine) {
241
+ baseColor(0.4f, 0.4f, 0.4f)
242
+ }
243
+ )
244
+
245
+ LightNode(
246
+ engine = engine,
247
+ type = LightManager.Type.DIRECTIONAL,
248
+ apply = {
249
+ intensity(100_000f)
250
+ castShadows(true)
251
+ direction(0f, -1f, -0.5f)
252
+ }
253
+ )
254
+ }
255
+ }`,
256
+ },
257
+ "projectile": {
258
+ title: "Projectile Motion",
259
+ description: "Launches a projectile with initial velocity, gravity, and trajectory tracking.",
260
+ android: `@Composable
261
+ fun ProjectileScene() {
262
+ val engine = rememberEngine()
263
+ val modelLoader = rememberModelLoader(engine)
264
+ val projectileModel = rememberModelInstance(modelLoader, "models/sphere.glb")
265
+
266
+ data class Projectile(var x: Float, var y: Float, var vx: Float, var vy: Float, var active: Boolean = true)
267
+ val projectiles = remember { mutableStateListOf<Projectile>() }
268
+ val gravity = -9.81f
269
+
270
+ // Launch on tap
271
+ SceneView(
272
+ modifier = Modifier
273
+ .fillMaxSize()
274
+ .clickable {
275
+ projectiles.add(
276
+ Projectile(
277
+ x = 0f, y = 0.5f,
278
+ vx = 3f + (Math.random() * 2f).toFloat(),
279
+ vy = 5f + (Math.random() * 3f).toFloat()
280
+ )
281
+ )
282
+ },
283
+ engine = engine,
284
+ modelLoader = modelLoader,
285
+ onFrame = { _ ->
286
+ val dt = 1f / 60f
287
+ projectiles.forEachIndexed { index, p ->
288
+ if (p.active) {
289
+ p.vy += gravity * dt
290
+ p.x += p.vx * dt
291
+ p.y += p.vy * dt
292
+ if (p.y < -1f) {
293
+ p.active = false
294
+ }
295
+ }
296
+ }
297
+ }
298
+ ) {
299
+ // Render active projectiles
300
+ projectiles.filter { it.active }.forEach { p ->
301
+ projectileModel?.let { instance ->
302
+ ModelNode(
303
+ modelInstance = instance,
304
+ scaleToUnits = 0.1f,
305
+ position = Position(p.x, p.y, 0f)
306
+ )
307
+ }
308
+ }
309
+
310
+ // Ground
311
+ CubeNode(
312
+ engine = engine,
313
+ size = Size(20f, 0.01f, 5f),
314
+ center = Position(5f, 0f, 0f),
315
+ materialInstance = rememberMaterialInstance(engine) {
316
+ baseColor(0.3f, 0.6f, 0.3f)
317
+ roughness(0.9f)
318
+ }
319
+ )
320
+
321
+ LightNode(
322
+ engine = engine,
323
+ type = LightManager.Type.DIRECTIONAL,
324
+ apply = {
325
+ intensity(100_000f)
326
+ castShadows(true)
327
+ direction(0f, -1f, -0.5f)
328
+ }
329
+ )
330
+ }
331
+ }`,
332
+ },
333
+ "ragdoll-basic": {
334
+ title: "Basic Ragdoll Joints",
335
+ description: "Simulates connected rigid bodies with joint constraints for ragdoll-like behavior.",
336
+ android: `@Composable
337
+ fun RagdollScene() {
338
+ val engine = rememberEngine()
339
+
340
+ data class RagdollPart(
341
+ var x: Float, var y: Float,
342
+ var vx: Float = 0f, var vy: Float = 0f,
343
+ val parentIndex: Int = -1,
344
+ val jointLength: Float = 0.3f
345
+ )
346
+
347
+ // Head, torso, left arm, right arm, left leg, right leg
348
+ val parts = remember {
349
+ mutableStateListOf(
350
+ RagdollPart(0f, 2.0f), // 0: Head
351
+ RagdollPart(0f, 1.5f), // 1: Torso (connected to head)
352
+ RagdollPart(-0.3f, 1.5f, parentIndex = 1, jointLength = 0.4f), // 2: Left arm
353
+ RagdollPart(0.3f, 1.5f, parentIndex = 1, jointLength = 0.4f), // 3: Right arm
354
+ RagdollPart(-0.1f, 1.0f, parentIndex = 1, jointLength = 0.5f), // 4: Left leg
355
+ RagdollPart(0.1f, 1.0f, parentIndex = 1, jointLength = 0.5f), // 5: Right leg
356
+ )
357
+ }
358
+
359
+ val gravity = -9.81f
360
+ val damping = 0.98f
361
+
362
+ SceneView(
363
+ modifier = Modifier.fillMaxSize(),
364
+ engine = engine,
365
+ onFrame = { _ ->
366
+ val dt = 1f / 60f
367
+ parts.forEachIndexed { index, part ->
368
+ // Apply gravity
369
+ part.vy += gravity * dt
370
+ part.x += part.vx * dt
371
+ part.y += part.vy * dt
372
+ part.vx *= damping
373
+ part.vy *= damping
374
+
375
+ // Ground constraint
376
+ if (part.y < 0f) {
377
+ part.y = 0f
378
+ part.vy = -part.vy * 0.3f
379
+ }
380
+
381
+ // Joint constraint
382
+ if (part.parentIndex >= 0) {
383
+ val parent = parts[part.parentIndex]
384
+ val dx = part.x - parent.x
385
+ val dy = part.y - parent.y
386
+ val dist = kotlin.math.sqrt(dx * dx + dy * dy)
387
+ if (dist > part.jointLength && dist > 0.001f) {
388
+ val correction = (dist - part.jointLength) / dist * 0.5f
389
+ part.x -= dx * correction
390
+ part.y -= dy * correction
391
+ parts[part.parentIndex] = parent.copy(
392
+ x = parent.x + dx * correction,
393
+ y = parent.y + dy * correction
394
+ )
395
+ }
396
+ }
397
+ }
398
+ }
399
+ ) {
400
+ // Render each part as a sphere
401
+ parts.forEach { part ->
402
+ SphereNode(
403
+ engine = engine,
404
+ radius = 0.08f,
405
+ center = Position(part.x, part.y, 0f),
406
+ materialInstance = rememberMaterialInstance(engine) {
407
+ baseColor(0.9f, 0.6f, 0.4f)
408
+ roughness(0.7f)
409
+ }
410
+ )
411
+ }
412
+
413
+ LightNode(
414
+ engine = engine,
415
+ type = LightManager.Type.DIRECTIONAL,
416
+ apply = {
417
+ intensity(100_000f)
418
+ direction(0f, -1f, -0.5f)
419
+ }
420
+ )
421
+ }
422
+ }`,
423
+ },
424
+ "rigid-body": {
425
+ title: "Rigid Body Simulation",
426
+ description: "Multiple rigid bodies with gravity, ground collision, and inter-body collision response.",
427
+ android: `@Composable
428
+ fun RigidBodyScene() {
429
+ val engine = rememberEngine()
430
+
431
+ data class RigidBody(
432
+ var x: Float, var y: Float, var z: Float,
433
+ var vx: Float = 0f, var vy: Float = 0f, var vz: Float = 0f,
434
+ val radius: Float = 0.25f,
435
+ val mass: Float = 1f,
436
+ val restitution: Float = 0.5f
437
+ )
438
+
439
+ val bodies = remember {
440
+ mutableStateListOf(
441
+ RigidBody(0f, 3f, 0f),
442
+ RigidBody(0.5f, 4f, 0.2f),
443
+ RigidBody(-0.3f, 5f, -0.1f),
444
+ RigidBody(0.1f, 6f, 0.3f),
445
+ )
446
+ }
447
+
448
+ val gravity = -9.81f
449
+
450
+ SceneView(
451
+ modifier = Modifier.fillMaxSize(),
452
+ engine = engine,
453
+ onFrame = { _ ->
454
+ val dt = 1f / 60f
455
+ bodies.forEachIndexed { i, body ->
456
+ // Gravity
457
+ body.vy += gravity * dt
458
+ body.x += body.vx * dt
459
+ body.y += body.vy * dt
460
+ body.z += body.vz * dt
461
+
462
+ // Ground bounce
463
+ if (body.y < body.radius) {
464
+ body.y = body.radius
465
+ body.vy = -body.vy * body.restitution
466
+ }
467
+
468
+ // Inter-body collision
469
+ for (j in (i + 1) until bodies.size) {
470
+ val other = bodies[j]
471
+ val dx = body.x - other.x
472
+ val dy = body.y - other.y
473
+ val dz = body.z - other.z
474
+ val dist = kotlin.math.sqrt(dx * dx + dy * dy + dz * dz)
475
+ val minDist = body.radius + other.radius
476
+ if (dist < minDist && dist > 0.001f) {
477
+ // Push apart
478
+ val nx = dx / dist
479
+ val ny = dy / dist
480
+ val nz = dz / dist
481
+ val overlap = (minDist - dist) * 0.5f
482
+ body.x += nx * overlap
483
+ body.y += ny * overlap
484
+ body.z += nz * overlap
485
+ other.x -= nx * overlap
486
+ other.y -= ny * overlap
487
+ other.z -= nz * overlap
488
+
489
+ // Elastic collision response
490
+ val relVx = body.vx - other.vx
491
+ val relVy = body.vy - other.vy
492
+ val relVz = body.vz - other.vz
493
+ val relVn = relVx * nx + relVy * ny + relVz * nz
494
+ if (relVn < 0) {
495
+ val impulse = relVn / (1f / body.mass + 1f / other.mass)
496
+ body.vx -= impulse / body.mass * nx
497
+ body.vy -= impulse / body.mass * ny
498
+ body.vz -= impulse / body.mass * nz
499
+ other.vx += impulse / other.mass * nx
500
+ other.vy += impulse / other.mass * ny
501
+ other.vz += impulse / other.mass * nz
502
+ }
503
+ }
504
+ }
505
+ }
506
+ }
507
+ ) {
508
+ bodies.forEach { body ->
509
+ SphereNode(
510
+ engine = engine,
511
+ radius = body.radius,
512
+ center = Position(body.x, body.y, body.z),
513
+ materialInstance = rememberMaterialInstance(engine) {
514
+ baseColor(0.2f, 0.5f, 0.9f)
515
+ metallic(0.3f)
516
+ roughness(0.4f)
517
+ }
518
+ )
519
+ }
520
+
521
+ // Ground
522
+ CubeNode(
523
+ engine = engine,
524
+ size = Size(10f, 0.02f, 10f),
525
+ center = Position(0f, 0f, 0f),
526
+ materialInstance = rememberMaterialInstance(engine) {
527
+ baseColor(0.4f, 0.4f, 0.4f)
528
+ roughness(0.9f)
529
+ }
530
+ )
531
+
532
+ LightNode(
533
+ engine = engine,
534
+ type = LightManager.Type.DIRECTIONAL,
535
+ apply = {
536
+ intensity(100_000f)
537
+ castShadows(true)
538
+ direction(0f, -1f, -0.5f)
539
+ }
540
+ )
541
+ }
542
+ }`,
543
+ },
544
+ };
545
+ export function generatePhysicsCode(physicsType, platform = "android") {
546
+ const template = PHYSICS_TEMPLATES[physicsType];
547
+ if (!template)
548
+ return null;
549
+ const code = platform === "ios" && template.ios ? template.ios : template.android;
550
+ return { code, title: template.title, description: template.description };
551
+ }
552
+ export function formatPhysicsCode(result, platform) {
553
+ const lang = platform === "ios" ? "swift" : "kotlin";
554
+ const platLabel = platform === "ios" ? "iOS (SwiftUI + RealityKit)" : "Android (Jetpack Compose)";
555
+ return [
556
+ `## ${result.title}`,
557
+ ``,
558
+ `**Platform:** ${platLabel}`,
559
+ ``,
560
+ result.description,
561
+ ``,
562
+ `\`\`\`${lang}`,
563
+ result.code,
564
+ `\`\`\``,
565
+ ``,
566
+ `### Available Physics Types`,
567
+ ``,
568
+ ...PHYSICS_TYPES.map((t) => `- \`${t}\``),
569
+ ].join("\n");
570
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * generate-scene.ts
3
3
  *
4
- * Generates a complete Scene{} or ARScene{} composable from a text description.
4
+ * Generates a complete SceneView{} or ARSceneView{} composable from a text description.
5
5
  * Maps common objects/concepts to SceneView node types and builds compilable code.
6
6
  *
7
- * All generated code targets SceneView v3.6.0 API and is verified against llms.txt.
7
+ * All generated code targets SceneView v3.6.2 API and is verified against llms.txt.
8
8
  */
9
9
  const OBJECT_MAPPINGS = [
10
10
  // Furniture
@@ -245,7 +245,7 @@ export function generateScene(description) {
245
245
  }
246
246
  // Build the code
247
247
  const isAR = parsed.isAR;
248
- dependencies.push(isAR ? "io.github.sceneview:arsceneview:3.6.0" : "io.github.sceneview:sceneview:3.6.0");
248
+ dependencies.push(isAR ? "io.github.sceneview:arsceneview:3.6.2" : "io.github.sceneview:sceneview:3.6.2");
249
249
  // Build model instance declarations
250
250
  const modelElements = elements.filter((e) => e.type === "model");
251
251
  const uniqueModels = new Map();
@@ -296,7 +296,7 @@ export function generateScene(description) {
296
296
  lines.push(" }");
297
297
  lines.push("");
298
298
  }
299
- // Scene or ARScene
299
+ // SceneView or ARSceneView
300
300
  if (isAR) {
301
301
  lines.push(" var anchor by remember { mutableStateOf<Anchor?>(null) }");
302
302
  lines.push("");