diff --git a/examples/map-server/mcp-app.html b/examples/map-server/mcp-app.html index 473eb50f6..acb9f384f 100644 --- a/examples/map-server/mcp-app.html +++ b/examples/map-server/mcp-app.html @@ -1,72 +1,83 @@ - + - - - + + + CesiumJS Globe - - + +
Loading globe...
- + diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts index 1475d81e7..f70cdb5fe 100644 --- a/examples/map-server/src/mcp-app.ts +++ b/examples/map-server/src/mcp-app.ts @@ -397,6 +397,17 @@ async function initCesium(): Promise { // CesiumJS sets image-rendering: pixelated by default which looks bad on scaled displays // Setting to "auto" allows the browser to apply smooth interpolation cesiumViewer.canvas.style.imageRendering = "auto"; + // Prevent touch events from propagating to the parent scroll view. + // CesiumJS uses pointer events internally, which don't suppress native + // scroll gesture recognition on touch devices. Explicit non-passive touch + // listeners with preventDefault() are needed. + for (const eventName of ["touchstart", "touchmove"] as const) { + cesiumViewer.canvas.addEventListener( + eventName, + (e: TouchEvent) => e.preventDefault(), + { passive: false }, + ); + } // Note: DO NOT set resolutionScale = devicePixelRatio here! // When useBrowserRecommendedResolution: false, Cesium already uses devicePixelRatio. // Setting resolutionScale = devicePixelRatio would double the scaling (e.g., 2x2=4x on Retina) diff --git a/examples/shadertoy-server/src/mcp-app.css b/examples/shadertoy-server/src/mcp-app.css index 0449ea5ea..0d5b6f114 100644 --- a/examples/shadertoy-server/src/mcp-app.css +++ b/examples/shadertoy-server/src/mcp-app.css @@ -1,4 +1,5 @@ -html, body { +html, +body { margin: 0; width: 100%; height: 100%; @@ -23,6 +24,7 @@ html, body { width: 100%; height: 100%; display: block; + touch-action: none; } #canvas.hidden { @@ -68,7 +70,9 @@ html, body { display: none; /* Hidden by default, shown when fullscreen available */ align-items: center; justify-content: center; - transition: background 0.2s, opacity 0.2s; + transition: + background 0.2s, + opacity 0.2s; opacity: 0; /* Initially invisible, shown on View hover */ z-index: 100; } diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 235931641..bf122166c 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -85,6 +85,7 @@ function LoadingShimmer({ height, code }: { height: number; code?: string }) { display: "flex", flexDirection: "column", overflow: "hidden", + touchAction: "none", background: "linear-gradient(135deg, var(--color-background-secondary, light-dark(#f0f0f5, #2a2a3c)) 0%, var(--color-background-tertiary, light-dark(#e5e5ed, #1e1e2e)) 100%)", }} @@ -275,6 +276,14 @@ export default function ThreeJSApp({ useEffect(() => { if (!code || !canvasRef.current || !containerRef.current) return; + // Prevent touch events from propagating to the parent scroll view. + // Three.js OrbitControls uses pointer events, which don't suppress native + // scroll gesture recognition on touch devices. + const canvas = canvasRef.current; + const preventDefault = (e: TouchEvent) => e.preventDefault(); + canvas.addEventListener("touchstart", preventDefault, { passive: false }); + canvas.addEventListener("touchmove", preventDefault, { passive: false }); + // Cleanup previous animation animControllerRef.current?.cleanup(); animControllerRef.current = createAnimationController(); @@ -289,7 +298,11 @@ export default function ThreeJSApp({ animControllerRef.current.visibilityAwareRAF, ).catch((e) => setError(e instanceof Error ? e.message : "Unknown error")); - return () => animControllerRef.current?.cleanup(); + return () => { + canvas.removeEventListener("touchstart", preventDefault); + canvas.removeEventListener("touchmove", preventDefault); + animControllerRef.current?.cleanup(); + }; }, [code, height]); if (isStreaming || !code) { @@ -314,6 +327,7 @@ export default function ThreeJSApp({ height, borderRadius: "var(--border-radius-lg, 8px)", display: "block", + touchAction: "none", }} /> {error &&
Error: {error}
} diff --git a/examples/wiki-explorer-server/src/mcp-app.css b/examples/wiki-explorer-server/src/mcp-app.css index 8f6f0246e..ea13ac2ac 100644 --- a/examples/wiki-explorer-server/src/mcp-app.css +++ b/examples/wiki-explorer-server/src/mcp-app.css @@ -1,6 +1,7 @@ #graph { width: 100%; height: 100vh; + touch-action: none; } #popup { diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 7d95cb8d6..69bec05d4 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -111,6 +111,18 @@ const graph = new ForceGraph(container) }) .graphData(graphData); +// Prevent touch events from propagating to the parent scroll view. +// force-graph uses pointer events, which don't suppress native scroll gesture +// recognition on touch devices. +const graphCanvas = container.querySelector("canvas"); +if (graphCanvas) { + for (const eventName of ["touchstart", "touchmove"] as const) { + graphCanvas.addEventListener(eventName, (e) => e.preventDefault(), { + passive: false, + }); + } +} + // Handle window resize function handleResize() { const { width, height } = container.getBoundingClientRect();