From 86cf27aae67c152af9d9f5ed17b303c0c8ee73a9 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Fri, 20 Aug 2021 00:05:52 +0200 Subject: [PATCH 01/13] prevent readding boxes at the end of its parent --- src/Box.tsx | 16 +++++++++++----- src/Flex.tsx | 10 ++++++++-- src/context.ts | 3 ++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Box.tsx b/src/Box.tsx index 83ac4cc..95e27c5 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -192,18 +192,24 @@ export function Box({ 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) + if (!parent) return // Remove child on unmount return () => { parent.removeChild(node) unregisterBox(node) } + }, [node, parent]) + + // Make child known to the parents yoga instance *before* it calculates layout + useLayoutEffect(() => { + if (!group.current || !parent) return + + if (registerBox(node, group.current, flexProps, centerAnchor)) { + //newly registered node: add it to the parent + parent.insertChild(node, parent.getChildCount()) + } }, [node, parent, flexProps, centerAnchor, registerBox, unregisterBox]) // We need to reflow if props change diff --git a/src/Flex.tsx b/src/Flex.tsx index c173097..c4172f4 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -214,10 +214,16 @@ export function Flex({ const registerBox = useCallback( (node: YogaNode, group: Group, flexProps: R3FlexProps, centerAnchor: boolean = false) => { const i = boxesRef.current.findIndex((b) => b.node === node) + const boxItem = { group, node, flexProps, centerAnchor } if (i !== -1) { - boxesRef.current.splice(i, 1) + //node already contained: update box + boxesRef.current[i] = boxItem + return false + } else { + //node not contained: insert new box + boxesRef.current.push(boxItem) + return true } - boxesRef.current.push({ group, node, flexProps, centerAnchor }) }, [] ) diff --git a/src/context.ts b/src/context.ts index b016dc2..5c23be5 100644 --- a/src/context.ts +++ b/src/context.ts @@ -6,7 +6,7 @@ import { R3FlexProps } from './props' export interface SharedFlexContext { scaleFactor: number requestReflow(): void - registerBox(node: YogaNode, group: Group, flexProps: R3FlexProps, centerAnchor?: boolean): void + registerBox(node: YogaNode, group: Group, flexProps: R3FlexProps, centerAnchor?: boolean): boolean unregisterBox(node: YogaNode): void } @@ -17,6 +17,7 @@ const initialSharedFlexContext: SharedFlexContext = { }, registerBox() { console.warn('Flex not initialized! Please report') + return false }, unregisterBox() { console.warn('Flex not initialized! Please report') From 0d2c4b9dd5f2483ca8c4f9fe4541b32d7987f154 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Sat, 21 Aug 2021 20:10:34 +0200 Subject: [PATCH 02/13] removed direct threejs bindings, added react-spring --- src/Box.tsx | 43 +++++------------- src/Flex.tsx | 108 ++++++++++++++++++---------------------------- src/SimpleBox.tsx | 26 +++++++++++ src/SpringBox.tsx | 52 ++++++++++++++++++++++ src/context.ts | 7 +-- src/hooks.ts | 5 --- src/util.ts | 11 ++++- 7 files changed, 141 insertions(+), 111 deletions(-) create mode 100644 src/SimpleBox.tsx create mode 100644 src/SpringBox.tsx diff --git a/src/Box.tsx b/src/Box.tsx index 95e27c5..0518d3b 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -1,7 +1,5 @@ -import React, { useLayoutEffect, useRef, useMemo, useState } from 'react' -import * as THREE from 'three' +import React, { useLayoutEffect, useMemo } from 'react' import Yoga from 'yoga-layout-prebuilt' -import { ReactThreeFiber, useFrame } from '@react-three/fiber' import { setYogaProperties, rmUndefFromObj } from './util' import { boxContext, flexContext } from './context' @@ -71,13 +69,15 @@ export function Box({ minHeight, minWidth, + onUpdateTransformation, + // other ...props }: { + onUpdateTransformation: (x: number, y: number, width: number, height: number) => void centerAnchor?: boolean - children: React.ReactNode | ((width: number, height: number) => React.ReactNode) -} & R3FlexProps & - Omit, 'children'>) { + children: React.ReactNode +} & R3FlexProps) { // must memoize or the object literal will cause every dependent of flexProps to rerender everytime const flexProps: R3FlexProps = useMemo(() => { const _flexProps = { @@ -184,7 +184,6 @@ export function Box({ const { registerBox, unregisterBox, scaleFactor } = useContext(flexContext) const { node: parent } = useContext(boxContext) - const group = useRef() const node = useMemo(() => Yoga.Node.create(), []) const reflow = useReflow() @@ -204,40 +203,20 @@ export function Box({ // Make child known to the parents yoga instance *before* it calculates layout useLayoutEffect(() => { - if (!group.current || !parent) return + if (!parent) return - if (registerBox(node, group.current, flexProps, centerAnchor)) { + if (registerBox(node, flexProps, onUpdateTransformation, centerAnchor)) { //newly registered node: add it to the parent parent.insertChild(node, parent.getChildCount()) } - }, [node, parent, flexProps, centerAnchor, registerBox, unregisterBox]) + }, [node, parent, flexProps, centerAnchor, onUpdateTransformation, 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 }), [node, size]) + const sharedBoxContext = useMemo(() => ({ node }), [node]) - return ( - - - {typeof children === 'function' ? children(size[0], size[1]) : children} - - - ) + return {children} } diff --git a/src/Flex.tsx b/src/Flex.tsx index c4172f4..2f55824 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -1,9 +1,7 @@ import React, { useLayoutEffect, useMemo, useCallback, PropsWithChildren, useRef } from 'react' import Yoga, { YogaNode } from 'yoga-layout-prebuilt' -import { Vector3, Group, Box3, Object3D } from 'three' -import { useFrame, useThree, ReactThreeFiber } from '@react-three/fiber' -import { setYogaProperties, rmUndefFromObj, vectorFromObject, Axis, getDepthAxis, getFlex2DSize } from './util' +import { setYogaProperties, rmUndefFromObj, Axis, getDepthAxis, getFlex2DSize, getAxis } from './util' import { boxContext, flexContext, SharedFlexContext, SharedBoxContext } from './context' import type { R3FlexProps, FlexYogaDirection, FlexPlane } from './props' @@ -16,25 +14,18 @@ export type FlexProps = PropsWithChildren< yogaDirection: FlexYogaDirection plane: FlexPlane scaleFactor?: number + maxUps?: number onReflow?: (totalWidth: number, totalHeight: number) => void - disableSizeRecalc?: boolean }> & - R3FlexProps & - Omit, 'children'> + R3FlexProps > interface BoxesItem { node: YogaNode - group: Group flexProps: R3FlexProps centerAnchor: boolean + onUpdateTransformation: (x: number, y: number, width: number, height: number) => void } -// This is not very performant -// We should probably optimize it, options are -// * Memoization -// * Precalculation of this when registering a box -const hasBoxChildren = (boxes: BoxesItem[], children: Object3D[]) => boxes.some(({ group }) => children.includes(group)) - /** * Flex container. Can contain Boxes */ @@ -46,7 +37,7 @@ export function Flex({ children, scaleFactor = 100, onReflow, - disableSizeRecalc, + maxUps, // flex props @@ -212,9 +203,14 @@ export function Flex({ // Keeps track of the yoga nodes of the children and the related wrapper groups const boxesRef = useRef([]) const registerBox = useCallback( - (node: YogaNode, group: Group, flexProps: R3FlexProps, centerAnchor: boolean = false) => { + ( + node: YogaNode, + flexProps: R3FlexProps, + onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, + centerAnchor: boolean = false + ) => { const i = boxesRef.current.findIndex((b) => b.node === node) - const boxItem = { group, node, flexProps, centerAnchor } + const boxItem = { node, flexProps, centerAnchor, onUpdateTransformation } if (i !== -1) { //node already contained: update box boxesRef.current[i] = boxItem @@ -241,12 +237,16 @@ export function Flex({ }, [node, flexProps, scaleFactor]) // Mechanism for invalidating and recalculating layout - const { invalidate } = useThree() - const dirtyRef = useRef(true) + const reflowTimeout = useRef(undefined) + const requestReflow = useCallback(() => { - dirtyRef.current = true - invalidate() - }, [invalidate]) + if (reflowTimeout.current == null) { + reflowTimeout.current = setTimeout(() => { + reflowTimeout.current = undefined + reflow() + }, 1000 / (maxUps ?? 10)) + } + }, [maxUps]) // We need to reflow everything if flex props changes useLayoutEffect(() => { @@ -254,8 +254,6 @@ export function Flex({ }, [children, flexProps, requestReflow]) // Common variables for reflow - const boundingBox = useMemo(() => new Box3(), []) - const vec = useMemo(() => new Vector3(), []) const mainAxis = plane[0] as Axis const crossAxis = plane[1] as Axis const depthAxis = getDepthAxis(plane) @@ -280,25 +278,6 @@ export function Flex({ // 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 (!hasBoxChildren(boxesRef.current, group.children)) { - // No size specified, calculate 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_) @@ -308,41 +287,36 @@ export function Flex({ let maxY = 0 // Reposition after recalculation - boxesRef.current.forEach(({ group, node, centerAnchor }) => { - const { left, top, width, height } = node.getComputedLayout() - const position = vectorFromObject({ - [mainAxis]: (left + (centerAnchor ? width / 2 : 0)) / scaleFactor, - [crossAxis]: -(top + (centerAnchor ? height / 2 : 0)) / scaleFactor, - [depthAxis]: 0, - } as any) + boxesRef.current.forEach(({ node, centerAnchor, onUpdateTransformation, flexProps }) => { + const { left, top, width: computedWidth, height: computedHeight } = node.getComputedLayout() + + const width = + (typeof flexProps.width === 'number' ? flexProps.width : null) || computedWidth.valueOf() / scaleFactor + const height = + (typeof flexProps.height === 'number' ? flexProps.height : null) || computedHeight.valueOf() / scaleFactor + + const axesValues = [ + (left + (centerAnchor ? width / 2 : 0)) / scaleFactor, + -(top + (centerAnchor ? height / 2 : 0)) / scaleFactor, + 0, + ] + const axes: Array = [mainAxis, crossAxis, depthAxis] + + onUpdateTransformation(getAxis('x', axes, axesValues), getAxis('y', axes, axesValues), width, height) + 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() } - // 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() - } - }) - return ( - - - {children} - - + + {children} + ) } diff --git a/src/SimpleBox.tsx b/src/SimpleBox.tsx new file mode 100644 index 0000000..c443569 --- /dev/null +++ b/src/SimpleBox.tsx @@ -0,0 +1,26 @@ +import React, { ComponentProps, useState } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import { Box } from './Box' + +export function SimpleBox({ + children, + ...props +}: Omit, 'onUpdateTransformation' | 'children'> & { + children: ((x: number, y: number, width: number, height: number) => React.ReactNode) | React.ReactNode +}) { + const [transformation, setTransformation] = useState([0, 0, 0, 0] as [number, number, number, number]) + const onUpdateTransformation = useCallback( + (...params: [x: number, y: number, width: number, height: number]) => setTransformation(params), + [setTransformation] + ) + const child = useMemo( + () => (typeof children === 'function' ? children(...transformation) : children), + [children, transformation] + ) + return ( + + {child} + + ) +} diff --git a/src/SpringBox.tsx b/src/SpringBox.tsx new file mode 100644 index 0000000..5281ecc --- /dev/null +++ b/src/SpringBox.tsx @@ -0,0 +1,52 @@ +import React, { ComponentProps } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import { Box } from './Box' +import { SpringConfig, SpringValue, useSpring } from 'react-spring' + +export function SpringBox({ + children, + config, + ...props +}: Omit, 'onUpdateTransformation' | 'children'> & { + config?: SpringConfig + children: + | (( + x: SpringValue, + y: SpringValue, + width: SpringValue, + height: SpringValue + ) => React.ReactNode) + | React.ReactNode +}) { + const [{ x, y, width, height }, api] = useSpring( + { + x: 0, + y: 0, + width: 0, + height: 0, + config, + }, + [config] + ) + + const onUpdateTransformation = useCallback( + (x: number, y: number, width: number, height: number) => + api.start({ + x, + y, + width, + height, + }), + [api] + ) + const child = useMemo( + () => (typeof children === 'function' ? children(x, y, width, height) : children), + [x, y, width, height, children] + ) + return ( + + {child} + + ) +} diff --git a/src/context.ts b/src/context.ts index 5c23be5..4167174 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,12 +1,11 @@ import { createContext } from 'react' import { YogaNode } from 'yoga-layout-prebuilt' -import { Group } from 'three' import { R3FlexProps } from './props' export interface SharedFlexContext { scaleFactor: number requestReflow(): void - registerBox(node: YogaNode, group: Group, flexProps: R3FlexProps, centerAnchor?: boolean): boolean + registerBox(node: YogaNode, flexProps: R3FlexProps, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, centerAnchor?: boolean): boolean unregisterBox(node: YogaNode): void } @@ -28,12 +27,10 @@ export const flexContext = createContext(initialSharedFlexCon export interface SharedBoxContext { node: YogaNode | null - size: [number, number] } const initialSharedBoxContext: SharedBoxContext = { - node: null, - size: [0, 0], + node: null } export const boxContext = createContext(initialSharedBoxContext) diff --git a/src/hooks.ts b/src/hooks.ts index d8ecf59..6244f15 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -15,11 +15,6 @@ export function useReflow() { return requestReflow } -export function useFlexSize() { - const { size } = useContext(boxContext) - return size -} - export function useFlexNode() { const { node } = useContext(boxContext) return node diff --git a/src/util.ts b/src/util.ts index 42a04a5..a9475cb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,3 @@ -import { Vector3 } from 'three' import Yoga, { YogaNode } from 'yoga-layout-prebuilt' import { R3FlexProps, FlexPlane } from './props' @@ -98,11 +97,19 @@ export const setYogaProperties = (node: YogaNode, props: R3FlexProps, scaleFacto }) } -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, i) => 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': From 2a94db9506e5583806bd5772252d4402f4a17fe1 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Mon, 23 Aug 2021 14:52:58 +0200 Subject: [PATCH 03/13] fixed reordering --- src/Box.tsx | 51 ++++++++++------------ src/Flex.tsx | 91 +++++++++++++++++++++++++++++---------- src/context.ts | 23 +++++----- src/hooks.ts | 20 +++++---- src/props.ts | 18 +++++++- src/{util.ts => util.tsx} | 33 +++++++++++--- 6 files changed, 161 insertions(+), 75 deletions(-) rename src/{util.ts => util.tsx} (83%) diff --git a/src/Box.tsx b/src/Box.tsx index 0518d3b..331f76a 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -2,9 +2,9 @@ import React, { useLayoutEffect, useMemo } from 'react' import Yoga from 'yoga-layout-prebuilt' import { setYogaProperties, rmUndefFromObj } from './util' -import { boxContext, flexContext } from './context' +import { boxNodeContext, boxIndexContext, flexContext } from './context' import { R3FlexProps } from './props' -import { useReflow, useContext } from './hooks' +import { useContext, useFlexNode, useBoxIndex } from './hooks' /** * Box container for 3D Objects. @@ -71,6 +71,9 @@ export function Box({ onUpdateTransformation, + measureFunc, + aspectRatio, + // other ...props }: { @@ -132,6 +135,9 @@ export function Box({ maxWidth, minHeight, minWidth, + + measureFunc, + aspectRatio, } rmUndefFromObj(_flexProps) @@ -180,43 +186,30 @@ export function Box({ pt, width, wrap, + measureFunc, + aspectRatio, ]) - const { registerBox, unregisterBox, scaleFactor } = useContext(flexContext) - const { node: parent } = useContext(boxContext) - const node = useMemo(() => Yoga.Node.create(), []) - const reflow = useReflow() + const { registerBox, unregisterBox, updateBox, scaleFactor } = useContext(flexContext) + const parent = useFlexNode() + const index = useBoxIndex() + const node = useMemo(() => Yoga.Node.create(), [index]) useLayoutEffect(() => { setYogaProperties(node, flexProps, scaleFactor) }, [flexProps, node, scaleFactor]) + //register and unregister box useLayoutEffect(() => { if (!parent) return + registerBox(node, parent, index) + return () => unregisterBox(node) + }, [node, index, parent, registerBox, unregisterBox]) - // Remove child on unmount - return () => { - parent.removeChild(node) - unregisterBox(node) - } - }, [node, parent]) - - // Make child known to the parents yoga instance *before* it calculates layout - useLayoutEffect(() => { - if (!parent) return - - if (registerBox(node, flexProps, onUpdateTransformation, centerAnchor)) { - //newly registered node: add it to the parent - parent.insertChild(node, parent.getChildCount()) - } - }, [node, parent, flexProps, centerAnchor, onUpdateTransformation, registerBox, unregisterBox]) - - // We need to reflow if props change + //update box properties useLayoutEffect(() => { - reflow() - }, [children, flexProps, reflow]) - - const sharedBoxContext = useMemo(() => ({ node }), [node]) + updateBox(node, flexProps, onUpdateTransformation, centerAnchor) + }, [node, flexProps, centerAnchor, onUpdateTransformation, updateBox]) - return {children} + return {children} } diff --git a/src/Flex.tsx b/src/Flex.tsx index 2f55824..e9c3e54 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -2,7 +2,7 @@ import React, { useLayoutEffect, useMemo, useCallback, PropsWithChildren, useRef import Yoga, { YogaNode } from 'yoga-layout-prebuilt' import { setYogaProperties, rmUndefFromObj, Axis, getDepthAxis, getFlex2DSize, getAxis } from './util' -import { boxContext, flexContext, SharedFlexContext, SharedBoxContext } from './context' +import { boxIndexContext, boxNodeContext, flexContext, SharedFlexContext } from './context' import type { R3FlexProps, FlexYogaDirection, FlexPlane } from './props' export type FlexProps = PropsWithChildren< @@ -21,9 +21,12 @@ export type FlexProps = PropsWithChildren< > interface BoxesItem { node: YogaNode - flexProps: R3FlexProps - centerAnchor: boolean - onUpdateTransformation: (x: number, y: number, width: number, height: number) => void + parent: YogaNode + yogaIndex: number + reactIndex: number + flexProps?: R3FlexProps + centerAnchor?: boolean + onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void } /** @@ -93,6 +96,9 @@ export function Flex({ minHeight, minWidth, + measureFunc, + aspectRatio, + // other ...props }: FlexProps) { @@ -150,6 +156,9 @@ export function Flex({ maxWidth, minHeight, minWidth, + + measureFunc, + aspectRatio, } rmUndefFromObj(_flexProps) @@ -198,27 +207,31 @@ export function Flex({ pt, width, wrap, + measureFunc, + aspectRatio, ]) // Keeps track of the yoga nodes of the children and the related wrapper groups const boxesRef = useRef([]) - const registerBox = useCallback( + const registerBox = useCallback((node: YogaNode, parent: YogaNode, index: number) => { + boxesRef.current.push({ node, reactIndex: index, yogaIndex: -1, parent }) + //TODO: defer just like the reflow + updateRealBoxIndices(boxesRef.current, parent) + requestReflow() + }, []) + const updateBox = useCallback( ( node: YogaNode, flexProps: R3FlexProps, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, - centerAnchor: boolean = false + centerAnchor?: boolean ) => { const i = boxesRef.current.findIndex((b) => b.node === node) - const boxItem = { node, flexProps, centerAnchor, onUpdateTransformation } if (i !== -1) { - //node already contained: update box - boxesRef.current[i] = boxItem - return false + boxesRef.current[i] = { ...boxesRef.current[i], flexProps, onUpdateTransformation, centerAnchor } + requestReflow() } else { - //node not contained: insert new box - boxesRef.current.push(boxItem) - return true + console.warn(`unable to unregister box (node could not be found)`) } }, [] @@ -226,7 +239,14 @@ export function Flex({ 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) + //TODO: defer just like the reflow + updateRealBoxIndices(boxesRef.current, parent) + requestReflow() + } else { + console.warn(`unable to unregister box (node could not be found)`) } }, []) @@ -251,7 +271,7 @@ export function Flex({ // We need to reflow everything if flex props changes useLayoutEffect(() => { requestReflow() - }, [children, flexProps, requestReflow]) + }, [flexProps, requestReflow]) // Common variables for reflow const mainAxis = plane[0] as Axis @@ -266,15 +286,12 @@ export function Flex({ () => ({ requestReflow, registerBox, + updateBox, unregisterBox, scaleFactor, }), [requestReflow, registerBox, unregisterBox, scaleFactor] ) - const sharedBoxContext = useMemo( - () => ({ node, size: [flexWidth, flexHeight] }), - [node, flexWidth, flexHeight] - ) // Handles the reflow procedure function reflow() { @@ -291,9 +308,9 @@ export function Flex({ const { left, top, width: computedWidth, height: computedHeight } = node.getComputedLayout() const width = - (typeof flexProps.width === 'number' ? flexProps.width : null) || computedWidth.valueOf() / scaleFactor + (typeof flexProps?.width === 'number' ? flexProps.width : null) || computedWidth.valueOf() / scaleFactor const height = - (typeof flexProps.height === 'number' ? flexProps.height : null) || computedHeight.valueOf() / scaleFactor + (typeof flexProps?.height === 'number' ? flexProps.height : null) || computedHeight.valueOf() / scaleFactor const axesValues = [ (left + (centerAnchor ? width / 2 : 0)) / scaleFactor, @@ -302,7 +319,8 @@ export function Flex({ ] const axes: Array = [mainAxis, crossAxis, depthAxis] - onUpdateTransformation(getAxis('x', axes, axesValues), getAxis('y', axes, axesValues), width, height) + onUpdateTransformation && + onUpdateTransformation(getAxis('x', axes, axesValues), getAxis('y', axes, axesValues), width, height) minX = Math.min(minX, left) minY = Math.min(minY, top) @@ -314,9 +332,38 @@ export function Flex({ onReflow && onReflow((maxX - minX) / scaleFactor, (maxY - minY) / scaleFactor) } + const indexedChildren = useMemo( + () => + React.Children.map(children, (child, index) => ( + {child} + )), + [children] + ) + return ( - {children} + {indexedChildren} ) } + +/** + * 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) + boxesItems + .filter(({ parent: boxParent }) => boxParent === parent) + .sort(({ reactIndex: r1 }, { reactIndex: r2 }) => r1 - r2) + .forEach((box, index) => { + if (box.yogaIndex != index) { + if (box.yogaIndex != -1) { + parent.removeChild(box.node) + } + parent.insertChild(box.node, index) + box.yogaIndex = index + } + }) +} diff --git a/src/context.ts b/src/context.ts index 4167174..5550a71 100644 --- a/src/context.ts +++ b/src/context.ts @@ -5,7 +5,13 @@ import { R3FlexProps } from './props' export interface SharedFlexContext { scaleFactor: number requestReflow(): void - registerBox(node: YogaNode, flexProps: R3FlexProps, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, centerAnchor?: boolean): boolean + registerBox(node: YogaNode, parent: YogaNode, index: number): void + updateBox( + node: YogaNode, + flexProps: R3FlexProps, + onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, + centerAnchor?: boolean + ): void unregisterBox(node: YogaNode): void } @@ -16,7 +22,10 @@ const initialSharedFlexContext: SharedFlexContext = { }, registerBox() { console.warn('Flex not initialized! Please report') - return false + return 0 + }, + updateBox() { + console.warn('Flex not initialized! Please report') }, unregisterBox() { console.warn('Flex not initialized! Please report') @@ -25,12 +34,6 @@ const initialSharedFlexContext: SharedFlexContext = { export const flexContext = createContext(initialSharedFlexContext) -export interface SharedBoxContext { - node: YogaNode | null -} - -const initialSharedBoxContext: SharedBoxContext = { - node: null -} +export const boxNodeContext = createContext(null) -export const boxContext = createContext(initialSharedBoxContext) +export const boxIndexContext = createContext(-1) diff --git a/src/hooks.ts b/src/hooks.ts index 6244f15..fc0879a 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,23 +1,27 @@ import { useCallback, useContext as useContextImpl } from 'react' import { Mesh, Vector3 } from 'three' -import { flexContext, boxContext } from './context' +import { flexContext, boxNodeContext, boxIndexContext } from './context' export function useContext(context: React.Context) { let result = useContextImpl(context) - if (!result) { + 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 +export function useFlexNode() { + return useContext(boxNodeContext) } -export function useFlexNode() { - const { node } = useContext(boxContext) - return node +export function useBoxIndex() { + const boxIndex = useContextImpl(boxIndexContext) + if (boxIndex == null) { + console.warn( + 'You must place this hook/component under a component directly above the use of multiple children!' + ) + } + return boxIndex } /** diff --git a/src/props.ts b/src/props.ts index b46b226..8ee459e 100644 --- a/src/props.ts +++ b/src/props.ts @@ -1,4 +1,11 @@ -import { YogaFlexDirection, YogaAlign, YogaJustifyContent, YogaFlexWrap, YogaDirection } from 'yoga-layout-prebuilt' +import { + YogaFlexDirection, + YogaAlign, + YogaJustifyContent, + YogaFlexWrap, + YogaDirection, + YogaMeasureMode, +} from 'yoga-layout-prebuilt' export type FlexYogaDirection = YogaDirection | 'ltr' | 'rtl' export type FlexPlane = 'xy' | 'yz' | 'xz' @@ -117,4 +124,13 @@ 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 }> diff --git a/src/util.ts b/src/util.tsx similarity index 83% rename from src/util.ts rename to src/util.tsx index a9475cb..6aa9eaf 100644 --- a/src/util.ts +++ b/src/util.tsx @@ -1,4 +1,7 @@ +import React, { ReactNode } from 'react' +import { useMemo } from 'react' import Yoga, { YogaNode } from 'yoga-layout-prebuilt' +import { boxIndexContext } from './context' import { R3FlexProps, FlexPlane } from './props' export const capitalize = (s: string) => s[0].toUpperCase() + s.slice(1) @@ -89,23 +92,27 @@ 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 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, i) => axis === searchAxis) - if(index == -1) { - throw new Error(`unable to find axis "${searchAxis}" in [${axes.join(", ")}] `) + if (index == -1) { + throw new Error(`unable to find axis "${searchAxis}" in [${axes.join(', ')}] `) } return values[index] } @@ -134,3 +141,19 @@ export function getFlex2DSize(sizes: [number, number, number], plane: FlexPlane) export const rmUndefFromObj = (obj: Record) => Object.keys(obj).forEach((key) => (obj[key] === undefined ? delete obj[key] : {})) + +/** + * need to be applied where new childs emerge (if a wrapper of any kind is in between the underlying childs can't be indexed) + * @param children + * @returns + */ +export function indexChildren(children: React.ReactNode) { + return React.Children.map(children, (child, index) => ( + {child} + )) +} + +export function IndexChildren({ children }: { children: React.ReactNode }) { + const child = useMemo(() => indexChildren(children), [children]) + return <>{child} +} From d41a6f57621b0d45cdcc0619d92418948506db09 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Mon, 23 Aug 2021 17:39:36 +0200 Subject: [PATCH 04/13] improved reordering performance --- src/Box.tsx | 18 +++++++++------- src/Flex.tsx | 44 +++++++++++++++++++++++++-------------- src/context.ts | 3 +-- src/hooks.ts | 12 +---------- src/{util.tsx => util.ts} | 19 ----------------- 5 files changed, 40 insertions(+), 56 deletions(-) rename src/{util.tsx => util.ts} (87%) diff --git a/src/Box.tsx b/src/Box.tsx index 331f76a..b0dbd99 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -2,9 +2,9 @@ import React, { useLayoutEffect, useMemo } from 'react' import Yoga from 'yoga-layout-prebuilt' import { setYogaProperties, rmUndefFromObj } from './util' -import { boxNodeContext, boxIndexContext, flexContext } from './context' +import { boxNodeContext, flexContext } from './context' import { R3FlexProps } from './props' -import { useContext, useFlexNode, useBoxIndex } from './hooks' +import { useContext, useFlexNode } from './hooks' /** * Box container for 3D Objects. @@ -74,12 +74,15 @@ export function Box({ measureFunc, aspectRatio, + index, + // other ...props }: { onUpdateTransformation: (x: number, y: number, width: number, height: number) => void centerAnchor?: boolean children: React.ReactNode + index?: number } & R3FlexProps) { // must memoize or the object literal will cause every dependent of flexProps to rerender everytime const flexProps: R3FlexProps = useMemo(() => { @@ -192,8 +195,7 @@ export function Box({ const { registerBox, unregisterBox, updateBox, scaleFactor } = useContext(flexContext) const parent = useFlexNode() - const index = useBoxIndex() - const node = useMemo(() => Yoga.Node.create(), [index]) + const node = useMemo(() => Yoga.Node.create(), []) useLayoutEffect(() => { setYogaProperties(node, flexProps, scaleFactor) @@ -202,14 +204,14 @@ export function Box({ //register and unregister box useLayoutEffect(() => { if (!parent) return - registerBox(node, parent, index) + registerBox(node, parent, index ?? 0) return () => unregisterBox(node) - }, [node, index, parent, registerBox, unregisterBox]) + }, [node, parent, registerBox, unregisterBox]) //update box properties useLayoutEffect(() => { - updateBox(node, flexProps, onUpdateTransformation, centerAnchor) - }, [node, flexProps, centerAnchor, onUpdateTransformation, updateBox]) + updateBox(node, index ?? 0, flexProps, onUpdateTransformation, centerAnchor) + }, [node, index, flexProps, centerAnchor, onUpdateTransformation, updateBox]) return {children} } diff --git a/src/Flex.tsx b/src/Flex.tsx index e9c3e54..653b98c 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -2,7 +2,7 @@ import React, { useLayoutEffect, useMemo, useCallback, PropsWithChildren, useRef import Yoga, { YogaNode } from 'yoga-layout-prebuilt' import { setYogaProperties, rmUndefFromObj, Axis, getDepthAxis, getFlex2DSize, getAxis } from './util' -import { boxIndexContext, boxNodeContext, flexContext, SharedFlexContext } from './context' +import { boxNodeContext, flexContext, SharedFlexContext } from './context' import type { R3FlexProps, FlexYogaDirection, FlexPlane } from './props' export type FlexProps = PropsWithChildren< @@ -213,22 +213,31 @@ export function Flex({ // 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, parent: YogaNode, index: number) => { boxesRef.current.push({ node, reactIndex: index, yogaIndex: -1, parent }) - //TODO: defer just like the reflow - updateRealBoxIndices(boxesRef.current, parent) + dirtyParents.current.add(parent) requestReflow() }, []) const updateBox = useCallback( ( node: YogaNode, + index: number, flexProps: R3FlexProps, 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], flexProps, onUpdateTransformation, centerAnchor } + boxesRef.current[i] = { + ...boxesRef.current[i], + reactIndex: index, + flexProps, + onUpdateTransformation, + centerAnchor, + } + dirtyParents.current.add(boxesRef.current[i].parent) requestReflow() } else { console.warn(`unable to unregister box (node could not be found)`) @@ -242,8 +251,7 @@ export function Flex({ const { parent, node } = boxesRef.current[i] boxesRef.current.splice(i, 1) parent.removeChild(node) - //TODO: defer just like the reflow - updateRealBoxIndices(boxesRef.current, parent) + dirtyParents.current.add(parent) requestReflow() } else { console.warn(`unable to unregister box (node could not be found)`) @@ -295,6 +303,9 @@ export function Flex({ // Handles the reflow procedure function reflow() { + dirtyParents.current.forEach((parent) => updateRealBoxIndices(boxesRef.current, parent)) + dirtyParents.current.clear() + // Perform yoga layout calculation node.calculateLayout(flexWidth * scaleFactor, flexHeight * scaleFactor, yogaDirection_) @@ -320,7 +331,12 @@ export function Flex({ const axes: Array = [mainAxis, crossAxis, depthAxis] onUpdateTransformation && - onUpdateTransformation(getAxis('x', axes, axesValues), getAxis('y', axes, axesValues), width, height) + onUpdateTransformation( + NaNToZero(getAxis('x', axes, axesValues)), + NaNToZero(getAxis('y', axes, axesValues)), + NaNToZero(width), + NaNToZero(height) + ) minX = Math.min(minX, left) minY = Math.min(minY, top) @@ -332,17 +348,9 @@ export function Flex({ onReflow && onReflow((maxX - minX) / scaleFactor, (maxY - minY) / scaleFactor) } - const indexedChildren = useMemo( - () => - React.Children.map(children, (child, index) => ( - {child} - )), - [children] - ) - return ( - {indexedChildren} + {children} ) } @@ -367,3 +375,7 @@ function updateRealBoxIndices(boxesItems: Array, parent: YogaNode): v } }) } + +function NaNToZero(val: number) { + return isNaN(val) ? 0 : val +} diff --git a/src/context.ts b/src/context.ts index 5550a71..c994bae 100644 --- a/src/context.ts +++ b/src/context.ts @@ -8,6 +8,7 @@ export interface SharedFlexContext { registerBox(node: YogaNode, parent: YogaNode, index: number): void updateBox( node: YogaNode, + index: number, flexProps: R3FlexProps, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, centerAnchor?: boolean @@ -35,5 +36,3 @@ const initialSharedFlexContext: SharedFlexContext = { export const flexContext = createContext(initialSharedFlexContext) export const boxNodeContext = createContext(null) - -export const boxIndexContext = createContext(-1) diff --git a/src/hooks.ts b/src/hooks.ts index fc0879a..702cb04 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,6 +1,6 @@ import { useCallback, useContext as useContextImpl } from 'react' import { Mesh, Vector3 } from 'three' -import { flexContext, boxNodeContext, boxIndexContext } from './context' +import { flexContext, boxNodeContext } from './context' export function useContext(context: React.Context) { let result = useContextImpl(context) @@ -14,16 +14,6 @@ export function useFlexNode() { return useContext(boxNodeContext) } -export function useBoxIndex() { - const boxIndex = useContextImpl(boxIndexContext) - if (boxIndex == null) { - console.warn( - 'You must place this hook/component under a component directly above the use of multiple children!' - ) - } - return boxIndex -} - /** * explicitly set the size of the box's yoga node * @requires that the surrounding Flex-Element has `disableSizeRecalc` set to `true` diff --git a/src/util.tsx b/src/util.ts similarity index 87% rename from src/util.tsx rename to src/util.ts index 6aa9eaf..6a7c9ef 100644 --- a/src/util.tsx +++ b/src/util.ts @@ -1,7 +1,4 @@ -import React, { ReactNode } from 'react' -import { useMemo } from 'react' import Yoga, { YogaNode } from 'yoga-layout-prebuilt' -import { boxIndexContext } from './context' import { R3FlexProps, FlexPlane } from './props' export const capitalize = (s: string) => s[0].toUpperCase() + s.slice(1) @@ -141,19 +138,3 @@ export function getFlex2DSize(sizes: [number, number, number], plane: FlexPlane) export const rmUndefFromObj = (obj: Record) => Object.keys(obj).forEach((key) => (obj[key] === undefined ? delete obj[key] : {})) - -/** - * need to be applied where new childs emerge (if a wrapper of any kind is in between the underlying childs can't be indexed) - * @param children - * @returns - */ -export function indexChildren(children: React.ReactNode) { - return React.Children.map(children, (child, index) => ( - {child} - )) -} - -export function IndexChildren({ children }: { children: React.ReactNode }) { - const child = useMemo(() => indexChildren(children), [children]) - return <>{child} -} From 0b9a312e33148388fcab2949c28e3ce9ec66e54e Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Sat, 28 Aug 2021 16:51:25 +0200 Subject: [PATCH 05/13] hooks for box instead of complete component --- src/Box.tsx | 91 ++++++++++++++++++++++++++++++++----------- src/Flex.tsx | 94 ++++++++++++++++++++++++++------------------- src/SimpleBox.tsx | 26 ------------- src/SpringBox.tsx | 52 ------------------------- src/context.ts | 8 ++-- src/index.ts | 2 + src/useBox.ts | 33 ++++++++++++++++ src/useSpringBox.ts | 40 +++++++++++++++++++ src/util.ts | 34 +++++++++++++++- 9 files changed, 236 insertions(+), 144 deletions(-) delete mode 100644 src/SimpleBox.tsx delete mode 100644 src/SpringBox.tsx create mode 100644 src/useBox.ts create mode 100644 src/useSpringBox.ts diff --git a/src/Box.tsx b/src/Box.tsx index b0dbd99..4910580 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -1,10 +1,18 @@ -import React, { useLayoutEffect, useMemo } from 'react' -import Yoga from 'yoga-layout-prebuilt' +import React, { useMemo, useRef, useState } from 'react' -import { setYogaProperties, rmUndefFromObj } from './util' +import { Axis, getOBBSize, rmUndefFromObj } from './util' import { boxNodeContext, flexContext } from './context' -import { R3FlexProps } from './props' -import { useContext, useFlexNode } from './hooks' +import { R3FlexProps, Value } from './props' +import { useBox } from './useBox' +import { useCallback } from 'react' +import { GroupProps } from '@react-three/fiber' +import { useEffect } from 'react' +import { Box3, Group, Vector3 } from 'three' +import { useContext } from 'react' +import { createContext } from 'react' + +const boundingBox = new Box3() +const vec = new Vector3() /** * Box container for 3D Objects. @@ -193,25 +201,64 @@ export function Box({ aspectRatio, ]) - const { registerBox, unregisterBox, updateBox, scaleFactor } = useContext(flexContext) - const parent = useFlexNode() - const node = useMemo(() => Yoga.Node.create(), []) + const [[x, y, w, h], setTransformation] = useState([0, 0, 0, 0] as [number, number, number, number]) + + const { plane, scaleFactor } = useContext(flexContext) + + const [sizeProps, setOverrideProps] = useState<{ width?: Value; height?: Value }>({}) + + const combinedProps = useMemo(() => ({ ...sizeProps, ...flexProps }), [sizeProps, flexProps]) + + const referenceGroup = useContext(boxReferenceContext) + + const node = useBox( + combinedProps, + centerAnchor, + index, + useCallback((...params: [x: number, y: number, w: number, h: number]) => setTransformation(params), []) + ) - useLayoutEffect(() => { - setYogaProperties(node, flexProps, scaleFactor) - }, [flexProps, node, scaleFactor]) + const group = useRef() + useEffect(() => { + if (width == null && height == null && group.current != null && node.getChildCount() === 0) { + getOBBSize(group.current, referenceGroup.current, boundingBox, vec) + const mainAxis = plane[0] as Axis + const crossAxis = plane[1] as Axis + setOverrideProps({ + width: vec[mainAxis] * scaleFactor, + height: vec[crossAxis] * scaleFactor, + }) + } else { + setOverrideProps({}) + } + }, [children, width, height]) + + const size = useMemo<[number, number]>(() => [w, h], [w, h]) + + return ( + + + + {useMemo(() => (typeof children === 'function' ? children(w, h) : children), [w, h, children])} + + + + ) +} + +const boxReferenceContext = createContext>(null as any) - //register and unregister box - useLayoutEffect(() => { - if (!parent) return - registerBox(node, parent, index ?? 0) - return () => unregisterBox(node) - }, [node, parent, registerBox, unregisterBox]) +export function BoxReferenceGroup({ children, ...props }: GroupProps) { + const ref = useRef() + return ( + + {children} + + ) +} - //update box properties - useLayoutEffect(() => { - updateBox(node, index ?? 0, flexProps, onUpdateTransformation, centerAnchor) - }, [node, index, flexProps, centerAnchor, onUpdateTransformation, updateBox]) +export const boxSizeContext = createContext<[number, number]>(null as any) - return {children} +export function useFlexSize() { + return useContext(boxSizeContext) } diff --git a/src/Flex.tsx b/src/Flex.tsx index 653b98c..fb08e66 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -4,6 +4,7 @@ import Yoga, { YogaNode } from 'yoga-layout-prebuilt' 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' export type FlexProps = PropsWithChildren< Partial<{ @@ -23,7 +24,7 @@ interface BoxesItem { node: YogaNode parent: YogaNode yogaIndex: number - reactIndex: number + reactIndex: number | undefined flexProps?: R3FlexProps centerAnchor?: boolean onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void @@ -211,19 +212,21 @@ export function Flex({ aspectRatio, ]) + const rootGroup = useRef() + // 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, parent: YogaNode, index: number) => { - boxesRef.current.push({ node, reactIndex: index, yogaIndex: -1, parent }) + const registerBox = useCallback((node: YogaNode, parent: YogaNode) => { + boxesRef.current.push({ node, reactIndex: undefined, yogaIndex: -1, parent }) dirtyParents.current.add(parent) requestReflow() }, []) const updateBox = useCallback( ( node: YogaNode, - index: number, + index: number | undefined, flexProps: R3FlexProps, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, centerAnchor?: boolean @@ -258,12 +261,6 @@ export function Flex({ } }, []) - // Reference to the yoga native node - const node = useMemo(() => Yoga.Node.create(), []) - useLayoutEffect(() => { - setYogaProperties(node, flexProps, scaleFactor) - }, [node, flexProps, scaleFactor]) - // Mechanism for invalidating and recalculating layout const reflowTimeout = useRef(undefined) @@ -276,10 +273,13 @@ export function Flex({ } }, [maxUps]) - // We need to reflow everything if flex props changes + // Reference to the yoga native node + const node = useMemo(() => Yoga.Node.create(), []) useLayoutEffect(() => { + setYogaProperties(node, flexProps, scaleFactor) + // We need to reflow everything if flex props changes requestReflow() - }, [flexProps, requestReflow]) + }, [node, flexProps, scaleFactor]) // Common variables for reflow const mainAxis = plane[0] as Axis @@ -292,13 +292,14 @@ export function Flex({ // Shared context for flex and box const sharedFlexContext = useMemo( () => ({ + plane, requestReflow, registerBox, updateBox, unregisterBox, scaleFactor, }), - [requestReflow, registerBox, unregisterBox, scaleFactor] + [plane, requestReflow, registerBox, unregisterBox, scaleFactor] ) // Handles the reflow procedure @@ -316,26 +317,17 @@ export function Flex({ // Reposition after recalculation boxesRef.current.forEach(({ node, centerAnchor, onUpdateTransformation, flexProps }) => { - const { left, top, width: computedWidth, height: computedHeight } = node.getComputedLayout() - - const width = - (typeof flexProps?.width === 'number' ? flexProps.width : null) || computedWidth.valueOf() / scaleFactor - const height = - (typeof flexProps?.height === 'number' ? flexProps.height : null) || computedHeight.valueOf() / scaleFactor - - const axesValues = [ - (left + (centerAnchor ? width / 2 : 0)) / scaleFactor, - -(top + (centerAnchor ? height / 2 : 0)) / scaleFactor, - 0, - ] + 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)), - NaNToZero(getAxis('y', axes, axesValues)), - NaNToZero(width), - NaNToZero(height) + NaNToZero(getAxis('x', axes, axesValues)) / scaleFactor, + NaNToZero(getAxis('y', axes, axesValues)) / scaleFactor, + NaNToZero(width) / scaleFactor, + NaNToZero(height) / scaleFactor ) minX = Math.min(minX, left) @@ -362,18 +354,40 @@ export function Flex({ */ 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) - boxesItems - .filter(({ parent: boxParent }) => boxParent === parent) - .sort(({ reactIndex: r1 }, { reactIndex: r2 }) => r1 - r2) - .forEach((box, index) => { - if (box.yogaIndex != index) { - if (box.yogaIndex != -1) { - parent.removeChild(box.node) - } - parent.insertChild(box.node, index) - box.yogaIndex = index + 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 + } + }) +} + +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 } function NaNToZero(val: number) { diff --git a/src/SimpleBox.tsx b/src/SimpleBox.tsx deleted file mode 100644 index c443569..0000000 --- a/src/SimpleBox.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { ComponentProps, useState } from 'react' -import { useMemo } from 'react' -import { useCallback } from 'react' -import { Box } from './Box' - -export function SimpleBox({ - children, - ...props -}: Omit, 'onUpdateTransformation' | 'children'> & { - children: ((x: number, y: number, width: number, height: number) => React.ReactNode) | React.ReactNode -}) { - const [transformation, setTransformation] = useState([0, 0, 0, 0] as [number, number, number, number]) - const onUpdateTransformation = useCallback( - (...params: [x: number, y: number, width: number, height: number]) => setTransformation(params), - [setTransformation] - ) - const child = useMemo( - () => (typeof children === 'function' ? children(...transformation) : children), - [children, transformation] - ) - return ( - - {child} - - ) -} diff --git a/src/SpringBox.tsx b/src/SpringBox.tsx deleted file mode 100644 index 5281ecc..0000000 --- a/src/SpringBox.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { ComponentProps } from 'react' -import { useMemo } from 'react' -import { useCallback } from 'react' -import { Box } from './Box' -import { SpringConfig, SpringValue, useSpring } from 'react-spring' - -export function SpringBox({ - children, - config, - ...props -}: Omit, 'onUpdateTransformation' | 'children'> & { - config?: SpringConfig - children: - | (( - x: SpringValue, - y: SpringValue, - width: SpringValue, - height: SpringValue - ) => React.ReactNode) - | React.ReactNode -}) { - const [{ x, y, width, height }, api] = useSpring( - { - x: 0, - y: 0, - width: 0, - height: 0, - config, - }, - [config] - ) - - const onUpdateTransformation = useCallback( - (x: number, y: number, width: number, height: number) => - api.start({ - x, - y, - width, - height, - }), - [api] - ) - const child = useMemo( - () => (typeof children === 'function' ? children(x, y, width, height) : children), - [x, y, width, height, children] - ) - return ( - - {child} - - ) -} diff --git a/src/context.ts b/src/context.ts index c994bae..f1f7427 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,14 +1,15 @@ import { createContext } from 'react' import { YogaNode } from 'yoga-layout-prebuilt' -import { R3FlexProps } from './props' +import { FlexPlane, R3FlexProps } from './props' export interface SharedFlexContext { scaleFactor: number + plane: FlexPlane requestReflow(): void - registerBox(node: YogaNode, parent: YogaNode, index: number): void + registerBox(node: YogaNode, parent: YogaNode): void updateBox( node: YogaNode, - index: number, + index: number | undefined, flexProps: R3FlexProps, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, centerAnchor?: boolean @@ -17,6 +18,7 @@ export interface SharedFlexContext { } const initialSharedFlexContext: SharedFlexContext = { + plane: 'xy', scaleFactor: 100, requestReflow() { console.warn('Flex not initialized! Please report') diff --git a/src/index.ts b/src/index.ts index ccfc616..8dcedf7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,6 @@ export * from './Box' export * from './Flex' export * from './props' export * from './hooks' +export * from './useBox' +export * from './useSpringBox' export type { Axis } from './util' diff --git a/src/useBox.ts b/src/useBox.ts new file mode 100644 index 0000000..e9fff2a --- /dev/null +++ b/src/useBox.ts @@ -0,0 +1,33 @@ +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 } = useContext(flexContext) + const parent = useFlexNode() + const node = useMemo(() => Yoga.Node.create(), []) + + useLayoutEffect(() => setYogaProperties(node, flexProps ?? {}, scaleFactor), [flexProps, node, scaleFactor]) + + //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, flexProps ?? {}, onUpdateTransformation, centerAnchor), + [node, index, flexProps, centerAnchor, onUpdateTransformation, updateBox] + ) + + return node +} diff --git a/src/useSpringBox.ts b/src/useSpringBox.ts new file mode 100644 index 0000000..6bccb88 --- /dev/null +++ b/src/useSpringBox.ts @@ -0,0 +1,40 @@ +import { useCallback } from 'react' +import { SpringConfig, useSpring } from 'react-spring' +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 6a7c9ef..bd0d3cb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,4 @@ +import { Box3, Matrix4, Object3D, Vector3 } from 'three' import Yoga, { YogaNode } from 'yoga-layout-prebuilt' import { R3FlexProps, FlexPlane } from './props' @@ -33,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) @@ -43,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) @@ -107,7 +116,7 @@ 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, i) => axis === searchAxis) + const index = axes.findIndex((axis) => axis === searchAxis) if (index == -1) { throw new Error(`unable to find axis "${searchAxis}" in [${axes.join(', ')}] `) } @@ -138,3 +147,26 @@ export function getFlex2DSize(sizes: [number, number, number], plane: FlexPlane) export const rmUndefFromObj = (obj: Record) => Object.keys(obj).forEach((key) => (obj[key] === undefined ? delete obj[key] : {})) + +export const getOBBSize = (object: Object3D, root: Object3D | undefined, 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() + 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() + } +} From 9792322918034f49b60f945695668e7cfad2ee70 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Sun, 12 Sep 2021 16:13:56 +0200 Subject: [PATCH 06/13] react spring box --- .storybook/stories/List.stories.tsx | 29 +-- package.json | 2 + src/Box.tsx | 249 +++++-------------------- src/Flex.tsx | 277 ++++++---------------------- src/SpringBox.tsx | 46 +++++ src/context.ts | 1 - src/index.ts | 5 +- src/props.ts | 13 ++ src/useBox.ts | 4 +- src/useSpringBox.ts | 2 +- 10 files changed, 189 insertions(+), 439 deletions(-) create mode 100644 src/SpringBox.tsx 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/package.json b/package.json index 6315981..d66151b 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 4910580..962c965 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -1,15 +1,14 @@ -import React, { useMemo, useRef, useState } from 'react' - -import { Axis, getOBBSize, rmUndefFromObj } from './util' +import React, { useCallback, useMemo, useRef, useState } from 'react' +import { Axis, getOBBSize } from './util' import { boxNodeContext, flexContext } from './context' -import { R3FlexProps, Value } from './props' -import { useBox } from './useBox' -import { useCallback } from 'react' +import { R3FlexProps, useProps, Value } from './props' import { GroupProps } from '@react-three/fiber' import { useEffect } from 'react' import { Box3, Group, Vector3 } from 'three' import { useContext } from 'react' import { createContext } from 'react' +import { FlexProps, useBox } from '.' +import { FrameValue } from '@react-spring/core' const boundingBox = new Box3() const vec = new Vector3() @@ -23,229 +22,73 @@ export function Box({ 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, - onUpdateTransformation, - - measureFunc, - aspectRatio, - index, + automaticSize, // other ...props }: { - onUpdateTransformation: (x: number, y: number, width: number, height: number) => void + automaticSize?: boolean, + onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void centerAnchor?: boolean - children: React.ReactNode + children: ((width: number, height: number) => React.ReactNode) | React.ReactNode index?: number } & R3FlexProps) { // must memoize or the object literal will cause every dependent of flexProps to rerender everytime - const flexProps: R3FlexProps = useMemo(() => { - const _flexProps = { - flexDirection, - flexDir, - dir, + const flexProps = useProps(props) - 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(_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, - measureFunc, - aspectRatio, - ]) - - const [[x, y, w, h], setTransformation] = useState([0, 0, 0, 0] as [number, number, number, number]) - - const { plane, scaleFactor } = useContext(flexContext) - - const [sizeProps, setOverrideProps] = useState<{ width?: Value; height?: Value }>({}) - - const combinedProps = useMemo(() => ({ ...sizeProps, ...flexProps }), [sizeProps, flexProps]) - - const referenceGroup = useContext(boxReferenceContext) + const [overwrittenProps, setRef] = useBoundingBoxSize(automaticSize, flexProps, children) + const [[x, y, width, height], setTransformation] = useState<[number, number, number, number]>([0, 0, 0, 0]) const node = useBox( - combinedProps, + overwrittenProps, centerAnchor, index, - useCallback((...params: [x: number, y: number, w: number, h: number]) => setTransformation(params), []) + useCallback((x: number, y: number, width: number, height: number) => { + onUpdateTransformation && onUpdateTransformation(x, y, width, height) + setTransformation([x, y, width, height]) + }, [onUpdateTransformation]) ) - const group = useRef() - useEffect(() => { - if (width == null && height == null && group.current != null && node.getChildCount() === 0) { - getOBBSize(group.current, referenceGroup.current, boundingBox, vec) - const mainAxis = plane[0] as Axis - const crossAxis = plane[1] as Axis - setOverrideProps({ - width: vec[mainAxis] * scaleFactor, - height: vec[crossAxis] * scaleFactor, - }) - } else { - setOverrideProps({}) - } - }, [children, width, height]) - - const size = useMemo<[number, number]>(() => [w, h], [w, h]) + const size = useMemoArray<[number, number]>([width, height]) return ( - + - {useMemo(() => (typeof children === 'function' ? children(w, h) : children), [w, h, children])} + {useMemo(() => (typeof children === 'function' ? children(width, height) : children), [width, height, children])} ) } +export function useMemoArray>(array: T): T { + return useMemo(() => array, [array]) +} + +export function useBoundingBoxSize(enable: boolean | undefined, flexProps: FlexProps, children: any): [overwrittenProps: R3FlexProps, setRef: (ref: Group) => void] { + const [ref, setRef] = useState(undefined) + const { plane, scaleFactor } = useContext(flexContext) + const referenceGroup = useContext(boxReferenceContext) + const overwrittenProps = useMemo(() => { + if (!enable && flexProps.width == null && flexProps.height == null && ref != null) { + getOBBSize(ref, referenceGroup.current, boundingBox, vec) + const mainAxis = plane[0] as Axis + const crossAxis = plane[1] as Axis + return { + width: vec[mainAxis] * scaleFactor, + height: vec[crossAxis] * scaleFactor, + ...flexProps + } + } else { + return flexProps + } + }, [enable, ref, flexProps, flexProps, children]) + return [overwrittenProps, setRef] +} + const boxReferenceContext = createContext>(null as any) export function BoxReferenceGroup({ children, ...props }: GroupProps) { @@ -257,7 +100,7 @@ export function BoxReferenceGroup({ children, ...props }: GroupProps) { ) } -export const boxSizeContext = createContext<[number, number]>(null as any) +export const boxSizeContext = createContext<[number | FrameValue, number | FrameValue]>(null as any) export function useFlexSize() { return useContext(boxSizeContext) diff --git a/src/Flex.tsx b/src/Flex.tsx index fb08e66..dbc7328 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -5,6 +5,7 @@ import { setYogaProperties, rmUndefFromObj, Axis, getDepthAxis, getFlex2DSize, g 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<{ @@ -18,14 +19,13 @@ export type FlexProps = PropsWithChildren< maxUps?: number onReflow?: (totalWidth: number, totalHeight: number) => void }> & - R3FlexProps + R3FlexProps > interface BoxesItem { node: YogaNode parent: YogaNode yogaIndex: number reactIndex: number | undefined - flexProps?: R3FlexProps centerAnchor?: boolean onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void } @@ -43,174 +43,11 @@ export function Flex({ onReflow, maxUps, - // 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, - - measureFunc, - aspectRatio, - // 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, - - padding, - p, - paddingBottom, - paddingLeft, - paddingRight, - paddingTop, - pb, - pl, - pr, - pt, - - height, - width, - - maxHeight, - maxWidth, - minHeight, - minWidth, - - measureFunc, - aspectRatio, - } - - 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, - measureFunc, - aspectRatio, - ]) + const flexProps = useProps(props) const rootGroup = useRef() @@ -227,7 +64,6 @@ export function Flex({ ( node: YogaNode, index: number | undefined, - flexProps: R3FlexProps, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, centerAnchor?: boolean ) => { @@ -236,7 +72,6 @@ export function Flex({ boxesRef.current[i] = { ...boxesRef.current[i], reactIndex: index, - flexProps, onUpdateTransformation, centerAnchor, } @@ -261,33 +96,79 @@ export function Flex({ } }, []) + const reflowRef = useRef<() => void>(null as any) + // Mechanism for invalidating and recalculating layout const reflowTimeout = useRef(undefined) const requestReflow = useCallback(() => { + console.log("request reflow") if (reflowTimeout.current == null) { - reflowTimeout.current = setTimeout(() => { + reflowTimeout.current = window.setTimeout(() => { + reflowRef.current() reflowTimeout.current = undefined - reflow() }, 1000 / (maxUps ?? 10)) } }, [maxUps]) + useLayoutEffect(() => { + reflowRef.current = () => { + console.log("reflow") + // 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) + }) + + // Call the reflow event to update resulting size + onReflow && onReflow((maxX - minX) / scaleFactor, (maxY - minY) / scaleFactor) + } + requestReflow() + }, [requestReflow, onReflow, size, plane, yogaDirection, scaleFactor]) + // Reference to the yoga native node const node = useMemo(() => Yoga.Node.create(), []) + useLayoutEffect(() => { setYogaProperties(node, flexProps, scaleFactor) // We need to reflow everything if flex props changes requestReflow() - }, [node, flexProps, scaleFactor]) - - // 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 + }, [node, flexProps, scaleFactor, requestReflow]) // Shared context for flex and box const sharedFlexContext = useMemo( @@ -302,44 +183,6 @@ export function Flex({ [plane, requestReflow, registerBox, unregisterBox, scaleFactor] ) - // Handles the reflow procedure - function reflow() { - 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, flexProps }) => { - 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) - }) - - // Call the reflow event to update resulting size - onReflow && onReflow((maxX - minX) / scaleFactor, (maxY - minY) / scaleFactor) - } - return ( {children} diff --git a/src/SpringBox.tsx b/src/SpringBox.tsx new file mode 100644 index 0000000..3056b07 --- /dev/null +++ b/src/SpringBox.tsx @@ -0,0 +1,46 @@ +import React, { useMemo } from "react" +import { boxSizeContext, useSpringBox } from "."; +import { R3FlexProps, useProps } from "./props"; +import { useMemoArray, useBoundingBoxSize } from "./Box" +import { FrameValue, a } from '@react-spring/three' +import { boxNodeContext } from "./context"; + +export function SpringBox({ + // Non-flex props + children, + centerAnchor, + + onUpdateTransformation, + index, + automaticSize, + + // other + ...props +}: { + automaticSize?: boolean, + onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void + centerAnchor?: boolean + children: ((width: FrameValue, height: FrameValue) => React.ReactNode) | React.ReactNode + index?: number +} & R3FlexProps) { + const flexProps = useProps(props) + + const [overwrittenProps, setRef] = useBoundingBoxSize(automaticSize, flexProps, children) + + const { node, x, y, width, height } = useSpringBox( + overwrittenProps, + centerAnchor, + index, + onUpdateTransformation + ) + + const size = useMemoArray<[FrameValue, FrameValue]>([width, height]) + + return + + + {useMemo(() => (typeof children === 'function' ? children(width, height) : children), [width, height, children])} + + + +} \ No newline at end of file diff --git a/src/context.ts b/src/context.ts index f1f7427..d443316 100644 --- a/src/context.ts +++ b/src/context.ts @@ -10,7 +10,6 @@ export interface SharedFlexContext { updateBox( node: YogaNode, index: number | undefined, - flexProps: R3FlexProps, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void, centerAnchor?: boolean ): void diff --git a/src/index.ts b/src/index.ts index 8dcedf7..d85685f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ -export * from './Box' -export * from './Flex' export * from './props' export * from './hooks' export * from './useBox' export * from './useSpringBox' +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 8ee459e..7ae599d 100644 --- a/src/props.ts +++ b/src/props.ts @@ -6,6 +6,8 @@ import { 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' @@ -134,3 +136,14 @@ export type R3FlexProps = Partial<{ aspectRatio: number }> + +export function useProps(props: R3FlexProps): R3FlexProps { + return useMemo(() => { + const _flexProps = { + ...props + } + + rmUndefFromObj(_flexProps) + return _flexProps + }, Object.values(props)) +} \ No newline at end of file diff --git a/src/useBox.ts b/src/useBox.ts index e9fff2a..ab5db06 100644 --- a/src/useBox.ts +++ b/src/useBox.ts @@ -25,8 +25,8 @@ export function useBox( //update box properties useLayoutEffect( - () => updateBox(node, index, flexProps ?? {}, onUpdateTransformation, centerAnchor), - [node, index, flexProps, centerAnchor, onUpdateTransformation, updateBox] + () => updateBox(node, index, onUpdateTransformation, centerAnchor), + [node, index, centerAnchor, onUpdateTransformation, updateBox] ) return node diff --git a/src/useSpringBox.ts b/src/useSpringBox.ts index 6bccb88..56a1892 100644 --- a/src/useSpringBox.ts +++ b/src/useSpringBox.ts @@ -1,5 +1,5 @@ import { useCallback } from 'react' -import { SpringConfig, useSpring } from 'react-spring' +import { SpringConfig, useSpring } from '@react-spring/three' import { useBox } from './useBox' import { R3FlexProps } from '.' From 6cec9173b7591f23db60ed0a268c72a8db799bbe Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Sun, 12 Sep 2021 22:04:53 +0200 Subject: [PATCH 07/13] merge with master --- .storybook/stories/NestedBoxes.stories.tsx | 54 ++++++++++++++ .storybook/stories/Rotation.stories.tsx | 84 ++++++++++++++++++++++ package.json | 2 +- src/Box.tsx | 45 ++++++------ src/Flex.tsx | 4 +- src/context.ts | 2 + src/hooks.ts | 29 +------- src/util.ts | 26 +++++++ 8 files changed, 195 insertions(+), 51 deletions(-) create mode 100644 .storybook/stories/NestedBoxes.stories.tsx create mode 100644 .storybook/stories/Rotation.stories.tsx diff --git a/.storybook/stories/NestedBoxes.stories.tsx b/.storybook/stories/NestedBoxes.stories.tsx new file mode 100644 index 0000000..615f0f3 --- /dev/null +++ b/.storybook/stories/NestedBoxes.stories.tsx @@ -0,0 +1,54 @@ +import { Box, Flex } from '../../src' +import React, { Suspense } from 'react' +import { ComponentMeta, ComponentStory } from '@storybook/react' +import { Box as Cube } from '@react-three/drei' +import { Color } from '@react-three/fiber' + +import { Setup } from '../Setup' + +const Block = ({ color1, color2 }: { color1: Color; color2: Color }) => { + return ( + + + + + + + + + + + + + ) +} + +const NestedBoxes = ({ width, height }: { width: number; height: number }) => { + return ( + + + + + + + ) +} + +export default { + title: 'Example/NestedBoxes', + component: NestedBoxes, +} as ComponentMeta + +const Template: ComponentStory = (args) => ( + + + + + +) + +export const Primary = Template.bind({}) +Primary.args = { + width: 3, + height: 2, +} diff --git a/.storybook/stories/Rotation.stories.tsx b/.storybook/stories/Rotation.stories.tsx new file mode 100644 index 0000000..9d85a30 --- /dev/null +++ b/.storybook/stories/Rotation.stories.tsx @@ -0,0 +1,84 @@ +import { Box, Flex } from '../../src' +import React, { Suspense } from 'react' +import { ComponentMeta, ComponentStory } from '@storybook/react' + +import { Setup } from '../Setup' +import { MathUtils } from 'three' + +const { degToRad } = MathUtils + +const Rotation = ({ + rotationX, + rotationY, + rotationZ, + rotationXItems, + rotationYItems, + rotationZItems, +}: { + rotationX: number + rotationY: number + rotationZ: number + rotationXItems: number + rotationYItems: number + rotationZItems: number +}) => { + const width = 3 + const height = 1 + return ( + + + + + + + + + + + + + + + ) +} + +export default { + title: 'Example/Rotation', + component: Rotation, +} as ComponentMeta + +const Template: ComponentStory = (args) => ( + + + + + +) + +export const RootRotated = Template.bind({}) +RootRotated.args = { + rotationX: 10, + rotationY: 30, + rotationZ: 10, + rotationXItems: 0, + rotationYItems: 0, + rotationZItems: 0, +} + +export const ItemsRotated = Template.bind({}) +ItemsRotated.args = { + rotationX: 0, + rotationY: 0, + rotationZ: 0, + rotationXItems: 0, + rotationYItems: 45, + rotationZItems: 0, +} diff --git a/package.json b/package.json index d66151b..77eb01f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@react-three/flex", - "version": "0.6.1", + "version": "0.7.0", "description": "`` component for the 3D World.", "keywords": [ "react", diff --git a/src/Box.tsx b/src/Box.tsx index 962c965..9bf3ebe 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react' +import React, { forwardRef, useCallback, useMemo, useRef, useState } from 'react' +import mergeRefs from 'react-merge-refs' import { Axis, getOBBSize } from './util' import { boxNodeContext, flexContext } from './context' import { R3FlexProps, useProps, Value } from './props' @@ -17,7 +18,13 @@ const vec = new Vector3() * Box container for 3D Objects. * For containing Boxes use ``. */ -export function Box({ +export const Box = forwardRef void + centerAnchor?: boolean + children: ((width: number, height: number) => React.ReactNode) | React.ReactNode + index?: number +} & R3FlexProps>(({ // Non-flex props children, centerAnchor, @@ -28,13 +35,7 @@ export function Box({ // other ...props -}: { - automaticSize?: boolean, - onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void - centerAnchor?: boolean - children: ((width: number, height: number) => React.ReactNode) | React.ReactNode - index?: number -} & R3FlexProps) { +}, ref) => { // must memoize or the object literal will cause every dependent of flexProps to rerender everytime const flexProps = useProps(props) @@ -54,7 +55,7 @@ export function Box({ const size = useMemoArray<[number, number]>([width, height]) return ( - + {useMemo(() => (typeof children === 'function' ? children(width, height) : children), [width, height, children])} @@ -62,7 +63,7 @@ export function Box({ ) -} +}) export function useMemoArray>(array: T): T { return useMemo(() => array, [array]) @@ -73,35 +74,37 @@ export function useBoundingBoxSize(enable: boolean | undefined, flexProps: FlexP const { plane, scaleFactor } = useContext(flexContext) const referenceGroup = useContext(boxReferenceContext) const overwrittenProps = useMemo(() => { - if (!enable && flexProps.width == null && flexProps.height == null && ref != null) { - getOBBSize(ref, referenceGroup.current, boundingBox, vec) + if (enable && flexProps.width == null && flexProps.height == null && ref != null) { + getOBBSize(ref, referenceGroup?.current, boundingBox, vec) const mainAxis = plane[0] as Axis const crossAxis = plane[1] as Axis return { - width: vec[mainAxis] * scaleFactor, - height: vec[crossAxis] * scaleFactor, + width: vec[mainAxis], + height: vec[crossAxis], ...flexProps } } else { return flexProps } - }, [enable, ref, flexProps, flexProps, children]) + }, [enable, referenceGroup, ref, flexProps, flexProps, children]) return [overwrittenProps, setRef] } const boxReferenceContext = createContext>(null as any) -export function BoxReferenceGroup({ children, ...props }: GroupProps) { - const ref = useRef() +export const BoxReferenceGroup = forwardRef(({ children, ...props }, ref) => { + const group = useRef() return ( - - {children} + + {children} ) -} +}) 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 dbc7328..ef7c254 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -49,8 +49,6 @@ export function Flex({ // must memoize or the object literal will cause every dependent of flexProps to rerender everytime const flexProps = useProps(props) - const rootGroup = useRef() - // Keeps track of the yoga nodes of the children and the related wrapper groups const boxesRef = useRef([]) const dirtyParents = useRef>(new Set()) @@ -236,3 +234,5 @@ function sortIndex(boxes: Array): Array { function NaNToZero(val: number) { return isNaN(val) ? 0 : val } + +Flex.displayName = 'Flex' diff --git a/src/context.ts b/src/context.ts index d443316..0bc1eb6 100644 --- a/src/context.ts +++ b/src/context.ts @@ -14,6 +14,7 @@ export interface SharedFlexContext { centerAnchor?: boolean ): void unregisterBox(node: YogaNode): void + notInitialized?: boolean } const initialSharedFlexContext: SharedFlexContext = { @@ -32,6 +33,7 @@ const initialSharedFlexContext: SharedFlexContext = { unregisterBox() { console.warn('Flex not initialized! Please report') }, + notInitialized: true, } export const flexContext = createContext(initialSharedFlexContext) diff --git a/src/hooks.ts b/src/hooks.ts index 702cb04..7db5e75 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useContext as useContextImpl } from 'react' +import { useCallback, useContext as useContextImpl, useMemo } from 'react' import { Mesh, Vector3 } from 'three' import { flexContext, boxNodeContext } from './context' @@ -19,6 +19,7 @@ export function useFlexNode() { * @requires that the surrounding Flex-Element has `disableSizeRecalc` set to `true` */ export function useSetSize(): (width: number, height: number) => void { + //TODO const { requestReflow, scaleFactor } = useContext(flexContext) const node = useFlexNode() @@ -36,29 +37,3 @@ export function useSetSize(): (width: number, height: number) => void { return sync } - -const helperVector = new Vector3() - -/** - * explicitly sync the yoga node size with a mesh's geometry and uniform global scale - * @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') - } - 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] - ) -} diff --git a/src/util.ts b/src/util.ts index bd0d3cb..d4a8791 100644 --- a/src/util.ts +++ b/src/util.ts @@ -148,6 +148,15 @@ export function getFlex2DSize(sizes: [number, number, number], plane: FlexPlane) export const rmUndefFromObj = (obj: Record) => Object.keys(obj).forEach((key) => (obj[key] === undefined ? delete obj[key] : {})) +/** + * Adapted code from https://github.com/mrdoob/three.js/issues/11967 + * Calculates oriented bounding box size + * Essentially it negates flex root rotation to provide proper number + * E.g. if root flex group rotatet 45 degress, a cube box of size 1 will report sizes of sqrt(2) + * but it should still be 1 + * + * NB: This doesn't work when object itself is rotated (well, for now) + */ export const getOBBSize = (object: Object3D, root: Object3D | undefined, bb: Box3, size: Vector3) => { if (root == null) { bb.setFromObject(object).getSize(size) @@ -170,3 +179,20 @@ export const getOBBSize = (object: Object3D, root: Object3D | undefined, bb: Box 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] + } + const mainAxisShift = -rootWidth / 2 + const crossAxisShift = -rootHeight / 2 + return [mainAxisShift, crossAxisShift] as const +} From 5f3c7484c80f65a6c0959379cf32fbb71ee8977e Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Sun, 12 Sep 2021 22:25:45 +0200 Subject: [PATCH 08/13] add reference group --- .storybook/stories/Rotation.stories.tsx | 49 +++++++++++++------------ 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/.storybook/stories/Rotation.stories.tsx b/.storybook/stories/Rotation.stories.tsx index 9d85a30..8459bf2 100644 --- a/.storybook/stories/Rotation.stories.tsx +++ b/.storybook/stories/Rotation.stories.tsx @@ -1,4 +1,4 @@ -import { Box, Flex } from '../../src' +import { Box, BoxReferenceGroup, Flex } from '../../src' import React, { Suspense } from 'react' import { ComponentMeta, ComponentStory } from '@storybook/react' @@ -25,28 +25,31 @@ const Rotation = ({ const width = 3 const height = 1 return ( - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + ) } From fc03083e4370ce1bd5c5444cbb3d1e6d41275091 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Sun, 12 Sep 2021 23:09:25 +0200 Subject: [PATCH 09/13] fix stories (rotation) --- .storybook/stories/Rotation.stories.tsx | 16 +- src/Box.tsx | 105 +++++++------ src/Flex.tsx | 7 +- src/SpringBox.tsx | 48 +++--- src/props.ts | 195 ++++++++++++++++++++++-- src/util.ts | 18 +-- 6 files changed, 286 insertions(+), 103 deletions(-) diff --git a/.storybook/stories/Rotation.stories.tsx b/.storybook/stories/Rotation.stories.tsx index 8459bf2..b98825c 100644 --- a/.storybook/stories/Rotation.stories.tsx +++ b/.storybook/stories/Rotation.stories.tsx @@ -34,15 +34,23 @@ const Rotation = ({ justifyContent="flex-start" > - - + + - - + + diff --git a/src/Box.tsx b/src/Box.tsx index 9bf3ebe..946addd 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -18,60 +18,79 @@ const vec = new Vector3() * Box container for 3D Objects. * For containing Boxes use ``. */ -export const Box = forwardRef void - centerAnchor?: boolean - children: ((width: number, height: number) => React.ReactNode) | React.ReactNode - index?: number -} & R3FlexProps>(({ - // Non-flex props - children, - centerAnchor, +export const Box = forwardRef< + Group, + { + automaticSize?: boolean + onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void + centerAnchor?: boolean + children: ((width: number, height: number) => React.ReactNode) | React.ReactNode + index?: number + } & R3FlexProps & + GroupProps +>( + ( + { + // Non-flex props + children, + centerAnchor, - onUpdateTransformation, - index, - automaticSize, + onUpdateTransformation, + index, + automaticSize, - // other - ...props -}, ref) => { - // must memoize or the object literal will cause every dependent of flexProps to rerender everytime - const flexProps = useProps(props) + // other + ...props + }, + ref + ) => { + // must memoize or the object literal will cause every dependent of flexProps to rerender everytime + const [flexProps, groupProps] = useProps(props) - const [overwrittenProps, setRef] = useBoundingBoxSize(automaticSize, flexProps, children) - const [[x, y, width, height], setTransformation] = useState<[number, number, number, number]>([0, 0, 0, 0]) + const [overwrittenProps, setRef] = useBoundingBoxSize(automaticSize, flexProps, children) + const [[x, y, width, height], setTransformation] = useState<[number, number, number, number]>([0, 0, 0, 0]) - const node = useBox( - overwrittenProps, - centerAnchor, - index, - useCallback((x: number, y: number, width: number, height: number) => { - onUpdateTransformation && onUpdateTransformation(x, y, width, height) - setTransformation([x, y, width, height]) - }, [onUpdateTransformation]) - ) + const node = useBox( + overwrittenProps, + 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]) + const size = useMemoArray<[number, number]>([width, height]) - return ( - - - - {useMemo(() => (typeof children === 'function' ? children(width, height) : children), [width, height, children])} - - - - ) -}) + return ( + + + + {useMemo( + () => (typeof children === 'function' ? children(width, height) : children), + [width, height, children] + )} + + + + ) + } +) export function useMemoArray>(array: T): T { return useMemo(() => array, [array]) } -export function useBoundingBoxSize(enable: boolean | undefined, flexProps: FlexProps, children: any): [overwrittenProps: R3FlexProps, setRef: (ref: Group) => void] { +export function useBoundingBoxSize( + enable: boolean | undefined, + flexProps: FlexProps, + children: any +): [overwrittenProps: R3FlexProps, setRef: (ref: Group) => void] { const [ref, setRef] = useState(undefined) - const { plane, scaleFactor } = useContext(flexContext) + const { plane } = useContext(flexContext) const referenceGroup = useContext(boxReferenceContext) const overwrittenProps = useMemo(() => { if (enable && flexProps.width == null && flexProps.height == null && ref != null) { @@ -81,7 +100,7 @@ export function useBoundingBoxSize(enable: boolean | undefined, flexProps: FlexP return { width: vec[mainAxis], height: vec[crossAxis], - ...flexProps + ...flexProps, } } else { return flexProps diff --git a/src/Flex.tsx b/src/Flex.tsx index ef7c254..c18a161 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -19,7 +19,7 @@ export type FlexProps = PropsWithChildren< maxUps?: number onReflow?: (totalWidth: number, totalHeight: number) => void }> & - R3FlexProps + R3FlexProps > interface BoxesItem { node: YogaNode @@ -47,7 +47,7 @@ export function Flex({ ...props }: FlexProps) { // must memoize or the object literal will cause every dependent of flexProps to rerender everytime - const flexProps = useProps(props) + const [flexProps] = useProps(props) // Keeps track of the yoga nodes of the children and the related wrapper groups const boxesRef = useRef([]) @@ -100,7 +100,6 @@ export function Flex({ const reflowTimeout = useRef(undefined) const requestReflow = useCallback(() => { - console.log("request reflow") if (reflowTimeout.current == null) { reflowTimeout.current = window.setTimeout(() => { reflowRef.current() @@ -111,7 +110,6 @@ export function Flex({ useLayoutEffect(() => { reflowRef.current = () => { - console.log("reflow") // Common variables for reflow const mainAxis = plane[0] as Axis const crossAxis = plane[1] as Axis @@ -120,7 +118,6 @@ export function Flex({ const yogaDirection_ = yogaDirection === 'ltr' ? Yoga.DIRECTION_LTR : yogaDirection === 'rtl' ? Yoga.DIRECTION_RTL : yogaDirection - dirtyParents.current.forEach((parent) => updateRealBoxIndices(boxesRef.current, parent)) dirtyParents.current.clear() diff --git a/src/SpringBox.tsx b/src/SpringBox.tsx index 3056b07..f39df4f 100644 --- a/src/SpringBox.tsx +++ b/src/SpringBox.tsx @@ -1,9 +1,10 @@ -import React, { useMemo } from "react" -import { boxSizeContext, useSpringBox } from "."; -import { R3FlexProps, useProps } from "./props"; -import { useMemoArray, useBoundingBoxSize } from "./Box" -import { FrameValue, a } from '@react-spring/three' -import { boxNodeContext } from "./context"; +import React, { useMemo } from 'react' +import { boxSizeContext, useSpringBox } from '.' +import { R3FlexProps, useProps } from './props' +import { useMemoArray, useBoundingBoxSize } from './Box' +import { FrameValue, a, AnimatedProps } from '@react-spring/three' +import { boxNodeContext } from './context' +import { GroupProps } from '@react-three/fiber' export function SpringBox({ // Non-flex props @@ -17,30 +18,31 @@ export function SpringBox({ // other ...props }: { - automaticSize?: boolean, + automaticSize?: boolean onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void centerAnchor?: boolean children: ((width: FrameValue, height: FrameValue) => React.ReactNode) | React.ReactNode index?: number -} & R3FlexProps) { - const flexProps = useProps(props) +} & R3FlexProps & + AnimatedProps) { + const [flexProps, groupProps] = useProps>(props) const [overwrittenProps, setRef] = useBoundingBoxSize(automaticSize, flexProps, children) - const { node, x, y, width, height } = useSpringBox( - overwrittenProps, - centerAnchor, - index, - onUpdateTransformation - ) + const { node, x, y, width, height } = useSpringBox(overwrittenProps, centerAnchor, index, onUpdateTransformation) const size = useMemoArray<[FrameValue, FrameValue]>([width, height]) - return - - - {useMemo(() => (typeof children === 'function' ? children(width, height) : children), [width, height, children])} - - - -} \ No newline at end of file + return ( + + + + {useMemo( + () => (typeof children === 'function' ? children(width, height) : children), + [width, height, children] + )} + + + + ) +} diff --git a/src/props.ts b/src/props.ts index 7ae599d..005dd90 100644 --- a/src/props.ts +++ b/src/props.ts @@ -6,8 +6,10 @@ import { YogaDirection, YogaMeasureMode, } from 'yoga-layout-prebuilt' -import { rmUndefFromObj } from "./util" -import { useMemo } from "react" +import { rmUndefFromObj } from './util' +import { useMemo } from 'react' +import { GroupProps } from '@react-three/fiber' +import { AnimatedProps } from '@react-spring/three' export type FlexYogaDirection = YogaDirection | 'ltr' | 'rtl' export type FlexPlane = 'xy' | 'yz' | 'xz' @@ -137,13 +139,184 @@ export type R3FlexProps = Partial<{ aspectRatio: number }> -export function useProps(props: R3FlexProps): R3FlexProps { - return useMemo(() => { - const _flexProps = { - ...props - } +export function useProps({ + flexDirection, + flexDir, + dir, - rmUndefFromObj(_flexProps) - return _flexProps - }, Object.values(props)) -} \ No newline at end of file + 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, T] { + 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/util.ts b/src/util.ts index d4a8791..4e644a3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -167,6 +167,7 @@ export const getOBBSize = (object: Object3D, root: Object3D | undefined, bb: Box 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 @@ -179,20 +180,3 @@ export const getOBBSize = (object: Object3D, root: Object3D | undefined, bb: Box 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] - } - const mainAxisShift = -rootWidth / 2 - const crossAxisShift = -rootHeight / 2 - return [mainAxisShift, crossAxisShift] as const -} From 82271c799b0e942f77a303ec91a5123bddd46114 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Tue, 14 Sep 2021 19:59:48 +0200 Subject: [PATCH 10/13] usePropsSetSize, AutomaticBox --- src/Box.tsx | 74 ++++++++++----------------------- src/ReferenceGroup.tsx | 14 +++++++ src/SpringBox.tsx | 93 +++++++++++++++++++++++++----------------- src/context.ts | 3 ++ src/hooks.ts | 69 +++++++++++++++++++++---------- src/index.ts | 3 +- src/props.ts | 2 +- src/useBox.ts | 7 +++- 8 files changed, 151 insertions(+), 114 deletions(-) create mode 100644 src/ReferenceGroup.tsx diff --git a/src/Box.tsx b/src/Box.tsx index 946addd..057dfab 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -1,27 +1,21 @@ -import React, { forwardRef, useCallback, useMemo, useRef, useState } from 'react' +import React, { forwardRef, ReactNode, useCallback, useMemo, useState } from 'react' import mergeRefs from 'react-merge-refs' -import { Axis, getOBBSize } from './util' -import { boxNodeContext, flexContext } from './context' -import { R3FlexProps, useProps, Value } from './props' +import { boxNodeContext } from './context' +import { R3FlexProps, useProps } from './props' import { GroupProps } from '@react-three/fiber' -import { useEffect } from 'react' -import { Box3, Group, Vector3 } from 'three' +import { Group } from 'three' import { useContext } from 'react' import { createContext } from 'react' -import { FlexProps, useBox } from '.' +import { useBox, usePropsSyncSize } from '.' import { FrameValue } from '@react-spring/core' -const boundingBox = new Box3() -const vec = new Vector3() - /** * Box container for 3D Objects. * For containing Boxes use ``. */ export const Box = forwardRef< - Group, + ReactNode, { - automaticSize?: boolean onUpdateTransformation?: (x: number, y: number, width: number, height: number) => void centerAnchor?: boolean children: ((width: number, height: number) => React.ReactNode) | React.ReactNode @@ -37,7 +31,6 @@ export const Box = forwardRef< onUpdateTransformation, index, - automaticSize, // other ...props @@ -47,11 +40,10 @@ export const Box = forwardRef< // must memoize or the object literal will cause every dependent of flexProps to rerender everytime const [flexProps, groupProps] = useProps(props) - const [overwrittenProps, setRef] = useBoundingBoxSize(automaticSize, flexProps, children) const [[x, y, width, height], setTransformation] = useState<[number, number, number, number]>([0, 0, 0, 0]) const node = useBox( - overwrittenProps, + flexProps, centerAnchor, index, useCallback( @@ -66,7 +58,7 @@ export const Box = forwardRef< const size = useMemoArray<[number, number]>([width, height]) return ( - + {useMemo( @@ -80,46 +72,24 @@ export const Box = forwardRef< } ) +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 & + GroupProps +>((props, ref) => { + const [overwrittenProps, setRef] = usePropsSyncSize(props) + return +}) + export function useMemoArray>(array: T): T { return useMemo(() => array, [array]) } -export function useBoundingBoxSize( - enable: boolean | undefined, - flexProps: FlexProps, - children: any -): [overwrittenProps: R3FlexProps, setRef: (ref: Group) => void] { - const [ref, setRef] = useState(undefined) - const { plane } = useContext(flexContext) - const referenceGroup = useContext(boxReferenceContext) - const overwrittenProps = useMemo(() => { - if (enable && flexProps.width == null && flexProps.height == null && ref != null) { - getOBBSize(ref, referenceGroup?.current, boundingBox, vec) - const mainAxis = plane[0] as Axis - const crossAxis = plane[1] as Axis - return { - width: vec[mainAxis], - height: vec[crossAxis], - ...flexProps, - } - } else { - return flexProps - } - }, [enable, referenceGroup, ref, flexProps, flexProps, children]) - return [overwrittenProps, setRef] -} - -const boxReferenceContext = createContext>(null as any) - -export const BoxReferenceGroup = forwardRef(({ children, ...props }, ref) => { - const group = useRef() - return ( - - {children} - - ) -}) - export const boxSizeContext = createContext<[number | FrameValue, number | FrameValue]>(null as any) export function useFlexSize() { diff --git a/src/ReferenceGroup.tsx b/src/ReferenceGroup.tsx new file mode 100644 index 0000000..4b65e71 --- /dev/null +++ b/src/ReferenceGroup.tsx @@ -0,0 +1,14 @@ +import { GroupProps } from '@react-three/fiber' +import React, { forwardRef, useRef } from 'react' +import { Group } from 'three' +import mergeRefs from 'react-merge-refs' +import { boxReferenceContext } from './context' + +export const ReferenceGroup = forwardRef(({ children, ...props }, ref) => { + const group = useRef() + return ( + + {children} + + ) +}) diff --git a/src/SpringBox.tsx b/src/SpringBox.tsx index f39df4f..cb0f4c9 100644 --- a/src/SpringBox.tsx +++ b/src/SpringBox.tsx @@ -1,48 +1,67 @@ -import React, { useMemo } from 'react' -import { boxSizeContext, useSpringBox } from '.' +import React, { forwardRef, ReactNode, useMemo } from 'react' +import { boxSizeContext, usePropsSyncSize, useSpringBox } from '.' import { R3FlexProps, useProps } from './props' -import { useMemoArray, useBoundingBoxSize } from './Box' +import { useMemoArray } from './Box' import { FrameValue, a, AnimatedProps } from '@react-spring/three' import { boxNodeContext } from './context' import { GroupProps } from '@react-three/fiber' +import mergeRefs from 'react-merge-refs' -export function SpringBox({ - // Non-flex props - children, - centerAnchor, +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, - automaticSize, + onUpdateTransformation, + index, - // other - ...props -}: { - automaticSize?: boolean - 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) { - const [flexProps, groupProps] = useProps>(props) + // other + ...props + }, + ref + ) => { + const [flexProps, groupProps] = useProps>(props) - const [overwrittenProps, setRef] = useBoundingBoxSize(automaticSize, flexProps, children) + const { node, x, y, width, height } = useSpringBox(flexProps, centerAnchor, index, onUpdateTransformation) - const { node, x, y, width, height } = useSpringBox(overwrittenProps, centerAnchor, index, onUpdateTransformation) + const size = useMemoArray<[FrameValue, FrameValue]>([width, height]) - const size = useMemoArray<[FrameValue, FrameValue]>([width, height]) + return ( + + + + {useMemo( + () => (typeof children === 'function' ? children(width, height) : children), + [width, height, children] + )} + + + + ) + } +) - 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 & + GroupProps +>((props, ref) => { + const [overwrittenProps, setRef] = usePropsSyncSize(props) + return +}) diff --git a/src/context.ts b/src/context.ts index 0bc1eb6..b46caa6 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,4 +1,5 @@ import { createContext } from 'react' +import { Group } from 'three' import { YogaNode } from 'yoga-layout-prebuilt' import { FlexPlane, R3FlexProps } from './props' @@ -39,3 +40,5 @@ const initialSharedFlexContext: SharedFlexContext = { export const flexContext = createContext(initialSharedFlexContext) export const boxNodeContext = createContext(null) + +export const boxReferenceContext = createContext>(null as any) diff --git a/src/hooks.ts b/src/hooks.ts index f18dd4f..6f14332 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,8 +1,9 @@ -import { useCallback, useContext as useContextImpl, useMemo } from 'react' -import { Mesh, Vector3 } from 'three' -import { flexContext, boxNodeContext } from './context' +import { useCallback, useContext as useContextImpl, useMemo, useState } from 'react' +import { Box3, Object3D, Vector3 } from 'three' +import { flexContext, boxNodeContext, boxReferenceContext } 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 == null) { console.warn('You must place this hook/component under a component!') @@ -14,26 +15,52 @@ export function useFlexNode() { 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 { - //TODO - 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 = useContext(boxReferenceContext) + const setRef = useCallback( + (ref: Object3D | undefined) => { + if (ref == null) { + setSize([undefined, undefined]) + } else { + getOBBSize(ref, referenceGroup?.current, 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 [overwrittenProps, setRef] +} - return sync +/** + * explicitly set the size of the box's yoga node + * @requires that the surrounding Flex-Element has `disableSizeRecalc` set to `true` + */ +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, + } + } else { + return flexProps + } + }, [flexProps, width, height]) + return [overwrittenProps, setSize] } diff --git a/src/index.ts b/src/index.ts index d85685f..e24932d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,8 @@ 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 * from './SpringBox' export type { Axis } from './util' diff --git a/src/props.ts b/src/props.ts index 005dd90..b761f5e 100644 --- a/src/props.ts +++ b/src/props.ts @@ -198,7 +198,7 @@ export function useProps({ // other ...props -}: R3FlexProps & T): [R3FlexProps, T] { +}: R3FlexProps & T): [R3FlexProps, typeof props] { return [ useMemo(() => { const result = { diff --git a/src/useBox.ts b/src/useBox.ts index ab5db06..cc61fa1 100644 --- a/src/useBox.ts +++ b/src/useBox.ts @@ -10,11 +10,14 @@ export function useBox( index: number | undefined, onUpdateTransformation: (x: number, y: number, width: number, height: number) => void ): YogaNode { - const { registerBox, unregisterBox, updateBox, scaleFactor } = useContext(flexContext) + const { registerBox, unregisterBox, updateBox, scaleFactor, requestReflow } = useContext(flexContext) const parent = useFlexNode() const node = useMemo(() => Yoga.Node.create(), []) - useLayoutEffect(() => setYogaProperties(node, flexProps ?? {}, scaleFactor), [flexProps, node, scaleFactor]) + useLayoutEffect(() => { + setYogaProperties(node, flexProps ?? {}, scaleFactor) + requestReflow() + }, [flexProps, node, scaleFactor, requestReflow]) //register and unregister box useLayoutEffect(() => { From 982ea7a499c82148fa09865f0bdccd45fa72c8b8 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Tue, 14 Sep 2021 20:57:40 +0200 Subject: [PATCH 11/13] fix stories --- .storybook/stories/NestedBoxes.stories.tsx | 10 +++++----- .storybook/stories/Rotation.stories.tsx | 16 +++++++--------- src/Box.tsx | 3 ++- src/ReferenceGroup.tsx | 11 ++++++----- src/SpringBox.tsx | 3 ++- src/context.ts | 2 +- src/hooks.ts | 6 +++--- src/util.ts | 2 +- 8 files changed, 27 insertions(+), 26 deletions(-) diff --git a/.storybook/stories/NestedBoxes.stories.tsx b/.storybook/stories/NestedBoxes.stories.tsx index 615f0f3..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,16 +9,16 @@ import { Setup } from '../Setup' const Block = ({ color1, color2 }: { color1: Color; color2: Color }) => { return ( - + - - + + - + ) } diff --git a/.storybook/stories/Rotation.stories.tsx b/.storybook/stories/Rotation.stories.tsx index b98825c..65231c3 100644 --- a/.storybook/stories/Rotation.stories.tsx +++ b/.storybook/stories/Rotation.stories.tsx @@ -1,4 +1,4 @@ -import { Box, BoxReferenceGroup, Flex } from '../../src' +import { Box, AutomaticBox, ReferenceGroup, Flex } from '../../src' import React, { Suspense } from 'react' import { ComponentMeta, ComponentStory } from '@storybook/react' @@ -33,29 +33,27 @@ const Rotation = ({ alignItems="stretch" justifyContent="flex-start" > - - + - + - - - + + ) diff --git a/src/Box.tsx b/src/Box.tsx index 057dfab..66d83a7 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -83,7 +83,8 @@ export const AutomaticBox = forwardRef< GroupProps >((props, ref) => { const [overwrittenProps, setRef] = usePropsSyncSize(props) - return + const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef]) + return }) export function useMemoArray>(array: T): T { diff --git a/src/ReferenceGroup.tsx b/src/ReferenceGroup.tsx index 4b65e71..33211b0 100644 --- a/src/ReferenceGroup.tsx +++ b/src/ReferenceGroup.tsx @@ -1,14 +1,15 @@ import { GroupProps } from '@react-three/fiber' -import React, { forwardRef, useRef } from 'react' +import React, { forwardRef, useMemo, useRef, useState } from 'react' import { Group } from 'three' import mergeRefs from 'react-merge-refs' -import { boxReferenceContext } from './context' +import { referenceGroupContext } from './context' export const ReferenceGroup = forwardRef(({ children, ...props }, ref) => { - const group = useRef() + const [group, setRef] = useState(null) + const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef]) return ( - - {children} + + {children} ) }) diff --git a/src/SpringBox.tsx b/src/SpringBox.tsx index cb0f4c9..f5e21b9 100644 --- a/src/SpringBox.tsx +++ b/src/SpringBox.tsx @@ -63,5 +63,6 @@ export const AutomaticSpringBox = forwardRef< GroupProps >((props, ref) => { const [overwrittenProps, setRef] = usePropsSyncSize(props) - return + const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef]) + return }) diff --git a/src/context.ts b/src/context.ts index b46caa6..eb85200 100644 --- a/src/context.ts +++ b/src/context.ts @@ -41,4 +41,4 @@ export const flexContext = createContext(initialSharedFlexCon export const boxNodeContext = createContext(null) -export const boxReferenceContext = createContext>(null as any) +export const referenceGroupContext = createContext(null as any) diff --git a/src/hooks.ts b/src/hooks.ts index 6f14332..ab76df8 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,6 +1,6 @@ import { useCallback, useContext as useContextImpl, useMemo, useState } from 'react' import { Box3, Object3D, Vector3 } from 'three' -import { flexContext, boxNodeContext, boxReferenceContext } from './context' +import { flexContext, boxNodeContext, referenceGroupContext } from './context' import { Axis, getOBBSize } from './util' export function useContext(context: React.Context) { @@ -23,13 +23,13 @@ export function usePropsSyncSize( ): [T & { width?: number; height?: number }, (ref: Object3D | undefined) => void] { const [overwrittenProps, setSize] = usePropsSetSize(flexProps) const { plane } = useContext(flexContext) - const referenceGroup = useContext(boxReferenceContext) + const referenceGroup = useContextImpl(referenceGroupContext) const setRef = useCallback( (ref: Object3D | undefined) => { if (ref == null) { setSize([undefined, undefined]) } else { - getOBBSize(ref, referenceGroup?.current, boundingBox, vec) + getOBBSize(ref, referenceGroup, boundingBox, vec) const mainAxis = plane[0] as Axis const crossAxis = plane[1] as Axis setSize([vec[mainAxis], vec[crossAxis]]) diff --git a/src/util.ts b/src/util.ts index 4e644a3..7cad5c1 100644 --- a/src/util.ts +++ b/src/util.ts @@ -157,7 +157,7 @@ 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 | undefined, bb: Box3, size: Vector3) => { +export const getOBBSize = (object: Object3D, root: Object3D | null, bb: Box3, size: Vector3) => { if (root == null) { bb.setFromObject(object).getSize(size) } else { From b5a5dc3b132ab69b946e905f037e46ae2b490da7 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Wed, 15 Sep 2021 16:31:30 +0200 Subject: [PATCH 12/13] fix eslint --- src/Box.tsx | 14 ++++---- src/Flex.tsx | 78 ++++++++++++++++++++++-------------------- src/ReferenceGroup.tsx | 6 ++-- src/SpringBox.tsx | 8 ++--- src/props.ts | 2 -- 5 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/Box.tsx b/src/Box.tsx index 66d83a7..fc8618f 100644 --- a/src/Box.tsx +++ b/src/Box.tsx @@ -1,13 +1,11 @@ -import React, { forwardRef, ReactNode, useCallback, useMemo, useState } from 'react' +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 { GroupProps } from '@react-three/fiber' -import { Group } from 'three' -import { useContext } from 'react' -import { createContext } from 'react' + import { useBox, usePropsSyncSize } from '.' import { FrameValue } from '@react-spring/core' +import type * as Fiber from '@react-three/fiber' /** * Box container for 3D Objects. @@ -21,7 +19,7 @@ export const Box = forwardRef< children: ((width: number, height: number) => React.ReactNode) | React.ReactNode index?: number } & R3FlexProps & - GroupProps + Fiber.GroupProps >( ( { @@ -38,7 +36,7 @@ export const Box = forwardRef< ref ) => { // must memoize or the object literal will cause every dependent of flexProps to rerender everytime - const [flexProps, groupProps] = useProps(props) + const [flexProps, groupProps] = useProps(props) const [[x, y, width, height], setTransformation] = useState<[number, number, number, number]>([0, 0, 0, 0]) @@ -80,7 +78,7 @@ export const AutomaticBox = forwardRef< children: ((width: number, height: number) => React.ReactNode) | React.ReactNode index?: number } & R3FlexProps & - GroupProps + Fiber.GroupProps >((props, ref) => { const [overwrittenProps, setRef] = usePropsSyncSize(props) const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef]) diff --git a/src/Flex.tsx b/src/Flex.tsx index 36ee98a..89e620d 100644 --- a/src/Flex.tsx +++ b/src/Flex.tsx @@ -49,17 +49,32 @@ export function Flex({ // must memoize or the object literal will cause every dependent of flexProps to rerender everytime const [flexProps] = useProps(props) - const rootGroup = useRef() + const reflowRef = useRef<() => void>(null as any) + + // Mechanism for invalidating and recalculating layout + const reflowTimeout = useRef(undefined) + + const requestReflow = useCallback(() => { + if (reflowTimeout.current == null) { + reflowTimeout.current = window.setTimeout(() => { + reflowRef.current() + reflowTimeout.current = undefined + }, 1000 / (maxUps ?? 10)) + } + }, [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, parent: YogaNode) => { - boxesRef.current.push({ node, reactIndex: undefined, yogaIndex: -1, parent }) - dirtyParents.current.add(parent) - requestReflow() - }, []) + const registerBox = useCallback( + (node: YogaNode, parent: YogaNode) => { + boxesRef.current.push({ node, reactIndex: undefined, yogaIndex: -1, parent }) + dirtyParents.current.add(parent) + requestReflow() + }, + [requestReflow] + ) const updateBox = useCallback( ( node: YogaNode, @@ -81,34 +96,26 @@ export function Flex({ 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)`) + } + }, + [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)`) - } - }, []) - - const reflowRef = useRef<() => void>(null as any) - - // Mechanism for invalidating and recalculating layout - const reflowTimeout = useRef(undefined) - const requestReflow = useCallback(() => { - if (reflowTimeout.current == null) { - reflowTimeout.current = window.setTimeout(() => { - reflowRef.current() - reflowTimeout.current = undefined - }, 1000 / (maxUps ?? 10)) - } - }, [maxUps]) + // Reference to the yoga native node + const node = useMemo(() => Yoga.Node.create(), []) useLayoutEffect(() => { reflowRef.current = () => { @@ -156,10 +163,7 @@ export function Flex({ onReflow && onReflow((maxX - minX) / scaleFactor, (maxY - minY) / scaleFactor) } requestReflow() - }, [requestReflow, onReflow, size, plane, yogaDirection, scaleFactor]) - - // Reference to the yoga native node - const node = useMemo(() => Yoga.Node.create(), []) + }, [requestReflow, node, onReflow, size, plane, yogaDirection, scaleFactor]) useLayoutEffect(() => { setYogaProperties(node, flexProps, scaleFactor) @@ -177,7 +181,7 @@ export function Flex({ unregisterBox, scaleFactor, }), - [plane, requestReflow, registerBox, unregisterBox, scaleFactor] + [plane, requestReflow, registerBox, unregisterBox, scaleFactor, updateBox] ) return ( diff --git a/src/ReferenceGroup.tsx b/src/ReferenceGroup.tsx index 33211b0..0dc85db 100644 --- a/src/ReferenceGroup.tsx +++ b/src/ReferenceGroup.tsx @@ -1,10 +1,10 @@ -import { GroupProps } from '@react-three/fiber' -import React, { forwardRef, useMemo, useRef, useState } from 'react' +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) => { +export const ReferenceGroup = forwardRef(({ children, ...props }, ref) => { const [group, setRef] = useState(null) const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef]) return ( diff --git a/src/SpringBox.tsx b/src/SpringBox.tsx index f5e21b9..431db0f 100644 --- a/src/SpringBox.tsx +++ b/src/SpringBox.tsx @@ -4,7 +4,7 @@ import { R3FlexProps, useProps } from './props' import { useMemoArray } from './Box' import { FrameValue, a, AnimatedProps } from '@react-spring/three' import { boxNodeContext } from './context' -import { GroupProps } from '@react-three/fiber' +import * as Fiber from '@react-three/fiber' import mergeRefs from 'react-merge-refs' export const SpringBox = forwardRef< @@ -15,7 +15,7 @@ export const SpringBox = forwardRef< children: ((width: FrameValue, height: FrameValue) => React.ReactNode) | React.ReactNode index?: number } & R3FlexProps & - AnimatedProps + AnimatedProps >( ( { @@ -31,7 +31,7 @@ export const SpringBox = forwardRef< }, ref ) => { - const [flexProps, groupProps] = useProps>(props) + const [flexProps, groupProps] = useProps>(props) const { node, x, y, width, height } = useSpringBox(flexProps, centerAnchor, index, onUpdateTransformation) @@ -60,7 +60,7 @@ export const AutomaticSpringBox = forwardRef< children: ((width: number, height: number) => React.ReactNode) | React.ReactNode index?: number } & R3FlexProps & - GroupProps + Fiber.GroupProps >((props, ref) => { const [overwrittenProps, setRef] = usePropsSyncSize(props) const mergedReds = useMemo(() => mergeRefs([ref, setRef]), [ref, setRef]) diff --git a/src/props.ts b/src/props.ts index b761f5e..cbf45f6 100644 --- a/src/props.ts +++ b/src/props.ts @@ -8,8 +8,6 @@ import { } from 'yoga-layout-prebuilt' import { rmUndefFromObj } from './util' import { useMemo } from 'react' -import { GroupProps } from '@react-three/fiber' -import { AnimatedProps } from '@react-spring/three' export type FlexYogaDirection = YogaDirection | 'ltr' | 'rtl' export type FlexPlane = 'xy' | 'yz' | 'xz' From c4ef91ed4b28153a98f6fcbb6586fe4b3b1ba860 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Wed, 6 Oct 2021 13:53:31 +0200 Subject: [PATCH 13/13] fixed yarn.lock file --- yarn.lock | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) 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"