UNPKG

@playcanvas/blocks

Version:

High level abstract 3D primitives for React

195 lines 7.63 kB
"use client"; import { jsx as _jsx } from "react/jsx-runtime"; import { createContext, useCallback, useEffect, useState, useContext, useRef } from "react"; import { useSubscribe } from "./hooks/use-subscribe.js"; export const AssetViewerContext = createContext(undefined); export function useAssetViewer() { const ctx = useContext(AssetViewerContext); if (!ctx) throw new Error("useAssetViewer must be used within an AssetViewerProvider"); return ctx; } function isMacPlatform() { // @ts-expect-error this is not available on all browsers const userAgentData = navigator.userAgentData; if (userAgentData?.platform) { return userAgentData.platform === "macOS"; } // Fallback for older browsers return /Mac/i.test(navigator.userAgent || ""); } export function AssetViewerProvider({ children, autoPlay = false, targetRef, mode, setMode, src, subscribe }) { const [isFullscreen, setIsFullscreen] = useState(false); const [overlay, setOverlay] = useState(null); const [autoRotate, setAutoRotate] = useState(false); // download const triggerDownload = useCallback(() => { if (typeof src === 'string') { // Single file download const link = document.createElement('a'); link.href = src; link.download = src.split('/').pop() || 'splat.ply'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else { // Multiple file download for SogsMeta const files = Object.values(src).reduce((acc, value) => { if (value && typeof value === 'object' && 'files' in value) { return [...acc, ...value.files]; } return acc; }, []); // Download each file sequentially files.forEach((file, index) => { const link = document.createElement('a'); link.href = file; link.download = file.split('/').pop() || `splat_${index}.texture`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); } }, [src]); // subscribe to the camera reset event const [subscribeCameraReset, notifyCameraReset] = useSubscribe(); const resetCamera = useCallback(() => { notifyCameraReset(); setMode('orbit'); setOverlay(null); }, [notifyCameraReset, setMode, setOverlay]); // Listen for keyboard events const handleKey = useCallback((e) => { const isCmdOrCtrl = isMacPlatform() ? e.metaKey : e.ctrlKey; if (e.key === "?" && e.shiftKey) setOverlay('help'); // help if (e.key.toLowerCase() === "f" && e.shiftKey && isCmdOrCtrl) toggleFullscreen(); // fullscreen if (e.key.toLowerCase() === "d" && e.shiftKey && isCmdOrCtrl) triggerDownload(); // download if (e.key.toLowerCase() === "r") resetCamera(); // reset camera }, []); useEffect(() => { window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [handleKey]); // toggle fullscreen const toggleFullscreen = useCallback(async () => { const el = targetRef.current; if (!el) return; if (document.fullscreenElement) { await document.exitFullscreen(); setOverlay(null); setIsFullscreen(false); } else { await el.requestFullscreen(); setOverlay(null); setIsFullscreen(true); } }, [targetRef]); // handle fullscreen state useEffect(() => { const handler = () => setIsFullscreen(!!document.fullscreenElement); document.addEventListener("fullscreenchange", handler); return () => document.removeEventListener("fullscreenchange", handler); }, []); // handle interaction state const [isInteracting, setIsInteracting] = useState(false); const isInteractingRef = useRef(false); const timeoutRef = useRef(null); useEffect(() => { const el = targetRef.current; if (!el) return; const onInteract = () => { if (!isInteractingRef.current) { isInteractingRef.current = true; setIsInteracting(true); } clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => { isInteractingRef.current = false; setIsInteracting(false); }, 2000); }; el.addEventListener("mousemove", onInteract); el.addEventListener("keydown", onInteract); el.addEventListener("pointerdown", onInteract); return () => { clearTimeout(timeoutRef.current); timeoutRef.current = null; el.removeEventListener("mousemove", onInteract); el.removeEventListener("keydown", onInteract); el.removeEventListener("pointerdown", onInteract); }; }, [targetRef]); return (_jsx(AssetViewerContext.Provider, { value: { isFullscreen, isInteracting, toggleFullscreen, src, mode, setMode, overlay, setOverlay, triggerDownload, autoRotate, setAutoRotate, subscribe, subscribeCameraReset, resetCamera }, children: _jsx(TimelineProvider, { autoPlay: autoPlay, children: children }) })); } const TimelineContext = createContext(undefined); export function useTimeline() { const ctx = useContext(TimelineContext); if (!ctx) throw new Error("useTimeline must be used within a TimelineProvider"); return ctx; } export function TimelineProvider({ children, autoPlay = false, }) { const [time, _setTime] = useState(0); const [isPlaying, setIsPlaying] = useState(autoPlay); const timeRef = useRef(time); const rafIdRef = useRef(null); const [subscribe, notify] = useSubscribe(); const setTime = useCallback((value) => { timeRef.current = value; notify(value); }, [notify]); const getTime = useCallback(() => timeRef.current, []); // Example: when time is changed and user commits it const onCommit = useCallback((value) => { timeRef.current = value; setTime(value); _setTime(value); }, [setTime]); const lastTimestampRef = useRef(null); useEffect(() => { if (isPlaying) { const step = (timestamp) => { if (lastTimestampRef.current == null) { lastTimestampRef.current = timestamp; } const delta = (timestamp - lastTimestampRef.current) / 1000; lastTimestampRef.current = timestamp; setTime(getTime() + delta); rafIdRef.current = requestAnimationFrame(step); }; lastTimestampRef.current = null; rafIdRef.current = requestAnimationFrame(step); } return () => { if (rafIdRef.current) { cancelAnimationFrame(rafIdRef.current); rafIdRef.current = null; } }; }, [isPlaying, getTime, setTime]); return (_jsx(TimelineContext.Provider, { value: { getTime, setTime, onCommit, subscribe, isPlaying, setIsPlaying }, children: children })); } //# sourceMappingURL=splat-viewer-context.js.map