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.
@@ -1,302 +0,0 @@
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
- }
@@ -1,246 +0,0 @@
1
- /**
2
- * explain-api.ts
3
- *
4
- * Explains specific SceneView APIs with examples, common mistakes, and tips.
5
- */
6
- const API_EXPLANATIONS = {
7
- rememberengine: {
8
- name: "rememberEngine()",
9
- summary: "Creates and remembers a Filament Engine tied to the Compose lifecycle. Automatically destroyed when the composable leaves the composition.",
10
- signature: "@Composable fun rememberEngine(): Engine",
11
- platform: "Android",
12
- example: `val engine = rememberEngine()
13
- SceneView(engine = engine, modifier = Modifier.fillMaxSize()) {
14
- // Your 3D content
15
- }`,
16
- commonMistakes: [
17
- "Creating multiple engines in different composables — wastes GPU memory. Use ONE engine per app.",
18
- "Calling engine.destroy() manually — rememberEngine handles this. Double-destroy causes SIGABRT.",
19
- "Using Engine.create() in a composable — use rememberEngine() instead for lifecycle safety.",
20
- ],
21
- tips: [
22
- "Create the engine at the top-level composable and pass it down to all SceneView composables.",
23
- "The engine is the most expensive resource in SceneView — reuse it everywhere.",
24
- ],
25
- relatedAPIs: ["rememberModelLoader()", "rememberEnvironmentLoader()", "rememberMaterialLoader()"],
26
- },
27
- remembermodelinstance: {
28
- name: "rememberModelInstance()",
29
- summary: "Asynchronously loads a GLB/glTF model and returns a ModelInstance. Returns null while loading and if the file fails to load.",
30
- signature: "@Composable fun rememberModelInstance(modelLoader: ModelLoader, path: String): ModelInstance?",
31
- platform: "Android",
32
- example: `val engine = rememberEngine()
33
- val modelLoader = rememberModelLoader(engine)
34
- val modelInstance = rememberModelInstance(modelLoader, "models/chair.glb")
35
-
36
- SceneView(engine = engine) {
37
- modelInstance?.let { instance ->
38
- ModelNode(modelInstance = instance, scaleToUnits = 1.0f)
39
- }
40
- }`,
41
- commonMistakes: [
42
- "Not null-checking the result — it's null while loading. Always use ?.let or if != null.",
43
- "Using modelLoader.createModelInstance() directly — blocks the main thread. Use rememberModelInstance.",
44
- "Putting a leading slash in the path — use 'models/file.glb' not '/models/file.glb'.",
45
- "Loading .gltf multi-file format — use .glb (single binary) to avoid missing texture references.",
46
- ],
47
- tips: [
48
- "Show a loading indicator while the model instance is null.",
49
- "The path is relative to src/main/assets/.",
50
- "For imperative (non-composable) loading, use modelLoader.loadModelInstanceAsync().",
51
- ],
52
- relatedAPIs: ["ModelNode()", "rememberModelLoader()", "ModelLoader"],
53
- },
54
- modelnode: {
55
- name: "ModelNode()",
56
- summary: "Displays a 3D model in the scene. Requires a ModelInstance from rememberModelInstance or loadModelInstanceAsync.",
57
- signature: "@Composable fun SceneScope.ModelNode(modelInstance: ModelInstance, scaleToUnits: Float? = null, centerOrigin: Position? = null, autoAnimate: Boolean = false, isEditable: Boolean = false, ...)",
58
- platform: "Android",
59
- example: `modelInstance?.let { instance ->
60
- ModelNode(
61
- modelInstance = instance,
62
- scaleToUnits = 1.0f,
63
- centerOrigin = Position(0f, 0f, 0f),
64
- autoAnimate = true,
65
- isEditable = true
66
- )
67
- }`,
68
- commonMistakes: [
69
- "Passing a null modelInstance without null-checking — will throw at runtime.",
70
- "Forgetting scaleToUnits — models can be enormous or microscopic depending on their original units.",
71
- "Not having a light in the scene — model appears black without illumination.",
72
- ],
73
- tips: [
74
- "scaleToUnits normalizes the model to fit within a unit sphere of the given size.",
75
- "centerOrigin moves the model's pivot point to the specified position.",
76
- "isEditable = true enables built-in pinch-to-scale and drag-to-rotate.",
77
- "autoAnimate = true plays the first embedded animation in a loop.",
78
- ],
79
- relatedAPIs: ["rememberModelInstance()", "LightNode()", "AnchorNode()"],
80
- },
81
- lightnode: {
82
- name: "LightNode()",
83
- summary: "Adds a light source to the scene. Configuration is via the named parameter `apply`, NOT a trailing lambda.",
84
- signature: "@Composable fun SceneScope.LightNode(engine: Engine, type: LightManager.Type, apply: LightManager.Builder.() -> Unit = {}, ...)",
85
- platform: "Android",
86
- example: `LightNode(
87
- engine = engine,
88
- type = LightManager.Type.DIRECTIONAL,
89
- apply = {
90
- intensity(100_000f)
91
- castShadows(true)
92
- direction(0f, -1f, -0.5f)
93
- }
94
- )`,
95
- commonMistakes: [
96
- "Using a trailing lambda: LightNode(engine, type) { ... } — the block is SILENTLY IGNORED. Must use apply = { ... }.",
97
- "Forgetting to add any light — PBR models appear completely black without light.",
98
- "Setting intensity too low — directional/sun lights typically need 100,000+ lux.",
99
- ],
100
- tips: [
101
- "Light types: DIRECTIONAL (sun), POINT (lamp), SPOT (flashlight), SUN (physically-based sun).",
102
- "For outdoor scenes, use Type.SUN with ~110,000 lux intensity.",
103
- "Each shadow-casting light adds a GPU render pass. Limit to 1-2 on mobile.",
104
- ],
105
- relatedAPIs: ["SceneView()", "rememberMainLightNode()", "DynamicSkyNode()"],
106
- },
107
- arscene: {
108
- name: "ARSceneView()",
109
- summary: "Composable that renders an augmented reality view with camera feed, plane detection, and light estimation.",
110
- signature: "@Composable fun ARSceneView(modifier: Modifier, engine: Engine, modelLoader: ModelLoader? = null, planeRenderer: Boolean = true, sessionConfiguration: (Session, Config) -> Unit = { _, _ -> }, onTouchEvent: (MotionEvent, HitResult?) -> Boolean = { _, _ -> false }, content: @Composable ARSceneScope.() -> Unit)",
111
- platform: "Android",
112
- example: `var anchor by remember { mutableStateOf<Anchor?>(null) }
113
-
114
- ARSceneView(
115
- modifier = Modifier.fillMaxSize(),
116
- engine = engine,
117
- modelLoader = modelLoader,
118
- planeRenderer = true,
119
- sessionConfiguration = { session, config ->
120
- config.lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
121
- },
122
- onTouchEvent = { event, hitResult ->
123
- if (event.action == MotionEvent.ACTION_UP && hitResult != null) {
124
- anchor = hitResult.createAnchor()
125
- }
126
- true
127
- }
128
- ) {
129
- anchor?.let { a ->
130
- AnchorNode(anchor = a) {
131
- // Place content at the anchor
132
- }
133
- }
134
- }`,
135
- commonMistakes: [
136
- "Not requesting camera permission at runtime — AR crashes without it.",
137
- "Missing AndroidManifest entries — need CAMERA permission and com.google.ar.core meta-data.",
138
- "Setting worldPosition on nodes instead of using AnchorNode — causes drift.",
139
- "Testing on emulator — ARCore support on emulators is limited; use a real device.",
140
- "Enabling Geospatial / Streetscape / Cloud Anchors without the ARCore Cloud API key wired into the manifest (com.google.android.ar.API_KEY) — backend handshake silently returns no data. Also requires ACCESS_FINE_LOCATION at runtime BEFORE Session.configure(GeospatialMode.ENABLED).",
141
- ],
142
- tips: [
143
- "Always use AnchorNode for placing objects in AR — anchors compensate for tracking drift.",
144
- "planeRenderer = true shows plane detection visualization to guide the user.",
145
- "ENVIRONMENTAL_HDR is the most realistic lighting mode for AR objects.",
146
- "Use isEditable = true on ModelNode for pinch-to-scale after placement.",
147
- ],
148
- relatedAPIs: ["SceneView()", "AnchorNode()", "HitResultNode()", "rememberModelInstance()"],
149
- },
150
- scene: {
151
- name: "SceneView()",
152
- summary: "Composable that renders a 3D viewport with Filament. The main entry point for 3D rendering in SceneView.",
153
- signature: "@Composable fun SceneView(modifier: Modifier, engine: Engine, modelLoader: ModelLoader? = null, environment: Environment? = null, cameraManipulator: CameraManipulator? = null, mainLightNode: LightNode? = null, onFrame: (Long) -> Unit = {}, content: @Composable SceneScope.() -> Unit)",
154
- platform: "Android",
155
- example: `val engine = rememberEngine()
156
- val modelLoader = rememberModelLoader(engine)
157
-
158
- SceneView(
159
- modifier = Modifier.fillMaxSize(),
160
- engine = engine,
161
- modelLoader = modelLoader,
162
- cameraManipulator = rememberCameraManipulator()
163
- ) {
164
- // Declare nodes as composables here
165
- }`,
166
- commonMistakes: [
167
- "Missing Modifier.fillMaxSize() — SceneView may have zero size and be invisible.",
168
- "Missing engine parameter — required in SceneView 3.0+.",
169
- "Creating nodes imperatively instead of declaratively — use composable DSL inside SceneView { }.",
170
- ],
171
- tips: [
172
- "Add cameraManipulator for orbit controls (drag to rotate, pinch to zoom).",
173
- "Use environment for HDR skybox and reflections.",
174
- "Use mainLightNode for the primary light source.",
175
- "onFrame callback runs every frame on the main thread — safe for Filament calls.",
176
- ],
177
- relatedAPIs: ["ARSceneView()", "rememberEngine()", "ModelNode()", "LightNode()"],
178
- },
179
- anchornode: {
180
- name: "AnchorNode()",
181
- summary: "An AR node anchored to a real-world position. Compensates for ARCore coordinate system changes during tracking.",
182
- signature: "@Composable fun ARSceneScope.AnchorNode(anchor: Anchor, content: @Composable () -> Unit)",
183
- platform: "Android",
184
- example: `anchor?.let { a ->
185
- AnchorNode(anchor = a) {
186
- modelInstance?.let { instance ->
187
- ModelNode(modelInstance = instance, scaleToUnits = 0.5f)
188
- }
189
- }
190
- }`,
191
- commonMistakes: [
192
- "Using AnchorNode() without an anchor parameter — requires a valid Anchor from hitResult.createAnchor().",
193
- "Setting worldPosition manually instead of using AnchorNode — causes drift as ARCore remaps coordinates.",
194
- ],
195
- tips: [
196
- "Create anchors from hit results: hitResult.createAnchor().",
197
- "Nest child nodes inside AnchorNode's content block — they inherit the anchor's position.",
198
- "For persistent anchors across sessions, use CloudAnchorNode.",
199
- ],
200
- relatedAPIs: ["ARSceneView()", "HitResultNode()", "ModelNode()"],
201
- },
202
- };
203
- export function explainAPI(apiName) {
204
- const key = apiName.toLowerCase().replace(/[^a-z]/g, "");
205
- return API_EXPLANATIONS[key] || null;
206
- }
207
- export function listExplainableAPIs() {
208
- return Object.values(API_EXPLANATIONS).map((e) => e.name);
209
- }
210
- export function formatAPIExplanation(explanation) {
211
- const parts = [
212
- `## ${explanation.name}`,
213
- ``,
214
- `**Platform:** ${explanation.platform}`,
215
- ``,
216
- `${explanation.summary}`,
217
- ``,
218
- `### Signature`,
219
- ``,
220
- "```kotlin",
221
- explanation.signature,
222
- "```",
223
- ``,
224
- `### Example`,
225
- ``,
226
- "```kotlin",
227
- explanation.example,
228
- "```",
229
- ``,
230
- ];
231
- if (explanation.commonMistakes.length > 0) {
232
- parts.push(`### Common Mistakes`);
233
- explanation.commonMistakes.forEach((m, i) => parts.push(`${i + 1}. ${m}`));
234
- parts.push(``);
235
- }
236
- if (explanation.tips.length > 0) {
237
- parts.push(`### Tips`);
238
- explanation.tips.forEach((t, i) => parts.push(`${i + 1}. ${t}`));
239
- parts.push(``);
240
- }
241
- if (explanation.relatedAPIs.length > 0) {
242
- parts.push(`### Related APIs`);
243
- parts.push(explanation.relatedAPIs.map((a) => `\`${a}\``).join(", "));
244
- }
245
- return parts.join("\n");
246
- }