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,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate-animation.ts
|
|
3
|
+
*
|
|
4
|
+
* Generates compilable code for animation playback, morph targets,
|
|
5
|
+
* transitions, and spring physics in SceneView (Android + iOS).
|
|
6
|
+
*/
|
|
7
|
+
const ANDROID_ANIMATIONS = {
|
|
8
|
+
"model-playback": {
|
|
9
|
+
description: "Play embedded glTF skeletal animation with autoAnimate or manual control",
|
|
10
|
+
code: `@Composable
|
|
11
|
+
fun AnimatedModelScreen() {
|
|
12
|
+
val engine = rememberEngine()
|
|
13
|
+
val modelLoader = rememberModelLoader(engine)
|
|
14
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/character.glb")
|
|
15
|
+
|
|
16
|
+
SceneView(
|
|
17
|
+
modifier = Modifier.fillMaxSize(),
|
|
18
|
+
engine = engine,
|
|
19
|
+
modelLoader = modelLoader,
|
|
20
|
+
cameraManipulator = rememberCameraManipulator()
|
|
21
|
+
) {
|
|
22
|
+
modelInstance?.let { instance ->
|
|
23
|
+
ModelNode(
|
|
24
|
+
modelInstance = instance,
|
|
25
|
+
scaleToUnits = 1.0f,
|
|
26
|
+
centerOrigin = Position(0f, 0f, 0f),
|
|
27
|
+
// autoAnimate plays the first embedded animation in a loop
|
|
28
|
+
autoAnimate = true
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
LightNode(
|
|
33
|
+
engine = engine,
|
|
34
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
35
|
+
apply = {
|
|
36
|
+
intensity(100_000f)
|
|
37
|
+
castShadows(true)
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}`,
|
|
42
|
+
notes: [
|
|
43
|
+
"The GLB file must contain embedded animations (skeletal or morph target).",
|
|
44
|
+
"Use `autoAnimate = true` for automatic looping of the first animation.",
|
|
45
|
+
"For manual control, use the Animator API in an `onFrame` callback.",
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
"morph-targets": {
|
|
49
|
+
description: "Animate morph targets (blend shapes) on a glTF model",
|
|
50
|
+
code: `@Composable
|
|
51
|
+
fun MorphTargetScreen() {
|
|
52
|
+
val engine = rememberEngine()
|
|
53
|
+
val modelLoader = rememberModelLoader(engine)
|
|
54
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/face.glb")
|
|
55
|
+
var smileWeight by remember { mutableFloatStateOf(0f) }
|
|
56
|
+
|
|
57
|
+
val infiniteTransition = rememberInfiniteTransition(label = "morph")
|
|
58
|
+
val animatedWeight by infiniteTransition.animateFloat(
|
|
59
|
+
initialValue = 0f,
|
|
60
|
+
targetValue = 1f,
|
|
61
|
+
animationSpec = infiniteRepeatable(
|
|
62
|
+
animation = tween(durationMillis = 2000, easing = FastOutSlowInEasing),
|
|
63
|
+
repeatMode = RepeatMode.Reverse
|
|
64
|
+
),
|
|
65
|
+
label = "smile"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
SceneView(
|
|
69
|
+
modifier = Modifier.fillMaxSize(),
|
|
70
|
+
engine = engine,
|
|
71
|
+
modelLoader = modelLoader,
|
|
72
|
+
cameraManipulator = rememberCameraManipulator(),
|
|
73
|
+
onFrame = { _ ->
|
|
74
|
+
modelInstance?.let { instance ->
|
|
75
|
+
// Apply morph target weights
|
|
76
|
+
// Index 0 is typically the first morph target defined in the GLB
|
|
77
|
+
instance.animator.let { animator ->
|
|
78
|
+
if (animator.morphTargetCount > 0) {
|
|
79
|
+
animator.setMorphTargetWeight(0, animatedWeight)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
) {
|
|
85
|
+
modelInstance?.let { instance ->
|
|
86
|
+
ModelNode(
|
|
87
|
+
modelInstance = instance,
|
|
88
|
+
scaleToUnits = 1.0f,
|
|
89
|
+
centerOrigin = Position(0f, 0f, 0f)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
LightNode(
|
|
94
|
+
engine = engine,
|
|
95
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
96
|
+
apply = { intensity(100_000f) }
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}`,
|
|
100
|
+
notes: [
|
|
101
|
+
"The GLB model must contain morph targets (also called blend shapes).",
|
|
102
|
+
"Morph target indices correspond to the order defined in the glTF file.",
|
|
103
|
+
"Check `animator.morphTargetCount` before applying weights.",
|
|
104
|
+
"Weight range is 0.0 to 1.0.",
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
"spring-position": {
|
|
108
|
+
description: "Spring-based position animation using KMP core SpringAnimation",
|
|
109
|
+
code: `@Composable
|
|
110
|
+
fun SpringPositionDemo() {
|
|
111
|
+
val engine = rememberEngine()
|
|
112
|
+
val modelLoader = rememberModelLoader(engine)
|
|
113
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/cube.glb")
|
|
114
|
+
|
|
115
|
+
var targetY by remember { mutableFloatStateOf(0f) }
|
|
116
|
+
val springY = remember {
|
|
117
|
+
SpringAnimation(
|
|
118
|
+
spring = Spring(
|
|
119
|
+
stiffness = Spring.StiffnessMedium,
|
|
120
|
+
dampingRatio = Spring.DampingRatioMediumBouncy
|
|
121
|
+
),
|
|
122
|
+
initialValue = 0f
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Column(modifier = Modifier.fillMaxSize()) {
|
|
127
|
+
SceneView(
|
|
128
|
+
modifier = Modifier.weight(1f).fillMaxWidth(),
|
|
129
|
+
engine = engine,
|
|
130
|
+
modelLoader = modelLoader,
|
|
131
|
+
cameraManipulator = rememberCameraManipulator(),
|
|
132
|
+
onFrame = { _ ->
|
|
133
|
+
springY.target = targetY
|
|
134
|
+
springY.advance(0.016f)
|
|
135
|
+
}
|
|
136
|
+
) {
|
|
137
|
+
modelInstance?.let { instance ->
|
|
138
|
+
ModelNode(
|
|
139
|
+
modelInstance = instance,
|
|
140
|
+
scaleToUnits = 0.5f,
|
|
141
|
+
position = Position(0f, springY.value, 0f)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
LightNode(
|
|
146
|
+
engine = engine,
|
|
147
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
148
|
+
apply = { intensity(100_000f) }
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Button(
|
|
153
|
+
onClick = { targetY = if (targetY < 1f) 2f else 0f },
|
|
154
|
+
modifier = Modifier.padding(16.dp).fillMaxWidth()
|
|
155
|
+
) {
|
|
156
|
+
Text("Bounce")
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}`,
|
|
160
|
+
notes: [
|
|
161
|
+
"SpringAnimation is from the `sceneview-core` KMP module.",
|
|
162
|
+
"Spring presets: StiffnessHigh (snappy), StiffnessMedium (general), StiffnessLow (heavy bounce).",
|
|
163
|
+
"DampingRatio: NoBouncy (1.0), LowBouncy (0.75), MediumBouncy (0.5), HighBouncy (0.2).",
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
"spring-scale": {
|
|
167
|
+
description: "Spring-based scale animation for bouncy resize effects",
|
|
168
|
+
code: `@Composable
|
|
169
|
+
fun SpringScaleDemo() {
|
|
170
|
+
val engine = rememberEngine()
|
|
171
|
+
val modelLoader = rememberModelLoader(engine)
|
|
172
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/star.glb")
|
|
173
|
+
|
|
174
|
+
var isExpanded by remember { mutableStateOf(false) }
|
|
175
|
+
val scale by animateFloatAsState(
|
|
176
|
+
targetValue = if (isExpanded) 2.0f else 1.0f,
|
|
177
|
+
animationSpec = spring(
|
|
178
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
179
|
+
stiffness = Spring.StiffnessMedium
|
|
180
|
+
),
|
|
181
|
+
label = "scale"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
Box(modifier = Modifier.fillMaxSize()) {
|
|
185
|
+
SceneView(
|
|
186
|
+
modifier = Modifier.fillMaxSize(),
|
|
187
|
+
engine = engine,
|
|
188
|
+
modelLoader = modelLoader,
|
|
189
|
+
cameraManipulator = rememberCameraManipulator(),
|
|
190
|
+
onTouchEvent = { event, _ ->
|
|
191
|
+
if (event.action == MotionEvent.ACTION_UP) {
|
|
192
|
+
isExpanded = !isExpanded
|
|
193
|
+
}
|
|
194
|
+
true
|
|
195
|
+
}
|
|
196
|
+
) {
|
|
197
|
+
modelInstance?.let { instance ->
|
|
198
|
+
ModelNode(
|
|
199
|
+
modelInstance = instance,
|
|
200
|
+
scaleToUnits = scale,
|
|
201
|
+
centerOrigin = Position(0f, 0f, 0f)
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
LightNode(
|
|
206
|
+
engine = engine,
|
|
207
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
208
|
+
apply = { intensity(100_000f) }
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
Text(
|
|
213
|
+
"Tap to \${if (isExpanded) "shrink" else "expand"}",
|
|
214
|
+
modifier = Modifier.align(Alignment.BottomCenter).padding(24.dp),
|
|
215
|
+
color = Color.White,
|
|
216
|
+
fontSize = 18.sp
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
}`,
|
|
220
|
+
notes: [
|
|
221
|
+
"Uses Compose's built-in `animateFloatAsState` with spring spec.",
|
|
222
|
+
"Tap anywhere on the scene to toggle between expanded and normal size.",
|
|
223
|
+
"The spring animation gives a natural bouncy feel.",
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
"compose-rotation": {
|
|
227
|
+
description: "Continuous rotation animation using Compose InfiniteTransition",
|
|
228
|
+
code: `@Composable
|
|
229
|
+
fun RotatingModelScreen() {
|
|
230
|
+
val engine = rememberEngine()
|
|
231
|
+
val modelLoader = rememberModelLoader(engine)
|
|
232
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/shoe.glb")
|
|
233
|
+
|
|
234
|
+
val infiniteTransition = rememberInfiniteTransition(label = "rotate")
|
|
235
|
+
val rotationY by infiniteTransition.animateFloat(
|
|
236
|
+
initialValue = 0f,
|
|
237
|
+
targetValue = 360f,
|
|
238
|
+
animationSpec = infiniteRepeatable(
|
|
239
|
+
animation = tween(durationMillis = 5000, easing = LinearEasing),
|
|
240
|
+
repeatMode = RepeatMode.Restart
|
|
241
|
+
),
|
|
242
|
+
label = "rotY"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
SceneView(
|
|
246
|
+
modifier = Modifier.fillMaxSize(),
|
|
247
|
+
engine = engine,
|
|
248
|
+
modelLoader = modelLoader,
|
|
249
|
+
cameraManipulator = rememberCameraManipulator()
|
|
250
|
+
) {
|
|
251
|
+
modelInstance?.let { instance ->
|
|
252
|
+
ModelNode(
|
|
253
|
+
modelInstance = instance,
|
|
254
|
+
scaleToUnits = 1.0f,
|
|
255
|
+
rotation = Rotation(0f, rotationY, 0f),
|
|
256
|
+
centerOrigin = Position(0f, 0f, 0f)
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
LightNode(
|
|
261
|
+
engine = engine,
|
|
262
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
263
|
+
apply = {
|
|
264
|
+
intensity(100_000f)
|
|
265
|
+
castShadows(true)
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}`,
|
|
270
|
+
notes: [
|
|
271
|
+
"Uses Compose's InfiniteTransition for smooth, battery-efficient rotation.",
|
|
272
|
+
"Rotation is in degrees around the Y axis (turntable style).",
|
|
273
|
+
"Change `durationMillis` to control rotation speed.",
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
"compose-scale": {
|
|
277
|
+
description: "Animate scale with Compose animateFloatAsState for toggle effects",
|
|
278
|
+
code: `@Composable
|
|
279
|
+
fun ScaleToggleScreen() {
|
|
280
|
+
val engine = rememberEngine()
|
|
281
|
+
val modelLoader = rememberModelLoader(engine)
|
|
282
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/trophy.glb")
|
|
283
|
+
|
|
284
|
+
var showModel by remember { mutableStateOf(true) }
|
|
285
|
+
val scale by animateFloatAsState(
|
|
286
|
+
targetValue = if (showModel) 1.0f else 0.01f,
|
|
287
|
+
animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing),
|
|
288
|
+
label = "scale"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
SceneView(
|
|
292
|
+
modifier = Modifier.fillMaxSize(),
|
|
293
|
+
engine = engine,
|
|
294
|
+
modelLoader = modelLoader,
|
|
295
|
+
cameraManipulator = rememberCameraManipulator()
|
|
296
|
+
) {
|
|
297
|
+
modelInstance?.let { instance ->
|
|
298
|
+
ModelNode(
|
|
299
|
+
modelInstance = instance,
|
|
300
|
+
scaleToUnits = scale,
|
|
301
|
+
centerOrigin = Position(0f, 0f, 0f)
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
LightNode(
|
|
306
|
+
engine = engine,
|
|
307
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
308
|
+
apply = { intensity(100_000f) }
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
}`,
|
|
312
|
+
notes: [
|
|
313
|
+
"Animate scale to near-zero (0.01f) instead of removing the node for smooth disappear.",
|
|
314
|
+
"Combine with `animateColorAsState` for material color transitions.",
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
"smooth-follow": {
|
|
318
|
+
description: "Smooth interpolation for a node following a moving target",
|
|
319
|
+
code: `@Composable
|
|
320
|
+
fun SmoothFollowDemo() {
|
|
321
|
+
val engine = rememberEngine()
|
|
322
|
+
val modelLoader = rememberModelLoader(engine)
|
|
323
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/drone.glb")
|
|
324
|
+
|
|
325
|
+
val smoothPosition = remember { SmoothTransform(smoothTime = 0.3f) }
|
|
326
|
+
var targetPosition by remember { mutableStateOf(Position(0f, 1f, 0f)) }
|
|
327
|
+
var time by remember { mutableFloatStateOf(0f) }
|
|
328
|
+
|
|
329
|
+
SceneView(
|
|
330
|
+
modifier = Modifier.fillMaxSize(),
|
|
331
|
+
engine = engine,
|
|
332
|
+
modelLoader = modelLoader,
|
|
333
|
+
cameraManipulator = rememberCameraManipulator(),
|
|
334
|
+
onFrame = { frameTimeNanos ->
|
|
335
|
+
time += 0.016f
|
|
336
|
+
// Move target in a circle
|
|
337
|
+
targetPosition = Position(
|
|
338
|
+
kotlin.math.sin(time) * 2f,
|
|
339
|
+
1f + kotlin.math.sin(time * 2f) * 0.5f,
|
|
340
|
+
kotlin.math.cos(time) * 2f
|
|
341
|
+
)
|
|
342
|
+
smoothPosition.target = targetPosition
|
|
343
|
+
smoothPosition.advance(0.016f)
|
|
344
|
+
}
|
|
345
|
+
) {
|
|
346
|
+
modelInstance?.let { instance ->
|
|
347
|
+
ModelNode(
|
|
348
|
+
modelInstance = instance,
|
|
349
|
+
scaleToUnits = 0.3f,
|
|
350
|
+
position = smoothPosition.current
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
LightNode(
|
|
355
|
+
engine = engine,
|
|
356
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
357
|
+
apply = { intensity(100_000f) }
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
}`,
|
|
361
|
+
notes: [
|
|
362
|
+
"SmoothTransform is from the `sceneview-core` KMP module.",
|
|
363
|
+
"Ideal for camera follow, target tracking, or any smooth interpolation.",
|
|
364
|
+
"`smoothTime` controls how quickly the node catches up (lower = faster).",
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
"transition": {
|
|
368
|
+
description: "Animated state transition between two model positions",
|
|
369
|
+
code: `@Composable
|
|
370
|
+
fun TransitionDemo() {
|
|
371
|
+
val engine = rememberEngine()
|
|
372
|
+
val modelLoader = rememberModelLoader(engine)
|
|
373
|
+
val modelA = rememberModelInstance(modelLoader, "models/chair.glb")
|
|
374
|
+
val modelB = rememberModelInstance(modelLoader, "models/table.glb")
|
|
375
|
+
|
|
376
|
+
var isSlotA by remember { mutableStateOf(true) }
|
|
377
|
+
val positionX by animateFloatAsState(
|
|
378
|
+
targetValue = if (isSlotA) -1.5f else 1.5f,
|
|
379
|
+
animationSpec = spring(
|
|
380
|
+
dampingRatio = Spring.DampingRatioLowBouncy,
|
|
381
|
+
stiffness = Spring.StiffnessMediumLow
|
|
382
|
+
),
|
|
383
|
+
label = "posX"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
Column(modifier = Modifier.fillMaxSize()) {
|
|
387
|
+
SceneView(
|
|
388
|
+
modifier = Modifier.weight(1f).fillMaxWidth(),
|
|
389
|
+
engine = engine,
|
|
390
|
+
modelLoader = modelLoader,
|
|
391
|
+
cameraManipulator = rememberCameraManipulator()
|
|
392
|
+
) {
|
|
393
|
+
modelA?.let { instance ->
|
|
394
|
+
ModelNode(
|
|
395
|
+
modelInstance = instance,
|
|
396
|
+
scaleToUnits = 0.8f,
|
|
397
|
+
position = Position(positionX, 0f, 0f),
|
|
398
|
+
centerOrigin = Position(0f, 0f, 0f)
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
modelB?.let { instance ->
|
|
403
|
+
ModelNode(
|
|
404
|
+
modelInstance = instance,
|
|
405
|
+
scaleToUnits = 0.8f,
|
|
406
|
+
position = Position(-positionX, 0f, 0f),
|
|
407
|
+
centerOrigin = Position(0f, 0f, 0f)
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
LightNode(
|
|
412
|
+
engine = engine,
|
|
413
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
414
|
+
apply = { intensity(100_000f) }
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
Button(
|
|
419
|
+
onClick = { isSlotA = !isSlotA },
|
|
420
|
+
modifier = Modifier.padding(16.dp).fillMaxWidth()
|
|
421
|
+
) {
|
|
422
|
+
Text("Swap Positions")
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}`,
|
|
426
|
+
notes: [
|
|
427
|
+
"Uses Compose spring animation for smooth position transitions.",
|
|
428
|
+
"Both models animate simultaneously in opposite directions.",
|
|
429
|
+
"Adjust spring parameters for different feels.",
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
const IOS_ANIMATIONS = {
|
|
434
|
+
"model-playback": {
|
|
435
|
+
description: "Play embedded USDZ animation in SwiftUI",
|
|
436
|
+
code: `import SwiftUI
|
|
437
|
+
import SceneViewSwift
|
|
438
|
+
import RealityKit
|
|
439
|
+
|
|
440
|
+
struct AnimatedModelView: View {
|
|
441
|
+
@State private var model: ModelNode?
|
|
442
|
+
|
|
443
|
+
var body: some View {
|
|
444
|
+
SceneView { root in
|
|
445
|
+
if let model {
|
|
446
|
+
root.addChild(model.entity)
|
|
447
|
+
// RealityKit plays animations automatically if present
|
|
448
|
+
model.entity.availableAnimations.forEach { animation in
|
|
449
|
+
model.entity.playAnimation(animation.repeat())
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
.cameraControls(.orbit)
|
|
454
|
+
.task {
|
|
455
|
+
do {
|
|
456
|
+
model = try await ModelNode.load("models/character.usdz")
|
|
457
|
+
model?.scaleToUnits(1.0)
|
|
458
|
+
} catch {
|
|
459
|
+
print("Failed to load model: \\(error)")
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}`,
|
|
464
|
+
notes: [
|
|
465
|
+
"USDZ files can embed animations. RealityKit plays them via `playAnimation()`.",
|
|
466
|
+
"Use `.repeat()` for looping, `.repeat(count:)` for finite repeats.",
|
|
467
|
+
"Check `entity.availableAnimations` to list all embedded animations.",
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
"spring-position": {
|
|
471
|
+
description: "Spring-based position animation in SwiftUI with withAnimation",
|
|
472
|
+
code: `import SwiftUI
|
|
473
|
+
import SceneViewSwift
|
|
474
|
+
import RealityKit
|
|
475
|
+
|
|
476
|
+
struct SpringPositionView: View {
|
|
477
|
+
@State private var model: ModelNode?
|
|
478
|
+
@State private var isUp = false
|
|
479
|
+
|
|
480
|
+
var body: some View {
|
|
481
|
+
VStack {
|
|
482
|
+
SceneView { root in
|
|
483
|
+
if let model {
|
|
484
|
+
root.addChild(model.entity)
|
|
485
|
+
let y: Float = isUp ? 1.5 : 0.0
|
|
486
|
+
model.entity.position = .init(x: 0, y: y, z: 0)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
.cameraControls(.orbit)
|
|
490
|
+
|
|
491
|
+
Button("Bounce") {
|
|
492
|
+
withAnimation(.spring(response: 0.5, dampingFraction: 0.4, blendDuration: 0)) {
|
|
493
|
+
isUp.toggle()
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
.padding()
|
|
497
|
+
}
|
|
498
|
+
.task {
|
|
499
|
+
do {
|
|
500
|
+
model = try await ModelNode.load("models/cube.usdz")
|
|
501
|
+
model?.scaleToUnits(0.5)
|
|
502
|
+
} catch {
|
|
503
|
+
print("Failed to load: \\(error)")
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}`,
|
|
508
|
+
notes: [
|
|
509
|
+
"SwiftUI's `withAnimation(.spring(...))` animates state changes.",
|
|
510
|
+
"For RealityKit entity transforms, use `move(to:relativeTo:duration:)` for smooth transitions.",
|
|
511
|
+
"Adjust `response` (speed) and `dampingFraction` (bounciness) for different spring feels.",
|
|
512
|
+
],
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
export function generateAnimationCode(animationType, platform = "android") {
|
|
516
|
+
if (platform === "ios") {
|
|
517
|
+
const iosAnim = IOS_ANIMATIONS[animationType];
|
|
518
|
+
if (iosAnim) {
|
|
519
|
+
return {
|
|
520
|
+
code: iosAnim.code,
|
|
521
|
+
platform: "ios",
|
|
522
|
+
animationType,
|
|
523
|
+
description: iosAnim.description,
|
|
524
|
+
notes: iosAnim.notes,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
// Fallback: not all types have iOS equivalents
|
|
528
|
+
return {
|
|
529
|
+
code: IOS_ANIMATIONS["model-playback"].code,
|
|
530
|
+
platform: "ios",
|
|
531
|
+
animationType: "model-playback",
|
|
532
|
+
description: `iOS equivalent for '${animationType}' not available. Showing model-playback instead.`,
|
|
533
|
+
notes: [`The '${animationType}' animation type is Android-specific. Use RealityKit's native animation APIs for iOS.`],
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const anim = ANDROID_ANIMATIONS[animationType];
|
|
537
|
+
if (!anim)
|
|
538
|
+
return null;
|
|
539
|
+
return {
|
|
540
|
+
code: anim.code,
|
|
541
|
+
platform: "android",
|
|
542
|
+
animationType,
|
|
543
|
+
description: anim.description,
|
|
544
|
+
notes: anim.notes,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
export const ANIMATION_TYPES = [
|
|
548
|
+
"model-playback",
|
|
549
|
+
"morph-targets",
|
|
550
|
+
"spring-position",
|
|
551
|
+
"spring-scale",
|
|
552
|
+
"compose-rotation",
|
|
553
|
+
"compose-scale",
|
|
554
|
+
"smooth-follow",
|
|
555
|
+
"transition",
|
|
556
|
+
];
|
|
557
|
+
export function formatAnimationCode(result) {
|
|
558
|
+
const lang = result.platform === "ios" ? "swift" : "kotlin";
|
|
559
|
+
const parts = [
|
|
560
|
+
`## Animation: ${result.animationType}`,
|
|
561
|
+
`**Platform:** ${result.platform === "ios" ? "iOS (SwiftUI + RealityKit)" : "Android (Jetpack Compose + Filament)"}`,
|
|
562
|
+
`**Description:** ${result.description}`,
|
|
563
|
+
``,
|
|
564
|
+
`### Code`,
|
|
565
|
+
``,
|
|
566
|
+
"```" + lang,
|
|
567
|
+
result.code,
|
|
568
|
+
"```",
|
|
569
|
+
``,
|
|
570
|
+
];
|
|
571
|
+
if (result.notes.length > 0) {
|
|
572
|
+
parts.push(`### Notes`);
|
|
573
|
+
result.notes.forEach((n, i) => parts.push(`${i + 1}. ${n}`));
|
|
574
|
+
}
|
|
575
|
+
return parts.join("\n");
|
|
576
|
+
}
|