sceneview-mcp 4.0.11 → 4.0.13

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
@@ -45,6 +45,17 @@ Restart Claude Desktop after saving.
45
45
 
46
46
  ### Claude Code
47
47
 
48
+ Two options.
49
+
50
+ **Recommended — install the [SceneView Claude Code plugin](https://github.com/sceneview/claude-marketplace)** to get this MCP server **plus** 11 namespaced contributor commands and cross-platform reminder hooks in one shot:
51
+
52
+ ```bash
53
+ /plugin marketplace add sceneview/claude-marketplace
54
+ /plugin install sceneview@sceneview
55
+ ```
56
+
57
+ **Or — just the MCP server** (lighter, no commands or hooks):
58
+
48
59
  ```bash
49
60
  claude mcp add sceneview -- npx -y sceneview-mcp
50
61
  ```
@@ -100,7 +111,7 @@ Every developer tool is **free**: setup guides for every platform, code samples,
100
111
 
101
112
  | Tool | What it does |
102
113
  |---|---|
103
- | `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 42+ node types — exact signatures, defaults, examples |
104
115
  | `list_platforms` | Supported platforms with their status, renderer, and framework |
105
116
  | `get_platform_roadmap` | Multi-platform status and timeline |
106
117
 
@@ -114,6 +125,8 @@ Every developer tool is **free**: setup guides for every platform, code samples,
114
125
  |---|---|
115
126
  | `search_models` | Searches Sketchfab for free 3D models (BYOK — set `SKETCHFAB_API_KEY`) |
116
127
  | `analyze_project` | Scans a local SceneView project on disk — detects platform, extracts version, flags outdated deps and known anti-patterns |
128
+ | `search_android_docs` | Searches Google's stock Android docs knowledge base (needs the `android` CLI on PATH) |
129
+ | `fetch_android_doc` | Fetches a full Android docs entry by its `kb://...` URI (needs the `android` CLI on PATH) |
117
130
 
118
131
  ### 2 resources
119
132
 
@@ -195,7 +208,7 @@ The assistant calls `validate_code` with the generated snippet and checks it aga
195
208
  - Always use the current SceneView 4.0.x API surface
196
209
  - Generate correct **Compose-native** 3D/AR code for Android
197
210
  - Generate correct **SwiftUI-native** code for iOS/macOS/visionOS
198
- - Know about all 35+ node types and their exact parameters
211
+ - Know about all 42+ node types and their exact parameters
199
212
  - Validate code against 15+ rules before presenting it
200
213
  - Provide working, tested sample code for 33 scenarios
201
214
 
@@ -21,9 +21,16 @@
21
21
  */
22
22
  import { promises as fs } from "node:fs";
23
23
  import path from "node:path";
24
+ import { LATEST_SCENEVIEW_RELEASE } from "./generated/version.js";
24
25
  // ─── Constants ───────────────────────────────────────────────────────────────
25
- /** The latest SceneView release known to this build of the MCP. */
26
- export const LATEST_SCENEVIEW_VERSION = "4.0.0";
26
+ /**
27
+ * The latest SceneView release known to this build of the MCP, snapshotted
28
+ * from the root `gradle.properties:VERSION_NAME` at build time via
29
+ * `scripts/generate-version.js`. See #941 — pre-fix this was hardcoded to
30
+ * "4.0.0" and never bumped, so every install of every MCP version told the
31
+ * LLM that "4.0.0" was current even when the real SDK was at 4.0.9.
32
+ */
33
+ export const LATEST_SCENEVIEW_VERSION = LATEST_SCENEVIEW_RELEASE;
27
34
  /** Hard cap on the number of source files inspected per call. */
28
35
  export const MAX_FILES_SCANNED = 30;
29
36
  /** Hard cap on total bytes read across all source files per call (500 KB). */
@@ -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
+ }
package/dist/artifact.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // - scene: multi-model 3D scene with lighting and environment (Filament.js)
13
13
  // - product-360: product turntable with hotspot annotations (Filament.js)
14
14
  // ─── Constants ───────────────────────────────────────────────────────────────
15
- const FILAMENT_CDN = "https://cdn.jsdelivr.net/npm/filament@1.70.1/filament.js";
15
+ const FILAMENT_CDN = "https://cdn.jsdelivr.net/npm/filament@1.70.2/filament.js";
16
16
  const DEFAULT_MODEL = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/DamagedHelmet/glTF-Binary/DamagedHelmet.glb";
17
17
  const DEFAULT_COLORS = [
18
18
  "#4285F4", "#EA4335", "#FBBC04", "#34A853", "#FF6D01",
@@ -1,9 +1,4 @@
1
- /**
2
- * debug-issue.ts
3
- *
4
- * Targeted debugging guide for common SceneView issues.
5
- * Given a symptom, returns a focused diagnostic checklist.
6
- */
1
+ import { LATEST_SCENEVIEW_RELEASE } from "./generated/version.js";
7
2
  export const DEBUG_CATEGORIES = [
8
3
  "model-not-showing",
9
4
  "ar-not-working",
@@ -305,7 +300,7 @@ fun DebugModelViewer() {
305
300
  title: "Build / Gradle Errors",
306
301
  guide: `## Debugging: Build Errors
307
302
 
308
- ### "Cannot resolve io.github.sceneview:sceneview:4.0.0"
303
+ ### "Cannot resolve io.github.sceneview:sceneview:${LATEST_SCENEVIEW_RELEASE}"
309
304
 
310
305
  1. Check repositories in \`settings.gradle.kts\`:
311
306
  \`\`\`kotlin
@@ -348,7 +343,7 @@ SceneView bundles Filament. If you also depend on Filament directly:
348
343
  \`\`\`kotlin
349
344
  // Remove direct Filament dependency — SceneView includes it
350
345
  // implementation("com.google.android.filament:filament-android:1.x.x") // REMOVE
351
- implementation("io.github.sceneview:sceneview:4.0.0") // This includes Filament
346
+ implementation("io.github.sceneview:sceneview:${LATEST_SCENEVIEW_RELEASE}") // This includes Filament
352
347
  \`\`\`
353
348
 
354
349
  ### "Cannot find Filament material"
@@ -672,10 +667,40 @@ export function autoDetectIssue(description) {
672
667
  if (lower.includes("wrong thread") || lower.includes("off main thread") || lower.includes("dispatchers.io") || lower.includes("background thread")) {
673
668
  return "crash";
674
669
  }
675
- if (lower.includes("not showing") || lower.includes("invisible") || lower.includes("can't see") || lower.includes("model doesn't appear") || lower.includes("model not visible") || lower.includes("nothing shows up") || lower.includes("model is null") || lower.includes("remembermodelinstance returns null")) {
670
+ if (lower.includes("not showing") || lower.includes("invisible") || lower.includes("can't see") || lower.includes("model doesn't appear") || lower.includes("model not visible") || lower.includes("nothing shows up") || lower.includes("model is null") || lower.includes("remembermodelinstance returns null") || lower.includes("no model")) {
676
671
  return "model-not-showing";
677
672
  }
678
- if (lower.includes("ar not") || lower.includes("ar doesn't") || lower.includes("arcore") || lower.includes("plane") || lower.includes("anchor") || lower.includes("camera permission") || lower.includes("augmented reality") || lower.includes("hit test") || lower.includes("hitresult")) {
673
+ // AR-not-working catches "the AR camera feed is dark" and the half-dozen
674
+ // ways a user describes a non-functional AR session. Pre-#940 only "ar not",
675
+ // "ar doesn't", and the technical terms (arcore/plane/anchor/hit test)
676
+ // matched — the natural phrasings "ar camera is black", "my AR is black",
677
+ // "ARScene shows nothing", etc. fell through to null.
678
+ //
679
+ // The regex `\b(ar|arscene|arsceneview|arcore)\b.*\b(black|dark)\b` catches
680
+ // any sentence where "AR" (in any of its forms) and a "no signal" keyword
681
+ // both appear, independent of the connecting words ("is", "feed is",
682
+ // "camera was", etc.). "dark" is added per the #940 review — "AR mode is
683
+ // dark" / "AR feed dimmed" are common synonyms users reach for.
684
+ //
685
+ // The bare `\bcamera\b.*\b(black|dark)\b` check is NOW gated on the
686
+ // sentence containing an AR-flavoured token — without that gate it
687
+ // false-positives on "the orbit camera in my 3D scene renders a black
688
+ // background" (a 3D-only issue that should route to model-not-showing
689
+ // or material). Caught by the #940 follow-up review.
690
+ const hasArContext = /\b(ar|arscene|arsceneview|arcore|arkit|arcore)\b/.test(lower)
691
+ || lower.includes("augmented reality");
692
+ const arBlackHints = (hasArContext && /\b(black|dark|dimmed)\b/.test(lower))
693
+ || /\b(ar|arscene|arsceneview|arcore)\b.*\b(black|dark|dimmed)\b/.test(lower);
694
+ if (lower.includes("ar not") ||
695
+ lower.includes("ar doesn't") ||
696
+ lower.includes("arcore") ||
697
+ lower.includes("plane") ||
698
+ lower.includes("anchor") ||
699
+ lower.includes("camera permission") ||
700
+ lower.includes("augmented reality") ||
701
+ lower.includes("hit test") ||
702
+ lower.includes("hitresult") ||
703
+ arBlackHints) {
679
704
  return "ar-not-working";
680
705
  }
681
706
  if (lower.includes("crash") || lower.includes("sigabrt") || lower.includes("native crash") || lower.includes("fatal") || lower.includes("exception") || lower.includes("destroy") || lower.includes("double free") || lower.includes("segfault") || (lower.includes("oom") && !lower.includes("zoom")) || lower.includes("out of memory") || lower.includes("nullpointerexception") || lower.includes("npe")) {
@@ -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
+ `;
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Material, collision, model optimization, and web rendering guides.
5
5
  */
6
+ import { LATEST_SCENEVIEW_RELEASE } from "./generated/version.js";
6
7
  // ─── Material Guide ─────────────────────────────────────────────────────────
7
8
  export const MATERIAL_GUIDE = `# SceneView Material & Shader Guide
8
9
 
@@ -406,7 +407,7 @@ export const WEB_RENDERING_GUIDE = `# SceneView Web Rendering Guide (Filament.js
406
407
 
407
408
  ## Architecture
408
409
 
409
- SceneView Web uses **Filament.js v1.70.1** — Google's Filament engine compiled to WebAssembly. This is the **same PBR rendering engine** as SceneView Android, ensuring visual parity.
410
+ SceneView Web uses **Filament.js v1.70.2** — Google's Filament engine compiled to WebAssembly. This is the **same PBR rendering engine** as SceneView Android, ensuring visual parity.
410
411
 
411
412
  \`\`\`
412
413
  Browser → WebGL2 → Filament.js (WASM) → GPU
@@ -419,7 +420,7 @@ Browser → WebGL2 → Filament.js (WASM) → GPU
419
420
  ### Using sceneview.js (npm or local)
420
421
  \`\`\`html
421
422
  <!-- Option 1: npm CDN -->
422
- <script src="https://cdn.jsdelivr.net/npm/sceneview-web@4.0.0/sceneview.js"></script>
423
+ <script src="https://cdn.jsdelivr.net/npm/sceneview-web@${LATEST_SCENEVIEW_RELEASE}/sceneview-web.js"></script>
423
424
 
424
425
  <!-- Option 2: local hosting (recommended for production) -->
425
426
  <!-- Copy js/filament/ directory to your server for faster WASM loading -->
@@ -517,7 +518,7 @@ camera {
517
518
 
518
519
  | Feature | SceneView (Filament.js) | model-viewer |
519
520
  |---------|------------------------|--------------|
520
- | **Engine** | Filament v1.70.1 WASM | Filament WASM (same engine) |
521
+ | **Engine** | Filament v1.70.2 WASM | Filament WASM (same engine) |
521
522
  | **Bundle size** | ~215KB JS + 3.3MB WASM | ~800 KB (subset) |
522
523
  | **Procedural geometry** | Yes (cubes, spheres, etc.) | No |
523
524
  | **Custom materials** | Yes (full Filament API) | Limited |
@@ -1,11 +1,3 @@
1
- /**
2
- * generate-scene.ts
3
- *
4
- * Generates a complete SceneView{} or ARSceneView{} composable from a text description.
5
- * Maps common objects/concepts to SceneView node types and builds compilable code.
6
- *
7
- * All generated code targets SceneView v4.0.0 API and is verified against llms.txt.
8
- */
9
1
  const OBJECT_MAPPINGS = [
10
2
  // Furniture
11
3
  { keywords: ["table"], nodeType: "CubeNode", geometryType: "cube", defaultScale: 1.0, defaultPosition: [0, 0.4, 0], color: "Color(0.55f, 0.35f, 0.17f)", comment: "Table (flat cube)" },
@@ -245,7 +237,7 @@ export function generateScene(description) {
245
237
  }
246
238
  // Build the code
247
239
  const isAR = parsed.isAR;
248
- dependencies.push(isAR ? "io.github.sceneview:arsceneview:4.0.0" : "io.github.sceneview:sceneview:4.0.0");
240
+ dependencies.push(isAR ? "io.github.sceneview:arsceneview:${LATEST_SCENEVIEW_RELEASE}" : "io.github.sceneview:sceneview:${LATEST_SCENEVIEW_RELEASE}");
249
241
  // Build model instance declarations
250
242
  const modelElements = elements.filter((e) => e.type === "model");
251
243
  const uniqueModels = new Map();