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 +1 -1
- package/README.md +10 -2
- package/dist/artifact.js +343 -0
- package/dist/index.js +116 -23
- package/dist/samples.js +564 -0
- package/package.json +2 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/sceneview-mcp)
|
|
4
4
|
[](https://www.npmjs.com/package/sceneview-mcp)
|
|
5
5
|
[](https://modelcontextprotocol.io/)
|
|
6
|
-
[](./LICENSE)
|
|
7
7
|
[](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
|
-
|
|
278
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/artifact.js
ADDED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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 • Scroll to zoom • 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 • Pinch to zoom${hotspots.length > 0 ? " • 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° 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.
|
|
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.
|
|
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
|
],
|