diff --git a/examples/dashboard/package.json b/examples/dashboard/package.json index 50b72553..7a73f677 100644 --- a/examples/dashboard/package.json +++ b/examples/dashboard/package.json @@ -9,7 +9,7 @@ "r3f-perf": "^7.1.2", "react-dom": "^18.2.0", "vite-plugin-mkcert": "^1.17.4", - "zustand": "4" + "zustand": "^4.4.7" }, "scripts": { "dev": "vite --host", diff --git a/examples/dashboard/src/App.tsx b/examples/dashboard/src/App.tsx index 126a512f..48247134 100644 --- a/examples/dashboard/src/App.tsx +++ b/examples/dashboard/src/App.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Canvas } from '@react-three/fiber' +import { Canvas, useFrame } from '@react-three/fiber' import { Container, Fullscreen, Text, setPreferredColorScheme } from '@react-three/uikit' import { Activity, CreditCard, DollarSign, Users } from '@react-three/uikit-lucide' @@ -15,31 +15,64 @@ import { Overview } from './components/Overview.js' import { RecentSales } from './components/RecentSales.js' import { TeamSwitcher } from './components/TeamSwitcher.js' import { UserNav } from './components/UserNav.js' +import { create } from 'zustand' setPreferredColorScheme('light') +const useFrameCounter = create(() => 0) + export default function App() { const [open, setOpen] = useState(false) return ( - + + + + + + + + + + + + + + + + ) +} + +function CountFrames() { + useFrame(() => useFrameCounter.setState(useFrameCounter.getState() + 1)) + return null +} + +function FrameCounter() { + const counter = useFrameCounter() + return ( +
- - - - - - - - - - - + {counter} +
) } diff --git a/examples/dashboard/src/components/MainNav.tsx b/examples/dashboard/src/components/MainNav.tsx index 3f9664eb..a835757c 100644 --- a/examples/dashboard/src/components/MainNav.tsx +++ b/examples/dashboard/src/components/MainNav.tsx @@ -5,7 +5,7 @@ import { colors } from '@/theme.js' export function MainNav(props: Omit, 'children'>) { return ( - + Overview diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx index 69641570..f85bc984 100644 --- a/packages/react/src/root.tsx +++ b/packages/react/src/root.tsx @@ -1,4 +1,4 @@ -import { invalidate, useFrame, useStore, useThree } from '@react-three/fiber' +import { addAfterEffect, addEffect, invalidate, useFrame, useStore, useThree } from '@react-three/fiber' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' import { ParentProvider } from './context.js' @@ -23,6 +23,11 @@ export type RootProperties = BaseRootProperties & children?: ReactNode } & EventHandlers +let isRendering = false + +addEffect(() => (isRendering = true)) +addAfterEffect(() => (isRendering = false)) + export const Root: (props: RootProperties & RefAttributes>) => ReactNode = forwardRef((properties, ref) => { const renderer = useThree((state) => state.gl) @@ -46,6 +51,15 @@ export const Root: (props: RootProperties & RefAttributes store.getState().camera, renderer, onFrameSet, + () => { + if (isRendering) { + //request render unnecassary -> already rendering + return + } + //not rendering -> requesting a new frame + invalidate() + }, + //requestFrame = invalidate, because invalidate always causes another frame invalidate, ), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index 5a69fdc9..2cc51962 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -81,6 +81,7 @@ export function createRoot( renderer: WebGLRenderer, onFrameSet: Set<(delta: number) => void>, requestRender: () => void = () => {}, + requestFrame: () => void = () => {}, ) { const rootSize = signal([0, 0]) const hoveredSignal = signal>([]) @@ -106,10 +107,11 @@ export function createRoot( const renderOrder = computedInheritableProperty(mergedProperties, 'renderOrder', 0) const depthTest = computedInheritableProperty(mergedProperties, 'depthTest', true) - const ctx: WithCameraDistance & Pick = { + const ctx: WithCameraDistance & Pick = { cameraDistance: 0, onFrameSet, requestRender, + requestFrame, pixelSize, } @@ -198,6 +200,7 @@ export function createRoot( const gylphGroupManager = new GlyphGroupManager(renderOrder, depthTest, pixelSize, ctx, object, initializers) const rootCtx: RootContext = Object.assign(ctx, { + requestFrame, scrollPosition, requestCalculateLayout, cameraDistance: 0, diff --git a/packages/uikit/src/context.ts b/packages/uikit/src/context.ts index 30103c8e..e05b55d6 100644 --- a/packages/uikit/src/context.ts +++ b/packages/uikit/src/context.ts @@ -30,4 +30,5 @@ export type RootContext = WithCameraDistance & renderer: WebGLRenderer size: Signal requestRender: () => void + requestFrame: () => void }> diff --git a/packages/uikit/src/scroll.ts b/packages/uikit/src/scroll.ts index 096835fe..6c8fecf2 100644 --- a/packages/uikit/src/scroll.ts +++ b/packages/uikit/src/scroll.ts @@ -75,7 +75,7 @@ export function computedScrollHandlers( { scrollable, maxScrollPosition }: FlexNodeState, object: Object3DRef, listeners: Signal, - root: Pick, + root: Pick, initializers: Initializers, ) { const isScrollable = computed(() => scrollable.value?.some((scrollable) => scrollable) ?? false) @@ -143,16 +143,16 @@ export function computedScrollHandlers( scrollVelocity.multiplyScalar(0.9) //damping scroll factor - if (Math.abs(scrollVelocity.x) < 0.01) { + if (Math.abs(scrollVelocity.x) < 10 /** px per second */) { scrollVelocity.x = 0 } else { - root.requestRender() + root.requestFrame() } - if (Math.abs(scrollVelocity.y) < 0.01) { + if (Math.abs(scrollVelocity.y) < 10 /** px per second */) { scrollVelocity.y = 0 } else { - root.requestRender() + root.requestFrame() } if (deltaX === 0 && deltaY === 0) { @@ -176,7 +176,10 @@ export function computedScrollHandlers( return undefined } const onPointerFinish = ({ nativeEvent }: ThreeEvent) => { - downPointerMap.delete(nativeEvent.pointerId) + if (!downPointerMap.delete(nativeEvent.pointerId) || downPointerMap.size > 0 || scrollPosition.value == null) { + return + } + //only request a render if the last pointer that was dragging stopped dragging and this panel is actually scrollable root.requestRender() } return { diff --git a/packages/uikit/src/vanilla/fullscreen.ts b/packages/uikit/src/vanilla/fullscreen.ts index 3040a34a..f7fb1988 100644 --- a/packages/uikit/src/vanilla/fullscreen.ts +++ b/packages/uikit/src/vanilla/fullscreen.ts @@ -20,6 +20,7 @@ export class Fullscreen extends Root { properties?: FullscreenProperties, defaultProperties?: AllOptionalProperties, fontFamilies?: FontFamilies, + requestRender?: () => void, ) { const sizeX = signal(0) const sizeY = signal(0) @@ -32,6 +33,7 @@ export class Fullscreen extends Root { { ...properties, sizeX, sizeY, pixelSize, transformTranslateZ }, defaultProperties, fontFamilies, + requestRender, ) this.matrixAutoUpdate = false this.parentCameraSignal = parentCameraSignal diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index 685d3af4..31f13332 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -22,6 +22,8 @@ export class Root extends Parent { properties?: RootProperties & WithReactive<{ pixelSize?: number }>, defaultProperties?: AllOptionalProperties, fontFamilies?: FontFamilies, + requestRender?: () => void, + requestFrame?: () => void, ) { super() this.pixelSizeSignal = signal(properties?.pixelSize ?? DEFAULT_PIXEL_SIZE) @@ -51,6 +53,8 @@ export class Root extends Parent { getCamera, renderer, this.onFrameSet, + requestRender, + requestFrame, ) this.mergedProperties = internals.mergedProperties this.contextSignal.value = Object.assign(internals, { fontFamiliesSignal: this.fontFamiliesSignal }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1a10ad0..33af7229 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,7 +355,7 @@ importers: specifier: ^1.17.4 version: 1.17.4(vite@5.0.12) zustand: - specifier: '4' + specifier: ^4.4.7 version: 4.4.7(@types/react@18.3.1)(react@18.3.1) examples/default: