sceneview-mcp 4.0.12 → 4.0.14

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/sceneview-mcp?color=6c35aa)](https://www.npmjs.com/package/sceneview-mcp)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/sceneview-mcp?color=blue)](https://www.npmjs.com/package/sceneview-mcp)
7
- [![Tests](https://img.shields.io/badge/tests-2918%20passing-brightgreen)](#quality)
7
+ [![Tests](https://img.shields.io/badge/tests-1898%20passing-brightgreen)](#quality)
8
8
  [![MCP](https://img.shields.io/badge/MCP-v1.12-blue)](https://modelcontextprotocol.io/)
9
9
  [![Registry](https://img.shields.io/badge/MCP%20Registry-listed-blueviolet)](https://registry.modelcontextprotocol.io)
10
10
  [![License](https://img.shields.io/badge/License-MIT-green)](./LICENSE)
@@ -111,7 +111,7 @@ Every developer tool is **free**: setup guides for every platform, code samples,
111
111
 
112
112
  | Tool | What it does |
113
113
  |---|---|
114
- | `get_node_reference` | Full API reference for any of 35+ node types — exact signatures, defaults, examples |
114
+ | `get_node_reference` | Full API reference for any of 44+ node types — exact signatures, defaults, examples |
115
115
  | `list_platforms` | Supported platforms with their status, renderer, and framework |
116
116
  | `get_platform_roadmap` | Multi-platform status and timeline |
117
117
 
@@ -124,7 +124,10 @@ Every developer tool is **free**: setup guides for every platform, code samples,
124
124
  | Tool | What it does |
125
125
  |---|---|
126
126
  | `search_models` | Searches Sketchfab for free 3D models (BYOK — set `SKETCHFAB_API_KEY`) |
127
+ | `generate_3d_model` | Generates a brand-new GLB from a text prompt or image via Tripo AI (BYOK — set `TRIPO_API_KEY`) |
127
128
  | `analyze_project` | Scans a local SceneView project on disk — detects platform, extracts version, flags outdated deps and known anti-patterns |
129
+ | `search_android_docs` | Searches Google's stock Android docs knowledge base (needs the `android` CLI on PATH) |
130
+ | `fetch_android_doc` | Fetches a full Android docs entry by its `kb://...` URI (needs the `android` CLI on PATH) |
128
131
 
129
132
  ### 2 resources
130
133
 
@@ -159,6 +162,38 @@ Generated SceneView code is only useful if it points at an asset that actually e
159
162
 
160
163
  Call it like `search_models({ query: "red sports car", category: "cars-vehicles", maxResults: 6 })`. If the key is missing, the tool returns a clear message explaining how to get one instead of failing silently.
161
164
 
165
+ ## `generate_3d_model` — create brand-new 3D assets from the AI
166
+
167
+ When no existing model fits, `generate_3d_model` closes the other half of the asset loop: it generates a fresh GLB from a **text prompt** (text→3D) or a **source image** (image→3D) via the [Tripo AI](https://www.tripo3d.ai) API, then returns a direct GLB download URL plus license/attribution metadata — ready for `rememberModelInstance(modelLoader, ...)` and AR placement.
168
+
169
+ Two quality tiers:
170
+
171
+ | `quality` | Tripo model | Topology | Latency | Approx. cost (July 2026) |
172
+ |---|---|---|---|---|
173
+ | `"fast"` (default) | P1 (`P1-20260311`) | low-poly, AR-ready | ~25–30 s | ~$0.10–0.25 of your credits |
174
+ | `"hd"` | H3.1 (`v3.1-20260211`) | quad mesh, detailed geometry + textures | up to ~100 s | ~$0.41 of your credits |
175
+
176
+ **Bring your own key (BYOK).** Exactly like `search_models`: SceneView never proxies the request or holds your key — generations are billed to **your** Tripo account. To set it up:
177
+
178
+ 1. Create an API key at [platform.tripo3d.ai/api-keys](https://platform.tripo3d.ai/api-keys) (new accounts get free trial credits)
179
+ 2. Set `TRIPO_API_KEY` in your MCP client config:
180
+
181
+ ```json
182
+ {
183
+ "mcpServers": {
184
+ "sceneview": {
185
+ "command": "npx",
186
+ "args": ["-y", "sceneview-mcp"],
187
+ "env": { "TRIPO_API_KEY": "YOUR_KEY_HERE" }
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ Call it like `generate_3d_model({ prompt: "a low-poly cactus in a striped pot" })` or `generate_3d_model({ imageUrl: "https://example.com/chair.jpg", quality: "hd" })`. Provide exactly one of `prompt` / `imageUrl`.
194
+
195
+ **⚠️ The GLB download URL expires ~5 minutes after generation** — download the file immediately and self-host it (e.g. copy it into your app's `assets/models/`). The tool result repeats this warning. Missing key, task failures, rate limits, and poll timeouts (2 min fast / 4 min hd cap) all return clear, actionable messages instead of hanging or crashing.
196
+
162
197
  ## `analyze_project` — local project scan
163
198
 
164
199
  Because the MCP server runs on the user's machine, `analyze_project` can read their project files directly. Given a `path` (default: `process.cwd()`), it:
@@ -206,7 +241,7 @@ The assistant calls `validate_code` with the generated snippet and checks it aga
206
241
  - Always use the current SceneView 4.0.x API surface
207
242
  - Generate correct **Compose-native** 3D/AR code for Android
208
243
  - Generate correct **SwiftUI-native** code for iOS/macOS/visionOS
209
- - Know about all 35+ node types and their exact parameters
244
+ - Know about all 44+ node types and their exact parameters
210
245
  - Validate code against 15+ rules before presenting it
211
246
  - Provide working, tested sample code for 33 scenarios
212
247
 
@@ -214,7 +249,7 @@ The assistant calls `validate_code` with the generated snippet and checks it aga
214
249
 
215
250
  ## Quality
216
251
 
217
- The MCP server is tested with **2,918 unit tests** across 132 test suites covering:
252
+ The MCP server is tested with **1,898 unit tests** across 81 test suites covering:
218
253
 
219
254
  - Every tool response (correct output, error handling, edge cases)
220
255
  - All 33 code samples (compilable structure, correct imports, no deprecated APIs)
@@ -223,11 +258,11 @@ The MCP server is tested with **2,918 unit tests** across 132 test suites coveri
223
258
  - Resource responses (API reference, GitHub issues integration)
224
259
 
225
260
  ```
226
- Test Files 132 passed (132)
227
- Tests 2918 passed (2918)
261
+ Test Files 81 passed (81)
262
+ Tests 1898 passed (1898)
228
263
  ```
229
264
 
230
- All tools work **fully offline** except `sceneview://known-issues` (GitHub API, cached 10 min) and `search_models` (Sketchfab, BYOK).
265
+ All tools work **fully offline** except `sceneview://known-issues` (GitHub API, cached 10 min), `search_models` (Sketchfab, BYOK), and `generate_3d_model` (Tripo AI, BYOK).
231
266
 
232
267
  ---
233
268
 
@@ -251,7 +286,7 @@ Install Node.js from [nodejs.org](https://nodejs.org/) (LTS recommended). npm an
251
286
 
252
287
  ### Firewall or proxy issues
253
288
 
254
- The only network calls are to the GitHub API (for known issues) and Sketchfab (when `SKETCHFAB_API_KEY` is set). Everything else works offline.
289
+ The only network calls are to the GitHub API (for known issues), Sketchfab (when `SKETCHFAB_API_KEY` is set), and Tripo AI (when `TRIPO_API_KEY` is set and `generate_3d_model` is called). Everything else works offline.
255
290
 
256
291
  ```json
257
292
  {
@@ -302,7 +337,7 @@ Enabled by default on the free tier (MCP client name/version and tool names —
302
337
  cd mcp
303
338
  npm install
304
339
  npm run prepare # Copy llms.txt + build TypeScript
305
- npm test # 2918 tests
340
+ npm test # 1898 tests
306
341
  npm run dev # Start with tsx (hot reload)
307
342
  ```
308
343
 
@@ -323,6 +358,7 @@ mcp/
323
358
  artifact.ts # HTML artifact generator (model-viewer, charts, product 360)
324
359
  issues.ts # GitHub issues fetcher (cached)
325
360
  search-models.ts # Sketchfab BYOK search
361
+ generate-model.ts # Tripo BYOK text/image -> GLB generation
326
362
  analyze-project.ts # Local project scanner
327
363
  proxy.ts # Pro-tool proxy to hosted gateway
328
364
  llms.txt # Bundled API reference (copied from repo root)
@@ -333,7 +369,7 @@ mcp/
333
369
  1. Fork the repository
334
370
  2. Create a feature branch
335
371
  3. Add tests for new tools or rules
336
- 4. Run `npm test` — all 2918+ tests must pass
372
+ 4. Run `npm test` — all 1898+ tests must pass
337
373
  5. Submit a pull request
338
374
 
339
375
  See [CONTRIBUTING.md](../CONTRIBUTING.md) for the full guide.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * android-docs — `search_android_docs` / `fetch_android_doc` MCP tools.
3
+ *
4
+ * Google's `android` CLI (https://developer.android.com/tools/agents/android-cli)
5
+ * ships a `docs` subcommand backed by a knowledge base of ~4 800 stock Android
6
+ * documentation entries: Jetpack Compose, Camera2, ARCore SDK, Kotlin APIs,
7
+ * platform guides, and more. Wrapping it as MCP tools lets any MCP-aware
8
+ * assistant cross-reference stock Android docs with SceneView code without
9
+ * leaving the SceneView chat (issue #1083).
10
+ *
11
+ * - `search_android_docs(query)` → `android docs search <query>`
12
+ * - `fetch_android_doc(uri)` → `android docs fetch kb://<path>`
13
+ *
14
+ * Runtime dependency: the `android` CLI must be on the consumer's PATH. It is
15
+ * NOT a hard dependency of `sceneview-mcp` — most MCP hosts won't have it
16
+ * installed. Every code path here detects the binary up front and returns a
17
+ * structured, friendly error (never throws / crashes the MCP server) when it
18
+ * is absent.
19
+ *
20
+ * Hygiene ported from `.claude/scripts/lib/android-cli.sh`:
21
+ * - `--no-metrics` is passed on every invocation (keeps telemetry off and
22
+ * output clean).
23
+ * - The CLI prints a one-time terms-of-service blurb to stderr on its first
24
+ * invocation; we run a throwaway `--version` once per process to consume
25
+ * it so the real `docs` call returns clean stdout.
26
+ */
27
+ import { execFile } from "node:child_process";
28
+ // ─── Configuration ──────────────────────────────────────────────────────────
29
+ /** Global flags applied to every `android` invocation (telemetry off). */
30
+ const ANDROID_CLI_GLOBAL_FLAGS = ["--no-metrics"];
31
+ /** Hard cap on a single `android docs` call so a hung CLI can't wedge MCP. */
32
+ const ANDROID_CLI_TIMEOUT_MS = 20_000;
33
+ /** Cap the captured stdout/stderr so a pathological response can't blow memory. */
34
+ const ANDROID_CLI_MAX_BUFFER = 4 * 1024 * 1024; // 4 MB
35
+ const INSTALL_URL = "https://developer.android.com/tools/agents/android-cli";
36
+ // ─── CLI detection ───────────────────────────────────────────────────────────
37
+ /**
38
+ * Whether the first-run ToS blurb has already been consumed for this process.
39
+ * The `android` CLI prints its terms-of-service notice to stderr exactly once
40
+ * per host; running any command absorbs it. We do it lazily, once.
41
+ */
42
+ let tosConsumed = false;
43
+ /** Cached binary-presence result for the lifetime of the MCP process. */
44
+ let cliPresenceCache;
45
+ /**
46
+ * Promisified `execFile` returning stdout + stderr, never rejecting.
47
+ * `error` is non-null when the process exits non-zero, is killed, or cannot
48
+ * be spawned (`ENOENT` when the binary is absent).
49
+ */
50
+ function run(file, args) {
51
+ return new Promise((resolve) => {
52
+ execFile(file, args, { timeout: ANDROID_CLI_TIMEOUT_MS, maxBuffer: ANDROID_CLI_MAX_BUFFER, encoding: "utf8" }, (error, stdout, stderr) => {
53
+ resolve({
54
+ error: error ?? null,
55
+ stdout: stdout ?? "",
56
+ stderr: stderr ?? "",
57
+ });
58
+ });
59
+ });
60
+ }
61
+ /**
62
+ * Detect the `android` CLI on PATH. Result is cached for the process lifetime
63
+ * and also consumes the one-time ToS blurb on first success.
64
+ *
65
+ * Exported for tests; production callers go through `runAndroidDocs`.
66
+ */
67
+ export async function isAndroidCliAvailable() {
68
+ if (cliPresenceCache !== undefined)
69
+ return cliPresenceCache;
70
+ const { error } = await run("android", [...ANDROID_CLI_GLOBAL_FLAGS, "--version"]);
71
+ // ENOENT (binary not found) ⇒ unavailable. Any other exit still means the
72
+ // binary spawned, so it IS present — and that --version run has now consumed
73
+ // the first-run ToS notice.
74
+ const spawnFailed = error?.code === "ENOENT";
75
+ cliPresenceCache = !spawnFailed;
76
+ if (cliPresenceCache)
77
+ tosConsumed = true;
78
+ return cliPresenceCache;
79
+ }
80
+ /** Test-only: reset the process-scoped CLI detection / ToS caches. */
81
+ export function __resetAndroidCliCache() {
82
+ cliPresenceCache = undefined;
83
+ tosConsumed = false;
84
+ }
85
+ function cliMissingError() {
86
+ return {
87
+ ok: false,
88
+ code: "cli_missing",
89
+ message: [
90
+ "This tool needs Google's `android` CLI, which is not installed on this MCP host.",
91
+ "",
92
+ "`android docs` is an optional runtime dependency — `sceneview-mcp` works fine without it,",
93
+ "but `search_android_docs` / `fetch_android_doc` cannot run until the CLI is on PATH.",
94
+ "",
95
+ `Install it from ${INSTALL_URL} (or run \`bash .claude/scripts/android-env-check.sh --fix\``,
96
+ "from a SceneView checkout), then retry.",
97
+ ].join("\n"),
98
+ };
99
+ }
100
+ // ─── Core runner ─────────────────────────────────────────────────────────────
101
+ /**
102
+ * Run an `android docs <subcommand> <arg>` invocation and return its stdout
103
+ * as a structured result. Never throws.
104
+ */
105
+ async function runAndroidDocs(subcommand, arg) {
106
+ if (!(await isAndroidCliAvailable())) {
107
+ return cliMissingError();
108
+ }
109
+ // Belt-and-braces: ensure the first-run ToS notice is consumed. Normally
110
+ // `isAndroidCliAvailable()` already did this via its `--version` probe, but
111
+ // a caller could have populated `cliPresenceCache` some other way.
112
+ if (!tosConsumed) {
113
+ await run("android", [...ANDROID_CLI_GLOBAL_FLAGS, "--version"]);
114
+ tosConsumed = true;
115
+ }
116
+ const { error, stdout, stderr } = await run("android", [
117
+ ...ANDROID_CLI_GLOBAL_FLAGS,
118
+ "docs",
119
+ subcommand,
120
+ arg,
121
+ ]);
122
+ if (error) {
123
+ // `execFile` sets `killed` + `signal` on a timeout kill.
124
+ const killedByTimeout = error.killed === true ||
125
+ error.signal === "SIGTERM";
126
+ if (killedByTimeout) {
127
+ return {
128
+ ok: false,
129
+ code: "timeout",
130
+ message: `\`android docs ${subcommand}\` did not finish within ${ANDROID_CLI_TIMEOUT_MS / 1000}s.`,
131
+ };
132
+ }
133
+ const detail = (stderr || stdout || error.message).trim();
134
+ return {
135
+ ok: false,
136
+ code: "cli_error",
137
+ message: `\`android docs ${subcommand}\` failed: ${detail}`,
138
+ };
139
+ }
140
+ return { ok: true, output: stdout.trim() };
141
+ }
142
+ // ─── Public API: search ──────────────────────────────────────────────────────
143
+ /**
144
+ * Search the stock Android documentation knowledge base.
145
+ *
146
+ * @param query Free-text query, e.g. `"LazyColumn paging"`.
147
+ */
148
+ export async function searchAndroidDocs(query) {
149
+ if (typeof query !== "string" || query.trim().length === 0) {
150
+ return {
151
+ ok: false,
152
+ code: "invalid_input",
153
+ message: "Missing required parameter: `query` must be a non-empty string.",
154
+ };
155
+ }
156
+ return runAndroidDocs("search", query.trim());
157
+ }
158
+ // ─── Public API: fetch ───────────────────────────────────────────────────────
159
+ /**
160
+ * Fetch a single Android documentation entry by its knowledge-base URI.
161
+ *
162
+ * @param uri A `kb://...` URI as returned by `search_android_docs`. A bare
163
+ * path (no scheme) is tolerated and normalised to `kb://`.
164
+ */
165
+ export async function fetchAndroidDoc(uri) {
166
+ if (typeof uri !== "string" || uri.trim().length === 0) {
167
+ return {
168
+ ok: false,
169
+ code: "invalid_input",
170
+ message: "Missing required parameter: `uri` must be a non-empty `kb://...` string.",
171
+ };
172
+ }
173
+ let normalised = uri.trim();
174
+ if (!normalised.startsWith("kb://")) {
175
+ // Tolerate a bare path or a leading slash — normalise to a kb:// URI so an
176
+ // assistant that drops the scheme still gets a result.
177
+ normalised = `kb://${normalised.replace(/^\/+/, "")}`;
178
+ }
179
+ return runAndroidDocs("fetch", normalised);
180
+ }
181
+ // ─── Formatting ──────────────────────────────────────────────────────────────
182
+ /** Render a search result as the markdown text block the MCP dispatcher returns. */
183
+ export function formatAndroidDocsSearch(query, result) {
184
+ if (!result.ok)
185
+ return result.message;
186
+ if (result.output.length === 0) {
187
+ return `No Android documentation entries found for "${query}". Try a broader query.`;
188
+ }
189
+ return [
190
+ `## Android docs — search results for "${query}"`,
191
+ "",
192
+ result.output,
193
+ "",
194
+ "---",
195
+ "Use `fetch_android_doc` with a `kb://...` URI above to read a full entry.",
196
+ ].join("\n");
197
+ }
198
+ /** Render a fetch result as the markdown text block the MCP dispatcher returns. */
199
+ export function formatAndroidDocsFetch(uri, result) {
200
+ if (!result.ok)
201
+ return result.message;
202
+ if (result.output.length === 0) {
203
+ return `No Android documentation entry found at \`${uri}\`.`;
204
+ }
205
+ return [`## Android docs — \`${uri}\``, "", result.output].join("\n");
206
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Inline example resources exposed via MCP `examples://` URIs.
3
+ *
4
+ * Two resources, intentionally inline (no fs reads / no fetches) so the
5
+ * `sceneview-mcp` package stays a pure code dependency:
6
+ *
7
+ * - `examples://demo-with-settings` — DemoScaffold v2 pattern (PR #1169 under
8
+ * issue #1154). Full-screen 3D / AR scene + ModalBottomSheet controls
9
+ * + horizontal FilterChip picker row.
10
+ *
11
+ * - `examples://sketchfab-streaming` — `SketchfabAssetResolver` pattern
12
+ * (Stage 2 of umbrella issue #1152). Stream CC-BY models from Sketchfab
13
+ * with per-slug bundled fallback + LRU cache + bounds sanity check.
14
+ *
15
+ * These strings deliberately stay small (≤ 4 KB each) so the resource list
16
+ * doesn't bloat the client's context window. The full recipes live at
17
+ * `docs/docs/recipes/sketchfab-streaming.md` and
18
+ * `docs/docs/recipes/demo-settings-sheet.md`.
19
+ */
20
+ export const DEMO_WITH_SETTINGS_EXAMPLE = `# Example — DemoScaffold v2 (full-screen scene + ModalBottomSheet controls)
21
+
22
+ \`DemoScaffold\` v2 is the shared scaffold every SceneView sample-app demo uses. It renders the 3D / AR scene **full-screen** under the top bar, with a \`Tune\` FAB pinned bottom-right that opens a Material 3 ModalBottomSheet containing the controls.
23
+
24
+ \`\`\`kotlin
25
+ @Composable
26
+ fun MyDemo(onBack: () -> Unit) {
27
+ var iblIntensity by remember { mutableFloatStateOf(5_000f) }
28
+ var spinScene by remember { mutableStateOf(true) }
29
+ val engine = rememberEngine()
30
+ val modelLoader = rememberModelLoader(engine)
31
+
32
+ DemoScaffold(
33
+ title = "My Demo",
34
+ onBack = onBack,
35
+ controls = {
36
+ // Same Column scope you'd use in any settings sheet.
37
+ Text(
38
+ "IBL intensity: \${iblIntensity.toInt()} lux",
39
+ style = MaterialTheme.typography.labelLarge,
40
+ )
41
+ Slider(
42
+ value = iblIntensity,
43
+ onValueChange = { iblIntensity = it },
44
+ valueRange = 0f..10_000f,
45
+ )
46
+ Spacer(modifier = Modifier.height(8.dp))
47
+ Row(
48
+ modifier = Modifier.fillMaxWidth(),
49
+ horizontalArrangement = Arrangement.SpaceBetween,
50
+ verticalAlignment = Alignment.CenterVertically,
51
+ ) {
52
+ Text("Spin scene", style = MaterialTheme.typography.bodyMedium)
53
+ Switch(checked = spinScene, onCheckedChange = { spinScene = it })
54
+ }
55
+ },
56
+ ) {
57
+ // BoxScope — full-screen scene.
58
+ SceneView(
59
+ modifier = Modifier.fillMaxSize(),
60
+ engine = engine,
61
+ modelLoader = modelLoader,
62
+ ) {
63
+ // … nodes go here.
64
+ }
65
+ }
66
+ }
67
+ \`\`\`
68
+
69
+ **Gestures.** Tap FAB → opens the sheet. Tap peek chip ("Settings", above the FAB) → opens the sheet (discoverability — added under issue #951). Long-press peek chip → toggles \`DemoSettings.qaMode\` for deterministic screenshot captures. Drag handle / outside tap / back → dismiss. AR sessions keep tracking underneath.
70
+
71
+ **Picker pattern.** Combine with the FilterChip horizontal row for bundled-vs-streamed asset selection — see \`examples://sketchfab-streaming\`.
72
+
73
+ **Full doc:** \`docs/docs/recipes/demo-settings-sheet.md\` (PR #1169, issue #1154).
74
+ `;
75
+ export const SKETCHFAB_STREAMING_EXAMPLE = `# Example — Stream Sketchfab CC-BY models into a SceneView demo
76
+
77
+ The sample app (\`samples/android-demo\`) streams CC-BY licensed glTF models from Sketchfab on demand instead of bundling 30 MB of GLBs in the APK. The same pattern works in any SceneView consumer.
78
+
79
+ \`\`\`kotlin
80
+ @Composable
81
+ fun MyStreamedDemo(onBack: () -> Unit) {
82
+ val context = LocalContext.current
83
+ val resolver = remember { SketchfabAssetResolver.getInstance(context) }
84
+ val engine = rememberEngine()
85
+ val modelLoader = rememberModelLoader(engine)
86
+
87
+ // Warm the cache so the first frame doesn't pop in. Concurrent calls for
88
+ // the same slug deduplicate at the service layer.
89
+ LaunchedEffect(Unit) {
90
+ runCatching { resolver.prefetchAll("animation") }
91
+ }
92
+
93
+ // Pick a slug from the curated registry — categories: solar, gallery,
94
+ // animation, park, ar_placement, physics, materials.
95
+ val slug = remember { SampleAssets.byCategory["animation"].orEmpty().first() }
96
+
97
+ // Resolve to a local file (null while downloading / staging the fallback).
98
+ val file: File? by produceState<File?>(initialValue = null, key1 = slug.uid) {
99
+ value = runCatching { resolver.resolve(slug) }.getOrNull()
100
+ }
101
+
102
+ val instance = file?.let {
103
+ rememberModelInstance(modelLoader, "file://\${it.absolutePath}")
104
+ }
105
+
106
+ DemoScaffold(title = slug.displayName, onBack = onBack) {
107
+ SceneView(
108
+ modifier = Modifier.fillMaxSize(),
109
+ engine = engine,
110
+ modelLoader = modelLoader,
111
+ ) {
112
+ instance?.let {
113
+ ModelNode(
114
+ modelInstance = it,
115
+ scaleToUnits = slug.scaleToUnits,
116
+ autoAnimate = slug.hasBakedAnimation,
117
+ )
118
+ }
119
+ }
120
+ }
121
+ }
122
+ \`\`\`
123
+
124
+ **Hard rules.**
125
+
126
+ - **CC-BY 4.0 only.** \`SketchfabSlug\`'s constructor rejects non-CC-BY models so the registry can't silently regress.
127
+ - **No Sketchfab WebView / external link.** All loading is in-process; Sketchfab is an invisible CDN, not a UX surface.
128
+ - **Never ship a build that needs the network to render something.** The resolver returns a bundled fallback when \`SketchfabConfig.apiKey == null\` or the download fails.
129
+ - **Attribute the author.** Every streamed model surfaced in the UI must show \`slug.author\` — CC-BY 4.0 attribution requirement.
130
+
131
+ **LRU cache.** \`Context.cacheDir/sketchfab/\` (250 MB samples-side cap, evicted oldest-first by \`lastModified\`).
132
+
133
+ **Bounds sanity check.** The resolver verifies the \`glTF\` magic header + size ≥ 12 B before returning a streamed file. Truncated downloads / wrong-format payloads fall back to the bundled asset.
134
+
135
+ **Full doc:** \`docs/docs/recipes/sketchfab-streaming.md\` (umbrella issue #1152, Stage 1 PR #1168).
136
+ `;