sceneview-mcp 3.6.2 → 3.6.5

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,532 @@
1
+ /**
2
+ * generate-gesture.ts
3
+ *
4
+ * Generates compilable gesture/interaction code for SceneView.
5
+ */
6
+ export const GESTURE_TYPES = [
7
+ "tap-to-select",
8
+ "drag-to-rotate",
9
+ "pinch-to-scale",
10
+ "tap-to-place-ar",
11
+ "editable-model",
12
+ "multi-select",
13
+ "surface-cursor",
14
+ "custom-touch",
15
+ ];
16
+ const GESTURE_TEMPLATES = {
17
+ "tap-to-select": {
18
+ title: "Tap to Select Node",
19
+ description: "Tap on a 3D model to select it and show a visual highlight.",
20
+ android: `@Composable
21
+ fun TapToSelectScene() {
22
+ val engine = rememberEngine()
23
+ val modelLoader = rememberModelLoader(engine)
24
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
25
+ var selectedNode by remember { mutableStateOf<String?>(null) }
26
+
27
+ SceneView(
28
+ modifier = Modifier.fillMaxSize(),
29
+ engine = engine,
30
+ modelLoader = modelLoader,
31
+ onTouchEvent = { event, hitResult ->
32
+ if (event.action == MotionEvent.ACTION_UP) {
33
+ selectedNode = hitResult?.node?.name
34
+ }
35
+ true
36
+ }
37
+ ) {
38
+ modelInstance?.let { instance ->
39
+ ModelNode(
40
+ modelInstance = instance,
41
+ scaleToUnits = 1.0f,
42
+ centerOrigin = Position(0f, 0f, 0f)
43
+ )
44
+ }
45
+
46
+ LightNode(
47
+ engine = engine,
48
+ type = LightManager.Type.DIRECTIONAL,
49
+ apply = {
50
+ intensity(100_000f)
51
+ castShadows(true)
52
+ direction(0f, -1f, -0.5f)
53
+ }
54
+ )
55
+ }
56
+
57
+ // Show selection UI
58
+ selectedNode?.let { name ->
59
+ Text(
60
+ text = "Selected: $name",
61
+ modifier = Modifier.padding(16.dp),
62
+ color = MaterialTheme.colorScheme.primary
63
+ )
64
+ }
65
+ }`,
66
+ ios: `import SwiftUI
67
+ import SceneViewSwift
68
+
69
+ struct TapToSelectScene: View {
70
+ @State private var selectedEntity: Entity?
71
+
72
+ var body: some View {
73
+ SceneView { root in
74
+ // Content loaded in .task
75
+ }
76
+ .onTapGesture { location in
77
+ // RealityKit handles entity selection via tap
78
+ }
79
+ .overlay(alignment: .bottom) {
80
+ if let entity = selectedEntity {
81
+ Text("Selected: \\(entity.name)")
82
+ .padding()
83
+ .background(.ultraThinMaterial)
84
+ .cornerRadius(8)
85
+ .padding()
86
+ }
87
+ }
88
+ }
89
+ }`,
90
+ },
91
+ "drag-to-rotate": {
92
+ title: "Drag to Rotate Model",
93
+ description: "Drag on the model to rotate it around its Y axis with sensitivity control.",
94
+ android: `@Composable
95
+ fun DragToRotateScene() {
96
+ val engine = rememberEngine()
97
+ val modelLoader = rememberModelLoader(engine)
98
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
99
+ var rotationY by remember { mutableFloatStateOf(0f) }
100
+ var lastX by remember { mutableFloatStateOf(0f) }
101
+ val sensitivity = 0.5f
102
+
103
+ SceneView(
104
+ modifier = Modifier.fillMaxSize(),
105
+ engine = engine,
106
+ modelLoader = modelLoader,
107
+ onTouchEvent = { event, _ ->
108
+ when (event.action) {
109
+ MotionEvent.ACTION_DOWN -> {
110
+ lastX = event.x
111
+ true
112
+ }
113
+ MotionEvent.ACTION_MOVE -> {
114
+ val dx = event.x - lastX
115
+ rotationY += dx * sensitivity
116
+ lastX = event.x
117
+ true
118
+ }
119
+ else -> false
120
+ }
121
+ }
122
+ ) {
123
+ modelInstance?.let { instance ->
124
+ ModelNode(
125
+ modelInstance = instance,
126
+ scaleToUnits = 1.0f,
127
+ centerOrigin = Position(0f, 0f, 0f),
128
+ rotation = Rotation(y = rotationY)
129
+ )
130
+ }
131
+
132
+ LightNode(
133
+ engine = engine,
134
+ type = LightManager.Type.DIRECTIONAL,
135
+ apply = {
136
+ intensity(100_000f)
137
+ direction(0f, -1f, -0.5f)
138
+ }
139
+ )
140
+ }
141
+ }`,
142
+ ios: `import SwiftUI
143
+ import SceneViewSwift
144
+
145
+ struct DragToRotateScene: View {
146
+ @State private var rotationY: Float = 0
147
+ @State private var lastDragX: CGFloat = 0
148
+
149
+ var body: some View {
150
+ SceneView { root in
151
+ // Load model in .task
152
+ }
153
+ .gesture(
154
+ DragGesture()
155
+ .onChanged { value in
156
+ let dx = Float(value.translation.width - lastDragX) * 0.5
157
+ rotationY += dx
158
+ lastDragX = value.translation.width
159
+ }
160
+ .onEnded { _ in
161
+ lastDragX = 0
162
+ }
163
+ )
164
+ }
165
+ }`,
166
+ },
167
+ "pinch-to-scale": {
168
+ title: "Pinch to Scale Model",
169
+ description: "Two-finger pinch gesture to scale the model with min/max limits.",
170
+ android: `@Composable
171
+ fun PinchToScaleScene() {
172
+ val engine = rememberEngine()
173
+ val modelLoader = rememberModelLoader(engine)
174
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
175
+ var scaleFactor by remember { mutableFloatStateOf(1.0f) }
176
+ val minScale = 0.3f
177
+ val maxScale = 3.0f
178
+
179
+ SceneView(
180
+ modifier = Modifier
181
+ .fillMaxSize()
182
+ .pointerInput(Unit) {
183
+ awaitEachGesture {
184
+ // Detect pinch with two pointers
185
+ var prevSpan = 0f
186
+ while (true) {
187
+ val event = awaitPointerEvent()
188
+ if (event.changes.size >= 2) {
189
+ val p1 = event.changes[0].position
190
+ val p2 = event.changes[1].position
191
+ val span = (p1 - p2).getDistance()
192
+ if (prevSpan > 0f) {
193
+ val ratio = span / prevSpan
194
+ scaleFactor = (scaleFactor * ratio).coerceIn(minScale, maxScale)
195
+ }
196
+ prevSpan = span
197
+ event.changes.forEach { it.consume() }
198
+ } else {
199
+ break
200
+ }
201
+ }
202
+ }
203
+ },
204
+ engine = engine,
205
+ modelLoader = modelLoader
206
+ ) {
207
+ modelInstance?.let { instance ->
208
+ ModelNode(
209
+ modelInstance = instance,
210
+ scaleToUnits = scaleFactor,
211
+ centerOrigin = Position(0f, 0f, 0f)
212
+ )
213
+ }
214
+
215
+ LightNode(
216
+ engine = engine,
217
+ type = LightManager.Type.DIRECTIONAL,
218
+ apply = {
219
+ intensity(100_000f)
220
+ direction(0f, -1f, -0.5f)
221
+ }
222
+ )
223
+ }
224
+ }`,
225
+ },
226
+ "tap-to-place-ar": {
227
+ title: "AR Tap to Place",
228
+ description: "Tap on a detected AR plane to place a 3D model at that position.",
229
+ android: `@Composable
230
+ fun TapToPlaceARScene() {
231
+ val engine = rememberEngine()
232
+ val modelLoader = rememberModelLoader(engine)
233
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
234
+ var anchor by remember { mutableStateOf<Anchor?>(null) }
235
+
236
+ ARSceneView(
237
+ modifier = Modifier.fillMaxSize(),
238
+ engine = engine,
239
+ modelLoader = modelLoader,
240
+ planeRenderer = true,
241
+ sessionConfiguration = { session, config ->
242
+ config.lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
243
+ config.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL
244
+ },
245
+ onTouchEvent = { event, hitResult ->
246
+ if (event.action == MotionEvent.ACTION_UP && hitResult != null) {
247
+ // Detach old anchor to free ARCore resources
248
+ anchor?.detach()
249
+ anchor = hitResult.createAnchor()
250
+ }
251
+ true
252
+ }
253
+ ) {
254
+ anchor?.let { a ->
255
+ AnchorNode(anchor = a) {
256
+ modelInstance?.let { instance ->
257
+ ModelNode(
258
+ modelInstance = instance,
259
+ scaleToUnits = 0.5f,
260
+ isEditable = true // Enables pinch-to-scale + drag-to-rotate
261
+ )
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }`,
267
+ ios: `import SwiftUI
268
+ import SceneViewSwift
269
+ import RealityKit
270
+
271
+ struct TapToPlaceARScene: View {
272
+ @State private var model: ModelEntity?
273
+
274
+ var body: some View {
275
+ ARSceneView(
276
+ planeDetection: .horizontal,
277
+ showCoachingOverlay: true,
278
+ onTapOnPlane: { position, arView in
279
+ guard let model else { return }
280
+ let anchor = AnchorEntity(world: position)
281
+ let clone = model.clone(recursive: true)
282
+ clone.scale = .init(repeating: 0.5)
283
+ anchor.addChild(clone)
284
+ arView.scene.addAnchor(anchor)
285
+ }
286
+ )
287
+ .edgesIgnoringSafeArea(.all)
288
+ .task {
289
+ do {
290
+ model = try await ModelEntity.load(named: "models/object.usdz")
291
+ } catch {
292
+ print("Failed to load model: \\(error)")
293
+ }
294
+ }
295
+ }
296
+ }`,
297
+ },
298
+ "editable-model": {
299
+ title: "Editable Model (One-Line Gestures)",
300
+ description: "Use isEditable = true to get pinch-to-scale, drag-to-rotate, and tap-to-select with zero extra code.",
301
+ android: `@Composable
302
+ fun EditableModelScene() {
303
+ val engine = rememberEngine()
304
+ val modelLoader = rememberModelLoader(engine)
305
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
306
+
307
+ SceneView(
308
+ modifier = Modifier.fillMaxSize(),
309
+ engine = engine,
310
+ modelLoader = modelLoader,
311
+ cameraManipulator = rememberCameraManipulator()
312
+ ) {
313
+ modelInstance?.let { instance ->
314
+ ModelNode(
315
+ modelInstance = instance,
316
+ scaleToUnits = 1.0f,
317
+ centerOrigin = Position(0f, 0f, 0f),
318
+ // This single flag enables:
319
+ // - Pinch to scale
320
+ // - Drag to rotate
321
+ // - Tap to select
322
+ isEditable = true
323
+ )
324
+ }
325
+
326
+ LightNode(
327
+ engine = engine,
328
+ type = LightManager.Type.DIRECTIONAL,
329
+ apply = {
330
+ intensity(100_000f)
331
+ castShadows(true)
332
+ direction(0f, -1f, -0.5f)
333
+ }
334
+ )
335
+ }
336
+ }`,
337
+ },
338
+ "multi-select": {
339
+ title: "Multi-Model Selection",
340
+ description: "Tap to select/deselect multiple models with visual feedback.",
341
+ android: `@Composable
342
+ fun MultiSelectScene() {
343
+ val engine = rememberEngine()
344
+ val modelLoader = rememberModelLoader(engine)
345
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
346
+ val selectedNodes = remember { mutableStateListOf<String>() }
347
+
348
+ SceneView(
349
+ modifier = Modifier.fillMaxSize(),
350
+ engine = engine,
351
+ modelLoader = modelLoader,
352
+ onTouchEvent = { event, hitResult ->
353
+ if (event.action == MotionEvent.ACTION_UP) {
354
+ hitResult?.node?.name?.let { name ->
355
+ if (selectedNodes.contains(name)) {
356
+ selectedNodes.remove(name)
357
+ } else {
358
+ selectedNodes.add(name)
359
+ }
360
+ }
361
+ }
362
+ true
363
+ }
364
+ ) {
365
+ // Place multiple copies
366
+ val positions = listOf(
367
+ Position(-1.5f, 0f, 0f),
368
+ Position(0f, 0f, 0f),
369
+ Position(1.5f, 0f, 0f),
370
+ )
371
+ positions.forEachIndexed { index, pos ->
372
+ modelInstance?.let { instance ->
373
+ ModelNode(
374
+ modelInstance = instance,
375
+ scaleToUnits = if ("model_$index" in selectedNodes) 1.2f else 1.0f,
376
+ centerOrigin = Position(0f, 0f, 0f),
377
+ position = pos
378
+ )
379
+ }
380
+ }
381
+
382
+ LightNode(
383
+ engine = engine,
384
+ type = LightManager.Type.DIRECTIONAL,
385
+ apply = {
386
+ intensity(100_000f)
387
+ direction(0f, -1f, -0.5f)
388
+ }
389
+ )
390
+ }
391
+ }`,
392
+ },
393
+ "surface-cursor": {
394
+ title: "AR Surface Cursor (HitResultNode)",
395
+ description: "Show a cursor that follows the surface detected by AR plane detection.",
396
+ android: `@Composable
397
+ fun SurfaceCursorARScene() {
398
+ val engine = rememberEngine()
399
+ val modelLoader = rememberModelLoader(engine)
400
+ val cursorModel = rememberModelInstance(modelLoader, "models/cursor.glb")
401
+ val objectModel = rememberModelInstance(modelLoader, "models/object.glb")
402
+ var anchor by remember { mutableStateOf<Anchor?>(null) }
403
+
404
+ ARSceneView(
405
+ modifier = Modifier.fillMaxSize(),
406
+ engine = engine,
407
+ modelLoader = modelLoader,
408
+ planeRenderer = true,
409
+ sessionConfiguration = { session, config ->
410
+ config.lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
411
+ config.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL
412
+ },
413
+ onTouchEvent = { event, hitResult ->
414
+ if (event.action == MotionEvent.ACTION_UP && hitResult != null) {
415
+ anchor = hitResult.createAnchor()
416
+ }
417
+ true
418
+ }
419
+ ) {
420
+ // Surface cursor follows the center of the screen
421
+ HitResultNode(engine = engine) {
422
+ cursorModel?.let { instance ->
423
+ ModelNode(
424
+ modelInstance = instance,
425
+ scaleToUnits = 0.1f
426
+ )
427
+ }
428
+ }
429
+
430
+ // Placed object
431
+ anchor?.let { a ->
432
+ AnchorNode(anchor = a) {
433
+ objectModel?.let { instance ->
434
+ ModelNode(
435
+ modelInstance = instance,
436
+ scaleToUnits = 0.5f,
437
+ isEditable = true
438
+ )
439
+ }
440
+ }
441
+ }
442
+ }
443
+ }`,
444
+ },
445
+ "custom-touch": {
446
+ title: "Custom Touch Handler",
447
+ description: "Full control over touch events with hit testing and custom behavior.",
448
+ android: `@Composable
449
+ fun CustomTouchScene() {
450
+ val engine = rememberEngine()
451
+ val modelLoader = rememberModelLoader(engine)
452
+ val modelInstance = rememberModelInstance(modelLoader, "models/object.glb")
453
+ var touchInfo by remember { mutableStateOf("Touch a model") }
454
+ var modelScale by remember { mutableFloatStateOf(1.0f) }
455
+
456
+ Column(modifier = Modifier.fillMaxSize()) {
457
+ Text(
458
+ text = touchInfo,
459
+ modifier = Modifier.padding(16.dp),
460
+ style = MaterialTheme.typography.bodyLarge
461
+ )
462
+
463
+ SceneView(
464
+ modifier = Modifier.weight(1f).fillMaxWidth(),
465
+ engine = engine,
466
+ modelLoader = modelLoader,
467
+ onTouchEvent = { event, hitResult ->
468
+ when (event.action) {
469
+ MotionEvent.ACTION_DOWN -> {
470
+ if (hitResult?.node != null) {
471
+ touchInfo = "Touching: \${hitResult.node?.name} at (\${hitResult.distance}m)"
472
+ modelScale = 1.1f // Slight grow on press
473
+ } else {
474
+ touchInfo = "Touched empty space at (\${event.x}, \${event.y})"
475
+ }
476
+ true
477
+ }
478
+ MotionEvent.ACTION_UP -> {
479
+ modelScale = 1.0f // Reset on release
480
+ true
481
+ }
482
+ else -> false
483
+ }
484
+ }
485
+ ) {
486
+ modelInstance?.let { instance ->
487
+ ModelNode(
488
+ modelInstance = instance,
489
+ scaleToUnits = modelScale,
490
+ centerOrigin = Position(0f, 0f, 0f)
491
+ )
492
+ }
493
+
494
+ LightNode(
495
+ engine = engine,
496
+ type = LightManager.Type.DIRECTIONAL,
497
+ apply = {
498
+ intensity(100_000f)
499
+ direction(0f, -1f, -0.5f)
500
+ }
501
+ )
502
+ }
503
+ }
504
+ }`,
505
+ },
506
+ };
507
+ export function generateGestureCode(gestureType, platform = "android") {
508
+ const template = GESTURE_TEMPLATES[gestureType];
509
+ if (!template)
510
+ return null;
511
+ const code = platform === "ios" && template.ios ? template.ios : template.android;
512
+ return { code, title: template.title, description: template.description };
513
+ }
514
+ export function formatGestureCode(result, platform) {
515
+ const lang = platform === "ios" ? "swift" : "kotlin";
516
+ const platLabel = platform === "ios" ? "iOS (SwiftUI + RealityKit)" : "Android (Jetpack Compose)";
517
+ return [
518
+ `## ${result.title}`,
519
+ ``,
520
+ `**Platform:** ${platLabel}`,
521
+ ``,
522
+ result.description,
523
+ ``,
524
+ `\`\`\`${lang}`,
525
+ result.code,
526
+ `\`\`\``,
527
+ ``,
528
+ `### Available Gesture Types`,
529
+ ``,
530
+ ...GESTURE_TYPES.map((t) => `- \`${t}\``),
531
+ ].join("\n");
532
+ }