sceneview-mcp 3.6.2 → 3.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/analyze-project.js +500 -0
- package/dist/auth.js +84 -0
- package/dist/billing.js +137 -0
- package/dist/convert-platform.js +302 -0
- package/dist/debug-issue.js +2 -2
- package/dist/explain-api.js +245 -0
- package/dist/extra-guides.js +1 -1
- package/dist/generate-animation.js +576 -0
- package/dist/generate-environment.js +483 -0
- package/dist/generate-gesture.js +532 -0
- package/dist/generate-physics.js +570 -0
- package/dist/generate-scene.js +4 -4
- package/dist/generated/llms-txt.js +6 -0
- package/dist/guides.js +8 -8
- package/dist/index.js +54 -1111
- package/dist/migration.js +2 -2
- package/dist/optimize-scene.js +173 -0
- package/dist/platform-setup.js +11 -11
- package/dist/samples.js +64 -64
- package/dist/search-models.js +214 -0
- package/dist/telemetry.js +120 -0
- package/dist/tiers.js +100 -0
- package/dist/tools/definitions.js +467 -0
- package/dist/tools/handler.js +791 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/types.js +8 -0
- package/dist/validator.js +1 -1
- package/llms.txt +24 -1
- package/package.json +9 -20
package/dist/billing.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// ─── Stripe Billing Validation for SceneView MCP Pro ─────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Validates API keys against Stripe subscriptions to determine tier access.
|
|
4
|
+
// Uses native fetch() — no external Stripe SDK dependency.
|
|
5
|
+
// MCP servers log to stderr (stdout is reserved for the JSON-RPC protocol).
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
8
|
+
function getCached(apiKey) {
|
|
9
|
+
const entry = cache.get(apiKey);
|
|
10
|
+
if (!entry)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (Date.now() - entry.cachedAt > CACHE_TTL_MS) {
|
|
13
|
+
cache.delete(apiKey);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return entry.status;
|
|
17
|
+
}
|
|
18
|
+
function setCache(apiKey, status) {
|
|
19
|
+
cache.set(apiKey, { status, cachedAt: Date.now() });
|
|
20
|
+
}
|
|
21
|
+
/** Clears the validation cache. Useful for testing. */
|
|
22
|
+
export function clearCache() {
|
|
23
|
+
cache.clear();
|
|
24
|
+
}
|
|
25
|
+
// ─── Development Allowlist ───────────────────────────────────────────────────
|
|
26
|
+
const DEV_ALLOWLIST = new Set([
|
|
27
|
+
"dev_test_key",
|
|
28
|
+
"sceneview_dev",
|
|
29
|
+
]);
|
|
30
|
+
function checkDevAllowlist(apiKey) {
|
|
31
|
+
if (DEV_ALLOWLIST.has(apiKey)) {
|
|
32
|
+
return { valid: true, tier: "pro" };
|
|
33
|
+
}
|
|
34
|
+
return { valid: false, tier: "free", error: "Invalid API key (dev mode — no Stripe configured)" };
|
|
35
|
+
}
|
|
36
|
+
async function fetchStripeSubscription(subscriptionId, stripeSecretKey) {
|
|
37
|
+
const url = `https://api.stripe.com/v1/subscriptions/${encodeURIComponent(subscriptionId)}`;
|
|
38
|
+
let response;
|
|
39
|
+
try {
|
|
40
|
+
response = await fetch(url, {
|
|
41
|
+
method: "GET",
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${stripeSecretKey}`,
|
|
44
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
return { valid: false, tier: "free", error: `Stripe API request failed: ${message}` };
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const body = await response.text().catch(() => "unknown error");
|
|
54
|
+
if (response.status === 404) {
|
|
55
|
+
return { valid: false, tier: "free", error: "Subscription not found" };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
tier: "free",
|
|
60
|
+
error: `Stripe API error (${response.status}): ${body}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
let subscription;
|
|
64
|
+
try {
|
|
65
|
+
subscription = (await response.json());
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { valid: false, tier: "free", error: "Failed to parse Stripe response" };
|
|
69
|
+
}
|
|
70
|
+
const isActive = subscription.status === "active" || subscription.status === "trialing";
|
|
71
|
+
const expiresAt = new Date(subscription.current_period_end * 1000).toISOString();
|
|
72
|
+
if (isActive) {
|
|
73
|
+
return {
|
|
74
|
+
valid: true,
|
|
75
|
+
tier: "pro",
|
|
76
|
+
customerId: subscription.customer,
|
|
77
|
+
expiresAt,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
tier: "free",
|
|
83
|
+
customerId: subscription.customer,
|
|
84
|
+
expiresAt,
|
|
85
|
+
error: `Subscription status is "${subscription.status}" (expected "active" or "trialing")`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Validates an API key by checking it against Stripe subscriptions.
|
|
91
|
+
*
|
|
92
|
+
* - Results are cached in memory for 5 minutes to avoid excessive Stripe calls.
|
|
93
|
+
* - If `STRIPE_SECRET_KEY` is not set, falls back to a development allowlist.
|
|
94
|
+
* - The API key is treated as a Stripe subscription ID for the lookup.
|
|
95
|
+
*/
|
|
96
|
+
export async function validateApiKey(apiKey) {
|
|
97
|
+
// Check cache first
|
|
98
|
+
const cached = getCached(apiKey);
|
|
99
|
+
if (cached)
|
|
100
|
+
return cached;
|
|
101
|
+
let result;
|
|
102
|
+
if (isDevMode()) {
|
|
103
|
+
// No Stripe key configured — use allowlist for development/testing
|
|
104
|
+
result = checkDevAllowlist(apiKey);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
|
108
|
+
result = await fetchStripeSubscription(apiKey, stripeKey);
|
|
109
|
+
}
|
|
110
|
+
setCache(apiKey, result);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns the configured SceneView API key from the environment, or undefined
|
|
115
|
+
* if the user is on the free tier (no key set).
|
|
116
|
+
*/
|
|
117
|
+
export function getConfiguredApiKey() {
|
|
118
|
+
return process.env.SCENEVIEW_API_KEY;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Returns true when `STRIPE_SECRET_KEY` is not set in the environment.
|
|
122
|
+
* In dev mode, API key validation falls back to a simple allowlist instead
|
|
123
|
+
* of calling the Stripe API.
|
|
124
|
+
*/
|
|
125
|
+
export function isDevMode() {
|
|
126
|
+
return !process.env.STRIPE_SECRET_KEY;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Records usage of an MCP tool for a given API key.
|
|
130
|
+
*
|
|
131
|
+
* Stub for future billing/metering integration. Currently logs to stderr
|
|
132
|
+
* (MCP servers must not write to stdout — it carries the JSON-RPC protocol).
|
|
133
|
+
*/
|
|
134
|
+
export async function recordUsage(apiKey, toolName) {
|
|
135
|
+
const timestamp = new Date().toISOString();
|
|
136
|
+
process.stderr.write(`[billing] ${timestamp} usage: key=${apiKey.slice(0, 8)}… tool=${toolName}\n`);
|
|
137
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* convert-platform.ts
|
|
3
|
+
*
|
|
4
|
+
* Convert SceneView code between Android (Kotlin/Compose) and iOS (Swift/SwiftUI).
|
|
5
|
+
* Also generates multiplatform code from a scene description.
|
|
6
|
+
*/
|
|
7
|
+
const CONVERSION_RULES = [
|
|
8
|
+
{
|
|
9
|
+
pattern: /\bSceneView\s*\(/g,
|
|
10
|
+
androidToIos: "SceneView {",
|
|
11
|
+
iosToAndroid: "SceneView(",
|
|
12
|
+
description: "SceneView composable → SceneView SwiftUI view",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
pattern: /\bARSceneView\s*\(/g,
|
|
16
|
+
androidToIos: "ARSceneView(",
|
|
17
|
+
iosToAndroid: "ARSceneView(",
|
|
18
|
+
description: "ARSceneView composable → ARSceneView SwiftUI view",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /rememberModelInstance\s*\(\s*modelLoader\s*,\s*"([^"]+)\.glb"\s*\)/g,
|
|
22
|
+
androidToIos: 'try await ModelNode.load("$1.usdz")',
|
|
23
|
+
iosToAndroid: 'rememberModelInstance(modelLoader, "$1.glb")',
|
|
24
|
+
description: "Model loading: rememberModelInstance → ModelNode.load, GLB → USDZ",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: /rememberEngine\(\)/g,
|
|
28
|
+
androidToIos: "// Engine managed by RealityKit automatically",
|
|
29
|
+
iosToAndroid: "rememberEngine()",
|
|
30
|
+
description: "Engine: explicit in Android, implicit in iOS/RealityKit",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pattern: /rememberModelLoader\(engine\)/g,
|
|
34
|
+
androidToIos: "// ModelLoader not needed — RealityKit loads models directly",
|
|
35
|
+
iosToAndroid: "rememberModelLoader(engine)",
|
|
36
|
+
description: "ModelLoader: Android-specific, not needed on iOS",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /rememberEnvironmentLoader\(engine\)/g,
|
|
40
|
+
androidToIos: "// Environment managed by RealityKit automatically",
|
|
41
|
+
iosToAndroid: "rememberEnvironmentLoader(engine)",
|
|
42
|
+
description: "EnvironmentLoader: Android-specific",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
pattern: /Modifier\.fillMaxSize\(\)/g,
|
|
46
|
+
androidToIos: ".edgesIgnoringSafeArea(.all)",
|
|
47
|
+
iosToAndroid: "Modifier.fillMaxSize()",
|
|
48
|
+
description: "Full-screen modifier",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /Position\(([^)]+)\)/g,
|
|
52
|
+
androidToIos: "SIMD3<Float>($1)",
|
|
53
|
+
iosToAndroid: "Position($1)",
|
|
54
|
+
description: "Position type: Position → SIMD3<Float>",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
pattern: /\.glb\b/g,
|
|
58
|
+
androidToIos: ".usdz",
|
|
59
|
+
iosToAndroid: ".glb",
|
|
60
|
+
description: "Model format: GLB (Android) ↔ USDZ (iOS)",
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
export function convertAndroidToIos(code) {
|
|
64
|
+
let result = code;
|
|
65
|
+
const changes = [];
|
|
66
|
+
const warnings = [];
|
|
67
|
+
for (const rule of CONVERSION_RULES) {
|
|
68
|
+
if (rule.pattern.test(result)) {
|
|
69
|
+
const replacement = typeof rule.androidToIos === "string" ? rule.androidToIos : rule.androidToIos;
|
|
70
|
+
result = result.replace(new RegExp(rule.pattern.source, rule.pattern.flags), replacement);
|
|
71
|
+
changes.push(rule.description);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Add Swift-specific warnings
|
|
75
|
+
warnings.push("RealityKit uses USDZ models, not GLB/glTF. Convert models using Apple's Reality Converter or Blender.");
|
|
76
|
+
warnings.push("RealityKit handles engine, material, and environment lifecycle automatically — no manual management needed.");
|
|
77
|
+
warnings.push("Swift async/await: model loading must use `try await` inside `.task { }` block.");
|
|
78
|
+
if (code.includes("LightNode")) {
|
|
79
|
+
warnings.push("LightNode API differs: RealityKit uses DirectionalLightComponent, PointLightComponent, SpotLightComponent on Entity.");
|
|
80
|
+
}
|
|
81
|
+
if (code.includes("materialLoader")) {
|
|
82
|
+
warnings.push("Material API differs: RealityKit uses SimpleMaterial, PhysicallyBasedMaterial, or UnlitMaterial.");
|
|
83
|
+
}
|
|
84
|
+
return { code: result, sourceplatform: "android", targetPlatform: "ios", changes, warnings };
|
|
85
|
+
}
|
|
86
|
+
export function convertIosToAndroid(code) {
|
|
87
|
+
let result = code;
|
|
88
|
+
const changes = [];
|
|
89
|
+
const warnings = [];
|
|
90
|
+
// iOS → Android specific replacements
|
|
91
|
+
const iosPatterns = [
|
|
92
|
+
{ pattern: /SceneView\s*\{/g, replacement: "SceneView(engine = engine) {", description: "SceneView → SceneView with engine" },
|
|
93
|
+
{ pattern: /ARSceneView\s*\(/g, replacement: "ARSceneView(engine = engine, ", description: "ARSceneView → ARSceneView with engine" },
|
|
94
|
+
{ pattern: /try\s+await\s+ModelNode\.load\s*\(\s*"([^"]+)\.usdz"\s*\)/g, replacement: 'rememberModelInstance(modelLoader, "$1.glb")', description: "ModelNode.load → rememberModelInstance, USDZ → GLB" },
|
|
95
|
+
{ pattern: /import\s+SwiftUI/g, replacement: "// SwiftUI → Jetpack Compose", description: "Import replacement" },
|
|
96
|
+
{ pattern: /import\s+SceneViewSwift/g, replacement: "import io.github.sceneview.*", description: "Import replacement" },
|
|
97
|
+
{ pattern: /import\s+RealityKit/g, replacement: "// RealityKit → Filament (included in SceneView)", description: "Import replacement" },
|
|
98
|
+
{ pattern: /\.task\s*\{/g, replacement: "LaunchedEffect(Unit) {", description: ".task → LaunchedEffect" },
|
|
99
|
+
{ pattern: /\.edgesIgnoringSafeArea\(.all\)/g, replacement: "", description: "Remove iOS-specific modifier" },
|
|
100
|
+
{ pattern: /\.usdz\b/g, replacement: ".glb", description: "Model format: USDZ → GLB" },
|
|
101
|
+
{ pattern: /@State\s+private\s+var/g, replacement: "var /* @State */ ", description: "@State → Compose state" },
|
|
102
|
+
{ pattern: /SIMD3<Float>\(([^)]+)\)/g, replacement: "Position($1)", description: "SIMD3 → Position" },
|
|
103
|
+
];
|
|
104
|
+
for (const rule of iosPatterns) {
|
|
105
|
+
if (rule.pattern.test(result)) {
|
|
106
|
+
result = result.replace(rule.pattern, rule.replacement);
|
|
107
|
+
changes.push(rule.description);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
warnings.push("Android uses GLB/glTF models, not USDZ. Convert models using Blender or gltf-transform.");
|
|
111
|
+
warnings.push("Android requires explicit engine, modelLoader, and environmentLoader management.");
|
|
112
|
+
warnings.push("Filament JNI calls must run on the main thread. Use rememberModelInstance in composables.");
|
|
113
|
+
if (code.includes("Entity") || code.includes("entity")) {
|
|
114
|
+
warnings.push("RealityKit Entity → SceneView Node types (ModelNode, LightNode, etc.).");
|
|
115
|
+
}
|
|
116
|
+
return { code: result, sourceplatform: "ios", targetPlatform: "android", changes, warnings };
|
|
117
|
+
}
|
|
118
|
+
export function generateMultiplatformCode(description) {
|
|
119
|
+
const lower = description.toLowerCase();
|
|
120
|
+
const isAR = lower.includes("ar") || lower.includes("augmented") || lower.includes("camera");
|
|
121
|
+
const hasModel = lower.includes("model") || lower.includes("object") || lower.includes("3d") || !isAR;
|
|
122
|
+
const modelName = "scene_object";
|
|
123
|
+
const androidCode = isAR
|
|
124
|
+
? `@Composable
|
|
125
|
+
fun MultiplatformARScreen() {
|
|
126
|
+
val engine = rememberEngine()
|
|
127
|
+
val modelLoader = rememberModelLoader(engine)
|
|
128
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/${modelName}.glb")
|
|
129
|
+
var anchor by remember { mutableStateOf<Anchor?>(null) }
|
|
130
|
+
|
|
131
|
+
ARSceneView(
|
|
132
|
+
modifier = Modifier.fillMaxSize(),
|
|
133
|
+
engine = engine,
|
|
134
|
+
modelLoader = modelLoader,
|
|
135
|
+
planeRenderer = true,
|
|
136
|
+
sessionConfiguration = { session, config ->
|
|
137
|
+
config.lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
|
|
138
|
+
config.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL
|
|
139
|
+
},
|
|
140
|
+
onTouchEvent = { event, hitResult ->
|
|
141
|
+
if (event.action == MotionEvent.ACTION_UP && hitResult != null) {
|
|
142
|
+
anchor = hitResult.createAnchor()
|
|
143
|
+
}
|
|
144
|
+
true
|
|
145
|
+
}
|
|
146
|
+
) {
|
|
147
|
+
anchor?.let { a ->
|
|
148
|
+
AnchorNode(anchor = a) {
|
|
149
|
+
modelInstance?.let { instance ->
|
|
150
|
+
ModelNode(
|
|
151
|
+
modelInstance = instance,
|
|
152
|
+
scaleToUnits = 0.5f,
|
|
153
|
+
isEditable = true
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}`
|
|
160
|
+
: `@Composable
|
|
161
|
+
fun MultiplatformSceneScreen() {
|
|
162
|
+
val engine = rememberEngine()
|
|
163
|
+
val modelLoader = rememberModelLoader(engine)
|
|
164
|
+
val environmentLoader = rememberEnvironmentLoader(engine)
|
|
165
|
+
val modelInstance = rememberModelInstance(modelLoader, "models/${modelName}.glb")
|
|
166
|
+
|
|
167
|
+
SceneView(
|
|
168
|
+
modifier = Modifier.fillMaxSize(),
|
|
169
|
+
engine = engine,
|
|
170
|
+
modelLoader = modelLoader,
|
|
171
|
+
environment = rememberEnvironment(environmentLoader) {
|
|
172
|
+
environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
|
|
173
|
+
?: createEnvironment(environmentLoader)
|
|
174
|
+
},
|
|
175
|
+
mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f },
|
|
176
|
+
cameraManipulator = rememberCameraManipulator()
|
|
177
|
+
) {
|
|
178
|
+
modelInstance?.let { instance ->
|
|
179
|
+
ModelNode(
|
|
180
|
+
modelInstance = instance,
|
|
181
|
+
scaleToUnits = 1.0f,
|
|
182
|
+
centerOrigin = Position(0f, 0f, 0f),
|
|
183
|
+
isEditable = true
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}`;
|
|
188
|
+
const iosCode = isAR
|
|
189
|
+
? `import SwiftUI
|
|
190
|
+
import SceneViewSwift
|
|
191
|
+
import RealityKit
|
|
192
|
+
|
|
193
|
+
struct MultiplatformARView: View {
|
|
194
|
+
@State private var model: ModelNode?
|
|
195
|
+
|
|
196
|
+
var body: some View {
|
|
197
|
+
ARSceneView(
|
|
198
|
+
planeDetection: .horizontal,
|
|
199
|
+
showCoachingOverlay: true,
|
|
200
|
+
onTapOnPlane: { position, arView in
|
|
201
|
+
guard let model else { return }
|
|
202
|
+
let anchor = AnchorNode.world(position: position)
|
|
203
|
+
let clone = model.entity.clone(recursive: true)
|
|
204
|
+
clone.scale = .init(repeating: 0.5)
|
|
205
|
+
anchor.add(clone)
|
|
206
|
+
arView.scene.addAnchor(anchor.entity)
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
.edgesIgnoringSafeArea(.all)
|
|
210
|
+
.task {
|
|
211
|
+
do {
|
|
212
|
+
model = try await ModelNode.load("models/${modelName}.usdz")
|
|
213
|
+
} catch {
|
|
214
|
+
print("Failed to load model: \\(error)")
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}`
|
|
219
|
+
: `import SwiftUI
|
|
220
|
+
import SceneViewSwift
|
|
221
|
+
import RealityKit
|
|
222
|
+
|
|
223
|
+
struct MultiplatformSceneView: View {
|
|
224
|
+
@State private var model: ModelNode?
|
|
225
|
+
|
|
226
|
+
var body: some View {
|
|
227
|
+
SceneView { root in
|
|
228
|
+
if let model {
|
|
229
|
+
root.addChild(model.entity)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
.cameraControls(.orbit)
|
|
233
|
+
.task {
|
|
234
|
+
do {
|
|
235
|
+
model = try await ModelNode.load("models/${modelName}.usdz")
|
|
236
|
+
model?.scaleToUnits(1.0)
|
|
237
|
+
} catch {
|
|
238
|
+
print("Failed to load model: \\(error)")
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}`;
|
|
243
|
+
return {
|
|
244
|
+
androidCode,
|
|
245
|
+
iosCode,
|
|
246
|
+
description,
|
|
247
|
+
notes: [
|
|
248
|
+
"Android uses GLB/glTF models, iOS uses USDZ format.",
|
|
249
|
+
"Both platforms render with PBR materials but use different engines (Filament vs RealityKit).",
|
|
250
|
+
"Android requires explicit engine/loader management; iOS/RealityKit handles this automatically.",
|
|
251
|
+
isAR ? "AR features: Android uses ARCore, iOS uses ARKit. Both support plane detection and anchoring." : "",
|
|
252
|
+
`Replace 'models/${modelName}.glb' and 'models/${modelName}.usdz' with your actual model files.`,
|
|
253
|
+
].filter(Boolean),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export function formatConversionResult(result) {
|
|
257
|
+
const targetLabel = result.targetPlatform === "ios" ? "iOS (SwiftUI + RealityKit)" : "Android (Jetpack Compose + Filament)";
|
|
258
|
+
const lang = result.targetPlatform === "ios" ? "swift" : "kotlin";
|
|
259
|
+
const parts = [
|
|
260
|
+
`## Code Converted to ${targetLabel}`,
|
|
261
|
+
``,
|
|
262
|
+
`**${result.changes.length} conversion(s) applied.**`,
|
|
263
|
+
``,
|
|
264
|
+
`### Converted Code`,
|
|
265
|
+
``,
|
|
266
|
+
"```" + lang,
|
|
267
|
+
result.code,
|
|
268
|
+
"```",
|
|
269
|
+
``,
|
|
270
|
+
];
|
|
271
|
+
if (result.changes.length > 0) {
|
|
272
|
+
parts.push(`### Changes`);
|
|
273
|
+
result.changes.forEach((c, i) => parts.push(`${i + 1}. ${c}`));
|
|
274
|
+
parts.push(``);
|
|
275
|
+
}
|
|
276
|
+
if (result.warnings.length > 0) {
|
|
277
|
+
parts.push(`### Manual Attention Required`);
|
|
278
|
+
result.warnings.forEach((w, i) => parts.push(`${i + 1}. ${w}`));
|
|
279
|
+
}
|
|
280
|
+
return parts.join("\n");
|
|
281
|
+
}
|
|
282
|
+
export function formatMultiplatformResult(result) {
|
|
283
|
+
return [
|
|
284
|
+
`## Multiplatform Scene Code`,
|
|
285
|
+
`**Description:** "${result.description}"`,
|
|
286
|
+
``,
|
|
287
|
+
`### Android (Kotlin / Jetpack Compose)`,
|
|
288
|
+
``,
|
|
289
|
+
"```kotlin",
|
|
290
|
+
result.androidCode,
|
|
291
|
+
"```",
|
|
292
|
+
``,
|
|
293
|
+
`### iOS (Swift / SwiftUI)`,
|
|
294
|
+
``,
|
|
295
|
+
"```swift",
|
|
296
|
+
result.iosCode,
|
|
297
|
+
"```",
|
|
298
|
+
``,
|
|
299
|
+
`### Notes`,
|
|
300
|
+
...result.notes.map((n, i) => `${i + 1}. ${n}`),
|
|
301
|
+
].join("\n");
|
|
302
|
+
}
|
package/dist/debug-issue.js
CHANGED
|
@@ -292,7 +292,7 @@ fun DebugModelViewer() {
|
|
|
292
292
|
title: "Build / Gradle Errors",
|
|
293
293
|
guide: `## Debugging: Build Errors
|
|
294
294
|
|
|
295
|
-
### "Cannot resolve io.github.sceneview:sceneview:3.6.
|
|
295
|
+
### "Cannot resolve io.github.sceneview:sceneview:3.6.2"
|
|
296
296
|
|
|
297
297
|
1. Check repositories in \`settings.gradle.kts\`:
|
|
298
298
|
\`\`\`kotlin
|
|
@@ -335,7 +335,7 @@ SceneView bundles Filament. If you also depend on Filament directly:
|
|
|
335
335
|
\`\`\`kotlin
|
|
336
336
|
// Remove direct Filament dependency — SceneView includes it
|
|
337
337
|
// implementation("com.google.android.filament:filament-android:1.x.x") // REMOVE
|
|
338
|
-
implementation("io.github.sceneview:sceneview:3.6.
|
|
338
|
+
implementation("io.github.sceneview:sceneview:3.6.2") // This includes Filament
|
|
339
339
|
\`\`\`
|
|
340
340
|
|
|
341
341
|
### "Cannot find Filament material"
|