sceneview-mcp 3.4.7 → 3.4.9

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Thomas Gorisse
3
+ Copyright (c) 2024-2026 Thomas Gorisse
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -3,9 +3,11 @@
3
3
  [![npm version](https://img.shields.io/npm/v/sceneview-mcp?color=6c35aa)](https://www.npmjs.com/package/sceneview-mcp)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/sceneview-mcp?color=blue)](https://www.npmjs.com/package/sceneview-mcp)
5
5
  [![MCP](https://img.shields.io/badge/MCP-v1.12-blue)](https://modelcontextprotocol.io/)
6
- [![License](https://img.shields.io/badge/License-Apache%202.0-green)](https://www.apache.org/licenses/LICENSE-2.0)
6
+ [![License](https://img.shields.io/badge/License-MIT-green)](./LICENSE)
7
7
  [![Node](https://img.shields.io/badge/Node-%3E%3D18-brightgreen)](https://nodejs.org/)
8
8
 
9
+ > **Disclaimer:** This tool generates code suggestions for the SceneView SDK. Generated code is provided "as is" without warranty. Always review generated code before use in production. This is not a substitute for professional software engineering review. See [TERMS.md](./TERMS.md) and [PRIVACY.md](./PRIVACY.md).
10
+
9
11
  The official [Model Context Protocol](https://modelcontextprotocol.io/) server for **SceneView** — giving AI assistants deep knowledge of the SceneView 3D/AR SDK so they generate correct, compilable Kotlin code.
10
12
 
11
13
  ---
@@ -265,6 +267,12 @@ Published to npm on each SceneView release:
265
267
  npm publish --access public
266
268
  ```
267
269
 
270
+ ## Legal
271
+
272
+ - [LICENSE](./LICENSE) — MIT License
273
+ - [TERMS.md](./TERMS.md) — Terms of Service
274
+ - [PRIVACY.md](./PRIVACY.md) — Privacy Policy (no data collected)
275
+
268
276
  ## License
269
277
 
270
- Apache 2.0 same as SceneView.
278
+ MITsee [LICENSE](./LICENSE).
@@ -0,0 +1,343 @@
1
+ // ─── 3D Artifact Generator ───────────────────────────────────────────────────
2
+ //
3
+ // Generates complete, self-contained HTML artifacts with interactive 3D content
4
+ // that Claude can render directly in conversations via artifacts.
5
+ //
6
+ // Types:
7
+ // - model-viewer: interactive 3D model viewer with orbit controls + AR
8
+ // - chart-3d: 3D bar/pie charts for data visualization
9
+ // - scene: multi-model 3D scene with lighting and environment
10
+ // - product-360: product turntable with hotspot annotations + AR
11
+ // ─── Constants ───────────────────────────────────────────────────────────────
12
+ const MODEL_VIEWER_CDN = "https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js";
13
+ const DEFAULT_MODEL = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/DamagedHelmet/glTF-Binary/DamagedHelmet.glb";
14
+ const DEFAULT_COLORS = [
15
+ "#4285F4", "#EA4335", "#FBBC04", "#34A853", "#FF6D01",
16
+ "#46BDC6", "#7BAAF7", "#F07B72", "#FCD04F", "#57BB8A",
17
+ ];
18
+ const BRANDING = `<div style="position:absolute;bottom:8px;right:12px;font-size:11px;color:#888;font-family:system-ui,sans-serif;pointer-events:none">Powered by SceneView</div>`;
19
+ // ─── Shared HTML skeleton ────────────────────────────────────────────────────
20
+ function htmlShell(title, body, extraHead = "") {
21
+ return `<!DOCTYPE html>
22
+ <html lang="en">
23
+ <head>
24
+ <meta charset="utf-8">
25
+ <meta name="viewport" content="width=device-width,initial-scale=1">
26
+ <title>${escapeHtml(title)}</title>
27
+ <style>
28
+ *{margin:0;padding:0;box-sizing:border-box}
29
+ html,body{width:100%;height:100%;overflow:hidden;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
30
+ </style>
31
+ ${extraHead}
32
+ </head>
33
+ <body>
34
+ ${body}
35
+ </body>
36
+ </html>`;
37
+ }
38
+ function escapeHtml(s) {
39
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
40
+ }
41
+ // ─── Validation ──────────────────────────────────────────────────────────────
42
+ export function validateArtifactInput(input) {
43
+ const validTypes = ["model-viewer", "chart-3d", "scene", "product-360"];
44
+ if (!validTypes.includes(input.type)) {
45
+ return `Invalid type "${input.type}". Must be one of: ${validTypes.join(", ")}`;
46
+ }
47
+ if (input.type === "chart-3d") {
48
+ if (!input.data || !Array.isArray(input.data) || input.data.length === 0) {
49
+ return 'Type "chart-3d" requires a non-empty `data` array with {label, value} objects.';
50
+ }
51
+ for (const d of input.data) {
52
+ if (typeof d.label !== "string" || typeof d.value !== "number") {
53
+ return "Each data item must have a string `label` and numeric `value`.";
54
+ }
55
+ }
56
+ }
57
+ if (input.modelUrl) {
58
+ if (!input.modelUrl.startsWith("https://") && !input.modelUrl.startsWith("http://")) {
59
+ return "modelUrl must be an HTTP(S) URL.";
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ // ─── Generators ──────────────────────────────────────────────────────────────
65
+ export function generateArtifact(input) {
66
+ switch (input.type) {
67
+ case "model-viewer":
68
+ return generateModelViewer(input);
69
+ case "chart-3d":
70
+ return generateChart3D(input);
71
+ case "scene":
72
+ return generateScene(input);
73
+ case "product-360":
74
+ return generateProduct360(input);
75
+ }
76
+ }
77
+ // ── model-viewer ─────────────────────────────────────────────────────────────
78
+ function generateModelViewer(input) {
79
+ const model = input.modelUrl || DEFAULT_MODEL;
80
+ const title = input.title || "3D Model Viewer";
81
+ const opts = input.options || {};
82
+ const bg = opts.backgroundColor || "#1a1a2e";
83
+ const orbit = opts.cameraOrbit || "0deg 75deg 105%";
84
+ const autoRotate = opts.autoRotate !== false;
85
+ const ar = opts.ar !== false;
86
+ const arAttrs = ar
87
+ ? `ar ar-modes="webxr scene-viewer quick-look"`
88
+ : "";
89
+ const rotateAttr = autoRotate ? `auto-rotate auto-rotate-delay="0"` : "";
90
+ const body = `
91
+ <style>
92
+ model-viewer{width:100%;height:100%;background:${bg};--poster-color:${bg}}
93
+ .title{position:absolute;top:16px;left:16px;font-size:18px;font-weight:600;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,0.5);z-index:10}
94
+ .ar-btn{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);padding:10px 24px;background:rgba(66,133,244,0.9);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:500;cursor:pointer;backdrop-filter:blur(8px);z-index:10}
95
+ .ar-btn:hover{background:rgba(66,133,244,1)}
96
+ </style>
97
+ <script type="module" src="${MODEL_VIEWER_CDN}"></script>
98
+ <div class="title">${escapeHtml(title)}</div>
99
+ <model-viewer
100
+ src="${escapeHtml(model)}"
101
+ camera-controls
102
+ touch-action="pan-y"
103
+ camera-orbit="${escapeHtml(orbit)}"
104
+ shadow-intensity="1"
105
+ shadow-softness="0.5"
106
+ exposure="1"
107
+ environment-image="neutral"
108
+ ${rotateAttr}
109
+ ${arAttrs}
110
+ style="width:100%;height:100%"
111
+ >
112
+ ${ar ? `<button slot="ar-button" class="ar-btn">View in AR</button>` : ""}
113
+ </model-viewer>
114
+ ${BRANDING}`;
115
+ return {
116
+ html: htmlShell(title, body),
117
+ title,
118
+ type: "model-viewer",
119
+ };
120
+ }
121
+ // ── chart-3d ─────────────────────────────────────────────────────────────────
122
+ function generateChart3D(input) {
123
+ const title = input.title || "3D Data Visualization";
124
+ const data = input.data;
125
+ const bg = input.options?.backgroundColor || "#1a1a2e";
126
+ const maxVal = Math.max(...data.map((d) => d.value));
127
+ const bars = data
128
+ .map((d, i) => {
129
+ const color = d.color || DEFAULT_COLORS[i % DEFAULT_COLORS.length];
130
+ const heightPct = maxVal > 0 ? (d.value / maxVal) * 100 : 0;
131
+ const height = Math.max(heightPct * 2.5, 8); // px scale, min 8px
132
+ return `
133
+ <div class="bar-group" style="--delay:${i * 0.08}s">
134
+ <div class="bar-wrapper">
135
+ <div class="bar-value">${formatNumber(d.value)}</div>
136
+ <div class="bar" style="height:${height}px;background:linear-gradient(180deg,${color},${darken(color, 0.3)});box-shadow:4px 4px 0 ${darken(color, 0.5)}, 0 0 20px ${color}40">
137
+ <div class="bar-face-top" style="background:${lighten(color, 0.15)}"></div>
138
+ <div class="bar-face-right" style="background:${darken(color, 0.2)}"></div>
139
+ </div>
140
+ </div>
141
+ <div class="bar-label">${escapeHtml(d.label)}</div>
142
+ </div>`;
143
+ })
144
+ .join("\n");
145
+ const body = `
146
+ <style>
147
+ body{background:${bg};display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;perspective:800px;overflow:hidden}
148
+ .chart-title{font-size:22px;font-weight:700;margin-bottom:24px;color:#fff;letter-spacing:-0.5px}
149
+ .chart-subtitle{font-size:13px;color:#888;margin-bottom:32px}
150
+ .chart-container{display:flex;align-items:flex-end;gap:clamp(8px,2vw,24px);padding:24px 32px;transform:rotateX(8deg) rotateY(-8deg);transform-style:preserve-3d;background:rgba(255,255,255,0.03);border-radius:16px;border:1px solid rgba(255,255,255,0.06);max-width:95vw;overflow-x:auto}
151
+ .bar-group{display:flex;flex-direction:column;align-items:center;animation:barIn 0.6s ease-out var(--delay) both}
152
+ .bar-wrapper{position:relative;display:flex;flex-direction:column;align-items:center}
153
+ .bar{width:clamp(28px,6vw,56px);border-radius:4px 4px 0 0;position:relative;transform-style:preserve-3d;transition:height 0.5s ease-out}
154
+ .bar-face-top{position:absolute;top:-6px;left:-2px;right:-2px;height:8px;border-radius:4px 4px 0 0;transform:rotateX(45deg);transform-origin:bottom}
155
+ .bar-face-right{position:absolute;top:0;right:-6px;bottom:0;width:8px;border-radius:0 4px 4px 0;transform:rotateY(45deg);transform-origin:left}
156
+ .bar-value{font-size:12px;font-weight:600;color:#fff;margin-bottom:6px;white-space:nowrap}
157
+ .bar-label{font-size:11px;color:#aaa;margin-top:8px;text-align:center;max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
158
+ @keyframes barIn{from{opacity:0;transform:scaleY(0) translateY(20px)}to{opacity:1;transform:scaleY(1) translateY(0)}}
159
+ .total-row{margin-top:24px;font-size:14px;color:#aaa}
160
+ .total-row strong{color:#fff;font-size:16px}
161
+ </style>
162
+ <div class="chart-title">${escapeHtml(title)}</div>
163
+ <div class="chart-subtitle">${data.length} data points</div>
164
+ <div class="chart-container">
165
+ ${bars}
166
+ </div>
167
+ <div class="total-row">Total: <strong>${formatNumber(data.reduce((s, d) => s + d.value, 0))}</strong></div>
168
+ ${BRANDING}`;
169
+ return {
170
+ html: htmlShell(title, body),
171
+ title,
172
+ type: "chart-3d",
173
+ };
174
+ }
175
+ // ── scene ────────────────────────────────────────────────────────────────────
176
+ function generateScene(input) {
177
+ const model = input.modelUrl || DEFAULT_MODEL;
178
+ const title = input.title || "3D Scene";
179
+ const opts = input.options || {};
180
+ const bg = opts.backgroundColor || "#1a1a2e";
181
+ const autoRotate = opts.autoRotate !== false;
182
+ const ar = opts.ar !== false;
183
+ const orbit = opts.cameraOrbit || "0deg 75deg 150%";
184
+ const arAttrs = ar ? `ar ar-modes="webxr scene-viewer quick-look"` : "";
185
+ const rotateAttr = autoRotate ? `auto-rotate auto-rotate-delay="0"` : "";
186
+ const body = `
187
+ <style>
188
+ model-viewer{width:100%;height:100%;background:${bg};--poster-color:${bg}}
189
+ .scene-title{position:absolute;top:16px;left:16px;font-size:20px;font-weight:700;color:#fff;text-shadow:0 2px 12px rgba(0,0,0,0.6);z-index:10;letter-spacing:-0.3px}
190
+ .scene-info{position:absolute;top:44px;left:16px;font-size:12px;color:#aaa;z-index:10}
191
+ .controls-hint{position:absolute;bottom:16px;left:16px;font-size:11px;color:#666;z-index:10}
192
+ .ar-btn{position:absolute;bottom:16px;right:16px;padding:10px 20px;background:rgba(66,133,244,0.9);color:#fff;border:none;border-radius:24px;font-size:13px;font-weight:500;cursor:pointer;z-index:10}
193
+ </style>
194
+ <script type="module" src="${MODEL_VIEWER_CDN}"></script>
195
+ <div class="scene-title">${escapeHtml(title)}</div>
196
+ <div class="scene-info">Interactive 3D Scene</div>
197
+ <model-viewer
198
+ src="${escapeHtml(model)}"
199
+ camera-controls
200
+ touch-action="pan-y"
201
+ camera-orbit="${escapeHtml(orbit)}"
202
+ shadow-intensity="1.5"
203
+ shadow-softness="0.8"
204
+ exposure="1.1"
205
+ environment-image="neutral"
206
+ tone-mapping="commerce"
207
+ ${rotateAttr}
208
+ ${arAttrs}
209
+ style="width:100%;height:100%"
210
+ >
211
+ ${ar ? `<button slot="ar-button" class="ar-btn">View in AR</button>` : ""}
212
+ </model-viewer>
213
+ <div class="controls-hint">Drag to orbit &bull; Scroll to zoom &bull; Two-finger to pan</div>
214
+ ${BRANDING}`;
215
+ return {
216
+ html: htmlShell(title, body),
217
+ title,
218
+ type: "scene",
219
+ };
220
+ }
221
+ // ── product-360 ──────────────────────────────────────────────────────────────
222
+ function generateProduct360(input) {
223
+ const model = input.modelUrl || DEFAULT_MODEL;
224
+ const title = input.title || "Product 360\u00B0 View";
225
+ const opts = input.options || {};
226
+ const bg = opts.backgroundColor || "#1a1a2e";
227
+ const orbit = opts.cameraOrbit || "0deg 75deg 105%";
228
+ const ar = opts.ar !== false;
229
+ const hotspots = input.hotspots || [];
230
+ const hotspotHtml = hotspots
231
+ .map((h, i) => `
232
+ <button class="hotspot" slot="hotspot-${i}"
233
+ data-position="${escapeHtml(h.position)}"
234
+ data-normal="${escapeHtml(h.normal)}"
235
+ data-visibility-attribute="visible">
236
+ <div class="hotspot-annotation">
237
+ <div class="hotspot-title">${escapeHtml(h.label)}</div>
238
+ ${h.description ? `<div class="hotspot-desc">${escapeHtml(h.description)}</div>` : ""}
239
+ </div>
240
+ </button>`)
241
+ .join("\n");
242
+ const body = `
243
+ <style>
244
+ model-viewer{width:100%;height:100%;background:${bg};--poster-color:${bg}}
245
+ .product-header{position:absolute;top:0;left:0;right:0;padding:16px 20px;background:linear-gradient(180deg,rgba(0,0,0,0.6),transparent);z-index:10}
246
+ .product-title{font-size:20px;font-weight:700;color:#fff}
247
+ .product-subtitle{font-size:12px;color:#aaa;margin-top:2px}
248
+ .hotspot{display:block;width:24px;height:24px;border-radius:50%;border:2px solid #fff;background:rgba(66,133,244,0.8);cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.4);transition:transform 0.2s}
249
+ .hotspot:hover{transform:scale(1.3)}
250
+ .hotspot-annotation{position:absolute;bottom:32px;left:50%;transform:translateX(-50%);background:rgba(30,30,50,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:8px 12px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity 0.2s}
251
+ .hotspot:hover .hotspot-annotation{opacity:1}
252
+ .hotspot-title{font-size:13px;font-weight:600;color:#fff}
253
+ .hotspot-desc{font-size:11px;color:#aaa;margin-top:2px}
254
+ .ar-strip{position:absolute;bottom:0;left:0;right:0;padding:12px 20px;background:linear-gradient(0deg,rgba(0,0,0,0.6),transparent);display:flex;align-items:center;justify-content:space-between;z-index:10}
255
+ .ar-btn{padding:10px 24px;background:rgba(66,133,244,0.9);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:500;cursor:pointer}
256
+ .ar-btn:hover{background:rgba(66,133,244,1)}
257
+ .rotate-hint{font-size:11px;color:#666}
258
+ </style>
259
+ <script type="module" src="${MODEL_VIEWER_CDN}"></script>
260
+ <div class="product-header">
261
+ <div class="product-title">${escapeHtml(title)}</div>
262
+ <div class="product-subtitle">Drag to rotate &bull; Pinch to zoom${hotspots.length > 0 ? " &bull; Tap hotspots for details" : ""}</div>
263
+ </div>
264
+ <model-viewer
265
+ src="${escapeHtml(model)}"
266
+ camera-controls
267
+ touch-action="pan-y"
268
+ camera-orbit="${escapeHtml(orbit)}"
269
+ auto-rotate
270
+ auto-rotate-delay="3000"
271
+ shadow-intensity="1"
272
+ shadow-softness="0.5"
273
+ exposure="1"
274
+ environment-image="neutral"
275
+ tone-mapping="commerce"
276
+ interaction-prompt="auto"
277
+ ${ar ? `ar ar-modes="webxr scene-viewer quick-look"` : ""}
278
+ style="width:100%;height:100%"
279
+ >
280
+ ${hotspotHtml}
281
+ ${ar ? `<button slot="ar-button" class="ar-btn">View in Your Space</button>` : ""}
282
+ </model-viewer>
283
+ <div class="ar-strip">
284
+ <div class="rotate-hint">360&deg; interactive view</div>
285
+ </div>
286
+ ${BRANDING}`;
287
+ return {
288
+ html: htmlShell(title, body),
289
+ title,
290
+ type: "product-360",
291
+ };
292
+ }
293
+ // ─── Utilities ───────────────────────────────────────────────────────────────
294
+ function formatNumber(n) {
295
+ if (n >= 1_000_000)
296
+ return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
297
+ if (n >= 1_000)
298
+ return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
299
+ return n.toLocaleString("en-US");
300
+ }
301
+ /** Darken a hex colour by a fraction (0..1). */
302
+ function darken(hex, amount) {
303
+ const rgb = hexToRgb(hex);
304
+ return rgbToHex(Math.round(rgb.r * (1 - amount)), Math.round(rgb.g * (1 - amount)), Math.round(rgb.b * (1 - amount)));
305
+ }
306
+ /** Lighten a hex colour by a fraction (0..1). */
307
+ function lighten(hex, amount) {
308
+ const rgb = hexToRgb(hex);
309
+ return rgbToHex(Math.min(255, Math.round(rgb.r + (255 - rgb.r) * amount)), Math.min(255, Math.round(rgb.g + (255 - rgb.g) * amount)), Math.min(255, Math.round(rgb.b + (255 - rgb.b) * amount)));
310
+ }
311
+ function hexToRgb(hex) {
312
+ const h = hex.replace("#", "");
313
+ return {
314
+ r: parseInt(h.substring(0, 2), 16),
315
+ g: parseInt(h.substring(2, 4), 16),
316
+ b: parseInt(h.substring(4, 6), 16),
317
+ };
318
+ }
319
+ function rgbToHex(r, g, b) {
320
+ return "#" + [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("");
321
+ }
322
+ // ─── Format response ─────────────────────────────────────────────────────────
323
+ export function formatArtifactResponse(result) {
324
+ const lines = [
325
+ `## ${result.title}`,
326
+ ``,
327
+ `Here is your interactive 3D ${result.type === "chart-3d" ? "chart" : "content"}:`,
328
+ ``,
329
+ `\`\`\`html`,
330
+ result.html,
331
+ `\`\`\``,
332
+ ``,
333
+ `**How to use this:**`,
334
+ `- Copy the HTML above into a file and open in a browser`,
335
+ `- Or paste into any HTML preview tool`,
336
+ `- On mobile: tap "View in AR" to see it in your space`,
337
+ `- Drag to orbit, scroll/pinch to zoom`,
338
+ ``,
339
+ `---`,
340
+ `*Powered by SceneView \u2014 3D & AR for every platform*`,
341
+ ];
342
+ return lines.join("\n");
343
+ }
package/dist/index.js CHANGED
@@ -12,7 +12,19 @@ import { fetchKnownIssues } from "./issues.js";
12
12
  import { parseNodeSections, findNodeSection, listNodeTypes } from "./node-reference.js";
13
13
  import { PLATFORM_ROADMAP, BEST_PRACTICES, AR_SETUP_GUIDE, TROUBLESHOOTING_GUIDE } from "./guides.js";
14
14
  import { buildPreviewUrl, validatePreviewInput, formatPreviewResponse } from "./preview.js";
15
+ import { validateArtifactInput, generateArtifact, formatArtifactResponse } from "./artifact.js";
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ // ─── Legal disclaimer ─────────────────────────────────────────────────────────
18
+ const DISCLAIMER = '\n\n---\n*Generated code suggestion. Review before use in production. See [TERMS.md](https://github.com/SceneView/sceneview/blob/main/mcp/TERMS.md).*';
19
+ function withDisclaimer(content) {
20
+ if (content.length === 0)
21
+ return content;
22
+ const last = content[content.length - 1];
23
+ return [
24
+ ...content.slice(0, -1),
25
+ { ...last, text: last.text + DISCLAIMER },
26
+ ];
27
+ }
16
28
  let API_DOCS;
17
29
  try {
18
30
  API_DOCS = readFileSync(resolve(__dirname, "../llms.txt"), "utf-8");
@@ -21,7 +33,7 @@ catch {
21
33
  API_DOCS = "SceneView API docs not found. Run `npm run prepare` to bundle llms.txt.";
22
34
  }
23
35
  const NODE_SECTIONS = parseNodeSections(API_DOCS);
24
- const server = new Server({ name: "@sceneview/mcp", version: "3.3.0" }, { capabilities: { resources: {}, tools: {} } });
36
+ const server = new Server({ name: "@sceneview/mcp", version: "3.4.9" }, { capabilities: { resources: {}, tools: {} } });
25
37
  // ─── Resources ───────────────────────────────────────────────────────────────
26
38
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
27
39
  resources: [
@@ -234,6 +246,66 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
234
246
  required: [],
235
247
  },
236
248
  },
249
+ {
250
+ name: "create_3d_artifact",
251
+ description: 'Generates a complete, self-contained HTML page with interactive 3D content that Claude can render as an artifact. Returns valid HTML using model-viewer (Google\'s web component for 3D). Use this when the user asks to "show", "preview", "visualize" 3D models, create 3D charts/dashboards, or view products in 360°. The HTML works standalone in any browser, supports AR on mobile, and includes orbit controls. Types: "model-viewer" for 3D model viewing, "chart-3d" for 3D data visualization (bar charts with perspective), "scene" for rich 3D scenes with lighting, "product-360" for product turntables with hotspot annotations.',
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ type: {
256
+ type: "string",
257
+ enum: ["model-viewer", "chart-3d", "scene", "product-360"],
258
+ description: '"model-viewer": interactive 3D model viewer with orbit + AR. "chart-3d": 3D bar chart for data visualization (revenue, analytics). "scene": rich 3D scene with lighting and environment. "product-360": product turntable with hotspot annotations + AR.',
259
+ },
260
+ modelUrl: {
261
+ type: "string",
262
+ description: "Public HTTPS URL to a .glb or .gltf model. If omitted, a default model is used. Not needed for chart-3d type.",
263
+ },
264
+ title: {
265
+ type: "string",
266
+ description: "Title displayed on the artifact. Defaults to a sensible name per type.",
267
+ },
268
+ data: {
269
+ type: "array",
270
+ items: {
271
+ type: "object",
272
+ properties: {
273
+ label: { type: "string", description: "Data point label (e.g. 'Q1 2025')" },
274
+ value: { type: "number", description: "Numeric value" },
275
+ color: { type: "string", description: "Optional hex color (e.g. '#4285F4'). Auto-assigned if omitted." },
276
+ },
277
+ required: ["label", "value"],
278
+ },
279
+ description: 'Data array for chart-3d type. Each item has {label, value, color?}. Required for "chart-3d", ignored for other types.',
280
+ },
281
+ options: {
282
+ type: "object",
283
+ properties: {
284
+ autoRotate: { type: "boolean", description: "Auto-rotate the model (default: true)" },
285
+ ar: { type: "boolean", description: "Enable AR on mobile devices (default: true)" },
286
+ backgroundColor: { type: "string", description: "Background color as hex (default: '#1a1a2e')" },
287
+ cameraOrbit: { type: "string", description: "Camera orbit string (default: '0deg 75deg 105%')" },
288
+ },
289
+ description: "Visual options for the 3D artifact.",
290
+ },
291
+ hotspots: {
292
+ type: "array",
293
+ items: {
294
+ type: "object",
295
+ properties: {
296
+ position: { type: "string", description: 'Hotspot 3D position, e.g. "0.5 1.2 0.3"' },
297
+ normal: { type: "string", description: 'Hotspot surface normal, e.g. "0 1 0"' },
298
+ label: { type: "string", description: "Hotspot label" },
299
+ description: { type: "string", description: "Hotspot description" },
300
+ },
301
+ required: ["position", "normal", "label"],
302
+ },
303
+ description: "Annotation hotspots for product-360 type. Each has position, normal, label, and optional description.",
304
+ },
305
+ },
306
+ required: ["type"],
307
+ },
308
+ },
237
309
  ],
238
310
  }));
239
311
  // ─── Tool handlers ────────────────────────────────────────────────────────────
@@ -271,7 +343,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
271
343
  const codeLang = isIos ? "swift" : "kotlin";
272
344
  const codeLabel = isIos ? "**Swift (SwiftUI):**" : "**Kotlin (Jetpack Compose):**";
273
345
  return {
274
- content: [
346
+ content: withDisclaimer([
275
347
  {
276
348
  type: "text",
277
349
  text: [
@@ -290,7 +362,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
290
362
  `> ${sample.prompt}`,
291
363
  ].join("\n"),
292
364
  },
293
- ],
365
+ ]),
294
366
  };
295
367
  }
296
368
  // ── list_samples ──────────────────────────────────────────────────────────
@@ -316,14 +388,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
316
388
  return `### \`${s.id}\`\n**${s.title}**${s.language === "swift" ? " (Swift/iOS)" : ""}\n${s.description}\n*Tags:* ${s.tags.join(", ")}\n${depLabel} \`${s.dependency}\`\n\nCall \`get_sample("${s.id}")\` for the full code.`;
317
389
  })
318
390
  .join("\n\n---\n\n");
319
- return { content: [{ type: "text", text: header + rows }] };
391
+ return { content: withDisclaimer([{ type: "text", text: header + rows }]) };
320
392
  }
321
393
  // ── get_setup ─────────────────────────────────────────────────────────────
322
394
  case "get_setup": {
323
395
  const type = request.params.arguments?.type;
324
396
  if (type === "3d") {
325
397
  return {
326
- content: [
398
+ content: withDisclaimer([
327
399
  {
328
400
  type: "text",
329
401
  text: [
@@ -339,12 +411,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
339
411
  `No manifest changes required for 3D-only scenes.`,
340
412
  ].join("\n"),
341
413
  },
342
- ],
414
+ ]),
343
415
  };
344
416
  }
345
417
  if (type === "ar") {
346
418
  return {
347
- content: [
419
+ content: withDisclaimer([
348
420
  {
349
421
  type: "text",
350
422
  text: [
@@ -367,7 +439,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
367
439
  `\`\`\``,
368
440
  ].join("\n"),
369
441
  },
370
- ],
442
+ ]),
371
443
  };
372
444
  }
373
445
  return {
@@ -386,11 +458,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
386
458
  }
387
459
  const issues = validateCode(code);
388
460
  const report = formatValidationReport(issues);
389
- return { content: [{ type: "text", text: report }] };
461
+ return { content: withDisclaimer([{ type: "text", text: report }]) };
390
462
  }
391
463
  // ── get_migration_guide ───────────────────────────────────────────────────
392
464
  case "get_migration_guide": {
393
- return { content: [{ type: "text", text: MIGRATION_GUIDE }] };
465
+ return { content: withDisclaimer([{ type: "text", text: MIGRATION_GUIDE }]) };
394
466
  }
395
467
  // ── get_node_reference ────────────────────────────────────────────────────
396
468
  case "get_node_reference": {
@@ -419,7 +491,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
419
491
  };
420
492
  }
421
493
  return {
422
- content: [
494
+ content: withDisclaimer([
423
495
  {
424
496
  type: "text",
425
497
  text: [
@@ -428,33 +500,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
428
500
  section.content,
429
501
  ].join("\n"),
430
502
  },
431
- ],
503
+ ]),
432
504
  };
433
505
  }
434
506
  // ── get_platform_roadmap ────────────────────────────────────────────────
435
507
  case "get_platform_roadmap": {
436
- return { content: [{ type: "text", text: PLATFORM_ROADMAP }] };
508
+ return { content: withDisclaimer([{ type: "text", text: PLATFORM_ROADMAP }]) };
437
509
  }
438
510
  // ── get_best_practices ───────────────────────────────────────────────────
439
511
  case "get_best_practices": {
440
512
  const category = request.params.arguments?.category || "all";
441
513
  const text = BEST_PRACTICES[category] ?? BEST_PRACTICES["all"];
442
- return { content: [{ type: "text", text }] };
514
+ return { content: withDisclaimer([{ type: "text", text }]) };
443
515
  }
444
516
  // ── get_ar_setup ─────────────────────────────────────────────────────────
445
517
  case "get_ar_setup": {
446
- return { content: [{ type: "text", text: AR_SETUP_GUIDE }] };
518
+ return { content: withDisclaimer([{ type: "text", text: AR_SETUP_GUIDE }]) };
447
519
  }
448
520
  // ── get_troubleshooting ──────────────────────────────────────────────────
449
521
  case "get_troubleshooting": {
450
- return { content: [{ type: "text", text: TROUBLESHOOTING_GUIDE }] };
522
+ return { content: withDisclaimer([{ type: "text", text: TROUBLESHOOTING_GUIDE }]) };
451
523
  }
452
524
  // ── get_ios_setup ─────────────────────────────────────────────────────────
453
525
  case "get_ios_setup": {
454
526
  const iosType = request.params.arguments?.type;
455
527
  if (iosType === "3d") {
456
528
  return {
457
- content: [
529
+ content: withDisclaimer([
458
530
  {
459
531
  type: "text",
460
532
  text: [
@@ -534,12 +606,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
534
606
  `No manifest or permission changes needed for 3D-only scenes.`,
535
607
  ].join("\n"),
536
608
  },
537
- ],
609
+ ]),
538
610
  };
539
611
  }
540
612
  if (iosType === "ar") {
541
613
  return {
542
- content: [
614
+ content: withDisclaimer([
543
615
  {
544
616
  type: "text",
545
617
  text: [
@@ -625,7 +697,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
625
697
  `\`\`\``,
626
698
  ].join("\n"),
627
699
  },
628
- ],
700
+ ]),
629
701
  };
630
702
  }
631
703
  return {
@@ -636,7 +708,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
636
708
  // ── get_web_setup ────────────────────────────────────────────────────────
637
709
  case "get_web_setup": {
638
710
  return {
639
- content: [
711
+ content: withDisclaimer([
640
712
  {
641
713
  type: "text",
642
714
  text: [
@@ -721,7 +793,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
721
793
  `- glTF/GLB format only (same as Android)`,
722
794
  ].join("\n"),
723
795
  },
724
- ],
796
+ ]),
725
797
  };
726
798
  }
727
799
  // ── render_3d_preview ──────────────────────────────────────────────────
@@ -740,7 +812,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
740
812
  }
741
813
  const result = buildPreviewUrl({ modelUrl, codeSnippet, autoRotate, ar, title });
742
814
  const text = formatPreviewResponse(result);
743
- return { content: [{ type: "text", text }] };
815
+ return { content: withDisclaimer([{ type: "text", text }]) };
816
+ }
817
+ // ── create_3d_artifact ───────────────────────────────────────────────────
818
+ case "create_3d_artifact": {
819
+ const artifactInput = {
820
+ type: request.params.arguments?.type,
821
+ modelUrl: request.params.arguments?.modelUrl,
822
+ title: request.params.arguments?.title,
823
+ data: request.params.arguments?.data,
824
+ options: request.params.arguments?.options,
825
+ hotspots: request.params.arguments?.hotspots,
826
+ };
827
+ const validationError = validateArtifactInput(artifactInput);
828
+ if (validationError) {
829
+ return {
830
+ content: [{ type: "text", text: `Error: ${validationError}` }],
831
+ isError: true,
832
+ };
833
+ }
834
+ const result = generateArtifact(artifactInput);
835
+ const text = formatArtifactResponse(result);
836
+ return { content: withDisclaimer([{ type: "text", text }]) };
744
837
  }
745
838
  default:
746
839
  return {
package/dist/samples.js CHANGED
@@ -181,6 +181,50 @@ fun PointCloudScreen() {
181
181
  ) {
182
182
  // Render point cloud model instances at detected positions
183
183
  }
184
+ }`,
185
+ },
186
+ "ar-face-mesh": {
187
+ id: "ar-face-mesh",
188
+ title: "AR Face Mesh",
189
+ description: "AR face tracking with AugmentedFaceNode — applies a textured mesh overlay to detected faces using the front camera.",
190
+ tags: ["ar", "face-tracking", "model"],
191
+ dependency: "io.github.sceneview:arsceneview:3.3.0",
192
+ prompt: "Create an AR screen that uses the front camera to detect faces and overlay a 3D mesh on them. Use SceneView `io.github.sceneview:arsceneview:3.3.0`.",
193
+ code: `@Composable
194
+ fun ARFaceMeshScreen() {
195
+ val engine = rememberEngine()
196
+ val modelLoader = rememberModelLoader(engine)
197
+ val materialLoader = rememberMaterialLoader(engine)
198
+ var trackedFaces by remember { mutableStateOf(listOf<AugmentedFace>()) }
199
+
200
+ val faceMaterial = remember(materialLoader) {
201
+ materialLoader.createColorInstance(
202
+ color = Color(0.8f, 0.6f, 0.4f, 0.5f),
203
+ metallic = 0f,
204
+ roughness = 0.9f
205
+ )
206
+ }
207
+
208
+ ARScene(
209
+ modifier = Modifier.fillMaxSize(),
210
+ engine = engine,
211
+ modelLoader = modelLoader,
212
+ sessionFeatures = setOf(Session.Feature.FRONT_CAMERA),
213
+ sessionConfiguration = { _, config ->
214
+ config.augmentedFaceMode = Config.AugmentedFaceMode.MESH3D
215
+ },
216
+ onSessionUpdated = { session, _ ->
217
+ trackedFaces = session.getAllTrackables(AugmentedFace::class.java)
218
+ .filter { it.trackingState == TrackingState.TRACKING }
219
+ }
220
+ ) {
221
+ trackedFaces.forEach { face ->
222
+ AugmentedFaceNode(
223
+ augmentedFace = face,
224
+ meshMaterialInstance = faceMaterial
225
+ )
226
+ }
227
+ }
184
228
  }`,
185
229
  },
186
230
  "gltf-camera": {
@@ -242,6 +286,67 @@ fun CameraManipulatorScreen() {
242
286
  ModelNode(modelInstance = instance, scaleToUnits = 1.0f)
243
287
  }
244
288
  }
289
+ }`,
290
+ },
291
+ "camera-animation": {
292
+ id: "camera-animation",
293
+ title: "Camera Animation",
294
+ description: "Animated camera flythrough around a 3D model — smooth orbit using LaunchedEffect and trigonometric interpolation.",
295
+ tags: ["3d", "camera", "animation", "model"],
296
+ dependency: "io.github.sceneview:sceneview:3.3.0",
297
+ prompt: "Create a 3D scene with a camera that automatically orbits around a model in a smooth circle. Include a play/pause button. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
298
+ code: `@Composable
299
+ fun CameraAnimationScreen() {
300
+ val engine = rememberEngine()
301
+ val modelLoader = rememberModelLoader(engine)
302
+ val environmentLoader = rememberEnvironmentLoader(engine)
303
+ var isOrbiting by remember { mutableStateOf(true) }
304
+ var angle by remember { mutableFloatStateOf(0f) }
305
+
306
+ val cameraNode = rememberCameraNode(engine) {
307
+ position = Position(x = 0f, y = 1.5f, z = 4f)
308
+ lookAt(Position(0f, 0f, 0f))
309
+ }
310
+
311
+ // Animate camera orbit
312
+ LaunchedEffect(isOrbiting) {
313
+ while (isOrbiting) {
314
+ withFrameNanos { _ ->
315
+ angle += 0.5f
316
+ val radians = Math.toRadians(angle.toDouble())
317
+ cameraNode.position = Position(
318
+ x = (4f * sin(radians)).toFloat(),
319
+ y = 1.5f,
320
+ z = (4f * cos(radians)).toFloat()
321
+ )
322
+ cameraNode.lookAt(Position(0f, 0f, 0f))
323
+ }
324
+ }
325
+ }
326
+
327
+ Column {
328
+ Scene(
329
+ modifier = Modifier.weight(1f).fillMaxWidth(),
330
+ engine = engine,
331
+ modelLoader = modelLoader,
332
+ cameraNode = cameraNode,
333
+ environment = rememberEnvironment(environmentLoader) {
334
+ environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
335
+ ?: createEnvironment(environmentLoader)
336
+ },
337
+ mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f }
338
+ ) {
339
+ rememberModelInstance(modelLoader, "models/damaged_helmet.glb")?.let { instance ->
340
+ ModelNode(modelInstance = instance, scaleToUnits = 1.0f)
341
+ }
342
+ }
343
+ Button(
344
+ onClick = { isOrbiting = !isOrbiting },
345
+ modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp)
346
+ ) {
347
+ Text(if (isOrbiting) "Stop Orbit" else "Start Orbit")
348
+ }
349
+ }
245
350
  }`,
246
351
  },
247
352
  "autopilot-demo": {
@@ -466,6 +571,465 @@ fun PostProcessingScreen() {
466
571
  }
467
572
  // Configure view.bloomOptions, view.vignetteOptions, etc.
468
573
  // See samples/post-processing for full interactive controls
574
+ }`,
575
+ },
576
+ "video-texture": {
577
+ id: "video-texture",
578
+ title: "Video Texture",
579
+ description: "Video playback on a 3D plane using VideoNode with MediaPlayer — supports looping, chroma-key, and auto-sizing.",
580
+ tags: ["3d", "video", "model"],
581
+ dependency: "io.github.sceneview:sceneview:3.3.0",
582
+ prompt: "Create a 3D scene with a video playing on a floating 3D plane. Include play/pause controls and chroma-key support. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
583
+ code: `@Composable
584
+ fun VideoTextureScreen() {
585
+ val context = LocalContext.current
586
+ val engine = rememberEngine()
587
+ var isPlaying by remember { mutableStateOf(true) }
588
+
589
+ val player = remember {
590
+ MediaPlayer().apply {
591
+ setDataSource(context, Uri.parse("android.resource://\${context.packageName}/raw/video"))
592
+ isLooping = true
593
+ prepare()
594
+ start()
595
+ }
596
+ }
597
+ DisposableEffect(Unit) { onDispose { player.release() } }
598
+
599
+ Column {
600
+ Scene(
601
+ modifier = Modifier.weight(1f).fillMaxWidth(),
602
+ engine = engine
603
+ ) {
604
+ VideoNode(
605
+ player = player,
606
+ // size = null auto-sizes from video aspect ratio (longer edge = 1 unit)
607
+ position = Position(z = -2f),
608
+ chromaKeyColor = null // set to android.graphics.Color.GREEN for green-screen
609
+ )
610
+ }
611
+ Row(
612
+ modifier = Modifier.fillMaxWidth().padding(16.dp),
613
+ horizontalArrangement = Arrangement.Center,
614
+ verticalAlignment = Alignment.CenterVertically
615
+ ) {
616
+ Button(onClick = {
617
+ if (isPlaying) player.pause() else player.start()
618
+ isPlaying = !isPlaying
619
+ }) {
620
+ Text(if (isPlaying) "Pause" else "Play")
621
+ }
622
+ }
623
+ }
624
+ }`,
625
+ },
626
+ "multi-model-scene": {
627
+ id: "multi-model-scene",
628
+ title: "Multi-Model Scene",
629
+ description: "Scene with multiple 3D models loaded independently, positioned and scaled to create a complete environment.",
630
+ tags: ["3d", "model", "multi-model", "environment"],
631
+ dependency: "io.github.sceneview:sceneview:3.3.0",
632
+ prompt: "Create a 3D scene that loads multiple GLB models (a car, a building, and trees) and positions them to form a street scene. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
633
+ code: `@Composable
634
+ fun MultiModelScreen() {
635
+ val engine = rememberEngine()
636
+ val modelLoader = rememberModelLoader(engine)
637
+ val materialLoader = rememberMaterialLoader(engine)
638
+ val environmentLoader = rememberEnvironmentLoader(engine)
639
+
640
+ Scene(
641
+ modifier = Modifier.fillMaxSize(),
642
+ engine = engine,
643
+ modelLoader = modelLoader,
644
+ cameraManipulator = rememberCameraManipulator(
645
+ orbitHomePosition = Position(x = 0f, y = 3f, z = 8f),
646
+ targetPosition = Position(0f, 0f, 0f)
647
+ ),
648
+ environment = rememberEnvironment(environmentLoader) {
649
+ environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
650
+ ?: createEnvironment(environmentLoader)
651
+ },
652
+ mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f }
653
+ ) {
654
+ // Ground plane
655
+ val groundMat = remember(materialLoader) {
656
+ materialLoader.createColorInstance(Color.DarkGray, roughness = 0.9f)
657
+ }
658
+ PlaneNode(size = Size(20f, 20f), materialInstance = groundMat)
659
+
660
+ // Car in the center
661
+ rememberModelInstance(modelLoader, "models/car.glb")?.let { car ->
662
+ ModelNode(
663
+ modelInstance = car,
664
+ scaleToUnits = 2.0f,
665
+ position = Position(x = 0f, y = 0f, z = 0f),
666
+ autoAnimate = true
667
+ )
668
+ }
669
+
670
+ // Building on the left
671
+ rememberModelInstance(modelLoader, "models/building.glb")?.let { building ->
672
+ ModelNode(
673
+ modelInstance = building,
674
+ scaleToUnits = 5.0f,
675
+ position = Position(x = -6f, y = 0f, z = -3f)
676
+ )
677
+ }
678
+
679
+ // Trees along the right side
680
+ for (i in 0..2) {
681
+ rememberModelInstance(modelLoader, "models/tree.glb")?.let { tree ->
682
+ ModelNode(
683
+ modelInstance = tree,
684
+ scaleToUnits = 3.0f,
685
+ position = Position(x = 5f, y = 0f, z = i * -3f)
686
+ )
687
+ }
688
+ }
689
+ }
690
+ }`,
691
+ },
692
+ "gesture-interaction": {
693
+ id: "gesture-interaction",
694
+ title: "Gesture Interaction",
695
+ description: "Full gesture handling — tap to select, double-tap to scale, long-press for info, pinch-to-scale, drag-to-move on editable nodes.",
696
+ tags: ["3d", "gestures", "model"],
697
+ dependency: "io.github.sceneview:sceneview:3.3.0",
698
+ prompt: "Create a 3D scene with a model that responds to tap (select), double-tap (scale up), long-press (show info), and supports pinch-to-scale and drag-to-move. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
699
+ code: `@Composable
700
+ fun GestureInteractionScreen() {
701
+ val engine = rememberEngine()
702
+ val modelLoader = rememberModelLoader(engine)
703
+ val environmentLoader = rememberEnvironmentLoader(engine)
704
+ var selectedNode by remember { mutableStateOf<String?>(null) }
705
+ var infoText by remember { mutableStateOf("Tap a model to select it") }
706
+
707
+ Box(modifier = Modifier.fillMaxSize()) {
708
+ Scene(
709
+ modifier = Modifier.fillMaxSize(),
710
+ engine = engine,
711
+ modelLoader = modelLoader,
712
+ cameraManipulator = rememberCameraManipulator(),
713
+ environment = rememberEnvironment(environmentLoader) {
714
+ environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
715
+ ?: createEnvironment(environmentLoader)
716
+ },
717
+ onGestureListener = rememberOnGestureListener(
718
+ onSingleTapConfirmed = { event, node ->
719
+ selectedNode = node?.name
720
+ infoText = if (node != null) "Selected: \${node.name}" else "Tap a model to select it"
721
+ },
722
+ onDoubleTap = { event, node ->
723
+ node?.let {
724
+ it.scale = if (it.scale.x > 1.5f) Scale(1f) else Scale(2f)
725
+ infoText = "Double-tap: toggled scale"
726
+ }
727
+ },
728
+ onLongPress = { event, node ->
729
+ node?.let {
730
+ infoText = "Position: \${it.worldPosition}, Scale: \${it.scale}"
731
+ }
732
+ }
733
+ )
734
+ ) {
735
+ rememberModelInstance(modelLoader, "models/damaged_helmet.glb")?.let { instance ->
736
+ ModelNode(
737
+ modelInstance = instance,
738
+ scaleToUnits = 1.0f,
739
+ isEditable = true, // enables pinch-to-scale and drag-to-move
740
+ autoAnimate = true
741
+ )
742
+ }
743
+ }
744
+
745
+ // Info overlay
746
+ Text(
747
+ text = infoText,
748
+ modifier = Modifier.align(Alignment.TopCenter).padding(24.dp)
749
+ .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), RoundedCornerShape(8.dp))
750
+ .padding(12.dp),
751
+ style = MaterialTheme.typography.bodyMedium
752
+ )
753
+ }
754
+ }`,
755
+ },
756
+ "environment-lighting": {
757
+ id: "environment-lighting",
758
+ title: "Environment & Lighting",
759
+ description: "Complete lighting setup — HDR environment (IBL + skybox), main directional light, point light, and spot light with LightNode.",
760
+ tags: ["3d", "environment", "lighting", "model"],
761
+ dependency: "io.github.sceneview:sceneview:3.3.0",
762
+ prompt: "Create a 3D scene with full HDR environment lighting (IBL + skybox), a directional sun light, a red point light, and a blue spot light. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
763
+ code: `@Composable
764
+ fun EnvironmentLightingScreen() {
765
+ val engine = rememberEngine()
766
+ val modelLoader = rememberModelLoader(engine)
767
+ val materialLoader = rememberMaterialLoader(engine)
768
+ val environmentLoader = rememberEnvironmentLoader(engine)
769
+
770
+ Scene(
771
+ modifier = Modifier.fillMaxSize(),
772
+ engine = engine,
773
+ modelLoader = modelLoader,
774
+ cameraManipulator = rememberCameraManipulator(
775
+ orbitHomePosition = Position(x = 0f, y = 2f, z = 5f),
776
+ targetPosition = Position(0f, 0f, 0f)
777
+ ),
778
+ // HDR environment provides both IBL (indirect lighting) and skybox (background)
779
+ environment = rememberEnvironment(environmentLoader) {
780
+ environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
781
+ ?: createEnvironment(environmentLoader)
782
+ },
783
+ // Main directional light (sun)
784
+ mainLightNode = rememberMainLightNode(engine) {
785
+ intensity = 100_000f
786
+ // castShadows is true by default for the main light
787
+ }
788
+ ) {
789
+ // Floor to receive shadows
790
+ val floorMat = remember(materialLoader) {
791
+ materialLoader.createColorInstance(Color.LightGray, roughness = 0.8f)
792
+ }
793
+ PlaneNode(size = Size(10f, 10f), materialInstance = floorMat)
794
+
795
+ // Model
796
+ rememberModelInstance(modelLoader, "models/damaged_helmet.glb")?.let { instance ->
797
+ ModelNode(modelInstance = instance, scaleToUnits = 1.0f, position = Position(y = 0.5f))
798
+ }
799
+
800
+ // Red point light on the left
801
+ LightNode(
802
+ type = LightManager.Type.POINT,
803
+ apply = {
804
+ color(1.0f, 0.2f, 0.2f)
805
+ intensity(200_000f)
806
+ falloff(5.0f)
807
+ },
808
+ position = Position(x = -2f, y = 2f, z = 1f)
809
+ )
810
+
811
+ // Blue spot light on the right
812
+ LightNode(
813
+ type = LightManager.Type.SPOT,
814
+ apply = {
815
+ color(0.2f, 0.4f, 1.0f)
816
+ intensity(300_000f)
817
+ falloff(8.0f)
818
+ castShadows(true)
819
+ },
820
+ position = Position(x = 2f, y = 3f, z = 1f)
821
+ )
822
+ }
823
+ }`,
824
+ },
825
+ "procedural-geometry": {
826
+ id: "procedural-geometry",
827
+ title: "Procedural Geometry",
828
+ description: "Procedural shapes — CubeNode, SphereNode, CylinderNode, PlaneNode — with PBR materials (metallic, roughness, color).",
829
+ tags: ["3d", "geometry", "model"],
830
+ dependency: "io.github.sceneview:sceneview:3.3.0",
831
+ prompt: "Create a 3D scene showing procedural geometry shapes (cube, sphere, cylinder, plane) with different PBR materials. No model files needed. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
832
+ code: `@Composable
833
+ fun ProceduralGeometryScreen() {
834
+ val engine = rememberEngine()
835
+ val materialLoader = rememberMaterialLoader(engine)
836
+ val environmentLoader = rememberEnvironmentLoader(engine)
837
+
838
+ Scene(
839
+ modifier = Modifier.fillMaxSize(),
840
+ engine = engine,
841
+ cameraManipulator = rememberCameraManipulator(
842
+ orbitHomePosition = Position(x = 0f, y = 2f, z = 6f),
843
+ targetPosition = Position(0f, 0.5f, 0f)
844
+ ),
845
+ environment = rememberEnvironment(environmentLoader) {
846
+ environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
847
+ ?: createEnvironment(environmentLoader)
848
+ },
849
+ mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f }
850
+ ) {
851
+ // Floor
852
+ val floorMat = remember(materialLoader) {
853
+ materialLoader.createColorInstance(Color.DarkGray, roughness = 0.9f)
854
+ }
855
+ PlaneNode(size = Size(8f, 8f), materialInstance = floorMat)
856
+
857
+ // Red matte cube
858
+ val redMat = remember(materialLoader) {
859
+ materialLoader.createColorInstance(Color.Red, metallic = 0f, roughness = 0.6f)
860
+ }
861
+ CubeNode(
862
+ size = Size(0.6f),
863
+ center = Position(0f, 0.3f, 0f),
864
+ materialInstance = redMat,
865
+ position = Position(x = -2f)
866
+ )
867
+
868
+ // Chrome sphere
869
+ val chromeMat = remember(materialLoader) {
870
+ materialLoader.createColorInstance(Color.Gray, metallic = 1f, roughness = 0.05f, reflectance = 0.9f)
871
+ }
872
+ SphereNode(
873
+ radius = 0.4f,
874
+ materialInstance = chromeMat,
875
+ position = Position(x = -0.7f, y = 0.4f)
876
+ )
877
+
878
+ // Green cylinder
879
+ val greenMat = remember(materialLoader) {
880
+ materialLoader.createColorInstance(Color.Green, metallic = 0.2f, roughness = 0.4f)
881
+ }
882
+ CylinderNode(
883
+ radius = 0.25f,
884
+ height = 0.8f,
885
+ materialInstance = greenMat,
886
+ position = Position(x = 0.7f, y = 0.4f)
887
+ )
888
+
889
+ // Gold sphere
890
+ val goldMat = remember(materialLoader) {
891
+ materialLoader.createColorInstance(
892
+ Color(1f, 0.84f, 0f),
893
+ metallic = 1f,
894
+ roughness = 0.3f
895
+ )
896
+ }
897
+ SphereNode(
898
+ radius = 0.35f,
899
+ materialInstance = goldMat,
900
+ position = Position(x = 2f, y = 0.35f)
901
+ )
902
+ }
903
+ }`,
904
+ },
905
+ "compose-ui-3d": {
906
+ id: "compose-ui-3d",
907
+ title: "Compose UI in 3D",
908
+ description: "Embed interactive Jetpack Compose UI (Cards, Buttons, Text) inside 3D space using ViewNode.",
909
+ tags: ["3d", "compose-ui", "text"],
910
+ dependency: "io.github.sceneview:sceneview:3.3.0",
911
+ prompt: "Create a 3D scene with interactive Compose UI elements (Card with text and a button) floating in 3D space using ViewNode. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
912
+ code: `@Composable
913
+ fun ComposeUI3DScreen() {
914
+ val engine = rememberEngine()
915
+ val modelLoader = rememberModelLoader(engine)
916
+ val windowManager = rememberViewNodeManager()
917
+ var clickCount by remember { mutableIntStateOf(0) }
918
+
919
+ Scene(
920
+ modifier = Modifier.fillMaxSize(),
921
+ engine = engine,
922
+ modelLoader = modelLoader,
923
+ cameraManipulator = rememberCameraManipulator(),
924
+ viewNodeWindowManager = windowManager
925
+ ) {
926
+ // 3D model behind the UI
927
+ rememberModelInstance(modelLoader, "models/damaged_helmet.glb")?.let { instance ->
928
+ ModelNode(modelInstance = instance, scaleToUnits = 1.0f, position = Position(z = -1f))
929
+ }
930
+
931
+ // Floating Compose Card in 3D space
932
+ ViewNode(
933
+ windowManager = windowManager,
934
+ position = Position(x = 0f, y = 1.2f, z = 0.5f)
935
+ ) {
936
+ Card(
937
+ modifier = Modifier.width(200.dp).padding(8.dp),
938
+ colors = CardDefaults.cardColors(
939
+ containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
940
+ )
941
+ ) {
942
+ Column(modifier = Modifier.padding(16.dp)) {
943
+ Text("Hello 3D World!", style = MaterialTheme.typography.titleMedium)
944
+ Text("Clicks: \$clickCount", style = MaterialTheme.typography.bodySmall)
945
+ Spacer(Modifier.height(8.dp))
946
+ Button(onClick = { clickCount++ }) {
947
+ Text("Click Me")
948
+ }
949
+ }
950
+ }
951
+ }
952
+ }
953
+ }`,
954
+ },
955
+ "node-hierarchy": {
956
+ id: "node-hierarchy",
957
+ title: "Node Hierarchy",
958
+ description: "Parent-child node relationships — a spinning solar system with planet groups orbiting a central sun.",
959
+ tags: ["3d", "hierarchy", "geometry", "animation"],
960
+ dependency: "io.github.sceneview:sceneview:3.3.0",
961
+ prompt: "Create a 3D solar system where planets orbit a sun using parent-child node hierarchies. Each planet group rotates independently. Use SceneView `io.github.sceneview:sceneview:3.3.0`.",
962
+ code: `@Composable
963
+ fun NodeHierarchyScreen() {
964
+ val engine = rememberEngine()
965
+ val materialLoader = rememberMaterialLoader(engine)
966
+ val environmentLoader = rememberEnvironmentLoader(engine)
967
+ var earthAngle by remember { mutableFloatStateOf(0f) }
968
+ var marsAngle by remember { mutableFloatStateOf(0f) }
969
+
970
+ // Animate planet orbits
971
+ LaunchedEffect(Unit) {
972
+ while (true) {
973
+ withFrameNanos { _ ->
974
+ earthAngle += 0.3f
975
+ marsAngle += 0.18f
976
+ }
977
+ }
978
+ }
979
+
980
+ Scene(
981
+ modifier = Modifier.fillMaxSize(),
982
+ engine = engine,
983
+ cameraManipulator = rememberCameraManipulator(
984
+ orbitHomePosition = Position(x = 0f, y = 4f, z = 8f),
985
+ targetPosition = Position(0f, 0f, 0f)
986
+ ),
987
+ environment = rememberEnvironment(environmentLoader) {
988
+ environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
989
+ ?: createEnvironment(environmentLoader)
990
+ }
991
+ ) {
992
+ // Sun (center)
993
+ val sunMat = remember(materialLoader) {
994
+ materialLoader.createColorInstance(Color.Yellow, metallic = 0f, roughness = 1f)
995
+ }
996
+ SphereNode(radius = 0.5f, materialInstance = sunMat)
997
+
998
+ // Earth orbit group — parent node rotates, child offset creates orbit
999
+ Node(rotation = Rotation(y = earthAngle)) {
1000
+ // Earth sphere
1001
+ val earthMat = remember(materialLoader) {
1002
+ materialLoader.createColorInstance(Color.Blue, metallic = 0f, roughness = 0.7f)
1003
+ }
1004
+ SphereNode(radius = 0.2f, materialInstance = earthMat, position = Position(x = 2.5f))
1005
+
1006
+ // Moon orbits Earth (nested hierarchy)
1007
+ Node(position = Position(x = 2.5f), rotation = Rotation(y = earthAngle * 3f)) {
1008
+ val moonMat = remember(materialLoader) {
1009
+ materialLoader.createColorInstance(Color.LightGray, metallic = 0f, roughness = 0.9f)
1010
+ }
1011
+ SphereNode(radius = 0.06f, materialInstance = moonMat, position = Position(x = 0.4f))
1012
+ }
1013
+ }
1014
+
1015
+ // Mars orbit group
1016
+ Node(rotation = Rotation(y = marsAngle)) {
1017
+ val marsMat = remember(materialLoader) {
1018
+ materialLoader.createColorInstance(Color.Red, metallic = 0f, roughness = 0.8f)
1019
+ }
1020
+ SphereNode(radius = 0.15f, materialInstance = marsMat, position = Position(x = 4f))
1021
+ }
1022
+
1023
+ // Sun light
1024
+ LightNode(
1025
+ type = LightManager.Type.POINT,
1026
+ apply = {
1027
+ color(1.0f, 0.95f, 0.8f)
1028
+ intensity(500_000f)
1029
+ falloff(15.0f)
1030
+ }
1031
+ )
1032
+ }
469
1033
  }`,
470
1034
  },
471
1035
  // ─── iOS Samples ────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sceneview-mcp",
3
- "version": "3.4.7",
3
+ "version": "3.4.9",
4
4
  "mcpName": "io.github.sceneview/mcp",
5
5
  "description": "MCP server for SceneView — cross-platform 3D & AR SDK for Android and iOS. Give Claude the full SceneView SDK so it writes correct, compilable code.",
6
6
  "keywords": [
@@ -44,6 +44,7 @@
44
44
  "dist/node-reference.js",
45
45
  "dist/samples.js",
46
46
  "dist/preview.js",
47
+ "dist/artifact.js",
47
48
  "dist/validator.js",
48
49
  "llms.txt"
49
50
  ],