sceneview-mcp 3.0.0 → 3.0.2
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 +64 -107
- package/dist/index.js +113 -8
- package/dist/issues.js +72 -0
- package/dist/issues.test.js +114 -0
- package/dist/migration.js +248 -0
- package/dist/migration.test.js +50 -0
- package/dist/samples.js +61 -0
- package/dist/samples.test.js +119 -0
- package/dist/validator.js +251 -0
- package/dist/validator.test.js +246 -0
- package/package.json +32 -5
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
export const MIGRATION_GUIDE = `# SceneView 2.x → 3.0 Migration Guide
|
|
2
|
+
|
|
3
|
+
SceneView 3.0 is a full rewrite from Android Views to **Jetpack Compose**. Nearly every public API changed. This guide covers every breaking change and how to fix it.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Gradle dependency
|
|
8
|
+
|
|
9
|
+
| 2.x | 3.0 |
|
|
10
|
+
|-----|-----|
|
|
11
|
+
| \`io.github.sceneview:sceneview:2.x.x\` | \`io.github.sceneview:sceneview:3.0.0\` |
|
|
12
|
+
| \`io.github.sceneview:arsceneview:2.x.x\` | \`io.github.sceneview:arsceneview:3.0.0\` |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 2. Root composable names
|
|
17
|
+
|
|
18
|
+
| 2.x | 3.0 |
|
|
19
|
+
|-----|-----|
|
|
20
|
+
| \`SceneView(…)\` | \`Scene(…)\` |
|
|
21
|
+
| \`ArSceneView(…)\` | \`ARScene(…)\` |
|
|
22
|
+
|
|
23
|
+
**Before:**
|
|
24
|
+
\`\`\`kotlin
|
|
25
|
+
SceneView(modifier = Modifier.fillMaxSize())
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
**After:**
|
|
29
|
+
\`\`\`kotlin
|
|
30
|
+
val engine = rememberEngine()
|
|
31
|
+
Scene(modifier = Modifier.fillMaxSize(), engine = engine)
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 3. Engine lifecycle
|
|
37
|
+
|
|
38
|
+
In 2.x the engine was managed internally. In 3.0 you own it — use \`rememberEngine()\` which ties it to the composition lifecycle.
|
|
39
|
+
|
|
40
|
+
| 2.x | 3.0 |
|
|
41
|
+
|-----|-----|
|
|
42
|
+
| Engine implicit | \`val engine = rememberEngine()\` |
|
|
43
|
+
| Never destroy manually | Never call \`engine.destroy()\` — \`rememberEngine\` does it |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 4. Model loading
|
|
48
|
+
|
|
49
|
+
| 2.x | 3.0 |
|
|
50
|
+
|-----|-----|
|
|
51
|
+
| \`modelLoader.loadModelAsync(path) { … }\` | \`rememberModelInstance(modelLoader, path)\` (returns \`null\` while loading) |
|
|
52
|
+
| \`modelLoader.loadModel(path)\` | \`modelLoader.loadModelInstanceAsync(path)\` (imperative) |
|
|
53
|
+
| \`ModelRenderable.builder()\` | Removed — use GLB/glTF assets |
|
|
54
|
+
|
|
55
|
+
**Before:**
|
|
56
|
+
\`\`\`kotlin
|
|
57
|
+
var modelInstance by remember { mutableStateOf<ModelInstance?>(null) }
|
|
58
|
+
LaunchedEffect(Unit) {
|
|
59
|
+
modelInstance = modelLoader.loadModelAsync("models/chair.glb")
|
|
60
|
+
}
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
**After:**
|
|
64
|
+
\`\`\`kotlin
|
|
65
|
+
Scene(engine = engine, modelLoader = modelLoader) {
|
|
66
|
+
rememberModelInstance(modelLoader, "models/chair.glb")?.let { instance ->
|
|
67
|
+
ModelNode(modelInstance = instance, scaleToUnits = 1.0f)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 5. Node hierarchy — imperative → declarative DSL
|
|
75
|
+
|
|
76
|
+
In 2.x nodes were added imperatively (\`scene.addChild(node)\`). In 3.0 nodes are declared as composables inside \`Scene { }\` or \`ARScene { }\`.
|
|
77
|
+
|
|
78
|
+
**Before:**
|
|
79
|
+
\`\`\`kotlin
|
|
80
|
+
val modelNode = ModelNode().apply {
|
|
81
|
+
loadModelGlbAsync(
|
|
82
|
+
glbFileLocation = "models/chair.glb",
|
|
83
|
+
scaleToUnits = 1f,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
sceneView.addChildNode(modelNode)
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
**After:**
|
|
90
|
+
\`\`\`kotlin
|
|
91
|
+
Scene(engine = engine, modelLoader = modelLoader) {
|
|
92
|
+
rememberModelInstance(modelLoader, "models/chair.glb")?.let { instance ->
|
|
93
|
+
ModelNode(modelInstance = instance, scaleToUnits = 1f)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 6. Removed nodes and replacements
|
|
101
|
+
|
|
102
|
+
| 2.x | 3.0 replacement |
|
|
103
|
+
|-----|-----------------|
|
|
104
|
+
| \`TransformableNode\` | Set \`isEditable = true\` on \`ModelNode\` |
|
|
105
|
+
| \`PlacementNode\` | \`AnchorNode(anchor = hitResult.createAnchor())\` + \`HitResultNode\` |
|
|
106
|
+
| \`ViewRenderable\` | \`ViewNode\` with a \`@Composable\` content lambda |
|
|
107
|
+
| \`AnchorNode()\` (no-arg) | \`AnchorNode(anchor = hitResult.createAnchor())\` |
|
|
108
|
+
|
|
109
|
+
**TransformableNode:**
|
|
110
|
+
\`\`\`kotlin
|
|
111
|
+
// Before
|
|
112
|
+
val node = TransformableNode(transformationSystem).apply {
|
|
113
|
+
setParent(anchorNode)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// After
|
|
117
|
+
ModelNode(
|
|
118
|
+
modelInstance = instance,
|
|
119
|
+
scaleToUnits = 1f,
|
|
120
|
+
isEditable = true // enables pinch-to-scale + drag-to-rotate
|
|
121
|
+
)
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
**ViewRenderable → ViewNode:**
|
|
125
|
+
\`\`\`kotlin
|
|
126
|
+
// Before
|
|
127
|
+
ViewRenderable.builder()
|
|
128
|
+
.setView(context, R.layout.my_layout)
|
|
129
|
+
.build()
|
|
130
|
+
.thenAccept { renderable -> … }
|
|
131
|
+
|
|
132
|
+
// After
|
|
133
|
+
val windowManager = rememberViewNodeManager()
|
|
134
|
+
Scene(viewNodeWindowManager = windowManager) {
|
|
135
|
+
ViewNode(windowManager = windowManager) {
|
|
136
|
+
Card { Text("Hello 3D World!") }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 7. Light configuration
|
|
144
|
+
|
|
145
|
+
LightNode's \`apply\` is a **named parameter** (not a trailing lambda). This is the most common silent breakage after migrating.
|
|
146
|
+
|
|
147
|
+
\`\`\`kotlin
|
|
148
|
+
// WRONG — trailing lambda is silently ignored
|
|
149
|
+
LightNode(engine = engine, type = LightManager.Type.DIRECTIONAL) {
|
|
150
|
+
intensity(100_000f)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// CORRECT
|
|
154
|
+
LightNode(
|
|
155
|
+
engine = engine,
|
|
156
|
+
type = LightManager.Type.DIRECTIONAL,
|
|
157
|
+
apply = {
|
|
158
|
+
intensity(100_000f)
|
|
159
|
+
castShadows(true)
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 8. Environment / IBL loading
|
|
167
|
+
|
|
168
|
+
| 2.x | 3.0 |
|
|
169
|
+
|-----|-----|
|
|
170
|
+
| \`environmentLoader.loadEnvironment(path)\` | \`environmentLoader.createHDREnvironment(path)\` |
|
|
171
|
+
|
|
172
|
+
\`\`\`kotlin
|
|
173
|
+
// 3.0
|
|
174
|
+
val environmentLoader = rememberEnvironmentLoader(engine)
|
|
175
|
+
Scene(
|
|
176
|
+
environment = rememberEnvironment(environmentLoader) {
|
|
177
|
+
environmentLoader.createHDREnvironment("environments/sky_2k.hdr")!!
|
|
178
|
+
}
|
|
179
|
+
) { … }
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 9. AR session configuration
|
|
185
|
+
|
|
186
|
+
In 3.0 \`sessionConfiguration\` is a lambda parameter on \`ARScene\` (not a separate builder).
|
|
187
|
+
|
|
188
|
+
\`\`\`kotlin
|
|
189
|
+
ARScene(
|
|
190
|
+
engine = engine,
|
|
191
|
+
modelLoader = modelLoader,
|
|
192
|
+
sessionConfiguration = { session, config ->
|
|
193
|
+
config.depthMode =
|
|
194
|
+
if (session.isDepthModeSupported(Config.DepthMode.AUTOMATIC))
|
|
195
|
+
Config.DepthMode.AUTOMATIC else Config.DepthMode.DISABLED
|
|
196
|
+
config.instantPlacementMode = Config.InstantPlacementMode.LOCAL_Y_UP
|
|
197
|
+
config.lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
|
|
198
|
+
}
|
|
199
|
+
) { … }
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 10. AR anchors (no more worldPosition hacks)
|
|
205
|
+
|
|
206
|
+
| 2.x pattern | 3.0 |
|
|
207
|
+
|-------------|-----|
|
|
208
|
+
| \`node.worldPosition = hitResult.hitPose.position\` | \`AnchorNode(anchor = hitResult.createAnchor())\` |
|
|
209
|
+
|
|
210
|
+
Plain nodes whose \`worldPosition\` is set manually will drift when ARCore remaps its coordinate system. \`AnchorNode\` compensates automatically.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 11. Shadows
|
|
215
|
+
|
|
216
|
+
In 3.0, \`ARScene\` has shadows enabled by default via \`createARView()\`. For \`Scene\` (3D only), shadows are disabled by default — enable with:
|
|
217
|
+
|
|
218
|
+
\`\`\`kotlin
|
|
219
|
+
Scene(
|
|
220
|
+
view = rememberView(engine).also { it.setShadowingEnabled(true) },
|
|
221
|
+
…
|
|
222
|
+
)
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 12. Camera
|
|
228
|
+
|
|
229
|
+
| 2.x | 3.0 |
|
|
230
|
+
|-----|-----|
|
|
231
|
+
| \`CameraManipulator\` set on the View | \`cameraManipulator = rememberCameraManipulator()\` on \`Scene\` |
|
|
232
|
+
| Custom camera via \`setCameraNode\` | \`cameraNode = rememberCameraNode(engine) { … }\` on \`Scene\` |
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Checklist
|
|
237
|
+
|
|
238
|
+
- [ ] Replace \`SceneView(…)\` → \`Scene(engine = rememberEngine(), …)\`
|
|
239
|
+
- [ ] Replace \`ArSceneView(…)\` → \`ARScene(engine = rememberEngine(), …)\`
|
|
240
|
+
- [ ] Replace \`modelLoader.loadModelAsync\` → \`rememberModelInstance\`
|
|
241
|
+
- [ ] Add null-check on every \`rememberModelInstance\` result
|
|
242
|
+
- [ ] Replace \`TransformableNode\` → \`isEditable = true\`
|
|
243
|
+
- [ ] Replace \`PlacementNode\` → \`AnchorNode\` + \`HitResultNode\`
|
|
244
|
+
- [ ] Replace \`ViewRenderable\` → \`ViewNode\` with Compose lambda
|
|
245
|
+
- [ ] Fix \`LightNode { … }\` → \`LightNode(apply = { … })\`
|
|
246
|
+
- [ ] Remove manual \`engine.destroy()\` calls
|
|
247
|
+
- [ ] Replace manual \`worldPosition\` in AR → \`AnchorNode\`
|
|
248
|
+
`;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { MIGRATION_GUIDE } from "./migration.js";
|
|
3
|
+
describe("MIGRATION_GUIDE", () => {
|
|
4
|
+
it("is a non-empty string", () => {
|
|
5
|
+
expect(typeof MIGRATION_GUIDE).toBe("string");
|
|
6
|
+
expect(MIGRATION_GUIDE.length).toBeGreaterThan(500);
|
|
7
|
+
});
|
|
8
|
+
it("covers composable renames (SceneView → Scene, ArSceneView → ARScene)", () => {
|
|
9
|
+
expect(MIGRATION_GUIDE).toContain("SceneView");
|
|
10
|
+
expect(MIGRATION_GUIDE).toContain("Scene");
|
|
11
|
+
expect(MIGRATION_GUIDE).toContain("ArSceneView");
|
|
12
|
+
expect(MIGRATION_GUIDE).toContain("ARScene");
|
|
13
|
+
});
|
|
14
|
+
it("covers model loading migration (loadModelAsync → rememberModelInstance)", () => {
|
|
15
|
+
expect(MIGRATION_GUIDE).toContain("loadModelAsync");
|
|
16
|
+
expect(MIGRATION_GUIDE).toContain("rememberModelInstance");
|
|
17
|
+
});
|
|
18
|
+
it("covers removed nodes", () => {
|
|
19
|
+
expect(MIGRATION_GUIDE).toContain("TransformableNode");
|
|
20
|
+
expect(MIGRATION_GUIDE).toContain("PlacementNode");
|
|
21
|
+
expect(MIGRATION_GUIDE).toContain("ViewRenderable");
|
|
22
|
+
});
|
|
23
|
+
it("covers LightNode named apply parameter gotcha", () => {
|
|
24
|
+
expect(MIGRATION_GUIDE).toContain("apply");
|
|
25
|
+
expect(MIGRATION_GUIDE).toContain("LightNode");
|
|
26
|
+
expect(MIGRATION_GUIDE).toContain("trailing lambda");
|
|
27
|
+
});
|
|
28
|
+
it("covers engine lifecycle (rememberEngine)", () => {
|
|
29
|
+
expect(MIGRATION_GUIDE).toContain("rememberEngine");
|
|
30
|
+
expect(MIGRATION_GUIDE).toContain("engine.destroy");
|
|
31
|
+
});
|
|
32
|
+
it("covers AR anchor drift (worldPosition → AnchorNode)", () => {
|
|
33
|
+
expect(MIGRATION_GUIDE).toContain("worldPosition");
|
|
34
|
+
expect(MIGRATION_GUIDE).toContain("AnchorNode");
|
|
35
|
+
expect(MIGRATION_GUIDE).toContain("drift");
|
|
36
|
+
});
|
|
37
|
+
it("covers gradle dependency changes", () => {
|
|
38
|
+
expect(MIGRATION_GUIDE).toContain("io.github.sceneview:sceneview:3.0.0");
|
|
39
|
+
expect(MIGRATION_GUIDE).toContain("io.github.sceneview:arsceneview:3.0.0");
|
|
40
|
+
});
|
|
41
|
+
it("includes a migration checklist", () => {
|
|
42
|
+
expect(MIGRATION_GUIDE).toContain("Checklist");
|
|
43
|
+
expect(MIGRATION_GUIDE).toContain("- [ ]");
|
|
44
|
+
});
|
|
45
|
+
it("includes before/after code examples", () => {
|
|
46
|
+
expect(MIGRATION_GUIDE).toContain("Before");
|
|
47
|
+
expect(MIGRATION_GUIDE).toContain("After");
|
|
48
|
+
expect(MIGRATION_GUIDE).toContain("```kotlin");
|
|
49
|
+
});
|
|
50
|
+
});
|
package/dist/samples.js
CHANGED
|
@@ -3,6 +3,7 @@ export const SAMPLES = {
|
|
|
3
3
|
id: "model-viewer",
|
|
4
4
|
title: "3D Model Viewer",
|
|
5
5
|
description: "Full-screen 3D scene with a GLB model, HDR environment, and orbit camera",
|
|
6
|
+
tags: ["3d", "model", "environment", "camera"],
|
|
6
7
|
dependency: "io.github.sceneview:sceneview:3.0.0",
|
|
7
8
|
prompt: "Create an Android Compose screen called `ModelViewerScreen` that loads a GLB file from assets/models/my_model.glb and displays it in a full-screen 3D scene with an orbit camera (drag to rotate, pinch to zoom). Add an HDR environment from assets/environments/sky_2k.hdr for realistic lighting. Use SceneView `io.github.sceneview:sceneview:3.0.0`.",
|
|
8
9
|
code: `@Composable
|
|
@@ -30,12 +31,69 @@ fun ModelViewerScreen() {
|
|
|
30
31
|
)
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
}`,
|
|
35
|
+
},
|
|
36
|
+
"geometry-scene": {
|
|
37
|
+
id: "geometry-scene",
|
|
38
|
+
title: "3D Geometry Scene",
|
|
39
|
+
description: "Procedural 3D scene using primitive geometry nodes (cube, sphere, plane) — no GLB required",
|
|
40
|
+
tags: ["3d", "geometry", "animation"],
|
|
41
|
+
dependency: "io.github.sceneview:sceneview:3.0.0",
|
|
42
|
+
prompt: "Create an Android Compose screen called `GeometrySceneScreen` that renders a full-screen 3D scene with a red rotating cube, a metallic blue sphere, and a green floor plane. No model files — use SceneView built-in geometry nodes. Orbit camera. Use SceneView `io.github.sceneview:sceneview:3.0.0`.",
|
|
43
|
+
code: `@Composable
|
|
44
|
+
fun GeometrySceneScreen() {
|
|
45
|
+
val engine = rememberEngine()
|
|
46
|
+
val materialLoader = rememberMaterialLoader(engine)
|
|
47
|
+
val t = rememberInfiniteTransition(label = "spin")
|
|
48
|
+
val angle by t.animateFloat(
|
|
49
|
+
initialValue = 0f, targetValue = 360f,
|
|
50
|
+
animationSpec = infiniteRepeatable(tween(4_000, easing = LinearEasing)),
|
|
51
|
+
label = "angle"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
Scene(
|
|
55
|
+
modifier = Modifier.fillMaxSize(),
|
|
56
|
+
engine = engine,
|
|
57
|
+
materialLoader = materialLoader,
|
|
58
|
+
mainLightNode = rememberMainLightNode(engine) { intensity(80_000f) },
|
|
59
|
+
cameraManipulator = rememberCameraManipulator()
|
|
60
|
+
) {
|
|
61
|
+
// Rotating red cube
|
|
62
|
+
CubeNode(
|
|
63
|
+
engine,
|
|
64
|
+
size = Size(0.5f, 0.5f, 0.5f),
|
|
65
|
+
materialInstance = materialLoader.createColorInstance(
|
|
66
|
+
Color.Red, metallic = 0f, roughness = 0.5f
|
|
67
|
+
),
|
|
68
|
+
position = Position(x = -0.6f),
|
|
69
|
+
rotation = Rotation(y = angle)
|
|
70
|
+
)
|
|
71
|
+
// Metallic blue sphere
|
|
72
|
+
SphereNode(
|
|
73
|
+
engine,
|
|
74
|
+
radius = 0.3f,
|
|
75
|
+
materialInstance = materialLoader.createColorInstance(
|
|
76
|
+
Color.Blue, metallic = 0.8f, roughness = 0.2f
|
|
77
|
+
),
|
|
78
|
+
position = Position(x = 0.6f)
|
|
79
|
+
)
|
|
80
|
+
// Floor plane
|
|
81
|
+
PlaneNode(
|
|
82
|
+
engine,
|
|
83
|
+
size = Size(2f, 0f, 2f),
|
|
84
|
+
materialInstance = materialLoader.createColorInstance(
|
|
85
|
+
Color(0xFF4CAF50), metallic = 0f, roughness = 0.9f
|
|
86
|
+
),
|
|
87
|
+
position = Position(y = -0.35f)
|
|
88
|
+
)
|
|
89
|
+
}
|
|
33
90
|
}`,
|
|
34
91
|
},
|
|
35
92
|
"ar-tap-to-place": {
|
|
36
93
|
id: "ar-tap-to-place",
|
|
37
94
|
title: "AR Tap-to-Place",
|
|
38
95
|
description: "AR scene where each tap places a GLB model on a detected surface. Placed models are pinch-to-scale and drag-to-rotate.",
|
|
96
|
+
tags: ["ar", "model", "anchor", "plane-detection", "placement", "gestures"],
|
|
39
97
|
dependency: "io.github.sceneview:arsceneview:3.0.0",
|
|
40
98
|
prompt: "Create an Android Compose screen called `TapToPlaceScreen` that opens the camera in AR mode. Show a plane detection grid. When the user taps a detected surface, place a 3D GLB model from assets/models/chair.glb at that point. The user should be able to pinch-to-scale and drag-to-rotate after placing. Multiple taps = multiple objects. Use SceneView `io.github.sceneview:arsceneview:3.0.0`.",
|
|
41
99
|
code: `@Composable
|
|
@@ -79,6 +137,7 @@ fun TapToPlaceScreen() {
|
|
|
79
137
|
id: "ar-placement-cursor",
|
|
80
138
|
title: "AR Placement Cursor",
|
|
81
139
|
description: "AR scene with a reticle that follows the surface at screen center. Tap to confirm placement.",
|
|
140
|
+
tags: ["ar", "model", "anchor", "plane-detection", "placement", "camera"],
|
|
82
141
|
dependency: "io.github.sceneview:arsceneview:3.0.0",
|
|
83
142
|
prompt: "Create an Android Compose AR screen called `ARCursorScreen`. Show a small reticle that snaps to the nearest detected surface at the center of the screen as the user moves the camera. When the user taps, place a GLB model from assets/models/object.glb at that position and hide the reticle. Use SceneView `io.github.sceneview:arsceneview:3.0.0`.",
|
|
84
143
|
code: `@Composable
|
|
@@ -125,6 +184,7 @@ fun ARCursorScreen() {
|
|
|
125
184
|
id: "ar-augmented-image",
|
|
126
185
|
title: "AR Augmented Image",
|
|
127
186
|
description: "Detects a reference image in the camera feed and overlays a 3D model above it.",
|
|
187
|
+
tags: ["ar", "model", "anchor", "image-tracking"],
|
|
128
188
|
dependency: "io.github.sceneview:arsceneview:3.0.0",
|
|
129
189
|
prompt: "Create an Android Compose AR screen called `AugmentedImageScreen` that detects a printed reference image (from R.drawable.target_image, physical width 15 cm) and places a 3D GLB model from assets/models/overlay.glb above it, scaled to match the image width. The model should disappear when the image is lost. Use SceneView `io.github.sceneview:arsceneview:3.0.0`.",
|
|
130
190
|
code: `@Composable
|
|
@@ -167,6 +227,7 @@ fun AugmentedImageScreen() {
|
|
|
167
227
|
id: "ar-face-filter",
|
|
168
228
|
title: "AR Face Filter",
|
|
169
229
|
description: "Front-camera AR that detects faces and renders a 3D mesh material over them.",
|
|
230
|
+
tags: ["ar", "face-tracking", "camera"],
|
|
170
231
|
dependency: "io.github.sceneview:arsceneview:3.0.0",
|
|
171
232
|
prompt: "Create an Android Compose AR screen called `FaceFilterScreen` using the front camera. Detect all visible faces and apply a custom material from assets/materials/face_mask.filamat to the face mesh. Use SceneView `io.github.sceneview:arsceneview:3.0.0` with `Session.Feature.FRONT_CAMERA` and `AugmentedFaceMode.MESH3D`.",
|
|
172
233
|
code: `@Composable
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { SAMPLES, SAMPLE_IDS, getSample } from "./samples.js";
|
|
3
|
+
describe("SAMPLE_IDS", () => {
|
|
4
|
+
it("contains all 6 expected scenarios", () => {
|
|
5
|
+
expect(SAMPLE_IDS).toContain("model-viewer");
|
|
6
|
+
expect(SAMPLE_IDS).toContain("geometry-scene");
|
|
7
|
+
expect(SAMPLE_IDS).toContain("ar-tap-to-place");
|
|
8
|
+
expect(SAMPLE_IDS).toContain("ar-placement-cursor");
|
|
9
|
+
expect(SAMPLE_IDS).toContain("ar-augmented-image");
|
|
10
|
+
expect(SAMPLE_IDS).toContain("ar-face-filter");
|
|
11
|
+
});
|
|
12
|
+
it("SAMPLE_IDS matches keys of SAMPLES", () => {
|
|
13
|
+
expect(SAMPLE_IDS.sort()).toEqual(Object.keys(SAMPLES).sort());
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe("every sample", () => {
|
|
17
|
+
for (const id of SAMPLE_IDS) {
|
|
18
|
+
const sample = SAMPLES[id];
|
|
19
|
+
it(`${id}: has all required fields`, () => {
|
|
20
|
+
expect(sample.id).toBe(id);
|
|
21
|
+
expect(sample.title).toBeTruthy();
|
|
22
|
+
expect(sample.description).toBeTruthy();
|
|
23
|
+
expect(sample.tags.length).toBeGreaterThan(0);
|
|
24
|
+
expect(sample.dependency).toMatch(/^io\.github\.sceneview:/);
|
|
25
|
+
expect(sample.prompt).toBeTruthy();
|
|
26
|
+
expect(sample.code).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
it(`${id}: code is non-empty Kotlin`, () => {
|
|
29
|
+
expect(sample.code).toContain("@Composable");
|
|
30
|
+
expect(sample.code).toContain("fun ");
|
|
31
|
+
});
|
|
32
|
+
it(`${id}: dependency is a valid sceneview artifact`, () => {
|
|
33
|
+
expect(["io.github.sceneview:sceneview:3.0.0", "io.github.sceneview:arsceneview:3.0.0"]).toContain(sample.dependency);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
describe("AR samples", () => {
|
|
38
|
+
const arIds = SAMPLE_IDS.filter((id) => SAMPLES[id].tags.includes("ar"));
|
|
39
|
+
it("all AR samples use arsceneview dependency", () => {
|
|
40
|
+
for (const id of arIds) {
|
|
41
|
+
expect(SAMPLES[id].dependency).toBe("io.github.sceneview:arsceneview:3.0.0");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
it("all AR samples contain ARScene in code", () => {
|
|
45
|
+
for (const id of arIds) {
|
|
46
|
+
expect(SAMPLES[id].code).toContain("ARScene");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it("all AR samples have the 'ar' tag", () => {
|
|
50
|
+
for (const id of arIds) {
|
|
51
|
+
expect(SAMPLES[id].tags).toContain("ar");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("3D samples", () => {
|
|
56
|
+
const d3Ids = SAMPLE_IDS.filter((id) => SAMPLES[id].tags.includes("3d"));
|
|
57
|
+
it("all 3D samples use sceneview dependency", () => {
|
|
58
|
+
for (const id of d3Ids) {
|
|
59
|
+
expect(SAMPLES[id].dependency).toBe("io.github.sceneview:sceneview:3.0.0");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
it("all 3D samples contain Scene in code", () => {
|
|
63
|
+
for (const id of d3Ids) {
|
|
64
|
+
expect(SAMPLES[id].code).toContain("Scene(");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe("getSample", () => {
|
|
69
|
+
it("returns the correct sample by ID", () => {
|
|
70
|
+
const s = getSample("model-viewer");
|
|
71
|
+
expect(s).toBeDefined();
|
|
72
|
+
expect(s.id).toBe("model-viewer");
|
|
73
|
+
expect(s.title).toBe("3D Model Viewer");
|
|
74
|
+
});
|
|
75
|
+
it("returns undefined for an unknown ID", () => {
|
|
76
|
+
expect(getSample("nonexistent-scenario")).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
it("returns all samples without undefined", () => {
|
|
79
|
+
for (const id of SAMPLE_IDS) {
|
|
80
|
+
expect(getSample(id)).toBeDefined();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe("tag filtering (simulating list_samples tool)", () => {
|
|
85
|
+
const filterByTag = (tag) => Object.values(SAMPLES).filter((s) => s.tags.includes(tag));
|
|
86
|
+
it("tag 'ar' returns only AR samples", () => {
|
|
87
|
+
const results = filterByTag("ar");
|
|
88
|
+
expect(results.length).toBeGreaterThan(0);
|
|
89
|
+
results.forEach((s) => expect(s.tags).toContain("ar"));
|
|
90
|
+
});
|
|
91
|
+
it("tag '3d' returns only 3D samples", () => {
|
|
92
|
+
const results = filterByTag("3d");
|
|
93
|
+
expect(results.length).toBeGreaterThan(0);
|
|
94
|
+
results.forEach((s) => expect(s.tags).toContain("3d"));
|
|
95
|
+
});
|
|
96
|
+
it("tag 'face-tracking' returns only the face filter sample", () => {
|
|
97
|
+
const results = filterByTag("face-tracking");
|
|
98
|
+
expect(results).toHaveLength(1);
|
|
99
|
+
expect(results[0].id).toBe("ar-face-filter");
|
|
100
|
+
});
|
|
101
|
+
it("tag 'image-tracking' returns only augmented image sample", () => {
|
|
102
|
+
const results = filterByTag("image-tracking");
|
|
103
|
+
expect(results).toHaveLength(1);
|
|
104
|
+
expect(results[0].id).toBe("ar-augmented-image");
|
|
105
|
+
});
|
|
106
|
+
it("tag 'geometry' returns only geometry scene", () => {
|
|
107
|
+
const results = filterByTag("geometry");
|
|
108
|
+
expect(results).toHaveLength(1);
|
|
109
|
+
expect(results[0].id).toBe("geometry-scene");
|
|
110
|
+
});
|
|
111
|
+
it("tag 'anchor' returns AR samples that use anchors", () => {
|
|
112
|
+
const results = filterByTag("anchor");
|
|
113
|
+
expect(results.length).toBeGreaterThan(0);
|
|
114
|
+
results.forEach((s) => expect(s.tags).toContain("anchor"));
|
|
115
|
+
});
|
|
116
|
+
it("unknown tag returns empty array", () => {
|
|
117
|
+
expect(filterByTag("nonexistent-tag")).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
});
|