diff --git a/.storybook/stories/List.stories.tsx b/.storybook/stories/List.stories.tsx
index b29662a..d9c0ea6 100644
--- a/.storybook/stories/List.stories.tsx
+++ b/.storybook/stories/List.stories.tsx
@@ -1,23 +1,26 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
+import { a } from '@react-spring/three'
-import { Flex, Box } from '../../src'
+import { Flex, Box, SpringBox } from '../../src'
import { Setup } from '../Setup'
const List = ({ width, height }: { width: number; height: number }) => {
return (
-
- {new Array(8).fill(undefined).map((_, i) => (
-
- {(width, height) => (
-
-
-
-
- )}
-
- ))}
-
+
+
+ {new Array(8).fill(undefined).map((_, i) => (
+
+ {(width, height) => (
+
+
+
+
+ )}
+
+ ))}
+
+
)
}
diff --git a/.storybook/stories/NestedBoxes.stories.tsx b/.storybook/stories/NestedBoxes.stories.tsx
index f1895fd..d523edf 100644
--- a/.storybook/stories/NestedBoxes.stories.tsx
+++ b/.storybook/stories/NestedBoxes.stories.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex } from '../../src'
+import { AutomaticBox, Box, Flex } from '../../src'
import React, { Suspense } from 'react'
import { ComponentMeta, ComponentStory } from '@storybook/react'
import { Box as Cube } from '@react-three/drei'
@@ -9,26 +9,28 @@ import { Setup } from '../Setup'
const Block = ({ color1, color2 }: { color1: Color; color2: Color }) => {
return (
-
+
-
+
-
-
+
+
-
+
-
+
)
}
const NestedBoxes = ({ width, height }: { width: number; height: number }) => {
return (
-
-
-
-
+
+
+
+
+
+
)
}
diff --git a/.storybook/stories/Rotation.stories.tsx b/.storybook/stories/Rotation.stories.tsx
index 9d85a30..65231c3 100644
--- a/.storybook/stories/Rotation.stories.tsx
+++ b/.storybook/stories/Rotation.stories.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex } from '../../src'
+import { Box, AutomaticBox, ReferenceGroup, Flex } from '../../src'
import React, { Suspense } from 'react'
import { ComponentMeta, ComponentStory } from '@storybook/react'
@@ -25,28 +25,37 @@ const Rotation = ({
const width = 3
const height = 1
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/package.json b/package.json
index 3480df3..77eb01f 100644
--- a/package.json
+++ b/package.json
@@ -102,6 +102,7 @@
"prettier": "^2.0.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+ "@react-spring/three": "^9.2.4",
"rimraf": "^3.0.2",
"rollup": "^2.26.10",
"rollup-plugin-filesize": "^9.1.1",
@@ -113,6 +114,7 @@
"peerDependencies": {
"@react-three/fiber": ">=6.0",
"react": "^17.0.2",
+ "@react-spring/three": "^9.2.4",
"three": ">=0.126"
}
}
diff --git a/src/Box.tsx b/src/Box.tsx
index 8ce17a7..fc8618f 100644
--- a/src/Box.tsx
+++ b/src/Box.tsx
@@ -1,247 +1,98 @@
-import React, { useLayoutEffect, useRef, useMemo, useState } from 'react'
-import * as THREE from 'three'
-import Yoga from 'yoga-layout-prebuilt'
-import { ReactThreeFiber, useFrame } from '@react-three/fiber'
+import React, { forwardRef, ReactNode, useCallback, useMemo, useState, useContext, createContext } from 'react'
import mergeRefs from 'react-merge-refs'
+import { boxNodeContext } from './context'
+import { R3FlexProps, useProps } from './props'
-import { setYogaProperties, rmUndefFromObj } from './util'
-import { boxContext, flexContext, SharedBoxContext } from './context'
-import { R3FlexProps } from './props'
-import { useReflow, useContext } from './hooks'
-
-export type BoxProps = {
- centerAnchor?: boolean
- children: React.ReactNode | ((width: number, height: number, centerAnchor?: boolean) => React.ReactNode)
-} & R3FlexProps &
- Omit, 'children'>
-
-function BoxImpl(
- {
- // Non-flex props
- children,
- centerAnchor,
-
- // flex props
- flexDirection,
- flexDir,
- dir,
-
- alignContent,
- alignItems,
- alignSelf,
- align,
-
- justifyContent,
- justify,
-
- flexBasis,
- basis,
- flexGrow,
- grow,
-
- flexShrink,
- shrink,
-
- flexWrap,
- wrap,
-
- margin,
- m,
- marginBottom,
- marginLeft,
- marginRight,
- marginTop,
- mb,
- ml,
- mr,
- mt,
-
- padding,
- p,
- paddingBottom,
- paddingLeft,
- paddingRight,
- paddingTop,
- pb,
- pl,
- pr,
- pt,
-
- height,
- width,
-
- maxHeight,
- maxWidth,
- minHeight,
- minWidth,
-
- // other
- ...props
- }: BoxProps,
- ref: React.Ref
-) {
- // must memoize or the object literal will cause every dependent of flexProps to rerender everytime
- const flexProps: R3FlexProps = useMemo(() => {
- const _flexProps = {
- flexDirection,
- flexDir,
- dir,
-
- alignContent,
- alignItems,
- alignSelf,
- align,
-
- justifyContent,
- justify,
-
- flexBasis,
- basis,
- flexGrow,
- grow,
- flexShrink,
- shrink,
-
- flexWrap,
- wrap,
-
- margin,
- m,
- marginBottom,
- marginLeft,
- marginRight,
- marginTop,
- mb,
- ml,
- mr,
- mt,
-
- padding,
- p,
- paddingBottom,
- paddingLeft,
- paddingRight,
- paddingTop,
- pb,
- pl,
- pr,
- pt,
-
- height,
- width,
-
- maxHeight,
- maxWidth,
- minHeight,
- minWidth,
- }
-
- rmUndefFromObj(_flexProps)
- return _flexProps
- }, [
- align,
- alignContent,
- alignItems,
- alignSelf,
- dir,
- flexBasis,
- basis,
- flexDir,
- flexDirection,
- flexGrow,
- grow,
- flexShrink,
- shrink,
- flexWrap,
- height,
- justify,
- justifyContent,
- m,
- margin,
- marginBottom,
- marginLeft,
- marginRight,
- marginTop,
- maxHeight,
- maxWidth,
- mb,
- minHeight,
- minWidth,
- ml,
- mr,
- mt,
- p,
- padding,
- paddingBottom,
- paddingLeft,
- paddingRight,
- paddingTop,
- pb,
- pl,
- pr,
- pt,
- width,
- wrap,
- ])
-
- const { registerBox, unregisterBox, scaleFactor } = useContext(flexContext)
- const { node: parent } = useContext(boxContext)
- const group = useRef()
- const node = useMemo(() => Yoga.Node.create(), [])
- const reflow = useReflow()
-
- useLayoutEffect(() => {
- setYogaProperties(node, flexProps, scaleFactor)
- }, [flexProps, node, scaleFactor])
-
- // Make child known to the parents yoga instance *before* it calculates layout
- useLayoutEffect(() => {
- if (!group.current || !parent) return
-
- parent.insertChild(node, parent.getChildCount())
- registerBox(node, group.current, flexProps, centerAnchor)
-
- // Remove child on unmount
- return () => {
- parent.removeChild(node)
- unregisterBox(node)
- }
- }, [node, parent, flexProps, centerAnchor, registerBox, unregisterBox])
-
- // We need to reflow if props change
- useLayoutEffect(() => {
- reflow()
- }, [children, flexProps, reflow])
-
- const [size, setSize] = useState<[number, number]>([0, 0])
- const epsilon = 1 / scaleFactor
- useFrame(() => {
- const width =
- (typeof flexProps.width === 'number' ? flexProps.width : null) || node.getComputedWidth().valueOf() / scaleFactor
- const height =
- (typeof flexProps.height === 'number' ? flexProps.height : null) ||
- node.getComputedHeight().valueOf() / scaleFactor
-
- if (Math.abs(width - size[0]) > epsilon || Math.abs(height - size[1]) > epsilon) {
- setSize([width, height])
- }
- })
-
- const sharedBoxContext = useMemo(() => ({ node, size, centerAnchor }), [node, size, centerAnchor])
-
- return (
-
-
- {typeof children === 'function' ? children(size[0], size[1], centerAnchor) : children}
-
-
- )
-}
+import { useBox, usePropsSyncSize } from '.'
+import { FrameValue } from '@react-spring/core'
+import type * as Fiber from '@react-three/fiber'
/**
* Box container for 3D Objects.
* For containing Boxes use ``.
*/
-export const Box = React.forwardRef(BoxImpl)
+export const Box = forwardRef<
+ ReactNode,
+ {
+ onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void
+ centerAnchor?: boolean
+ children: ((width: number, height: number) => React.ReactNode) | React.ReactNode
+ index?: number
+ } & R3FlexProps &
+ Fiber.GroupProps
+>(
+ (
+ {
+ // Non-flex props
+ children,
+ centerAnchor,
+
+ onUpdateTransformation,
+ index,
+
+ // other
+ ...props
+ },
+ ref
+ ) => {
+ // must memoize or the object literal will cause every dependent of flexProps to rerender everytime
+ const [flexProps, groupProps] = useProps(props)
+
+ const [[x, y, width, height], setTransformation] = useState<[number, number, number, number]>([0, 0, 0, 0])
+
+ const node = useBox(
+ flexProps,
+ centerAnchor,
+ index,
+ useCallback(
+ (x: number, y: number, width: number, height: number) => {
+ onUpdateTransformation && onUpdateTransformation(x, y, width, height)
+ setTransformation([x, y, width, height])
+ },
+ [onUpdateTransformation]
+ )
+ )
+
+ const size = useMemoArray<[number, number]>([width, height])
+
+ return (
+
+
+
+ {useMemo(
+ () => (typeof children === 'function' ? children(width, height) : children),
+ [width, height, children]
+ )}
+
+
+
+ )
+ }
+)
+
+export const AutomaticBox = forwardRef<
+ ReactNode,
+ {
+ onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void
+ centerAnchor?: boolean
+ children: ((width: number, height: number) => React.ReactNode) | React.ReactNode
+ index?: number
+ } & R3FlexProps &
+ Fiber.GroupProps
+>((props, ref) => {
+ const [overwrittenProps, setRef] = usePropsSyncSize(props)
+ const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef])
+ return
+})
+
+export function useMemoArray>(array: T): T {
+ return useMemo(() => array, [array])
+}
+
+export const boxSizeContext = createContext<[number | FrameValue, number | FrameValue]>(null as any)
+
+export function useFlexSize() {
+ return useContext(boxSizeContext)
+}
Box.displayName = 'Box'
diff --git a/src/Flex.tsx b/src/Flex.tsx
index 44d30b4..89e620d 100644
--- a/src/Flex.tsx
+++ b/src/Flex.tsx
@@ -1,21 +1,11 @@
import React, { useLayoutEffect, useMemo, useCallback, PropsWithChildren, useRef } from 'react'
import Yoga, { YogaNode } from 'yoga-layout-prebuilt'
-import * as THREE from 'three'
-import { useFrame, useThree, ReactThreeFiber } from '@react-three/fiber'
-import mergeRefs from 'react-merge-refs'
-import {
- setYogaProperties,
- rmUndefFromObj,
- vectorFromObject,
- Axis,
- getDepthAxis,
- getFlex2DSize,
- getOBBSize,
- getRootShift,
-} from './util'
-import { boxContext, flexContext, SharedFlexContext, SharedBoxContext } from './context'
+import { setYogaProperties, rmUndefFromObj, Axis, getDepthAxis, getFlex2DSize, getAxis } from './util'
+import { boxNodeContext, flexContext, SharedFlexContext } from './context'
import type { R3FlexProps, FlexYogaDirection, FlexPlane } from './props'
+import { Group } from 'three'
+import { useProps } from '.'
export type FlexProps = PropsWithChildren<
Partial<{
@@ -26,348 +16,226 @@ export type FlexProps = PropsWithChildren<
yogaDirection: FlexYogaDirection
plane: FlexPlane
scaleFactor?: number
+ maxUps?: number
onReflow?: (totalWidth: number, totalHeight: number) => void
- disableSizeRecalc?: boolean
- /** Centers flex container in position.
- *
- * !NB center is based on provided flex size, not on the actual content */
- centerAnchor?: boolean
}> &
- R3FlexProps &
- Omit, 'children'>
+ R3FlexProps
>
interface BoxesItem {
node: YogaNode
- group: THREE.Group
- flexProps: R3FlexProps
- centerAnchor: boolean
+ parent: YogaNode
+ yogaIndex: number
+ reactIndex: number | undefined
+ centerAnchor?: boolean
+ onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void
}
-function FlexImpl(
- {
- // Non flex props
- size = [1, 1, 1],
- yogaDirection = 'ltr',
- plane = 'xy',
- children,
- scaleFactor = 100,
- onReflow,
- disableSizeRecalc,
- centerAnchor: rootCenterAnchor,
-
- // flex props
-
- flexDirection,
- flexDir,
- dir,
-
- alignContent,
- alignItems,
- alignSelf,
- align,
-
- justifyContent,
- justify,
-
- flexBasis,
- basis,
- flexGrow,
- grow,
- flexShrink,
- shrink,
-
- flexWrap,
- wrap,
-
- margin,
- m,
- marginBottom,
- marginLeft,
- marginRight,
- marginTop,
- mb,
- ml,
- mr,
- mt,
-
- padding,
- p,
- paddingBottom,
- paddingLeft,
- paddingRight,
- paddingTop,
- pb,
- pl,
- pr,
- pt,
-
- height,
- width,
-
- maxHeight,
- maxWidth,
- minHeight,
- minWidth,
-
- // other
- ...props
- }: FlexProps,
- ref: React.Ref
-) {
+/**
+ * Flex container. Can contain Boxes
+ */
+export function Flex({
+ // Non flex props
+ size = [1, 1, 1],
+ yogaDirection = 'ltr',
+ plane = 'xy',
+ children,
+ scaleFactor = 100,
+ onReflow,
+ maxUps,
+
+ // other
+ ...props
+}: FlexProps) {
// must memoize or the object literal will cause every dependent of flexProps to rerender everytime
- const flexProps: R3FlexProps = useMemo(() => {
- const _flexProps = {
- flexDirection,
- flexDir,
- dir,
-
- alignContent,
- alignItems,
- alignSelf,
- align,
-
- justifyContent,
- justify,
-
- flexBasis,
- basis,
- flexGrow,
- grow,
- flexShrink,
- shrink,
-
- flexWrap,
- wrap,
-
- margin,
- m,
- marginBottom,
- marginLeft,
- marginRight,
- marginTop,
- mb,
- ml,
- mr,
- mt,
+ const [flexProps] = useProps(props)
- padding,
- p,
- paddingBottom,
- paddingLeft,
- paddingRight,
- paddingTop,
- pb,
- pl,
- pr,
- pt,
+ const reflowRef = useRef<() => void>(null as any)
- height,
- width,
+ // Mechanism for invalidating and recalculating layout
+ const reflowTimeout = useRef(undefined)
- maxHeight,
- maxWidth,
- minHeight,
- minWidth,
+ const requestReflow = useCallback(() => {
+ if (reflowTimeout.current == null) {
+ reflowTimeout.current = window.setTimeout(() => {
+ reflowRef.current()
+ reflowTimeout.current = undefined
+ }, 1000 / (maxUps ?? 10))
}
-
- rmUndefFromObj(_flexProps)
- return _flexProps
- }, [
- align,
- alignContent,
- alignItems,
- alignSelf,
- dir,
- flexBasis,
- basis,
- flexDir,
- flexDirection,
- flexGrow,
- grow,
- flexShrink,
- shrink,
- flexWrap,
- height,
- justify,
- justifyContent,
- m,
- margin,
- marginBottom,
- marginLeft,
- marginRight,
- marginTop,
- maxHeight,
- maxWidth,
- mb,
- minHeight,
- minWidth,
- ml,
- mr,
- mt,
- p,
- padding,
- paddingBottom,
- paddingLeft,
- paddingRight,
- paddingTop,
- pb,
- pl,
- pr,
- pt,
- width,
- wrap,
- ])
-
- const rootGroup = useRef()
+ }, [maxUps])
// Keeps track of the yoga nodes of the children and the related wrapper groups
const boxesRef = useRef([])
+ const dirtyParents = useRef>(new Set())
+
const registerBox = useCallback(
- (node: YogaNode, group: THREE.Group, flexProps: R3FlexProps, centerAnchor: boolean = false) => {
+ (node: YogaNode, parent: YogaNode) => {
+ boxesRef.current.push({ node, reactIndex: undefined, yogaIndex: -1, parent })
+ dirtyParents.current.add(parent)
+ requestReflow()
+ },
+ [requestReflow]
+ )
+ const updateBox = useCallback(
+ (
+ node: YogaNode,
+ index: number | undefined,
+ onUpdateTransformation: (x: number, y: number, width: number, height: number) => void,
+ centerAnchor?: boolean
+ ) => {
+ const i = boxesRef.current.findIndex((b) => b.node === node)
+ if (i !== -1) {
+ boxesRef.current[i] = {
+ ...boxesRef.current[i],
+ reactIndex: index,
+ onUpdateTransformation,
+ centerAnchor,
+ }
+ dirtyParents.current.add(boxesRef.current[i].parent)
+ requestReflow()
+ } else {
+ console.warn(`unable to unregister box (node could not be found)`)
+ }
+ },
+ [requestReflow]
+ )
+ const unregisterBox = useCallback(
+ (node: YogaNode) => {
const i = boxesRef.current.findIndex((b) => b.node === node)
if (i !== -1) {
+ const { parent, node } = boxesRef.current[i]
boxesRef.current.splice(i, 1)
+ parent.removeChild(node)
+ dirtyParents.current.add(parent)
+ requestReflow()
+ } else {
+ console.warn(`unable to unregister box (node could not be found)`)
}
- boxesRef.current.push({ group, node, flexProps, centerAnchor })
},
- []
+ [requestReflow]
)
- const unregisterBox = useCallback((node: YogaNode) => {
- const i = boxesRef.current.findIndex((b) => b.node === node)
- if (i !== -1) {
- boxesRef.current.splice(i, 1)
- }
- }, [])
// Reference to the yoga native node
const node = useMemo(() => Yoga.Node.create(), [])
+
useLayoutEffect(() => {
- setYogaProperties(node, flexProps, scaleFactor)
- }, [node, flexProps, scaleFactor])
+ reflowRef.current = () => {
+ // Common variables for reflow
+ const mainAxis = plane[0] as Axis
+ const crossAxis = plane[1] as Axis
+ const depthAxis = getDepthAxis(plane)
+ const [flexWidth, flexHeight] = getFlex2DSize(size, plane)
+ const yogaDirection_ =
+ yogaDirection === 'ltr' ? Yoga.DIRECTION_LTR : yogaDirection === 'rtl' ? Yoga.DIRECTION_RTL : yogaDirection
+
+ dirtyParents.current.forEach((parent) => updateRealBoxIndices(boxesRef.current, parent))
+ dirtyParents.current.clear()
+
+ // Perform yoga layout calculation
+ node.calculateLayout(flexWidth * scaleFactor, flexHeight * scaleFactor, yogaDirection_)
+
+ let minX = 0
+ let maxX = 0
+ let minY = 0
+ let maxY = 0
+
+ // Reposition after recalculation
+ boxesRef.current.forEach(({ node, centerAnchor, onUpdateTransformation }) => {
+ const { left, top, width, height } = node.getComputedLayout()
+
+ const axesValues = [left + (centerAnchor ? width / 2 : 0), -(top + (centerAnchor ? height / 2 : 0)), 0]
+ const axes: Array = [mainAxis, crossAxis, depthAxis]
+
+ onUpdateTransformation &&
+ onUpdateTransformation(
+ NaNToZero(getAxis('x', axes, axesValues)) / scaleFactor,
+ NaNToZero(getAxis('y', axes, axesValues)) / scaleFactor,
+ NaNToZero(width) / scaleFactor,
+ NaNToZero(height) / scaleFactor
+ )
+
+ minX = Math.min(minX, left)
+ minY = Math.min(minY, top)
+ maxX = Math.max(maxX, left + width)
+ maxY = Math.max(maxY, top + height)
+ })
- // Mechanism for invalidating and recalculating layout
- const { invalidate } = useThree()
- const dirtyRef = useRef(true)
- const requestReflow = useCallback(() => {
- dirtyRef.current = true
- invalidate()
- }, [invalidate])
+ // Call the reflow event to update resulting size
+ onReflow && onReflow((maxX - minX) / scaleFactor, (maxY - minY) / scaleFactor)
+ }
+ requestReflow()
+ }, [requestReflow, node, onReflow, size, plane, yogaDirection, scaleFactor])
- // We need to reflow everything if flex props changes
useLayoutEffect(() => {
+ setYogaProperties(node, flexProps, scaleFactor)
+ // We need to reflow everything if flex props changes
requestReflow()
- }, [children, flexProps, requestReflow])
-
- // Common variables for reflow
- const boundingBox = useMemo(() => new THREE.Box3(), [])
- const vec = useMemo(() => new THREE.Vector3(), [])
- const mainAxis = plane[0] as Axis
- const crossAxis = plane[1] as Axis
- const depthAxis = getDepthAxis(plane)
- const [flexWidth, flexHeight] = getFlex2DSize(size, plane)
- const yogaDirection_ =
- yogaDirection === 'ltr' ? Yoga.DIRECTION_LTR : yogaDirection === 'rtl' ? Yoga.DIRECTION_RTL : yogaDirection
+ }, [node, flexProps, scaleFactor, requestReflow])
// Shared context for flex and box
const sharedFlexContext = useMemo(
() => ({
+ plane,
requestReflow,
registerBox,
+ updateBox,
unregisterBox,
scaleFactor,
}),
- [requestReflow, registerBox, unregisterBox, scaleFactor]
+ [plane, requestReflow, registerBox, unregisterBox, scaleFactor, updateBox]
)
- const sharedBoxContext = useMemo(
- () => ({ node, size: [flexWidth, flexHeight], centerAnchor: rootCenterAnchor }),
- [node, flexWidth, flexHeight, rootCenterAnchor]
- )
-
- // Handles the reflow procedure
- function reflow() {
- if (!disableSizeRecalc) {
- // Recalc all the sizes
- boxesRef.current.forEach(({ group, node, flexProps }) => {
- const scaledWidth = typeof flexProps.width === 'number' ? flexProps.width * scaleFactor : flexProps.width
- const scaledHeight = typeof flexProps.height === 'number' ? flexProps.height * scaleFactor : flexProps.height
-
- if (scaledWidth !== undefined && scaledHeight !== undefined) {
- // Forced size, no need to calculate bounding box
- node.setWidth(scaledWidth)
- node.setHeight(scaledHeight)
- } else if (node.getChildCount() === 0) {
- // No size specified, calculate size
- if (rootGroup.current) {
- getOBBSize(group, rootGroup.current, boundingBox, vec)
- } else {
- // rootGroup ref is missing for some reason, let's just use usual bounding box
- boundingBox.setFromObject(group).getSize(vec)
- }
-
- node.setWidth(scaledWidth || vec[mainAxis] * scaleFactor)
- node.setHeight(scaledHeight || vec[crossAxis] * scaleFactor)
- }
- })
- }
-
- // Perform yoga layout calculation
- node.calculateLayout(flexWidth * scaleFactor, flexHeight * scaleFactor, yogaDirection_)
-
- const rootWidth = node.getComputedWidth()
- const rootHeight = node.getComputedHeight()
-
- let minX = 0
- let maxX = 0
- let minY = 0
- let maxY = 0
-
- // Reposition after recalculation
- boxesRef.current.forEach(({ group, node, centerAnchor }) => {
- const { left, top, width, height } = node.getComputedLayout()
- const [mainAxisShift, crossAxisShift] = getRootShift(rootCenterAnchor, rootWidth, rootHeight, node)
-
- const position = vectorFromObject({
- [mainAxis]: (mainAxisShift + left + (centerAnchor ? width / 2 : 0)) / scaleFactor,
- [crossAxis]: -(crossAxisShift + top + (centerAnchor ? height / 2 : 0)) / scaleFactor,
- [depthAxis]: 0,
- } as any)
-
- minX = Math.min(minX, left)
- minY = Math.min(minY, top)
- maxX = Math.max(maxX, left + width)
- maxY = Math.max(maxY, top + height)
- group.position.copy(position)
- })
-
- // Call the reflow event to update resulting size
- onReflow && onReflow((maxX - minX) / scaleFactor, (maxY - minY) / scaleFactor)
-
- // Ask react-three-fiber to perform a render (invalidateFrameLoop)
- invalidate()
- }
+ return (
+
+ {children}
+
+ )
+}
- // We check if we have to reflow every frame
- // This way we can batch the reflow if we have multiple reflow requests
- useFrame(() => {
- if (dirtyRef.current) {
- dirtyRef.current = false
- reflow()
+/**
+ * aligns react index with an ordered continous yogaIndex
+ * @param boxesItems all boxes
+ * @param parent the parent in which the reordering should happen
+ */
+function updateRealBoxIndices(boxesItems: Array, parent: YogaNode): void {
+ //could be done without the filter more efficiently with another data structure (e.g. map with parent as key)
+ sortIndex(boxesItems.filter(({ parent: boxParent }) => boxParent === parent)).forEach((box, index) => {
+ if (box.yogaIndex != index) {
+ if (box.yogaIndex != -1) {
+ parent.removeChild(box.node)
+ }
+ parent.insertChild(box.node, index)
+ box.yogaIndex = index
}
})
+}
- return (
-
-
- {children}
-
-
+function sortIndex(boxes: Array): Array {
+ //split array
+ const { unindexed, indexed } = boxes.reduce<{ indexed: Array; unindexed: Array }>(
+ ({ indexed, unindexed }, box) => ({
+ indexed: box.reactIndex != null ? [...indexed, box] : indexed,
+ unindexed: box.reactIndex == null ? [...unindexed, box] : unindexed,
+ }),
+ { indexed: [], unindexed: [] }
)
+ //sort after react Index
+ const result = indexed.sort(({ reactIndex: r1 }, { reactIndex: r2 }) => r1! - r2!)
+ //fillup array
+ let i = 0
+ let nextUnindexed = unindexed.shift()
+ while (nextUnindexed != null) {
+ const boxAtIndex = result[i]
+ if (boxAtIndex == null || (boxAtIndex.reactIndex != null && boxAtIndex.reactIndex > i)) {
+ result.splice(i, 0, nextUnindexed)
+ nextUnindexed = unindexed.shift()
+ }
+ i++
+ }
+ return result
}
-/**
- * Flex container. Can contain Boxes
- */
-export const Flex = React.forwardRef(FlexImpl)
+function NaNToZero(val: number) {
+ return isNaN(val) ? 0 : val
+}
Flex.displayName = 'Flex'
diff --git a/src/ReferenceGroup.tsx b/src/ReferenceGroup.tsx
new file mode 100644
index 0000000..0dc85db
--- /dev/null
+++ b/src/ReferenceGroup.tsx
@@ -0,0 +1,15 @@
+import * as Fiber from '@react-three/fiber'
+import React, { forwardRef, useMemo, useState } from 'react'
+import { Group } from 'three'
+import mergeRefs from 'react-merge-refs'
+import { referenceGroupContext } from './context'
+
+export const ReferenceGroup = forwardRef(({ children, ...props }, ref) => {
+ const [group, setRef] = useState(null)
+ const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef])
+ return (
+
+ {children}
+
+ )
+})
diff --git a/src/SpringBox.tsx b/src/SpringBox.tsx
new file mode 100644
index 0000000..431db0f
--- /dev/null
+++ b/src/SpringBox.tsx
@@ -0,0 +1,68 @@
+import React, { forwardRef, ReactNode, useMemo } from 'react'
+import { boxSizeContext, usePropsSyncSize, useSpringBox } from '.'
+import { R3FlexProps, useProps } from './props'
+import { useMemoArray } from './Box'
+import { FrameValue, a, AnimatedProps } from '@react-spring/three'
+import { boxNodeContext } from './context'
+import * as Fiber from '@react-three/fiber'
+import mergeRefs from 'react-merge-refs'
+
+export const SpringBox = forwardRef<
+ ReactNode,
+ {
+ onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void
+ centerAnchor?: boolean
+ children: ((width: FrameValue, height: FrameValue) => React.ReactNode) | React.ReactNode
+ index?: number
+ } & R3FlexProps &
+ AnimatedProps
+>(
+ (
+ {
+ // Non-flex props
+ children,
+ centerAnchor,
+
+ onUpdateTransformation,
+ index,
+
+ // other
+ ...props
+ },
+ ref
+ ) => {
+ const [flexProps, groupProps] = useProps>(props)
+
+ const { node, x, y, width, height } = useSpringBox(flexProps, centerAnchor, index, onUpdateTransformation)
+
+ const size = useMemoArray<[FrameValue, FrameValue]>([width, height])
+
+ return (
+
+
+
+ {useMemo(
+ () => (typeof children === 'function' ? children(width, height) : children),
+ [width, height, children]
+ )}
+
+
+
+ )
+ }
+)
+
+export const AutomaticSpringBox = forwardRef<
+ ReactNode,
+ {
+ onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void
+ centerAnchor?: boolean
+ children: ((width: number, height: number) => React.ReactNode) | React.ReactNode
+ index?: number
+ } & R3FlexProps &
+ Fiber.GroupProps
+>((props, ref) => {
+ const [overwrittenProps, setRef] = usePropsSyncSize(props)
+ const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef])
+ return
+})
diff --git a/src/context.ts b/src/context.ts
index 03c242d..eb85200 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -1,23 +1,35 @@
import { createContext } from 'react'
-import { YogaNode } from 'yoga-layout-prebuilt'
import { Group } from 'three'
-import { R3FlexProps } from './props'
+import { YogaNode } from 'yoga-layout-prebuilt'
+import { FlexPlane, R3FlexProps } from './props'
export interface SharedFlexContext {
scaleFactor: number
+ plane: FlexPlane
requestReflow(): void
- registerBox(node: YogaNode, group: Group, flexProps: R3FlexProps, centerAnchor?: boolean): void
+ registerBox(node: YogaNode, parent: YogaNode): void
+ updateBox(
+ node: YogaNode,
+ index: number | undefined,
+ onUpdateTransformation: (x: number, y: number, width: number, height: number) => void,
+ centerAnchor?: boolean
+ ): void
unregisterBox(node: YogaNode): void
notInitialized?: boolean
}
const initialSharedFlexContext: SharedFlexContext = {
+ plane: 'xy',
scaleFactor: 100,
requestReflow() {
console.warn('Flex not initialized! Please report')
},
registerBox() {
console.warn('Flex not initialized! Please report')
+ return 0
+ },
+ updateBox() {
+ console.warn('Flex not initialized! Please report')
},
unregisterBox() {
console.warn('Flex not initialized! Please report')
@@ -27,17 +39,6 @@ const initialSharedFlexContext: SharedFlexContext = {
export const flexContext = createContext(initialSharedFlexContext)
-export interface SharedBoxContext {
- node: YogaNode | null
- size: [number, number]
- centerAnchor?: boolean
- notInitialized?: boolean
-}
-
-const initialSharedBoxContext: SharedBoxContext = {
- node: null,
- size: [0, 0],
- notInitialized: true,
-}
+export const boxNodeContext = createContext(null)
-export const boxContext = createContext(initialSharedBoxContext)
+export const referenceGroupContext = createContext(null as any)
diff --git a/src/hooks.ts b/src/hooks.ts
index 79ae372..ab76df8 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -1,82 +1,66 @@
-import { useCallback, useContext as useContextImpl, useMemo } from 'react'
-import { Mesh, Vector3 } from 'three'
-import { flexContext, boxContext } from './context'
+import { useCallback, useContext as useContextImpl, useMemo, useState } from 'react'
+import { Box3, Object3D, Vector3 } from 'three'
+import { flexContext, boxNodeContext, referenceGroupContext } from './context'
+import { Axis, getOBBSize } from './util'
-export function useContext(context: React.Context) {
+export function useContext(context: React.Context) {
let result = useContextImpl(context)
- if (result.notInitialized) {
+ if (result == null) {
console.warn('You must place this hook/component under a component!')
}
return result
}
-export function useReflow() {
- const { requestReflow } = useContext(flexContext)
- return requestReflow
-}
-
-/**
- * @returns [width, height, centerAnchor]
- */
-export function useFlexSize() {
- const {
- size: [width, height],
- centerAnchor,
- } = useContext(boxContext)
- const value = useMemo(() => [width, height, centerAnchor] as const, [width, height, centerAnchor])
- return value
-}
-
export function useFlexNode() {
- const { node } = useContext(boxContext)
- return node
+ return useContext(boxNodeContext)
}
-/**
- * explicitly set the size of the box's yoga node
- * @requires that the surrounding Flex-Element has `disableSizeRecalc` set to `true`
- */
-export function useSetSize(): (width: number, height: number) => void {
- const { requestReflow, scaleFactor } = useContext(flexContext)
- const node = useFlexNode()
+const boundingBox = new Box3()
+const vec = new Vector3()
- const sync = useCallback(
- (width: number, height: number) => {
- if (node == null) {
- throw new Error('yoga node is null. sync size is impossible')
+export function usePropsSyncSize(
+ flexProps: T
+): [T & { width?: number; height?: number }, (ref: Object3D | undefined) => void] {
+ const [overwrittenProps, setSize] = usePropsSetSize(flexProps)
+ const { plane } = useContext(flexContext)
+ const referenceGroup = useContextImpl(referenceGroupContext)
+ const setRef = useCallback(
+ (ref: Object3D | undefined) => {
+ if (ref == null) {
+ setSize([undefined, undefined])
+ } else {
+ getOBBSize(ref, referenceGroup, boundingBox, vec)
+ const mainAxis = plane[0] as Axis
+ const crossAxis = plane[1] as Axis
+ setSize([vec[mainAxis], vec[crossAxis]])
}
- node.setWidth(width * scaleFactor)
- node.setHeight(height * scaleFactor)
- requestReflow()
},
- [node, requestReflow]
+ [setSize, referenceGroup, plane]
)
-
- return sync
+ return [overwrittenProps, setRef]
}
-const helperVector = new Vector3()
-
/**
- * explicitly sync the yoga node size with a mesh's geometry and uniform global scale
+ * explicitly set the size of the box's yoga node
* @requires that the surrounding Flex-Element has `disableSizeRecalc` set to `true`
*/
-export function useSyncGeometrySize(): (mesh: Mesh) => void {
- const setSize = useSetSize()
- return useCallback(
- (mesh: Mesh) => {
- mesh.updateMatrixWorld()
- helperVector.setFromMatrixScale(mesh.matrixWorld)
-
- //since the scale is in global space but the box boundings are in local space, scaling can't be translated, thus a uniform scaling is required to have this work properly
- if (Math.abs(helperVector.x - helperVector.y) > 0.001 || Math.abs(helperVector.y - helperVector.z) > 0.001) {
- throw new Error('object was not scaled uniformly')
+export function usePropsSetSize(
+ flexProps: T
+): [T & { width?: number; height?: number }, (size: [width: number | undefined, height: number | undefined]) => void] {
+ const [[width, height], setSize] = useState<[width: number | undefined, height: number | undefined]>([
+ undefined,
+ undefined,
+ ])
+ const overwrittenProps = useMemo(() => {
+ if (width != null && height != null) {
+ return {
+ width,
+ height,
+ ...flexProps,
}
- const worldScale = helperVector.x
- mesh.geometry.computeBoundingBox()
- const box = mesh.geometry.boundingBox!
- setSize((box.max.x - box.min.x) * worldScale, (box.max.y - box.min.y) * worldScale)
- },
- [setSize]
- )
+ } else {
+ return flexProps
+ }
+ }, [flexProps, width, height])
+ return [overwrittenProps, setSize]
}
diff --git a/src/index.ts b/src/index.ts
index ccfc616..e24932d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,9 @@
-export * from './Box'
-export * from './Flex'
export * from './props'
export * from './hooks'
+export * from './useBox'
+export * from './useSpringBox'
+export * from './ReferenceGroup'
+export * from './Flex'
+export * from './Box'
+export * from './SpringBox'
export type { Axis } from './util'
diff --git a/src/props.ts b/src/props.ts
index b46b226..cbf45f6 100644
--- a/src/props.ts
+++ b/src/props.ts
@@ -1,4 +1,13 @@
-import { YogaFlexDirection, YogaAlign, YogaJustifyContent, YogaFlexWrap, YogaDirection } from 'yoga-layout-prebuilt'
+import {
+ YogaFlexDirection,
+ YogaAlign,
+ YogaJustifyContent,
+ YogaFlexWrap,
+ YogaDirection,
+ YogaMeasureMode,
+} from 'yoga-layout-prebuilt'
+import { rmUndefFromObj } from './util'
+import { useMemo } from 'react'
export type FlexYogaDirection = YogaDirection | 'ltr' | 'rtl'
export type FlexPlane = 'xy' | 'yz' | 'xz'
@@ -117,4 +126,195 @@ export type R3FlexProps = Partial<{
marginBottom: Value
// Shorthand for marginBottom
mb: Value
+
+ measureFunc: (
+ width: number,
+ widthMeasureMode: YogaMeasureMode,
+ height: number,
+ heightMeasureMode: YogaMeasureMode
+ ) => { width?: number; height?: number } | null
+
+ aspectRatio: number
}>
+
+export function useProps({
+ flexDirection,
+ flexDir,
+ dir,
+
+ alignContent,
+ alignItems,
+ alignSelf,
+ align,
+
+ justifyContent,
+ justify,
+
+ flexBasis,
+ basis,
+ flexGrow,
+ grow,
+
+ flexShrink,
+ shrink,
+
+ flexWrap,
+ wrap,
+
+ margin,
+ m,
+ marginBottom,
+ marginLeft,
+ marginRight,
+ marginTop,
+ mb,
+ ml,
+ mr,
+ mt,
+
+ padding,
+ p,
+ paddingBottom,
+ paddingLeft,
+ paddingRight,
+ paddingTop,
+ pb,
+ pl,
+ pr,
+ pt,
+
+ height,
+ width,
+
+ maxHeight,
+ maxWidth,
+ minHeight,
+ minWidth,
+
+ measureFunc,
+ aspectRatio,
+
+ // other
+ ...props
+}: R3FlexProps & T): [R3FlexProps, typeof props] {
+ return [
+ useMemo(() => {
+ const result = {
+ flexDirection,
+ flexDir,
+ dir,
+
+ alignContent,
+ alignItems,
+ alignSelf,
+ align,
+
+ justifyContent,
+ justify,
+
+ flexBasis,
+ basis,
+ flexGrow,
+ grow,
+
+ flexShrink,
+ shrink,
+
+ flexWrap,
+ wrap,
+
+ margin,
+ m,
+ marginBottom,
+ marginLeft,
+ marginRight,
+ marginTop,
+ mb,
+ ml,
+ mr,
+ mt,
+
+ padding,
+ p,
+ paddingBottom,
+ paddingLeft,
+ paddingRight,
+ paddingTop,
+ pb,
+ pl,
+ pr,
+ pt,
+
+ height,
+ width,
+
+ maxHeight,
+ maxWidth,
+ minHeight,
+ minWidth,
+
+ measureFunc,
+ aspectRatio,
+ }
+ rmUndefFromObj(result)
+ return result
+ }, [
+ flexDirection,
+ flexDir,
+ dir,
+
+ alignContent,
+ alignItems,
+ alignSelf,
+ align,
+
+ justifyContent,
+ justify,
+
+ flexBasis,
+ basis,
+ flexGrow,
+ grow,
+
+ flexShrink,
+ shrink,
+
+ flexWrap,
+ wrap,
+
+ margin,
+ m,
+ marginBottom,
+ marginLeft,
+ marginRight,
+ marginTop,
+ mb,
+ ml,
+ mr,
+ mt,
+
+ padding,
+ p,
+ paddingBottom,
+ paddingLeft,
+ paddingRight,
+ paddingTop,
+ pb,
+ pl,
+ pr,
+ pt,
+
+ height,
+ width,
+
+ maxHeight,
+ maxWidth,
+ minHeight,
+ minWidth,
+
+ measureFunc,
+ aspectRatio,
+ ]),
+ props,
+ ]
+}
diff --git a/src/useBox.ts b/src/useBox.ts
new file mode 100644
index 0000000..cc61fa1
--- /dev/null
+++ b/src/useBox.ts
@@ -0,0 +1,36 @@
+import { useContext, useMemo, useLayoutEffect } from 'react'
+import Yoga, { YogaNode } from 'yoga-layout-prebuilt'
+import { R3FlexProps, useFlexNode } from '.'
+import { flexContext } from './context'
+import { setYogaProperties } from './util'
+
+export function useBox(
+ flexProps: R3FlexProps | undefined,
+ centerAnchor: boolean | undefined,
+ index: number | undefined,
+ onUpdateTransformation: (x: number, y: number, width: number, height: number) => void
+): YogaNode {
+ const { registerBox, unregisterBox, updateBox, scaleFactor, requestReflow } = useContext(flexContext)
+ const parent = useFlexNode()
+ const node = useMemo(() => Yoga.Node.create(), [])
+
+ useLayoutEffect(() => {
+ setYogaProperties(node, flexProps ?? {}, scaleFactor)
+ requestReflow()
+ }, [flexProps, node, scaleFactor, requestReflow])
+
+ //register and unregister box
+ useLayoutEffect(() => {
+ if (!parent) return
+ registerBox(node, parent)
+ return () => unregisterBox(node)
+ }, [node, parent, registerBox, unregisterBox])
+
+ //update box properties
+ useLayoutEffect(
+ () => updateBox(node, index, onUpdateTransformation, centerAnchor),
+ [node, index, centerAnchor, onUpdateTransformation, updateBox]
+ )
+
+ return node
+}
diff --git a/src/useSpringBox.ts b/src/useSpringBox.ts
new file mode 100644
index 0000000..56a1892
--- /dev/null
+++ b/src/useSpringBox.ts
@@ -0,0 +1,40 @@
+import { useCallback } from 'react'
+import { SpringConfig, useSpring } from '@react-spring/three'
+import { useBox } from './useBox'
+import { R3FlexProps } from '.'
+
+export function useSpringBox(
+ flexProps: R3FlexProps | undefined,
+ centerAnchor: boolean | undefined,
+ index: number | undefined,
+ onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void,
+ config?: SpringConfig
+) {
+ const [spring, api] = useSpring(
+ {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ config,
+ },
+ [config]
+ )
+
+ const update = useCallback(
+ (x: number, y: number, width: number, height: number) => {
+ onUpdateTransformation && onUpdateTransformation(x, y, width, height)
+ api.start({
+ x,
+ y,
+ width,
+ height,
+ })
+ },
+ [api, onUpdateTransformation]
+ )
+
+ const node = useBox(flexProps, centerAnchor, index, update)
+
+ return { node, ...spring }
+}
diff --git a/src/util.ts b/src/util.ts
index d2ad6bf..7cad5c1 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -34,6 +34,10 @@ export const setYogaProperties = (node: YogaNode, props: R3FlexProps, scaleFacto
case 'basis':
case 'flexBasis':
return node.setFlexBasis(value)
+ case 'width':
+ return node.setWidth(value)
+ case 'height':
+ return node.setHeight(value)
default:
return (node[`set${capitalize(name)}` as keyof YogaNode] as any)(value)
@@ -44,6 +48,10 @@ export const setYogaProperties = (node: YogaNode, props: R3FlexProps, scaleFacto
case 'basis':
case 'flexBasis':
return node.setFlexBasis(scaledValue)
+ case 'width':
+ return node.setWidth(scaledValue)
+ case 'height':
+ return node.setHeight(scaledValue)
case 'grow':
case 'flexGrow':
return node.setFlexGrow(scaledValue)
@@ -90,19 +98,31 @@ export const setYogaProperties = (node: YogaNode, props: R3FlexProps, scaleFacto
case 'marginBottom':
case 'mb':
return node.setMargin(Yoga.EDGE_BOTTOM, scaledValue)
-
+ case 'aspectRatio':
+ return node.setAspectRatio(value)
default:
return (node[`set${capitalize(name)}` as keyof YogaNode] as any)(scaledValue)
}
+ } else if (typeof value === 'function') {
+ switch (name) {
+ case 'measureFunc':
+ return node.setMeasureFunc(value)
+ }
}
})
}
-export const vectorFromObject = ({ x, y, z }: { x: number; y: number; z: number }) => new Vector3(x, y, z)
-
export type Axis = 'x' | 'y' | 'z'
export const axes: Axis[] = ['x', 'y', 'z']
+export function getAxis(searchAxis: Axis, axes: Array, values: Array) {
+ const index = axes.findIndex((axis) => axis === searchAxis)
+ if (index == -1) {
+ throw new Error(`unable to find axis "${searchAxis}" in [${axes.join(', ')}] `)
+ }
+ return values[index]
+}
+
export function getDepthAxis(plane: FlexPlane) {
switch (plane) {
case 'xy':
@@ -137,38 +157,26 @@ export const rmUndefFromObj = (obj: Record) =>
*
* NB: This doesn't work when object itself is rotated (well, for now)
*/
-export const getOBBSize = (object: Object3D, root: Object3D, bb: Box3, size: Vector3) => {
- object.updateMatrix()
- const oldMatrix = object.matrix
- const oldMatrixAutoUpdate = object.matrixAutoUpdate
-
- root.updateMatrixWorld()
- const m = new Matrix4().copy(root.matrixWorld).invert()
- object.matrix = m
- // to prevent matrix being reassigned
- object.matrixAutoUpdate = false
- root.updateMatrixWorld()
-
- bb.setFromObject(object).getSize(size)
-
- object.matrix = oldMatrix
- object.matrixAutoUpdate = oldMatrixAutoUpdate
- root.updateMatrixWorld()
-}
-
-const getIsTopLevelChild = (node: YogaNode) => !node.getParent()?.getParent()
-
-/** @returns [mainAxisShift, crossAxisShift] */
-export const getRootShift = (
- rootCenterAnchor: boolean | undefined,
- rootWidth: number,
- rootHeight: number,
- node: YogaNode
-) => {
- if (!rootCenterAnchor || !getIsTopLevelChild(node)) {
- return [0, 0]
+export const getOBBSize = (object: Object3D, root: Object3D | null, bb: Box3, size: Vector3) => {
+ if (root == null) {
+ bb.setFromObject(object).getSize(size)
+ } else {
+ object.updateMatrix()
+ const oldMatrix = object.matrix
+ const oldMatrixAutoUpdate = object.matrixAutoUpdate
+
+ root.updateMatrixWorld()
+ const m = new Matrix4().copy(root.matrixWorld).invert()
+ //this also inverts all transformations by "object"
+ object.matrix = m
+ // to prevent matrix being reassigned
+ object.matrixAutoUpdate = false
+ root.updateMatrixWorld()
+
+ bb.setFromObject(object).getSize(size)
+
+ object.matrix = oldMatrix
+ object.matrixAutoUpdate = oldMatrixAutoUpdate
+ root.updateMatrixWorld()
}
- const mainAxisShift = -rootWidth / 2
- const crossAxisShift = -rootHeight / 2
- return [mainAxisShift, crossAxisShift] as const
}
diff --git a/yarn.lock b/yarn.lock
index a06dbc0..26bfa1a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1622,6 +1622,51 @@
prop-types "^15.6.1"
react-lifecycles-compat "^3.0.4"
+"@react-spring/animated@~9.2.5-beta.0":
+ version "9.2.5"
+ resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.5.tgz#7c52e4845b572459a922bc8f5b07122293eede07"
+ integrity sha512-SDIgozNdxQ8xj4xbrF9aDsU/xyZ1WlnbnLo6BAvTciVIwp4Zhbt8keISQ2bq3THDjPxQbRTzxry8pSW2qUwXaw==
+ dependencies:
+ "@react-spring/shared" "~9.2.5-beta.0"
+ "@react-spring/types" "~9.2.5-beta.0"
+
+"@react-spring/core@~9.2.5-beta.0":
+ version "9.2.5"
+ resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.2.5.tgz#fd0cae8e291467dcb94d5dc4eabe43e07cca9697"
+ integrity sha512-3pQOA1QyEu3/8tEfZ0DGklPULyM+bXqJE0JJ0S0lBUivd2MvxhVbJzqoHKxdoHI8CsVSFWMNwwJQ4Vd/XpAk8w==
+ dependencies:
+ "@react-spring/animated" "~9.2.5-beta.0"
+ "@react-spring/shared" "~9.2.5-beta.0"
+ "@react-spring/types" "~9.2.5-beta.0"
+
+"@react-spring/rafz@~9.2.5-beta.0":
+ version "9.2.5"
+ resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.2.5.tgz#517b6bf6407dd791719e5aae11c18fd321c08af1"
+ integrity sha512-FZdbgcBMF1DM/eCnHZ28nHUG984gqcZHWlz2aIfj5TikPTzgVYDECCW/Pvt3ncHLTxikjYn2wvDV3/Q68yqv8A==
+
+"@react-spring/shared@~9.2.5-beta.0":
+ version "9.2.5"
+ resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.5.tgz#ce96cd1063bd644e820b19d9f3ebce8f6077b872"
+ integrity sha512-kutUl8PN0xBSXBmpnHJVFDsgOCFP448syiCcRfdSsryO8kVfJOcSNT4BzIqmzDCWto/neBQJs2iEhKOZTfwQnA==
+ dependencies:
+ "@react-spring/rafz" "~9.2.5-beta.0"
+ "@react-spring/types" "~9.2.5-beta.0"
+
+"@react-spring/three@^9.2.4":
+ version "9.2.5"
+ resolved "https://registry.yarnpkg.com/@react-spring/three/-/three-9.2.5.tgz#1b66dfe4ca3982a3800410608e11894424940802"
+ integrity sha512-FtrxN6KDVMYaymMvwxNNZAJinLQHeKJ5TMQK+GqfLkFuJyNjUgiAThnAWYrp76iZrSR3AxOqNkZ36YZ7cjwNjA==
+ dependencies:
+ "@react-spring/animated" "~9.2.5-beta.0"
+ "@react-spring/core" "~9.2.5-beta.0"
+ "@react-spring/shared" "~9.2.5-beta.0"
+ "@react-spring/types" "~9.2.5-beta.0"
+
+"@react-spring/types@~9.2.5-beta.0":
+ version "9.2.5"
+ resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.2.5.tgz#14eeca9ed7d5beed8c3fc943ee8f365c5c9fa635"
+ integrity sha512-ayitxzSUGO4MTQ6VOeNgUviTV/8nxjwGq6Ie+pFgv6JUlOecwdzo2/apEeHN6ae9tbcxQJx6nuDw/yb590M8Uw==
+
"@react-three/drei@^7.3.1":
version "7.3.1"
resolved "https://registry.yarnpkg.com/@react-three/drei/-/drei-7.3.1.tgz#df35632f41e4b23f8b68b887ed9d5af7a9ebf36a"