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.
- package/README.md +39 -0
- package/dist/analyze-project.js +500 -0
- package/dist/auth.js +84 -0
- package/dist/billing.js +137 -0
- package/dist/convert-platform.js +302 -0
- package/dist/debug-issue.js +2 -2
- package/dist/explain-api.js +245 -0
- package/dist/extra-guides.js +1 -1
- package/dist/generate-animation.js +576 -0
- package/dist/generate-environment.js +483 -0
- package/dist/generate-gesture.js +532 -0
- package/dist/generate-physics.js +570 -0
- package/dist/generate-scene.js +4 -4
- package/dist/generated/llms-txt.js +6 -0
- package/dist/guides.js +8 -8
- package/dist/index.js +54 -1111
- package/dist/migration.js +2 -2
- package/dist/optimize-scene.js +173 -0
- package/dist/platform-setup.js +11 -11
- package/dist/samples.js +64 -64
- package/dist/search-models.js +214 -0
- package/dist/telemetry.js +120 -0
- package/dist/tiers.js +100 -0
- package/dist/tools/definitions.js +467 -0
- package/dist/tools/handler.js +791 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/types.js +8 -0
- package/dist/validator.js +1 -1
- package/llms.txt +24 -1
- package/package.json +9 -20
|
@@ -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
|
+
}
|