Merge pull request #299 from modelcontextprotocol/feat/threejs-perf-styling
feat(threejs): add visibility optimization, host styling, transparent background
This commit is contained in:
commit
f71155ac7c
5 changed files with 142 additions and 56 deletions
|
|
@ -108,3 +108,27 @@ React component that:
|
||||||
- Displays streaming preview from `toolInputsPartial.code` as code arrives
|
- Displays streaming preview from `toolInputsPartial.code` as code arrives
|
||||||
- Executes final code from `toolInputs.code` when complete
|
- Executes final code from `toolInputs.code` when complete
|
||||||
- Renders to a pre-created canvas with configurable height
|
- Renders to a pre-created canvas with configurable height
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### Visibility-Based Pause
|
||||||
|
|
||||||
|
Animation automatically pauses when the widget scrolls out of view:
|
||||||
|
|
||||||
|
- Uses `IntersectionObserver` to track visibility (browser-native, no polling)
|
||||||
|
- Wraps `requestAnimationFrame` to skip frames when not visible
|
||||||
|
- Animation loop stops completely → zero CPU usage while hidden
|
||||||
|
- Queued callbacks resume instantly when scrolled back into view
|
||||||
|
|
||||||
|
This is transparent to user code - just use `requestAnimationFrame` normally.
|
||||||
|
|
||||||
|
### Transparent Background
|
||||||
|
|
||||||
|
Supports alpha transparency for seamless host UI integration:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||||
|
renderer.setClearColor(0x000000, 0); // Fully transparent
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the default - 3D objects composite directly over the host background.
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ const DIST_DIR = import.meta.filename.endsWith(".ts")
|
||||||
// Default code example for the Three.js widget
|
// Default code example for the Three.js widget
|
||||||
const DEFAULT_THREEJS_CODE = `const scene = new THREE.Scene();
|
const DEFAULT_THREEJS_CODE = `const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
renderer.setClearColor(0x1a1a2e);
|
renderer.setClearColor(0x000000, 0); // Transparent background
|
||||||
|
|
||||||
const cube = new THREE.Mesh(
|
const cube = new THREE.Mesh(
|
||||||
new THREE.BoxGeometry(1, 1, 1),
|
new THREE.BoxGeometry(1, 1, 1),
|
||||||
|
|
@ -54,27 +54,31 @@ const THREEJS_DOCUMENTATION = `# Three.js Widget Documentation
|
||||||
- \`OrbitControls\` - Interactive camera controls
|
- \`OrbitControls\` - Interactive camera controls
|
||||||
- \`EffectComposer\`, \`RenderPass\`, \`UnrealBloomPass\` - Post-processing effects
|
- \`EffectComposer\`, \`RenderPass\`, \`UnrealBloomPass\` - Post-processing effects
|
||||||
|
|
||||||
## Basic Template
|
## Basic Template (Transparent Background)
|
||||||
\`\`\`javascript
|
\`\`\`javascript
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
renderer.setClearColor(0x1a1a2e); // Dark background
|
renderer.setClearColor(0x000000, 0); // Transparent - blends with host UI
|
||||||
|
|
||||||
// Add objects here...
|
// Add objects here...
|
||||||
|
|
||||||
camera.position.z = 5;
|
camera.position.z = 5;
|
||||||
renderer.render(scene, camera); // Static render
|
renderer.render(scene, camera);
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Example: Rotating Cube with Lighting
|
## Transparent vs Solid Background
|
||||||
|
- **Transparent (default)**: Use \`alpha: true\` and \`setClearColor(0x000000, 0)\`
|
||||||
|
- **Solid color**: Use \`setClearColor(0x1a1a2e)\` (omit alpha param)
|
||||||
|
|
||||||
|
## Example: Rotating Cube
|
||||||
\`\`\`javascript
|
\`\`\`javascript
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
renderer.setClearColor(0x1a1a2e);
|
renderer.setClearColor(0x000000, 0);
|
||||||
|
|
||||||
const cube = new THREE.Mesh(
|
const cube = new THREE.Mesh(
|
||||||
new THREE.BoxGeometry(1, 1, 1),
|
new THREE.BoxGeometry(1, 1, 1),
|
||||||
|
|
@ -82,7 +86,6 @@ const cube = new THREE.Mesh(
|
||||||
);
|
);
|
||||||
scene.add(cube);
|
scene.add(cube);
|
||||||
|
|
||||||
// Lighting - keep intensity at 1 or below
|
|
||||||
scene.add(new THREE.DirectionalLight(0xffffff, 1));
|
scene.add(new THREE.DirectionalLight(0xffffff, 1));
|
||||||
scene.add(new THREE.AmbientLight(0x404040));
|
scene.add(new THREE.AmbientLight(0x404040));
|
||||||
|
|
||||||
|
|
@ -101,9 +104,9 @@ animate();
|
||||||
\`\`\`javascript
|
\`\`\`javascript
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
renderer.setClearColor(0x2d2d44);
|
renderer.setClearColor(0x000000, 0);
|
||||||
|
|
||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
|
|
@ -128,7 +131,7 @@ animate();
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
- Always set \`renderer.setClearColor()\` to a dark color
|
- Use \`alpha: true\` for transparent backgrounds that blend with host UI
|
||||||
- Keep light intensity ≤ 1 to avoid washed-out scenes
|
- Keep light intensity ≤ 1 to avoid washed-out scenes
|
||||||
- Use \`MeshStandardMaterial\` for realistic lighting
|
- Use \`MeshStandardMaterial\` for realistic lighting
|
||||||
- For animations, use \`requestAnimationFrame\`
|
- For animations, use \`requestAnimationFrame\`
|
||||||
|
|
@ -157,7 +160,7 @@ export function createServer(): McpServer {
|
||||||
{
|
{
|
||||||
title: "Show Three.js Scene",
|
title: "Show Three.js Scene",
|
||||||
description:
|
description:
|
||||||
"Render an interactive 3D scene with custom Three.js code. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.",
|
"Render an interactive 3D scene with custom Three.js code. Supports transparent backgrounds (alpha: true) for seamless host UI integration. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
code: z
|
code: z
|
||||||
.string()
|
.string()
|
||||||
|
|
@ -171,16 +174,14 @@ export function createServer(): McpServer {
|
||||||
.describe("Height in pixels"),
|
.describe("Height in pixels"),
|
||||||
},
|
},
|
||||||
outputSchema: z.object({
|
outputSchema: z.object({
|
||||||
code: z.string(),
|
success: z.boolean(),
|
||||||
height: z.number(),
|
|
||||||
}),
|
}),
|
||||||
_meta: { ui: { resourceUri } },
|
_meta: { ui: { resourceUri } },
|
||||||
},
|
},
|
||||||
async ({ code, height }) => {
|
async () => {
|
||||||
const data = { code, height };
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: JSON.stringify(data) }],
|
content: [{ type: "text", text: "Three.js scene rendered" }],
|
||||||
structuredContent: data,
|
structuredContent: { success: true },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #1a1a2e;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
|
|
@ -15,13 +19,13 @@ html, body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
color: #888;
|
color: var(--color-text-secondary, #888);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
color: #ff6b6b;
|
color: var(--color-text-danger, #ff6b6b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.threejs-container {
|
.threejs-container {
|
||||||
|
|
@ -35,9 +39,9 @@ html, body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #ff6b6b;
|
color: var(--color-text-danger, #ff6b6b);
|
||||||
background: rgba(26, 26, 46, 0.9);
|
background: var(--color-background-secondary, rgba(26, 26, 46, 0.9));
|
||||||
border-radius: 8px;
|
border-radius: var(--border-radius-md, 8px);
|
||||||
font-family: system-ui;
|
font-family: var(--font-sans, system-ui);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* props to the actual widget component.
|
* props to the actual widget component.
|
||||||
*/
|
*/
|
||||||
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
|
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
|
||||||
import { useApp } from "@modelcontextprotocol/ext-apps/react";
|
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
|
||||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { StrictMode, useState, useCallback, useEffect } from "react";
|
import { StrictMode, useState, useCallback, useEffect } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
@ -78,6 +78,9 @@ function McpAppWrapper() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Apply host styling (theme, CSS variables, fonts)
|
||||||
|
useHostStyles(app);
|
||||||
|
|
||||||
// Get initial host context after connection
|
// Get initial host context after connection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (app) {
|
if (app) {
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,10 @@ type ThreeJSAppProps = WidgetProps<ThreeJSToolInput>;
|
||||||
// Default demo code shown when no code is provided
|
// Default demo code shown when no code is provided
|
||||||
const DEFAULT_THREEJS_CODE = `const scene = new THREE.Scene();
|
const DEFAULT_THREEJS_CODE = `const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
renderer.setClearColor(0x1a1a2e);
|
// Transparent background - scene composites over host UI
|
||||||
|
renderer.setClearColor(0x000000, 0);
|
||||||
|
|
||||||
const cube = new THREE.Mesh(
|
const cube = new THREE.Mesh(
|
||||||
new THREE.BoxGeometry(1, 1, 1),
|
new THREE.BoxGeometry(1, 1, 1),
|
||||||
|
|
@ -66,13 +67,6 @@ animate();`;
|
||||||
// Streaming Preview
|
// Streaming Preview
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
const SHIMMER_STYLE = `
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% { background-position: 200% 0; }
|
|
||||||
100% { background-position: -200% 0; }
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function LoadingShimmer({ height, code }: { height: number; code?: string }) {
|
function LoadingShimmer({ height, code }: { height: number; code?: string }) {
|
||||||
const preRef = useRef<HTMLPreElement>(null);
|
const preRef = useRef<HTMLPreElement>(null);
|
||||||
|
|
||||||
|
|
@ -85,28 +79,25 @@ function LoadingShimmer({ height, code }: { height: number; code?: string }) {
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height,
|
height,
|
||||||
borderRadius: 8,
|
borderRadius: "var(--border-radius-lg, 8px)",
|
||||||
padding: 16,
|
padding: 16,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
background:
|
background:
|
||||||
"linear-gradient(90deg, #1a1a2e 25%, #2d2d44 50%, #1a1a2e 75%)",
|
"linear-gradient(135deg, var(--color-background-secondary, light-dark(#f0f0f5, #2a2a3c)) 0%, var(--color-background-tertiary, light-dark(#e5e5ed, #1e1e2e)) 100%)",
|
||||||
backgroundSize: "200% 100%",
|
|
||||||
animation: "shimmer 1.5s ease-in-out infinite",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>{SHIMMER_STYLE}</style>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
color: "#888",
|
color: "var(--color-text-tertiary, light-dark(#666, #888))",
|
||||||
fontFamily: "system-ui",
|
fontFamily: "var(--font-sans, system-ui)",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🎮 Three.js
|
Three.js
|
||||||
</div>
|
</div>
|
||||||
{code && (
|
{code && (
|
||||||
<pre
|
<pre
|
||||||
|
|
@ -116,10 +107,10 @@ function LoadingShimmer({ height, code }: { height: number; code?: string }) {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
color: "#aaa",
|
color: "var(--color-text-ghost, light-dark(#777, #aaa))",
|
||||||
fontFamily: "monospace",
|
fontFamily: "var(--font-mono, monospace)",
|
||||||
fontSize: 11,
|
fontSize: "var(--font-text-xs-size, 11px)",
|
||||||
lineHeight: 1.4,
|
lineHeight: "var(--font-text-xs-line-height, 1.4)",
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
|
|
@ -135,6 +126,43 @@ function LoadingShimmer({ height, code }: { height: number; code?: string }) {
|
||||||
// Three.js Execution
|
// Three.js Execution
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
// Visibility-aware animation controller
|
||||||
|
function createAnimationController() {
|
||||||
|
let isVisible = true;
|
||||||
|
let pendingCallbacks: FrameRequestCallback[] = [];
|
||||||
|
let rafIds: number[] = [];
|
||||||
|
|
||||||
|
const visibilityAwareRAF = (callback: FrameRequestCallback): number => {
|
||||||
|
if (isVisible) {
|
||||||
|
const id = requestAnimationFrame(callback);
|
||||||
|
rafIds.push(id);
|
||||||
|
return id;
|
||||||
|
} else {
|
||||||
|
// Queue callback for when visible again
|
||||||
|
pendingCallbacks.push(callback);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setVisible = (visible: boolean) => {
|
||||||
|
isVisible = visible;
|
||||||
|
if (visible && pendingCallbacks.length > 0) {
|
||||||
|
// Resume queued animations
|
||||||
|
const callbacks = pendingCallbacks;
|
||||||
|
pendingCallbacks = [];
|
||||||
|
callbacks.forEach((cb) => visibilityAwareRAF(cb));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
rafIds.forEach((id) => cancelAnimationFrame(id));
|
||||||
|
rafIds = [];
|
||||||
|
pendingCallbacks = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return { visibilityAwareRAF, setVisible, cleanup };
|
||||||
|
}
|
||||||
|
|
||||||
const threeContext = {
|
const threeContext = {
|
||||||
THREE,
|
THREE,
|
||||||
OrbitControls,
|
OrbitControls,
|
||||||
|
|
@ -148,16 +176,18 @@ async function executeThreeCode(
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
|
visibilityAwareRAF: (callback: FrameRequestCallback) => number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const fn = new Function(
|
const fn = new Function(
|
||||||
"ctx",
|
"ctx",
|
||||||
"canvas",
|
"canvas",
|
||||||
"width",
|
"width",
|
||||||
"height",
|
"height",
|
||||||
|
"requestAnimationFrame",
|
||||||
`const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = ctx;
|
`const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = ctx;
|
||||||
return (async () => { ${code} })();`,
|
return (async () => { ${code} })();`,
|
||||||
);
|
);
|
||||||
await fn(threeContext, canvas, width, height);
|
await fn(threeContext, canvas, width, height, visibilityAwareRAF);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -177,6 +207,7 @@ export default function ThreeJSApp({
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const animControllerRef = useRef<ReturnType<typeof createAnimationController> | null>(null);
|
||||||
|
|
||||||
const height = toolInputs?.height ?? toolInputsPartial?.height ?? 400;
|
const height = toolInputs?.height ?? toolInputsPartial?.height ?? 400;
|
||||||
const code = toolInputs?.code || DEFAULT_THREEJS_CODE;
|
const code = toolInputs?.code || DEFAULT_THREEJS_CODE;
|
||||||
|
|
@ -191,14 +222,38 @@ export default function ThreeJSApp({
|
||||||
paddingLeft: safeAreaInsets?.left,
|
paddingLeft: safeAreaInsets?.left,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Visibility-based pause/play
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
animControllerRef.current?.setVisible(entry.isIntersecting);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(containerRef.current);
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!code || !canvasRef.current || !containerRef.current) return;
|
if (!code || !canvasRef.current || !containerRef.current) return;
|
||||||
|
|
||||||
|
// Cleanup previous animation
|
||||||
|
animControllerRef.current?.cleanup();
|
||||||
|
animControllerRef.current = createAnimationController();
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
const width = containerRef.current.offsetWidth || 800;
|
const width = containerRef.current.offsetWidth || 800;
|
||||||
executeThreeCode(code, canvasRef.current, width, height).catch((e) =>
|
executeThreeCode(
|
||||||
setError(e instanceof Error ? e.message : "Unknown error"),
|
code,
|
||||||
);
|
canvasRef.current,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
animControllerRef.current.visibilityAwareRAF,
|
||||||
|
).catch((e) => setError(e instanceof Error ? e.message : "Unknown error"));
|
||||||
|
|
||||||
|
return () => animControllerRef.current?.cleanup();
|
||||||
}, [code, height]);
|
}, [code, height]);
|
||||||
|
|
||||||
if (isStreaming || !code) {
|
if (isStreaming || !code) {
|
||||||
|
|
@ -221,9 +276,8 @@ export default function ThreeJSApp({
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height,
|
height,
|
||||||
borderRadius: 8,
|
borderRadius: "var(--border-radius-lg, 8px)",
|
||||||
display: "block",
|
display: "block",
|
||||||
background: "#1a1a2e",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{error && <div className="error-overlay">Error: {error}</div>}
|
{error && <div className="error-overlay">Error: {error}</div>}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue