@playcanvas/blocks
Version:
High level abstract 3D primitives for React
195 lines • 7.63 kB
JavaScript
"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