diff --git a/README.md b/README.md index 356bd0da..77027820 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,8 @@ Build performant 3D user interfaces for Three.js using @react-three/fiber and yo TODO Release - fix: zoom with ortho camera -- fix: changing font weight with hot reload (test if its the same for normal react state change) -- feat: ref.current.setStyle({ ... }) +- fix: scroll jumps after scrolling once - feat: nesting inside non root/container components (e.g. image) -- fix: scrollbar border radius to high (happens with very long panels) - feat: drag/click threshold - feat: input - fix: decrease clipping rect when scrollbar present diff --git a/examples/apfel/src/App.tsx b/examples/apfel/src/App.tsx index 75ac3fd7..90b0f223 100644 --- a/examples/apfel/src/App.tsx +++ b/examples/apfel/src/App.tsx @@ -21,7 +21,7 @@ const componentPages = { list: ListsPage, slider: SlidersPage, tabs: TabsPage, - tabBar: TabBarsPage, + 'tab-bar': TabBarsPage, progress: ProgressPage, loading: LoadingPage, } @@ -59,17 +59,19 @@ export default function App() { alignItems="center" padding={32} > - - - {Object.keys(componentPages).map((name) => ( - - - {name[0].toUpperCase()} - {name.slice(1)} - - - ))} - + + + + {Object.keys(componentPages).map((name) => ( + + + {name[0].toUpperCase()} + {name.slice(1)} + + + ))} + + diff --git a/examples/default/src/App.tsx b/examples/default/src/App.tsx index c4b65038..08984271 100644 --- a/examples/default/src/App.tsx +++ b/examples/default/src/App.tsx @@ -1,7 +1,26 @@ import { useEffect, useState } from 'react' import { Canvas } from '@react-three/fiber' -import { DefaultProperties, Fullscreen, Text, Container } from '@react-three/uikit' -import { BellRing, Bold, Check, ChevronRight, Italic, Terminal, Underline } from '@react-three/uikit-lucide' +import { + DefaultProperties, + Fullscreen, + Text, + Container, + getPreferredColorScheme, + setPreferredColorScheme, +} from '@react-three/uikit' +import { + BellRing, + Bold, + Check, + ChevronRight, + Copy, + Italic, + Moon, + Sun, + SunMoon, + Terminal, + Underline, +} from '@react-three/uikit-lucide' import { Perf } from 'r3f-perf' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/accordion' @@ -40,7 +59,6 @@ import { DialogHeader, DialogTitle, DialogTrigger, - useCloseDialog, } from '@/dialog.js' import { XWebPointers, noEvents } from '@coconut-xr/xinteraction/react' import { @@ -56,29 +74,100 @@ import { } from '@/alert-dialog.js' import { Tooltip, TooltipContent, TooltipTrigger } from '@/tooltip.js' +const componentPages = { + accordion: AccordionDemo, + alert: AlertDemo, + 'alert-dialog': AlertDialogDemo, + avatar: AvatarDemo, + badge: BadgeDemo, + button: ButtonDemo, + card: CardDemo, + checkbox: CheckboxDemo, + dialog: DialogDemo, + //label: LabelDemo, + pagination: PaginationDemo, + progress: ProgressDemo, + 'radio-group': RadioGroupDemo, + separator: SeparatorDemo, + skeleton: SkeletonDemo, + slider: SliderDemo, + switch: SwitchDemo, + tabs: TabsDemo, + toggle: ToggleDemo, + 'toggle-group': ToggleGroupDemo, + tooltip: TooltipDemo, +} + +const defaultComponent = 'card' + export default function App() { + const [component, set] = useState(() => { + const params = new URLSearchParams(window.location.search) + let selected = params.get('component') + if (selected == null || !(selected in componentPages)) { + selected = defaultComponent + } + return selected as keyof typeof componentPages + }) + const setComponent = (value: keyof typeof componentPages) => { + const params = new URLSearchParams(window.location.search) + params.set('component', value) + history.replaceState(null, '', '?' + params.toString()) + set(value) + } + const [pcs, updatePCS] = useState(() => getPreferredColorScheme()) return ( - - - + + - - - + + + {Object.keys(componentPages).map((name) => ( + + + {name[0].toUpperCase()} + {name.slice(1)} + + + ))} + + {Object.entries(componentPages).map(([name, Component]) => ( + + + + + + ))} + + + + + npx uikit component add apfel {component} + + - - + + ) } @@ -285,7 +374,7 @@ function SwitchDemo() { } function SliderDemo() { - return + return } function SkeletonDemo() { @@ -355,7 +444,7 @@ function ProgressDemo() { return () => clearTimeout(timer) }, []) - return + return } function PaginationDemo() { @@ -517,31 +606,33 @@ function AlertDemo() { //TODO: type="single" collapsible export function AccordionDemo() { return ( - - - - Is it accessible? - - - Yes. It adheres to the WAI-ARIA design pattern. - - - - - Is it styled? - - - Yes. It comes with default styles that matches the other components' aesthetic. - - - - - Is it animated? - - - Yes. It's animated by default, but you can disable it if you prefer. - - - + + + + + Is it accessible? + + + Yes. It adheres to the WAI-ARIA design pattern. + + + + + Is it styled? + + + Yes. It comes with default styles that matches the other components' aesthetic. + + + + + Is it animated? + + + Yes. It's animated by default, but you can disable it if you prefer. + + + + ) } diff --git a/packages/kits/default/button.tsx b/packages/kits/default/button.tsx index 952196aa..b757c06b 100644 --- a/packages/kits/default/button.tsx +++ b/packages/kits/default/button.tsx @@ -2,13 +2,7 @@ import { AllOptionalProperties, Container, DefaultProperties } from '@react-thre import { ComponentPropsWithoutRef } from 'react' import { colors } from './theme' -const buttonVariants: { - [Key in string]: { - containerHoverProps?: ComponentPropsWithoutRef['hover'] - containerProps?: Omit, 'hover'> - defaultProps?: AllOptionalProperties - } -} = { +const buttonVariants = { default: { containerHoverProps: { backgroundOpacity: 0.9, @@ -85,7 +79,13 @@ export function Button({ size?: keyof typeof buttonSizes disabled?: boolean }) { - const { containerProps, defaultProps, containerHoverProps } = buttonVariants[variant] + const { containerProps, defaultProps, containerHoverProps } = buttonVariants[variant] as { + [Key in string]: { + containerHoverProps?: ComponentPropsWithoutRef['hover'] + containerProps?: Omit, 'hover'> + defaultProps?: AllOptionalProperties + } + } const sizeProps = buttonSizes[size] return ( diff --git a/packages/kits/default/dialog.tsx b/packages/kits/default/dialog.tsx index 13437716..efd461a5 100644 --- a/packages/kits/default/dialog.tsx +++ b/packages/kits/default/dialog.tsx @@ -184,7 +184,8 @@ export function DialogFooter(props: ComponentPropsWithoutRef) return ( ) diff --git a/packages/kits/default/slider.tsx b/packages/kits/default/slider.tsx index ec3097d0..d81fabf9 100644 --- a/packages/kits/default/slider.tsx +++ b/packages/kits/default/slider.tsx @@ -93,7 +93,7 @@ export function Slider({ setValue?.(value)} + onClick={disabled ? undefined : (e) => setValue?.(value)} cursor={disabled ? undefined : 'pointer'} flexDirection="row" alignItems="center" diff --git a/packages/uikit/src/clipping.ts b/packages/uikit/src/clipping.ts index 3533867c..bb293b80 100644 --- a/packages/uikit/src/clipping.ts +++ b/packages/uikit/src/clipping.ts @@ -105,23 +105,22 @@ const multiplier = [ export function useIsClipped( parentClippingRect: Signal | undefined, - globalMatrix: Signal, + globalMatrix: Signal, size: Signal, psRef: { pixelSize: number }, ): Signal { return useMemo( () => computed(() => { + const global = globalMatrix.value const rect = parentClippingRect?.value - if (rect == null) { + if (rect == null || global == null) { return false } const [width, height] = size.value for (let i = 0; i < 4; i++) { const [mx, my] = multiplier[i] - helperPoints[i] - .set(mx * psRef.pixelSize * width, my * psRef.pixelSize * height, 0) - .applyMatrix4(globalMatrix.value) + helperPoints[i].set(mx * psRef.pixelSize * width, my * psRef.pixelSize * height, 0).applyMatrix4(global) } const { planes } = rect @@ -147,7 +146,7 @@ export function useIsClipped( } export function useClippingRect( - globalMatrix: Signal, + globalMatrix: Signal, size: Signal, borderInset: Signal, overflow: Signal, @@ -157,13 +156,14 @@ export function useClippingRect( return useMemo( () => computed(() => { - if (overflow.value === OVERFLOW_VISIBLE) { + const global = globalMatrix.value + if (global == null || overflow.value === OVERFLOW_VISIBLE) { return parentClippingRect?.value } const [width, height] = size.value const [top, right, bottom, left] = borderInset.value const rect = new ClippingRect( - globalMatrix.value, + global, ((right - left) * psRef.pixelSize) / 2, ((top - bottom) * psRef.pixelSize) / 2, (width - left - right) * psRef.pixelSize, diff --git a/packages/uikit/src/components/content.tsx b/packages/uikit/src/components/content.tsx index bae7e9e8..d03c5a8a 100644 --- a/packages/uikit/src/components/content.tsx +++ b/packages/uikit/src/components/content.tsx @@ -223,8 +223,12 @@ function useNormalizedContent( } box3Helper.getCenter(center) return effect(() => { + const get = getPropertySignal.value + if (get == null) { + return + } group.position.copy(center).negate() - group.position.z -= alignmentZMap[getPropertySignal.value('depthAlign') ?? 'back'] * size.z + group.position.z -= alignmentZMap[get('depthAlign') ?? 'back'] * size.z group.position.divide(size) group.updateMatrix() }) diff --git a/packages/uikit/src/components/icon.tsx b/packages/uikit/src/components/icon.tsx index 78820cb7..e6530282 100644 --- a/packages/uikit/src/components/icon.tsx +++ b/packages/uikit/src/components/icon.tsx @@ -117,8 +117,12 @@ export const SvgIconFromText = forwardRef< const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) useSignalEffect(() => { - const colorRepresentation = getPropertySignal.value('color') - const opacity = getPropertySignal.value('opacity') + const get = getPropertySignal.value + if (get == null) { + return + } + const colorRepresentation = get('color') + const opacity = get('opacity') let color: Color | undefined if (Array.isArray(colorRepresentation)) { color = colorHelper.setRGB(...colorRepresentation) diff --git a/packages/uikit/src/components/image.tsx b/packages/uikit/src/components/image.tsx index ee15c83b..dbc1cfd4 100644 --- a/packages/uikit/src/components/image.tsx +++ b/packages/uikit/src/components/image.tsx @@ -188,14 +188,15 @@ function useTextureFit( ): void { const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) useSignalEffect(() => { + const get = getPropertySignal.value const texture = textureSignal.value - if (texture == null) { + if (texture == null || get == null) { return } - const fitValue = getPropertySignal.value('fit') ?? FIT_DEFAULT + const fit = get('fit') ?? FIT_DEFAULT texture.matrix.identity() - if (fitValue === 'fill' || texture == null) { + if (fit === 'fill' || texture == null) { transformInsideBorder(borderInset, size, texture) return } diff --git a/packages/uikit/src/components/root.tsx b/packages/uikit/src/components/root.tsx index 8f1dcd36..30823693 100644 --- a/packages/uikit/src/components/root.tsx +++ b/packages/uikit/src/components/root.tsx @@ -237,7 +237,7 @@ function useDivide( const matrixHelper = new Matrix4() function useRootMatrix( - matrix: Signal, + matrix: Signal, size: Signal, pixelSize: number, { @@ -253,7 +253,7 @@ function useRootMatrix( computed(() => { const [width, height] = size.value return matrix.value - .clone() + ?.clone() .premultiply( matrixHelper.makeTranslation( alignmentXMap[anchorX] * width * pixelSize, diff --git a/packages/uikit/src/components/svg.tsx b/packages/uikit/src/components/svg.tsx index 7bdbc008..fc5b00dd 100644 --- a/packages/uikit/src/components/svg.tsx +++ b/packages/uikit/src/components/svg.tsx @@ -169,8 +169,12 @@ export const Svg = forwardRef< const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) useSignalEffect(() => { - const colorRepresentation = getPropertySignal.value('color') - const opacity = getPropertySignal.value('opacity') + const get = getPropertySignal.value + if (get == null) { + return + } + const colorRepresentation = get('color') + const opacity = get('opacity') let color: Color | undefined if (Array.isArray(colorRepresentation)) { color = colorHelper.setRGB(...colorRepresentation) diff --git a/packages/uikit/src/components/utils.ts b/packages/uikit/src/components/utils.ts index 52094ee9..17d9a830 100644 --- a/packages/uikit/src/components/utils.ts +++ b/packages/uikit/src/components/utils.ts @@ -84,14 +84,22 @@ export function useViewportListeners({ onIsInViewportChange }: ViewportListeners useEffect(() => unsubscribe, [unsubscribe]) } -export function useGlobalMatrix(localMatrix: Signal): Signal { +export function useGlobalMatrix(localMatrix: Signal): Signal { const parentMatrix = useContext(MatrixContext) return useMemo( - () => computed(() => parentMatrix.value.clone().multiply(localMatrix.value)), + () => + computed(() => { + const local = localMatrix.value + const parent = parentMatrix.value + if (local == null || parent == null) { + return undefined + } + return parent.clone().multiply(local) + }), [localMatrix, parentMatrix], ) } -const MatrixContext = createContext>(null as any) +const MatrixContext = createContext>(null as any) export const MatrixProvider = MatrixContext.Provider diff --git a/packages/uikit/src/order.ts b/packages/uikit/src/order.ts index e51171b5..dceb0afb 100644 --- a/packages/uikit/src/order.ts +++ b/packages/uikit/src/order.ts @@ -105,6 +105,7 @@ export function useOrderInfo( minorIndex += minorOffset return { + instancedGroupDependencies, elementType: type, majorIndex, minorIndex, diff --git a/packages/uikit/src/panel/instanced-panel.ts b/packages/uikit/src/panel/instanced-panel.ts index cdd6f926..287e1fc0 100644 --- a/packages/uikit/src/panel/instanced-panel.ts +++ b/packages/uikit/src/panel/instanced-panel.ts @@ -81,7 +81,7 @@ export class InstancedPanel implements WithImmediateProperties, WithBatchedPrope constructor( private readonly group: InstancedPanelGroup, - private readonly matrix: Signal, + private readonly matrix: Signal, private readonly size: Signal, private readonly offset: Signal | undefined, private readonly borderInset: Signal, @@ -92,6 +92,7 @@ export class InstancedPanel implements WithImmediateProperties, WithBatchedPrope this.unsubscribeVisible = effect(() => { const get = this.getProperty.value if ( + get != null && isPanelVisible( borderInset, size, @@ -107,7 +108,9 @@ export class InstancedPanel implements WithImmediateProperties, WithBatchedPrope this.hide() }) } - getProperty: Signal<(key: K) => BatchedProperties[K]> = signal(() => undefined) + getProperty: Signal< + ((key: K) => BatchedProperties[K]) | undefined + > = signal(undefined) hasBatchedProperty(key: BatchedPropertiesKey): boolean { return batchedProperties.includes(key) @@ -147,6 +150,10 @@ export class InstancedPanel implements WithImmediateProperties, WithBatchedPrope this.active.value = true this.unsubscribeList.push( effect(() => { + const matrix = this.matrix.value + if (matrix == null) { + return + } const { instanceMatrix, pixelSize } = this.group const index = this.getIndexInBuffer() if (index == null) { @@ -159,7 +166,7 @@ export class InstancedPanel implements WithImmediateProperties, WithBatchedPrope const [x, y] = this.offset.value matrixHelper1.premultiply(matrixHelper2.makeTranslation(x * pixelSize, y * pixelSize, 0)) } - matrixHelper1.premultiply(this.matrix.value) + matrixHelper1.premultiply(matrix) matrixHelper1.toArray(instanceMatrix.array, arrayIndex) instanceMatrix.addUpdateRange(arrayIndex, 16) instanceMatrix.needsUpdate = true diff --git a/packages/uikit/src/panel/panel-material.ts b/packages/uikit/src/panel/panel-material.ts index 63783ec0..ba5ac2a1 100644 --- a/packages/uikit/src/panel/panel-material.ts +++ b/packages/uikit/src/panel/panel-material.ts @@ -101,14 +101,16 @@ export class MaterialSetter implements WithBatchedProperties, WithImmediatePrope this.size = size this.unsubscribe = effect(() => { const get = this.getProperty.value - const isVisible = isPanelVisible( - borderInset, - size, - isClipped, - get('borderOpacity'), - get('backgroundOpacity'), - get('backgroundColor'), - ) + const isVisible = + get != null && + isPanelVisible( + borderInset, + size, + isClipped, + get('borderOpacity'), + get('backgroundOpacity'), + get('backgroundColor'), + ) this.active.value = isVisible if (!isVisible) { this.deactivate() @@ -117,7 +119,6 @@ export class MaterialSetter implements WithBatchedProperties, WithImmediatePrope this.activate(size, borderInset) }) } - addMaterial(material: Material) { material.visible = this.visible this.materials.push(material) @@ -127,7 +128,8 @@ export class MaterialSetter implements WithBatchedProperties, WithImmediatePrope return batchedProperties.includes(key) } - getProperty: Signal<(key: K) => BatchedProperties[K]> = signal(() => undefined) + getProperty: Signal(key: K) => BatchedProperties[K]) | undefined> = + signal(undefined) hasImmediateProperty(key: string): boolean { return key in panelMaterialSetters diff --git a/packages/uikit/src/panel/react.tsx b/packages/uikit/src/panel/react.tsx index f3f71183..13cab8f2 100644 --- a/packages/uikit/src/panel/react.tsx +++ b/packages/uikit/src/panel/react.tsx @@ -29,7 +29,7 @@ export function InteractionGroup({ handlers: EventHandlers hoverHandlers: HoverEventHandlers | undefined activeHandlers: ActiveEventHandlers | undefined - matrix: Signal + matrix: Signal children?: ReactNode groupRef: RefObject }) { @@ -38,7 +38,7 @@ export function InteractionGroup({ if (group == null) { return } - return effect(() => group.matrix.copy(matrix.value)) + return effect(() => matrix.value != null && group.matrix.copy(matrix.value)) }, [groupRef, matrix]) return ( , + matrix: Signal, size: Signal, offset: Signal | undefined, borderInset: Signal, diff --git a/packages/uikit/src/properties/batched.ts b/packages/uikit/src/properties/batched.ts index b09b23da..3b77f821 100644 --- a/packages/uikit/src/properties/batched.ts +++ b/packages/uikit/src/properties/batched.ts @@ -11,7 +11,7 @@ import { Signal } from '@preact/signals-core' export type WithBatchedProperties

= {}> = { hasBatchedProperty(key: keyof P): boolean - getProperty: Signal<(key: K) => P[K]> + getProperty: Signal<((key: K) => P[K]) | undefined> } export function useBatchedProperties( @@ -34,7 +34,7 @@ export function useBatchedProperties( } changed ||= prevPropertiesLength != propertiesLength prevProperties = properties - if (!changed) { + if (!changed && object.getProperty.peek() != null) { return } object.getProperty.value = (key) => readReactiveProperty(properties[key]) as never diff --git a/packages/uikit/src/properties/utils.ts b/packages/uikit/src/properties/utils.ts index b4db3707..23a58b28 100644 --- a/packages/uikit/src/properties/utils.ts +++ b/packages/uikit/src/properties/utils.ts @@ -150,7 +150,10 @@ export function useGetBatchedProperties>( keys: ReadonlyArray, propertyTransformation?: PropertyTransformation, ) { - const getPropertySignal: WithBatchedProperties>['getProperty'] = useMemo(() => signal(() => undefined), []) + const getPropertySignal: WithBatchedProperties>['getProperty'] | undefined = useMemo( + () => signal(undefined), + [], + ) const object = useMemo>>( () => ({ hasBatchedProperty: (key) => keys.includes(key), diff --git a/packages/uikit/src/scroll.tsx b/packages/uikit/src/scroll.tsx index 0fc9f1cd..a0f3b8ee 100644 --- a/packages/uikit/src/scroll.tsx +++ b/packages/uikit/src/scroll.tsx @@ -5,7 +5,13 @@ import { Group, Matrix4, MeshBasicMaterial, Vector2, Vector2Tuple, Vector3, Vect import { FlexNode, Inset } from './flex/node.js' import { Color as ColorRepresentation, useFrame } from '@react-three/fiber' import { useSignalEffect } from './utils.js' -import { GetInstancedPanelGroup, MaterialClass, PanelGroupDependencies, useInstancedPanel } from './panel/react.js' +import { + GetInstancedPanelGroup, + MaterialClass, + PanelGroupDependencies, + useInstancedPanel, + usePanelGroupDependencies, +} from './panel/react.js' import { ClippingRect } from './clipping.js' import { clamp } from 'three/src/math/MathUtils.js' import { PanelProperties } from './panel/instanced-panel.js' @@ -32,16 +38,18 @@ export function useScrollPosition() { export function useGlobalScrollMatrix( scrollPosition: Signal, node: FlexNode, - globalMatrix: Signal, + globalMatrix: Signal, ) { return useMemo( () => computed(() => { + const global = globalMatrix.value + if (global == null) { + return undefined + } const [scrollX, scrollY] = scrollPosition.value const { pixelSize } = node - return new Matrix4() - .makeTranslation(-scrollX * pixelSize, scrollY * pixelSize, 0) - .premultiply(globalMatrix.value) + return new Matrix4().makeTranslation(-scrollX * pixelSize, scrollY * pixelSize, 0).premultiply(global) }), [scrollPosition, node, globalMatrix], ) @@ -330,14 +338,15 @@ export function useScrollbars( collection: ManagerCollection, scrollPosition: Signal, node: FlexNode, - globalMatrix: Signal, + globalMatrix: Signal, isClipped: Signal | undefined, materialClass: MaterialClass | undefined, parentClippingRect: Signal | undefined, orderInfo: OrderInfo, providedGetGroup?: GetInstancedPanelGroup, ): void { - const scrollbarOrderInfo = useOrderInfo(ElementType.Panel, undefined, materialClass, orderInfo) + const groupDeps = usePanelGroupDependencies(materialClass, { castShadow: false, receiveShadow: false }) + const scrollbarOrderInfo = useOrderInfo(ElementType.Panel, undefined, groupDeps, orderInfo) const getScrollbarWidthSignal = useGetBatchedProperties<{ scrollbarWidth?: number }>(collection, propertyKeys) const getBorderSignal = useGetBatchedProperties<{ @@ -351,10 +360,10 @@ export function useScrollbars( computed(() => { const get = getBorderSignal.value return [ - get('scrollbarBorderTop') ?? 0, - get('scrollbarBorderRight') ?? 0, - get('scrollbarBorderBottom') ?? 0, - get('scrollbarBorderLeft') ?? 0, + get?.('scrollbarBorderTop') ?? 0, + get?.('scrollbarBorderRight') ?? 0, + get?.('scrollbarBorderBottom') ?? 0, + get?.('scrollbarBorderLeft') ?? 0, ] }), [getBorderSignal], @@ -412,29 +421,33 @@ function useScrollbar( mainIndex: number, scrollPosition: Signal, node: FlexNode, - globalMatrix: Signal, + globalMatrix: Signal, isClipped: Signal | undefined, materialClass: MaterialClass | undefined, parentClippingRect: Signal | undefined, orderInfo: OrderInfo, providedGetGroup: GetInstancedPanelGroup | undefined, - getScrollbarWidthSignal: Signal<(key: 'scrollbarWidth') => number | undefined>, + getScrollbarWidthSignal: Signal number | undefined)>, borderSize: ReadonlySignal, ) { const [scrollbarPosition, scrollbarSize] = useMemo(() => { - const scrollbarTransformation = computed(() => - computeScrollbarTransformation( + const scrollbarTransformation = computed(() => { + const get = getScrollbarWidthSignal.value + if (get == null) { + return undefined + } + return computeScrollbarTransformation( mainIndex, - getScrollbarWidthSignal.value('scrollbarWidth') ?? 10, + get('scrollbarWidth') ?? 10, node.size.value, node.maxScrollPosition.value, node.borderInset.value, scrollPosition.value, - ), - ) + ) + }) return [ - computed(() => scrollbarTransformation.value.slice(0, 2) as Vector2Tuple), - computed(() => scrollbarTransformation.value.slice(2, 4) as Vector2Tuple), + computed(() => (scrollbarTransformation.value?.slice(0, 2) ?? [0, 0]) as Vector2Tuple), + computed(() => (scrollbarTransformation.value?.slice(2, 4) ?? [0, 0]) as Vector2Tuple), ] }, [mainIndex, node, scrollPosition, getScrollbarWidthSignal]) diff --git a/packages/uikit/src/text/react.tsx b/packages/uikit/src/text/react.tsx index 3ea4676e..4ea60660 100644 --- a/packages/uikit/src/text/react.tsx +++ b/packages/uikit/src/text/react.tsx @@ -115,7 +115,7 @@ export type InstancedTextProperties = TextAlignProperties & export function useInstancedText( collection: ManagerCollection, text: string | ReadonlySignal | Array>, - matrix: Signal, + matrix: Signal, node: FlexNode, isHidden: Signal | undefined, parentClippingRect: Signal | undefined, @@ -183,11 +183,15 @@ export function useFont(collection: ManagerCollection) { const getProperties = useGetBatchedProperties(collection, fontKeys) const renderer = useThree(({ gl }) => gl) useSignalEffect(() => { - let fontWeight = getProperties.value('fontWeight') ?? 'normal' + const get = getProperties.value + if (get == null) { + return + } + let fontWeight = get('fontWeight') ?? 'normal' if (typeof fontWeight === 'string') { fontWeight = fontWeightNames[fontWeight] } - let fontFamily = getProperties.value('fontFamily') + let fontFamily = get('fontFamily') if (fontFamily == null) { fontFamily = Object.keys(fontFamilies)[0] } @@ -240,17 +244,18 @@ export function useMeasureFunc( () => computed(() => { const font = fontSignal.value - if (font == null) { + const get = getGlyphProperties.value + if (font == null || get == null) { return undefined } const textSignalValue = textSignal.value const text = Array.isArray(textSignalValue) ? textSignalValue.map((t) => readReactive(t)).join('') : readReactive(textSignalValue) - const letterSpacing = getGlyphProperties.value('letterSpacing') ?? 0 - const lineHeight = getGlyphProperties.value('lineHeight') ?? 1.2 - const fontSize = getGlyphProperties.value('fontSize') ?? 16 - const wordBreak = getGlyphProperties.value('wordBreak') ?? 'break-word' + const letterSpacing = get('letterSpacing') ?? 0 + const lineHeight = get('lineHeight') ?? 1.2 + const fontSize = get('fontSize') ?? 16 + const wordBreak = get('wordBreak') ?? 'break-word' return (width, widthMode) => { const availableWidth = widthMode === MEASURE_MODE_UNDEFINED ? undefined : width diff --git a/packages/uikit/src/text/render/instanced-glyph.ts b/packages/uikit/src/text/render/instanced-glyph.ts index 3566316c..a95fe04a 100644 --- a/packages/uikit/src/text/render/instanced-glyph.ts +++ b/packages/uikit/src/text/render/instanced-glyph.ts @@ -24,7 +24,7 @@ export class InstancedGlyph { constructor( private readonly group: InstancedGlyphGroup, //modifiable using update... - private baseMatrix: Matrix4, + private baseMatrix: Matrix4 | undefined, private color: ColorRepresentation, private opacity: number, private clippingRect: ClippingRect | undefined, @@ -96,7 +96,7 @@ export class InstancedGlyph { } updateGlyphAndTransformation(glyphInfo: GlyphInfo, x: number, y: number, fontSize: number): void { - if (this.glyphInfo === this.glyphInfo && this.x === x && this.y === y && this.fontSize === fontSize) { + if (this.glyphInfo === glyphInfo && this.x === x && this.y === y && this.fontSize === fontSize) { return } if (this.glyphInfo != glyphInfo) { @@ -129,7 +129,7 @@ export class InstancedGlyph { } private writeUpdatedMatrix(): void { - if (this.index == null || this.glyphInfo == null) { + if (this.index == null || this.glyphInfo == null || this.baseMatrix == null) { return } const offset = this.index * 16 diff --git a/packages/uikit/src/text/render/instanced-text.ts b/packages/uikit/src/text/render/instanced-text.ts index 2646ddfc..4129f0c5 100644 --- a/packages/uikit/src/text/render/instanced-text.ts +++ b/packages/uikit/src/text/render/instanced-text.ts @@ -36,18 +36,20 @@ export class InstancedText { constructor( private group: InstancedGlyphGroup, - private getAlignmentProperties: Signal<(key: K) => TextAlignProperties[K]>, + private getAlignmentProperties: Signal< + ((key: K) => TextAlignProperties[K]) | undefined + >, private getAppearanceProperties: Signal< - (key: K) => TextAppearanceProperties[K] + ((key: K) => TextAppearanceProperties[K]) | undefined >, private layout: Signal, - private matrix: Signal, + private matrix: Signal, isHidden: Signal | undefined, private parentClippingRect: Signal | undefined, ) { this.unsubscribe = effect(() => { - const opacity = getAppearanceProperties.value('opacity') ?? 1 - if (isHidden?.value === true || opacity < 0.01) { + const get = getAppearanceProperties.value + if (get == null || isHidden?.value === true || (get('opacity') ?? 1) < 0.01) { this.hide() return } @@ -63,6 +65,9 @@ export class InstancedText { this.unsubscribeList.push( effect(() => { const matrix = this.matrix.value + if (matrix == null) { + return + } traverseGlyphs(this.glyphLines, (glyph) => glyph.updateBaseMatrix(matrix)) }), effect(() => { @@ -70,16 +75,25 @@ export class InstancedText { traverseGlyphs(this.glyphLines, (glyph) => glyph.updateClippingRect(clippingRect)) }), effect(() => { - const color = (this.color = this.getAppearanceProperties.value('color') ?? 0xffffff) + const get = this.getAppearanceProperties.value + if (get == null) { + return + } + const color = (this.color = get('color') ?? 0xffffff) traverseGlyphs(this.glyphLines, (glyph) => glyph.updateColor(color)) }), effect(() => { - const opacity = (this.opacity = this.getAppearanceProperties.value('opacity') ?? 1) + const get = this.getAppearanceProperties.value + if (get == null) { + return + } + const opacity = (this.opacity = get('opacity') ?? 1) traverseGlyphs(this.glyphLines, (glyph) => glyph.updateOpacity(opacity)) }), effect(() => { const layout = this.layout.value - if (layout == null) { + const get = this.getAlignmentProperties.value + if (layout == null || get == null) { return } const { @@ -95,7 +109,6 @@ export class InstancedText { let y = -availableHeight / 2 - const get = this.getAlignmentProperties.value switch (get('verticalAlign')) { case 'center': y += (availableHeight - getGlyphLayoutHeight(layout.lines.length, layout)) / 2 diff --git a/packages/uikit/src/transform.ts b/packages/uikit/src/transform.ts index 82ebf200..285dda07 100644 --- a/packages/uikit/src/transform.ts +++ b/packages/uikit/src/transform.ts @@ -49,7 +49,7 @@ function toQuaternion([x, y, z]: Vector3Tuple): Quaternion { return quaternionHelper.setFromEuler(eulerHelper.set(x * toRad, y * toRad, z * toRad)) } -export function useTransformMatrix(collection: ManagerCollection, node: FlexNode): Signal { +export function useTransformMatrix(collection: ManagerCollection, node: FlexNode): Signal { //B * O^-1 * T * O //B = bound transformation matrix //O = matrix to transform the origin for matrix T @@ -62,12 +62,14 @@ export function useTransformMatrix(collection: ManagerCollection, node: FlexNode return useMemo( () => computed(() => { + const get = getPropertySignal.value + if (get == null) { + return undefined + } const { pixelSize, relativeCenter } = node const [x, y] = relativeCenter.value const result = new Matrix4().makeTranslation(x * pixelSize, y * pixelSize, 0) - const get = getPropertySignal.value - const tOriginX = get('transformOriginX') ?? 'center' const tOriginY = get('transformOriginY') ?? 'center' let originCenter = true