Skip to content

Commit 3960367

Browse files
marklundinCopilot
andauthored
Adds SplatViewer progress bar (#154)
* Initial onProgress implementation * Refactor useAsset hook to support options for props and progress callback - Updated useAsset to accept an options object, allowing for additional properties and a subscribe callback for loading progress. - Adjusted fetchAsset calls to utilize the new options structure. - Updated related hooks (useSplat, useTexture, useEnvAtlas, useModel) to align with the new useAsset signature. - Enhanced documentation to reflect changes in API and usage. * Enhance useAsset hook and documentation for improved asset loading - Refactored useAsset to accept an options object, allowing for additional properties and a subscribe callback for loading progress. - Updated related hooks (useSplat, useTexture, useEnvAtlas, useModel) to align with the new useAsset signature. - Added documentation for the new loading progress feature and updated API references accordingly. - Introduced a new index page for hooks documentation. * Add Progress component and update context for asset loading progress - Introduced a new Progress component to visually indicate asset loading progress. - Enhanced AssetViewerContext to include asset and error states, along with a subscribeToProgress method for tracking loading progress. - Updated SplatViewer and related components to integrate the new Progress component. - Added a useSubscribe hook for managing subscription to progress updates. - Updated documentation to reflect the addition of the Progress component. * Comment out unused useFrame hook in Progress component * Refactor useAsset hook and update related hooks for improved prop handling - Changed useAsset to accept props directly instead of an options object, simplifying the API. - Updated useSplat, useTexture, useEnvAtlas, and useModel hooks to align with the new useAsset signature. - Enhanced documentation to reflect changes in parameter types and usage. - Updated fetchAsset utility to support new prop structure and progress callback. * Update useModel documentation to reflect removal of subscribe callback - Adjusted the documentation for the useModel hook to clarify that the subscribe callback has been removed from the props parameter. - Ensured consistency with the updated useAsset hook signature. * Refactor Progress component and update asset viewer context - Refactored the Progress component to improve its functionality and styling, allowing for customizable visibility and width transitions. - Updated the AssetViewerContext to streamline the subscription method for asset progress updates. - Enhanced the SplatViewer component to utilize the new Progress component and handle asset progress callbacks. - Adjusted documentation to reflect changes in the Progress component and its integration within the viewer. * Enhance Progress component and SplatViewer asset progress handling - Added a timeout reference in the Progress component to manage visibility more effectively after progress completion. - Updated the cleanup function in the Progress component to clear the timeout if it exists, preventing potential memory leaks. - Modified the SplatViewer component to include the 'notify' function in the dependency array of the asset progress callback, ensuring proper updates during re-renders. * Update packages/docs/content/docs/api/hooks/index.mdx Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 0ce3422 commit 3960367

File tree

9 files changed

+127
-63
lines changed

9 files changed

+127
-63
lines changed

packages/blocks/src/index.ts

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,3 @@
1-
21
// Splat Viewer
32
import * as Viewer from "./splat-viewer"
43
export { Viewer };
5-
6-
// export const MUPS = {
7-
// hello: () => console.log("hello from MUPS"),
8-
// // HelpButton,
9-
// };
10-
// import { HelpButton } from './splat-viewer/help-button';
11-
12-
// Controls: SplatViewer.Controls,
13-
// FullScreenButton: SplatViewer.FullScreenButton,
14-
// DownloadButton: SplatViewer.DownloadButton,
15-
// MenuButton: SplatViewer.MenuButton,
16-
// CameraModeToggle: SplatViewer.CameraModeToggle,
17-
// Splat: SplatViewer.Splat,
18-
// etc...
19-
20-
// import { FullScreenButton } from "./splat-viewer/full-screen-button"
21-
// import { DownloadButton } from "./splat-viewer/download-button"
22-
// import { MenuButton } from "./splat-viewer/menu-button"
23-
// import { Controls } from "./splat-viewer/controls"
24-
// import { CameraModeToggle } from "./splat-viewer/camera-mode-toggle"
25-
// import { HelpButton } from "./splat-viewer/help-button"
26-
// import { SplatViewer } from "./splat-viewer/splat-viewer"
27-
28-
// Export namespace for MDX
29-
// const Viewer = {
30-
// Splat: SplatViewer,
31-
// FullScreenButton,
32-
// DownloadButton,
33-
// MenuButton,
34-
// Controls,
35-
// CameraModeToggle,
36-
// HelpButton,
37-
// };
38-
39-
// console.log('[DEBUG] Exporting Viewer keys:', Object.keys(Viewer));
40-
41-
// export { Viewer };
42-
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useCallback, useRef } from 'react';
2+
3+
/**
4+
* A hook for subscribing to a value.
5+
* @example
6+
* const { subscribe, notify } = useSubscribe<number>();
7+
* subscribe((value) => console.log(value));
8+
* notify(1);
9+
*
10+
* @returns A tuple of `subscribe` and `notify` functions.
11+
*/
12+
export function useSubscribe<T>() {
13+
const subscribers = useRef(new Set<(value: T) => void>());
14+
15+
const subscribe = useCallback((fn: (value: T) => void) => {
16+
subscribers.current.add(fn);
17+
return () => subscribers.current.delete(fn);
18+
}, []);
19+
20+
const notify = useCallback((value: T) => {
21+
subscribers.current.forEach((fn) => fn(value));
22+
}, []);
23+
24+
return { subscribe, notify };
25+
}

packages/blocks/src/splat-viewer/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Controls } from "./controls"
55
import { CameraModeToggle } from "./camera-mode-toggle"
66
import { HelpButton } from "./help-button"
77
import { SplatViewer } from "./splat-viewer"
8+
import { Progress } from "./progress-indicator"
89

910
export {
1011
FullScreenButton,
@@ -14,4 +15,5 @@ export {
1415
CameraModeToggle,
1516
HelpButton,
1617
SplatViewer as Splat,
18+
Progress,
1719
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client";
2+
3+
import { useEffect, useRef, useState } from "react";
4+
import { useAssetViewer } from "./splat-viewer-context";
5+
import { cn } from "@lib/utils";
6+
7+
type ProgressProps = {
8+
variant?: "top" | "bottom";
9+
className?: string;
10+
style?: React.CSSProperties;
11+
};
12+
13+
export function Progress({ variant = "top", className, style }: ProgressProps) {
14+
const { subscribe } = useAssetViewer();
15+
const ref = useRef<HTMLDivElement>(null);
16+
const [visible, setVisible] = useState(true);
17+
const timeoutRef = useRef<number>(null);
18+
19+
useEffect(() => {
20+
const unsubscribe = subscribe((progress) => {
21+
if (!ref.current) return;
22+
ref.current.style.width = `${progress * 100}%`;
23+
24+
if (progress >= 1) {
25+
timeoutRef.current = setTimeout(() => setVisible(false), 500);
26+
} else {
27+
setVisible(true);
28+
}
29+
});
30+
31+
return () => {
32+
unsubscribe();
33+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
34+
};
35+
}, [subscribe]);
36+
37+
if (!visible) return null;
38+
39+
return (
40+
<div
41+
ref={ref}
42+
className={cn(
43+
"absolute left-0 w-full h-1 bg-accent transition-[width] duration-300 will-change-[width]",
44+
variant === "top" && "top-0",
45+
variant === "bottom" && "bottom-0",
46+
className
47+
)}
48+
style={{ width: "0%", ...style }}
49+
/>
50+
);
51+
}

packages/blocks/src/splat-viewer/splat-viewer-context.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import { createContext, useCallback, useEffect, useState, useContext, ReactNode, useRef } from "react";
44
import { CameraMode } from "./splat-viewer";
5+
import { useSubscribe } from "./hooks/use-subscribe";
56

67
type AssetViewerContextValue = {
7-
88
/**
99
* Whether the viewer is in fullscreen mode.
1010
*/
@@ -60,6 +60,11 @@ type AssetViewerContextValue = {
6060
* Toggles the auto rotate.
6161
*/
6262
setAutoRotate: (autoRotate: boolean) => void;
63+
64+
/**
65+
* Subscribes to the asset progress.
66+
*/
67+
subscribe: (fn: (progress: number) => void) => () => void;
6368
};
6469

6570
export const AssetViewerContext = createContext<AssetViewerContextValue | undefined>(undefined);
@@ -88,14 +93,17 @@ export function AssetViewerProvider({
8893
mode,
8994
setMode,
9095
src,
96+
subscribe,
9197
}: {
9298
children: React.ReactNode;
9399
autoPlay?: boolean;
94100
targetRef: React.RefObject<HTMLElement>;
95101
src: string;
96102
mode: CameraMode;
97103
setMode: (mode: CameraMode) => void;
104+
subscribe: (fn: (progress: number) => void) => () => void;
98105
}) {
106+
99107
const [isFullscreen, setIsFullscreen] = useState(false);
100108
const [overlay, setOverlay] = useState<"help" | "settings" | null>(null);
101109
const [autoRotate, setAutoRotate] = useState(false);
@@ -196,7 +204,8 @@ export function AssetViewerProvider({
196204
setOverlay,
197205
triggerDownload,
198206
autoRotate,
199-
setAutoRotate
207+
setAutoRotate,
208+
subscribe
200209
}}
201210
>
202211
<TimelineProvider autoPlay={autoPlay}>
@@ -234,26 +243,21 @@ export function TimelineProvider({
234243
const [isPlaying, setIsPlaying] = useState(autoPlay);
235244
const timeRef = useRef(time);
236245
const rafIdRef = useRef<number | null>(null);
237-
const subscribers = useRef(new Set<(v: number) => void>());
246+
const { subscribe, notify } = useSubscribe<number>();
238247

239248
const setTime = useCallback((value: number) => {
240249
timeRef.current = value;
241-
subscribers.current.forEach((fn) => fn(value));
242-
}, []);
250+
notify(value);
251+
}, [notify]);
243252

244253
const getTime = useCallback(() => timeRef.current, []);
245254

246-
const subscribe = useCallback((fn: (v: number) => void) => {
247-
subscribers.current.add(fn);
248-
return () => subscribers.current.delete(fn);
249-
}, []);
250-
251255
// Example: when time is changed and user commits it
252256
const onCommit = useCallback((value: number) => {
253257
timeRef.current = value;
254258
setTime(value);
255259
_setTime(value);
256-
}, []);
260+
}, [setTime]);
257261

258262
const lastTimestampRef = useRef<number | null>(null);
259263

packages/blocks/src/splat-viewer/splat-viewer.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SmartCamera } from "./smart-camera"
1212
import { HelpDialog } from "./help-dialog"
1313
import { FILLMODE_NONE } from "playcanvas"
1414
import { RESOLUTION_AUTO } from "playcanvas"
15+
import { useSubscribe } from "./hooks/use-subscribe"
1516
export type CameraMode = 'orbit' | 'fly';
1617

1718
type CameraControlsProps = {
@@ -38,6 +39,7 @@ type SplatViewerComponentProps = CameraControlsProps & {
3839
* The url of an image to display whilst the asset is loading.
3940
*/
4041
src: string,
42+
4143
/**
4244
* The track to use for the animation
4345
*/
@@ -53,6 +55,11 @@ type SplatViewerComponentProps = CameraControlsProps & {
5355
* A callback function for when the type of camera changes
5456
*/
5557
onTypeChange?: (type: 'orbit' | 'fly') => void,
58+
59+
/**
60+
* A callback function for when the asset progress changes
61+
*/
62+
onAssetProgress?: (progress: number) => void,
5663
}
5764

5865
type PosterComponentProps = {
@@ -75,11 +82,11 @@ export type SplatViewerProps = SplatViewerComponentProps & PosterComponentProps
7582
children?: React.ReactNode,
7683
}
7784

78-
function SplatComponent({
79-
src
85+
function SplatComponent({
86+
src,
87+
onAssetProgress
8088
}: SplatViewerComponentProps) {
81-
82-
const { asset, error } = useSplat(src)
89+
const { asset, error, subscribe } = useSplat(src);
8390
const { isInteracting } = useAssetViewer();
8491
const { isPlaying } = useTimeline();
8592
const app = useApp();
@@ -89,6 +96,10 @@ function SplatComponent({
8996
return () => asset?.unload();
9097
}, [asset]);
9198

99+
useEffect(() => {
100+
return subscribe(({ progress }) => onAssetProgress?.(progress));
101+
}, [subscribe, onAssetProgress]);
102+
92103
// Hide the cursor when the timeline is playing and the user is not interacting
93104
useEffect(() => {
94105
if (app.graphicsDevice.canvas) {
@@ -129,10 +140,12 @@ export function SplatViewer( {
129140
defaultMode = 'orbit',
130141
onTypeChange,
131142
className,
132-
children,
133-
...props
143+
children
134144
} : SplatViewerProps) {
135145

146+
const { subscribe, notify } = useSubscribe<number>();
147+
const onAssetProgress = useCallback((progress: number) => notify(progress), [src, notify]);
148+
136149
const isControlled = !mode;
137150
const containerRef = useRef<HTMLDivElement>(null!);
138151
const [uncontrolledMode, setUncontrolledMode] = useState<CameraMode>(
@@ -156,13 +169,14 @@ export function SplatViewer( {
156169
src={src}
157170
mode={currentMode}
158171
setMode={setCameraMode}
172+
subscribe={subscribe}
159173
>
160174
<Suspense fallback={<PosterComponent poster={poster} />} >
161175
<Application fillMode={FILLMODE_NONE} resolutionMode={RESOLUTION_AUTO} autoRender={false}>
162-
<SplatComponent src={src} {...props} />
176+
<SplatComponent src={src} onAssetProgress={onAssetProgress} />
163177
</Application>
164178
<TooltipProvider>
165-
{children}
179+
{ children }
166180
</TooltipProvider>
167181
</Suspense>
168182
<HelpDialog />

packages/docs/content/blocks/index.mdx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ export const splatVersion = registry.items.find(item => item.name === 'splat-vie
2626
High-level 3D primitives for React — minimal setup and beautiful by default.
2727
</div>
2828

29-
PlayCanvas Blocks are high-level component primitives for React. Clean, simple, and beautiful by default, they're designed to drop into your project and get to work. Built around **@playcanvas/react** and **@shadcn/ui** they're composable, themeable and ready to use out of the box.
29+
PlayCanvas Blocks are high-level component primitives for React. Clean, simple, and beautiful by default, they're designed to drop into your project and get to work.
30+
31+
Built on **@playcanvas/react** and compatible with **@shadcn/ui** they're composable, themeable and ready to use out of the box.
3032

3133
<div className='flex flex-row gap-2 mt-4 items-center'>
3234
<a className="flex items-center -ml-2 flex-row gap-2 bg-muted rounded-full p-2 px-4 max-w-fit" href="blocks/installation">
@@ -42,10 +44,12 @@ PlayCanvas Blocks are high-level component primitives for React. Clean, simple,
4244
</div>
4345

4446
<div className='relative mb-4 lg:mb-12'>
45-
<Viewer.Splat src={splatUrl} className="aspect-4:1 mt-8 lg:my-8 -mx-4 lg:-mx-6 bg-gray-200 lg:rounded-lg shadow-xl cursor-grab active:cursor-grabbing" />
47+
<Viewer.Splat src={splatUrl} className="aspect-4:1 mt-8 lg:my-8 -mx-4 lg:-mx-6 bg-gray-200 md:rounded-lg shadow-xl cursor-grab active:cursor-grabbing" >
48+
<Viewer.Progress className="bg-[#e0004d]" />
49+
</Viewer.Splat>
4650
<div className='lg:absolute bottom-0 right-0 w-full py-2 lg:py-8 lg:text-background text-xs italic text-center pointer-events-none'>
4751
<span className="pointer-events-auto">
48-
Rendered with the <code>Viewer.Splat</code> component — a React block for Gaussian splats.{" "}
52+
Rendered with the <code>Splat</code> block — a React component for Gaussian splats.{" "}
4953
<a className="underline text-bold" href="/blocks/splat-viewer">Learn more →</a>
5054
</span>
5155
</div>
@@ -87,7 +91,7 @@ PlayCanvas Blocks are high-level component primitives for React. Clean, simple,
8791

8892
## How it works?
8993

90-
Built on @playcanvas/react and the PlayCanvas engine, Blocks abstract the underlying engine to provide a high-level component API that's easy to use and understand. Internally, each block maps to a PlayCanvas entity and component hierarchy, giving you full control with a familiar dev experience.
94+
Built on **@playcanvas/react** and the PlayCanvas engine, Blocks abstract the underlying engine to provide a high-level component API that's easy to use and understand. Internally, each block maps to a PlayCanvas entity and component hierarchy, giving you full control with a familiar dev experience.
9195

9296
## Featured blocks
9397

packages/docs/content/blocks/splat-viewer.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Supports compressed gaussian splats.
1717
<div className="lg:outline lg:p-28 -mx-4 lg:-mx-6 mt-12 mb-0 rounded-t-lg bg-linear-to-br from-indigo-500 to-purple-600 relative">
1818
<OpenInV0Button url={`https://${process.env.VERCEL_URL}/r/splat-viewer.json`} className="absolute top-4 right-4" />
1919
<Viewer.Splat src={splatUrl} className="aspect-2.39:1 bg-purple-200 rounded-t-lg lg:rounded-lg shadow-xl cursor-grab active:cursor-grabbing" >
20+
<Viewer.Progress className="bg-[#8a01ff]" />
2021
<Viewer.Controls autoHide >
2122
<div className="flex gap-1 flex-grow">
2223
<Viewer.FullScreenButton />
@@ -39,6 +40,7 @@ Supports compressed gaussian splats.
3940
export function SplatViewerDemo() {
4041
return (
4142
<Viewer.Splat src={splatUrl} autoPlay >
43+
<Viewer.Progress />
4244
<Viewer.Controls >
4345
<div className="flex gap-2 flex-grow">
4446
<Viewer.FullScreenButton />
@@ -146,6 +148,7 @@ export default () => (
146148
<Viewer.DownloadButton />
147149
<Viewer.CameraModeToggle />
148150
<Viewer.MenuButton />
151+
<Viewer.Progress />
149152
</Viewer.Controls>
150153
</Viewer.Viewer>
151154
)

packages/docs/content/docs/api/hooks/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ openGraph:
1212

1313
# Hooks
1414

15-
The are a a number of important hooks that are useful when working with `@playcanvas/react`. These can often be useful when creating custom components, hooks or utilities.
15+
There are a number of important hooks that are useful when working with `@playcanvas/react`. These can often be useful when creating custom components, hooks or utilities.
1616

1717
## useApp
1818

0 commit comments

Comments
 (0)