sceneview-mcp 4.0.13 → 4.0.15
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 +49 -15
- package/dist/generate-model.js +402 -0
- package/dist/generated/llms-txt.js +1 -1
- package/dist/generated/symbols.js +8622 -0
- package/dist/generated/version.js +2 -2
- package/dist/guides.js +1 -1
- package/dist/platform-setup.js +7 -16
- package/dist/symbols.js +204 -0
- package/dist/tiers.js +26 -3
- package/dist/tools/definitions.js +29 -1
- package/dist/tools/handler.js +15 -1
- package/dist/validator.js +199 -0
- package/package.json +9 -7
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
// build time so the MCP server, telemetry, and the install-snippet
|
|
5
5
|
// generators all report the actually-published versions instead of
|
|
6
6
|
// stale hardcoded constants. See #941.
|
|
7
|
-
export const PACKAGE_VERSION = "4.0.
|
|
8
|
-
export const LATEST_SCENEVIEW_RELEASE = "4.
|
|
7
|
+
export const PACKAGE_VERSION = "4.0.15";
|
|
8
|
+
export const LATEST_SCENEVIEW_RELEASE = "4.25.0";
|
package/dist/guides.js
CHANGED
|
@@ -33,7 +33,7 @@ KMP shares **logic**, not **rendering**. Each platform uses its native renderer.
|
|
|
33
33
|
- **3D rendering** via Google Filament: PBR materials, HDR environments, glTF/GLB models, post-processing.
|
|
34
34
|
- **AR** via ARCore: plane detection, hit testing, anchors, cloud anchors, augmented images, depth, light estimation, point cloud.
|
|
35
35
|
- **Compose-native DSL**: all nodes are \`@Composable\` functions inside \`SceneView { }\` or \`ARSceneView { }\`.
|
|
36
|
-
- **
|
|
36
|
+
- **44+ node types**: ModelNode, LightNode, AnchorNode, CameraNode, TextNode, PathNode, ViewNode, PlaneNode, SphereNode, CylinderNode, CubeNode, DynamicSkyNode, FogNode, ReflectionProbeNode, PhysicsNode, BillboardNode, LineNode, and more.
|
|
37
37
|
|
|
38
38
|
## Apple — Alpha (SceneViewSwift)
|
|
39
39
|
|
package/dist/platform-setup.js
CHANGED
|
@@ -52,8 +52,7 @@ fun My3DScreen() {
|
|
|
52
52
|
rememberModelInstance(modelLoader, "models/chair.glb")?.let { instance ->
|
|
53
53
|
ModelNode(
|
|
54
54
|
modelInstance = instance,
|
|
55
|
-
scaleToUnits = 1.0f
|
|
56
|
-
centerOrigin = Position(0f, 0f, 0f)
|
|
55
|
+
scaleToUnits = 1.0f
|
|
57
56
|
)
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -415,18 +414,14 @@ SceneView Flutter uses **PlatformView** to embed native SceneView (Android: Fila
|
|
|
415
414
|
|
|
416
415
|
### 1. Dependencies
|
|
417
416
|
|
|
418
|
-
> **Note:** the
|
|
419
|
-
> (
|
|
420
|
-
>
|
|
417
|
+
> **Note:** the plugin is published on pub.dev as \`flutter_sceneview\`
|
|
418
|
+
> (the pub.dev names \`sceneview\` and \`sceneview_flutter\` are unrelated
|
|
419
|
+
> third-party uploads — do not use them).
|
|
421
420
|
|
|
422
421
|
\`\`\`yaml
|
|
423
422
|
# pubspec.yaml
|
|
424
423
|
dependencies:
|
|
425
|
-
|
|
426
|
-
git:
|
|
427
|
-
url: https://github.com/sceneview/sceneview
|
|
428
|
-
path: flutter/sceneview_flutter
|
|
429
|
-
ref: v${LATEST_SCENEVIEW_RELEASE}
|
|
424
|
+
flutter_sceneview: ^${LATEST_SCENEVIEW_RELEASE}
|
|
430
425
|
\`\`\`
|
|
431
426
|
|
|
432
427
|
### 2. Android Setup
|
|
@@ -484,13 +479,9 @@ const FLUTTER_AR = `## SceneView Flutter — AR Setup
|
|
|
484
479
|
### 1. Dependencies
|
|
485
480
|
|
|
486
481
|
\`\`\`yaml
|
|
487
|
-
#
|
|
482
|
+
# pubspec.yaml — published on pub.dev (the names sceneview / sceneview_flutter are unrelated third-party uploads)
|
|
488
483
|
dependencies:
|
|
489
|
-
|
|
490
|
-
git:
|
|
491
|
-
url: https://github.com/sceneview/sceneview
|
|
492
|
-
path: flutter/sceneview_flutter
|
|
493
|
-
ref: v${LATEST_SCENEVIEW_RELEASE}
|
|
484
|
+
flutter_sceneview: ^${LATEST_SCENEVIEW_RELEASE}
|
|
494
485
|
\`\`\`
|
|
495
486
|
|
|
496
487
|
### 2. Android Manifest
|
package/dist/symbols.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol-existence lookups over the generated public-API index (#2760).
|
|
3
|
+
*
|
|
4
|
+
* The index (`src/generated/symbols.ts`) is parsed from the committed
|
|
5
|
+
* binary-compatibility `.api` dumps at build time — see
|
|
6
|
+
* `scripts/generate-symbols.js` for the full provenance chain. Everything
|
|
7
|
+
* here answers one question for the validator: "does this identifier exist
|
|
8
|
+
* in the real Android/KMP SceneView API?", with edit-distance suggestions
|
|
9
|
+
* when it doesn't.
|
|
10
|
+
*
|
|
11
|
+
* Scope, stated honestly: the dumps cover `sceneview`, `arsceneview` and
|
|
12
|
+
* `sceneview-core` (Android/KMP). SceneViewSwift and sceneview-web have no
|
|
13
|
+
* committed dump, so Swift/Web snippets are NOT symbol-checked.
|
|
14
|
+
*/
|
|
15
|
+
import { SYMBOLS } from "./generated/symbols.js";
|
|
16
|
+
export const SCENEVIEW_NAMESPACE = "io.github.sceneview";
|
|
17
|
+
// ─── Lazily-built lookup sets ────────────────────────────────────────────────
|
|
18
|
+
let typeNamesCache = null;
|
|
19
|
+
let classBySimpleNameCache = null;
|
|
20
|
+
/**
|
|
21
|
+
* Every PascalCase identifier a Kotlin user can legitimately reference as a
|
|
22
|
+
* type or declarative factory:
|
|
23
|
+
* - class simple names (`ModelNode`, `WallPlacement`, …),
|
|
24
|
+
* - PascalCase top-level functions — the composables (`SceneView`,
|
|
25
|
+
* `ARSceneView`, `WallPlacementScene`, `DynamicSkyNode`, …),
|
|
26
|
+
* - PascalCase members of the `*Scope` classes — the declarative node
|
|
27
|
+
* factories (`SceneScope.ModelNode`, `ARSceneScope.…`).
|
|
28
|
+
*/
|
|
29
|
+
export function knownTypeNames() {
|
|
30
|
+
if (typeNamesCache)
|
|
31
|
+
return typeNamesCache;
|
|
32
|
+
const names = new Set();
|
|
33
|
+
for (const fqcn of Object.keys(SYMBOLS.classes)) {
|
|
34
|
+
const simple = fqcn.slice(fqcn.lastIndexOf(".") + 1);
|
|
35
|
+
if (/^[A-Z]/.test(simple))
|
|
36
|
+
names.add(simple);
|
|
37
|
+
}
|
|
38
|
+
for (const name of Object.keys(SYMBOLS.topLevel)) {
|
|
39
|
+
if (/^[A-Z]/.test(name))
|
|
40
|
+
names.add(name);
|
|
41
|
+
}
|
|
42
|
+
for (const [fqcn, entry] of Object.entries(SYMBOLS.classes)) {
|
|
43
|
+
if (!fqcn.endsWith("Scope"))
|
|
44
|
+
continue;
|
|
45
|
+
for (const member of entry.members) {
|
|
46
|
+
if (/^[A-Z]/.test(member))
|
|
47
|
+
names.add(member);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
typeNamesCache = names;
|
|
51
|
+
return names;
|
|
52
|
+
}
|
|
53
|
+
/** Class simple name → the FQCNs that declare it (usually exactly one). */
|
|
54
|
+
function classBySimpleName() {
|
|
55
|
+
if (classBySimpleNameCache)
|
|
56
|
+
return classBySimpleNameCache;
|
|
57
|
+
const map = new Map();
|
|
58
|
+
for (const fqcn of Object.keys(SYMBOLS.classes)) {
|
|
59
|
+
const simple = fqcn.slice(fqcn.lastIndexOf(".") + 1);
|
|
60
|
+
const list = map.get(simple);
|
|
61
|
+
if (list)
|
|
62
|
+
list.push(fqcn);
|
|
63
|
+
else
|
|
64
|
+
map.set(simple, [fqcn]);
|
|
65
|
+
}
|
|
66
|
+
classBySimpleNameCache = map;
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
export function isKnownTypeName(name) {
|
|
70
|
+
return knownTypeNames().has(name);
|
|
71
|
+
}
|
|
72
|
+
/** Top-level function (any case) — composables and extension helpers. */
|
|
73
|
+
export function isKnownTopLevelFunction(name) {
|
|
74
|
+
return name in SYMBOLS.topLevel;
|
|
75
|
+
}
|
|
76
|
+
/** Every real `remember*` composable helper — suggestion pool for typos. */
|
|
77
|
+
export function rememberHelperNames() {
|
|
78
|
+
return Object.keys(SYMBOLS.topLevel).filter((name) => name.startsWith("remember"));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve an `io.github.sceneview.*` import target.
|
|
82
|
+
*
|
|
83
|
+
* Returns "class" for an exact class FQCN, "package" for a package that
|
|
84
|
+
* exists (covers `import io.github.sceneview.node.*`), "member" for a
|
|
85
|
+
* `Class.member` or package-level function import, and null when nothing
|
|
86
|
+
* in the index matches. Callers must only pass SceneView-namespace paths —
|
|
87
|
+
* other namespaces are out of index scope.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveImport(path) {
|
|
90
|
+
if (path in SYMBOLS.classes)
|
|
91
|
+
return "class";
|
|
92
|
+
if (SYMBOLS.packages.includes(path))
|
|
93
|
+
return "package";
|
|
94
|
+
const lastDot = path.lastIndexOf(".");
|
|
95
|
+
if (lastDot === -1)
|
|
96
|
+
return null;
|
|
97
|
+
const parent = path.slice(0, lastDot);
|
|
98
|
+
const leaf = path.slice(lastDot + 1);
|
|
99
|
+
// `import io.github.sceneview.ar.rememberARCameraStream` — a top-level
|
|
100
|
+
// function imported from a package.
|
|
101
|
+
if (SYMBOLS.packages.includes(parent) && leaf in SYMBOLS.topLevel) {
|
|
102
|
+
return "member";
|
|
103
|
+
}
|
|
104
|
+
// A nested class member — accept when the parent class exists and
|
|
105
|
+
// declares it.
|
|
106
|
+
const parentClass = SYMBOLS.classes[parent];
|
|
107
|
+
if (parentClass?.members.includes(leaf))
|
|
108
|
+
return "member";
|
|
109
|
+
// `import io.github.sceneview.node.ModelNode.Companion` — the generator
|
|
110
|
+
// folds companion members into the host class and drops the `.Companion`
|
|
111
|
+
// entry, so resolve the companion itself through its host.
|
|
112
|
+
if (leaf === "Companion" && parent in SYMBOLS.classes)
|
|
113
|
+
return "member";
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** Union of member/property names declared by classes with this simple name. */
|
|
117
|
+
export function membersOfClass(simpleName) {
|
|
118
|
+
const fqcns = classBySimpleName().get(simpleName);
|
|
119
|
+
if (!fqcns)
|
|
120
|
+
return null;
|
|
121
|
+
const members = new Set();
|
|
122
|
+
for (const fqcn of fqcns) {
|
|
123
|
+
for (const m of SYMBOLS.classes[fqcn].members)
|
|
124
|
+
members.add(m);
|
|
125
|
+
}
|
|
126
|
+
return members;
|
|
127
|
+
}
|
|
128
|
+
// ─── Edit-distance suggestions ───────────────────────────────────────────────
|
|
129
|
+
/** Classic Levenshtein with a band cutoff — returns Infinity past `max`. */
|
|
130
|
+
export function editDistance(a, b, max) {
|
|
131
|
+
if (Math.abs(a.length - b.length) > max)
|
|
132
|
+
return Number.POSITIVE_INFINITY;
|
|
133
|
+
const la = a.length;
|
|
134
|
+
const lb = b.length;
|
|
135
|
+
let prev = Array.from({ length: lb + 1 }, (_, i) => i);
|
|
136
|
+
let curr = new Array(lb + 1);
|
|
137
|
+
for (let i = 1; i <= la; i++) {
|
|
138
|
+
curr[0] = i;
|
|
139
|
+
let rowMin = i;
|
|
140
|
+
for (let j = 1; j <= lb; j++) {
|
|
141
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
142
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
143
|
+
if (curr[j] < rowMin)
|
|
144
|
+
rowMin = curr[j];
|
|
145
|
+
}
|
|
146
|
+
if (rowMin > max)
|
|
147
|
+
return Number.POSITIVE_INFINITY;
|
|
148
|
+
[prev, curr] = [curr, prev];
|
|
149
|
+
}
|
|
150
|
+
return prev[lb] <= max ? prev[lb] : Number.POSITIVE_INFINITY;
|
|
151
|
+
}
|
|
152
|
+
/** camelCase identifier → lowercased token set (`loadModelAsync` → load,model,async). */
|
|
153
|
+
function camelTokens(name) {
|
|
154
|
+
return new Set(name
|
|
155
|
+
.split(/(?=[A-Z])/)
|
|
156
|
+
.map((t) => t.toLowerCase())
|
|
157
|
+
.filter((t) => t.length > 0));
|
|
158
|
+
}
|
|
159
|
+
/** Jaccard similarity of the camelCase token sets of two identifiers. */
|
|
160
|
+
export function tokenSimilarity(a, b) {
|
|
161
|
+
const ta = camelTokens(a);
|
|
162
|
+
const tb = camelTokens(b);
|
|
163
|
+
let inter = 0;
|
|
164
|
+
for (const t of ta)
|
|
165
|
+
if (tb.has(t))
|
|
166
|
+
inter++;
|
|
167
|
+
const union = ta.size + tb.size - inter;
|
|
168
|
+
return union === 0 ? 0 : inter / union;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Closest candidates to `name`, nearest first, by a hybrid metric:
|
|
172
|
+
* - Levenshtein within a length-scaled band (2 for short names, 3 for long)
|
|
173
|
+
* catches character typos (`ModellNode` → `ModelNode`);
|
|
174
|
+
* - camelCase token overlap (Jaccard ≥ 0.5) catches structural
|
|
175
|
+
* hallucinations that raw edit distance can't reach —
|
|
176
|
+
* `createModelInstanceAsync` → `loadModelInstanceAsync` is distance 5,
|
|
177
|
+
* but shares 3 of 5 camelCase tokens.
|
|
178
|
+
* Token matches are scored `10 × (1 − similarity)` so an exact-ish edit
|
|
179
|
+
* match always outranks a merely structural one.
|
|
180
|
+
*/
|
|
181
|
+
export function suggestClosest(name, candidates, limit = 3) {
|
|
182
|
+
const max = name.length < 12 ? 2 : 3;
|
|
183
|
+
const lower = name.toLowerCase();
|
|
184
|
+
const scored = [];
|
|
185
|
+
for (const candidate of candidates) {
|
|
186
|
+
const d = editDistance(lower, candidate.toLowerCase(), max);
|
|
187
|
+
if (d !== Number.POSITIVE_INFINITY) {
|
|
188
|
+
scored.push([candidate, d]);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const similarity = tokenSimilarity(name, candidate);
|
|
192
|
+
if (similarity >= 0.5)
|
|
193
|
+
scored.push([candidate, 10 * (1 - similarity)]);
|
|
194
|
+
}
|
|
195
|
+
scored.sort((x, y) => x[1] - y[1] || x[0].localeCompare(y[0]));
|
|
196
|
+
return scored.slice(0, limit).map(([candidate]) => candidate);
|
|
197
|
+
}
|
|
198
|
+
/** Human-readable " Did you mean …?" suffix, or "" when nothing is close. */
|
|
199
|
+
export function didYouMean(name, candidates) {
|
|
200
|
+
const close = suggestClosest(name, candidates);
|
|
201
|
+
if (close.length === 0)
|
|
202
|
+
return "";
|
|
203
|
+
return ` Did you mean ${close.map((c) => `\`${c}\``).join(", ")}?`;
|
|
204
|
+
}
|
package/dist/tiers.js
CHANGED
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
// the 4 vertical packages (Automotive / Gaming / Healthcare / Interior) and
|
|
11
11
|
// the 3 generation tools (preview, artifact, scene generation).
|
|
12
12
|
const FREE_TOOLS = [
|
|
13
|
-
|
|
13
|
+
// Gateway-only widget tool. It is defined in
|
|
14
|
+
// `mcp-gateway/src/mcp/widget-tools.ts`, NOT in this package's
|
|
15
|
+
// TOOL_DEFINITIONS — the stdio server never lists or serves it. The entry
|
|
16
|
+
// here is load-bearing anyway: the gateway's tier gate
|
|
17
|
+
// (`mcp-gateway/src/mcp/access.ts`) resolves tiers through this map and
|
|
18
|
+
// unknown tools default to "pro", so removing this line would silently
|
|
19
|
+
// paywall the anonymous ChatGPT 3D-viewer widget.
|
|
14
20
|
"view_3d_model",
|
|
15
21
|
"list_samples",
|
|
16
22
|
"get_sample",
|
|
@@ -28,6 +34,7 @@ const FREE_TOOLS = [
|
|
|
28
34
|
"get_collision_guide",
|
|
29
35
|
"get_platform_roadmap",
|
|
30
36
|
"search_models",
|
|
37
|
+
"generate_3d_model",
|
|
31
38
|
"analyze_project",
|
|
32
39
|
"search_android_docs",
|
|
33
40
|
"fetch_android_doc",
|
|
@@ -56,6 +63,9 @@ const PRO_TOOLS = [
|
|
|
56
63
|
"get_hud_overlay",
|
|
57
64
|
"get_ar_showroom",
|
|
58
65
|
"list_car_models",
|
|
66
|
+
"get_ev_charging_station_viewer",
|
|
67
|
+
"get_car_paint_shader",
|
|
68
|
+
"validate_automotive_code",
|
|
59
69
|
// Gaming package
|
|
60
70
|
"get_physics_game",
|
|
61
71
|
"get_particle_effects",
|
|
@@ -63,6 +73,7 @@ const PRO_TOOLS = [
|
|
|
63
73
|
"get_inventory_3d",
|
|
64
74
|
"get_character_viewer",
|
|
65
75
|
"list_game_models",
|
|
76
|
+
"validate_game_code",
|
|
66
77
|
// Healthcare package
|
|
67
78
|
"get_surgical_planning",
|
|
68
79
|
"get_dental_viewer",
|
|
@@ -70,6 +81,7 @@ const PRO_TOOLS = [
|
|
|
70
81
|
"get_anatomy_viewer",
|
|
71
82
|
"get_molecule_viewer",
|
|
72
83
|
"list_medical_models",
|
|
84
|
+
"validate_medical_code",
|
|
73
85
|
// Interior package
|
|
74
86
|
"get_room_planner",
|
|
75
87
|
"get_lighting_design",
|
|
@@ -77,6 +89,17 @@ const PRO_TOOLS = [
|
|
|
77
89
|
"get_material_switcher",
|
|
78
90
|
"get_furniture_placement",
|
|
79
91
|
"list_furniture_models",
|
|
92
|
+
"validate_interior_code",
|
|
93
|
+
// Rerun package (Pro per its README — "All 5 rerun tools are Pro tier").
|
|
94
|
+
// Until #2697 these five (and the six package additions above) rode the
|
|
95
|
+
// unknown-tool default-to-pro fallback; behaviour is unchanged, the map
|
|
96
|
+
// is just explicit now so a forgotten mapping is distinguishable from a
|
|
97
|
+
// deliberate Pro tool.
|
|
98
|
+
"setup_rerun_project",
|
|
99
|
+
"generate_ar_logger",
|
|
100
|
+
"generate_python_sidecar",
|
|
101
|
+
"embed_web_viewer",
|
|
102
|
+
"explain_concept",
|
|
80
103
|
];
|
|
81
104
|
// ─── Tier map ─────────────────────────────────────────────────────────────────
|
|
82
105
|
export const TOOL_TIERS = Object.fromEntries([
|
|
@@ -103,10 +126,10 @@ export function getFreeToolNames() {
|
|
|
103
126
|
// ─── Upgrade message ──────────────────────────────────────────────────────────
|
|
104
127
|
export const PRO_UPGRADE_MESSAGE = `## \u{1F512} Pro Feature
|
|
105
128
|
|
|
106
|
-
This tool is part of a specialized package (Automotive / Gaming / Healthcare / Interior) or a heavier generation tool. SceneView MCP Pro unlocks them.
|
|
129
|
+
This tool is part of a specialized package (Automotive / Gaming / Healthcare / Interior / Rerun) or a heavier generation tool. SceneView MCP Pro unlocks them.
|
|
107
130
|
|
|
108
131
|
**Upgrade for \u20AC19/month** to unlock:
|
|
109
|
-
-
|
|
132
|
+
- 5 vertical packages (Automotive, Gaming, Healthcare, Interior, Rerun — 35 specialized tools)
|
|
110
133
|
- 3D preview, artifact, and scene-generation helpers
|
|
111
134
|
|
|
112
135
|
All setup, migration, and reference guides remain free.
|
|
@@ -72,7 +72,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
name: "validate_code",
|
|
75
|
-
description: "Checks a Kotlin or Swift SceneView snippet for common mistakes. For Kotlin: threading violations, wrong destroy order, missing null-checks, LightNode trailing-lambda bug, deprecated 2.x APIs. For Swift: missing @MainActor, async/await patterns, missing imports, RealityKit mistakes. Language is auto-detected. Always call this before presenting generated SceneView code to the user.",
|
|
75
|
+
description: "Checks a Kotlin or Swift SceneView snippet for common mistakes. For Kotlin: symbol existence against the real public API (unknown imports, made-up node types, nonexistent loader methods — with did-you-mean suggestions), threading violations, wrong destroy order, missing null-checks, LightNode trailing-lambda bug, deprecated 2.x APIs. For Swift: missing @MainActor, async/await patterns, missing imports, RealityKit mistakes. Language is auto-detected. Always call this before presenting generated SceneView code to the user.",
|
|
76
76
|
inputSchema: {
|
|
77
77
|
type: "object",
|
|
78
78
|
properties: {
|
|
@@ -585,6 +585,34 @@ export const TOOL_DEFINITIONS = [
|
|
|
585
585
|
destructiveHint: false,
|
|
586
586
|
},
|
|
587
587
|
},
|
|
588
|
+
{
|
|
589
|
+
name: "generate_3d_model",
|
|
590
|
+
description: "Generates a brand-new 3D model (GLB) from a text prompt OR a source image using the Tripo AI API — use this when `search_models` finds no suitable existing asset and the user needs a custom one (\"a low-poly cactus in a striped pot\"). Returns a direct GLB download URL (expires ~5 minutes — download it immediately and self-host it) plus license/attribution metadata, ready for `rememberModelInstance(modelLoader, ...)` and AR placement. Quality tiers: \"fast\" (default, Tripo P1 low-poly — AR-ready, ~25-30 s) or \"hd\" (Tripo H3.1 quad topology with detailed geometry/textures — up to ~100 s, pricier). Requires a `TRIPO_API_KEY` environment variable (BYOK — generations are billed to the USER'S Tripo account, roughly $0.10-0.25 per fast generation and ~$0.41 per hd generation as of July 2026; nothing is charged by SceneView). If the key is missing, the tool returns setup instructions for platform.tripo3d.ai/api-keys.",
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: "object",
|
|
593
|
+
properties: {
|
|
594
|
+
prompt: {
|
|
595
|
+
type: "string",
|
|
596
|
+
description: "Text description of the model to generate (text→3D), e.g. \"a low-poly cactus in a striped pot\". Max 1024 characters. Provide exactly one of `prompt` or `imageUrl`.",
|
|
597
|
+
},
|
|
598
|
+
imageUrl: {
|
|
599
|
+
type: "string",
|
|
600
|
+
description: "Public HTTPS URL of a source image to convert to 3D (image→3D). JPEG or PNG, max 20 MB. Provide exactly one of `prompt` or `imageUrl`.",
|
|
601
|
+
},
|
|
602
|
+
quality: {
|
|
603
|
+
type: "string",
|
|
604
|
+
enum: ["fast", "hd"],
|
|
605
|
+
description: '"fast" (default): Tripo P1 low-poly — cheap, AR-ready, ~25-30 s. "hd": Tripo H3.1 quad topology + detailed geometry/textures — up to ~100 s, higher credit cost.',
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
required: [],
|
|
609
|
+
},
|
|
610
|
+
annotations: {
|
|
611
|
+
readOnlyHint: false,
|
|
612
|
+
openWorldHint: true,
|
|
613
|
+
destructiveHint: false,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
588
616
|
{
|
|
589
617
|
name: "analyze_project",
|
|
590
618
|
description: "Scans a local SceneView project on the user's machine and returns a structured analysis: detected project type (Android, iOS, Web), extracted SceneView dependency version, whether it is outdated vs the latest known release, and any known anti-patterns found by reading source files (threading violations, LightNode trailing-lambda bug, deprecated 2.x APIs, Sceneform imports). Safe: scans at most 30 source files and 500 KB total, never writes to disk. Use this when a user asks 'is my project up to date?', 'what's wrong with my SceneView code?', or when you want a fast sanity check of a project before generating code for it.",
|
package/dist/tools/handler.js
CHANGED
|
@@ -33,6 +33,7 @@ import { generateScene, formatGeneratedScene } from "../generate-scene.js";
|
|
|
33
33
|
import { ANIMATION_GUIDE, GESTURE_GUIDE, PERFORMANCE_TIPS, } from "../advanced-guides.js";
|
|
34
34
|
import { MATERIAL_GUIDE, COLLISION_GUIDE, MODEL_OPTIMIZATION_GUIDE, WEB_RENDERING_GUIDE, } from "../extra-guides.js";
|
|
35
35
|
import { searchModels, formatSearchResults } from "../search-models.js";
|
|
36
|
+
import { generateModel, formatGenerateResult } from "../generate-model.js";
|
|
36
37
|
import { analyzeProject, formatAnalysisReport } from "../analyze-project.js";
|
|
37
38
|
import { searchAndroidDocs, fetchAndroidDoc, formatAndroidDocsSearch, formatAndroidDocsFetch, } from "../android-docs.js";
|
|
38
39
|
import { LLMS_TXT } from "../generated/llms-txt.js";
|
|
@@ -696,7 +697,7 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
|
|
|
696
697
|
{ platform: "visionOS", renderer: "RealityKit", framework: "SwiftUI", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "SceneViewSwift (SPM)", features: ["3D", "Immersive spaces", "Hand tracking (planned)"] },
|
|
697
698
|
{ platform: "Web", renderer: "Filament.js (WASM)", framework: "Kotlin/JS", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "sceneview-web (npm)", features: ["3D", "WebXR AR/VR", "GLB models", "WebGL2"] },
|
|
698
699
|
{ platform: "Desktop", renderer: "Software / Filament JNI", framework: "Compose Desktop", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "sceneview-desktop (local)", features: ["3D", "Software renderer", "Wireframe"] },
|
|
699
|
-
{ platform: "Flutter", renderer: "Filament / RealityKit", framework: "PlatformView", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "flutter pub:
|
|
700
|
+
{ platform: "Flutter", renderer: "Filament / RealityKit", framework: "PlatformView", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "flutter pub: flutter_sceneview (git ref until first publish, #2735)", features: ["3D", "AR", "Android + iOS bridge"] },
|
|
700
701
|
];
|
|
701
702
|
const lines = [
|
|
702
703
|
"## SceneView Supported Platforms\n",
|
|
@@ -769,6 +770,19 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
|
|
|
769
770
|
isError: searchResult.ok ? undefined : true,
|
|
770
771
|
};
|
|
771
772
|
}
|
|
773
|
+
// ── generate_3d_model ────────────────────────────────────────────────────
|
|
774
|
+
case "generate_3d_model": {
|
|
775
|
+
const genResult = await generateModel({
|
|
776
|
+
prompt: args?.prompt,
|
|
777
|
+
imageUrl: args?.imageUrl,
|
|
778
|
+
quality: args?.quality,
|
|
779
|
+
});
|
|
780
|
+
const genText = formatGenerateResult(genResult);
|
|
781
|
+
return {
|
|
782
|
+
content: withDisclaimer([{ type: "text", text: genText }]),
|
|
783
|
+
isError: genResult.ok ? undefined : true,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
772
786
|
// ── analyze_project ───────────────────────────────────────────────────────
|
|
773
787
|
case "analyze_project": {
|
|
774
788
|
const rawPath = args?.path;
|
package/dist/validator.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
import { didYouMean, isKnownTopLevelFunction, isKnownTypeName, knownTypeNames, membersOfClass, rememberHelperNames, resolveImport, } from "./symbols.js";
|
|
1
2
|
function findLines(lines, pattern) {
|
|
2
3
|
return lines
|
|
3
4
|
.map((l, i) => (pattern.test(l) ? i + 1 : -1))
|
|
4
5
|
.filter((n) => n !== -1);
|
|
5
6
|
}
|
|
7
|
+
// Symbols the snippet declares itself (`fun MyShowcaseScene(…)`, `class ShelfNode(…) :
|
|
8
|
+
// Node(engine)`) exist by definition — they are the caller's own wrappers, not calls into
|
|
9
|
+
// SceneView, and the SDK's naming idiom (`WallPlacementScene`, `PhysicsNode`) makes such
|
|
10
|
+
// suffixes natural. The optional `<…>` skips a generic parameter list before the name.
|
|
11
|
+
function locallyDeclaredNames(code) {
|
|
12
|
+
const names = new Set();
|
|
13
|
+
for (const m of code.matchAll(/\b(?:fun|class|interface|object)\s+(?:<[^>]*>\s+)?(\w+)/g)) {
|
|
14
|
+
names.add(m[1]);
|
|
15
|
+
}
|
|
16
|
+
return names;
|
|
17
|
+
}
|
|
6
18
|
const RULES = [
|
|
7
19
|
// ─── Threading ────────────────────────────────────────────────────────────
|
|
8
20
|
{
|
|
@@ -450,6 +462,193 @@ const RULES = [
|
|
|
450
462
|
return issues;
|
|
451
463
|
},
|
|
452
464
|
},
|
|
465
|
+
// ─── Symbol existence (#2760) — backed by the generated .api index ────────
|
|
466
|
+
//
|
|
467
|
+
// These rules answer "does this API actually exist?", the #1 failure mode
|
|
468
|
+
// of AI-generated snippets. The index covers the Android/KMP surface
|
|
469
|
+
// (sceneview, arsceneview, sceneview-core) — see `symbols.ts`. Each rule is
|
|
470
|
+
// deliberately narrow so its false-positive space is ~zero.
|
|
471
|
+
{
|
|
472
|
+
// `import io.github.sceneview.…` that resolves to nothing. Our own
|
|
473
|
+
// namespace: nothing outside the index can legitimately live there.
|
|
474
|
+
// Known tolerated false NEGATIVE: aliased imports (`import … as MN`)
|
|
475
|
+
// don't match the end-of-line anchor and are silently skipped — an
|
|
476
|
+
// unchecked alias can never become a false positive.
|
|
477
|
+
id: "symbols/unknown-import",
|
|
478
|
+
severity: "error",
|
|
479
|
+
check(code, lines) {
|
|
480
|
+
const issues = [];
|
|
481
|
+
const importRe = /^\s*import\s+(io\.github\.sceneview(?:\.\w+)*)(\.\*)?\s*$/;
|
|
482
|
+
lines.forEach((content, i) => {
|
|
483
|
+
const m = importRe.exec(content);
|
|
484
|
+
if (!m)
|
|
485
|
+
return;
|
|
486
|
+
const path = m[1];
|
|
487
|
+
const isWildcard = m[2] !== undefined;
|
|
488
|
+
const resolved = resolveImport(path);
|
|
489
|
+
const ok = isWildcard ? resolved === "package" : resolved !== null;
|
|
490
|
+
if (ok)
|
|
491
|
+
return;
|
|
492
|
+
const leaf = path.slice(path.lastIndexOf(".") + 1);
|
|
493
|
+
const suggestion = didYouMean(leaf, knownTypeNames());
|
|
494
|
+
issues.push({
|
|
495
|
+
severity: "error",
|
|
496
|
+
rule: "symbols/unknown-import",
|
|
497
|
+
message: `\`import ${path}${isWildcard ? ".*" : ""}\` does not exist in the SceneView public API.${suggestion} Check the class name and package against \`llms.txt\`.`,
|
|
498
|
+
line: i + 1,
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
return issues;
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
// A made-up `…Node(` / `…Node {` / `…Scene(` type or declarative factory.
|
|
506
|
+
id: "symbols/unknown-type",
|
|
507
|
+
severity: "error",
|
|
508
|
+
check(code, lines) {
|
|
509
|
+
const issues = [];
|
|
510
|
+
// Names already handled (with a better, migration-specific message) by
|
|
511
|
+
// `migration/old-api` — don't double-report them here.
|
|
512
|
+
const migrationCovered = new Set([
|
|
513
|
+
"Scene",
|
|
514
|
+
"ARScene",
|
|
515
|
+
"ArSceneView",
|
|
516
|
+
"PlacementNode",
|
|
517
|
+
"TransformableNode",
|
|
518
|
+
]);
|
|
519
|
+
const callRe = /\b([A-Z]\w*(?:Node|Scene))\s*[({]/g;
|
|
520
|
+
const localNames = locallyDeclaredNames(code);
|
|
521
|
+
const seen = new Set();
|
|
522
|
+
for (const match of code.matchAll(callRe)) {
|
|
523
|
+
const name = match[1];
|
|
524
|
+
if (seen.has(name))
|
|
525
|
+
continue;
|
|
526
|
+
seen.add(name);
|
|
527
|
+
if (migrationCovered.has(name))
|
|
528
|
+
continue;
|
|
529
|
+
if (localNames.has(name))
|
|
530
|
+
continue;
|
|
531
|
+
if (isKnownTypeName(name))
|
|
532
|
+
continue;
|
|
533
|
+
const suggestion = didYouMean(name, knownTypeNames());
|
|
534
|
+
findLines(lines, new RegExp(`\\b${name}\\s*[({]`)).forEach((line) => issues.push({
|
|
535
|
+
severity: "error",
|
|
536
|
+
rule: "symbols/unknown-type",
|
|
537
|
+
message: `\`${name}\` does not exist in the SceneView public API.${suggestion} Node types and declarative factories are listed in \`llms.txt\`.`,
|
|
538
|
+
line,
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
return issues;
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
// A nonexistent member called on a canonically-named loader variable —
|
|
546
|
+
// e.g. `modelLoader.createModelInstanceAsync(...)` (the real API is
|
|
547
|
+
// `loadModelInstanceAsync`). Heuristic on the receiver NAME, which the
|
|
548
|
+
// docs and every sample use consistently.
|
|
549
|
+
id: "symbols/unknown-member",
|
|
550
|
+
severity: "error",
|
|
551
|
+
check(code, lines) {
|
|
552
|
+
const issues = [];
|
|
553
|
+
const receivers = {
|
|
554
|
+
modelLoader: "ModelLoader",
|
|
555
|
+
materialLoader: "MaterialLoader",
|
|
556
|
+
environmentLoader: "EnvironmentLoader",
|
|
557
|
+
};
|
|
558
|
+
// Kotlin stdlib scope/extension functions callable on any receiver.
|
|
559
|
+
const stdlib = new Set([
|
|
560
|
+
"apply",
|
|
561
|
+
"also",
|
|
562
|
+
"let",
|
|
563
|
+
"run",
|
|
564
|
+
"takeIf",
|
|
565
|
+
"takeUnless",
|
|
566
|
+
"to",
|
|
567
|
+
"use",
|
|
568
|
+
"toString",
|
|
569
|
+
]);
|
|
570
|
+
// A snippet may rebind a canonical name to its own type
|
|
571
|
+
// (`val modelLoader = MyRepository()`) — member-checking that variable
|
|
572
|
+
// against the SceneView loader would be a false positive
|
|
573
|
+
// (adversarial-review finding #3 on PR #2814). Keep checking only when
|
|
574
|
+
// the declaration is recognizably the real loader: a `remember<Class>`
|
|
575
|
+
// call, a direct constructor, a `: <Class>` type annotation, or a
|
|
576
|
+
// `.<receiver>` property access — or when there is no local
|
|
577
|
+
// declaration at all (the scope provides it, the canonical case).
|
|
578
|
+
const rebound = new Set();
|
|
579
|
+
for (const [receiver, className] of Object.entries(receivers)) {
|
|
580
|
+
const decl = new RegExp(`\\b(?:val|var)\\s+${receiver}\\s*(?::\\s*([^=\\n]+?))?\\s*=\\s*(.+)`).exec(code);
|
|
581
|
+
if (!decl)
|
|
582
|
+
continue;
|
|
583
|
+
const [, typeAnnotation, rhs] = decl;
|
|
584
|
+
const looksReal = (typeAnnotation?.includes(className) ?? false) ||
|
|
585
|
+
rhs.includes(`remember${className}`) ||
|
|
586
|
+
rhs.includes(`${className}(`) ||
|
|
587
|
+
rhs.trimEnd().endsWith(`.${receiver}`);
|
|
588
|
+
if (!looksReal)
|
|
589
|
+
rebound.add(receiver);
|
|
590
|
+
}
|
|
591
|
+
const callRe = /\b(modelLoader|materialLoader|environmentLoader)\.(\w+)\s*\(/g;
|
|
592
|
+
const reported = new Set();
|
|
593
|
+
for (const match of code.matchAll(callRe)) {
|
|
594
|
+
const [, receiver, member] = match;
|
|
595
|
+
const key = `${receiver}.${member}`;
|
|
596
|
+
if (reported.has(key))
|
|
597
|
+
continue;
|
|
598
|
+
reported.add(key);
|
|
599
|
+
if (rebound.has(receiver))
|
|
600
|
+
continue;
|
|
601
|
+
if (stdlib.has(member))
|
|
602
|
+
continue;
|
|
603
|
+
const members = membersOfClass(receivers[receiver]);
|
|
604
|
+
if (!members || members.has(member))
|
|
605
|
+
continue;
|
|
606
|
+
const suggestion = didYouMean(member, members);
|
|
607
|
+
findLines(lines, new RegExp(`\\b${receiver}\\.${member}\\s*\\(`)).forEach((line) => issues.push({
|
|
608
|
+
severity: "error",
|
|
609
|
+
rule: "symbols/unknown-member",
|
|
610
|
+
message: `\`${receivers[receiver]}.${member}()\` does not exist.${suggestion} The full member list is in \`llms.txt\`.`,
|
|
611
|
+
line,
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
return issues;
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
// `remember<SceneView-ish>` helper that isn't a real top-level function —
|
|
619
|
+
// e.g. an invented `rememberModelInstanceAsync`. Warning, not error: a
|
|
620
|
+
// snippet may legitimately mix non-SceneView Compose libraries whose
|
|
621
|
+
// helpers match the keyword net (e.g. Maps `rememberCameraPositionState`).
|
|
622
|
+
id: "symbols/unknown-remember-helper",
|
|
623
|
+
severity: "warning",
|
|
624
|
+
check(code, lines) {
|
|
625
|
+
const issues = [];
|
|
626
|
+
const sceneViewish = /^(Engine|Model|Material|Environment|Scene|Node|Collision|Render|Skybox|View|AR|Camera|Splat)/;
|
|
627
|
+
const callRe = /\bremember([A-Z]\w*)\s*\(/g;
|
|
628
|
+
const localNames = locallyDeclaredNames(code);
|
|
629
|
+
const seen = new Set();
|
|
630
|
+
for (const match of code.matchAll(callRe)) {
|
|
631
|
+
const fullName = `remember${match[1]}`;
|
|
632
|
+
if (seen.has(fullName))
|
|
633
|
+
continue;
|
|
634
|
+
seen.add(fullName);
|
|
635
|
+
if (!sceneViewish.test(match[1]))
|
|
636
|
+
continue;
|
|
637
|
+
if (localNames.has(fullName))
|
|
638
|
+
continue;
|
|
639
|
+
if (isKnownTopLevelFunction(fullName))
|
|
640
|
+
continue;
|
|
641
|
+
const suggestion = didYouMean(fullName, rememberHelperNames());
|
|
642
|
+
findLines(lines, new RegExp(`\\b${fullName}\\s*\\(`)).forEach((line) => issues.push({
|
|
643
|
+
severity: "warning",
|
|
644
|
+
rule: "symbols/unknown-remember-helper",
|
|
645
|
+
message: `\`${fullName}\` is not a SceneView composable helper.${suggestion} If it comes from another library, add its import; otherwise check \`llms.txt\`.`,
|
|
646
|
+
line,
|
|
647
|
+
}));
|
|
648
|
+
}
|
|
649
|
+
return issues;
|
|
650
|
+
},
|
|
651
|
+
},
|
|
453
652
|
];
|
|
454
653
|
// ─── Swift Validation Rules ──────────────────────────────────────────────────
|
|
455
654
|
const SWIFT_RULES = [
|