From 1f3bff3a351177894f39a9d8171e7f59e20d45f2 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Fri, 8 Mar 2024 09:24:24 +0100 Subject: [PATCH 01/20] humble beginnings --- packages/react/src/clipping.ts | 9 + packages/react/src/default.tsx | 21 ++ packages/uikit/src/active.ts | 19 +- packages/uikit/src/clipping.ts | 114 ++++----- packages/uikit/src/components/container.ts | 108 ++++++++ packages/uikit/src/components/utils.ts | 115 +-------- packages/uikit/src/dark.ts | 14 +- packages/uikit/src/flex/.gitignore | 3 +- packages/uikit/src/flex/index.ts | 2 - packages/uikit/src/flex/node.ts | 79 ++++-- packages/uikit/src/flex/utils.ts | 20 -- packages/uikit/src/hover.ts | 24 +- packages/uikit/src/order.ts | 65 +++-- packages/uikit/src/panel/instanced-panel.ts | 128 +++++----- packages/uikit/src/panel/panel-material.ts | 194 ++++++++------- packages/uikit/src/panel/react.tsx | 117 ++------- packages/uikit/src/properties/alias.ts | 72 +++--- packages/uikit/src/properties/batched.ts | 67 ++--- packages/uikit/src/properties/default.tsx | 51 ++-- packages/uikit/src/properties/immediate.ts | 145 +++++------ packages/uikit/src/properties/merged.ts | 156 ++++++++++++ packages/uikit/src/properties/utils.ts | 167 ------------- .../src/{components => react}/container.tsx | 26 +- .../src/{components => react}/content.tsx | 18 +- .../src/{components => react}/custom.tsx | 18 +- .../src/{components => react}/fullscreen.tsx | 0 .../uikit/src/{components => react}/icon.tsx | 18 +- .../uikit/src/{components => react}/image.tsx | 22 +- .../uikit/src/{components => react}/index.ts | 0 .../uikit/src/{components => react}/input.tsx | 6 +- .../src/{components => react}/portal.tsx | 0 packages/uikit/src/{flex => react}/react.ts | 22 +- .../uikit/src/{components => react}/root.tsx | 26 +- .../src/{components => react}/suspending.tsx | 0 .../uikit/src/{components => react}/svg.tsx | 18 +- .../uikit/src/{components => react}/text.tsx | 18 +- packages/uikit/src/react/utils.ts | 97 ++++++++ packages/uikit/src/responsive.ts | 28 +-- packages/uikit/src/scroll.tsx | 234 ++++++++---------- packages/uikit/src/transform.ts | 91 +++---- packages/uikit/src/utils.tsx | 31 ++- 41 files changed, 1165 insertions(+), 1198 deletions(-) create mode 100644 packages/react/src/clipping.ts create mode 100644 packages/react/src/default.tsx create mode 100644 packages/uikit/src/components/container.ts delete mode 100644 packages/uikit/src/flex/utils.ts create mode 100644 packages/uikit/src/properties/merged.ts delete mode 100644 packages/uikit/src/properties/utils.ts rename packages/uikit/src/{components => react}/container.tsx (85%) rename packages/uikit/src/{components => react}/content.tsx (93%) rename packages/uikit/src/{components => react}/custom.tsx (88%) rename packages/uikit/src/{components => react}/fullscreen.tsx (100%) rename packages/uikit/src/{components => react}/icon.tsx (92%) rename packages/uikit/src/{components => react}/image.tsx (92%) rename packages/uikit/src/{components => react}/index.ts (100%) rename packages/uikit/src/{components => react}/input.tsx (97%) rename packages/uikit/src/{components => react}/portal.tsx (100%) rename packages/uikit/src/{flex => react}/react.ts (57%) rename packages/uikit/src/{components => react}/root.tsx (91%) rename packages/uikit/src/{components => react}/suspending.tsx (100%) rename packages/uikit/src/{components => react}/svg.tsx (93%) rename packages/uikit/src/{components => react}/text.tsx (87%) create mode 100644 packages/uikit/src/react/utils.ts diff --git a/packages/react/src/clipping.ts b/packages/react/src/clipping.ts new file mode 100644 index 00000000..9522d5da --- /dev/null +++ b/packages/react/src/clipping.ts @@ -0,0 +1,9 @@ +import { createContext, useContext } from "react" + +const ClippingRectContext = createContext>(null as any) + +export const ClippingRectProvider = ClippingRectContext.Provider + +export function useParentClippingRect(): Signal | undefined { + return useContext(ClippingRectContext) +} diff --git a/packages/react/src/default.tsx b/packages/react/src/default.tsx new file mode 100644 index 00000000..b21afe28 --- /dev/null +++ b/packages/react/src/default.tsx @@ -0,0 +1,21 @@ +const DefaultPropertiesContext = createContext(undefined) + +export function useDefaultProperties(): AllOptionalProperties | undefined { + return useContext(DefaultPropertiesContext) +} + +export function DefaultProperties(properties: { children?: ReactNode } & AllOptionalProperties) { + const existingDefaultProperties = useContext(DefaultPropertiesContext) + const result: any = { ...existingDefaultProperties } + for (const key in properties) { + if (key === 'children') { + continue + } + const value = properties[key as keyof AllOptionalProperties] + if (value == null) { + continue + } + result[key] = value as any + } + return {properties.children} +} diff --git a/packages/uikit/src/active.ts b/packages/uikit/src/active.ts index 3a2fd301..b0cf908e 100644 --- a/packages/uikit/src/active.ts +++ b/packages/uikit/src/active.ts @@ -1,9 +1,9 @@ import { signal } from '@preact/signals-core' import type { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { useMemo } from 'react' -import { ManagerCollection, Properties } from './properties/utils.js' -import { WithClasses, useTraverseProperties } from './properties/default.js' +import { Properties } from './properties/utils.js' +import { AllOptionalProperties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' +import { MergedProperties } from './properties/merged.js' export type WithActive = T & { active?: T @@ -12,21 +12,22 @@ export type WithActive = T & { export type ActiveEventHandlers = Pick -export function useApplyActiveProperties( - collection: ManagerCollection, +export function applyActiveProperties( + merged: MergedProperties, + defaultProperties: AllOptionalProperties | undefined, properties: WithClasses> & EventHandlers, ): ActiveEventHandlers | undefined { - const activeSignal = useMemo(() => signal>([]), []) + const activeSignal = signal>([]) // eslint-disable-next-line react-hooks/exhaustive-deps - const translate = useMemo(() => createConditionalPropertyTranslator(() => activeSignal.value.length > 0), []) + const translate = createConditionalPropertyTranslator(() => activeSignal.value.length > 0) let activePropertiesExist = false - useTraverseProperties(properties, (p) => { + traverseProperties(defaultProperties, properties, (p) => { if (p.active == null) { return } activePropertiesExist = true - translate(collection, p.active) + translate(merged, p.active) }) if (!activePropertiesExist && properties.onActiveChange == null) { diff --git a/packages/uikit/src/clipping.ts b/packages/uikit/src/clipping.ts index 818cd74e..d3856101 100644 --- a/packages/uikit/src/clipping.ts +++ b/packages/uikit/src/clipping.ts @@ -87,14 +87,6 @@ export class ClippingRect { } } -const ClippingRectContext = createContext>(null as any) - -export const ClippingRectProvider = ClippingRectContext.Provider - -export function useParentClippingRect(): Signal | undefined { - return useContext(ClippingRectContext) -} - const helperPoints = [new Vector3(), new Vector3(), new Vector3(), new Vector3()] const multiplier = [ [-0.5, -0.5], @@ -103,49 +95,45 @@ const multiplier = [ [-0.5, 0.5], ] -export function useIsClipped( +export function computeIsClipped( parentClippingRect: Signal | undefined, globalMatrix: Signal, size: Signal, psRef: { pixelSize: number }, ): Signal { - return useMemo( - () => - computed(() => { - const global = globalMatrix.value - const rect = parentClippingRect?.value - 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(global) - } + return computed(() => { + const global = globalMatrix.value + const rect = parentClippingRect?.value + 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(global) + } - const { planes } = rect - let allOutside: boolean - for (let planeIndex = 0; planeIndex < 4; planeIndex++) { - const clippingPlane = planes[planeIndex] - allOutside = true - for (let pointIndex = 0; pointIndex < 4; pointIndex++) { - const point = helperPoints[pointIndex] - if (clippingPlane.distanceToPoint(point) >= 0) { - //inside - allOutside = false - } - } - if (allOutside) { - return true - } + const { planes } = rect + let allOutside: boolean + for (let planeIndex = 0; planeIndex < 4; planeIndex++) { + const clippingPlane = planes[planeIndex] + allOutside = true + for (let pointIndex = 0; pointIndex < 4; pointIndex++) { + const point = helperPoints[pointIndex] + if (clippingPlane.distanceToPoint(point) >= 0) { + //inside + allOutside = false } - return false - }), - [globalMatrix, parentClippingRect, psRef, size], - ) + } + if (allOutside) { + return true + } + } + return false + }) } -export function useClippingRect( +export function computeClippingRect( globalMatrix: Signal, size: Signal, borderInset: Signal, @@ -153,29 +141,25 @@ export function useClippingRect( psRef: { pixelSize: number }, parentClippingRect: Signal | undefined, ): Signal { - return useMemo( - () => - computed(() => { - 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( - global, - ((right - left) * psRef.pixelSize) / 2, - ((top - bottom) * psRef.pixelSize) / 2, - (width - left - right) * psRef.pixelSize, - (height - top - bottom) * psRef.pixelSize, - ) - if (parentClippingRect?.value != null) { - rect.min(parentClippingRect.value) - } - return rect - }), - [globalMatrix, size, borderInset, psRef, overflow, parentClippingRect], - ) + return computed(() => { + 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( + global, + ((right - left) * psRef.pixelSize) / 2, + ((top - bottom) * psRef.pixelSize) / 2, + (width - left - right) * psRef.pixelSize, + (height - top - bottom) * psRef.pixelSize, + ) + if (parentClippingRect?.value != null) { + rect.min(parentClippingRect.value) + } + return rect + }) } export const NoClippingPlane = new Plane(new Vector3(-1, 0, 0), Number.MAX_SAFE_INTEGER) diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts new file mode 100644 index 00000000..8a680990 --- /dev/null +++ b/packages/uikit/src/components/container.ts @@ -0,0 +1,108 @@ +import { RefObject } from 'react' +import { WithReactive } from '../properties/utils.js' +import { PanelGroupDependencies, createInteractionPanel } from '../panel/react.js' +import { FlexNode, YogaProperties } from '../flex/node.js' +import { applyHoverProperties } from '../hover.js' +import { ClippingRect, computeIsClipped, computeClippingRect } from '../clipping.js' +import { ScrollbarProperties, computeGlobalScrollMatrix, createScrollPosition, createScrollbars } from '../scroll.js' +import { WithAllAliases, panelAliasPropertyTransformation } from '../properties/alias.js' +import { InstancedPanel, PanelProperties } from '../panel/instanced-panel.js' +import { TransformProperties, computeTransformMatrix } from '../transform.js' +import { AllOptionalProperties, WithClasses } from '../properties/default.js' +import { applyResponsiveProperties } from '../responsive.js' +import { Group, Matrix4, Vector2Tuple } from 'three' +import { ElementType, computeOrderInfo } from '../order.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' +import { Signal } from '@preact/signals-core' +import { computeGlobalMatrix } from './utils.js' +import { WithConditionals } from '../react/utils.js' +import { Subscriptions } from '../utils.js' + +export type ContainerProperties = WithConditionals< + WithClasses< + WithAllAliases & ScrollbarProperties> + > +> + +export function createContainer( + parentNode: FlexNode, + parentClippingRect: Signal | undefined, + parentMatrix: Signal, + groupRef: RefObject, + defaultProperties: AllOptionalProperties | undefined, + rootSize: Signal, + rootGroupRef: RefObject, +): () => void { + const subscriptions: Subscriptions = [] + const node = parentNode.createChild(propertiesSignal, groupRef, subscriptions) + parentNode.addChild(node) + + const transformMatrix = computeTransformMatrix(node, propertiesSignal) + const globalMatrix = computeGlobalMatrix(parentMatrix, transformMatrix) + const isClipped = computeIsClipped(parentClippingRect, globalMatrix, node.size, node) + const groupDeps: PanelGroupDependencies = { + materialClass: properties.panelMaterialClass, + castShadow: properties.castShadow, + receiveShadow: properties.receiveShadow, + } + + const orderInfo = computeOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) + + new InstancedPanel( + propertiesSignal, + getInstancedPanelGroup, + orderInfo, + panelGroupDependencies, + globalMatrix, + node.size, + undefined, + node.borderInset, + parentClippingRect, + isClipped, + subscriptions, + panelAliasPropertyTransformation, + ) + + const scrollPosition = createScrollPosition() + const globalScrollMatrix = computeGlobalScrollMatrix(scrollPosition, node, globalMatrix) + createScrollbars( + collection, + scrollPosition, + node, + globalMatrix, + isClipped, + properties.scrollbarPanelMaterialClass, + parentClippingRect, + orderInfo, + ) + + //apply all properties + addToMerged(collection, defaultProperties, properties) + applyPreferredColorSchemeProperties(collection, defaultProperties, properties) + applyResponsiveProperties(collection, defaultProperties, properties, rootSize) + const hoverHandlers = applyHoverProperties(collection, defaultProperties, properties) + const activeHandlers = applyActiveProperties(collection, defaultProperties, properties) + finalizeCollection(collection) + + useLayoutListeners(properties, node.size) + useViewportListeners(properties, isClipped) + + const clippingRect = computeClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + node, + parentClippingRect, + ) + + const interactionPanel = createInteractionPanel(node.size, node, orderInfo, rootGroupRef) + + useComponentInternals(ref, node, interactionPanel, scrollPosition) + + return () => { + parentNode.removeChild(node) + node.destroy() + } +} diff --git a/packages/uikit/src/components/utils.ts b/packages/uikit/src/components/utils.ts index 17d9a830..54c22a96 100644 --- a/packages/uikit/src/components/utils.ts +++ b/packages/uikit/src/components/utils.ts @@ -1,105 +1,16 @@ -import { ReadonlySignal, Signal, computed, effect } from '@preact/signals-core' -import { useMemo, useEffect, createContext, useContext, useImperativeHandle, ForwardedRef, RefObject } from 'react' -import { Matrix4, Mesh, Vector2Tuple } from 'three' -import { FlexNode, Inset } from '../flex/node.js' -import { WithHover } from '../hover.js' -import { WithResponsive } from '../responsive.js' -import { WithPreferredColorScheme } from '../dark.js' -import { WithActive } from '../active.js' - -export type WithConditionals = WithHover & WithResponsive & WithPreferredColorScheme & WithActive - -export type ComponentInternals = { - pixelSize: number - size: ReadonlySignal - center: ReadonlySignal - borderInset: ReadonlySignal - paddingInset: ReadonlySignal - scrollPosition?: Signal - interactionPanel: Mesh -} - -export function useComponentInternals( - ref: ForwardedRef, - node: FlexNode, - interactionPanel: Mesh | RefObject, - scrollPosition?: Signal, -): void { - useImperativeHandle( - ref, - () => ({ - borderInset: node.borderInset, - paddingInset: node.paddingInset, - pixelSize: node.pixelSize, - center: node.relativeCenter, - size: node.size, - interactionPanel: interactionPanel instanceof Mesh ? interactionPanel : interactionPanel.current!, - scrollPosition, - }), - [interactionPanel, node, scrollPosition], - ) -} - -export type LayoutListeners = { - onSizeChange?: (width: number, height: number) => void -} - -export function useLayoutListeners({ onSizeChange }: LayoutListeners, size: Signal): void { - const unsubscribe = useMemo(() => { - if (onSizeChange == null) { - return undefined - } - let first = true - return effect(() => { - const s = size.value - if (first) { - first = false - return - } - onSizeChange(...s) - }) - }, [onSizeChange, size]) - useEffect(() => unsubscribe, [unsubscribe]) -} - -export type ViewportListeners = { - onIsInViewportChange?: (isInViewport: boolean) => void -} - -export function useViewportListeners({ onIsInViewportChange }: ViewportListeners, isClipped: Signal) { - const unsubscribe = useMemo(() => { - if (onIsInViewportChange == null) { +import { Signal, computed } from '@preact/signals-core' +import { Matrix4 } from 'three' + +export function computeGlobalMatrix( + parentMatrix: Signal, + localMatrix: Signal, +): Signal { + return computed(() => { + const local = localMatrix.value + const parent = parentMatrix.value + if (local == null || parent == null) { return undefined } - let first = true - return effect(() => { - const isInViewport = !isClipped.value - if (first) { - first = false - return - } - onIsInViewportChange(isInViewport) - }) - }, [isClipped, onIsInViewportChange]) - useEffect(() => unsubscribe, [unsubscribe]) + return parent.clone().multiply(local) + }) } - -export function useGlobalMatrix(localMatrix: Signal): Signal { - const parentMatrix = useContext(MatrixContext) - return useMemo( - () => - 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) - -export const MatrixProvider = MatrixContext.Provider diff --git a/packages/uikit/src/dark.ts b/packages/uikit/src/dark.ts index 43105814..82ce75c5 100644 --- a/packages/uikit/src/dark.ts +++ b/packages/uikit/src/dark.ts @@ -1,9 +1,10 @@ import { ReadonlySignal, computed, signal } from '@preact/signals-core' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ManagerCollection, Properties } from './properties/utils.js' -import { WithClasses, useTraverseProperties } from './properties/default.js' +import { Properties } from './properties/utils.js' +import { AllOptionalProperties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' import { Color as ColorRepresentation } from '@react-three/fiber' +import { MergedProperties } from './properties/merged.js' export type WithPreferredColorScheme = { dark?: T } & T @@ -38,16 +39,17 @@ export function getPreferredColorScheme() { const translator = createConditionalPropertyTranslator(() => isDarkMode.value) -export function useApplyPreferredColorSchemeProperties( - collection: ManagerCollection, +export function applyPreferredColorSchemeProperties( + merged: MergedProperties, + defaultProperties: AllOptionalProperties | undefined, properties: WithClasses> & EventHandlers, ): void { - useTraverseProperties(properties, (p) => { + traverseProperties(defaultProperties, properties, (p) => { const properties = p.dark if (properties == null) { return } - translator(collection, properties) + translator(merged, properties) }) } diff --git a/packages/uikit/src/flex/.gitignore b/packages/uikit/src/flex/.gitignore index 1cec944d..b67bae81 100644 --- a/packages/uikit/src/flex/.gitignore +++ b/packages/uikit/src/flex/.gitignore @@ -1,2 +1 @@ -setter.ts -wasm.ts \ No newline at end of file +setter.ts \ No newline at end of file diff --git a/packages/uikit/src/flex/index.ts b/packages/uikit/src/flex/index.ts index 2ab4094b..2b21e123 100644 --- a/packages/uikit/src/flex/index.ts +++ b/packages/uikit/src/flex/index.ts @@ -1,4 +1,2 @@ -export * from './utils.js' export * from './setter.js' export * from './node.js' -export * from './react.js' diff --git a/packages/uikit/src/flex/node.ts b/packages/uikit/src/flex/node.ts index ebc7a394..07808477 100644 --- a/packages/uikit/src/flex/node.ts +++ b/packages/uikit/src/flex/node.ts @@ -1,11 +1,13 @@ import { Group, Object3D, Vector2Tuple } from 'three' import { Signal, batch, computed, effect, signal } from '@preact/signals-core' -import { Edge, Node, Yoga, Overflow } from 'yoga-layout/wasm-async' +import { Edge, Node, Yoga, Overflow, MeasureFunction } from 'yoga-layout/wasm-async' import { setter } from './setter.js' -import { setMeasureFunc, yogaNodeEqual } from './utils.js' -import { WithImmediateProperties } from '../properties/immediate.js' import { RefObject } from 'react' import { CameraDistanceRef } from '../order.js' +import { Subscriptions } from '../utils.js' +import { setupImmediateProperties } from '../properties/immediate.js' +import { MergedProperties } from '../properties/merged.js' +import { flexAliasPropertyTransformation } from '../properties/alias.js' export type YogaProperties = { [Key in keyof typeof setter]?: Parameters<(typeof setter)[Key]>[2] @@ -13,7 +15,14 @@ export type YogaProperties = { export type Inset = [top: number, right: number, bottom: number, left: number] -export class FlexNode implements WithImmediateProperties { +function hasImmediateProperty(key: string): boolean { + if (key === 'measureFunc') { + return true + } + return key in setter +} + +export class FlexNode { public readonly size = signal([0, 0]) public readonly relativeCenter = signal([0, 0]) public readonly borderInset = signal([0, 0, 0, 0]) @@ -30,9 +39,10 @@ export class FlexNode implements WithImmediateProperties { public requestCalculateLayout: () => void - active = signal(false) + private active = signal(false) constructor( + propertiesSignal: Signal, private groupRef: RefObject, public cameraDistance: CameraDistanceRef, public readonly yoga: Signal, @@ -40,6 +50,7 @@ export class FlexNode implements WithImmediateProperties { public readonly pixelSize: number, requestCalculateLayout: (node: FlexNode) => void, public readonly anyAncestorScrollable: Signal<[boolean, boolean]> | undefined, + subscriptions: Subscriptions, ) { this.requestCalculateLayout = () => requestCalculateLayout(this) this.unsubscribeYoga = effect(() => { @@ -51,22 +62,21 @@ export class FlexNode implements WithImmediateProperties { this.yogaNode = yoga.value.Node.create() this.active.value = true }) - } - - setProperty(key: string, value: unknown): void { - if (key === 'measureFunc') { - setMeasureFunc(this.yogaNode!, this.precision, value as any) - } else { - setter[key as keyof typeof setter](this.yogaNode!, this.precision, value as any) - } - this.requestCalculateLayout() - } - - hasImmediateProperty(key: string): boolean { - if (key === 'measureFunc') { - return true - } - return key in setter + setupImmediateProperties( + propertiesSignal, + this.active, + hasImmediateProperty, + (key: string, value: unknown) => { + if (key === 'measureFunc') { + setMeasureFunc(this.yogaNode!, this.precision, value as any) + } else { + setter[key as keyof typeof setter](this.yogaNode!, this.precision, value as any) + } + this.requestCalculateLayout() + }, + subscriptions, + flexAliasPropertyTransformation, + ) } destroy() { @@ -86,8 +96,13 @@ export class FlexNode implements WithImmediateProperties { batch(() => this.updateMeasurements(undefined, undefined)) } - createChild(groupRef: RefObject): FlexNode { + createChild( + propertiesSignal: Signal, + groupRef: RefObject, + subscriptions: Subscriptions, + ): FlexNode { const child = new FlexNode( + propertiesSignal, groupRef, this.cameraDistance, this.yoga, @@ -99,6 +114,7 @@ export class FlexNode implements WithImmediateProperties { const [x, y] = this.scrollable.value return [ancestorX || x, ancestorY || y] }), + subscriptions, ) return child } @@ -289,3 +305,22 @@ function assertNodeNotNull(val: T | undefined): T { } return val } + +function yogaNodeEqual(n1: Node, n2: Node): boolean { + return (n1 as any)['L'] === (n2 as any)['L'] +} + +function setMeasureFunc(node: Node, precision: number, func: MeasureFunction | undefined): void { + if (func == null) { + node.setMeasureFunc(null) + return + } + node.setMeasureFunc((width, wMode, height, hMode) => { + const result = func(width * precision, wMode, height * precision, hMode) + return { + width: Math.ceil(Math.ceil(result.width) / precision), + height: Math.ceil(Math.ceil(result.height) / precision), + } + }) + node.markDirty() +} diff --git a/packages/uikit/src/flex/utils.ts b/packages/uikit/src/flex/utils.ts deleted file mode 100644 index 8b6fbcaf..00000000 --- a/packages/uikit/src/flex/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Node, MeasureFunction } from 'yoga-layout/wasm-async' - -export function yogaNodeEqual(n1: Node, n2: Node): boolean { - return (n1 as any)['L'] === (n2 as any)['L'] -} - -export function setMeasureFunc(node: Node, precision: number, func: MeasureFunction | undefined): void { - if (func == null) { - node.setMeasureFunc(null) - return - } - node.setMeasureFunc((width, wMode, height, hMode) => { - const result = func(width * precision, wMode, height * precision, hMode) - return { - width: Math.ceil(Math.ceil(result.width) / precision), - height: Math.ceil(Math.ceil(result.height) / precision), - } - }) - node.markDirty() -} diff --git a/packages/uikit/src/hover.ts b/packages/uikit/src/hover.ts index 6a48b594..1649cb0b 100644 --- a/packages/uikit/src/hover.ts +++ b/packages/uikit/src/hover.ts @@ -1,10 +1,10 @@ import { signal } from '@preact/signals-core' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { useEffect, useMemo } from 'react' import { setCursorType, unsetCursorType } from './cursor.js' -import { ManagerCollection, Properties } from './properties/utils.js' -import { WithClasses, useTraverseProperties } from './properties/default.js' -import { createConditionalPropertyTranslator } from './utils.js' +import { Properties } from './properties/utils.js' +import { AllOptionalProperties, WithClasses, traverseProperties } from './properties/default.js' +import { Subscriptions, createConditionalPropertyTranslator } from './utils.js' +import { MergedProperties } from './properties/merged.js' export type WithHover = T & { cursor?: string @@ -14,26 +14,28 @@ export type WithHover = T & { export type HoverEventHandlers = Pick -export function useApplyHoverProperties( - collection: ManagerCollection, +export function applyHoverProperties( + merged: MergedProperties, + defaultProperties: AllOptionalProperties | undefined, properties: WithClasses> & EventHandlers, + subscriptions: Subscriptions, ): HoverEventHandlers | undefined { - const hoveredSignal = useMemo(() => signal>([]), []) + const hoveredSignal = signal>([]) // eslint-disable-next-line react-hooks/exhaustive-deps - const translate = useMemo(() => createConditionalPropertyTranslator(() => hoveredSignal.value.length > 0), []) + const translate = createConditionalPropertyTranslator(() => hoveredSignal.value.length > 0) let hoverPropertiesExist = false - useTraverseProperties(properties, (p) => { + traverseProperties(defaultProperties, properties, (p) => { if (p.hover == null) { return } hoverPropertiesExist = true - translate(collection, p.hover) + translate(merged, p.hover) }) //cleanup cursor effect // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => () => unsetCursorType(hoveredSignal), []) + subscriptions.push(() => unsetCursorType(hoveredSignal)) if (!hoverPropertiesExist && properties.onHoverChange == null && properties.cursor == null) { //no need to listen to hover diff --git a/packages/uikit/src/order.ts b/packages/uikit/src/order.ts index dceb0afb..5c40e4b9 100644 --- a/packages/uikit/src/order.ts +++ b/packages/uikit/src/order.ts @@ -66,7 +66,7 @@ export const OrderInfoProvider = OrderInfoContext.Provider export type ZIndexOffset = { major?: number; minor?: number } | number -export function useOrderInfo( +export function computeOrderInfo( type: ElementType, offset: ZIndexOffset | undefined, instancedGroupDependencies: Record | undefined, @@ -76,41 +76,40 @@ export function useOrderInfo( const parentOrderInfo = providedParentOrderInfo ?? (useContext(OrderInfoContext) as OrderInfo | undefined) const majorOffset = typeof offset === 'number' ? offset : offset?.major ?? 0 const minorOffset = typeof offset === 'number' ? 0 : offset?.minor ?? 0 - return useMemo(() => { - let majorIndex: number - let minorIndex: number - - if (parentOrderInfo == null) { - majorIndex = 0 - minorIndex = 0 - } else if (type > parentOrderInfo.elementType) { - majorIndex = parentOrderInfo.majorIndex - minorIndex = 0 - } else if ( - type != parentOrderInfo.elementType || - !shallowEqualRecord(instancedGroupDependencies, parentOrderInfo.instancedGroupDependencies) - ) { - majorIndex = parentOrderInfo.majorIndex + 1 - minorIndex = 0 - } else { - majorIndex = parentOrderInfo.majorIndex - minorIndex = parentOrderInfo.minorIndex + 1 - } - if (majorOffset > 0) { - majorIndex += majorOffset - minorIndex = 0 - } + let majorIndex: number + let minorIndex: number + + if (parentOrderInfo == null) { + majorIndex = 0 + minorIndex = 0 + } else if (type > parentOrderInfo.elementType) { + majorIndex = parentOrderInfo.majorIndex + minorIndex = 0 + } else if ( + type != parentOrderInfo.elementType || + !shallowEqualRecord(instancedGroupDependencies, parentOrderInfo.instancedGroupDependencies) + ) { + majorIndex = parentOrderInfo.majorIndex + 1 + minorIndex = 0 + } else { + majorIndex = parentOrderInfo.majorIndex + minorIndex = parentOrderInfo.minorIndex + 1 + } - minorIndex += minorOffset + if (majorOffset > 0) { + majorIndex += majorOffset + minorIndex = 0 + } - return { - instancedGroupDependencies, - elementType: type, - majorIndex, - minorIndex, - } - }, [majorOffset, minorOffset, parentOrderInfo, type, instancedGroupDependencies]) + minorIndex += minorOffset + + return { + instancedGroupDependencies, + elementType: type, + majorIndex, + minorIndex, + } } function shallowEqualRecord(r1: Record | undefined, r2: Record | undefined): boolean { diff --git a/packages/uikit/src/panel/instanced-panel.ts b/packages/uikit/src/panel/instanced-panel.ts index 287e1fc0..0e75b1ea 100644 --- a/packages/uikit/src/panel/instanced-panel.ts +++ b/packages/uikit/src/panel/instanced-panel.ts @@ -5,11 +5,14 @@ import { ClippingRect, defaultClippingData } from '../clipping.js' import { Inset } from '../flex/node.js' import { InstancedPanelGroup } from './instanced-panel-group.js' import { panelDefaultColor } from './panel-material.js' -import { colorToBuffer } from '../utils.js' +import { Subscriptions, colorToBuffer } from '../utils.js' import { Color as ColorRepresentation } from '@react-three/fiber' -import { WithImmediateProperties } from '../properties/immediate.js' -import { WithBatchedProperties } from '../properties/batched.js' import { isPanelVisible, setBorderRadius } from './utils.js' +import { MergedProperties } from '../properties/merged.js' +import { createGetBatchedProperties } from '../properties/batched.js' +import { setupImmediateProperties } from '../properties/immediate.js' +import { GetInstancedPanelGroup, PanelGroupDependencies } from './react.js' +import { OrderInfo } from '../order.js' export type PanelProperties = { borderTopLeftRadius?: number @@ -56,80 +59,91 @@ const instancedPanelMaterialSetters: { backgroundOpacity: (m, i, p) => writeComponent(m.instanceData, i, 15, p ?? -1), } -const batchedProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] as const -type BatchedProperties = Pick -type BatchedPropertiesKey = keyof BatchedProperties +const batchedProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] + +function hasBatchedProperty(key: string): boolean { + return batchedProperties.includes(key) +} + +function hasImmediateProperty(key: string): boolean { + return key in instancedPanelMaterialSetters +} export type InstancedPanelSetter = (typeof instancedPanelMaterialSetters)[keyof typeof instancedPanelMaterialSetters] const matrixHelper1 = new Matrix4() const matrixHelper2 = new Matrix4() -export class InstancedPanel implements WithImmediateProperties, WithBatchedProperties { +export class InstancedPanel { private indexInBucket?: number private bucket?: Bucket private unsubscribeList: Array<() => void> = [] - private unsubscribeVisible: () => void - public destroyed = false private insertedIntoGroup = false - active = signal(false) + private active = signal(false) + + private group: InstancedPanelGroup constructor( - private readonly group: InstancedPanelGroup, + propertiesSignal: Signal, + getGroup: GetInstancedPanelGroup, + private readonly orderInfo: OrderInfo, + panelGroupDependencies: PanelGroupDependencies, private readonly matrix: Signal, private readonly size: Signal, private readonly offset: Signal | undefined, private readonly borderInset: Signal, private readonly clippingRect: Signal | undefined, isHidden: Signal | undefined, - private readonly minorIndex: number, + subscriptions: Subscriptions, + renameOutput?: Record, ) { - this.unsubscribeVisible = effect(() => { - const get = this.getProperty.value - if ( - get != null && - isPanelVisible( - borderInset, - size, - isHidden, - get('borderOpacity'), - get('backgroundOpacity'), - get('backgroundColor'), + this.group = getGroup(orderInfo.minorIndex, panelGroupDependencies) + setupImmediateProperties( + propertiesSignal, + this.active, + hasImmediateProperty, + (key, value) => { + const index = this.getIndexInBuffer() + if (index == null) { + return + } + instancedPanelMaterialSetters[key as keyof typeof instancedPanelMaterialSetters]( + this.group, + index, + value as any, + this.size, ) - ) { - this.requestShow() - return - } - this.hide() - }) - } - getProperty: Signal< - ((key: K) => BatchedProperties[K]) | undefined - > = signal(undefined) - - hasBatchedProperty(key: BatchedPropertiesKey): boolean { - return batchedProperties.includes(key) - } - - hasImmediateProperty(key: string): boolean { - return key in instancedPanelMaterialSetters - } - - setProperty(key: string, value: unknown) { - const index = this.getIndexInBuffer() - if (index == null) { - return - } - instancedPanelMaterialSetters[key as keyof typeof instancedPanelMaterialSetters]( - this.group, - index, - value as any, - this.size, + }, + subscriptions, + renameOutput, + ) + const get = createGetBatchedProperties(propertiesSignal, hasBatchedProperty, renameOutput) + subscriptions.push( + effect(() => { + if ( + isPanelVisible( + borderInset, + size, + isHidden, + get('borderOpacity') as number, + get('backgroundOpacity') as number, + get('backgroundColor') as ColorRepresentation, + ) + ) { + this.requestShow() + return + } + this.hide() + }), + () => { + this.destroyed = true + this.hide() + }, ) } @@ -220,7 +234,7 @@ export class InstancedPanel implements WithImmediateProperties, WithBatchedPrope return } this.insertedIntoGroup = true - this.group.insert(this.minorIndex, this) + this.group.insert(this.orderInfo.minorIndex, this) } private hide(): void { @@ -228,7 +242,7 @@ export class InstancedPanel implements WithImmediateProperties, WithBatchedPrope return } this.active.value = false - this.group.delete(this.minorIndex, this.indexInBucket, this) + this.group.delete(this.orderInfo.minorIndex, this.indexInBucket, this) this.insertedIntoGroup = false this.bucket = undefined this.indexInBucket = undefined @@ -238,12 +252,6 @@ export class InstancedPanel implements WithImmediateProperties, WithBatchedPrope } this.unsubscribeList.length = 0 } - - destroy(): void { - this.destroyed = true - this.hide() - this.unsubscribeVisible() - } } function writeBorderRadius( diff --git a/packages/uikit/src/panel/panel-material.ts b/packages/uikit/src/panel/panel-material.ts index ba5ac2a1..b3bba94a 100644 --- a/packages/uikit/src/panel/panel-material.ts +++ b/packages/uikit/src/panel/panel-material.ts @@ -1,9 +1,12 @@ import { Color, + ColorRepresentation, FrontSide, Material, + MeshBasicMaterial, MeshDepthMaterial, MeshDistanceMaterial, + Plane, RGBADepthPacking, Vector2Tuple, WebGLProgramParametersWithUniforms, @@ -13,8 +16,35 @@ import { Constructor, isPanelVisible, setBorderRadius } from './utils.js' import { Signal, effect, signal } from '@preact/signals-core' import { Inset } from '../flex/node.js' import { PanelProperties } from './instanced-panel.js' -import { WithImmediateProperties } from '../properties/immediate.js' -import { WithBatchedProperties } from '../properties/batched.js' +import { setupImmediateProperties } from '../properties/immediate.js' +import { createGetBatchedProperties } from '../properties/batched.js' +import { MergedProperties } from '../properties/merged.js' +import { Subscriptions } from '../utils.js' + +export type MaterialClass = { new (...args: Array): Material } + +export function createPanelMaterials( + propertiesSignal: Signal, + size: Signal, + borderInset: Signal, + isClipped: Signal, + materialClass: MaterialClass | undefined, + clippingPlanes: Array, + subscriptions: Subscriptions, + renameOutput?: Record, +): readonly [Material, Material, Material] { + const data = new Float32Array(16) + const info = { data: data, type: 'normal' } as const + const material = createPanelMaterial(materialClass ?? MeshBasicMaterial, info) + const depthMaterial = new PanelDepthMaterial(info) + const distanceMaterial = new PanelDistanceMaterial(info) + material.clippingPlanes = clippingPlanes + depthMaterial.clippingPlanes = clippingPlanes + distanceMaterial.clippingPlanes = clippingPlanes + const materials = [material, depthMaterial, distanceMaterial] as const + applyPropsToMaterialData(propertiesSignal, data, size, borderInset, isClipped, materials, subscriptions, renameOutput) + return materials +} type InstanceOf = T extends { new (): infer K } ? K : never @@ -78,109 +108,91 @@ export const panelMaterialDefaultData = [ -1, //background opacity ] -const batchedProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] as const -type BatchedProperties = Pick -type BatchedPropertiesKey = keyof BatchedProperties - -export class MaterialSetter implements WithBatchedProperties, WithImmediateProperties { - //data layout: vec4 borderSize = data[0]; vec4 borderRadius = data[1]; vec3 borderColor = data[2].xyz; float borderBend = data[2].w; float borderOpacity = data[3].x; float width = data[3].y; float height = data[3].z; float backgroundOpacity = data[3].w; - public readonly data = new Float32Array(16) - - private unsubscribeList: Array<() => void> = [] - private unsubscribe: () => void - private visible = false - private materials: Array = [] - - active = signal(false) - - constructor( - private size: Signal, - borderInset: Signal, - isClipped: Signal, - ) { - this.size = size - this.unsubscribe = effect(() => { - const get = this.getProperty.value - const isVisible = - get != null && - isPanelVisible( - borderInset, - size, - isClipped, - get('borderOpacity'), - get('backgroundOpacity'), - get('backgroundColor'), - ) - this.active.value = isVisible - if (!isVisible) { - this.deactivate() - return - } - this.activate(size, borderInset) - }) - } - addMaterial(material: Material) { - material.visible = this.visible - this.materials.push(material) - } - - hasBatchedProperty(key: BatchedPropertiesKey): boolean { - return batchedProperties.includes(key) - } +const batchedProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] - getProperty: Signal(key: K) => BatchedProperties[K]) | undefined> = - signal(undefined) - - hasImmediateProperty(key: string): boolean { - return key in panelMaterialSetters - } +function hasBatchedProperty(key: string): boolean { + return batchedProperties.includes(key) +} - setProperty(key: string, value: unknown): void { - const setter = panelMaterialSetters[key as keyof typeof panelMaterialSetters] - setter(this.data, value as any, this.size) - } +function hasImmediateProperty(key: string): boolean { + return key in panelMaterialSetters +} - private activate(size: Signal, borderInset: Signal): void { - if (this.visible) { - return +export function applyPropsToMaterialData( + propertiesSignal: Signal, + data: Float32Array, + size: Signal, + borderInset: Signal, + isClipped: Signal, + materials: ReadonlyArray, + subscriptions: Subscriptions, + renameOutput?: Record, +) { + const unsubscribeList: Array<() => void> = [] + const active = signal(false) + let visible = false + setupImmediateProperties( + propertiesSignal, + active, + hasImmediateProperty, + (key, value) => { + const setter = panelMaterialSetters[key as keyof typeof panelMaterialSetters] + setter(data, value as any, size) + }, + subscriptions, + renameOutput, + ) + const materialsLength = materials.length + const syncVisible = () => { + for (let i = 0; i < materialsLength; i++) { + materials[i].visible = visible } - - this.visible = true - this.syncVisible() - - this.data.set(panelMaterialDefaultData) - this.unsubscribeList.push( - effect(() => this.data.set(size.value, 13)), - effect(() => this.data.set(borderInset.value, 0)), - ) } - - private deactivate(): void { - if (!this.visible) { + const deactivate = () => { + if (!visible) { return } - this.visible = false - this.syncVisible() + visible = false + syncVisible() - const unsubscribeListLength = this.unsubscribeList.length + const unsubscribeListLength = unsubscribeList.length for (let i = 0; i < unsubscribeListLength; i++) { - this.unsubscribeList[i]() + unsubscribeList[i]() } - this.unsubscribeList.length = 0 + unsubscribeList.length = 0 } + const get = createGetBatchedProperties(propertiesSignal, hasBatchedProperty, renameOutput) + subscriptions.push( + effect(() => { + const isVisible = isPanelVisible( + borderInset, + size, + isClipped, + get('borderOpacity') as number, + get('backgroundOpacity') as number, + get('backgroundColor') as ColorRepresentation, + ) + active.value = isVisible + if (!isVisible) { + deactivate() + return + } + if (visible) { + return + } - destroy(): void { - this.deactivate() - this.unsubscribe() - } + visible = true + syncVisible() - private syncVisible() { - const materialsLength = this.materials.length - for (let i = 0; i < materialsLength; i++) { - this.materials[i].visible = this.visible - } - } + data.set(panelMaterialDefaultData) + unsubscribeList.push( + effect(() => data.set(size.value, 13)), + effect(() => data.set(borderInset.value, 0)), + ) + }), + ) + subscriptions.push(deactivate) } export type PanelMaterialInfo = { type: 'instanced' } | { type: 'normal'; data: Float32Array } diff --git a/packages/uikit/src/panel/react.tsx b/packages/uikit/src/panel/react.tsx index 13cab8f2..cc389733 100644 --- a/packages/uikit/src/panel/react.tsx +++ b/packages/uikit/src/panel/react.tsx @@ -1,22 +1,19 @@ -import { ReactNode, RefObject, createContext, useCallback, useContext, useEffect, useMemo } from 'react' +import { ReactNode, RefObject, createContext, useCallback, useEffect, useMemo } from 'react' import { Group, Material, Matrix4, Mesh, MeshBasicMaterial, Plane, Vector2Tuple } from 'three' import type { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' import { Signal, effect } from '@preact/signals-core' import { Inset } from '../flex/node.js' -import { useSignalEffect } from '../utils.js' +import { Subscriptions } from '../utils.js' import { useFrame } from '@react-three/fiber' -import { ClippingRect, useParentClippingRect } from '../clipping.js' +import { ClippingRect } from '../clipping.js' import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' import { HoverEventHandlers } from '../hover.js' import { InstancedPanelGroup } from './instanced-panel-group.js' -import { InstancedPanel } from './instanced-panel.js' import { MaterialSetter, PanelDepthMaterial, PanelDistanceMaterial, createPanelMaterial } from './panel-material.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { ManagerCollection, PropertyTransformation } from '../properties/utils.js' -import { useBatchedProperties } from '../properties/batched.js' import { CameraDistanceRef, ElementType, OrderInfo } from '../order.js' import { panelGeometry } from './utils.js' import { ActiveEventHandlers } from '../active.js' +import { MergedProperties } from '../properties/merged.js' export function InteractionGroup({ handlers, @@ -85,30 +82,28 @@ function mergeHandlers( } } -export function useInteractionPanel( +export function createInteractionPanel( size: Signal, psRef: { pixelSize: number }, orderInfo: OrderInfo, + parentClippingRect: Signal, rootGroupRef: RefObject, + subscriptions: Subscriptions, ): Mesh { - const parentClippingRect = useParentClippingRect() - const panel = useMemo(() => { - const result = new Mesh(panelGeometry) - result.matrixAutoUpdate = false - result.raycast = makeClippedRaycast(result, makePanelRaycast(result), rootGroupRef, parentClippingRect, orderInfo) - result.visible = false - return result - }, [parentClippingRect, orderInfo, rootGroupRef]) - useSignalEffect(() => { - const [width, height] = size.value - panel.scale.set(width * psRef.pixelSize, height * psRef.pixelSize, 1) - panel.updateMatrix() - }, [size, psRef]) + const panel = new Mesh(panelGeometry) + panel.matrixAutoUpdate = false + panel.raycast = makeClippedRaycast(panel, makePanelRaycast(panel), rootGroupRef, parentClippingRect, orderInfo) + panel.visible = false + subscriptions.push( + effect(() => { + const [width, height] = size.value + panel.scale.set(width * psRef.pixelSize, height * psRef.pixelSize, 1) + panel.updateMatrix() + }), + ) return panel } -export type MaterialClass = { new (...args: Array): Material } - export type GetInstancedPanelGroup = ( majorIndex: number, panelGroupDependencies: PanelGroupDependencies, @@ -116,31 +111,6 @@ export type GetInstancedPanelGroup = ( const InstancedPanelContext = createContext(null as any) -export function usePanelMaterials( - collection: ManagerCollection, - size: Signal, - borderInset: Signal, - isClipped: Signal, - materialClass: MaterialClass | undefined, - clippingPlanes: Array, - propertyTransformation: PropertyTransformation, -): readonly [Material, Material, Material] { - const { materials, setter } = useMemo(() => { - const setter = new MaterialSetter(size, borderInset, isClipped) - const info = { data: setter.data, type: 'normal' } as const - const material = createPanelMaterial(materialClass ?? MeshBasicMaterial, info) - const depthMaterial = new PanelDepthMaterial(info) - const distanceMaterial = new PanelDistanceMaterial(info) - material.clippingPlanes = clippingPlanes - depthMaterial.clippingPlanes = clippingPlanes - distanceMaterial.clippingPlanes = clippingPlanes - return { materials: [material, depthMaterial, distanceMaterial], setter } as const - }, [size, borderInset, isClipped, materialClass, clippingPlanes]) - useImmediateProperties(collection, setter, propertyTransformation) - useBatchedProperties(collection, setter, propertyTransformation) - useEffect(() => () => setter.destroy(), [setter]) - return materials -} export type PanelGroupDependencies = { materialClass: MaterialClass @@ -150,57 +120,6 @@ export type PanelGroupDependencies = { export type ShadowProperties = { receiveShadow?: boolean; castShadow?: boolean } -export function usePanelGroupDependencies( - materialClass: MaterialClass = MeshBasicMaterial, - { castShadow = false, receiveShadow = false }: ShadowProperties, -): PanelGroupDependencies { - return useMemo( - () => ({ - materialClass, - castShadow, - receiveShadow, - }), - [materialClass, castShadow, receiveShadow], - ) -} - -/** - * @param providedGetGroup provdedGetGroup should onlyever be used for inside the root component (don't provide it otherwise) - */ -export function useInstancedPanel( - collection: ManagerCollection, - matrix: Signal, - size: Signal, - offset: Signal | undefined, - borderInset: Signal, - isHidden: Signal | undefined, - orderInfo: OrderInfo, - parentClippingRect: Signal | undefined, - panelGroupDependencies: PanelGroupDependencies, - propertyTransformation: PropertyTransformation, - providedGetGroup?: GetInstancedPanelGroup, -): void { - // eslint-disable-next-line react-hooks/rules-of-hooks - const getGroup = providedGetGroup ?? useContext(InstancedPanelContext) - const panel = useMemo( - () => - new InstancedPanel( - getGroup(orderInfo.majorIndex, panelGroupDependencies), - matrix, - size, - offset, - borderInset, - parentClippingRect, - isHidden, - orderInfo.minorIndex, - ), - [getGroup, matrix, size, borderInset, parentClippingRect, isHidden, orderInfo, offset, panelGroupDependencies], - ) - useEffect(() => () => panel.destroy(), [panel]) - useImmediateProperties(collection, panel, propertyTransformation) - useBatchedProperties(collection, panel, propertyTransformation) -} - export function useGetInstancedPanelGroup( pixelSize: number, cameraDistance: CameraDistanceRef, diff --git a/packages/uikit/src/properties/alias.ts b/packages/uikit/src/properties/alias.ts index d0759a50..74ef4493 100644 --- a/packages/uikit/src/properties/alias.ts +++ b/packages/uikit/src/properties/alias.ts @@ -1,36 +1,9 @@ -import { PropertyTransformation } from './utils.js' - type Aliases = Readonly | undefined>> export type WithAliases>> = T & { [K in keyof A]?: A[K][number] extends keyof T ? T[A[K][number]] : never } -function createAliasPropertyTransformation(aliases: Aliases): PropertyTransformation { - return (key, value, hasProperty, setProperty) => { - if (hasProperty(key)) { - setProperty(key, value) - return - } - const aliasList = aliases[key] - if (aliasList == null) { - return - } - const aliasListLength = aliasList.length - if (!hasProperty(aliasList[0])) { - //if one alias doesnt exist on the object, all aliases dont exist - return - } - //and also, if one alias exists on the object, all aliases exist - for (let i = 0; i < aliasListLength; i++) { - const alias = aliasList[i] - setProperty(alias, value) - } - } -} - -export type AllAliases = typeof flexAliases & typeof panelAliases & typeof scrollbarAliases & typeof transformAliases - export type WithAllAliases = WithAliases const borderAliases = { @@ -39,8 +12,6 @@ const borderAliases = { borderY: ['borderTop', 'borderBottom'], } as const satisfies Aliases -export const borderAliasPropertyTransformation = createAliasPropertyTransformation(borderAliases) - const flexAliases = { ...borderAliases, inset: ['positionTop', 'positionLeft', 'positionRight', 'positionBottom'], @@ -53,8 +24,6 @@ const flexAliases = { gap: ['gapRow', 'gapColumn'], } as const satisfies Aliases -export const flexAliasPropertyTransformation = createAliasPropertyTransformation(flexAliases) - const panelAliases = { borderRadius: ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius'], borderTopRadius: ['borderTopLeftRadius', 'borderTopRightRadius'], @@ -63,25 +32,40 @@ const panelAliases = { borderBottomRadius: ['borderBottomLeftRadius', 'borderBottomRightRadius'], } as const satisfies Aliases -export const panelAliasPropertyTransformation = createAliasPropertyTransformation(panelAliases) - -const scrollbarAliases = { +const scrollbarPanelAliases = { scrollbarBorderRadius: [ - 'borderTopLeftRadius', - 'borderTopRightRadius', - 'borderBottomLeftRadius', - 'borderBottomRightRadius', + 'scrollbarBorderTopLeftRadius', + 'scrollbarBorderTopRightRadius', + 'scrollbarBorderBottomLeftRadius', + 'scrollbarBorderBottomRightRadius', ], - scrollbarBorderTopRadius: ['borderTopLeftRadius', 'borderTopRightRadius'], - scrollbarBorderLeftRadius: ['borderTopLeftRadius', 'borderBottomLeftRadius'], - scrollbarBorderRightRadius: ['borderTopRightRadius', 'borderBottomRightRadius'], - scrollbarBorderBottomRadius: ['borderBottomLeftRadius', 'borderBottomRightRadius'], + scrollbarBorderTopRadius: ['scrollbarBorderTopLeftRadius', 'scrollbarBorderTopRightRadius'], + scrollbarBorderLeftRadius: ['scrollbarBorderTopLeftRadius', 'scrollbarBorderBottomLeftRadius'], + scrollbarBorderRightRadius: ['scrollbarBorderTopRightRadius', 'scrollbarBorderBottomRightRadius'], + scrollbarBorderBottomRadius: ['scrollbarBorderBottomLeftRadius', 'scrollbarBorderBottomRightRadius'], } as const satisfies Aliases -export const scrollbarAliasPropertyTransformation = createAliasPropertyTransformation(scrollbarAliases) +const scrollbarBorderAliases = { + scrollbarBorder: ['scrollbarBorderBottom', 'scrollbarBorderTop', 'scrollbarBorderLeft', 'scrollbarBorderRight'], + scrollbarBorderX: ['scrollbarBorderLeft', 'scrollbarBorderRight'], + scrollbarBorderY: ['scrollbarBorderTop', 'scrollbarBorderBottom'], +} as const satisfies Aliases const transformAliases = { transformScale: ['transformScaleX', 'transformScaleY', 'transformScaleZ'], } as const satisfies Aliases -export const transformAliasPropertyTransformation = createAliasPropertyTransformation(transformAliases) +export type AllAliases = typeof flexAliases & + typeof panelAliases & + typeof scrollbarPanelAliases & + typeof transformAliases & + typeof scrollbarBorderAliases + +export const allAliases: AllAliases = Object.assign( + {}, + flexAliases, + panelAliases, + scrollbarPanelAliases, + transformAliases, + scrollbarBorderAliases, +) diff --git a/packages/uikit/src/properties/batched.ts b/packages/uikit/src/properties/batched.ts index 3b77f821..58cf73dd 100644 --- a/packages/uikit/src/properties/batched.ts +++ b/packages/uikit/src/properties/batched.ts @@ -1,44 +1,31 @@ -import { useMemo } from 'react' -import { - PropertyTransformation, - equalReactiveProperty, - readReactiveProperty, - usePropertyManager, - Properties, - ManagerCollection, -} from './utils.js' -import { Signal } from '@preact/signals-core' +import { Signal, computed } from '@preact/signals-core' +import { MergedProperties } from './merged.js' -export type WithBatchedProperties

= {}> = { - hasBatchedProperty(key: keyof P): boolean - getProperty: Signal<((key: K) => P[K]) | undefined> -} +export type GetBatchedProperties = (key: string) => unknown -export function useBatchedProperties( - collection: ManagerCollection, - object: WithBatchedProperties, - transformProperty?: PropertyTransformation, -): void { - const hasProperty = useMemo(() => object.hasBatchedProperty.bind(object), [object]) - const finishProperties = useMemo(() => { - let prevProperties: Properties = {} - return (properties: Properties, propertiesLength: number) => { - let prevPropertiesLength = 0 - let changed = false - for (const key in prevProperties) { - if (!equalReactiveProperty(prevProperties[key], properties[key])) { - changed = true - break - } - ++prevPropertiesLength - } - changed ||= prevPropertiesLength != propertiesLength - prevProperties = properties - if (!changed && object.getProperty.peek() != null) { - return - } - object.getProperty.value = (key) => readReactiveProperty(properties[key]) as never +export function createGetBatchedProperties( + propertiesSignal: Signal, + hasProperty: (key: string) => boolean, + renameOutput?: Record, +): GetBatchedProperties { + const reverseRenameMap: Record = {} + for (const key in renameOutput) { + reverseRenameMap[renameOutput[key]] = key + } + let currentProperties: MergedProperties | undefined + const computedProperties = computed(() => { + const newProperties = propertiesSignal.value + if (!newProperties.filterIsEqual(hasProperty, currentProperties)) { + //update current properties + currentProperties = newProperties + } + //due to the referencial equality check, the computed value only updates when filterIsEqual returns false + return currentProperties + }) + return (key) => { + if (key in reverseRenameMap) { + key = reverseRenameMap[key] } - }, [object]) - usePropertyManager(collection, hasProperty as (key: string) => boolean, finishProperties, transformProperty) + return computedProperties.value?.read(key) + } } diff --git a/packages/uikit/src/properties/default.tsx b/packages/uikit/src/properties/default.tsx index 6e13575d..24830808 100644 --- a/packages/uikit/src/properties/default.tsx +++ b/packages/uikit/src/properties/default.tsx @@ -1,12 +1,11 @@ -import { createContext, ReactNode, useContext } from 'react' -import { applyProperties, ManagerCollection, PropertyManager } from './utils.js' +import { ReadonlySignal } from '@preact/signals-core' import { ContainerProperties } from '../components/container.js' -import { ContentProperties } from '../components/content.js' -import { ImageProperties } from '../components/image.js' -import { RootProperties } from '../components/root.js' -import { SvgProperties } from '../components/svg.js' -import { CustomContainerProperties } from '../components/custom.js' -import { TextProperties } from '../index.js' +import { ContentProperties } from '../react/content.js' +import { CustomContainerProperties } from '../react/custom.js' +import { ImageProperties } from '../react/image.js' +import { RootProperties } from '../react/root.js' +import { SvgProperties } from '../react/svg.js' +import { TextProperties } from '../react/text.js' export type AllOptionalProperties = | ContainerProperties @@ -17,12 +16,19 @@ export type AllOptionalProperties = | SvgProperties | TextProperties -const DefaultPropertiesContext = createContext(null as any) +export type WithReactive = { + [Key in keyof T]?: T[Key] | ReadonlySignal +} + +export type Properties = Record export type WithClasses = T & { classes?: T | Array } -export function useTraverseProperties(properties: WithClasses, fn: (properties: T) => void): void { - const defaultProperties = useContext(DefaultPropertiesContext) +export function traverseProperties( + defaultProperties: AllOptionalProperties | undefined, + properties: WithClasses, + fn: (properties: T) => void, +): void { if (defaultProperties != null) { fn(defaultProperties as T) } @@ -37,26 +43,3 @@ export function useTraverseProperties(properties: WithClasses, fn: (proper } fn(properties) } - -export function useApplyProperties( - collection: ManagerCollection, - properties: WithClasses>, -): void { - useTraverseProperties(properties, (p) => applyProperties(collection, p)) -} - -export function DefaultProperties(properties: { children?: ReactNode } & AllOptionalProperties) { - const existingDefaultProperties = useContext(DefaultPropertiesContext) - const result: any = { ...existingDefaultProperties } - for (const key in properties) { - if (key === 'children') { - continue - } - const value = properties[key as keyof AllOptionalProperties] - if (value == null) { - continue - } - result[key] = value as any - } - return {properties.children} -} diff --git a/packages/uikit/src/properties/immediate.ts b/packages/uikit/src/properties/immediate.ts index 751b6cfc..441df8d3 100644 --- a/packages/uikit/src/properties/immediate.ts +++ b/packages/uikit/src/properties/immediate.ts @@ -1,100 +1,89 @@ -import { Signal, effect } from '@preact/signals-core' -import { useCallback, useMemo, useRef } from 'react' -import { useSignalEffect } from '../utils.js' -import { - PropertyTransformation, - PropertyManager, - usePropertyManager, - Properties, - equalReactiveProperty, - readReactiveProperty, - ManagerCollection, -} from './utils.js' - -export type WithImmediateProperties = { - active: Signal - hasImmediateProperty: (key: string) => boolean - setProperty: (key: string, value: unknown) => void -} +import { Signal, effect, untracked } from '@preact/signals-core' +import { MergedProperties } from './merged.js' +import { Subscriptions } from '../utils.js' type PropertySubscriptions = Record void> -const EmptyProperties: Properties = {} - -export function useImmediateProperties( - collection: ManagerCollection, - object: WithImmediateProperties, - transformProperty?: PropertyTransformation, +export function setupImmediateProperties( + propertiesSignal: Signal, + activeSignal: Signal, + objectHasProperty: (key: string) => boolean, + objectSetProperty: (key: string, value: unknown) => void, + subscriptions: Subscriptions, + renameOutput?: Record, ): void { - const activeRef = useRef(false) - const propertiesRef = useRef({}) - const subscriptions = useRef({}) + let active = false + let currentProperties: MergedProperties | undefined + let propertySubscriptions: PropertySubscriptions = {} - const hasProperty = useMemo(() => object.hasImmediateProperty.bind(object), [object]) - const finishProperties = useCallback( - (properties: Properties) => { - if (!activeRef.current) { - propertiesRef.current = properties + const setProperty = + renameOutput == null + ? objectSetProperty + : (key: string, value: unknown) => { + if (key in renameOutput) { + key = renameOutput[key] + } + objectSetProperty(key, value) + } + //the following 2 effects are seperated so that the cleanup call only happens when active changes from true to false + //or everything is cleaned up because the component is destroyed + subscriptions.push( + effect(() => { + const newProperties = propertiesSignal.value + if (active) { + applyProperties(objectHasProperty, newProperties, currentProperties, propertySubscriptions, setProperty) + } + currentProperties = newProperties + }), + effect(() => { + active = activeSignal.value + if (!active) { return } - applyProperties(properties, propertiesRef.current, subscriptions.current, object) - propertiesRef.current = properties - }, - [object], + if (currentProperties == null) { + return + } + //(re-)write all current properties since the object is (re-)activiated it might not have its values set + applyProperties(objectHasProperty, currentProperties, undefined, propertySubscriptions, setProperty) + return () => { + unsubscribeProperties(propertySubscriptions) + propertySubscriptions = {} + } + }), ) - useSignalEffect(() => { - activeRef.current = object.active.value - if (!activeRef.current) { - unsubscribe(subscriptions.current) - subscriptions.current = {} - return - } - applyProperties(propertiesRef.current, EmptyProperties, subscriptions.current, object) - return () => { - unsubscribe(subscriptions.current) - subscriptions.current = {} - } - }, [object]) - usePropertyManager(collection, hasProperty, finishProperties, transformProperty) } function applyProperties( - currentProperties: Properties, - oldProperties: Properties, + hasProperty: (key: string) => boolean, + currentProperties: MergedProperties, + oldProperties: MergedProperties | undefined, subscriptions: PropertySubscriptions, - object: WithImmediateProperties, + setProperty: (key: string, value: unknown) => void, ) { - for (const key in currentProperties) { - const currentProperty = currentProperties[key] - if (key in oldProperties) { - const oldProperty = oldProperties[key] - delete oldProperties[key] - if (equalReactiveProperty(currentProperty, oldProperty)) { - //no changes => nothing to do - continue - } - //property changed => unsubscribe old property - subscriptions[key]?.() - } - //new property => subscribe new property - subscriptions[key] = effect(() => { - const currentValue = readReactiveProperty(currentProperty) - - object.setProperty(key, currentValue) - }) - } - for (const key in oldProperties) { - //reset properties + const onNew = (key: string) => + //subscribe and write property + (subscriptions[key] = effect(() => setProperty(key, currentProperties.read(key)))) + const onDelete = (key: string) => { + //remove subscription subscriptions[key]?.() delete subscriptions[key] - if (readReactiveProperty(oldProperties[key]) === undefined) { - continue + //read is fine since we execute the compare in "untracked" + if (currentProperties.read(key) === undefined) { + //no need to set to undefined if already was undefined + return } - object.setProperty(key, undefined) + //reset property + setProperty(key, undefined) + } + const onChange = (key: string) => { + //unsubscribe old property + subscriptions[key]?.() + onNew(key) } + untracked(() => currentProperties.filterCompare(hasProperty, oldProperties, onNew, onChange, onDelete)) } -function unsubscribe(subscriptions: PropertySubscriptions): void { +function unsubscribeProperties(subscriptions: PropertySubscriptions): void { for (const key in subscriptions) { subscriptions[key]() } diff --git a/packages/uikit/src/properties/merged.ts b/packages/uikit/src/properties/merged.ts new file mode 100644 index 00000000..384fdfb5 --- /dev/null +++ b/packages/uikit/src/properties/merged.ts @@ -0,0 +1,156 @@ +import { Signal } from '@preact/signals-core' +import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './default' +import { AllAliases, allAliases } from './alias' + +export class MergedProperties { + private propertyMap = new Map>>() + + add(key: string, value: unknown) { + if (value === undefined) { + //only adding non undefined values to the properties + return + } + //applying the aliases + const aliases = allAliases[key as keyof AllAliases] + if (aliases == null) { + this.addToMap(key, value) + return + } + const length = aliases.length + for (let i = 0; i < length; i++) { + this.addToMap(aliases[i], value) + } + } + + private addToMap(key: string, value: unknown) { + let entry = this.propertyMap.get(key) + if (entry == null) { + this.propertyMap.set(key, (entry = [])) + } + if (!(value instanceof Signal)) { + //if its not a signal we can clear the previous values + entry.length = 0 + } + entry.push(value) + } + + /** + * @returns undefined if the property doesn't exist + */ + read(key: string): unknown { + const entry = this.propertyMap.get(key) + if (entry == null) { + return undefined + } + const length = entry.length + //searching for the property with the highest precedence (most right) that is not undefined + for (let i = length - 1; i >= 0; i--) { + const value = entry[i] + const result = value instanceof Signal ? value.value : value + if (result === undefined) { + continue + } + return result + } + //no property found that is not undefined + return undefined + } + + filterIsEqual(filter: (key: string) => boolean, old: MergedProperties | undefined): boolean { + if (old == null) { + return this.propertyMap.size === 0 + } + if (old.propertyMap.size != this.propertyMap.size) { + return false + } + for (const key of this.propertyMap.keys()) { + if (!filter(key)) { + continue + } + if (!this.isEqual(old, key)) { + return false + } + } + for (const key of old.propertyMap.keys()) { + if (!filter(key)) { + continue + } + if (!old.isEqual(this, key)) { + return false + } + } + return true + } + + filterCompare( + filter: (key: string) => boolean, + old: MergedProperties | undefined, + onNew: (key: string) => void, + onChange: (key: string) => void, + onDelete: (key: string) => void, + ): void { + for (const key of this.propertyMap.keys()) { + if (!filter(key)) { + continue + } + if (old == null) { + onNew(key) + continue + } + const oldEntry = old.propertyMap.get(key) + if (oldEntry == null) { + //new + onNew(key) + continue + } + const thisEntry = this.propertyMap.get(key) + if (shallodwEqual(oldEntry, thisEntry!)) { + continue + } + //changed + onChange(key) + } + if (old == null) { + return + } + for (const key of old.propertyMap.keys()) { + if (!filter(key)) { + continue + } + if (this.propertyMap.has(key)) { + continue + } + onDelete(key) + } + } + + isEqual(otherMap: MergedProperties, key: string): boolean { + const entry1 = this.propertyMap.get(key) + const entry2 = otherMap.propertyMap.get(key) + if (entry1 == null || entry2 == null) { + return entry1 === entry2 + } + return shallodwEqual(entry1, entry2) + } + + addAll(defaultProperties: AllOptionalProperties | undefined, properties: WithClasses): void { + traverseProperties(defaultProperties, properties, (p) => { + for (const key in p) { + this.add(key, p[key]) + } + }) + } +} + +function shallodwEqual(a1: Array, a2: Array): boolean { + const length = a1.length + if (length != a2.length) { + return false + } + for (let i = 0; i < length; i++) { + if (a1[i] != a2[i]) { + return false + } + } + return true +} diff --git a/packages/uikit/src/properties/utils.ts b/packages/uikit/src/properties/utils.ts deleted file mode 100644 index 23a58b28..00000000 --- a/packages/uikit/src/properties/utils.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { useMemo } from 'react' -import { ReadonlySignal, Signal, signal } from '@preact/signals-core' -import { WithBatchedProperties, useBatchedProperties } from './batched.js' - -export type Properties = Record - -export type WithReactive = { - [Key in keyof T]?: T[Key] | ReadonlySignal -} - -export type PropertyTransformation = ( - key: string, - value: unknown, - hasProperty: (key: string) => boolean, - setProperty: (key: string, value: unknown) => void, -) => void - -export type PropertyManager = { - add(key: string, value: unknown): void - finish(): void -} - -export function applyProperties(collection: ManagerCollection, properties: Properties): void { - const collectionLength = collection.length - for (const key in properties) { - for (let i = 0; i < collectionLength; i++) { - collection[i].add(key, properties[key]) - } - } -} - -export type ManagerCollection = Array - -export function createCollection(): ManagerCollection { - return [] -} - -export function writeCollection(collection: ManagerCollection, key: string, value: unknown): void { - const collectionLength = collection.length - for (let i = 0; i < collectionLength; i++) { - collection[i].add(key, value) - } -} - -export function finalizeCollection(collection: ManagerCollection): void { - const collectionLength = collection.length - for (let i = 0; i < collectionLength; i++) { - collection[i].finish() - } -} - -export function usePropertyManager( - collection: ManagerCollection, - hasProperty: (key: string) => boolean, - finishProperties: (properties: Properties, propertiesLength: number) => void, - transformProperty?: PropertyTransformation, -): void { - collection.push( - useMemo(() => { - let currentProperties: Properties = {} - let currentPropertiesLength: number = 0 - const setProperty = (key: string, newValue: unknown): void => { - if (newValue === undefined) { - //only adding non undefined values to the properties - return - } - const currentValue = currentProperties[key] - if (currentValue === undefined) { - //insert - ++currentPropertiesLength - } - if (currentValue == null || !(newValue instanceof Signal)) { - //replace or insert - currentProperties[key] = newValue - return - } - //we adding a signal to an existing property / to existing property - if (Array.isArray(currentValue)) { - currentValue.push(newValue) - return - } - currentProperties[key] = [currentValue, newValue] - return - } - const add = (key: string, value: unknown) => { - if (value === undefined) { - return - } - if (transformProperty != null) { - transformProperty(key, value, hasProperty, setProperty) - return - } - if (hasProperty(key)) { - setProperty(key, value) - } - } - return { - add, - finish: () => { - finishProperties(currentProperties, currentPropertiesLength) - currentPropertiesLength = 0 - currentProperties = {} - }, - } - }, [hasProperty, finishProperties, transformProperty]), - ) -} - -export function equalReactiveProperty(val1: unknown, val2: unknown): boolean { - if (!Array.isArray(val1)) { - return val1 === val2 - } - if (!Array.isArray(val2)) { - return false - } - const length = val1.length - if (length != val2.length) { - return false - } - for (let i = 0; i < length; i++) { - if (val1[i] != val2[i]) { - return false - } - } - return true -} - -export function readReactiveProperty(value: unknown): unknown { - if (value instanceof Signal) { - return (value as unknown as Signal).value - } - if (!Array.isArray(value)) { - return value - } - let result = undefined - const length = value.length - for (let i = 0; i < length; i++) { - const val = value[i] - const current = val instanceof Signal ? val.value : val - if (current === undefined) { - continue - } - result = current - } - return result -} - -export function useGetBatchedProperties>( - collection: ManagerCollection, - keys: ReadonlyArray, - propertyTransformation?: PropertyTransformation, -) { - const getPropertySignal: WithBatchedProperties>['getProperty'] | undefined = useMemo( - () => signal(undefined), - [], - ) - const object = useMemo>>( - () => ({ - hasBatchedProperty: (key) => keys.includes(key), - getProperty: getPropertySignal, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [keys], - ) - useBatchedProperties(collection, object, propertyTransformation) - return getPropertySignal -} diff --git a/packages/uikit/src/components/container.tsx b/packages/uikit/src/react/container.tsx similarity index 85% rename from packages/uikit/src/components/container.tsx rename to packages/uikit/src/react/container.tsx index 4ebea473..1a98b815 100644 --- a/packages/uikit/src/components/container.tsx +++ b/packages/uikit/src/react/container.tsx @@ -1,5 +1,5 @@ import { ReactNode, forwardRef, useRef } from 'react' -import { useFlexNode, FlexProvider } from '../flex/react.js' +import { useFlexNode, FlexProvider } from './react.js' import { WithReactive, createCollection, finalizeCollection } from '../properties/utils.js' import { InteractionGroup, @@ -11,8 +11,8 @@ import { } from '../panel/react.js' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { YogaProperties } from '../flex/node.js' -import { useApplyHoverProperties } from '../hover.js' -import { ClippingRectProvider, useClippingRect, useIsClipped, useParentClippingRect } from '../clipping.js' +import { applyHoverProperties } from '../hover.js' +import { ClippingRectProvider, computeClippingRect, useIsClipped, useParentClippingRect } from '../clipping.js' import { ViewportListeners, LayoutListeners, @@ -31,7 +31,7 @@ import { ScrollbarProperties, useGlobalScrollMatrix, useScrollPosition, - useScrollbars, + createScrollbars, } from '../scroll.js' import { WithAllAliases, @@ -41,13 +41,13 @@ import { import { PanelProperties } from '../panel/instanced-panel.js' import { TransformProperties, useTransformMatrix } from '../transform.js' import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' +import { WithClasses, addToMerged } from '../properties/default.js' import { useRootGroupRef } from '../utils.js' import { useApplyResponsiveProperties } from '../responsive.js' import { Group } from 'three' import { ElementType, OrderInfoProvider, ZIndexOffset, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' export type ContainerProperties = WithConditionals< WithClasses< @@ -93,7 +93,7 @@ export const Container = forwardRef< const scrollPosition = useScrollPosition() const globalScrollMatrix = useGlobalScrollMatrix(scrollPosition, node, globalMatrix) - useScrollbars( + createScrollbars( collection, scrollPosition, node, @@ -105,17 +105,17 @@ export const Container = forwardRef< ) //apply all properties - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) + addToMerged(collection, properties) + applyPreferredColorSchemeProperties(collection, properties) useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) + const hoverHandlers = applyHoverProperties(collection, properties) + const activeHandlers = applyActiveProperties(collection, properties) finalizeCollection(collection) useLayoutListeners(properties, node.size) useViewportListeners(properties, isClipped) - const clippingRect = useClippingRect( + const clippingRect = computeClippingRect( globalMatrix, node.size, node.borderInset, diff --git a/packages/uikit/src/components/content.tsx b/packages/uikit/src/react/content.tsx similarity index 93% rename from packages/uikit/src/components/content.tsx rename to packages/uikit/src/react/content.tsx index c9eb8b7d..ee0a28f3 100644 --- a/packages/uikit/src/components/content.tsx +++ b/packages/uikit/src/react/content.tsx @@ -1,7 +1,7 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { ReactNode, RefObject, forwardRef, useEffect, useMemo, useRef } from 'react' import { YogaProperties } from '../flex/node.js' -import { FlexProvider, useFlexNode } from '../flex/react.js' +import { FlexProvider, useFlexNode } from './react.js' import { InteractionGroup, MaterialClass, @@ -21,7 +21,7 @@ import { import { alignmentZMap, useRootGroupRef } from '../utils.js' import { Box3, Group, Mesh, Vector3 } from 'three' import { computed, effect, Signal, signal } from '@preact/signals-core' -import { useApplyHoverProperties } from '../hover.js' +import { applyHoverProperties } from '../hover.js' import { ComponentInternals, LayoutListeners, @@ -42,11 +42,11 @@ import { } from '../properties/alias.js' import { TransformProperties, useTransformMatrix } from '../transform.js' import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' +import { WithClasses, addToMerged } from '../properties/default.js' import { useApplyResponsiveProperties } from '../responsive.js' import { CameraDistanceRef, ElementType, OrderInfo, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' export type ContentProperties = WithConditionals< WithClasses< @@ -108,11 +108,11 @@ export const Content = forwardRef< ) //apply all properties - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) + addToMerged(collection, properties) + applyPreferredColorSchemeProperties(collection, properties) useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) + const hoverHandlers = applyHoverProperties(collection, properties) + const activeHandlers = applyActiveProperties(collection, properties) const aspectRatio = useMemo( () => computed(() => { diff --git a/packages/uikit/src/components/custom.tsx b/packages/uikit/src/react/custom.tsx similarity index 88% rename from packages/uikit/src/components/custom.tsx rename to packages/uikit/src/react/custom.tsx index 8541a46b..820aa3ca 100644 --- a/packages/uikit/src/components/custom.tsx +++ b/packages/uikit/src/react/custom.tsx @@ -1,8 +1,8 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { forwardRef, ReactNode, useEffect, useRef } from 'react' import { YogaProperties } from '../flex/node.js' -import { useFlexNode, FlexProvider } from '../flex/react.js' -import { useApplyHoverProperties } from '../hover.js' +import { useFlexNode, FlexProvider } from './react.js' +import { applyHoverProperties } from '../hover.js' import { InteractionGroup, ShadowProperties } from '../panel/react.js' import { createCollection, finalizeCollection, WithReactive } from '../properties/utils.js' import { useRootGroupRef } from '../utils.js' @@ -23,12 +23,12 @@ import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' import { flexAliasPropertyTransformation, WithAllAliases } from '../properties/alias.js' import { TransformProperties, useTransformMatrix } from '../transform.js' import { useImmediateProperties } from '../properties/immediate.js' -import { useApplyProperties, WithClasses } from '../properties/default.js' +import { addToMerged, WithClasses } from '../properties/default.js' import { useApplyResponsiveProperties } from '../responsive.js' import { ElementType, setupRenderOrder, useOrderInfo, ZIndexOffset } from '../order.js' import { effect } from '@preact/signals-core' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' export type CustomContainerProperties = WithConditionals< WithClasses>> @@ -94,11 +94,11 @@ export const CustomContainer = forwardRef< }, [clippingPlanes, node, isClipped, parentClippingRect, orderInfo, rootGroupRef]) //apply all properties - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) + addToMerged(collection, properties) + applyPreferredColorSchemeProperties(collection, properties) useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) + const hoverHandlers = applyHoverProperties(collection, properties) + const activeHandlers = applyActiveProperties(collection, properties) finalizeCollection(collection) useLayoutListeners(properties, node.size) diff --git a/packages/uikit/src/components/fullscreen.tsx b/packages/uikit/src/react/fullscreen.tsx similarity index 100% rename from packages/uikit/src/components/fullscreen.tsx rename to packages/uikit/src/react/fullscreen.tsx diff --git a/packages/uikit/src/components/icon.tsx b/packages/uikit/src/react/icon.tsx similarity index 92% rename from packages/uikit/src/components/icon.tsx rename to packages/uikit/src/react/icon.tsx index 2eac9287..158f7c57 100644 --- a/packages/uikit/src/components/icon.tsx +++ b/packages/uikit/src/react/icon.tsx @@ -1,6 +1,6 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { ReactNode, forwardRef, useMemo, useRef } from 'react' -import { useFlexNode } from '../flex/react.js' +import { useFlexNode } from './react.js' import { InteractionGroup, MaterialClass, @@ -12,7 +12,7 @@ import { import { createCollection, finalizeCollection, useGetBatchedProperties, writeCollection } from '../properties/utils.js' import { useSignalEffect, fitNormalizedContentInside, useRootGroupRef } from '../utils.js' import { Color, Group, Mesh, MeshBasicMaterial, ShapeGeometry } from 'three' -import { useApplyHoverProperties } from '../hover.js' +import { applyHoverProperties } from '../hover.js' import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js' import { ComponentInternals, @@ -28,12 +28,12 @@ import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' import { flexAliasPropertyTransformation, panelAliasPropertyTransformation } from '../properties/alias.js' import { useTransformMatrix } from '../transform.js' import { useImmediateProperties } from '../properties/immediate.js' -import { useApplyProperties } from '../properties/default.js' +import { addToMerged } from '../properties/default.js' import { SvgProperties, AppearanceProperties } from './svg.js' import { useApplyResponsiveProperties } from '../responsive.js' import { ElementType, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' const colorHelper = new Color() @@ -144,11 +144,11 @@ export const SvgIconFromText = forwardRef< //apply all properties writeCollection(collection, 'width', properties.svgWidth) writeCollection(collection, 'height', properties.svgHeight) - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) + addToMerged(collection, properties) + applyPreferredColorSchemeProperties(collection, properties) useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) + const hoverHandlers = applyHoverProperties(collection, properties) + const activeHandlers = applyActiveProperties(collection, properties) writeCollection(collection, 'aspectRatio', properties.svgWidth / properties.svgHeight) finalizeCollection(collection) diff --git a/packages/uikit/src/components/image.tsx b/packages/uikit/src/react/image.tsx similarity index 92% rename from packages/uikit/src/components/image.tsx rename to packages/uikit/src/react/image.tsx index dbc1cfd4..3f8e5204 100644 --- a/packages/uikit/src/components/image.tsx +++ b/packages/uikit/src/react/image.tsx @@ -5,9 +5,9 @@ import { Signal, computed } from '@preact/signals-core' import { Inset, YogaProperties } from '../flex/node.js' import { panelGeometry } from '../panel/utils.js' import { InteractionGroup, MaterialClass, ShadowProperties, usePanelMaterials } from '../panel/react.js' -import { useFlexNode } from '../flex/react.js' +import { useFlexNode } from './react.js' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { useApplyHoverProperties } from '../hover.js' +import { applyHoverProperties } from '../hover.js' import { ComponentInternals, LayoutListeners, @@ -29,7 +29,7 @@ import { import { TransformProperties, useTransformMatrix } from '../transform.js' import { ManagerCollection, - PropertyTransformation, + PropertyKeyTransformation, WithReactive, createCollection, finalizeCollection, @@ -37,11 +37,11 @@ import { writeCollection, } from '../properties/utils.js' import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' +import { WithClasses, addToMerged } from '../properties/default.js' import { useApplyResponsiveProperties } from '../responsive.js' import { ElementType, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' export type ImageFit = 'cover' | 'fill' const FIT_DEFAULT: ImageFit = 'fill' @@ -64,7 +64,7 @@ export type ImageFitProperties = { fit?: ImageFit } -const materialPropertyTransformation: PropertyTransformation = (key, value, hasProperty, setProperty) => { +const materialPropertyTransformation: PropertyKeyTransformation = (key, value, hasProperty, setProperty) => { if (key === 'opacity') { setProperty('backgroundOpacity', value) return @@ -132,11 +132,11 @@ export const Image = forwardRef< }, [node, materials, rootGroupRef, parentClippingRect, orderInfo, properties.receiveShadow, properties.castShadow]) //apply all properties - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) + addToMerged(collection, properties) + applyPreferredColorSchemeProperties(collection, properties) useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) + const hoverHandlers = applyHoverProperties(collection, properties) + const activeHandlers = applyActiveProperties(collection, properties) writeCollection(collection, 'backgroundColor', 0xffffff) if (properties.keepAspectRatio ?? true) { writeCollection(collection, 'aspectRatio', aspectRatio) diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/react/index.ts similarity index 100% rename from packages/uikit/src/components/index.ts rename to packages/uikit/src/react/index.ts diff --git a/packages/uikit/src/components/input.tsx b/packages/uikit/src/react/input.tsx similarity index 97% rename from packages/uikit/src/components/input.tsx rename to packages/uikit/src/react/input.tsx index a9172123..5041fc2b 100644 --- a/packages/uikit/src/components/input.tsx +++ b/packages/uikit/src/react/input.tsx @@ -1,6 +1,6 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { YogaProperties } from '../flex/node.js' -import { useApplyHoverProperties } from '../hover.js' +import { applyHoverProperties } from '../hover.js' import { PanelProperties } from '../panel/instanced-panel.js' import { InteractionGroup, MaterialClass, useInstancedPanel, useInteractionPanel } from '../panel/react.js' import { @@ -8,7 +8,7 @@ import { flexAliasPropertyTransformation, panelAliasPropertyTransformation, } from '../properties/alias.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' +import { WithClasses, addToMerged } from '../properties/default.js' import { WithReactive, createCollection, finalizeCollection, writeCollection } from '../properties/utils.js' import { ScrollListeners } from '../scroll.js' import { TransformProperties, useTransformMatrix } from '../transform.js' @@ -24,7 +24,7 @@ import { } from './utils.js' import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react' import { useParentClippingRect, useIsClipped } from '../clipping.js' -import { useFlexNode } from '../flex/react.js' +import { useFlexNode } from './react.js' import { useImmediateProperties } from '../properties/immediate.js' import { InstancedTextProperties, useInstancedText } from '../text/react.js' import { Signal, signal } from '@preact/signals-core' diff --git a/packages/uikit/src/components/portal.tsx b/packages/uikit/src/react/portal.tsx similarity index 100% rename from packages/uikit/src/components/portal.tsx rename to packages/uikit/src/react/portal.tsx diff --git a/packages/uikit/src/flex/react.ts b/packages/uikit/src/react/react.ts similarity index 57% rename from packages/uikit/src/flex/react.ts rename to packages/uikit/src/react/react.ts index f6748928..566b4b5e 100644 --- a/packages/uikit/src/flex/react.ts +++ b/packages/uikit/src/react/react.ts @@ -1,27 +1,15 @@ -import { createContext, useContext, useMemo, useEffect, RefObject, useCallback, useRef } from 'react' -import { FlexNode } from './node.js' -import { Group } from 'three' +import { createContext, useContext, useCallback, useRef } from 'react' +import { FlexNode } from '../flex/node.js' import { useFrame } from '@react-three/fiber' const FlexContext = createContext(null as any) +export const FlexProvider = FlexContext.Provider + export function useParentFlexNode() { return useContext(FlexContext) } -export function useFlexNode(groupRef: RefObject): FlexNode { - const parentNode = useParentFlexNode() - const node = useMemo(() => parentNode.createChild(groupRef), [groupRef, parentNode]) - useEffect(() => { - parentNode.addChild(node) - return () => { - parentNode.removeChild(node) - node.destroy() - } - }, [parentNode, node]) - return node -} - export function useDeferredRequestLayoutCalculation(): (node: FlexNode) => void { let requestedNodeRef = useRef(undefined) useFrame(() => { @@ -38,5 +26,3 @@ export function useDeferredRequestLayoutCalculation(): (node: FlexNode) => void requestedNodeRef.current = node }, []) } - -export const FlexProvider = FlexContext.Provider diff --git a/packages/uikit/src/components/root.tsx b/packages/uikit/src/react/root.tsx similarity index 91% rename from packages/uikit/src/components/root.tsx rename to packages/uikit/src/react/root.tsx index 08bc1f6e..6a59136a 100644 --- a/packages/uikit/src/components/root.tsx +++ b/packages/uikit/src/react/root.tsx @@ -12,12 +12,12 @@ import { usePanelGroupDependencies, } from '../panel/react.js' import { WithReactive, createCollection, finalizeCollection, writeCollection } from '../properties/utils.js' -import { FlexProvider, useDeferredRequestLayoutCalculation } from '../flex/react.js' +import { FlexProvider, useDeferredRequestLayoutCalculation } from './react.js' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { ReadonlySignal, Signal, computed } from '@preact/signals-core' import { Group, Matrix4, Plane, Vector2Tuple, Vector3 } from 'three' import { useFrame, useThree } from '@react-three/fiber' -import { useApplyHoverProperties } from '../hover.js' +import { applyHoverProperties } from '../hover.js' import { LayoutListeners, useLayoutListeners, @@ -26,7 +26,7 @@ import { useComponentInternals, WithConditionals, } from './utils.js' -import { ClippingRectProvider, useClippingRect } from '../clipping.js' +import { ClippingRectProvider, computeClippingRect } from '../clipping.js' import { ScrollGroup, ScrollHandler, @@ -34,7 +34,7 @@ import { ScrollbarProperties, useGlobalScrollMatrix, useScrollPosition, - useScrollbars, + createScrollbars, } from '../scroll.js' import { WithAllAliases, @@ -43,13 +43,13 @@ import { } from '../properties/alias.js' import { TransformProperties, useTransformMatrix } from '../transform.js' import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' +import { WithClasses, addToMerged } from '../properties/default.js' import { InstancedGlyphProvider, useGetInstancedGlyphGroup } from '../text/react.js' import { PanelProperties } from '../panel/instanced-panel.js' import { RootSizeProvider, useApplyResponsiveProperties } from '../responsive.js' import { ElementType, OrderInfoProvider, patchRenderOrder, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' export const DEFAULT_PRECISION = 0.1 export const DEFAULT_PIXEL_SIZE = 0.002 @@ -124,7 +124,7 @@ export const Root = forwardRef< const rootMatrix = useRootMatrix(transformMatrix, node.size, pixelSize, properties) const scrollPosition = useScrollPosition() const globalScrollMatrix = useGlobalScrollMatrix(scrollPosition, node, rootMatrix) - useScrollbars( + createScrollbars( collection, scrollPosition, node, @@ -151,16 +151,16 @@ export const Root = forwardRef< ) //apply all properties - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) + addToMerged(collection, properties) + applyPreferredColorSchemeProperties(collection, properties) useApplyResponsiveProperties(collection, properties, node.size) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) + const hoverHandlers = applyHoverProperties(collection, properties) + const activeHandlers = applyActiveProperties(collection, properties) writeCollection(collection, 'width', useDivide(sizeX, pixelSize)) writeCollection(collection, 'height', useDivide(sizeY, pixelSize)) finalizeCollection(collection) - const clippingRect = useClippingRect(rootMatrix, node.size, node.borderInset, node.overflow, node, undefined) + const clippingRect = computeClippingRect(rootMatrix, node.size, node.borderInset, node.overflow, node, undefined) useLayoutListeners(properties, node.size) const internactionPanel = useInteractionPanel(node.size, node, orderInfo, groupRef) diff --git a/packages/uikit/src/components/suspending.tsx b/packages/uikit/src/react/suspending.tsx similarity index 100% rename from packages/uikit/src/components/suspending.tsx rename to packages/uikit/src/react/suspending.tsx diff --git a/packages/uikit/src/components/svg.tsx b/packages/uikit/src/react/svg.tsx similarity index 93% rename from packages/uikit/src/components/svg.tsx rename to packages/uikit/src/react/svg.tsx index 0f9d1bed..cfe6f672 100644 --- a/packages/uikit/src/components/svg.tsx +++ b/packages/uikit/src/react/svg.tsx @@ -1,7 +1,7 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { ReactNode, RefObject, forwardRef, useMemo, useRef } from 'react' import { YogaProperties } from '../flex/node.js' -import { useFlexNode } from '../flex/react.js' +import { useFlexNode } from './react.js' import { InteractionGroup, MaterialClass, @@ -20,7 +20,7 @@ import { import { useResourceWithParams, useSignalEffect, fitNormalizedContentInside, useRootGroupRef } from '../utils.js' import { Box3, Color, Group, Mesh, MeshBasicMaterial, Plane, ShapeGeometry, Vector3 } from 'three' import { computed, ReadonlySignal, Signal } from '@preact/signals-core' -import { useApplyHoverProperties } from '../hover.js' +import { applyHoverProperties } from '../hover.js' import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js' import { Color as ColorRepresentation } from '@react-three/fiber' import { @@ -43,11 +43,11 @@ import { } from '../properties/alias.js' import { TransformProperties, useTransformMatrix } from '../transform.js' import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' +import { WithClasses, addToMerged } from '../properties/default.js' import { useApplyResponsiveProperties } from '../responsive.js' import { CameraDistanceRef, ElementType, OrderInfo, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' export type SvgProperties = WithConditionals< WithClasses< @@ -195,11 +195,11 @@ export const Svg = forwardRef< const aspectRatio = useMemo(() => computed(() => svgObject.value?.aspectRatio), [svgObject]) //apply all properties - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) + addToMerged(collection, properties) + applyPreferredColorSchemeProperties(collection, properties) useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) + const hoverHandlers = applyHoverProperties(collection, properties) + const activeHandlers = applyActiveProperties(collection, properties) writeCollection(collection, 'aspectRatio', aspectRatio) finalizeCollection(collection) diff --git a/packages/uikit/src/components/text.tsx b/packages/uikit/src/react/text.tsx similarity index 87% rename from packages/uikit/src/components/text.tsx rename to packages/uikit/src/react/text.tsx index 8063d8b3..2faeb92c 100644 --- a/packages/uikit/src/components/text.tsx +++ b/packages/uikit/src/react/text.tsx @@ -1,6 +1,6 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { YogaProperties } from '../flex/node.js' -import { useApplyHoverProperties } from '../hover.js' +import { applyHoverProperties } from '../hover.js' import { PanelProperties } from '../panel/instanced-panel.js' import { InteractionGroup, @@ -15,7 +15,7 @@ import { flexAliasPropertyTransformation, panelAliasPropertyTransformation, } from '../properties/alias.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' +import { WithClasses, addToMerged } from '../properties/default.js' import { WithReactive, createCollection, finalizeCollection, writeCollection } from '../properties/utils.js' import { ScrollListeners } from '../scroll.js' import { TransformProperties, useTransformMatrix } from '../transform.js' @@ -31,7 +31,7 @@ import { } from './utils.js' import { forwardRef, useRef } from 'react' import { useParentClippingRect, useIsClipped } from '../clipping.js' -import { useFlexNode } from '../flex/react.js' +import { useFlexNode } from './react.js' import { useImmediateProperties } from '../properties/immediate.js' import { InstancedTextProperties, useInstancedText } from '../text/react.js' import { ReadonlySignal } from '@preact/signals-core' @@ -39,8 +39,8 @@ import { useRootGroupRef } from '../utils.js' import { useApplyResponsiveProperties } from '../responsive.js' import { Group } from 'three' import { ElementType, ZIndexOffset, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' +import { applyPreferredColorSchemeProperties } from '../dark.js' +import { applyActiveProperties } from '../active.js' export type TextProperties = WithConditionals< WithClasses< @@ -97,11 +97,11 @@ export const Text = forwardRef< orderInfo, ) - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) + addToMerged(collection, properties) + applyPreferredColorSchemeProperties(collection, properties) useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) + const hoverHandlers = applyHoverProperties(collection, properties) + const activeHandlers = applyActiveProperties(collection, properties) writeCollection(collection, 'measureFunc', measureFunc) finalizeCollection(collection) diff --git a/packages/uikit/src/react/utils.ts b/packages/uikit/src/react/utils.ts new file mode 100644 index 00000000..c13a1a75 --- /dev/null +++ b/packages/uikit/src/react/utils.ts @@ -0,0 +1,97 @@ +import { ReadonlySignal, Signal, computed, effect } from '@preact/signals-core' +import { useMemo, useEffect, createContext, useContext, useImperativeHandle, ForwardedRef, RefObject } from 'react' +import { Group, Matrix4, Mesh, Vector2Tuple } from 'three' +import { FlexNode, Inset } from '../flex/node.js' +import { WithHover } from '../hover.js' +import { WithResponsive } from '../responsive.js' +import { WithPreferredColorScheme } from '../dark.js' +import { WithActive } from '../active.js' + +export type WithConditionals = WithHover & WithResponsive & WithPreferredColorScheme & WithActive + +export type ComponentInternals = { + pixelSize: number + size: ReadonlySignal + center: ReadonlySignal + borderInset: ReadonlySignal + paddingInset: ReadonlySignal + scrollPosition?: Signal + interactionPanel: Mesh +} + +export function useComponentInternals( + ref: ForwardedRef, + node: FlexNode, + interactionPanel: Mesh | RefObject, + scrollPosition?: Signal, +): void { + useImperativeHandle( + ref, + () => ({ + borderInset: node.borderInset, + paddingInset: node.paddingInset, + pixelSize: node.pixelSize, + center: node.relativeCenter, + size: node.size, + interactionPanel: interactionPanel instanceof Mesh ? interactionPanel : interactionPanel.current!, + scrollPosition, + }), + [interactionPanel, node, scrollPosition], + ) +} + +export type LayoutListeners = { + onSizeChange?: (width: number, height: number) => void +} + +export function useLayoutListeners({ onSizeChange }: LayoutListeners, size: Signal): void { + const unsubscribe = useMemo(() => { + if (onSizeChange == null) { + return undefined + } + let first = true + return effect(() => { + const s = size.value + if (first) { + first = false + return + } + onSizeChange(...s) + }) + }, [onSizeChange, size]) + useEffect(() => unsubscribe, [unsubscribe]) +} + +export type ViewportListeners = { + onIsInViewportChange?: (isInViewport: boolean) => void +} + +export function useViewportListeners({ onIsInViewportChange }: ViewportListeners, isClipped: Signal) { + const unsubscribe = useMemo(() => { + if (onIsInViewportChange == null) { + return undefined + } + let first = true + return effect(() => { + const isInViewport = !isClipped.value + if (first) { + first = false + return + } + onIsInViewportChange(isInViewport) + }) + }, [isClipped, onIsInViewportChange]) + useEffect(() => unsubscribe, [unsubscribe]) +} + +const MatrixContext = createContext>(null as any) + +export const MatrixProvider = MatrixContext.Provider + +const RootGroupRefContext = createContext>(null as any) + +export function useRootGroupRef() { + return useContext(RootGroupRefContext) +} + +export const RootGroupProvider = RootGroupRefContext.Provider diff --git a/packages/uikit/src/responsive.ts b/packages/uikit/src/responsive.ts index a4438221..a7c1fb5f 100644 --- a/packages/uikit/src/responsive.ts +++ b/packages/uikit/src/responsive.ts @@ -2,7 +2,7 @@ import { Signal } from '@preact/signals-core' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { createContext, useContext, useMemo } from 'react' import { ManagerCollection, Properties } from './properties/utils.js' -import { WithClasses, useTraverseProperties } from './properties/default.js' +import { AllOptionalProperties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' import { Vector2Tuple } from 'three' @@ -28,25 +28,21 @@ const RootSizeContext = createContext>(null as any) export const RootSizeProvider = RootSizeContext.Provider -export function useApplyResponsiveProperties( +export function applyResponsiveProperties( collection: ManagerCollection, + defaultProperties: AllOptionalProperties | undefined, properties: WithClasses> & EventHandlers, - providedSize?: Signal, + rootSize: Signal, ): void { - // eslint-disable-next-line react-hooks/rules-of-hooks - const size = providedSize ?? useContext(RootSizeContext) - const translator = useMemo( - () => ({ - sm: createConditionalPropertyTranslator(() => size.value[0] > breakPoints.sm), - md: createConditionalPropertyTranslator(() => size.value[0] > breakPoints.md), - lg: createConditionalPropertyTranslator(() => size.value[0] > breakPoints.lg), - xl: createConditionalPropertyTranslator(() => size.value[0] > breakPoints.xl), - '2xl': createConditionalPropertyTranslator(() => size.value[0] > breakPoints['2xl']), - }), - [size], - ) + const translator = { + sm: createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints.sm), + md: createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints.md), + lg: createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints.lg), + xl: createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints.xl), + '2xl': createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints['2xl']), + } - useTraverseProperties(properties, (p) => { + traverseProperties(defaultProperties, properties, (p) => { for (let i = 0; i < breakPointKeysLength; i++) { const key = breakPointKeys[i] as keyof typeof breakPoints const properties = p[key] diff --git a/packages/uikit/src/scroll.tsx b/packages/uikit/src/scroll.tsx index 7f6846a6..5b520203 100644 --- a/packages/uikit/src/scroll.tsx +++ b/packages/uikit/src/scroll.tsx @@ -2,22 +2,18 @@ import { ReadonlySignal, Signal, computed, effect, signal } from '@preact/signal import { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Group, Matrix4, MeshBasicMaterial, Vector2, Vector2Tuple, Vector3, Vector4Tuple } from 'three' -import { FlexNode, Inset } from './flex/node.js' +import { FlexNode, Inset, YogaProperties } from './flex/node.js' import { Color as ColorRepresentation, useFrame } from '@react-three/fiber' -import { useSignalEffect } from './utils.js' -import { - GetInstancedPanelGroup, - MaterialClass, - PanelGroupDependencies, - useInstancedPanel, - usePanelGroupDependencies, -} from './panel/react.js' +import { Subscriptions, useSignalEffect } from './utils.js' +import { GetInstancedPanelGroup, MaterialClass, PanelGroupDependencies } from './panel/react.js' import { ClippingRect } from './clipping.js' import { clamp } from 'three/src/math/MathUtils.js' -import { PanelProperties } from './panel/instanced-panel.js' +import { InstancedPanel, PanelProperties } from './panel/instanced-panel.js' import { borderAliasPropertyTransformation, panelAliasPropertyTransformation } from './properties/alias.js' -import { ManagerCollection, PropertyTransformation, WithReactive, useGetBatchedProperties } from './properties/utils.js' -import { ElementType, OrderInfo, useOrderInfo } from './order.js' +import { PropertyKeyTransformation, WithReactive } from './properties/utils.js' +import { ElementType, OrderInfo, computeOrderInfo } from './order.js' +import { createGetBatchedProperties } from './properties/batched.js' +import { MergedProperties } from './properties/merged.js' const distanceHelper = new Vector3() const localPointHelper = new Vector3() @@ -31,28 +27,24 @@ export type ScrollListeners = { onScroll?: (scrollX: number, scrollY: number, event?: ThreeEvent) => void } -export function useScrollPosition() { - return useMemo(() => signal([0, 0]), []) +export function createScrollPosition() { + return signal([0, 0]) } -export function useGlobalScrollMatrix( +export function computeGlobalScrollMatrix( scrollPosition: Signal, node: FlexNode, 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(global) - }), - [scrollPosition, node, globalMatrix], - ) + return 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(global) + }) } export function ScrollGroup({ @@ -288,6 +280,10 @@ export type ScrollbarProperties = { scrollbarWidth?: number scrollbarOpacity?: number scrollbarColor?: ColorRepresentation + scrollbarBorderRight?: number + scrollbarBorderTop?: ColorRepresentation + scrollbarBorderLeft?: ColorRepresentation + scrollbarBorderBottom?: ColorRepresentation } & { [Key in `scrollbar${Capitalize< keyof Omit @@ -302,40 +298,41 @@ function removeScrollbar(key: string) { return firstKeyUncapitalized + key.slice(scrollbarLength + 1) } -const scrollbarBorderPropertyTransformation: PropertyTransformation = (key, value, hasProperty, setProperty) => { - if (!key.startsWith('scrollbarBorder')) { - return - } - key = removeScrollbar(key) - if (hasProperty(key)) { - setProperty(key, value) - return - } - borderAliasPropertyTransformation(key, value, hasProperty, setProperty) -} - -const scrollbarPanelPropertyTransformation: PropertyTransformation = (key, value, hasProperty, setProperty) => { - if (!key.startsWith('scrollbar')) { - return - } +const scrollbarPanelPropertyTransformation: PropertyKeyTransformation = (key, value, setProperty) => { if (key === 'scrollbarOpacity') { setProperty('backgroundOpacity', value) - return + return true } if (key === 'scrollbarColor') { setProperty('backgroundColor', value) - return + return true + } + if(!key.startsWith("scrollbar")) { + } key = removeScrollbar(key) - if (hasProperty(key)) { - setProperty(key, value) + if (panelAliasPropertyTransformation.hasProperty(key)) { + panelAliasPropertyTransformation.setProperty(key, value, setProperty) return } - panelAliasPropertyTransformation(key, value, hasProperty, setProperty) + setProperty(key, value) +} + +function isScrollbarWidthPropertyKey(key: string) { + return key === 'scrollbarWidth' +} +const borderPropertyKeys = [ + 'scrollbarBorderLeft', + 'scrollbarBorderRight', + 'scrollbarBorderTop', + 'scrollbarBorderBottom', +] as const +function isBorderPropertyKey(key: (typeof borderPropertyKeys)[number]) { + return borderPropertyKeys.includes(key) } -export function useScrollbars( - collection: ManagerCollection, +export function createScrollbars( + propertiesSignal: Signal, scrollPosition: Signal, node: FlexNode, globalMatrix: Signal, @@ -343,36 +340,35 @@ export function useScrollbars( materialClass: MaterialClass | undefined, parentClippingRect: Signal | undefined, orderInfo: OrderInfo, - providedGetGroup?: GetInstancedPanelGroup, + getGroup: GetInstancedPanelGroup, + subscriptions: Subscriptions, ): void { - const groupDeps = usePanelGroupDependencies(materialClass, { castShadow: false, receiveShadow: false }) - const scrollbarOrderInfo = useOrderInfo(ElementType.Panel, undefined, groupDeps, orderInfo) + const groupDeps: PanelGroupDependencies = { + materialClass: materialClass ?? MeshBasicMaterial, + castShadow: false, + receiveShadow: false, + } + const scrollbarOrderInfo = computeOrderInfo(ElementType.Panel, undefined, groupDeps, orderInfo) - const getScrollbarWidthSignal = useGetBatchedProperties<{ scrollbarWidth?: number }>(collection, propertyKeys) - const getBorderSignal = useGetBatchedProperties<{ + const getScrollbarWidth = createGetBatchedProperties<{ scrollbarWidth?: number }>( + propertiesSignal, + isScrollbarWidthPropertyKey, + ) + const getBorder = createGetBatchedProperties<{ scrollbarBorderLeft?: number scrollbarBorderRight?: number scrollbarBorderBottom?: number scrollbarBorderTop?: number - }>(collection, borderPropertyKeys, scrollbarBorderPropertyTransformation) - const borderSize = useMemo( - () => - computed(() => { - const get = getBorderSignal.value - return [ - get?.('scrollbarBorderTop') ?? 0, - get?.('scrollbarBorderRight') ?? 0, - get?.('scrollbarBorderBottom') ?? 0, - get?.('scrollbarBorderLeft') ?? 0, - ] - }), - [getBorderSignal], - ) - - const startIndex = collection.length - - useScrollbar( - collection, + }>(propertiesSignal, isBorderPropertyKey, scrollbarBorderPropertyTransformation) + const borderSize = computed(() => [ + getBorder('borderTop') ?? 0, + getBorder('borderRight') ?? 0, + getBorder('borderBottom') ?? 0, + getBorder('borderLeft') ?? 0, + ]) + + createScrollbar( + propertiesSignal, 0, scrollPosition, node, @@ -381,12 +377,13 @@ export function useScrollbars( materialClass, parentClippingRect, scrollbarOrderInfo, - providedGetGroup, - getScrollbarWidthSignal, + getGroup, + getScrollbarWidth, borderSize, + subscriptions, ) - useScrollbar( - collection, + createScrollbar( + propertiesSignal, 1, scrollPosition, node, @@ -395,29 +392,22 @@ export function useScrollbars( materialClass, parentClippingRect, scrollbarOrderInfo, - providedGetGroup, - getScrollbarWidthSignal, + getGroup, + getScrollbarWidth, borderSize, + subscriptions, ) - //setting the scrollbar color and opacity default for all property managers of the instanced panel - const collectionLength = collection.length + //TODO: setting the scrollbar color and opacity default for all property managers of the instanced panel + /*const collectionLength = collection.length for (let i = startIndex; i < collectionLength; i++) { collection[i].add('scrollbarColor', 0xffffff) collection[i].add('scrollbarOpacity', 1) - } + }*/ } -const propertyKeys = ['scrollbarWidth'] as const -const borderPropertyKeys = [ - 'scrollbarBorderLeft', - 'scrollbarBorderRight', - 'scrollbarBorderTop', - 'scrollbarBorderBottom', -] as const - -function useScrollbar( - collection: ManagerCollection, +function createScrollbar( + propertiesSignal: Signal, mainIndex: number, scrollPosition: Signal, node: FlexNode, @@ -426,47 +416,37 @@ function useScrollbar( materialClass: MaterialClass | undefined, parentClippingRect: Signal | undefined, orderInfo: OrderInfo, - providedGetGroup: GetInstancedPanelGroup | undefined, - getScrollbarWidthSignal: Signal number | undefined)>, + getGroup: GetInstancedPanelGroup, + get: (key: 'scrollbarWidth') => number | undefined, borderSize: ReadonlySignal, + subscriptions: Subscriptions, ) { - const [scrollbarPosition, scrollbarSize] = useMemo(() => { - const scrollbarTransformation = computed(() => { - const get = getScrollbarWidthSignal.value - if (get == null) { - return undefined - } - return computeScrollbarTransformation( - mainIndex, - get('scrollbarWidth') ?? 10, - node.size.value, - node.maxScrollPosition.value, - node.borderInset.value, - scrollPosition.value, - ) - }) - return [ - computed(() => (scrollbarTransformation.value?.slice(0, 2) ?? [0, 0]) as Vector2Tuple), - computed(() => (scrollbarTransformation.value?.slice(2, 4) ?? [0, 0]) as Vector2Tuple), - ] - }, [mainIndex, node, scrollPosition, getScrollbarWidthSignal]) - - const groupDeps = useMemo( - () => ({ materialClass: materialClass ?? MeshBasicMaterial, receiveShadow: false, castShadow: false }), - [materialClass], - ) - useInstancedPanel( - collection, + const scrollbarTransformation = computed(() => { + return computeScrollbarTransformation( + mainIndex, + get('scrollbarWidth') ?? 10, + node.size.value, + node.maxScrollPosition.value, + node.borderInset.value, + scrollPosition.value, + ) + }) + const scrollbarPosition = computed(() => (scrollbarTransformation.value?.slice(0, 2) ?? [0, 0]) as Vector2Tuple) + const scrollbarSize = computed(() => (scrollbarTransformation.value?.slice(2, 4) ?? [0, 0]) as Vector2Tuple) + + new InstancedPanel( + propertiesSignal, + getGroup, + orderInfo, + { materialClass: materialClass ?? MeshBasicMaterial, receiveShadow: false, castShadow: false }, globalMatrix, scrollbarSize, scrollbarPosition, borderSize, - isClipped, - orderInfo, parentClippingRect, - groupDeps, + isClipped, + subscriptions, scrollbarPanelPropertyTransformation, - providedGetGroup, ) } diff --git a/packages/uikit/src/transform.ts b/packages/uikit/src/transform.ts index 285dda07..eaf1f5ae 100644 --- a/packages/uikit/src/transform.ts +++ b/packages/uikit/src/transform.ts @@ -1,10 +1,10 @@ import { Signal, computed } from '@preact/signals-core' -import { useMemo } from 'react' import { Euler, Matrix4, Quaternion, Vector3, Vector3Tuple } from 'three' import { FlexNode } from './flex/node.js' import { alignmentXMap, alignmentYMap } from './utils.js' -import { ManagerCollection, useGetBatchedProperties } from './properties/utils.js' import { transformAliasPropertyTransformation } from './properties/alias.js' +import { createGetBatchedProperties } from './properties/batched.js' +import { MergedProperties } from './properties/merged.js' export type TransformProperties = { transformTranslateX?: number @@ -49,58 +49,49 @@ 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 computeTransformMatrix( + propertiesSignal: Signal, + node: FlexNode, +): Signal { //B * O^-1 * T * O //B = bound transformation matrix //O = matrix to transform the origin for matrix T //T = transform matrix (translate, rotate, scale) - const getPropertySignal = useGetBatchedProperties( - collection, - propertyKeys, + const get = createGetBatchedProperties( + propertiesSignal, + (key) => propertyKeys.includes(key), transformAliasPropertyTransformation, ) - 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 tOriginX = get('transformOriginX') ?? 'center' - const tOriginY = get('transformOriginY') ?? 'center' - let originCenter = true - - if (tOriginX != 'center' || tOriginY != 'center') { - const [width, height] = node.size.value - originCenter = false - originVector.set( - -alignmentXMap[tOriginX] * width * pixelSize, - -alignmentYMap[tOriginY] * height * pixelSize, - 0, - ) - result.multiply(matrixHelper.makeTranslation(originVector)) - originVector.negate() - } - - const r: Vector3Tuple = [get(rX) ?? 0, get(rY) ?? 0, get(rZ) ?? 0] - const t: Vector3Tuple = [get(tX) ?? 0, -(get(tY) ?? 0), get(tZ) ?? 0] - const s: Vector3Tuple = [get(sX) ?? 1, get(sY) ?? 1, get(sZ) ?? 1] - if (t.some((v) => v != 0) || r.some((v) => v != 0) || s.some((v) => v != 1)) { - result.multiply( - matrixHelper.compose(tHelper.fromArray(t).multiplyScalar(pixelSize), toQuaternion(r), sHelper.fromArray(s)), - ) - } - - if (!originCenter) { - result.multiply(matrixHelper.makeTranslation(originVector)) - } - - return result - }), - [getPropertySignal, node], - ) + return computed(() => { + const { pixelSize, relativeCenter } = node + const [x, y] = relativeCenter.value + const result = new Matrix4().makeTranslation(x * pixelSize, y * pixelSize, 0) + + const tOriginX = get('transformOriginX') ?? 'center' + const tOriginY = get('transformOriginY') ?? 'center' + let originCenter = true + + if (tOriginX != 'center' || tOriginY != 'center') { + const [width, height] = node.size.value + originCenter = false + originVector.set(-alignmentXMap[tOriginX] * width * pixelSize, -alignmentYMap[tOriginY] * height * pixelSize, 0) + result.multiply(matrixHelper.makeTranslation(originVector)) + originVector.negate() + } + + const r: Vector3Tuple = [get(rX) ?? 0, get(rY) ?? 0, get(rZ) ?? 0] + const t: Vector3Tuple = [get(tX) ?? 0, -(get(tY) ?? 0), get(tZ) ?? 0] + const s: Vector3Tuple = [get(sX) ?? 1, get(sY) ?? 1, get(sZ) ?? 1] + if (t.some((v) => v != 0) || r.some((v) => v != 0) || s.some((v) => v != 1)) { + result.multiply( + matrixHelper.compose(tHelper.fromArray(t).multiplyScalar(pixelSize), toQuaternion(r), sHelper.fromArray(s)), + ) + } + + if (!originCenter) { + result.multiply(matrixHelper.makeTranslation(originVector)) + } + + return result + }) } diff --git a/packages/uikit/src/utils.tsx b/packages/uikit/src/utils.tsx index 57c7ade0..a4d00860 100644 --- a/packages/uikit/src/utils.tsx +++ b/packages/uikit/src/utils.tsx @@ -3,8 +3,18 @@ import { computed, effect, Signal, signal } from '@preact/signals-core' import { Vector2Tuple, BufferAttribute, Color, Group } from 'three' import { Color as ColorRepresentation } from '@react-three/fiber' import { Inset } from './flex/node.js' -import { ManagerCollection, Properties } from './properties/utils.js' +import { Properties } from './properties/utils.js' import { Yoga, loadYoga } from 'yoga-layout/wasm-async' +import { MergedProperties } from './properties/merged.js' + +export type Subscriptions = Array<() => void> + +export function unsubscribeSubscriptions(subscriptions: Subscriptions): void { + const length = subscriptions.length + for (let i = 0; i < length; i++) { + subscriptions[i]() + } +} export const alignmentXMap = { left: 0.5, center: 0, right: -0.5 } export const alignmentYMap = { top: -0.5, center: 0, bottom: 0.5 } @@ -98,20 +108,9 @@ export function readReactive(value: T | Signal): T { return value instanceof Signal ? value.value : value } -const RootGroupRefContext = createContext>(null as any) - -export function useRootGroupRef() { - return useContext(RootGroupRefContext) -} - -export const RootGroupProvider = RootGroupRefContext.Provider - -export function createConditionalPropertyTranslator( - condition: () => boolean, -): (collection: ManagerCollection, properties: Properties) => void { +export function createConditionalPropertyTranslator(condition: () => boolean) { const signalMap = new Map>() - return (collection, properties) => { - const collectionLength = collection.length + return (merged: MergedProperties, properties: Properties) => { for (const key in properties) { const value = properties[key] if (value === undefined) { @@ -121,9 +120,7 @@ export function createConditionalPropertyTranslator( if (result == null) { signalMap.set(value, (result = computed(() => (condition() ? readReactive(value) : undefined)))) } - for (let i = 0; i < collectionLength; i++) { - collection[i].add(key, result) - } + merged.add(key, result) } } } From b49b08c15c63b1a85659e0c2862f24e42afd17d3 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Tue, 12 Mar 2024 09:46:28 +0100 Subject: [PATCH 02/20] continue to remove react code --- .../{uikit => react}/src/cli/component/add.ts | 0 .../src/cli/component/index.ts | 0 packages/{uikit => react}/src/cli/index.ts | 0 packages/react/src/index.ts | 15 + packages/react/src/order.ts | 3 + packages/react/src/responsive.ts | 7 + packages/react/src/text.ts | 7 + packages/react/src/utils.ts | 29 ++ packages/uikit/src/active.ts | 3 +- packages/uikit/src/clipping.ts | 37 +-- packages/uikit/src/components/container.ts | 173 +++++----- packages/uikit/src/dark.ts | 7 +- packages/uikit/src/flex/node.ts | 4 +- packages/uikit/src/hover.ts | 3 +- packages/uikit/src/layer.tsx | 1 - packages/uikit/src/listeners.ts | 47 +++ packages/uikit/src/order.ts | 91 +++--- packages/uikit/src/panel/instanced-panel.ts | 26 +- .../uikit/src/panel/interaction-panel-mesh.ts | 9 +- packages/uikit/src/panel/panel-material.ts | 6 +- .../uikit/src/panel/{react.tsx => react.ts} | 53 ++- packages/uikit/src/properties/batched.ts | 5 +- .../properties/{default.tsx => default.ts} | 0 packages/uikit/src/react/utils.ts | 44 --- packages/uikit/src/responsive.ts | 17 +- packages/uikit/src/{scroll.tsx => scroll.ts} | 305 +++++++----------- packages/uikit/src/text/react.ts | 253 +++++++++++++++ packages/uikit/src/text/react.tsx | 278 ---------------- .../uikit/src/text/render/instanced-text.ts | 31 +- packages/uikit/src/transform.ts | 20 +- packages/uikit/src/{utils.tsx => utils.ts} | 41 +-- 31 files changed, 728 insertions(+), 787 deletions(-) rename packages/{uikit => react}/src/cli/component/add.ts (100%) rename packages/{uikit => react}/src/cli/component/index.ts (100%) rename packages/{uikit => react}/src/cli/index.ts (100%) create mode 100644 packages/react/src/index.ts create mode 100644 packages/react/src/order.ts create mode 100644 packages/react/src/responsive.ts create mode 100644 packages/react/src/text.ts create mode 100644 packages/react/src/utils.ts delete mode 100644 packages/uikit/src/layer.tsx create mode 100644 packages/uikit/src/listeners.ts rename packages/uikit/src/panel/{react.tsx => react.ts} (74%) rename packages/uikit/src/properties/{default.tsx => default.ts} (100%) rename packages/uikit/src/{scroll.tsx => scroll.ts} (58%) create mode 100644 packages/uikit/src/text/react.ts delete mode 100644 packages/uikit/src/text/react.tsx rename packages/uikit/src/{utils.tsx => utils.ts} (67%) diff --git a/packages/uikit/src/cli/component/add.ts b/packages/react/src/cli/component/add.ts similarity index 100% rename from packages/uikit/src/cli/component/add.ts rename to packages/react/src/cli/component/add.ts diff --git a/packages/uikit/src/cli/component/index.ts b/packages/react/src/cli/component/index.ts similarity index 100% rename from packages/uikit/src/cli/component/index.ts rename to packages/react/src/cli/component/index.ts diff --git a/packages/uikit/src/cli/index.ts b/packages/react/src/cli/index.ts similarity index 100% rename from packages/uikit/src/cli/index.ts rename to packages/react/src/cli/index.ts diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 00000000..8fed1ea2 --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,15 @@ +export { + basedOnPreferredColorScheme, + setPreferredColorScheme, + getPreferredColorScheme, + type PreferredColorScheme, +} from './dark.js' +export { FontFamilyProvider } from './text/react.js' +export { useRootSize } from './responsive.js' +export type { ComponentInternals } from './components/utils.js' +export type { MaterialClass } from './panel/react.js' +export type { LayoutListeners, ViewportListeners } from './components/utils.js' +export type { ScrollListeners } from './scroll.js' +export type { AllOptionalProperties } from './properties/default.js' +export { DefaultProperties } from './properties/default.js' +export * from './components/index.js' diff --git a/packages/react/src/order.ts b/packages/react/src/order.ts new file mode 100644 index 00000000..4fed00eb --- /dev/null +++ b/packages/react/src/order.ts @@ -0,0 +1,3 @@ +export const OrderInfoContext = createContext(null as any) + +export const OrderInfoProvider = OrderInfoContext.Provider \ No newline at end of file diff --git a/packages/react/src/responsive.ts b/packages/react/src/responsive.ts new file mode 100644 index 00000000..9dd8c21b --- /dev/null +++ b/packages/react/src/responsive.ts @@ -0,0 +1,7 @@ +export function useRootSize() { + return useContext(RootSizeContext) +} + +const RootSizeContext = createContext>(null as any) + +export const RootSizeProvider = RootSizeContext.Provider diff --git a/packages/react/src/text.ts b/packages/react/src/text.ts new file mode 100644 index 00000000..d0cca855 --- /dev/null +++ b/packages/react/src/text.ts @@ -0,0 +1,7 @@ + + +const InstancedGlyphContext = createContext(null as any) + +export const InstancedGlyphProvider = InstancedGlyphContext.Provider + +const FontFamiliesContext = createContext>(null as any) \ No newline at end of file diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts new file mode 100644 index 00000000..8f414fdb --- /dev/null +++ b/packages/react/src/utils.ts @@ -0,0 +1,29 @@ +export function useSignalEffect(fn: () => (() => void) | void, deps: Array) { + // eslint-disable-next-line react-hooks/exhaustive-deps + const unsubscribe = useMemo(() => effect(fn), deps) + useEffect(() => unsubscribe, [unsubscribe]) +} + +export function useResourceWithParams>( + fn: (param: P, ...additional: A) => Promise, + param: Signal

| P, + ...additionals: A +): Signal { + const result = useMemo(() => signal(undefined), []) + useEffect(() => { + if (!(param instanceof Signal)) { + let canceled = false + fn(param, ...additionals).then((value) => (canceled ? undefined : (result.value = value))) + return () => (canceled = true) + } + return effect(() => { + let canceled = false + fn(param.value, ...additionals) + .then((value) => (canceled ? undefined : (result.value = value))) + .catch(console.error) + return () => (canceled = true) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [param, ...additionals]) + return result +} diff --git a/packages/uikit/src/active.ts b/packages/uikit/src/active.ts index b0cf908e..cafacf68 100644 --- a/packages/uikit/src/active.ts +++ b/packages/uikit/src/active.ts @@ -1,7 +1,6 @@ import { signal } from '@preact/signals-core' import type { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { Properties } from './properties/utils.js' -import { AllOptionalProperties, WithClasses, traverseProperties } from './properties/default.js' +import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' import { MergedProperties } from './properties/merged.js' diff --git a/packages/uikit/src/clipping.ts b/packages/uikit/src/clipping.ts index d3856101..6890f9da 100644 --- a/packages/uikit/src/clipping.ts +++ b/packages/uikit/src/clipping.ts @@ -1,6 +1,4 @@ import { Signal, computed } from '@preact/signals-core' -import { useFrame } from '@react-three/fiber' -import { RefObject, createContext, useContext, useMemo } from 'react' import { Group, Matrix4, Plane, Vector3 } from 'three' import type { Vector2Tuple } from 'three' import { Inset } from './flex/node.js' @@ -169,26 +167,23 @@ for (let i = 0; i < 4; i++) { defaultClippingData[i * 4 + 3] = NoClippingPlane.constant } -export function useGlobalClippingPlanes( +export function createGlobalClippingPlanes() { + return [new Plane(), new Plane(), new Plane(), new Plane()] +} + +export function updateGlobalClippingPlanes( clippingRect: Signal | undefined, - rootGroupRef: RefObject, -): Array { - const clippingPlanes = useMemo>(() => [new Plane(), new Plane(), new Plane(), new Plane()], []) - useFrame(() => { - const rootGroup = rootGroupRef.current - if (rootGroup == null) { - return - } - const localPlanes = clippingRect?.value?.planes - if (localPlanes == null) { - for (let i = 0; i < 4; i++) { - clippingPlanes[i].copy(NoClippingPlane) - } - return - } + rootGroup: Group, + clippingPlanes: Array, +): void { + const localPlanes = clippingRect?.value?.planes + if (localPlanes == null) { for (let i = 0; i < 4; i++) { - clippingPlanes[i].copy(localPlanes[i]).applyMatrix4(rootGroup.matrixWorld) + clippingPlanes[i].copy(NoClippingPlane) } - }) - return clippingPlanes + return + } + for (let i = 0; i < 4; i++) { + clippingPlanes[i].copy(localPlanes[i]).applyMatrix4(rootGroup.matrixWorld) + } } diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 8a680990..97af2375 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -1,23 +1,28 @@ -import { RefObject } from 'react' import { WithReactive } from '../properties/utils.js' -import { PanelGroupDependencies, createInteractionPanel } from '../panel/react.js' -import { FlexNode, YogaProperties } from '../flex/node.js' +import { + PanelGroupDependencies, + computePanelGroupDependencies, + createInstancePanel, + createInteractionPanel, +} from '../panel/react.js' +import { YogaProperties } from '../flex/node.js' import { applyHoverProperties } from '../hover.js' -import { ClippingRect, computeIsClipped, computeClippingRect } from '../clipping.js' +import { computeIsClipped, computeClippingRect } from '../clipping.js' import { ScrollbarProperties, computeGlobalScrollMatrix, createScrollPosition, createScrollbars } from '../scroll.js' -import { WithAllAliases, panelAliasPropertyTransformation } from '../properties/alias.js' +import { WithAllAliases } from '../properties/alias.js' import { InstancedPanel, PanelProperties } from '../panel/instanced-panel.js' import { TransformProperties, computeTransformMatrix } from '../transform.js' -import { AllOptionalProperties, WithClasses } from '../properties/default.js' +import { Properties, WithClasses } from '../properties/default.js' import { applyResponsiveProperties } from '../responsive.js' -import { Group, Matrix4, Vector2Tuple } from 'three' import { ElementType, computeOrderInfo } from '../order.js' import { applyPreferredColorSchemeProperties } from '../dark.js' import { applyActiveProperties } from '../active.js' -import { Signal } from '@preact/signals-core' +import { Signal, signal } from '@preact/signals-core' import { computeGlobalMatrix } from './utils.js' import { WithConditionals } from '../react/utils.js' import { Subscriptions } from '../utils.js' +import { MergedProperties } from '../properties/merged.js' +import { LayoutListeners, ViewportListeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' export type ContainerProperties = WithConditionals< WithClasses< @@ -25,84 +30,98 @@ export type ContainerProperties = WithConditionals< > > -export function createContainer( - parentNode: FlexNode, - parentClippingRect: Signal | undefined, - parentMatrix: Signal, - groupRef: RefObject, - defaultProperties: AllOptionalProperties | undefined, - rootSize: Signal, - rootGroupRef: RefObject, -): () => void { - const subscriptions: Subscriptions = [] - const node = parentNode.createChild(propertiesSignal, groupRef, subscriptions) - parentNode.addChild(node) +export class Container { + //undefined as any is okay here since the value of the signal will be overwritten before its use + private propertiesSignal: Signal = signal(undefined as any) + private subscriptions: Subscriptions = [] - const transformMatrix = computeTransformMatrix(node, propertiesSignal) - const globalMatrix = computeGlobalMatrix(parentMatrix, transformMatrix) - const isClipped = computeIsClipped(parentClippingRect, globalMatrix, node.size, node) - const groupDeps: PanelGroupDependencies = { - materialClass: properties.panelMaterialClass, - castShadow: properties.castShadow, - receiveShadow: properties.receiveShadow, - } + private listeners: LayoutListeners & ViewportListeners = {} + + constructor(properties: Properties, defaultProperties?: Properties) { + this.setProperties(properties, defaultProperties) + + //setup the container + const node = parentNode.createChild(this.propertiesSignal, groupRef, subscriptions) + parentNode.addChild(node) - const orderInfo = computeOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) + const transformMatrix = computeTransformMatrix(this.propertiesSignal, node) + const globalMatrix = computeGlobalMatrix(parentMatrix, transformMatrix) + const isClipped = computeIsClipped(parentClippingRect, globalMatrix, node.size, node) + const groupDeps = computePanelGroupDependencies(this.propertiesSignal) - new InstancedPanel( - propertiesSignal, - getInstancedPanelGroup, - orderInfo, - panelGroupDependencies, - globalMatrix, - node.size, - undefined, - node.borderInset, - parentClippingRect, - isClipped, - subscriptions, - panelAliasPropertyTransformation, - ) + const orderInfo = computeOrderInfo(this.propertiesSignal, ElementType.Panel, groupDeps, parentOrderInfo) - const scrollPosition = createScrollPosition() - const globalScrollMatrix = computeGlobalScrollMatrix(scrollPosition, node, globalMatrix) - createScrollbars( - collection, - scrollPosition, - node, - globalMatrix, - isClipped, - properties.scrollbarPanelMaterialClass, - parentClippingRect, - orderInfo, - ) + createInstancePanel( + this.propertiesSignal, + orderInfo, + groupDeps, + getInstancedPanelGroup, + globalMatrix, + node.size, + undefined, + node.borderInset, + parentClippingRect, + isClipped, + undefined, + this.subscriptions, + ) - //apply all properties - addToMerged(collection, defaultProperties, properties) - applyPreferredColorSchemeProperties(collection, defaultProperties, properties) - applyResponsiveProperties(collection, defaultProperties, properties, rootSize) - const hoverHandlers = applyHoverProperties(collection, defaultProperties, properties) - const activeHandlers = applyActiveProperties(collection, defaultProperties, properties) - finalizeCollection(collection) + const scrollPosition = createScrollPosition() + const globalScrollMatrix = computeGlobalScrollMatrix(scrollPosition, node, globalMatrix) + createScrollbars( + this.propertiesSignal, + scrollPosition, + node, + globalMatrix, + isClipped, + properties.scrollbarPanelMaterialClass, + parentClippingRect, + orderInfo, + getInstancedPanelGroup, + this.subscriptions, + ) - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) + const clippingRect = computeClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + node, + parentClippingRect, + ) - const clippingRect = computeClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - node, - parentClippingRect, - ) + const interactionPanel = createInteractionPanel( + node.size, + node, + orderInfo, + parentClippingRect, + rootGroupRef, + this.subscriptions, + ) - const interactionPanel = createInteractionPanel(node.size, node, orderInfo, rootGroupRef) + setupLayoutListeners(this.listeners, node.size, this.subscriptions) + setupViewportListeners(this.listeners, isClipped, this.subscriptions) - useComponentInternals(ref, node, interactionPanel, scrollPosition) + this.subscriptions.push(() => { + parentNode.removeChild(node) + node.destroy() + }) + } + + setProperties(properties: Properties, defaultProperties?: Properties): void { + const merged = new MergedProperties() + addToMerged(collection, defaultProperties, properties) + applyPreferredColorSchemeProperties(collection, defaultProperties, properties) + applyResponsiveProperties(collection, defaultProperties, properties, rootSize) + const hoverHandlers = applyHoverProperties(collection, defaultProperties, properties) + const activeHandlers = applyActiveProperties(collection, defaultProperties, properties) + this.propertiesSignal.value = merged + } - return () => { - parentNode.removeChild(node) - node.destroy() + destroy() { + const subscriptionsLength = this.subscriptions.length + for (let i = 0; i < subscriptionsLength; i++) { + this.subscriptions[i]() + } } } diff --git a/packages/uikit/src/dark.ts b/packages/uikit/src/dark.ts index 82ce75c5..0cd50c9f 100644 --- a/packages/uikit/src/dark.ts +++ b/packages/uikit/src/dark.ts @@ -1,10 +1,11 @@ import { ReadonlySignal, computed, signal } from '@preact/signals-core' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { Properties } from './properties/utils.js' -import { AllOptionalProperties, WithClasses, traverseProperties } from './properties/default.js' +import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' -import { Color as ColorRepresentation } from '@react-three/fiber' import { MergedProperties } from './properties/merged.js' +import { Color, Vector3Tuple } from 'three' + +export type ColorRepresentation = Color | string | number | Vector3Tuple export type WithPreferredColorScheme = { dark?: T } & T diff --git a/packages/uikit/src/flex/node.ts b/packages/uikit/src/flex/node.ts index 07808477..b4118426 100644 --- a/packages/uikit/src/flex/node.ts +++ b/packages/uikit/src/flex/node.ts @@ -7,7 +7,6 @@ import { CameraDistanceRef } from '../order.js' import { Subscriptions } from '../utils.js' import { setupImmediateProperties } from '../properties/immediate.js' import { MergedProperties } from '../properties/merged.js' -import { flexAliasPropertyTransformation } from '../properties/alias.js' export type YogaProperties = { [Key in keyof typeof setter]?: Parameters<(typeof setter)[Key]>[2] @@ -51,6 +50,7 @@ export class FlexNode { requestCalculateLayout: (node: FlexNode) => void, public readonly anyAncestorScrollable: Signal<[boolean, boolean]> | undefined, subscriptions: Subscriptions, + renameOutput?: Record, ) { this.requestCalculateLayout = () => requestCalculateLayout(this) this.unsubscribeYoga = effect(() => { @@ -75,7 +75,7 @@ export class FlexNode { this.requestCalculateLayout() }, subscriptions, - flexAliasPropertyTransformation, + renameOutput, ) } diff --git a/packages/uikit/src/hover.ts b/packages/uikit/src/hover.ts index 1649cb0b..89abe76d 100644 --- a/packages/uikit/src/hover.ts +++ b/packages/uikit/src/hover.ts @@ -1,8 +1,7 @@ import { signal } from '@preact/signals-core' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' import { setCursorType, unsetCursorType } from './cursor.js' -import { Properties } from './properties/utils.js' -import { AllOptionalProperties, WithClasses, traverseProperties } from './properties/default.js' +import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' import { Subscriptions, createConditionalPropertyTranslator } from './utils.js' import { MergedProperties } from './properties/merged.js' diff --git a/packages/uikit/src/layer.tsx b/packages/uikit/src/layer.tsx deleted file mode 100644 index bc2425bc..00000000 --- a/packages/uikit/src/layer.tsx +++ /dev/null @@ -1 +0,0 @@ -//used to force an order for instanced elements with different materials diff --git a/packages/uikit/src/listeners.ts b/packages/uikit/src/listeners.ts new file mode 100644 index 00000000..4327d26a --- /dev/null +++ b/packages/uikit/src/listeners.ts @@ -0,0 +1,47 @@ +import { Signal, effect } from '@preact/signals-core' +import { Vector2Tuple } from 'three' +import { Subscriptions } from './utils' + +export type LayoutListeners = { + onSizeChange?: (width: number, height: number) => void +} + +export function setupLayoutListeners( + listeners: LayoutListeners, + size: Signal, + subscriptions: Subscriptions, +) { + let first = true + subscriptions.push( + effect(() => { + const s = size.value + if (first) { + first = false + return + } + listeners.onSizeChange?.(...s) + }), + ) +} + +export type ViewportListeners = { + onIsInViewportChange?: (isInViewport: boolean) => void +} + +export function setupViewportListeners( + listeners: ViewportListeners, + isClipped: Signal, + subscriptions: Subscriptions, +) { + let first = true + subscriptions.push( + effect(() => { + const isInViewport = !isClipped.value + if (first) { + first = false + return + } + listeners.onIsInViewportChange?.(isInViewport) + }), + ) +} diff --git a/packages/uikit/src/order.ts b/packages/uikit/src/order.ts index 5c40e4b9..6d9e4815 100644 --- a/packages/uikit/src/order.ts +++ b/packages/uikit/src/order.ts @@ -1,5 +1,7 @@ -import { createContext, useContext, useMemo } from 'react' +import { Signal, computed } from '@preact/signals-core' import { RenderItem, WebGLRenderer } from 'three' +import { MergedProperties } from './properties/merged' +import { createGetBatchedProperties } from './properties/batched' export type CameraDistanceRef = { current: number } @@ -60,56 +62,59 @@ function compareOrderInfo(o1: OrderInfo, o2: OrderInfo): number { return o1.minorIndex - o2.minorIndex } -export const OrderInfoContext = createContext(null as any) - -export const OrderInfoProvider = OrderInfoContext.Provider - export type ZIndexOffset = { major?: number; minor?: number } | number +const propertyKeys = ['zIndexOffset'] + export function computeOrderInfo( + propertiesSignal: Signal, type: ElementType, - offset: ZIndexOffset | undefined, instancedGroupDependencies: Record | undefined, - providedParentOrderInfo?: OrderInfo, -): OrderInfo { - // eslint-disable-next-line react-hooks/rules-of-hooks - const parentOrderInfo = providedParentOrderInfo ?? (useContext(OrderInfoContext) as OrderInfo | undefined) - const majorOffset = typeof offset === 'number' ? offset : offset?.major ?? 0 - const minorOffset = typeof offset === 'number' ? 0 : offset?.minor ?? 0 - - let majorIndex: number - let minorIndex: number - - if (parentOrderInfo == null) { - majorIndex = 0 - minorIndex = 0 - } else if (type > parentOrderInfo.elementType) { - majorIndex = parentOrderInfo.majorIndex - minorIndex = 0 - } else if ( - type != parentOrderInfo.elementType || - !shallowEqualRecord(instancedGroupDependencies, parentOrderInfo.instancedGroupDependencies) - ) { - majorIndex = parentOrderInfo.majorIndex + 1 - minorIndex = 0 - } else { - majorIndex = parentOrderInfo.majorIndex - minorIndex = parentOrderInfo.minorIndex + 1 - } + parentOrderInfoSignal: Signal, +): Signal { + const get = createGetBatchedProperties(propertiesSignal, propertyKeys) + return computed(() => { + const parentOrderInfo = parentOrderInfoSignal.value + + const offset = get('zIndexOffset') as ZIndexOffset + + const majorOffset = typeof offset === 'number' ? offset : offset?.major ?? 0 + const minorOffset = typeof offset === 'number' ? 0 : offset?.minor ?? 0 + + let majorIndex: number + let minorIndex: number + + if (parentOrderInfo == null) { + majorIndex = 0 + minorIndex = 0 + } else if (type > parentOrderInfo.elementType) { + majorIndex = parentOrderInfo.majorIndex + minorIndex = 0 + } else if ( + type != parentOrderInfo.elementType || + !shallowEqualRecord(instancedGroupDependencies, parentOrderInfo.instancedGroupDependencies) + ) { + majorIndex = parentOrderInfo.majorIndex + 1 + minorIndex = 0 + } else { + majorIndex = parentOrderInfo.majorIndex + minorIndex = parentOrderInfo.minorIndex + 1 + } - if (majorOffset > 0) { - majorIndex += majorOffset - minorIndex = 0 - } + if (majorOffset > 0) { + majorIndex += majorOffset + minorIndex = 0 + } - minorIndex += minorOffset + minorIndex += minorOffset - return { - instancedGroupDependencies, - elementType: type, - majorIndex, - minorIndex, - } + return { + instancedGroupDependencies, + elementType: type, + majorIndex, + minorIndex, + } + }) } function shallowEqualRecord(r1: Record | undefined, r2: Record | undefined): boolean { diff --git a/packages/uikit/src/panel/instanced-panel.ts b/packages/uikit/src/panel/instanced-panel.ts index 0e75b1ea..a90f94a8 100644 --- a/packages/uikit/src/panel/instanced-panel.ts +++ b/packages/uikit/src/panel/instanced-panel.ts @@ -61,10 +61,6 @@ const instancedPanelMaterialSetters: { const batchedProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] -function hasBatchedProperty(key: string): boolean { - return batchedProperties.includes(key) -} - function hasImmediateProperty(key: string): boolean { return key in instancedPanelMaterialSetters } @@ -80,29 +76,22 @@ export class InstancedPanel { private unsubscribeList: Array<() => void> = [] - public destroyed = false - private insertedIntoGroup = false private active = signal(false) - private group: InstancedPanelGroup - constructor( propertiesSignal: Signal, - getGroup: GetInstancedPanelGroup, - private readonly orderInfo: OrderInfo, - panelGroupDependencies: PanelGroupDependencies, + private group: InstancedPanelGroup, + private readonly minorIndex: number, private readonly matrix: Signal, private readonly size: Signal, private readonly offset: Signal | undefined, private readonly borderInset: Signal, private readonly clippingRect: Signal | undefined, isHidden: Signal | undefined, - subscriptions: Subscriptions, renameOutput?: Record, ) { - this.group = getGroup(orderInfo.minorIndex, panelGroupDependencies) setupImmediateProperties( propertiesSignal, this.active, @@ -122,7 +111,7 @@ export class InstancedPanel { subscriptions, renameOutput, ) - const get = createGetBatchedProperties(propertiesSignal, hasBatchedProperty, renameOutput) + const get = createGetBatchedProperties(propertiesSignal, batchedProperties, renameOutput) subscriptions.push( effect(() => { if ( @@ -140,10 +129,7 @@ export class InstancedPanel { } this.hide() }), - () => { - this.destroyed = true - this.hide() - }, + () => this.hide(), ) } @@ -234,7 +220,7 @@ export class InstancedPanel { return } this.insertedIntoGroup = true - this.group.insert(this.orderInfo.minorIndex, this) + this.group.insert(this.minorIndex, this) } private hide(): void { @@ -242,7 +228,7 @@ export class InstancedPanel { return } this.active.value = false - this.group.delete(this.orderInfo.minorIndex, this.indexInBucket, this) + this.group.delete(this.minorIndex, this.indexInBucket, this) this.insertedIntoGroup = false this.bucket = undefined this.indexInBucket = undefined diff --git a/packages/uikit/src/panel/interaction-panel-mesh.ts b/packages/uikit/src/panel/interaction-panel-mesh.ts index 80ead583..b4231376 100644 --- a/packages/uikit/src/panel/interaction-panel-mesh.ts +++ b/packages/uikit/src/panel/interaction-panel-mesh.ts @@ -51,13 +51,14 @@ export function makeClippedRaycast( fn: Mesh['raycast'], rootGroupRef: RefObject, clippingRect: Signal | undefined, - orderInfo: OrderInfo, + orderInfo: Signal, ): Mesh['raycast'] { return (raycaster: Raycaster, intersects: Intersection>[]) => { const rootGroup = rootGroupRef.current if (rootGroup == null) { return } + const { majorIndex, minorIndex, elementType } = orderInfo.value const oldLength = intersects.length fn.call(mesh, raycaster, intersects) const clippingPlanes = clippingRect?.value?.planes @@ -65,9 +66,9 @@ export function makeClippedRaycast( outer: for (let i = intersects.length - 1; i >= oldLength; i--) { const intersection = intersects[i] intersection.distance -= - orderInfo.majorIndex * 0.01 + - orderInfo.elementType * 0.001 + //1-10 - orderInfo.minorIndex * 0.00001 //1-100 + majorIndex * 0.01 + + elementType * 0.001 + //1-10 + minorIndex * 0.00001 //1-100 if (clippingPlanes == null) { continue } diff --git a/packages/uikit/src/panel/panel-material.ts b/packages/uikit/src/panel/panel-material.ts index b3bba94a..3783a87e 100644 --- a/packages/uikit/src/panel/panel-material.ts +++ b/packages/uikit/src/panel/panel-material.ts @@ -110,10 +110,6 @@ export const panelMaterialDefaultData = [ const batchedProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] -function hasBatchedProperty(key: string): boolean { - return batchedProperties.includes(key) -} - function hasImmediateProperty(key: string): boolean { return key in panelMaterialSetters } @@ -162,7 +158,7 @@ export function applyPropsToMaterialData( } unsubscribeList.length = 0 } - const get = createGetBatchedProperties(propertiesSignal, hasBatchedProperty, renameOutput) + const get = createGetBatchedProperties(propertiesSignal, batchedProperties, renameOutput) subscriptions.push( effect(() => { const isVisible = isPanelVisible( diff --git a/packages/uikit/src/panel/react.tsx b/packages/uikit/src/panel/react.ts similarity index 74% rename from packages/uikit/src/panel/react.tsx rename to packages/uikit/src/panel/react.ts index cc389733..984e5ae2 100644 --- a/packages/uikit/src/panel/react.tsx +++ b/packages/uikit/src/panel/react.ts @@ -1,19 +1,20 @@ -import { ReactNode, RefObject, createContext, useCallback, useEffect, useMemo } from 'react' -import { Group, Material, Matrix4, Mesh, MeshBasicMaterial, Plane, Vector2Tuple } from 'three' +import { Group, Matrix4, Mesh, MeshBasicMaterial, Vector2Tuple } from 'three' import type { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { Signal, effect } from '@preact/signals-core' -import { Inset } from '../flex/node.js' +import { Signal, computed, effect } from '@preact/signals-core' import { Subscriptions } from '../utils.js' import { useFrame } from '@react-three/fiber' import { ClippingRect } from '../clipping.js' import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' import { HoverEventHandlers } from '../hover.js' import { InstancedPanelGroup } from './instanced-panel-group.js' -import { MaterialSetter, PanelDepthMaterial, PanelDistanceMaterial, createPanelMaterial } from './panel-material.js' +import { MaterialClass, createPanelMaterial } from './panel-material.js' import { CameraDistanceRef, ElementType, OrderInfo } from '../order.js' import { panelGeometry } from './utils.js' import { ActiveEventHandlers } from '../active.js' import { MergedProperties } from '../properties/merged.js' +import { createGetBatchedProperties } from '../properties/batched.js' +import { Inset } from '../flex/node.js' +import { InstancedPanel } from './instanced-panel.js' export function InteractionGroup({ handlers, @@ -85,9 +86,9 @@ function mergeHandlers( export function createInteractionPanel( size: Signal, psRef: { pixelSize: number }, - orderInfo: OrderInfo, + orderInfo: Signal, parentClippingRect: Signal, - rootGroupRef: RefObject, + rootGroupRef: { current: Group }, subscriptions: Subscriptions, ): Mesh { const panel = new Mesh(panelGeometry) @@ -109,17 +110,43 @@ export type GetInstancedPanelGroup = ( panelGroupDependencies: PanelGroupDependencies, ) => InstancedPanelGroup -const InstancedPanelContext = createContext(null as any) - - export type PanelGroupDependencies = { materialClass: MaterialClass - receiveShadow: boolean - castShadow: boolean } & ShadowProperties +const propertyKeys = ["materialClass", "castShadow", "receiveShadow"] + +export function computePanelGroupDependencies(propertiesSignal: Signal) { + const get = createGetBatchedProperties(propertiesSignal, propertyKeys) + return computed(() => ({ + materialClass: get("materialClass") as MaterialClass | undefined ?? MeshBasicMaterial, + castShadow: get("castShadow") as boolean | undefined, + receiveShadow: get("receiveShadow") as boolean | undefined + })) +} + export type ShadowProperties = { receiveShadow?: boolean; castShadow?: boolean } +export function createInstancePanel( + propertiesSignal: Signal, + orderInfo: Signal, + panelGroupDependencies: Signal, + getGroup: GetInstancedPanelGroup, + matrix: Signal, + size: Signal, + offset: Signal | undefined, + borderInset: Signal, + clippingRect: Signal | undefined, + isHidden: Signal | undefined, + renameOutput?: Record, + subscriptions: Subscriptions) { + subscriptions.push(effect(() => { + const group = getGroup(orderInfo.value.majorIndex, panelGroupDependencies.value) + const panel = new InstancedPanel(propertiesSignal, group, orderInfo.value.minorIndex, matrix, size, offset, borderInset, clippingRect, isHidden, renameOutput) + return () => panel.destroy() + })) +} + export function useGetInstancedPanelGroup( pixelSize: number, cameraDistance: CameraDistanceRef, @@ -167,5 +194,3 @@ export function useGetInstancedPanelGroup( }) return getGroup } - -export const InstancedPanelProvider = InstancedPanelContext.Provider diff --git a/packages/uikit/src/properties/batched.ts b/packages/uikit/src/properties/batched.ts index 58cf73dd..139a0c26 100644 --- a/packages/uikit/src/properties/batched.ts +++ b/packages/uikit/src/properties/batched.ts @@ -5,7 +5,7 @@ export type GetBatchedProperties = (key: string) => unknown export function createGetBatchedProperties( propertiesSignal: Signal, - hasProperty: (key: string) => boolean, + keys: Array, renameOutput?: Record, ): GetBatchedProperties { const reverseRenameMap: Record = {} @@ -13,9 +13,10 @@ export function createGetBatchedProperties( reverseRenameMap[renameOutput[key]] = key } let currentProperties: MergedProperties | undefined + const hasPropertiy = (key: string) => keys.includes(key) const computedProperties = computed(() => { const newProperties = propertiesSignal.value - if (!newProperties.filterIsEqual(hasProperty, currentProperties)) { + if (!newProperties.filterIsEqual(hasPropertiy, currentProperties)) { //update current properties currentProperties = newProperties } diff --git a/packages/uikit/src/properties/default.tsx b/packages/uikit/src/properties/default.ts similarity index 100% rename from packages/uikit/src/properties/default.tsx rename to packages/uikit/src/properties/default.ts diff --git a/packages/uikit/src/react/utils.ts b/packages/uikit/src/react/utils.ts index c13a1a75..e6920937 100644 --- a/packages/uikit/src/react/utils.ts +++ b/packages/uikit/src/react/utils.ts @@ -40,50 +40,6 @@ export function useComponentInternals( ) } -export type LayoutListeners = { - onSizeChange?: (width: number, height: number) => void -} - -export function useLayoutListeners({ onSizeChange }: LayoutListeners, size: Signal): void { - const unsubscribe = useMemo(() => { - if (onSizeChange == null) { - return undefined - } - let first = true - return effect(() => { - const s = size.value - if (first) { - first = false - return - } - onSizeChange(...s) - }) - }, [onSizeChange, size]) - useEffect(() => unsubscribe, [unsubscribe]) -} - -export type ViewportListeners = { - onIsInViewportChange?: (isInViewport: boolean) => void -} - -export function useViewportListeners({ onIsInViewportChange }: ViewportListeners, isClipped: Signal) { - const unsubscribe = useMemo(() => { - if (onIsInViewportChange == null) { - return undefined - } - let first = true - return effect(() => { - const isInViewport = !isClipped.value - if (first) { - first = false - return - } - onIsInViewportChange(isInViewport) - }) - }, [isClipped, onIsInViewportChange]) - useEffect(() => unsubscribe, [unsubscribe]) -} - const MatrixContext = createContext>(null as any) export const MatrixProvider = MatrixContext.Provider diff --git a/packages/uikit/src/responsive.ts b/packages/uikit/src/responsive.ts index a7c1fb5f..e028c8e1 100644 --- a/packages/uikit/src/responsive.ts +++ b/packages/uikit/src/responsive.ts @@ -1,10 +1,9 @@ import { Signal } from '@preact/signals-core' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { createContext, useContext, useMemo } from 'react' -import { ManagerCollection, Properties } from './properties/utils.js' -import { AllOptionalProperties, WithClasses, traverseProperties } from './properties/default.js' +import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' import { Vector2Tuple } from 'three' +import { MergedProperties } from './properties/merged.js' const breakPoints = { sm: 640, @@ -20,16 +19,8 @@ export type WithResponsive = T & { [Key in keyof typeof breakPoints]?: T } -export function useRootSize() { - return useContext(RootSizeContext) -} - -const RootSizeContext = createContext>(null as any) - -export const RootSizeProvider = RootSizeContext.Provider - export function applyResponsiveProperties( - collection: ManagerCollection, + merged: MergedProperties, defaultProperties: AllOptionalProperties | undefined, properties: WithClasses> & EventHandlers, rootSize: Signal, @@ -49,7 +40,7 @@ export function applyResponsiveProperties( if (properties == null) { continue } - translator[key](collection, properties) + translator[key](merged, properties) } }) } diff --git a/packages/uikit/src/scroll.tsx b/packages/uikit/src/scroll.ts similarity index 58% rename from packages/uikit/src/scroll.tsx rename to packages/uikit/src/scroll.ts index 5b520203..fade9a94 100644 --- a/packages/uikit/src/scroll.tsx +++ b/packages/uikit/src/scroll.ts @@ -1,19 +1,18 @@ import { ReadonlySignal, Signal, computed, effect, signal } from '@preact/signals-core' import { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Group, Matrix4, MeshBasicMaterial, Vector2, Vector2Tuple, Vector3, Vector4Tuple } from 'three' -import { FlexNode, Inset, YogaProperties } from './flex/node.js' +import { Group, Matrix4, MeshBasicMaterial, Object3D, Vector2, Vector2Tuple, Vector3, Vector4Tuple } from 'three' +import { FlexNode, Inset } from './flex/node.js' import { Color as ColorRepresentation, useFrame } from '@react-three/fiber' -import { Subscriptions, useSignalEffect } from './utils.js' -import { GetInstancedPanelGroup, MaterialClass, PanelGroupDependencies } from './panel/react.js' +import { Subscriptions } from './utils.js' +import { GetInstancedPanelGroup, PanelGroupDependencies } from './panel/react.js' import { ClippingRect } from './clipping.js' import { clamp } from 'three/src/math/MathUtils.js' import { InstancedPanel, PanelProperties } from './panel/instanced-panel.js' -import { borderAliasPropertyTransformation, panelAliasPropertyTransformation } from './properties/alias.js' -import { PropertyKeyTransformation, WithReactive } from './properties/utils.js' import { ElementType, OrderInfo, computeOrderInfo } from './order.js' import { createGetBatchedProperties } from './properties/batched.js' import { MergedProperties } from './properties/merged.js' +import { MaterialClass } from './panel/panel-material.js' +import { WithReactive } from './properties/default.js' const distanceHelper = new Vector3() const localPointHelper = new Vector3() @@ -47,91 +46,66 @@ export function computeGlobalScrollMatrix( }) } -export function ScrollGroup({ - node, - scrollPosition, - children, -}: { - node: FlexNode - scrollPosition: Signal - children?: ReactNode -}) { - const ref = useRef(null) - - useEffect( - () => - effect(() => { - const [scrollX, scrollY] = scrollPosition.value - const { pixelSize } = node - ref.current?.position.set(-scrollX * pixelSize, scrollY * pixelSize, 0) - ref.current?.updateMatrix() - }), - [node, scrollPosition], - ) - - return {children} +export function setupScrollGroup(node: FlexNode, scrollPosition: Signal, ref: { current: Object3D }) { + return effect(() => { + const [scrollX, scrollY] = scrollPosition.value + const { pixelSize } = node + ref.current?.position.set(-scrollX * pixelSize, scrollY * pixelSize, 0) + ref.current?.updateMatrix() + }) } -export function ScrollHandler({ - listeners, - node, - scrollPosition, - children, -}: { - node: FlexNode - scrollPosition: Signal - listeners: ScrollListeners - children?: ReactNode -}) { - const [isScrollable, setIsScrollable] = useState(() => node.scrollable.value.some((scrollable) => scrollable)) - useSignalEffect(() => setIsScrollable(node.scrollable.value.some((scrollable) => scrollable)), [node]) - const onScrollRef = useRef(listeners.onScroll) - onScrollRef.current = listeners.onScroll - const downPointerMap = useMemo(() => new Map(), []) - const scrollVelocity = useMemo(() => new Vector2(), []) - - const scroll = useCallback( - ( - event: ThreeEvent | undefined, - deltaX: number, - deltaY: number, - deltaTime: number | undefined, - enableRubberBand: boolean, - ) => { - const [wasScrolledX, wasScrolledY] = event == null ? [false, false] : getWasScrolled(event.nativeEvent) - if (wasScrolledX && wasScrolledY) { - return - } - const [x, y] = scrollPosition.value - const [maxX, maxY] = node.maxScrollPosition.value - let [newX, newY] = scrollPosition.value - const [ancestorScrollableX, ancestorScrollableY] = node.anyAncestorScrollable?.value ?? [false, false] - if (!wasScrolledX) { - newX = computeScroll(x, maxX, deltaX, enableRubberBand && !ancestorScrollableX) - } - if (!wasScrolledY) { - newY = computeScroll(y, maxY, deltaY, enableRubberBand && !ancestorScrollableY) - } - if (deltaTime != null && deltaTime > 0) { - scrollVelocity.set(deltaX, deltaY).divideScalar(deltaTime) - } - - if (event != null) { - setWasScrolled( - event.nativeEvent, - wasScrolledX || Math.min(x, (maxX ?? 0) - x) > 5, - wasScrolledY || Math.min(y, (maxY ?? 0) - y) > 5, - ) - } - if (x != newX || y != newY) { - scrollPosition.value = [newX, newY] - onScrollRef.current?.(...scrollPosition.value, event) - } - }, - [node, scrollPosition, scrollVelocity], - ) +export function setupScrollHandler( + node: FlexNode, + scrollPosition: Signal, + ref: { current: Object3D }, + onScrollRef: { current: ScrollListeners['onScroll'] }, + onFrames: Array<(delta: number) => void>, +): Signal { + const isScrollable = computed(() => node.scrollable.value.some((scrollable) => scrollable)) + + const downPointerMap = new Map() + const scrollVelocity = new Vector2() + + const scroll = ( + event: ThreeEvent | undefined, + deltaX: number, + deltaY: number, + deltaTime: number | undefined, + enableRubberBand: boolean, + ) => { + const [wasScrolledX, wasScrolledY] = event == null ? [false, false] : getWasScrolled(event.nativeEvent) + if (wasScrolledX && wasScrolledY) { + return + } + const [x, y] = scrollPosition.value + const [maxX, maxY] = node.maxScrollPosition.value + let [newX, newY] = scrollPosition.value + const [ancestorScrollableX, ancestorScrollableY] = node.anyAncestorScrollable?.value ?? [false, false] + if (!wasScrolledX) { + newX = computeScroll(x, maxX, deltaX, enableRubberBand && !ancestorScrollableX) + } + if (!wasScrolledY) { + newY = computeScroll(y, maxY, deltaY, enableRubberBand && !ancestorScrollableY) + } + if (deltaTime != null && deltaTime > 0) { + scrollVelocity.set(deltaX, deltaY).divideScalar(deltaTime) + } + + if (event != null) { + setWasScrolled( + event.nativeEvent, + wasScrolledX || Math.min(x, (maxX ?? 0) - x) > 5, + wasScrolledY || Math.min(y, (maxY ?? 0) - y) > 5, + ) + } + if (x != newX || y != newY) { + scrollPosition.value = [newX, newY] + onScrollRef.current?.(...scrollPosition.value, event) + } + } - useFrame((_, deltaTime) => { + onFrames.push((deltaTime) => { if (downPointerMap.size > 0) { return } @@ -163,35 +137,24 @@ export function ScrollHandler({ scroll(undefined, deltaX, deltaY, undefined, true) }) - const ref = useRef(null) - - if (!isScrollable) { - return {children} - } - - return ( - { + return computed(() => { + if (!isScrollable.value) { + return {} + } + return { + onPointerDown: (event) => { let interaction = downPointerMap.get(event.pointerId) if (interaction == null) { downPointerMap.set(event.pointerId, (interaction = { timestamp: 0, point: new Vector3() })) } interaction.timestamp = performance.now() / 1000 ref.current!.worldToLocal(interaction.point.copy(event.point)) - }} - onPointerUp={(event) => { - downPointerMap.delete(event.pointerId) - }} - onPointerLeave={(event) => { - downPointerMap.delete(event.pointerId) - }} - onPointerCancel={(event) => { - downPointerMap.delete(event.pointerId) - }} - onContextMenu={(e) => e.nativeEvent.preventDefault()} - onPointerMove={(event) => { + }, + onPointerUp: (event) => downPointerMap.delete(event.pointerId), + onPointerLeave: (event) => downPointerMap.delete(event.pointerId), + onPointerCancel: (event) => downPointerMap.delete(event.pointerId), + onContextMenu: (e) => e.nativeEvent.preventDefault(), + onPointerMove: (event) => { const prevInteraction = downPointerMap.get(event.pointerId) if (prevInteraction == null) { @@ -210,17 +173,15 @@ export function ScrollHandler({ } scroll(event, -distanceHelper.x, distanceHelper.y, deltaTime, true) - }} - onWheel={(event) => { + }, + onWheel: (event) => { if (event.defaultPrevented) { return } scroll(event, event.deltaX, event.deltaY, undefined, false) - }} - > - {children} - - ) + }, + } + }) } const wasScrolledSymbol = Symbol('was-scrolled') @@ -291,45 +252,26 @@ export type ScrollbarProperties = { } > -const scrollbarLength = 'scrollbar'.length - -function removeScrollbar(key: string) { - const firstKeyUncapitalized = key[scrollbarLength].toLowerCase() - return firstKeyUncapitalized + key.slice(scrollbarLength + 1) +const scrollbarPanelPropertyRename = { + scrollbarColor: 'backgroundColor', + scrollbarBorderBottomLeftRadius: 'borderBottomLeftRadius', + scrollbarBorderBottomRightRadius: 'borderBottomRightRadius', + scrollbarBorderTopRightRadius: 'borderTopRightRadius', + scrollbarBorderTopLeftRadius: 'borderTopLeftRadius', + scrollbarBorderColor: 'borderColor', + scrollbarBorderBend: 'borderBend', + scrollbarBorderOpacity: 'borderOpacity', + scrollbarOpacity: 'backgroundOpacity', } -const scrollbarPanelPropertyTransformation: PropertyKeyTransformation = (key, value, setProperty) => { - if (key === 'scrollbarOpacity') { - setProperty('backgroundOpacity', value) - return true - } - if (key === 'scrollbarColor') { - setProperty('backgroundColor', value) - return true - } - if(!key.startsWith("scrollbar")) { - - } - key = removeScrollbar(key) - if (panelAliasPropertyTransformation.hasProperty(key)) { - panelAliasPropertyTransformation.setProperty(key, value, setProperty) - return - } - setProperty(key, value) -} +const scrollbarWidthPropertyKeys = ['scrollbarWidth'] -function isScrollbarWidthPropertyKey(key: string) { - return key === 'scrollbarWidth' -} -const borderPropertyKeys = [ +const scrollbarBorderPropertyKeys = [ 'scrollbarBorderLeft', 'scrollbarBorderRight', 'scrollbarBorderTop', 'scrollbarBorderBottom', -] as const -function isBorderPropertyKey(key: (typeof borderPropertyKeys)[number]) { - return borderPropertyKeys.includes(key) -} +] export function createScrollbars( propertiesSignal: Signal, @@ -339,7 +281,7 @@ export function createScrollbars( isClipped: Signal | undefined, materialClass: MaterialClass | undefined, parentClippingRect: Signal | undefined, - orderInfo: OrderInfo, + orderInfo: Signal, getGroup: GetInstancedPanelGroup, subscriptions: Subscriptions, ): void { @@ -348,24 +290,11 @@ export function createScrollbars( castShadow: false, receiveShadow: false, } - const scrollbarOrderInfo = computeOrderInfo(ElementType.Panel, undefined, groupDeps, orderInfo) + const scrollbarOrderInfo = computeOrderInfo(propertiesSignal, ElementType.Panel, groupDeps, orderInfo) - const getScrollbarWidth = createGetBatchedProperties<{ scrollbarWidth?: number }>( - propertiesSignal, - isScrollbarWidthPropertyKey, - ) - const getBorder = createGetBatchedProperties<{ - scrollbarBorderLeft?: number - scrollbarBorderRight?: number - scrollbarBorderBottom?: number - scrollbarBorderTop?: number - }>(propertiesSignal, isBorderPropertyKey, scrollbarBorderPropertyTransformation) - const borderSize = computed(() => [ - getBorder('borderTop') ?? 0, - getBorder('borderRight') ?? 0, - getBorder('borderBottom') ?? 0, - getBorder('borderLeft') ?? 0, - ]) + const getScrollbarWidth = createGetBatchedProperties(propertiesSignal, scrollbarWidthPropertyKeys) + const getBorder = createGetBatchedProperties(propertiesSignal, scrollbarBorderPropertyKeys) + const borderSize = computed(() => scrollbarBorderPropertyKeys.map((key) => (getBorder(key) as number) ?? 0) as Inset) createScrollbar( propertiesSignal, @@ -412,19 +341,19 @@ function createScrollbar( scrollPosition: Signal, node: FlexNode, globalMatrix: Signal, + panelGroupDependencies: Signal, isClipped: Signal | undefined, - materialClass: MaterialClass | undefined, parentClippingRect: Signal | undefined, - orderInfo: OrderInfo, + orderInfo: Signal, getGroup: GetInstancedPanelGroup, - get: (key: 'scrollbarWidth') => number | undefined, + get: (key: string) => unknown, borderSize: ReadonlySignal, subscriptions: Subscriptions, ) { const scrollbarTransformation = computed(() => { return computeScrollbarTransformation( mainIndex, - get('scrollbarWidth') ?? 10, + (get('scrollbarWidth') as number) ?? 10, node.size.value, node.maxScrollPosition.value, node.borderInset.value, @@ -434,19 +363,23 @@ function createScrollbar( const scrollbarPosition = computed(() => (scrollbarTransformation.value?.slice(0, 2) ?? [0, 0]) as Vector2Tuple) const scrollbarSize = computed(() => (scrollbarTransformation.value?.slice(2, 4) ?? [0, 0]) as Vector2Tuple) - new InstancedPanel( - propertiesSignal, - getGroup, - orderInfo, - { materialClass: materialClass ?? MeshBasicMaterial, receiveShadow: false, castShadow: false }, - globalMatrix, - scrollbarSize, - scrollbarPosition, - borderSize, - parentClippingRect, - isClipped, - subscriptions, - scrollbarPanelPropertyTransformation, + subscriptions.push( + effect(() => { + const panel = new InstancedPanel( + propertiesSignal, + getGroup, + orderInfo, + panelGroupDependencies, + globalMatrix, + scrollbarSize, + scrollbarPosition, + borderSize, + parentClippingRect, + isClipped, + subscriptions, + scrollbarPanelPropertyRename, + ) + }), ) } diff --git a/packages/uikit/src/text/react.ts b/packages/uikit/src/text/react.ts new file mode 100644 index 00000000..1f6894c6 --- /dev/null +++ b/packages/uikit/src/text/react.ts @@ -0,0 +1,253 @@ +import { ReadonlySignal, Signal, computed, effect, signal } from '@preact/signals-core' +import { InstancedText, TextAlignProperties, TextAppearanceProperties } from './render/instanced-text.js' +import { InstancedGlyphGroup } from './render/instanced-glyph-group.js' +import { FlexNode } from '../flex/node.js' +import { Group, Matrix4, WebGLRenderer } from 'three' +import { ClippingRect } from '../clipping.js' +import { Subscriptions, readReactive } from '../utils.js' +import { loadCachedFont } from './cache.js' +import { MeasureFunction, MeasureMode } from 'yoga-layout/wasm-async' +import { Font } from './font.js' +import { GlyphLayout, GlyphLayoutProperties, buildGlyphLayout, measureGlyphLayout } from './layout.js' +import { useFrame, useThree } from '@react-three/fiber' +import { CameraDistanceRef, ElementType, OrderInfo } from '../order.js' +import { MergedProperties } from '../properties/merged.js' +import { createGetBatchedProperties } from '../properties/batched.js' + +export type GetInstancedGlyphGroup = (majorIndex: number, font: Font) => InstancedGlyphGroup + +export function createGetInstancedGlyphGroup( + pixelSize: number, + cameraDistance: CameraDistanceRef, + groupsContainer: Group, +) { + const map = new Map>() + const getGroup: GetInstancedGlyphGroup = (majorIndex, font) => { + let groups = map.get(font) + if (groups == null) { + map.set(font, (groups = new Map())) + } + let group = groups?.get(majorIndex) + if (group == null) { + groups.set( + majorIndex, + (group = new InstancedGlyphGroup(font, pixelSize, cameraDistance, { + majorIndex, + elementType: ElementType.Text, + minorIndex: 0, + })), + ) + groupsContainer.add(group) + } + return group + } + + useFrame((_, delta) => { + for (const groups of map.values()) { + for (const group of groups.values()) { + group.onFrame(delta) + } + } + }) + + return getGroup +} + +export type FontFamilyUrls = Partial> + +export type FontFamilies = Record + +const fontWeightNames = { + thin: 100, + 'extra-light': 200, + light: 300, + normal: 400, + medium: 500, + 'semi-bold': 600, + bold: 700, + 'extra-bold': 800, + black: 900, + 'extra-black': 950, +} + +export type FontWeight = keyof typeof fontWeightNames | number + +const alignPropertyKeys = ['horizontalAlign', 'verticalAlign'] +const appearancePropertyKeys = ['color', 'opacity'] +const glyphPropertyKeys = ['fontSize', 'letterSpacing', 'lineHeight', 'wordBreak'] + +export type InstancedTextProperties = TextAlignProperties & + TextAppearanceProperties & + Omit & + FontFamilyProperties + +export function createInstancedText( + properties: Signal, + text: string | ReadonlySignal | Array>, + matrix: Signal, + node: FlexNode, + isHidden: Signal | undefined, + parentClippingRect: Signal | undefined, + orderInfo: OrderInfo, + fontFamilies: FontFamilies | undefined, + renderer: WebGLRenderer, + getGroup: GetInstancedGlyphGroup, + subscriptions: Subscriptions, +) { + const fontSignal = computeFont(properties, fontFamilies, renderer, subscriptions) + // eslint-disable-next-line react-hooks/exhaustive-deps + const textSignal = signal | Array>>(text) + let layoutPropertiesRef: { current: GlyphLayoutProperties | undefined } = { current: undefined } + + const measureFunc = computeMeasureFunc(properties, fontSignal, textSignal, layoutPropertiesRef) + + const getAlign = createGetBatchedProperties(properties, alignPropertyKeys) + const getAppearance = createGetBatchedProperties(properties, appearancePropertyKeys) + + const layoutSignal = signal(undefined) + subscriptions.push( + node.addLayoutChangeListener(() => { + const layoutProperties = layoutPropertiesRef.current + if (layoutProperties == null) { + return + } + const { size, paddingInset, borderInset } = node + const [width, height] = size.value + const [pTop, pRight, pBottom, pLeft] = paddingInset.value + const [bTop, bRight, bBottom, bLeft] = borderInset.value + const actualWidth = width - pRight - pLeft - bRight - bLeft + const actualheight = height - pTop - pBottom - bTop - bBottom + layoutSignal.value = buildGlyphLayout(layoutProperties, actualWidth, actualheight) + }), + ) + + subscriptions.push( + effect(() => { + const font = fontSignal.value + if (font == null) { + return + } + const instancedText = new InstancedText( + getGroup(orderInfo.majorIndex, font), + getAlign, + getAppearance, + layoutSignal, + matrix, + isHidden, + parentClippingRect, + ) + return () => instancedText.destroy() + }), + ) + + return measureFunc +} + +const fontKeys = ['fontFamily', 'fontWeight'] + +export type FontFamilyProperties = { fontFamily?: string; fontWeight?: FontWeight } + +const defaultFontFamilyUrls = { + inter: { + light: 'https://pmndrs.github.io/uikit/fonts/inter-light.json', + normal: 'https://pmndrs.github.io/uikit/fonts/inter-normal.json', + medium: 'https://pmndrs.github.io/uikit/fonts/inter-medium.json', + 'semi-bold': 'https://pmndrs.github.io/uikit/fonts/inter-semi-bold.json', + bold: 'https://pmndrs.github.io/uikit/fonts/inter-bold.json', + }, +} satisfies FontFamilies + +export function computeFont( + properties: Signal, + fontFamilies: FontFamilies = defaultFontFamilyUrls, + renderer: WebGLRenderer, + subscriptions: Subscriptions, +): Signal { + const result = signal(undefined) + const get = createGetBatchedProperties(properties, fontKeys) + subscriptions.push( + effect(() => { + let fontWeight = (get('fontWeight') as FontWeight) ?? 'normal' + if (typeof fontWeight === 'string') { + fontWeight = fontWeightNames[fontWeight] + } + let fontFamily = get('fontFamily') as string + if (fontFamily == null) { + fontFamily = Object.keys(fontFamilies)[0] + } + const url = getMatchingFontUrl(fontFamilies[fontFamily], fontWeight) + let canceled = false + loadCachedFont(url, renderer, (font) => (canceled ? undefined : (result.value = font))) + return () => (canceled = true) + }), + ) + return result +} + +function getMatchingFontUrl(fontFamily: FontFamilyUrls, weight: number): string { + let distance = Infinity + let result: string | undefined + for (const fontWeight in fontFamily) { + const d = Math.abs(weight - getWeightNumber(fontWeight)) + if (d === 0) { + return fontFamily[fontWeight]! + } + if (d < distance) { + distance = d + result = fontFamily[fontWeight] + } + } + if (result == null) { + throw new Error(`font family has no entries ${fontFamily}`) + } + return result +} + +function getWeightNumber(value: string): number { + if (value in fontWeightNames) { + return fontWeightNames[value as keyof typeof fontWeightNames] + } + const number = parseFloat(value) + if (isNaN(number)) { + throw new Error(`invalid font weight "${value}"`) + } + return number +} + +export function computeMeasureFunc( + properties: Signal, + fontSignal: Signal, + textSignal: Signal | Array | string>>, + propertiesRef: { current: GlyphLayoutProperties | undefined }, +) { + const get = createGetBatchedProperties(properties, glyphPropertyKeys) + return computed(() => { + const font = fontSignal.value + if (font == null) { + return undefined + } + const textSignalValue = textSignal.value + const text = Array.isArray(textSignalValue) + ? textSignalValue.map((t) => readReactive(t)).join('') + : readReactive(textSignalValue) + const letterSpacing = (get('letterSpacing') as number) ?? 0 + const lineHeight = (get('lineHeight') as number) ?? 1.2 + const fontSize = (get('fontSize') as number) ?? 16 + const wordBreak = (get('wordBreak') as GlyphLayoutProperties['wordBreak']) ?? 'break-word' + + return (width, widthMode) => { + const availableWidth = widthMode === MeasureMode.Undefined ? undefined : width + return measureGlyphLayout( + (propertiesRef.current = { + font, + fontSize, + letterSpacing, + lineHeight, + text, + wordBreak, + }), + availableWidth, + ) + } + }) +} diff --git a/packages/uikit/src/text/react.tsx b/packages/uikit/src/text/react.tsx deleted file mode 100644 index 8ba87e80..00000000 --- a/packages/uikit/src/text/react.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import { ReadonlySignal, Signal, computed, signal } from '@preact/signals-core' -import { InstancedText, TextAlignProperties, TextAppearanceProperties } from './render/instanced-text.js' -import { InstancedGlyphGroup } from './render/instanced-glyph-group.js' -import { MutableRefObject, ReactNode, createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react' -import { FlexNode } from '../flex/node.js' -import { Group, Matrix4 } from 'three' -import { ClippingRect } from '../clipping.js' -import { ManagerCollection, useGetBatchedProperties } from '../properties/utils.js' -import { readReactive, useSignalEffect } from '../utils.js' -import { loadCachedFont } from './cache.js' -import { MeasureFunction, MeasureMode } from 'yoga-layout/wasm-async' -import { Font } from './font.js' -import { GlyphLayout, GlyphLayoutProperties, buildGlyphLayout, measureGlyphLayout } from './layout.js' -import { useFrame, useThree } from '@react-three/fiber' -import { CameraDistanceRef, ElementType, OrderInfo } from '../order.js' - -export type GetInstancedGlyphGroup = (majorIndex: number, font: Font) => InstancedGlyphGroup - -const InstancedGlyphContext = createContext(null as any) - -export const InstancedGlyphProvider = InstancedGlyphContext.Provider - -export function useGetInstancedGlyphGroup( - pixelSize: number, - cameraDistance: CameraDistanceRef, - groupsContainer: Group, -) { - const map = useMemo(() => new Map>(), []) - const getGroup = useCallback( - (majorIndex, font) => { - let groups = map.get(font) - if (groups == null) { - map.set(font, (groups = new Map())) - } - let group = groups?.get(majorIndex) - if (group == null) { - groups.set( - majorIndex, - (group = new InstancedGlyphGroup(font, pixelSize, cameraDistance, { - majorIndex, - elementType: ElementType.Text, - minorIndex: 0, - })), - ) - groupsContainer.add(group) - } - return group - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [pixelSize, cameraDistance, groupsContainer], - ) - - useFrame((_, delta) => { - for (const groups of map.values()) { - for (const group of groups.values()) { - group.onFrame(delta) - } - } - }) - - return getGroup -} - -export type FontFamilyUrls = Partial> - -const FontFamiliesContext = createContext>(null as any) - -const defaultFontFamilyUrls = { - inter: { - light: 'https://pmndrs.github.io/uikit/fonts/inter-light.json', - normal: 'https://pmndrs.github.io/uikit/fonts/inter-normal.json', - medium: 'https://pmndrs.github.io/uikit/fonts/inter-medium.json', - 'semi-bold': 'https://pmndrs.github.io/uikit/fonts/inter-semi-bold.json', - bold: 'https://pmndrs.github.io/uikit/fonts/inter-bold.json', - }, -} satisfies Record - -const fontWeightNames = { - thin: 100, - 'extra-light': 200, - light: 300, - normal: 400, - medium: 500, - 'semi-bold': 600, - bold: 700, - 'extra-bold': 800, - black: 900, - 'extra-black': 950, -} - -export type FontWeight = keyof typeof fontWeightNames | number - -export function FontFamilyProvider({ - children, - ...fontFamilies -}: Record & { children?: ReactNode }) { - const existinFontFamilyUrls = useContext(FontFamiliesContext) - if (existinFontFamilyUrls != null) { - fontFamilies = { ...existinFontFamilyUrls, ...fontFamilies } - } - return {children} -} - -const alignPropertyKeys = ['horizontalAlign', 'verticalAlign'] as const -const appearancePropertyKeys = ['color', 'opacity'] as const -const glyphPropertyKeys = ['fontSize', 'letterSpacing', 'lineHeight', 'wordBreak'] satisfies Array< - keyof GlyphLayoutProperties -> - -export type InstancedTextProperties = TextAlignProperties & - TextAppearanceProperties & - Omit & - FontFamilyProperties - -export function useInstancedText( - collection: ManagerCollection, - text: string | ReadonlySignal | Array>, - matrix: Signal, - node: FlexNode, - isHidden: Signal | undefined, - parentClippingRect: Signal | undefined, - orderInfo: OrderInfo, -) { - const getGroup = useContext(InstancedGlyphContext) - const fontSignal = useFont(collection) - // eslint-disable-next-line react-hooks/exhaustive-deps - const textSignal = useMemo(() => signal | Array>>(text), []) - textSignal.value = text - const propertiesRef = useRef(undefined) - - const measureFunc = useMeasureFunc(collection, fontSignal, textSignal, propertiesRef) - - const alignProperties = useGetBatchedProperties(collection, alignPropertyKeys) - const appearanceProperties = useGetBatchedProperties(collection, appearancePropertyKeys) - - const layoutSignal = useMemo(() => signal(undefined), []) - useEffect( - () => - node.addLayoutChangeListener(() => { - const layoutProperties = propertiesRef.current - if (layoutProperties == null) { - return - } - const { size, paddingInset, borderInset } = node - const [width, height] = size.value - const [pTop, pRight, pBottom, pLeft] = paddingInset.value - const [bTop, bRight, bBottom, bLeft] = borderInset.value - const actualWidth = width - pRight - pLeft - bRight - bLeft - const actualheight = height - pTop - pBottom - bTop - bBottom - layoutSignal.value = buildGlyphLayout(layoutProperties, actualWidth, actualheight) - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [node], - ) - - useSignalEffect(() => { - const font = fontSignal.value - if (font == null) { - return - } - const instancedText = new InstancedText( - getGroup(orderInfo.majorIndex, font), - alignProperties, - appearanceProperties, - layoutSignal, - matrix, - isHidden, - parentClippingRect, - ) - return () => instancedText.destroy() - }, [getGroup, matrix, node, isHidden, parentClippingRect, orderInfo.majorIndex]) - - return measureFunc -} - -const fontKeys = ['fontFamily', 'fontWeight'] as const - -export type FontFamilyProperties = { fontFamily?: string; fontWeight?: FontWeight } - -export function useFont(collection: ManagerCollection) { - const result = useMemo(() => signal(undefined), []) - const fontFamilies = useContext(FontFamiliesContext) ?? defaultFontFamilyUrls - const getProperties = useGetBatchedProperties(collection, fontKeys) - const renderer = useThree(({ gl }) => gl) - useSignalEffect(() => { - const get = getProperties.value - if (get == null) { - return - } - let fontWeight = get('fontWeight') ?? 'normal' - if (typeof fontWeight === 'string') { - fontWeight = fontWeightNames[fontWeight] - } - let fontFamily = get('fontFamily') - if (fontFamily == null) { - fontFamily = Object.keys(fontFamilies)[0] - } - const url = getMatchingFontUrl(fontFamilies[fontFamily], fontWeight) - let canceled = false - loadCachedFont(url, renderer, (font) => (canceled ? undefined : (result.value = font))) - return () => (canceled = true) - }, [fontFamilies, renderer]) - return result -} - -function getMatchingFontUrl(fontFamily: FontFamilyUrls, weight: number): string { - let distance = Infinity - let result: string | undefined - for (const fontWeight in fontFamily) { - const d = Math.abs(weight - getWeightNumber(fontWeight)) - if (d === 0) { - return fontFamily[fontWeight]! - } - if (d < distance) { - distance = d - result = fontFamily[fontWeight] - } - } - if (result == null) { - throw new Error(`font family has no entries ${fontFamily}`) - } - return result -} - -function getWeightNumber(value: string): number { - if (value in fontWeightNames) { - return fontWeightNames[value as keyof typeof fontWeightNames] - } - const number = parseFloat(value) - if (isNaN(number)) { - throw new Error(`invalid font weight "${value}"`) - } - return number -} - -export function useMeasureFunc( - collection: ManagerCollection, - fontSignal: Signal, - textSignal: Signal | Array | string>>, - propertiesRef: MutableRefObject, -) { - const getGlyphProperties = useGetBatchedProperties(collection, glyphPropertyKeys) - const measureFunc = useMemo( - () => - computed(() => { - const font = fontSignal.value - 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 = 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 === MeasureMode.Undefined ? undefined : width - return measureGlyphLayout( - (propertiesRef.current = { - font, - fontSize, - letterSpacing, - lineHeight, - text, - wordBreak, - }), - availableWidth, - ) - } - }), - [fontSignal, getGlyphProperties, propertiesRef, textSignal], - ) - return measureFunc -} diff --git a/packages/uikit/src/text/render/instanced-text.ts b/packages/uikit/src/text/render/instanced-text.ts index 4129f0c5..795149f4 100644 --- a/packages/uikit/src/text/render/instanced-text.ts +++ b/packages/uikit/src/text/render/instanced-text.ts @@ -13,6 +13,7 @@ import { } from '../utils.js' import { InstancedGlyphGroup } from './instanced-glyph-group.js' import { GlyphLayout } from '../layout.js' +import { GetBatchedProperties } from '../../properties/batched.js' export type TextAlignProperties = { horizontalAlign?: keyof typeof alignmentXMap | 'block' @@ -36,20 +37,15 @@ export class InstancedText { constructor( private group: InstancedGlyphGroup, - private getAlignmentProperties: Signal< - ((key: K) => TextAlignProperties[K]) | undefined - >, - private getAppearanceProperties: Signal< - ((key: K) => TextAppearanceProperties[K]) | undefined - >, + private getAlignment: GetBatchedProperties, + private getAppearance: GetBatchedProperties, private layout: Signal, private matrix: Signal, isHidden: Signal | undefined, private parentClippingRect: Signal | undefined, ) { this.unsubscribe = effect(() => { - const get = getAppearanceProperties.value - if (get == null || isHidden?.value === true || (get('opacity') ?? 1) < 0.01) { + if (isHidden?.value === true || ((getAppearance('opacity') as number) ?? 1) < 0.01) { this.hide() return } @@ -75,25 +71,16 @@ export class InstancedText { traverseGlyphs(this.glyphLines, (glyph) => glyph.updateClippingRect(clippingRect)) }), effect(() => { - const get = this.getAppearanceProperties.value - if (get == null) { - return - } - const color = (this.color = get('color') ?? 0xffffff) + const color = (this.color = (this.getAppearance('color') as number) ?? 0xffffff) traverseGlyphs(this.glyphLines, (glyph) => glyph.updateColor(color)) }), effect(() => { - const get = this.getAppearanceProperties.value - if (get == null) { - return - } - const opacity = (this.opacity = get('opacity') ?? 1) + const opacity = (this.opacity = (this.getAppearance('opacity') as number) ?? 1) traverseGlyphs(this.glyphLines, (glyph) => glyph.updateOpacity(opacity)) }), effect(() => { const layout = this.layout.value - const get = this.getAlignmentProperties.value - if (layout == null || get == null) { + if (layout == null) { return } const { @@ -109,7 +96,7 @@ export class InstancedText { let y = -availableHeight / 2 - switch (get('verticalAlign')) { + switch (this.getAlignment('verticalAlign')) { case 'center': y += (availableHeight - getGlyphLayoutHeight(layout.lines.length, layout)) / 2 break @@ -118,7 +105,7 @@ export class InstancedText { break } - const horizontalAlign = get('horizontalAlign') ?? 'left' + const horizontalAlign = this.getAlignment('horizontalAlign') ?? 'left' const linesLength = lines.length const pixelSize = this.group.pixelSize for (let lineIndex = 0; lineIndex < linesLength; lineIndex++) { diff --git a/packages/uikit/src/transform.ts b/packages/uikit/src/transform.ts index eaf1f5ae..41c2baeb 100644 --- a/packages/uikit/src/transform.ts +++ b/packages/uikit/src/transform.ts @@ -2,7 +2,6 @@ import { Signal, computed } from '@preact/signals-core' import { Euler, Matrix4, Quaternion, Vector3, Vector3Tuple } from 'three' import { FlexNode } from './flex/node.js' import { alignmentXMap, alignmentYMap } from './utils.js' -import { transformAliasPropertyTransformation } from './properties/alias.js' import { createGetBatchedProperties } from './properties/batched.js' import { MergedProperties } from './properties/merged.js' @@ -35,7 +34,7 @@ const sX = 'transformScaleX' const sY = 'transformScaleY' const sZ = 'transformScaleZ' -const propertyKeys: Array = [tX, tY, tZ, rX, rY, rZ, sX, sY, sZ] +const propertyKeys = [tX, tY, tZ, rX, rY, rZ, sX, sY, sZ] const tHelper = new Vector3() const sHelper = new Vector3() @@ -52,23 +51,20 @@ function toQuaternion([x, y, z]: Vector3Tuple): Quaternion { export function computeTransformMatrix( propertiesSignal: Signal, node: FlexNode, + renameOutput?: Record, ): Signal { //B * O^-1 * T * O //B = bound transformation matrix //O = matrix to transform the origin for matrix T //T = transform matrix (translate, rotate, scale) - const get = createGetBatchedProperties( - propertiesSignal, - (key) => propertyKeys.includes(key), - transformAliasPropertyTransformation, - ) + const get = createGetBatchedProperties(propertiesSignal, propertyKeys, renameOutput) return computed(() => { const { pixelSize, relativeCenter } = node const [x, y] = relativeCenter.value const result = new Matrix4().makeTranslation(x * pixelSize, y * pixelSize, 0) - const tOriginX = get('transformOriginX') ?? 'center' - const tOriginY = get('transformOriginY') ?? 'center' + const tOriginX = (get('transformOriginX') ?? 'center') as keyof typeof alignmentXMap + const tOriginY = (get('transformOriginY') ?? 'center') as keyof typeof alignmentYMap let originCenter = true if (tOriginX != 'center' || tOriginY != 'center') { @@ -79,9 +75,9 @@ export function computeTransformMatrix( originVector.negate() } - const r: Vector3Tuple = [get(rX) ?? 0, get(rY) ?? 0, get(rZ) ?? 0] - const t: Vector3Tuple = [get(tX) ?? 0, -(get(tY) ?? 0), get(tZ) ?? 0] - const s: Vector3Tuple = [get(sX) ?? 1, get(sY) ?? 1, get(sZ) ?? 1] + const r: Vector3Tuple = [(get(rX) as number) ?? 0, (get(rY) as number) ?? 0, (get(rZ) as number) ?? 0] + const t: Vector3Tuple = [(get(tX) as number) ?? 0, -((get(tY) as number) ?? 0), (get(tZ) as number) ?? 0] + const s: Vector3Tuple = [(get(sX) as number) ?? 1, (get(sY) as number) ?? 1, (get(sZ) as number) ?? 1] if (t.some((v) => v != 0) || r.some((v) => v != 0) || s.some((v) => v != 1)) { result.multiply( matrixHelper.compose(tHelper.fromArray(t).multiplyScalar(pixelSize), toQuaternion(r), sHelper.fromArray(s)), diff --git a/packages/uikit/src/utils.tsx b/packages/uikit/src/utils.ts similarity index 67% rename from packages/uikit/src/utils.tsx rename to packages/uikit/src/utils.ts index a4d00860..2c30d6c2 100644 --- a/packages/uikit/src/utils.tsx +++ b/packages/uikit/src/utils.ts @@ -1,11 +1,10 @@ -import { RefObject, createContext, useContext, useEffect, useMemo } from 'react' import { computed, effect, Signal, signal } from '@preact/signals-core' -import { Vector2Tuple, BufferAttribute, Color, Group } from 'three' +import { Vector2Tuple, BufferAttribute, Color } from 'three' import { Color as ColorRepresentation } from '@react-three/fiber' import { Inset } from './flex/node.js' -import { Properties } from './properties/utils.js' -import { Yoga, loadYoga } from 'yoga-layout/wasm-async' +import { Yoga, loadYoga as loadYogaImpl } from 'yoga-layout/wasm-async' import { MergedProperties } from './properties/merged.js' +import { Properties } from './properties/default.js' export type Subscriptions = Array<() => void> @@ -20,46 +19,16 @@ export const alignmentXMap = { left: 0.5, center: 0, right: -0.5 } export const alignmentYMap = { top: -0.5, center: 0, bottom: 0.5 } export const alignmentZMap = { back: -0.5, center: 0, front: 0.5 } -export function useSignalEffect(fn: () => (() => void) | void, deps: Array) { - // eslint-disable-next-line react-hooks/exhaustive-deps - const unsubscribe = useMemo(() => effect(fn), deps) - useEffect(() => unsubscribe, [unsubscribe]) -} - let yoga: Signal | undefined -export function useLoadYoga(): Signal { +export function loadYoga(): Signal { if (yoga == null) { const result = (yoga = signal(undefined)) - loadYoga().then((value) => (result.value = value)) + loadYogaImpl().then((value) => (result.value = value)) } return yoga } -export function useResourceWithParams>( - fn: (param: P, ...additional: A) => Promise, - param: Signal

| P, - ...additionals: A -): Signal { - const result = useMemo(() => signal(undefined), []) - useEffect(() => { - if (!(param instanceof Signal)) { - let canceled = false - fn(param, ...additionals).then((value) => (canceled ? undefined : (result.value = value))) - return () => (canceled = true) - } - return effect(() => { - let canceled = false - fn(param.value, ...additionals) - .then((value) => (canceled ? undefined : (result.value = value))) - .catch(console.error) - return () => (canceled = true) - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [param, ...additionals]) - return result -} - /** * calculates the offsetX, offsetY, and scale to fit content with size [aspectRatio, 1] inside */ From 77936a873ee5277a577a6c57523d92c143ace24a Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Thu, 21 Mar 2024 17:48:33 +0100 Subject: [PATCH 03/20] vanilla root, container, and image implemented --- .gitignore | 4 +- examples/vanilla/index.html | 13 + examples/vanilla/index.ts | 61 ++++ examples/vanilla/package.json | 14 + examples/vanilla/vite.config.ts | 13 + packages/react/LICENSE | 15 + packages/react/package.json | 56 +++ packages/react/src/clipping.ts | 9 - packages/react/src/container.tsx | 95 +++++ packages/react/src/context.tsx | 16 + packages/react/src/default.tsx | 3 + packages/react/src/image.tsx | 0 packages/react/src/index.ts | 21 +- packages/react/src/order.ts | 3 - packages/react/src/responsive.ts | 7 +- packages/react/src/root.tsx | 112 ++++++ packages/react/src/text.ts | 7 - packages/react/src/utils.ts | 29 -- packages/react/src/utilts.tsx | 36 ++ packages/react/tsconfig.json | 9 + packages/uikit/package.json | 31 +- packages/uikit/src/active.ts | 52 +-- packages/uikit/src/allocation/index.ts | 1 + packages/uikit/src/clipping.ts | 22 +- packages/uikit/src/components/container.ts | 277 +++++++++------ packages/uikit/src/components/image.ts | 328 ++++++++++++++++++ packages/uikit/src/components/index.ts | 4 + packages/uikit/src/components/root.ts | 292 ++++++++++++++++ packages/uikit/src/components/utils.ts | 34 +- packages/uikit/src/context.ts | 29 ++ packages/uikit/src/cursor.ts | 18 - packages/uikit/src/dark.ts | 25 +- packages/uikit/src/events.ts | 23 ++ packages/uikit/src/flex/node.ts | 41 +-- packages/uikit/src/hover.ts | 94 ++--- packages/uikit/src/index.ts | 12 +- packages/uikit/src/internals.ts | 19 + packages/uikit/src/listeners.ts | 28 +- packages/uikit/src/order.ts | 14 +- packages/uikit/src/panel/index.ts | 5 + .../uikit/src/panel/instanced-panel-group.ts | 110 ++++-- .../uikit/src/panel/instanced-panel-mesh.ts | 77 +++- packages/uikit/src/panel/instanced-panel.ts | 63 ++-- .../uikit/src/panel/interaction-panel-mesh.ts | 12 +- packages/uikit/src/panel/panel-material.ts | 91 +++-- packages/uikit/src/panel/react.ts | 196 ----------- packages/uikit/src/panel/utils.ts | 45 ++- packages/uikit/src/properties/default.ts | 25 +- packages/uikit/src/properties/index.ts | 5 + packages/uikit/src/properties/merged.ts | 9 + packages/uikit/src/react/container.tsx | 155 --------- packages/uikit/src/react/content.tsx | 239 ------------- packages/uikit/src/react/custom.tsx | 130 ------- packages/uikit/src/react/fullscreen.tsx | 64 ---- packages/uikit/src/react/icon.tsx | 190 ---------- packages/uikit/src/react/image.tsx | 254 -------------- packages/uikit/src/react/index.ts | 11 - packages/uikit/src/react/input.tsx | 245 ------------- packages/uikit/src/react/portal.tsx | 142 -------- packages/uikit/src/react/react.ts | 28 -- packages/uikit/src/react/root.tsx | 264 -------------- packages/uikit/src/react/suspending.tsx | 12 - packages/uikit/src/react/svg.tsx | 255 -------------- packages/uikit/src/react/text.tsx | 123 ------- packages/uikit/src/react/utils.ts | 53 --- packages/uikit/src/responsive.ts | 35 +- packages/uikit/src/scroll.ts | 195 +++++------ packages/uikit/src/text/font.ts | 97 +++++- packages/uikit/src/text/index.ts | 5 + packages/uikit/src/text/layout.ts | 45 +++ packages/uikit/src/text/react.ts | 253 -------------- packages/uikit/src/text/render/index.ts | 5 + .../src/text/render/instanced-glyph-group.ts | 60 +++- .../uikit/src/text/render/instanced-glyph.ts | 86 ++++- .../uikit/src/text/render/instanced-text.ts | 3 +- packages/uikit/src/text/wrapper/index.ts | 2 +- packages/uikit/src/transform.ts | 24 +- packages/uikit/src/utils.ts | 16 +- packages/uikit/src/vanilla/container.ts | 84 +++++ packages/uikit/src/vanilla/image.ts | 98 ++++++ packages/uikit/src/vanilla/index.ts | 3 + packages/uikit/src/vanilla/root.ts | 98 ++++++ packages/uikit/src/vanilla/utils.ts | 9 + pnpm-lock.yaml | 210 +++-------- 84 files changed, 2577 insertions(+), 3421 deletions(-) create mode 100644 examples/vanilla/index.html create mode 100644 examples/vanilla/index.ts create mode 100644 examples/vanilla/package.json create mode 100644 examples/vanilla/vite.config.ts create mode 100644 packages/react/LICENSE create mode 100644 packages/react/package.json delete mode 100644 packages/react/src/clipping.ts create mode 100644 packages/react/src/container.tsx create mode 100644 packages/react/src/context.tsx create mode 100644 packages/react/src/image.tsx delete mode 100644 packages/react/src/order.ts create mode 100644 packages/react/src/root.tsx delete mode 100644 packages/react/src/text.ts delete mode 100644 packages/react/src/utils.ts create mode 100644 packages/react/src/utilts.tsx create mode 100644 packages/react/tsconfig.json create mode 100644 packages/uikit/src/allocation/index.ts create mode 100644 packages/uikit/src/components/image.ts create mode 100644 packages/uikit/src/components/index.ts create mode 100644 packages/uikit/src/components/root.ts create mode 100644 packages/uikit/src/context.ts delete mode 100644 packages/uikit/src/cursor.ts create mode 100644 packages/uikit/src/events.ts create mode 100644 packages/uikit/src/internals.ts create mode 100644 packages/uikit/src/panel/index.ts delete mode 100644 packages/uikit/src/panel/react.ts create mode 100644 packages/uikit/src/properties/index.ts delete mode 100644 packages/uikit/src/react/container.tsx delete mode 100644 packages/uikit/src/react/content.tsx delete mode 100644 packages/uikit/src/react/custom.tsx delete mode 100644 packages/uikit/src/react/fullscreen.tsx delete mode 100644 packages/uikit/src/react/icon.tsx delete mode 100644 packages/uikit/src/react/image.tsx delete mode 100644 packages/uikit/src/react/index.ts delete mode 100644 packages/uikit/src/react/input.tsx delete mode 100644 packages/uikit/src/react/portal.tsx delete mode 100644 packages/uikit/src/react/react.ts delete mode 100644 packages/uikit/src/react/root.tsx delete mode 100644 packages/uikit/src/react/suspending.tsx delete mode 100644 packages/uikit/src/react/svg.tsx delete mode 100644 packages/uikit/src/react/text.tsx delete mode 100644 packages/uikit/src/react/utils.ts create mode 100644 packages/uikit/src/text/index.ts delete mode 100644 packages/uikit/src/text/react.ts create mode 100644 packages/uikit/src/text/render/index.ts create mode 100644 packages/uikit/src/vanilla/container.ts create mode 100644 packages/uikit/src/vanilla/image.ts create mode 100644 packages/uikit/src/vanilla/index.ts create mode 100644 packages/uikit/src/vanilla/root.ts create mode 100644 packages/uikit/src/vanilla/utils.ts diff --git a/.gitignore b/.gitignore index 7f3358ca..8982f6f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules dist package-lock.json -.DS_Store \ No newline at end of file +.DS_Store +*.js +*.d.ts \ No newline at end of file diff --git a/examples/vanilla/index.html b/examples/vanilla/index.html new file mode 100644 index 00000000..1746e2e6 --- /dev/null +++ b/examples/vanilla/index.html @@ -0,0 +1,13 @@ + + + + + + + Document + + + + + + \ No newline at end of file diff --git a/examples/vanilla/index.ts b/examples/vanilla/index.ts new file mode 100644 index 00000000..96804fdd --- /dev/null +++ b/examples/vanilla/index.ts @@ -0,0 +1,61 @@ +import { BoxGeometry, Mesh, MeshNormalMaterial, PerspectiveCamera, Scene, WebGLRenderer } from 'three' +import { patchRenderOrder, Container, Root, Image } from '@react-three/uikit' +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' + +// init + +const camera = new PerspectiveCamera(70, 1, 0.01, 10) +camera.position.z = 1 + +const scene = new Scene() + +const canvas = document.getElementById('root') as HTMLCanvasElement +const controls = new OrbitControls(camera, canvas) + +//UI +const root = new Root(camera, scene, { + flexDirection: 'row', + gap: 10, + padding: 10, + sizeX: 1, + sizeY: 0.5, + backgroundColor: 'red', +}) +new Image(root, { flexBasis: 0, flexGrow: 1, border: 10, borderColor: 'green', src: 'https://picsum.photos/300/300' }) +new Container(root, { flexGrow: 1, backgroundColor: 'blue' }) +new Container(root, { flexGrow: 1, backgroundColor: 'green' }) + +const geometry = new BoxGeometry(0.2, 0.2, 0.2) +const material = new MeshNormalMaterial() + +const mesh = new Mesh(geometry, material) +scene.add(mesh) + +const renderer = new WebGLRenderer({ antialias: true, canvas }) +renderer.setAnimationLoop(animation) +renderer.localClippingEnabled = true +patchRenderOrder(renderer) + +function updateSize() { + renderer.setSize(window.innerWidth, window.innerHeight) + camera.aspect = window.innerWidth / window.innerHeight + camera.updateProjectionMatrix() +} + +updateSize() +window.addEventListener('resize', updateSize) + +// animation + +let prev: number | undefined +function animation(time: number) { + const delta = prev == null ? 0 : time - prev + prev = time + mesh.rotation.x = time / 2000 + mesh.rotation.y = time / 1000 + + root.update(delta) + controls.update(delta) + + renderer.render(scene, camera) +} diff --git a/examples/vanilla/package.json b/examples/vanilla/package.json new file mode 100644 index 00000000..95ecaca8 --- /dev/null +++ b/examples/vanilla/package.json @@ -0,0 +1,14 @@ +{ + "type": "module", + "dependencies": { + "@react-three/uikit": "workspace:^", + "react-dom": "^18.2.0", + "three": "^0.161.0" + }, + "scripts": { + "dev": "vite --host" + }, + "devDependencies": { + "@types/three": "^0.161.0" + } +} diff --git a/examples/vanilla/vite.config.ts b/examples/vanilla/vite.config.ts new file mode 100644 index 00000000..93888ac4 --- /dev/null +++ b/examples/vanilla/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import path from 'path' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + resolve: { + alias: [ + { find: '@', replacement: path.resolve(__dirname, '../../packages/kits/default') }, + { find: '@react-three/uikit', replacement: path.resolve(__dirname, '../../packages/uikit/src/index.ts') }, + ], + }, +}) diff --git a/packages/react/LICENSE b/packages/react/LICENSE new file mode 100644 index 00000000..8662f42e --- /dev/null +++ b/packages/react/LICENSE @@ -0,0 +1,15 @@ +Copyright 2024 Bela Bohlender + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Copyright 2023 Coconut Capital + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 00000000..a0bf0ad4 --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,56 @@ +{ + "name": "@react-three/uikit", + "type": "module", + "version": "0.0.0", + "license": "SEE LICENSE IN LICENSE", + "homepage": "https://github.com/pmndrs/uikit", + "author": "Bela Bohlender", + "keywords": [ + "r3f", + "uikit", + "three.js", + "userinterface", + "react", + "flexbox", + "yoga", + "typescript" + ], + "repository": { + "type": "git", + "url": "git@github.com:pmndrs/uikit.git" + }, + "files": [ + "dist" + ], + "main": "dist/index.js", + "bin": { + "uikit": "./dist/cli/index.js" + }, + "scripts": { + "build": "tsc", + "check:prettier": "prettier --check src scripts tests", + "check:eslint": "eslint 'src/**/*.{tsx,ts}'", + "fix:prettier": "prettier --write src scripts tests", + "fix:eslint": "eslint 'src/**/*.{tsx,ts}' --fix" + }, + "peerDependencies": { + "@react-three/fiber": ">=8", + "react": ">=18" + }, + "dependencies": { + "@preact/signals-core": "^1.5.1", + "@vanilla-three/uikit": "workspace:^" + }, + "devDependencies": { + "@react-three/drei": "^9.96.1", + "@react-three/fiber": "^8.15.13", + "@types/prompts": "^2.4.9", + "@types/react": "^18.2.47", + "@types/react-dom": "^18.2.18", + "@types/three": "^0.161.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "replace-in-files-cli": "^2.2.0", + "three": "^0.161.0" + } +} diff --git a/packages/react/src/clipping.ts b/packages/react/src/clipping.ts deleted file mode 100644 index 9522d5da..00000000 --- a/packages/react/src/clipping.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext, useContext } from "react" - -const ClippingRectContext = createContext>(null as any) - -export const ClippingRectProvider = ClippingRectContext.Provider - -export function useParentClippingRect(): Signal | undefined { - return useContext(ClippingRectContext) -} diff --git a/packages/react/src/container.tsx b/packages/react/src/container.tsx new file mode 100644 index 00000000..d13bd444 --- /dev/null +++ b/packages/react/src/container.tsx @@ -0,0 +1,95 @@ +import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' +import { forwardRef, ReactNode, useEffect, useMemo, useRef } from 'react' +import { Object3D } from 'three' +import { ParentProvider, useParent } from './context' +import { AddHandlers, AddScrollHandler } from './utilts' +import { + createContainer, + createInteractionPanel, + createListeners, + MergedProperties, + Subscriptions, + unsubscribeSubscriptions, + updateListeners, + EventHandlers as CoreEventHandlers, + updateContainerProperties, + createContainerPropertyTransfomers, + ContainerProperties, +} from '@vanilla-three/uikit/internals' +import { signal } from '@preact/signals-core' +import { useDefaultProperties } from './default' + +export const Container: ( + props: { + children?: ReactNode + } & ContainerProperties & + EventHandlers, +) => ReactNode = forwardRef((properties, ref) => { + //TODO: ComponentInternals + const outerRef = useRef(null) + const innerRef = useRef(null) + const parent = useParent() + const defaultProperties = useDefaultProperties() + const propertiesSignal = useMemo(() => signal(undefined as any), []) + const hoveredSignal = useMemo(() => signal>([]), []) + const activeSignal = useMemo(() => signal>([]), []) + const tranformers = useMemo( + () => createContainerPropertyTransfomers(parent.root.node.size, hoveredSignal, activeSignal), + [parent, hoveredSignal, activeSignal], + ) + const propertiesSubscriptions = useMemo(() => [], []) + unsubscribeSubscriptions(propertiesSubscriptions) + const handlers = updateContainerProperties( + propertiesSignal, + properties, + defaultProperties, + hoveredSignal, + activeSignal, + tranformers, + propertiesSubscriptions, + ) + + const listeners = useMemo(() => createListeners(), []) + updateListeners(listeners, properties) + + const scrollHandlers = useMemo(() => signal({}), []) + + const subscriptions = useMemo(() => [], []) + const ctx = useMemo( + () => createContainer(propertiesSignal, outerRef, innerRef, parent, scrollHandlers, listeners, subscriptions), + [listeners, parent, propertiesSignal, scrollHandlers, subscriptions], + ) + useEffect( + () => () => { + unsubscribeSubscriptions(propertiesSubscriptions) + unsubscribeSubscriptions(subscriptions) + }, + [propertiesSubscriptions, subscriptions], + ) + + //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) + + const interactionPanel = useMemo( + () => + createInteractionPanel( + ctx.node.size, + parent.root.pixelSize, + ctx.orderInfo, + parent.clippingRect, + ctx.root.object, + subscriptions, + ), + [ctx, parent, subscriptions], + ) + + return ( + + + + + + {properties.children} + + + ) +}) diff --git a/packages/react/src/context.tsx b/packages/react/src/context.tsx new file mode 100644 index 00000000..34a08638 --- /dev/null +++ b/packages/react/src/context.tsx @@ -0,0 +1,16 @@ +import { WithContext } from '@vanilla-three/uikit/internals' +import { createContext, useContext } from 'react' + +//const FontFamiliesContext = createContext>(null as any) + +const ParentContext = createContext(undefined) + +export function useParent(): WithContext { + const parent = useContext(ParentContext) + if (parent == null) { + throw new Error(`Cannot be used outside of a uikit component.`) + } + return parent +} + +export const ParentProvider = ParentContext.Provider diff --git a/packages/react/src/default.tsx b/packages/react/src/default.tsx index b21afe28..b3a831d1 100644 --- a/packages/react/src/default.tsx +++ b/packages/react/src/default.tsx @@ -1,3 +1,6 @@ +import { AllOptionalProperties } from '@vanilla-three/uikit' +import { ReactNode, createContext, useContext } from 'react' + const DefaultPropertiesContext = createContext(undefined) export function useDefaultProperties(): AllOptionalProperties | undefined { diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 8fed1ea2..7c3b2b26 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,15 +1,16 @@ +export { useRootSize } from './responsive.js' export { basedOnPreferredColorScheme, setPreferredColorScheme, getPreferredColorScheme, type PreferredColorScheme, -} from './dark.js' -export { FontFamilyProvider } from './text/react.js' -export { useRootSize } from './responsive.js' -export type { ComponentInternals } from './components/utils.js' -export type { MaterialClass } from './panel/react.js' -export type { LayoutListeners, ViewportListeners } from './components/utils.js' -export type { ScrollListeners } from './scroll.js' -export type { AllOptionalProperties } from './properties/default.js' -export { DefaultProperties } from './properties/default.js' -export * from './components/index.js' + type MaterialClass, + type ScrollListeners, + type LayoutListeners, + type ViewportListeners, + type Listeners, + type AllOptionalProperties, +} from '@vanilla-three/uikit' +export { DefaultProperties } from './default.js' +export * from './container.js' +export * from './root.js' diff --git a/packages/react/src/order.ts b/packages/react/src/order.ts deleted file mode 100644 index 4fed00eb..00000000 --- a/packages/react/src/order.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const OrderInfoContext = createContext(null as any) - -export const OrderInfoProvider = OrderInfoContext.Provider \ No newline at end of file diff --git a/packages/react/src/responsive.ts b/packages/react/src/responsive.ts index 9dd8c21b..493e6839 100644 --- a/packages/react/src/responsive.ts +++ b/packages/react/src/responsive.ts @@ -1,7 +1,4 @@ -export function useRootSize() { - return useContext(RootSizeContext) -} -const RootSizeContext = createContext>(null as any) +export function useRootSize() { -export const RootSizeProvider = RootSizeContext.Provider +} \ No newline at end of file diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx new file mode 100644 index 00000000..433ccbee --- /dev/null +++ b/packages/react/src/root.tsx @@ -0,0 +1,112 @@ +import { useFrame, useStore, useThree } from '@react-three/fiber' +import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' +import { forwardRef, ReactNode, useEffect, useMemo, useRef } from 'react' +import { ParentProvider } from './context' +import { AddHandlers, AddScrollHandler } from './utilts' +import { + MergedProperties, + RootProperties, + Subscriptions, + createInteractionPanel, + createListeners, + patchRenderOrder, + unsubscribeSubscriptions, + updateListeners, + createRoot, + updateRootProperties, + createRootPropertyTransformers, + EventHandlers as CoreEventHandlers, +} from '@vanilla-three/uikit/internals' +import { signal } from '@preact/signals-core' +import { Object3D, Vector2Tuple } from 'three' +import { useDefaultProperties } from './default' + +export const Root: ( + props: RootProperties & { + children?: ReactNode + } & EventHandlers, +) => ReactNode = forwardRef((properties, ref) => { + const renderer = useThree((state) => state.gl) + + useEffect(() => patchRenderOrder(renderer), [renderer]) + + const outerRef = useRef(null) + const innerRef = useRef(null) + const defaultProperties = useDefaultProperties() + const propertiesSignal = useMemo(() => signal(undefined as any), []) + const rootSize = useMemo(() => signal([0, 0]), []) + const hoveredSignal = useMemo(() => signal>([]), []) + const activeSignal = useMemo(() => signal>([]), []) + const tranformers = useMemo( + () => createRootPropertyTransformers(rootSize, hoveredSignal, activeSignal), + [rootSize, hoveredSignal, activeSignal], + ) + const propertiesSubscriptions = useMemo(() => [], []) + unsubscribeSubscriptions(propertiesSubscriptions) + const handlers = updateRootProperties( + propertiesSignal, + properties, + defaultProperties, + hoveredSignal, + activeSignal, + tranformers, + propertiesSubscriptions, + ) + + const listeners = useMemo(() => createListeners(), []) + updateListeners(listeners, properties) + + const scrollHandlers = useMemo(() => signal({}), []) + + const subscriptions = useMemo(() => [], []) + const onFrameSet = useMemo(() => new Set<(delta: number) => void>(), []) + const store = useStore() + const ctx = useMemo( + () => + createRoot( + propertiesSignal, + rootSize, + outerRef, + innerRef, + scrollHandlers, + listeners, + properties.pixelSize, + onFrameSet, + () => store.getState().camera, + subscriptions, + ), + [listeners, onFrameSet, properties.pixelSize, propertiesSignal, rootSize, scrollHandlers, store, subscriptions], + ) + useEffect( + () => () => { + unsubscribeSubscriptions(propertiesSubscriptions) + unsubscribeSubscriptions(subscriptions) + }, + [propertiesSubscriptions, subscriptions], + ) + + useFrame((_, delta) => { + for (const onFrame of onFrameSet) { + onFrame(delta) + } + }) + + //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) + + const interactionPanel = useMemo( + () => + createInteractionPanel(ctx.node.size, ctx.pixelSize, ctx.orderInfo, undefined, ctx.root.object, subscriptions), + [ctx, subscriptions], + ) + + return ( + + + + + + {properties.children} + + + ) +}) diff --git a/packages/react/src/text.ts b/packages/react/src/text.ts deleted file mode 100644 index d0cca855..00000000 --- a/packages/react/src/text.ts +++ /dev/null @@ -1,7 +0,0 @@ - - -const InstancedGlyphContext = createContext(null as any) - -export const InstancedGlyphProvider = InstancedGlyphContext.Provider - -const FontFamiliesContext = createContext>(null as any) \ No newline at end of file diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts deleted file mode 100644 index 8f414fdb..00000000 --- a/packages/react/src/utils.ts +++ /dev/null @@ -1,29 +0,0 @@ -export function useSignalEffect(fn: () => (() => void) | void, deps: Array) { - // eslint-disable-next-line react-hooks/exhaustive-deps - const unsubscribe = useMemo(() => effect(fn), deps) - useEffect(() => unsubscribe, [unsubscribe]) -} - -export function useResourceWithParams>( - fn: (param: P, ...additional: A) => Promise, - param: Signal

| P, - ...additionals: A -): Signal { - const result = useMemo(() => signal(undefined), []) - useEffect(() => { - if (!(param instanceof Signal)) { - let canceled = false - fn(param, ...additionals).then((value) => (canceled ? undefined : (result.value = value))) - return () => (canceled = true) - } - return effect(() => { - let canceled = false - fn(param.value, ...additionals) - .then((value) => (canceled ? undefined : (result.value = value))) - .catch(console.error) - return () => (canceled = true) - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [param, ...additionals]) - return result -} diff --git a/packages/react/src/utilts.tsx b/packages/react/src/utilts.tsx new file mode 100644 index 00000000..98863d71 --- /dev/null +++ b/packages/react/src/utilts.tsx @@ -0,0 +1,36 @@ +import { Signal, effect } from '@preact/signals-core' +import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' +import { ReactNode, forwardRef, useEffect, useMemo, useState } from 'react' +import { Object3D } from 'three' + +export const AddHandlers = forwardRef( + ({ handlers, children }, ref) => { + return ( + + {children} + + ) + }, +) + +export function AddScrollHandler({ + handlers, + children, +}: { + handlers: Signal + children?: ReactNode +}) { + const [scrollHandlers, setScrollHandlers] = useState(() => handlers.value) + useSignalEffect(() => setScrollHandlers(handlers.value), [handlers]) + return ( + + {children} + + ) +} + +export function useSignalEffect(fn: () => (() => void) | void, deps: Array) { + // eslint-disable-next-line react-hooks/exhaustive-deps + const unsubscribe = useMemo(() => effect(fn), deps) + useEffect(() => unsubscribe, [unsubscribe]) +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 00000000..7fa05329 --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "skipLibCheck": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/uikit/package.json b/packages/uikit/package.json index 349f7602..3303bc18 100644 --- a/packages/uikit/package.json +++ b/packages/uikit/package.json @@ -1,30 +1,29 @@ { - "name": "@react-three/uikit", + "name": "@vanilla-three/uikit", "type": "module", "version": "0.0.0", "license": "SEE LICENSE IN LICENSE", - "homepage": "https://github.com/pmndrs/react-three-uikit", + "homepage": "https://github.com/pmndrs/uikit", "author": "Bela Bohlender", "keywords": [ - "r3f", "uikit", "three.js", "userinterface", - "react", "flexbox", "yoga", "typescript" ], "repository": { "type": "git", - "url": "git@github.com:pmndrs/react-three-uikit.git" + "url": "git@github.com:pmndrs/uikit.git" }, "files": [ "dist" ], "main": "dist/index.js", - "bin": { - "uikit": "./dist/cli/index.js" + "exports": { + ".": "./dist/index.js", + "./internals": "./dist/internals.js" }, "scripts": { "test": "mocha ./tests/allocation.spec.ts", @@ -36,30 +35,14 @@ "fix:eslint": "eslint 'src/**/*.{tsx,ts}' --fix" }, "peerDependencies": { - "@react-three/fiber": ">=8", - "react": ">=18", "three": ">=0.160" }, "dependencies": { "@preact/signals-core": "^1.5.1", - "chalk": "^5.3.0", - "commander": "^12.0.0", - "ora": "^8.0.1", - "prompts": "^2.4.2", - "yoga-layout": "^2.0.1", - "zod": "^3.22.4" + "yoga-layout": "^2.0.1" }, "devDependencies": { - "@react-three/drei": "^9.96.1", - "@react-three/fiber": "^8.15.13", - "@types/node": "^20.11.0", - "@types/prompts": "^2.4.9", - "@types/react": "^18.2.47", - "@types/react-dom": "^18.2.18", "@types/three": "^0.161.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "replace-in-files-cli": "^2.2.0", "three": "^0.161.0" } } diff --git a/packages/uikit/src/active.ts b/packages/uikit/src/active.ts index cafacf68..4be27270 100644 --- a/packages/uikit/src/active.ts +++ b/packages/uikit/src/active.ts @@ -1,8 +1,8 @@ -import { signal } from '@preact/signals-core' -import type { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' +import { Signal } from '@preact/signals-core' import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' -import { MergedProperties } from './properties/merged.js' +import { addHandler } from './panel/instanced-panel-mesh.js' +import { EventHandlers, ThreeEvent } from './events.js' export type WithActive = T & { active?: T @@ -11,45 +11,45 @@ export type WithActive = T & { export type ActiveEventHandlers = Pick -export function applyActiveProperties( - merged: MergedProperties, - defaultProperties: AllOptionalProperties | undefined, +export function addActiveHandlers( + target: EventHandlers, properties: WithClasses> & EventHandlers, -): ActiveEventHandlers | undefined { - const activeSignal = signal>([]) - // eslint-disable-next-line react-hooks/exhaustive-deps - const translate = createConditionalPropertyTranslator(() => activeSignal.value.length > 0) + defaultProperties: AllOptionalProperties | undefined, + activeSignal: Signal>, +): void { let activePropertiesExist = false traverseProperties(defaultProperties, properties, (p) => { - if (p.active == null) { - return + if ('active' in p) { + activePropertiesExist = true } - activePropertiesExist = true - translate(merged, p.active) }) if (!activePropertiesExist && properties.onActiveChange == null) { //no need to listen to hover activeSignal.value.length = 0 - return undefined + return } - const onLeave = (e: ThreeEvent) => { - activeSignal.value = activeSignal.value.filter((id) => id != e.pointerId) + const onLeave = ({ nativeEvent }: ThreeEvent) => { + activeSignal.value = activeSignal.value.filter((id) => id != nativeEvent.pointerId) if (properties.onActiveChange == null || activeSignal.value.length > 0) { return } properties.onActiveChange(false) } + addHandler('onPointerDown', target, ({ nativeEvent }) => { + activeSignal.value = [nativeEvent.pointerId, ...activeSignal.value] + if (properties.onActiveChange == null || activeSignal.value.length != 1) { + return + } + properties.onActiveChange(true) + }) + addHandler('onPointerUp', target, onLeave) + addHandler('onPointerLeave', target, onLeave) +} + +export function createActivePropertyTransfomers(activeSignal: Signal>) { return { - onPointerDown: (e) => { - activeSignal.value = [e.pointerId, ...activeSignal.value] - if (properties.onActiveChange == null || activeSignal.value.length != 1) { - return - } - properties.onActiveChange(true) - }, - onPointerUp: onLeave, - onPointerLeave: onLeave, + active: createConditionalPropertyTranslator(() => activeSignal.value.length > 0), } } diff --git a/packages/uikit/src/allocation/index.ts b/packages/uikit/src/allocation/index.ts new file mode 100644 index 00000000..6c07d138 --- /dev/null +++ b/packages/uikit/src/allocation/index.ts @@ -0,0 +1 @@ +export * from './sorted-buckets' diff --git a/packages/uikit/src/clipping.ts b/packages/uikit/src/clipping.ts index 6890f9da..51e5a7ac 100644 --- a/packages/uikit/src/clipping.ts +++ b/packages/uikit/src/clipping.ts @@ -3,6 +3,7 @@ import { Group, Matrix4, Plane, Vector3 } from 'three' import type { Vector2Tuple } from 'three' import { Inset } from './flex/node.js' import { Overflow } from 'yoga-layout/wasm-async' +import { Object3DRef } from './context.js' const dotLt45deg = Math.cos((45 / 180) * Math.PI) @@ -97,7 +98,7 @@ export function computeIsClipped( parentClippingRect: Signal | undefined, globalMatrix: Signal, size: Signal, - psRef: { pixelSize: number }, + pixelSize: number, ): Signal { return computed(() => { const global = globalMatrix.value @@ -108,7 +109,7 @@ export function computeIsClipped( 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(global) + helperPoints[i].set(mx * pixelSize * width, my * pixelSize * height, 0).applyMatrix4(global) } const { planes } = rect @@ -136,7 +137,7 @@ export function computeClippingRect( size: Signal, borderInset: Signal, overflow: Signal, - psRef: { pixelSize: number }, + pixelSize: number, parentClippingRect: Signal | undefined, ): Signal { return computed(() => { @@ -148,10 +149,10 @@ export function computeClippingRect( const [top, right, bottom, left] = borderInset.value const rect = new ClippingRect( global, - ((right - left) * psRef.pixelSize) / 2, - ((top - bottom) * psRef.pixelSize) / 2, - (width - left - right) * psRef.pixelSize, - (height - top - bottom) * psRef.pixelSize, + ((right - left) * pixelSize) / 2, + ((top - bottom) * pixelSize) / 2, + (width - left - right) * pixelSize, + (height - top - bottom) * pixelSize, ) if (parentClippingRect?.value != null) { rect.min(parentClippingRect.value) @@ -173,9 +174,12 @@ export function createGlobalClippingPlanes() { export function updateGlobalClippingPlanes( clippingRect: Signal | undefined, - rootGroup: Group, + rootObject: Object3DRef, clippingPlanes: Array, ): void { + if (rootObject.current == null) { + return + } const localPlanes = clippingRect?.value?.planes if (localPlanes == null) { for (let i = 0; i < 4; i++) { @@ -184,6 +188,6 @@ export function updateGlobalClippingPlanes( return } for (let i = 0; i < 4; i++) { - clippingPlanes[i].copy(localPlanes[i]).applyMatrix4(rootGroup.matrixWorld) + clippingPlanes[i].copy(localPlanes[i]).applyMatrix4(rootObject.current.matrixWorld) } } diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 97af2375..82e6f62d 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -1,127 +1,174 @@ -import { WithReactive } from '../properties/utils.js' -import { - PanelGroupDependencies, - computePanelGroupDependencies, - createInstancePanel, - createInteractionPanel, -} from '../panel/react.js' import { YogaProperties } from '../flex/node.js' -import { applyHoverProperties } from '../hover.js' +import { addHoverHandlers, createHoverPropertyTransformers } from '../hover.js' import { computeIsClipped, computeClippingRect } from '../clipping.js' -import { ScrollbarProperties, computeGlobalScrollMatrix, createScrollPosition, createScrollbars } from '../scroll.js' +import { + ScrollbarProperties, + applyScrollPosition, + computeGlobalScrollMatrix, + createScrollPosition, + createScrollbars, + setupScrollHandler, +} from '../scroll.js' import { WithAllAliases } from '../properties/alias.js' -import { InstancedPanel, PanelProperties } from '../panel/instanced-panel.js' -import { TransformProperties, computeTransformMatrix } from '../transform.js' -import { Properties, WithClasses } from '../properties/default.js' -import { applyResponsiveProperties } from '../responsive.js' -import { ElementType, computeOrderInfo } from '../order.js' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' -import { Signal, signal } from '@preact/signals-core' -import { computeGlobalMatrix } from './utils.js' -import { WithConditionals } from '../react/utils.js' -import { Subscriptions } from '../utils.js' -import { MergedProperties } from '../properties/merged.js' -import { LayoutListeners, ViewportListeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' - -export type ContainerProperties = WithConditionals< +import { PanelProperties, createInstancedPanel } from '../panel/instanced-panel.js' +import { TransformProperties, applyTransform, computeTransformMatrix } from '../transform.js' +import { AllOptionalProperties, Properties, WithClasses, WithReactive } from '../properties/default.js' +import { createResponsivePropertyTransformers } from '../responsive.js' +import { ElementType, ZIndexOffset, computeOrderInfo } from '../order.js' +import { preferredColorSchemePropertyTransformers } from '../dark.js' +import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' +import { Signal } from '@preact/signals-core' +import { WithConditionals, computeGlobalMatrix } from './utils.js' +import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' +import { MergedProperties, PropertyTransformers } from '../properties/merged.js' +import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' +import { Object3DRef, WithContext } from '../context.js' +import { ShadowProperties, computePanelGroupDependencies } from '../panel/instanced-panel-group.js' +import { cloneHandlers } from '../panel/instanced-panel-mesh.js' +import { MaterialClass } from '../panel/panel-material.js' +import { Vector2Tuple } from 'three' +import { EventHandlers } from '../events.js' + +export type InheritableContainerProperties = WithConditionals< WithClasses< - WithAllAliases & ScrollbarProperties> + WithAllAliases< + WithReactive< + YogaProperties & + PanelProperties & + TransformProperties & { + zIndexOffset?: ZIndexOffset + panelMaterialClass?: MaterialClass + } & ScrollbarProperties & + ShadowProperties + > + > > > -export class Container { - //undefined as any is okay here since the value of the signal will be overwritten before its use - private propertiesSignal: Signal = signal(undefined as any) - private subscriptions: Subscriptions = [] - - private listeners: LayoutListeners & ViewportListeners = {} - - constructor(properties: Properties, defaultProperties?: Properties) { - this.setProperties(properties, defaultProperties) - - //setup the container - const node = parentNode.createChild(this.propertiesSignal, groupRef, subscriptions) - parentNode.addChild(node) - - const transformMatrix = computeTransformMatrix(this.propertiesSignal, node) - const globalMatrix = computeGlobalMatrix(parentMatrix, transformMatrix) - const isClipped = computeIsClipped(parentClippingRect, globalMatrix, node.size, node) - const groupDeps = computePanelGroupDependencies(this.propertiesSignal) - - const orderInfo = computeOrderInfo(this.propertiesSignal, ElementType.Panel, groupDeps, parentOrderInfo) - - createInstancePanel( - this.propertiesSignal, - orderInfo, - groupDeps, - getInstancedPanelGroup, - globalMatrix, - node.size, - undefined, - node.borderInset, - parentClippingRect, - isClipped, - undefined, - this.subscriptions, - ) - - const scrollPosition = createScrollPosition() - const globalScrollMatrix = computeGlobalScrollMatrix(scrollPosition, node, globalMatrix) - createScrollbars( - this.propertiesSignal, - scrollPosition, - node, - globalMatrix, - isClipped, - properties.scrollbarPanelMaterialClass, - parentClippingRect, - orderInfo, - getInstancedPanelGroup, - this.subscriptions, - ) - - const clippingRect = computeClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - node, - parentClippingRect, - ) - - const interactionPanel = createInteractionPanel( - node.size, - node, - orderInfo, - parentClippingRect, - rootGroupRef, - this.subscriptions, - ) - - setupLayoutListeners(this.listeners, node.size, this.subscriptions) - setupViewportListeners(this.listeners, isClipped, this.subscriptions) - - this.subscriptions.push(() => { - parentNode.removeChild(node) - node.destroy() - }) - } +export type ContainerProperties = InheritableContainerProperties & Listeners & EventHandlers + +export function createContainer( + propertiesSignal: Signal, + object: Object3DRef, + childrenContainer: Object3DRef, + parent: WithContext, + scrollHandlers: Signal, + listeners: Listeners, + subscriptions: Subscriptions, +): WithContext { + const node = parent.node.createChild(propertiesSignal, object, subscriptions) + parent.node.addChild(node) + + const transformMatrix = computeTransformMatrix(propertiesSignal, node, parent.root.pixelSize) + applyTransform(object, transformMatrix, subscriptions) + + const globalMatrix = computeGlobalMatrix(parent.matrix, transformMatrix) + + const isClipped = computeIsClipped(parent.clippingRect, globalMatrix, node.size, parent.root.pixelSize) + const groupDeps = computePanelGroupDependencies(propertiesSignal) + + const orderInfo = computeOrderInfo(propertiesSignal, ElementType.Panel, groupDeps, parent.orderInfo) + + createInstancedPanel( + propertiesSignal, + orderInfo, + groupDeps, + parent.root.panelGroupManager, + globalMatrix, + node.size, + undefined, + node.borderInset, + parent.clippingRect, + isClipped, + subscriptions, + ) - setProperties(properties: Properties, defaultProperties?: Properties): void { - const merged = new MergedProperties() - addToMerged(collection, defaultProperties, properties) - applyPreferredColorSchemeProperties(collection, defaultProperties, properties) - applyResponsiveProperties(collection, defaultProperties, properties, rootSize) - const hoverHandlers = applyHoverProperties(collection, defaultProperties, properties) - const activeHandlers = applyActiveProperties(collection, defaultProperties, properties) - this.propertiesSignal.value = merged + const scrollPosition = createScrollPosition() + applyScrollPosition(childrenContainer, scrollPosition, parent.root.pixelSize) + const matrix = computeGlobalScrollMatrix(scrollPosition, globalMatrix, parent.root.pixelSize) + createScrollbars( + propertiesSignal, + scrollPosition, + node, + globalMatrix, + isClipped, + parent.clippingRect, + orderInfo, + parent.root.panelGroupManager, + subscriptions, + ) + + const clippingRect = computeClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parent.root.pixelSize, + parent.clippingRect, + ) + + setupLayoutListeners(listeners, node.size, subscriptions) + setupViewportListeners(listeners, isClipped, subscriptions) + + const onScrollFrame = setupScrollHandler( + node, + scrollPosition, + object, + listeners, + parent.root.pixelSize, + scrollHandlers, + subscriptions, + ) + parent.root.onFrameSet.add(onScrollFrame) + + subscriptions.push(() => { + parent.root.onFrameSet.delete(onScrollFrame) + parent.node.removeChild(node) + node.destroy() + }) + + return { + isClipped, + clippingRect, + matrix, + node, + object, + orderInfo, + root: parent.root, } +} - destroy() { - const subscriptionsLength = this.subscriptions.length - for (let i = 0; i < subscriptionsLength; i++) { - this.subscriptions[i]() - } +export function createContainerPropertyTransfomers( + rootSize: Signal, + hoveredSignal: Signal>, + activeSignal: Signal>, +): PropertyTransformers { + return { + ...preferredColorSchemePropertyTransformers, + ...createResponsivePropertyTransformers(rootSize), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), } } + +export function updateContainerProperties( + propertiesSignal: Signal, + properties: Properties, + defaultProperties: AllOptionalProperties | undefined, + hoveredSignal: Signal>, + activeSignal: Signal>, + transformers: PropertyTransformers, + propertiesSubscriptions: Subscriptions, +) { + //build merged properties + const merged = new MergedProperties(transformers) + merged.addAll(defaultProperties, properties) + propertiesSignal.value = merged + + //build handlers + const handlers = cloneHandlers(properties) + unsubscribeSubscriptions(propertiesSubscriptions) + addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal, propertiesSubscriptions) + addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + return handlers +} diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts new file mode 100644 index 00000000..afd56318 --- /dev/null +++ b/packages/uikit/src/components/image.ts @@ -0,0 +1,328 @@ +import { Signal, computed, effect } from '@preact/signals-core' +import { Mesh, MeshBasicMaterial, PlaneGeometry, SRGBColorSpace, Texture, TextureLoader, Vector2Tuple } from 'three' +import { Listeners } from '..' +import { Object3DRef, WithContext } from '../context' +import { Inset, YogaProperties } from '../flex' +import { ElementType, ZIndexOffset, computeOrderInfo, setupRenderOrder } from '../order' +import { PanelProperties } from '../panel/instanced-panel' +import { ShadowProperties } from '../panel/instanced-panel-group' +import { MaterialClass, setupPanelMaterials } from '../panel/panel-material' +import { WithAllAliases } from '../properties/alias' +import { AllOptionalProperties, Properties, WithClasses, WithReactive } from '../properties/default' +import { + ScrollbarProperties, + applyScrollPosition, + computeGlobalScrollMatrix, + createScrollPosition, + createScrollbars, + setupScrollHandler, +} from '../scroll' +import { TransformProperties, applyTransform, computeTransformMatrix } from '../transform' +import { WithConditionals, computeGlobalMatrix, loadResourceWithParams } from './utils' +import { MergedProperties, PropertyTransformers } from '../properties/merged' +import { Subscriptions, unsubscribeSubscriptions } from '../utils' +import { computeIsPanelVisible, panelGeometry } from '../panel/utils' +import { setupImmediateProperties } from '../properties/immediate' +import { makeClippedRaycast, makePanelRaycast } from '../panel/interaction-panel-mesh' +import { + computeIsClipped, + computeClippingRect, + createGlobalClippingPlanes, + updateGlobalClippingPlanes, +} from '../clipping' +import { setupLayoutListeners, setupViewportListeners } from '../listeners' +import { createGetBatchedProperties } from '../properties/batched' +import { addActiveHandlers, createActivePropertyTransfomers } from '../active' +import { addHoverHandlers, createHoverPropertyTransformers } from '../hover' +import { cloneHandlers } from '../panel/instanced-panel-mesh' +import { preferredColorSchemePropertyTransformers } from '../dark' +import { createResponsivePropertyTransformers } from '../responsive' +import { EventHandlers } from '../events' + +export type ImageFit = 'cover' | 'fill' +const FIT_DEFAULT: ImageFit = 'fill' + +export type InheritableImageProperties = WithConditionals< + WithClasses< + WithAllAliases< + WithReactive< + YogaProperties & + Omit & { + opacity?: number + fit?: ImageFit + panelMaterialClass?: MaterialClass + zIndexOffset?: ZIndexOffset + keepAspectRatio?: boolean + } & TransformProperties & + ShadowProperties + > & + ScrollbarProperties + > + > +> + +export type ImageProperties = InheritableImageProperties & Listeners & EventHandlers & { src: Signal | string } + +const shadowProperties = ['castShadow', 'receiveShadow'] + +export function createImage( + propertiesSignal: Signal, + object: Object3DRef, + childrenContainer: Object3DRef, + parent: WithContext, + scrollHandlers: Signal, + listeners: Listeners, + subscriptions: Subscriptions, +): WithContext { + const node = parent.node.createChild(propertiesSignal, object, subscriptions) + parent.node.addChild(node) + + const transformMatrix = computeTransformMatrix(propertiesSignal, node, parent.root.pixelSize) + applyTransform(object, transformMatrix, subscriptions) + + const globalMatrix = computeGlobalMatrix(parent.matrix, transformMatrix) + + const isClipped = computeIsClipped(parent.clippingRect, globalMatrix, node.size, parent.root.pixelSize) + + const orderInfo = computeOrderInfo(propertiesSignal, ElementType.Image, undefined, parent.orderInfo) + + const scrollPosition = createScrollPosition() + applyScrollPosition(childrenContainer, scrollPosition, parent.root.pixelSize) + const matrix = computeGlobalScrollMatrix(scrollPosition, globalMatrix, parent.root.pixelSize) + createScrollbars( + propertiesSignal, + scrollPosition, + node, + globalMatrix, + isClipped, + parent.clippingRect, + orderInfo, + parent.root.panelGroupManager, + subscriptions, + ) + + const clippingRect = computeClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parent.root.pixelSize, + parent.clippingRect, + ) + + setupLayoutListeners(listeners, node.size, subscriptions) + setupViewportListeners(listeners, isClipped, subscriptions) + + const onScrollFrame = setupScrollHandler( + node, + scrollPosition, + object, + listeners, + parent.root.pixelSize, + scrollHandlers, + subscriptions, + ) + parent.root.onFrameSet.add(onScrollFrame) + + subscriptions.push(() => { + parent.root.onFrameSet.delete(onScrollFrame) + parent.node.removeChild(node) + node.destroy() + }) + + return { + isClipped, + clippingRect, + matrix, + node, + object, + orderInfo, + root: parent.root, + } +} + +export function computeTextureAspectRatio(texture: Signal) { + return computed(() => { + const tex = texture.value + if (tex == null) { + return undefined + } + const image = tex.source.data as { width: number; height: number } + return image.width / image.height + }) +} + +export function createImagePropertyTransformers( + rootSize: Signal, + hoveredSignal: Signal>, + activeSignal: Signal>, + textureAspectRatio: Signal, +): PropertyTransformers { + return { + keepAspectRatio: (value, target) => target.add('aspectRatio', value === false ? undefined : textureAspectRatio), + ...preferredColorSchemePropertyTransformers, + ...createResponsivePropertyTransformers(rootSize), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + } +} + +export function updateImageProperties( + propertiesSignal: Signal, + textureAspectRatio: Signal, + properties: Properties, + defaultProperties: AllOptionalProperties | undefined, + hoveredSignal: Signal>, + activeSignal: Signal>, + transformers: PropertyTransformers, + subscriptions: Subscriptions, +) { + //build merged properties + const merged = new MergedProperties(transformers) + merged.add('backgroundColor', 0xffffff) + merged.add('aspectRatio', textureAspectRatio) + merged.addAll(defaultProperties, properties) + propertiesSignal.value = merged + + //build handlers + const handlers = cloneHandlers(properties) + addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal, subscriptions) + addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + return handlers +} + +export function createImageMesh( + propertiesSignal: Signal, + texture: Signal, + parent: WithContext, + { node, orderInfo, root, clippingRect, isClipped }: WithContext, + subscriptions: Subscriptions, +) { + const mesh = new Mesh(panelGeometry) + mesh.matrixAutoUpdate = false + const clippingPlanes = createGlobalClippingPlanes() + const updateClippingPlanes = () => updateGlobalClippingPlanes(clippingRect, root.object, clippingPlanes) + root.onFrameSet.add(updateClippingPlanes) + subscriptions.push(() => root.onFrameSet.delete(updateClippingPlanes)) + setupPanelMaterials(propertiesSignal, mesh, node.size, node.borderInset, isClipped, clippingPlanes, subscriptions) + const isVisible = computeIsPanelVisible(propertiesSignal, node.borderInset, node.size, isClipped) + setupImmediateProperties( + propertiesSignal, + isVisible, + (key) => shadowProperties.includes(key), + (key, value) => (mesh[key as 'castShadow' | 'receiveShadow'] = (value as boolean | undefined) ?? false), + subscriptions, + ) + mesh.raycast = makeClippedRaycast(mesh, makePanelRaycast(mesh), root.object, parent.clippingRect, orderInfo) + subscriptions.push(effect(() => setupRenderOrder(mesh, root, orderInfo.value))) + + setupTextureFit(propertiesSignal, texture, node.borderInset, node.size, subscriptions) + + subscriptions.push(() => effect(() => (mesh.visible = isVisible.value))) + + subscriptions.push( + effect(() => { + const map = texture.value ?? null + if (mesh.material.map === map) { + return + } + mesh.material.map = map + mesh.material.needsUpdate = true + }), + ) + + subscriptions.push( + effect(() => { + const [width, height] = node.size.value + const pixelSize = parent.root.pixelSize + mesh.scale.set(width * pixelSize, height * pixelSize, 1) + mesh.updateMatrix() + }), + ) + return mesh +} + +const propertyKeys = ['fit'] + +function setupTextureFit( + propertiesSignal: Signal, + textureSignal: Signal, + borderInset: Signal, + size: Signal, + subscriptions: Subscriptions, +): void { + const get = createGetBatchedProperties(propertiesSignal, propertyKeys) + subscriptions.push( + effect(() => { + const texture = textureSignal.value + if (texture == null) { + return + } + const fit = (get('fit') as ImageFit | undefined) ?? FIT_DEFAULT + texture.matrix.identity() + + if (fit === 'fill' || texture == null) { + transformInsideBorder(borderInset, size, texture) + return + } + + const { width: textureWidth, height: textureHeight } = texture.source.data as { width: number; height: number } + const textureRatio = textureWidth / textureHeight + + const [width, height] = size.value + const [top, right, bottom, left] = borderInset.value + const boundsRatioValue = (width - left - right) / (height - top - bottom) + + if (textureRatio > boundsRatioValue) { + texture.matrix + .translate(-(0.5 * (boundsRatioValue - textureRatio)) / boundsRatioValue, 0) + .scale(boundsRatioValue / textureRatio, 1) + } else { + texture.matrix + .translate(0, -(0.5 * (textureRatio - boundsRatioValue)) / textureRatio) + .scale(1, textureRatio / boundsRatioValue) + } + transformInsideBorder(borderInset, size, texture) + }), + ) +} + +function transformInsideBorder(borderInset: Signal, size: Signal, texture: Texture): void { + const [outerWidth, outerHeight] = size.value + const [top, right, bottom, left] = borderInset.value + + const width = outerWidth - left - right + const height = outerHeight - top - bottom + + texture.matrix + .translate(-1 + (left + width) / outerWidth, -1 + (top + height) / outerHeight) + .scale(outerWidth / width, outerHeight / height) +} + +export function loadImageTexture( + target: Signal, + src: Signal | string, + subscriptions: Subscriptions, +): void { + loadResourceWithParams(target, loadTextureImpl, subscriptions, src) +} + +const textureLoader = new TextureLoader() + +async function loadTextureImpl(src?: string | Texture) { + if (src == null) { + return Promise.resolve(undefined) + } + if (src instanceof Texture) { + return Promise.resolve(src) + } + try { + const texture = await textureLoader.loadAsync(src) + texture.colorSpace = SRGBColorSpace + texture.matrixAutoUpdate = false + return texture + } catch (error) { + console.error(error) + return undefined + } +} diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/components/index.ts new file mode 100644 index 00000000..6d6ab319 --- /dev/null +++ b/packages/uikit/src/components/index.ts @@ -0,0 +1,4 @@ +export * from './container' +export * from './image' +export * from './root' +export * from './utils' diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts new file mode 100644 index 00000000..8af1441d --- /dev/null +++ b/packages/uikit/src/components/root.ts @@ -0,0 +1,292 @@ +import { Signal, computed, signal } from '@preact/signals-core' +import { Object3DRef, RootContext, WithContext } from '../context' +import { FlexNode, YogaProperties } from '../flex' +import { LayoutListeners, Listeners, ScrollListeners, setupLayoutListeners } from '../listeners' +import { PanelProperties, createInstancedPanel } from '../panel/instanced-panel' +import { PanelGroupManager, ShadowProperties, computePanelGroupDependencies } from '../panel/instanced-panel-group' +import { MaterialClass } from '../panel/panel-material' +import { WithAllAliases } from '../properties/alias' +import { AllOptionalProperties, Properties, WithClasses, WithReactive } from '../properties/default' +import { MergedProperties, PropertyTransformers } from '../properties/merged' +import { + ScrollbarProperties, + applyScrollPosition, + computeGlobalScrollMatrix, + createScrollPosition, + createScrollbars, + setupScrollHandler, +} from '../scroll' +import { TransformProperties, applyTransform, computeTransformMatrix } from '../transform' +import { Subscriptions, alignmentXMap, alignmentYMap, loadYoga, readReactive, unsubscribeSubscriptions } from '../utils' +import { WithConditionals } from './utils' +import { computeClippingRect } from '../clipping' +import { computeOrderInfo, ElementType, WithCameraDistance } from '../order' +import { Camera, Matrix4, Plane, Vector2Tuple, Vector3 } from 'three' +import { GlyphGroupManager } from '../text/render/instanced-glyph-group' +import { createGetBatchedProperties } from '../properties/batched' +import { addActiveHandlers, createActivePropertyTransfomers } from '../active' +import { preferredColorSchemePropertyTransformers } from '../dark' +import { addHoverHandlers, createHoverPropertyTransformers } from '../hover' +import { cloneHandlers } from '../panel/instanced-panel-mesh' +import { createResponsivePropertyTransformers } from '../responsive' +import { EventHandlers } from '../events' + +export type InheritableRootProperties = WithConditionals< + WithClasses< + WithAllAliases< + WithReactive< + Omit & + TransformProperties & + PanelProperties & + ScrollbarProperties & + ShadowProperties & { + panelMaterialClass?: MaterialClass + sizeX?: number + sizeY?: number + anchorX?: keyof typeof alignmentXMap + anchorY?: keyof typeof alignmentYMap + } + > + > + > +> + +export type RootProperties = InheritableRootProperties & { + pixelSize?: number +} & EventHandlers & + LayoutListeners & + ScrollListeners + +const DEFAULT_PIXEL_SIZE = 0.002 + +const vectorHelper = new Vector3() +const planeHelper = new Plane() + +const notClipped = signal(false) + +export function createRoot( + propertiesSignal: Signal, + rootSize: Signal, + object: Object3DRef, + childrenContainer: Object3DRef, + scrollHandlers: Signal, + listeners: Listeners, + pixelSize: number | undefined, + onFrameSet: Set<(delta: number) => void>, + getCamera: () => Camera, + subscriptions: Subscriptions, +): RootContext & WithContext { + pixelSize ??= DEFAULT_PIXEL_SIZE + + const requestCalculateLayout = createDeferredRequestLayoutCalculation(onFrameSet, subscriptions) + const node = new FlexNode( + propertiesSignal, + rootSize, + object, + loadYoga(), + 0.01, + requestCalculateLayout, + undefined, + subscriptions, + ) + subscriptions.push(() => node.destroy()) + + const transformMatrix = computeTransformMatrix(propertiesSignal, node, pixelSize) + const rootMatrix = computeRootMatrix(propertiesSignal, transformMatrix, node.size, pixelSize) + + applyTransform(object, transformMatrix, subscriptions) + const groupDeps = computePanelGroupDependencies(propertiesSignal) + + const orderInfo = computeOrderInfo(propertiesSignal, ElementType.Panel, groupDeps, undefined) + + const ctx: WithCameraDistance = { cameraDistance: 0 } + + const panelGroupManager = new PanelGroupManager(pixelSize, ctx, object) + onFrameSet.add(panelGroupManager.onFrame) + subscriptions.push(() => onFrameSet.delete(panelGroupManager.onFrame)) + + const onCameraDistanceFrame = () => { + if (object.current == null) { + ctx.cameraDistance = 0 + return + } + planeHelper.normal.set(0, 0, 1) + planeHelper.constant = 0 + planeHelper.applyMatrix4(object.current.matrixWorld) + vectorHelper.setFromMatrixPosition(getCamera().matrixWorld) + ctx.cameraDistance = planeHelper.distanceToPoint(vectorHelper) + } + onFrameSet.add(onCameraDistanceFrame) + subscriptions.push(() => onFrameSet.delete(onCameraDistanceFrame)) + + createInstancedPanel( + propertiesSignal, + orderInfo, + groupDeps, + panelGroupManager, + rootMatrix, + node.size, + undefined, + node.borderInset, + undefined, + undefined, + subscriptions, + ) + + const scrollPosition = createScrollPosition() + applyScrollPosition(childrenContainer, scrollPosition, pixelSize) + const matrix = computeGlobalScrollMatrix(scrollPosition, rootMatrix, pixelSize) + createScrollbars( + propertiesSignal, + scrollPosition, + node, + rootMatrix, + undefined, + undefined, + orderInfo, + panelGroupManager, + subscriptions, + ) + + const clippingRect = computeClippingRect(rootMatrix, node.size, node.borderInset, node.overflow, pixelSize, undefined) + + setupLayoutListeners(listeners, node.size, subscriptions) + + const onScrollFrame = setupScrollHandler( + node, + scrollPosition, + object, + listeners, + pixelSize, + scrollHandlers, + subscriptions, + ) + onFrameSet.add(onScrollFrame) + subscriptions.push(() => onFrameSet.delete(onScrollFrame)) + const gylphGroupManager = new GlyphGroupManager(pixelSize, ctx, object) + onFrameSet.add(gylphGroupManager.onFrame) + subscriptions.push(() => onFrameSet.delete(gylphGroupManager.onFrame)) + + const rootCtx: RootContext = Object.assign(ctx, { + isClipped: notClipped, + onFrameSet, + cameraDistance: 0, + clippingRect, + gylphGroupManager, + matrix, + node, + object, + orderInfo, + panelGroupManager, + pixelSize, + }) + + return Object.assign(rootCtx, { root: rootCtx }) +} + +export function createRootPropertyTransformers( + rootSize: Signal, + hoveredSignal: Signal>, + activeSignal: Signal>, + pixelSize: number = DEFAULT_PIXEL_SIZE, +): PropertyTransformers { + return { + ...createSizeTranslator(pixelSize, 'sizeX', 'width'), + ...createSizeTranslator(pixelSize, 'sizeY', 'height'), + ...preferredColorSchemePropertyTransformers, + ...createResponsivePropertyTransformers(rootSize), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + } +} + +export function updateRootProperties( + propertiesSignal: Signal, + properties: Properties, + defaultProperties: AllOptionalProperties | undefined, + hoveredSignal: Signal>, + activeSignal: Signal>, + transformers: PropertyTransformers, + propertiesSubscriptions: Subscriptions, +) { + //build merged properties + const merged = new MergedProperties(transformers) + merged.addAll(defaultProperties, properties) + propertiesSignal.value = merged + + //build handlers + const handlers = cloneHandlers(properties) + unsubscribeSubscriptions(propertiesSubscriptions) + addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal, propertiesSubscriptions) + addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + return handlers +} + +function createDeferredRequestLayoutCalculation( + onFrameSet: Set<(delta: number) => void>, + subscriptions: Subscriptions, +) { + let requestedNode: FlexNode | undefined + const onFrame = () => { + if (requestedNode == null) { + return + } + const node = requestedNode + requestedNode = undefined + node.calculateLayout() + } + onFrameSet.add(onFrame) + subscriptions.push(() => onFrameSet.delete(onFrame)) + return (node: FlexNode) => { + if (requestedNode != null || node['yogaNode'] == null) { + return + } + requestedNode = node + } +} + +function createSizeTranslator(pixelSize: number, key: 'sizeX' | 'sizeY', to: string): PropertyTransformers { + const map = new Map>() + return { + [key]: (value: unknown, target: MergedProperties) => { + let entry = map.get(value) + if (entry == null) { + map.set( + value, + (entry = computed(() => { + const s = readReactive(value) as number | undefined + if (s == null) { + return undefined + } + return s / pixelSize + })), + ) + } + target.add(to, entry) + }, + } +} +const matrixHelper = new Matrix4() + +const keys = ['anchorX', 'anchorY'] + +function computeRootMatrix( + propertiesSignal: Signal, + matrix: Signal, + size: Signal, + pixelSize: number, +) { + const get = createGetBatchedProperties(propertiesSignal, keys) + return computed(() => { + const [width, height] = size.value + return matrix.value + ?.clone() + .premultiply( + matrixHelper.makeTranslation( + alignmentXMap[(get('anchorX') as keyof typeof alignmentXMap) ?? 'center'] * width * pixelSize, + alignmentYMap[(get('anchorY') as keyof typeof alignmentYMap) ?? 'center'] * height * pixelSize, + 0, + ), + ) + }) +} diff --git a/packages/uikit/src/components/utils.ts b/packages/uikit/src/components/utils.ts index 54c22a96..d207b5f1 100644 --- a/packages/uikit/src/components/utils.ts +++ b/packages/uikit/src/components/utils.ts @@ -1,5 +1,10 @@ -import { Signal, computed } from '@preact/signals-core' +import { Signal, computed, effect, signal } from '@preact/signals-core' import { Matrix4 } from 'three' +import { WithActive } from '../active' +import { WithPreferredColorScheme } from '../dark' +import { WithHover } from '../hover' +import { WithResponsive } from '../responsive' +import { Subscriptions } from '../utils' export function computeGlobalMatrix( parentMatrix: Signal, @@ -14,3 +19,30 @@ export function computeGlobalMatrix( return parent.clone().multiply(local) }) } + +export type WithConditionals = WithHover & WithResponsive & WithPreferredColorScheme & WithActive + +export function loadResourceWithParams>( + target: Signal, + fn: (param: P, ...additional: A) => Promise, + subscriptions: Subscriptions, + param: Signal

| P, + ...additionals: A +): void { + if (!(param instanceof Signal)) { + let canceled = false + fn(param, ...additionals).then((value) => (canceled ? undefined : (target.value = value))) + subscriptions.push(() => (canceled = true)) + return + } + subscriptions.push( + effect(() => { + let canceled = false + fn(param.value, ...additionals) + .then((value) => (canceled ? undefined : (target.value = value))) + .catch(console.error) + return () => (canceled = true) + // eslint-disable-next-line react-hooks/exhaustive-deps + }), + ) +} diff --git a/packages/uikit/src/context.ts b/packages/uikit/src/context.ts new file mode 100644 index 00000000..66ea6f73 --- /dev/null +++ b/packages/uikit/src/context.ts @@ -0,0 +1,29 @@ +import { Signal } from '@preact/signals-core' +import { FlexNode } from './flex' +import { Matrix4, Object3D } from 'three' +import { ClippingRect } from './clipping' +import { OrderInfo, WithCameraDistance } from './order' +import { GlyphGroupManager } from './text/render/instanced-glyph-group' +import { PanelGroupManager } from './panel/instanced-panel-group' + +export type WithContext = ElementContext & Readonly<{ root: RootContext }> + +export type Object3DRef = { current: Object3D | null } + +export type RootContext = WithCameraDistance & + Readonly<{ + gylphGroupManager: GlyphGroupManager + panelGroupManager: PanelGroupManager + pixelSize: number + onFrameSet: Set<(delta: number) => void> + }> & + ElementContext + +export type ElementContext = Readonly<{ + node: FlexNode + clippingRect: Signal + isClipped: Signal + matrix: Signal + orderInfo: Signal + object: Object3DRef +}> diff --git a/packages/uikit/src/cursor.ts b/packages/uikit/src/cursor.ts deleted file mode 100644 index ae0d9572..00000000 --- a/packages/uikit/src/cursor.ts +++ /dev/null @@ -1,18 +0,0 @@ -const cursorRefStack: Array = [] -const cursorTypeStack: Array = [] - -export function setCursorType(ref: unknown, type: string): void { - cursorRefStack.push(ref) - cursorTypeStack.push(type) - document.body.style.cursor = type -} - -export function unsetCursorType(ref: unknown): void { - const index = cursorRefStack.indexOf(ref) - if (index == -1) { - return - } - cursorRefStack.splice(index, 1) - cursorTypeStack.splice(index, 1) - document.body.style.cursor = cursorTypeStack[cursorTypeStack.length - 1] ?? 'default' -} diff --git a/packages/uikit/src/dark.ts b/packages/uikit/src/dark.ts index 0cd50c9f..b0a9b5f2 100644 --- a/packages/uikit/src/dark.ts +++ b/packages/uikit/src/dark.ts @@ -1,11 +1,6 @@ import { ReadonlySignal, computed, signal } from '@preact/signals-core' -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' -import { createConditionalPropertyTranslator } from './utils.js' -import { MergedProperties } from './properties/merged.js' -import { Color, Vector3Tuple } from 'three' - -export type ColorRepresentation = Color | string | number | Vector3Tuple +import { ColorRepresentation, createConditionalPropertyTranslator } from './utils.js' +import { PropertyTransformers } from './properties/merged.js' export type WithPreferredColorScheme = { dark?: T } & T @@ -38,20 +33,8 @@ export function getPreferredColorScheme() { return preferredColorScheme.peek() } -const translator = createConditionalPropertyTranslator(() => isDarkMode.value) - -export function applyPreferredColorSchemeProperties( - merged: MergedProperties, - defaultProperties: AllOptionalProperties | undefined, - properties: WithClasses> & EventHandlers, -): void { - traverseProperties(defaultProperties, properties, (p) => { - const properties = p.dark - if (properties == null) { - return - } - translator(merged, properties) - }) +export const preferredColorSchemePropertyTransformers: PropertyTransformers = { + dark: createConditionalPropertyTranslator(() => isDarkMode.value), } export function basedOnPreferredColorScheme({ diff --git a/packages/uikit/src/events.ts b/packages/uikit/src/events.ts new file mode 100644 index 00000000..66b55cc4 --- /dev/null +++ b/packages/uikit/src/events.ts @@ -0,0 +1,23 @@ +import { Intersection } from 'three' + +export type ThreeEvent = Intersection & { + nativeEvent: TSourceEvent + defaultPrevented?: boolean + stopped?: boolean +} + +export type EventHandlers = { + onClick?: (event: ThreeEvent) => void + onContextMenu?: (event: ThreeEvent) => void + onDoubleClick?: (event: ThreeEvent) => void + onPointerUp?: (event: ThreeEvent) => void + onPointerDown?: (event: ThreeEvent) => void + onPointerOver?: (event: ThreeEvent) => void + onPointerOut?: (event: ThreeEvent) => void + onPointerEnter?: (event: ThreeEvent) => void + onPointerLeave?: (event: ThreeEvent) => void + onPointerMove?: (event: ThreeEvent) => void + onPointerMissed?: (event: MouseEvent) => void + onPointerCancel?: (event: ThreeEvent) => void + onWheel?: (event: ThreeEvent) => void +} diff --git a/packages/uikit/src/flex/node.ts b/packages/uikit/src/flex/node.ts index b4118426..0096595d 100644 --- a/packages/uikit/src/flex/node.ts +++ b/packages/uikit/src/flex/node.ts @@ -1,12 +1,11 @@ -import { Group, Object3D, Vector2Tuple } from 'three' +import { Object3D, Vector2Tuple } from 'three' import { Signal, batch, computed, effect, signal } from '@preact/signals-core' import { Edge, Node, Yoga, Overflow, MeasureFunction } from 'yoga-layout/wasm-async' import { setter } from './setter.js' -import { RefObject } from 'react' -import { CameraDistanceRef } from '../order.js' import { Subscriptions } from '../utils.js' import { setupImmediateProperties } from '../properties/immediate.js' import { MergedProperties } from '../properties/merged.js' +import { Object3DRef } from '../context.js' export type YogaProperties = { [Key in keyof typeof setter]?: Parameters<(typeof setter)[Key]>[2] @@ -22,7 +21,6 @@ function hasImmediateProperty(key: string): boolean { } export class FlexNode { - public readonly size = signal([0, 0]) public readonly relativeCenter = signal([0, 0]) public readonly borderInset = signal([0, 0, 0, 0]) public readonly paddingInset = signal([0, 0, 0, 0]) @@ -42,11 +40,10 @@ export class FlexNode { constructor( propertiesSignal: Signal, - private groupRef: RefObject, - public cameraDistance: CameraDistanceRef, + public readonly size = signal([0, 0]), + private object: Object3DRef, public readonly yoga: Signal, private precision: number, - public readonly pixelSize: number, requestCalculateLayout: (node: FlexNode) => void, public readonly anyAncestorScrollable: Signal<[boolean, boolean]> | undefined, subscriptions: Subscriptions, @@ -96,18 +93,13 @@ export class FlexNode { batch(() => this.updateMeasurements(undefined, undefined)) } - createChild( - propertiesSignal: Signal, - groupRef: RefObject, - subscriptions: Subscriptions, - ): FlexNode { + createChild(propertiesSignal: Signal, object: Object3DRef, subscriptions: Subscriptions): FlexNode { const child = new FlexNode( propertiesSignal, - groupRef, - this.cameraDistance, + undefined, + object, this.yoga, this.precision, - this.pixelSize, this.requestCalculateLayout, computed(() => { const [ancestorX, ancestorY] = this.anyAncestorScrollable?.value ?? [false, false] @@ -141,21 +133,22 @@ export class FlexNode { //commiting the children let groupChildren: Array | undefined this.children.sort((child1, child2) => { - groupChildren ??= child1.groupRef.current?.parent?.children + groupChildren ??= child1.object.current?.parent?.children if (groupChildren == null) { return 0 } - const i1 = groupChildren.indexOf(child1.groupRef.current as any) + const group1 = child1.object.current + const group2 = child2.object.current + if (group1 == null || group2 == null) { + return 0 + } + const i1 = groupChildren.indexOf(group1) if (i1 === -1) { - throw new Error( - `${child1.groupRef.current} doesnt have the same parent as ${this.children[0].groupRef.current}`, - ) + throw new Error(`parent mismatch`) } - const i2 = groupChildren.indexOf(child2.groupRef.current as any) + const i2 = groupChildren.indexOf(group2) if (i2 === -1) { - throw new Error( - `${child2.groupRef.current} doesnt have the same parent as ${this.children[0].groupRef.current}`, - ) + throw new Error(`parent mismatch`) } return i1 - i2 }) diff --git a/packages/uikit/src/hover.ts b/packages/uikit/src/hover.ts index 89abe76d..82a9baa5 100644 --- a/packages/uikit/src/hover.ts +++ b/packages/uikit/src/hover.ts @@ -1,62 +1,78 @@ -import { signal } from '@preact/signals-core' -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { setCursorType, unsetCursorType } from './cursor.js' -import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' +import { Signal } from '@preact/signals-core' +import { AllOptionalProperties, traverseProperties } from './properties/default.js' import { Subscriptions, createConditionalPropertyTranslator } from './utils.js' -import { MergedProperties } from './properties/merged.js' +import { PropertyTransformers } from './properties/merged.js' +import { addHandler } from './panel/instanced-panel-mesh.js' +import { EventHandlers } from './events.js' export type WithHover = T & { cursor?: string hover?: T onHoverChange?: (hover: boolean) => void } - export type HoverEventHandlers = Pick -export function applyHoverProperties( - merged: MergedProperties, +export function addHoverHandlers( + target: EventHandlers, + properties: WithHover<{}>, defaultProperties: AllOptionalProperties | undefined, - properties: WithClasses> & EventHandlers, + hoveredSignal: Signal>, subscriptions: Subscriptions, -): HoverEventHandlers | undefined { - const hoveredSignal = signal>([]) - // eslint-disable-next-line react-hooks/exhaustive-deps - const translate = createConditionalPropertyTranslator(() => hoveredSignal.value.length > 0) - let hoverPropertiesExist = false +): void { + //cleanup cursor effect + subscriptions.push(() => unsetCursorType(hoveredSignal)) + let hoverPropertiesExist = false traverseProperties(defaultProperties, properties, (p) => { - if (p.hover == null) { - return + if ('hover' in p) { + hoverPropertiesExist = true } - hoverPropertiesExist = true - translate(merged, p.hover) }) - //cleanup cursor effect - // eslint-disable-next-line react-hooks/exhaustive-deps - subscriptions.push(() => unsetCursorType(hoveredSignal)) - if (!hoverPropertiesExist && properties.onHoverChange == null && properties.cursor == null) { //no need to listen to hover hoveredSignal.value.length = 0 - return undefined + return } + addHandler('onPointerOver', target, ({ nativeEvent }) => { + hoveredSignal.value = [nativeEvent.pointerId, ...hoveredSignal.value] + if (properties.onHoverChange != null && hoveredSignal.value.length === 1) { + properties.onHoverChange(true) + } + if (properties.cursor != null) { + setCursorType(hoveredSignal, properties.cursor) + } + }) + addHandler('onPointerOut', target, ({ nativeEvent }) => { + hoveredSignal.value = hoveredSignal.value.filter((id) => id != nativeEvent.pointerId) + if (properties.onHoverChange != null && hoveredSignal.value.length === 0) { + properties.onHoverChange(false) + } + unsetCursorType(hoveredSignal) + }) +} + +export function createHoverPropertyTransformers(hoveredSignal: Signal>): PropertyTransformers { return { - onPointerOver: (e) => { - hoveredSignal.value = [e.pointerId, ...hoveredSignal.value] - if (properties.onHoverChange != null && hoveredSignal.value.length === 1) { - properties.onHoverChange(true) - } - if (properties.cursor != null) { - setCursorType(hoveredSignal, properties.cursor) - } - }, - onPointerOut: (e) => { - hoveredSignal.value = hoveredSignal.value.filter((id) => id != e.pointerId) - if (properties.onHoverChange != null && hoveredSignal.value.length === 0) { - properties.onHoverChange(false) - } - unsetCursorType(hoveredSignal) - }, + hover: createConditionalPropertyTranslator(() => hoveredSignal.value.length > 0), + } +} + +const cursorRefStack: Array = [] +const cursorTypeStack: Array = [] + +export function setCursorType(ref: unknown, type: string): void { + cursorRefStack.push(ref) + cursorTypeStack.push(type) + document.body.style.cursor = type +} + +export function unsetCursorType(ref: unknown): void { + const index = cursorRefStack.indexOf(ref) + if (index == -1) { + return } + cursorRefStack.splice(index, 1) + cursorTypeStack.splice(index, 1) + document.body.style.cursor = cursorTypeStack[cursorTypeStack.length - 1] ?? 'default' } diff --git a/packages/uikit/src/index.ts b/packages/uikit/src/index.ts index 8fed1ea2..929be2cc 100644 --- a/packages/uikit/src/index.ts +++ b/packages/uikit/src/index.ts @@ -1,15 +1,11 @@ +export { patchRenderOrder } from './order.js' export { basedOnPreferredColorScheme, setPreferredColorScheme, getPreferredColorScheme, type PreferredColorScheme, } from './dark.js' -export { FontFamilyProvider } from './text/react.js' -export { useRootSize } from './responsive.js' -export type { ComponentInternals } from './components/utils.js' -export type { MaterialClass } from './panel/react.js' -export type { LayoutListeners, ViewportListeners } from './components/utils.js' -export type { ScrollListeners } from './scroll.js' +export type { MaterialClass } from './panel/panel-material.js' +export type { Listeners, LayoutListeners, ScrollListeners, ViewportListeners } from './listeners.js' export type { AllOptionalProperties } from './properties/default.js' -export { DefaultProperties } from './properties/default.js' -export * from './components/index.js' +export * from './vanilla/index.js' diff --git a/packages/uikit/src/internals.ts b/packages/uikit/src/internals.ts new file mode 100644 index 00000000..b23a8526 --- /dev/null +++ b/packages/uikit/src/internals.ts @@ -0,0 +1,19 @@ +export * from './utils' +export * from './order' +export * from './listeners' +export * from './scroll' +export * from './transform' +export * from './clipping' +export * from './properties/index' +export * from './allocation/index' +export * from './flex/index' +export * from './hover' +export * from './hover' +export * from './dark' +export * from './responsive' +export * from './text/index' +export * from './panel/index' +export * from './components/index' + +export type * from './events' +export type * from './context' diff --git a/packages/uikit/src/listeners.ts b/packages/uikit/src/listeners.ts index 4327d26a..e893d959 100644 --- a/packages/uikit/src/listeners.ts +++ b/packages/uikit/src/listeners.ts @@ -1,11 +1,35 @@ import { Signal, effect } from '@preact/signals-core' import { Vector2Tuple } from 'three' import { Subscriptions } from './utils' +import { ThreeEvent } from './events' + +export type Listeners = ScrollListeners & LayoutListeners & ViewportListeners + +export function createListeners(): Listeners { + return {} +} + +export function updateListeners( + target: ScrollListeners & LayoutListeners & ViewportListeners, + { onIsInViewportChange, onScroll, onSizeChange }: ScrollListeners & LayoutListeners & ViewportListeners, +): void { + target.onIsInViewportChange = onIsInViewportChange + target.onScroll = onScroll + target.onSizeChange = onSizeChange +} + +export type ScrollListeners = { + onScroll?: (scrollX: number, scrollY: number, event?: ThreeEvent) => void +} export type LayoutListeners = { onSizeChange?: (width: number, height: number) => void } +export type ViewportListeners = { + onIsInViewportChange?: (isInViewport: boolean) => void +} + export function setupLayoutListeners( listeners: LayoutListeners, size: Signal, @@ -24,10 +48,6 @@ export function setupLayoutListeners( ) } -export type ViewportListeners = { - onIsInViewportChange?: (isInViewport: boolean) => void -} - export function setupViewportListeners( listeners: ViewportListeners, isClipped: Signal, diff --git a/packages/uikit/src/order.ts b/packages/uikit/src/order.ts index 6d9e4815..5d2c06cb 100644 --- a/packages/uikit/src/order.ts +++ b/packages/uikit/src/order.ts @@ -3,7 +3,7 @@ import { RenderItem, WebGLRenderer } from 'three' import { MergedProperties } from './properties/merged' import { createGetBatchedProperties } from './properties/batched' -export type CameraDistanceRef = { current: number } +export type WithCameraDistance = { cameraDistance: number } export const cameraDistanceKey = Symbol('camera-distance-key') export const orderInfoKey = Symbol('order-info-key') @@ -15,8 +15,8 @@ function reversePainterSortStable(a: RenderItem, b: RenderItem) { if (a.renderOrder !== b.renderOrder) { return a.renderOrder - b.renderOrder } - const aDistanceRef = (a.object as any)[cameraDistanceKey] as CameraDistanceRef - const bDistanceRef = (b.object as any)[cameraDistanceKey] as CameraDistanceRef + const aDistanceRef = (a.object as any)[cameraDistanceKey] as WithCameraDistance + const bDistanceRef = (b.object as any)[cameraDistanceKey] as WithCameraDistance if (aDistanceRef == null || bDistanceRef == null) { //default z comparison return a.z !== b.z ? b.z - a.z : a.id - b.id @@ -24,7 +24,7 @@ function reversePainterSortStable(a: RenderItem, b: RenderItem) { if (aDistanceRef === bDistanceRef) { return compareOrderInfo((a.object as any)[orderInfoKey], (b.object as any)[orderInfoKey]) } - return bDistanceRef.current - aDistanceRef.current + return bDistanceRef.cameraDistance - aDistanceRef.cameraDistance } export function patchRenderOrder(renderer: WebGLRenderer): void { @@ -70,11 +70,11 @@ export function computeOrderInfo( propertiesSignal: Signal, type: ElementType, instancedGroupDependencies: Record | undefined, - parentOrderInfoSignal: Signal, + parentOrderInfoSignal: Signal | undefined, ): Signal { const get = createGetBatchedProperties(propertiesSignal, propertyKeys) return computed(() => { - const parentOrderInfo = parentOrderInfoSignal.value + const parentOrderInfo = parentOrderInfoSignal?.value const offset = get('zIndexOffset') as ZIndexOffset @@ -135,7 +135,7 @@ function shallowEqualRecord(r1: Record | undefined, r2: Record(result: T, rootCameraDistance: CameraDistanceRef, orderInfo: OrderInfo): T { +export function setupRenderOrder(result: T, rootCameraDistance: WithCameraDistance, orderInfo: OrderInfo): T { ;(result as any)[cameraDistanceKey] = rootCameraDistance ;(result as any)[orderInfoKey] = orderInfo return result diff --git a/packages/uikit/src/panel/index.ts b/packages/uikit/src/panel/index.ts new file mode 100644 index 00000000..4b75dab2 --- /dev/null +++ b/packages/uikit/src/panel/index.ts @@ -0,0 +1,5 @@ +export * from './utils' +export * from './interaction-panel-mesh' +export * from './panel-material' +export * from './instanced-panel-mesh' +export * from './instanced-panel-group' diff --git a/packages/uikit/src/panel/instanced-panel-group.ts b/packages/uikit/src/panel/instanced-panel-group.ts index 0c634458..36501d19 100644 --- a/packages/uikit/src/panel/instanced-panel-group.ts +++ b/packages/uikit/src/panel/instanced-panel-group.ts @@ -1,4 +1,4 @@ -import { Group, InstancedBufferAttribute, Material, Usage, DynamicDrawUsage } from 'three' +import { InstancedBufferAttribute, Material, DynamicDrawUsage, MeshBasicMaterial } from 'three' import { Bucket, addToSortedBuckets, @@ -6,12 +6,80 @@ import { updateSortedBucketsAllocation, resizeSortedBucketsSpace, } from '../allocation/sorted-buckets.js' -import { panelMaterialDefaultData } from './panel-material.js' +import { MaterialClass, createPanelMaterial, panelMaterialDefaultData } from './panel-material.js' import { InstancedPanel } from './instanced-panel.js' import { InstancedPanelMesh } from './instanced-panel-mesh.js' -import { CameraDistanceRef, OrderInfo, setupRenderOrder } from '../order.js' +import { ElementType, OrderInfo, WithCameraDistance, setupRenderOrder } from '../order.js' +import { Signal, computed } from '@preact/signals-core' +import { createGetBatchedProperties } from '../properties/batched.js' +import { MergedProperties } from '../properties/merged.js' +import { Object3DRef } from '../context.js' -export class InstancedPanelGroup extends Group { +export type PanelGroupDependencies = { + materialClass: MaterialClass +} & ShadowProperties + +const propertyKeys = ['materialClass', 'castShadow', 'receiveShadow'] + +export function computePanelGroupDependencies(propertiesSignal: Signal) { + const get = createGetBatchedProperties(propertiesSignal, propertyKeys) + return computed(() => ({ + materialClass: (get('materialClass') as MaterialClass | undefined) ?? MeshBasicMaterial, + castShadow: get('castShadow') as boolean | undefined, + receiveShadow: get('receiveShadow') as boolean | undefined, + })) +} + +export type ShadowProperties = { receiveShadow?: boolean; castShadow?: boolean } + +export class PanelGroupManager { + private map = new Map>() + + constructor( + private pixelSize: number, + private root: WithCameraDistance, + private object: Object3DRef, + ) {} + + getGroup(majorIndex: number, { materialClass, receiveShadow, castShadow }: PanelGroupDependencies) { + let groups = this.map.get(materialClass) + if (groups == null) { + this.map.set(materialClass, (groups = new Map())) + } + const key = (majorIndex << 2) + ((receiveShadow ? 1 : 0) << 1) + (castShadow ? 1 : 0) + let panelGroup = groups.get(key) + if (panelGroup == null) { + const material = createPanelMaterial(materialClass, { type: 'instanced' }) + groups.set( + key, + (panelGroup = new InstancedPanelGroup( + this.object, + material, + this.pixelSize, + this.root, + { + elementType: ElementType.Panel, + majorIndex, + minorIndex: 0, + }, + receiveShadow, + castShadow, + )), + ) + } + return panelGroup + } + + onFrame = (delta: number) => { + for (const groups of this.map.values()) { + for (const group of groups.values()) { + group.onFrame(delta) + } + } + } +} + +export class InstancedPanelGroup { private mesh?: InstancedPanelMesh public instanceMatrix!: InstancedBufferAttribute public instanceData!: InstancedBufferAttribute @@ -49,23 +117,23 @@ export class InstancedPanelGroup extends Group { } constructor( + private readonly object: Object3DRef, private readonly material: Material, public readonly pixelSize: number, - private readonly cameraDistance: CameraDistanceRef, + private readonly root: WithCameraDistance, private readonly orderInfo: OrderInfo, - private readonly meshReceiveShadow: boolean, - private readonly meshCastShadow: boolean, - ) { - super() - } + private readonly meshReceiveShadow?: boolean, + private readonly meshCastShadow?: boolean, + ) {} private updateCount(): void { const lastBucket = this.buckets[this.buckets.length - 1] const count = lastBucket.offset + lastBucket.elements.length - if (this.mesh != null) { - this.mesh.count = count + if (this.mesh == null) { + return } - this.visible = count > 0 + this.mesh.count = count + this.mesh.visible = count > 0 } insert(bucketIndex: number, panel: InstancedPanel): void { @@ -115,7 +183,9 @@ export class InstancedPanelGroup extends Group { private update(): void { if (this.elementCount === 0) { - this.visible = false + if (this.mesh != null) { + this.mesh.visible = false + } return } //buffer is resized to have space for 150% of the actually needed elements @@ -128,7 +198,7 @@ export class InstancedPanelGroup extends Group { } updateSortedBucketsAllocation(this.buckets, this.activateElement, this.bufferCopyWithin) this.mesh!.count = this.elementCount - this.visible = true + this.mesh!.visible = true } private resize(): void { @@ -136,7 +206,7 @@ export class InstancedPanelGroup extends Group { this.bufferElementSize = Math.ceil(this.elementCount * 1.5) if (this.mesh != null) { this.mesh.dispose() - this.remove(this.mesh) + this.object.current?.remove(this.mesh) } resizeSortedBucketsSpace(this.buckets, oldBufferSize, this.bufferElementSize) const matrixArray = new Float32Array(this.bufferElementSize * 16) @@ -158,11 +228,11 @@ export class InstancedPanelGroup extends Group { this.instanceClipping = new InstancedBufferAttribute(clippingArray, 16, false) this.instanceClipping.setUsage(DynamicDrawUsage) this.mesh = new InstancedPanelMesh(this.instanceMatrix, this.instanceData, this.instanceClipping) - setupRenderOrder(this.mesh, this.cameraDistance, this.orderInfo) + setupRenderOrder(this.mesh, this.root, this.orderInfo) this.mesh.material = this.material - this.mesh.receiveShadow = this.meshReceiveShadow - this.mesh.castShadow = this.meshCastShadow - this.add(this.mesh) + this.mesh.receiveShadow = this.meshReceiveShadow ?? false + this.mesh.castShadow = this.meshCastShadow ?? false + this.object.current?.add(this.mesh) } destroy(): void {} diff --git a/packages/uikit/src/panel/instanced-panel-mesh.ts b/packages/uikit/src/panel/instanced-panel-mesh.ts index 3e4ecdf3..fd2e7885 100644 --- a/packages/uikit/src/panel/instanced-panel-mesh.ts +++ b/packages/uikit/src/panel/instanced-panel-mesh.ts @@ -1,6 +1,79 @@ -import { Box3, InstancedBufferAttribute, Mesh, Object3DEventMap, Sphere } from 'three' -import { createPanelGeometry } from './utils.js' +import { Box3, InstancedBufferAttribute, Mesh, Object3DEventMap, Sphere, Vector2Tuple } from 'three' +import { createPanelGeometry, panelGeometry } from './utils.js' import { instancedPanelDepthMaterial, instancedPanelDistanceMaterial } from './panel-material.js' +import { Signal, effect } from '@preact/signals-core' +import { ClippingRect } from '../clipping.js' +import { OrderInfo } from '../order.js' +import { Subscriptions } from '../utils.js' +import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' +import { Object3DRef } from '../context.js' +import { EventHandlers, ThreeEvent } from '../events.js' + +export function createInteractionPanel( + size: Signal, + pixelSize: number, + orderInfo: Signal, + parentClippingRect: Signal | undefined, + rootObject: Object3DRef, + subscriptions: Subscriptions, +): Mesh { + const panel = new Mesh(panelGeometry) + panel.matrixAutoUpdate = false + panel.raycast = makeClippedRaycast(panel, makePanelRaycast(panel), rootObject, parentClippingRect, orderInfo) + panel.visible = false + subscriptions.push( + effect(() => { + const [width, height] = size.value + panel.scale.set(width * pixelSize, height * pixelSize, 1) + panel.updateMatrix() + }), + ) + return panel +} + +export function cloneHandlers(handlers: EventHandlers): EventHandlers { + return { + onClick: handlers.onClick, + onContextMenu: handlers.onContextMenu, + onDoubleClick: handlers.onDoubleClick, + onPointerCancel: handlers.onPointerCancel, + onPointerDown: handlers.onPointerDown, + onPointerEnter: handlers.onPointerEnter, + onPointerLeave: handlers.onPointerLeave, + onPointerMissed: handlers.onPointerMissed, + onPointerMove: handlers.onPointerMove, + onPointerOut: handlers.onPointerOut, + onPointerOver: handlers.onPointerOver, + onPointerUp: handlers.onPointerUp, + onWheel: handlers.onWheel, + } +} + +function setHandler(key: K, target: EventHandlers, fn: EventHandlers[K]) { + if (fn == null) { + return + } + target[key] = fn +} + +export function addHandler( + key: Exclude, + target: EventHandlers, + handler: (event: ThreeEvent) => void, +): void { + const existingHandler = target[key] + if (existingHandler == null) { + target[key] = handler + return + } + target[key] = (e: ThreeEvent) => { + existingHandler(e) + if (e.stopped) { + return + } + handler(e) + } +} export class InstancedPanelMesh extends Mesh { public count = 0 diff --git a/packages/uikit/src/panel/instanced-panel.ts b/packages/uikit/src/panel/instanced-panel.ts index a90f94a8..5e24512e 100644 --- a/packages/uikit/src/panel/instanced-panel.ts +++ b/packages/uikit/src/panel/instanced-panel.ts @@ -3,15 +3,12 @@ import { InstancedBufferAttribute, Matrix4, Vector2Tuple } from 'three' import { Bucket } from '../allocation/sorted-buckets.js' import { ClippingRect, defaultClippingData } from '../clipping.js' import { Inset } from '../flex/node.js' -import { InstancedPanelGroup } from './instanced-panel-group.js' +import { InstancedPanelGroup, PanelGroupManager, PanelGroupDependencies } from './instanced-panel-group.js' import { panelDefaultColor } from './panel-material.js' -import { Subscriptions, colorToBuffer } from '../utils.js' -import { Color as ColorRepresentation } from '@react-three/fiber' -import { isPanelVisible, setBorderRadius } from './utils.js' +import { ColorRepresentation, Subscriptions, colorToBuffer, unsubscribeSubscriptions } from '../utils.js' +import { computeIsPanelVisible, setBorderRadius } from './utils.js' import { MergedProperties } from '../properties/merged.js' -import { createGetBatchedProperties } from '../properties/batched.js' import { setupImmediateProperties } from '../properties/immediate.js' -import { GetInstancedPanelGroup, PanelGroupDependencies } from './react.js' import { OrderInfo } from '../order.js' export type PanelProperties = { @@ -26,6 +23,42 @@ export type PanelProperties = { borderOpacity?: number } +export function createInstancedPanel( + propertiesSignal: Signal, + orderInfo: Signal, + panelGroupDependencies: Signal, + panelGroupManager: PanelGroupManager, + matrix: Signal, + size: Signal, + offset: Signal | undefined, + borderInset: Signal, + clippingRect: Signal | undefined, + isHidden: Signal | undefined, + outerSubscriptions: Subscriptions, + renameOutput?: Record, +) { + outerSubscriptions.push( + effect(() => { + const subscriptions: Subscriptions = [] + const group = panelGroupManager.getGroup(orderInfo.value.majorIndex, panelGroupDependencies.value) + new InstancedPanel( + propertiesSignal, + group, + orderInfo.value.minorIndex, + matrix, + size, + offset, + borderInset, + clippingRect, + isHidden, + outerSubscriptions, + renameOutput, + ) + return () => unsubscribeSubscriptions(subscriptions) + }), + ) +} + const instancedPanelMaterialSetters: { [Key in keyof PanelProperties]-?: ( group: InstancedPanelGroup, @@ -59,8 +92,6 @@ const instancedPanelMaterialSetters: { backgroundOpacity: (m, i, p) => writeComponent(m.instanceData, i, 15, p ?? -1), } -const batchedProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] - function hasImmediateProperty(key: string): boolean { return key in instancedPanelMaterialSetters } @@ -78,7 +109,7 @@ export class InstancedPanel { private insertedIntoGroup = false - private active = signal(false) + private active = signal(false) constructor( propertiesSignal: Signal, @@ -90,6 +121,7 @@ export class InstancedPanel { private readonly borderInset: Signal, private readonly clippingRect: Signal | undefined, isHidden: Signal | undefined, + subscriptions: Subscriptions, renameOutput?: Record, ) { setupImmediateProperties( @@ -111,19 +143,10 @@ export class InstancedPanel { subscriptions, renameOutput, ) - const get = createGetBatchedProperties(propertiesSignal, batchedProperties, renameOutput) + const isVisible = computeIsPanelVisible(propertiesSignal, borderInset, size, isHidden, renameOutput) subscriptions.push( effect(() => { - if ( - isPanelVisible( - borderInset, - size, - isHidden, - get('borderOpacity') as number, - get('backgroundOpacity') as number, - get('backgroundColor') as ColorRepresentation, - ) - ) { + if (isVisible.value) { this.requestShow() return } diff --git a/packages/uikit/src/panel/interaction-panel-mesh.ts b/packages/uikit/src/panel/interaction-panel-mesh.ts index b4231376..41a2783c 100644 --- a/packages/uikit/src/panel/interaction-panel-mesh.ts +++ b/packages/uikit/src/panel/interaction-panel-mesh.ts @@ -1,8 +1,8 @@ -import { Group, Intersection, Mesh, Object3D, Object3DEventMap, Plane, Raycaster, Vector3 } from 'three' +import { Intersection, Mesh, Object3D, Object3DEventMap, Plane, Raycaster, Vector3 } from 'three' import { ClippingRect } from '../clipping.js' import { Signal } from '@preact/signals-core' -import { RefObject } from 'react' import { OrderInfo } from '../order.js' +import { Object3DRef } from '../context.js' const planeHelper = new Plane() const vectorHelper = new Vector3() @@ -49,20 +49,20 @@ export function makePanelRaycast(mesh: Mesh): Mesh['raycast'] { export function makeClippedRaycast( mesh: Mesh, fn: Mesh['raycast'], - rootGroupRef: RefObject, + rootObject: Object3DRef, clippingRect: Signal | undefined, orderInfo: Signal, ): Mesh['raycast'] { return (raycaster: Raycaster, intersects: Intersection>[]) => { - const rootGroup = rootGroupRef.current - if (rootGroup == null) { + const obj = rootObject instanceof Object3D ? rootObject : rootObject.current + if (obj == null) { return } const { majorIndex, minorIndex, elementType } = orderInfo.value const oldLength = intersects.length fn.call(mesh, raycaster, intersects) const clippingPlanes = clippingRect?.value?.planes - const outerMatrixWorld = rootGroup.matrixWorld + const outerMatrixWorld = obj.matrixWorld outer: for (let i = intersects.length - 1; i >= oldLength; i--) { const intersection = intersects[i] intersection.distance -= diff --git a/packages/uikit/src/panel/panel-material.ts b/packages/uikit/src/panel/panel-material.ts index 3783a87e..aefddc72 100644 --- a/packages/uikit/src/panel/panel-material.ts +++ b/packages/uikit/src/panel/panel-material.ts @@ -1,8 +1,8 @@ import { Color, - ColorRepresentation, FrontSide, Material, + Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshDistanceMaterial, @@ -12,38 +12,45 @@ import { WebGLProgramParametersWithUniforms, WebGLRenderer, } from 'three' -import { Constructor, isPanelVisible, setBorderRadius } from './utils.js' -import { Signal, effect, signal } from '@preact/signals-core' +import { Constructor, computeIsPanelVisible, setBorderRadius } from './utils.js' +import { Signal, effect } from '@preact/signals-core' import { Inset } from '../flex/node.js' -import { PanelProperties } from './instanced-panel.js' +import type { PanelProperties } from './instanced-panel.js' import { setupImmediateProperties } from '../properties/immediate.js' import { createGetBatchedProperties } from '../properties/batched.js' import { MergedProperties } from '../properties/merged.js' -import { Subscriptions } from '../utils.js' +import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' export type MaterialClass = { new (...args: Array): Material } -export function createPanelMaterials( +const panelMaterialClassKey = ['panelMaterialClass'] + +export function setupPanelMaterials( propertiesSignal: Signal, + target: Mesh, size: Signal, borderInset: Signal, isClipped: Signal, - materialClass: MaterialClass | undefined, clippingPlanes: Array, subscriptions: Subscriptions, renameOutput?: Record, -): readonly [Material, Material, Material] { +) { const data = new Float32Array(16) const info = { data: data, type: 'normal' } as const - const material = createPanelMaterial(materialClass ?? MeshBasicMaterial, info) - const depthMaterial = new PanelDepthMaterial(info) - const distanceMaterial = new PanelDistanceMaterial(info) - material.clippingPlanes = clippingPlanes - depthMaterial.clippingPlanes = clippingPlanes - distanceMaterial.clippingPlanes = clippingPlanes - const materials = [material, depthMaterial, distanceMaterial] as const - applyPropsToMaterialData(propertiesSignal, data, size, borderInset, isClipped, materials, subscriptions, renameOutput) - return materials + target.customDepthMaterial = new PanelDepthMaterial(info) + target.customDistanceMaterial = new PanelDistanceMaterial(info) + target.customDepthMaterial.clippingPlanes = clippingPlanes + target.customDistanceMaterial.clippingPlanes = clippingPlanes + + const get = createGetBatchedProperties(propertiesSignal, panelMaterialClassKey) + subscriptions.push( + effect(() => { + const materialClass = get('panelMaterialClass') as MaterialClass | undefined + target.material = createPanelMaterial(materialClass ?? MeshBasicMaterial, info) + target.material.clippingPlanes = clippingPlanes + }), + ) + applyPropsToMaterialData(propertiesSignal, data, size, borderInset, isClipped, [], subscriptions, renameOutput) } type InstanceOf = T extends { new (): infer K } ? K : never @@ -108,8 +115,6 @@ export const panelMaterialDefaultData = [ -1, //background opacity ] -const batchedProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] - function hasImmediateProperty(key: string): boolean { return key in panelMaterialSetters } @@ -124,20 +129,9 @@ export function applyPropsToMaterialData( subscriptions: Subscriptions, renameOutput?: Record, ) { - const unsubscribeList: Array<() => void> = [] - const active = signal(false) + const internalSubscriptions: Array<() => void> = [] + const isVisible = computeIsPanelVisible(propertiesSignal, borderInset, size, isClipped, renameOutput) let visible = false - setupImmediateProperties( - propertiesSignal, - active, - hasImmediateProperty, - (key, value) => { - const setter = panelMaterialSetters[key as keyof typeof panelMaterialSetters] - setter(data, value as any, size) - }, - subscriptions, - renameOutput, - ) const materialsLength = materials.length const syncVisible = () => { for (let i = 0; i < materialsLength; i++) { @@ -151,26 +145,11 @@ export function applyPropsToMaterialData( visible = false syncVisible() - - const unsubscribeListLength = unsubscribeList.length - for (let i = 0; i < unsubscribeListLength; i++) { - unsubscribeList[i]() - } - unsubscribeList.length = 0 + unsubscribeSubscriptions(internalSubscriptions) } - const get = createGetBatchedProperties(propertiesSignal, batchedProperties, renameOutput) subscriptions.push( effect(() => { - const isVisible = isPanelVisible( - borderInset, - size, - isClipped, - get('borderOpacity') as number, - get('backgroundOpacity') as number, - get('backgroundColor') as ColorRepresentation, - ) - active.value = isVisible - if (!isVisible) { + if (!isVisible.value) { deactivate() return } @@ -182,13 +161,25 @@ export function applyPropsToMaterialData( syncVisible() data.set(panelMaterialDefaultData) - unsubscribeList.push( + + internalSubscriptions.push( effect(() => data.set(size.value, 13)), effect(() => data.set(borderInset.value, 0)), ) }), ) subscriptions.push(deactivate) + setupImmediateProperties( + propertiesSignal, + isVisible, + hasImmediateProperty, + (key, value) => { + const setter = panelMaterialSetters[key as keyof typeof panelMaterialSetters] + setter(data, value as any, size) + }, + subscriptions, + renameOutput, + ) } export type PanelMaterialInfo = { type: 'instanced' } | { type: 'normal'; data: Float32Array } diff --git a/packages/uikit/src/panel/react.ts b/packages/uikit/src/panel/react.ts deleted file mode 100644 index 984e5ae2..00000000 --- a/packages/uikit/src/panel/react.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { Group, Matrix4, Mesh, MeshBasicMaterial, Vector2Tuple } from 'three' -import type { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { Signal, computed, effect } from '@preact/signals-core' -import { Subscriptions } from '../utils.js' -import { useFrame } from '@react-three/fiber' -import { ClippingRect } from '../clipping.js' -import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' -import { HoverEventHandlers } from '../hover.js' -import { InstancedPanelGroup } from './instanced-panel-group.js' -import { MaterialClass, createPanelMaterial } from './panel-material.js' -import { CameraDistanceRef, ElementType, OrderInfo } from '../order.js' -import { panelGeometry } from './utils.js' -import { ActiveEventHandlers } from '../active.js' -import { MergedProperties } from '../properties/merged.js' -import { createGetBatchedProperties } from '../properties/batched.js' -import { Inset } from '../flex/node.js' -import { InstancedPanel } from './instanced-panel.js' - -export function InteractionGroup({ - handlers, - hoverHandlers, - activeHandlers, - matrix, - children, - groupRef, -}: { - handlers: EventHandlers - hoverHandlers: HoverEventHandlers | undefined - activeHandlers: ActiveEventHandlers | undefined - matrix: Signal - children?: ReactNode - groupRef: RefObject -}) { - useEffect(() => { - const group = groupRef.current - if (group == null) { - return - } - return effect(() => matrix.value != null && group.matrix.copy(matrix.value)) - }, [groupRef, matrix]) - return ( - - {children} - - ) -} - -function mergeHandlers( - userHandler: ((event: ThreeEvent) => void) | undefined, - systemHandler: ((event: ThreeEvent) => void) | undefined, -): ((event: ThreeEvent) => void) | undefined { - if (userHandler == null) { - return systemHandler - } - if (systemHandler == null) { - return userHandler - } - return (e: ThreeEvent) => { - systemHandler(e) - if (e.stopped) { - return - } - userHandler(e) - } -} - -export function createInteractionPanel( - size: Signal, - psRef: { pixelSize: number }, - orderInfo: Signal, - parentClippingRect: Signal, - rootGroupRef: { current: Group }, - subscriptions: Subscriptions, -): Mesh { - const panel = new Mesh(panelGeometry) - panel.matrixAutoUpdate = false - panel.raycast = makeClippedRaycast(panel, makePanelRaycast(panel), rootGroupRef, parentClippingRect, orderInfo) - panel.visible = false - subscriptions.push( - effect(() => { - const [width, height] = size.value - panel.scale.set(width * psRef.pixelSize, height * psRef.pixelSize, 1) - panel.updateMatrix() - }), - ) - return panel -} - -export type GetInstancedPanelGroup = ( - majorIndex: number, - panelGroupDependencies: PanelGroupDependencies, -) => InstancedPanelGroup - -export type PanelGroupDependencies = { - materialClass: MaterialClass -} & ShadowProperties - -const propertyKeys = ["materialClass", "castShadow", "receiveShadow"] - -export function computePanelGroupDependencies(propertiesSignal: Signal) { - const get = createGetBatchedProperties(propertiesSignal, propertyKeys) - return computed(() => ({ - materialClass: get("materialClass") as MaterialClass | undefined ?? MeshBasicMaterial, - castShadow: get("castShadow") as boolean | undefined, - receiveShadow: get("receiveShadow") as boolean | undefined - })) -} - -export type ShadowProperties = { receiveShadow?: boolean; castShadow?: boolean } - -export function createInstancePanel( - propertiesSignal: Signal, - orderInfo: Signal, - panelGroupDependencies: Signal, - getGroup: GetInstancedPanelGroup, - matrix: Signal, - size: Signal, - offset: Signal | undefined, - borderInset: Signal, - clippingRect: Signal | undefined, - isHidden: Signal | undefined, - renameOutput?: Record, - subscriptions: Subscriptions) { - subscriptions.push(effect(() => { - const group = getGroup(orderInfo.value.majorIndex, panelGroupDependencies.value) - const panel = new InstancedPanel(propertiesSignal, group, orderInfo.value.minorIndex, matrix, size, offset, borderInset, clippingRect, isHidden, renameOutput) - return () => panel.destroy() - })) -} - -export function useGetInstancedPanelGroup( - pixelSize: number, - cameraDistance: CameraDistanceRef, - groupsContainer: Group, -) { - const map = useMemo(() => new Map>(), []) - const getGroup = useCallback( - (majorIndex, { materialClass, receiveShadow, castShadow }) => { - let groups = map.get(materialClass) - if (groups == null) { - map.set(materialClass, (groups = new Map())) - } - const key = (majorIndex << 2) + ((receiveShadow ? 1 : 0) << 1) + (castShadow ? 1 : 0) - let group = groups.get(key) - if (group == null) { - const material = createPanelMaterial(materialClass, { type: 'instanced' }) - groups.set( - key, - (group = new InstancedPanelGroup( - material, - pixelSize, - cameraDistance, - { - elementType: ElementType.Panel, - majorIndex, - minorIndex: 0, - }, - receiveShadow, - castShadow, - )), - ) - groupsContainer.add(group) - } - return group - }, - [pixelSize, map, cameraDistance, groupsContainer], - ) - - useFrame((_, delta) => { - for (const groups of map.values()) { - for (const group of groups.values()) { - group.onFrame(delta) - } - } - }) - return getGroup -} diff --git a/packages/uikit/src/panel/utils.ts b/packages/uikit/src/panel/utils.ts index 613ed2ca..1420f724 100644 --- a/packages/uikit/src/panel/utils.ts +++ b/packages/uikit/src/panel/utils.ts @@ -1,8 +1,10 @@ -import { Signal } from '@preact/signals-core' +import { Signal, computed } from '@preact/signals-core' import { BufferAttribute, PlaneGeometry, TypedArray, Vector2Tuple } from 'three' import { clamp } from 'three/src/math/MathUtils.js' import { Inset } from '../flex/node.js' -import { Color as ColorRepresentation } from '@react-three/fiber' +import { createGetBatchedProperties } from '../properties/batched.js' +import { MergedProperties } from '../properties/merged.js' +import { ColorRepresentation } from '../utils.js' export type Constructor = new (...args: any[]) => T export type FirstConstructorParameter any> = T extends new ( @@ -32,28 +34,35 @@ export function setComponentInFloat(from: number, index: number, value: number): export const panelGeometry = createPanelGeometry() -export function isPanelVisible( +const visibleProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] + +export function computeIsPanelVisible( + propertiesSignal: Signal, borderInset: Signal, size: Signal, isHidden: Signal | undefined, - borderOpacity: number | undefined, - backgroundOpacity: number | undefined, - backgroundColor: ColorRepresentation | undefined, -): boolean { - const borderVisible = borderInset.value.some((s) => s > 0) && (borderOpacity == null || borderOpacity > 0) - const [width, height] = size.value - const backgroundVisible = - width > 0 && height > 0 && (backgroundOpacity == null || backgroundOpacity > 0) && backgroundColor != null + renameOutput?: Record, +) { + const get = createGetBatchedProperties(propertiesSignal, visibleProperties, renameOutput) + return computed(() => { + const borderOpacity = get('borderOpacity') as number + const backgroundOpacity = get('backgroundOpacity') as number + const backgroundColor = get('backgroundColor') as ColorRepresentation + const borderVisible = borderInset.value.some((s) => s > 0) && (borderOpacity == null || borderOpacity > 0) + const [width, height] = size.value + const backgroundVisible = + width > 0 && height > 0 && (backgroundOpacity == null || backgroundOpacity > 0) && backgroundColor != null - if (!backgroundVisible && !borderVisible) { - return false - } + if (!backgroundVisible && !borderVisible) { + return false + } - if (isHidden == null) { - return true - } + if (isHidden == null) { + return true + } - return !isHidden.value + return !isHidden.value + }) } export function setBorderRadius( diff --git a/packages/uikit/src/properties/default.ts b/packages/uikit/src/properties/default.ts index 24830808..be7abe1a 100644 --- a/packages/uikit/src/properties/default.ts +++ b/packages/uikit/src/properties/default.ts @@ -1,20 +1,17 @@ import { ReadonlySignal } from '@preact/signals-core' -import { ContainerProperties } from '../components/container.js' -import { ContentProperties } from '../react/content.js' -import { CustomContainerProperties } from '../react/custom.js' -import { ImageProperties } from '../react/image.js' -import { RootProperties } from '../react/root.js' -import { SvgProperties } from '../react/svg.js' -import { TextProperties } from '../react/text.js' +import { InheritableContainerProperties } from '../components/container.js' +import { InheritableRootProperties } from '../components/root.js' +import { InheritableImageProperties } from '../components/image.js' export type AllOptionalProperties = - | ContainerProperties - | ContentProperties - | CustomContainerProperties - | ImageProperties - | RootProperties - | SvgProperties - | TextProperties + | InheritableContainerProperties + | InheritableRootProperties + | InheritableImageProperties +// | ContentProperties +// | CustomContainerProperties +// | ImageProperties +// | SvgProperties +// | TextProperties export type WithReactive = { [Key in keyof T]?: T[Key] | ReadonlySignal diff --git a/packages/uikit/src/properties/index.ts b/packages/uikit/src/properties/index.ts new file mode 100644 index 00000000..c334eb5e --- /dev/null +++ b/packages/uikit/src/properties/index.ts @@ -0,0 +1,5 @@ +export * from './alias' +export * from './batched' +export * from './default' +export * from './immediate' +export * from './merged' diff --git a/packages/uikit/src/properties/merged.ts b/packages/uikit/src/properties/merged.ts index 384fdfb5..1966936c 100644 --- a/packages/uikit/src/properties/merged.ts +++ b/packages/uikit/src/properties/merged.ts @@ -2,14 +2,23 @@ import { Signal } from '@preact/signals-core' import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './default' import { AllAliases, allAliases } from './alias' +export type PropertyTransformers = Record void> + export class MergedProperties { private propertyMap = new Map>>() + constructor(private transformers: PropertyTransformers) {} + add(key: string, value: unknown) { if (value === undefined) { //only adding non undefined values to the properties return } + const transform = this.transformers[key] + if (transform != null) { + transform(value, this) + return + } //applying the aliases const aliases = allAliases[key as keyof AllAliases] if (aliases == null) { diff --git a/packages/uikit/src/react/container.tsx b/packages/uikit/src/react/container.tsx deleted file mode 100644 index 1a98b815..00000000 --- a/packages/uikit/src/react/container.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { ReactNode, forwardRef, useRef } from 'react' -import { useFlexNode, FlexProvider } from './react.js' -import { WithReactive, createCollection, finalizeCollection } from '../properties/utils.js' -import { - InteractionGroup, - MaterialClass, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { YogaProperties } from '../flex/node.js' -import { applyHoverProperties } from '../hover.js' -import { ClippingRectProvider, computeClippingRect, useIsClipped, useParentClippingRect } from '../clipping.js' -import { - ViewportListeners, - LayoutListeners, - useViewportListeners, - useLayoutListeners, - useGlobalMatrix, - MatrixProvider, - ComponentInternals, - useComponentInternals, - WithConditionals, -} from './utils.js' -import { - ScrollGroup, - ScrollHandler, - ScrollListeners, - ScrollbarProperties, - useGlobalScrollMatrix, - useScrollPosition, - createScrollbars, -} from '../scroll.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { PanelProperties } from '../panel/instanced-panel.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, addToMerged } from '../properties/default.js' -import { useRootGroupRef } from '../utils.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { Group } from 'three' -import { ElementType, OrderInfoProvider, ZIndexOffset, useOrderInfo } from '../order.js' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' - -export type ContainerProperties = WithConditionals< - WithClasses< - WithAllAliases & ScrollbarProperties> - > -> - -export const Container = forwardRef< - ComponentInternals, - { - children?: ReactNode - zIndexOffset?: ZIndexOffset - panelMaterialClass?: MaterialClass - } & ContainerProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ScrollListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const parentClippingRect = useParentClippingRect() - const globalMatrix = useGlobalMatrix(transformMatrix) - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const orderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - orderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - - const scrollPosition = useScrollPosition() - const globalScrollMatrix = useGlobalScrollMatrix(scrollPosition, node, globalMatrix) - createScrollbars( - collection, - scrollPosition, - node, - globalMatrix, - isClipped, - properties.scrollbarPanelMaterialClass, - parentClippingRect, - orderInfo, - ) - - //apply all properties - addToMerged(collection, properties) - applyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = applyHoverProperties(collection, properties) - const activeHandlers = applyActiveProperties(collection, properties) - finalizeCollection(collection) - - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - - const clippingRect = computeClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - node, - parentClippingRect, - ) - - const rootGroupRef = useRootGroupRef() - - const interactionPanel = useInteractionPanel(node.size, node, orderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel, scrollPosition) - - return ( - - - - - - - - - {properties.children} - - - - - - ) -}) diff --git a/packages/uikit/src/react/content.tsx b/packages/uikit/src/react/content.tsx deleted file mode 100644 index ee0a28f3..00000000 --- a/packages/uikit/src/react/content.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ReactNode, RefObject, forwardRef, useEffect, useMemo, useRef } from 'react' -import { YogaProperties } from '../flex/node.js' -import { FlexProvider, useFlexNode } from './react.js' -import { - InteractionGroup, - MaterialClass, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { - ManagerCollection, - WithReactive, - createCollection, - finalizeCollection, - useGetBatchedProperties, - writeCollection, -} from '../properties/utils.js' -import { alignmentZMap, useRootGroupRef } from '../utils.js' -import { Box3, Group, Mesh, Vector3 } from 'three' -import { computed, effect, Signal, signal } from '@preact/signals-core' -import { applyHoverProperties } from '../hover.js' -import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - WithConditionals, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { ClippingRect, useGlobalClippingPlanes, useIsClipped, useParentClippingRect } from '../clipping.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' -import { PanelProperties } from '../panel/instanced-panel.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, addToMerged } from '../properties/default.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { CameraDistanceRef, ElementType, OrderInfo, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' - -export type ContentProperties = WithConditionals< - WithClasses< - WithAllAliases> - > -> - -export type DepthAlignProperties = { - depthAlign?: keyof typeof alignmentZMap -} - -export const Content = forwardRef< - ComponentInternals, - { - children?: ReactNode - zIndexOffset?: ZIndexOffset - panelMaterialClass?: MaterialClass - keepAspectRatio?: boolean - } & ContentProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - const innerGroupRef = useRef(null) - const rootGroupRef = useRootGroupRef() - const orderInfo = useOrderInfo(ElementType.Object, undefined, undefined, backgroundOrderInfo) - const size = useNormalizedContent( - collection, - innerGroupRef, - rootGroupRef, - node.cameraDistance, - parentClippingRect, - orderInfo, - ) - - //apply all properties - addToMerged(collection, properties) - applyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = applyHoverProperties(collection, properties) - const activeHandlers = applyActiveProperties(collection, properties) - const aspectRatio = useMemo( - () => - computed(() => { - const [x, y] = size.value - return x / y - }), - [size], - ) - if (properties.keepAspectRatio ?? true) { - writeCollection(collection, 'aspectRatio', aspectRatio) - } - finalizeCollection(collection) - - const outerGroupRef = useRef(null) - useEffect( - () => - effect(() => { - const [width, height] = node.size.value - const [pTop, pRight, pBottom, pLeft] = node.paddingInset.value - const [bTop, bRight, bBottom, bLeft] = node.borderInset.value - const topInset = pTop + bTop - const rightInset = pRight + bRight - const bottomInset = pBottom + bBottom - const leftInset = pLeft + bLeft - - const innerWidth = width - leftInset - rightInset - const innerHeight = height - topInset - bottomInset - - const { pixelSize } = node - - const { current } = outerGroupRef - current?.position.set((leftInset - rightInset) * 0.5 * pixelSize, (bottomInset - topInset) * 0.5 * pixelSize, 0) - const [, y, z] = size.value - current?.scale.set( - innerWidth * pixelSize, - innerHeight * pixelSize, - properties.keepAspectRatio ? (innerHeight * pixelSize * z) / y : z, - ) - current?.updateMatrix() - }), - [node, properties.keepAspectRatio, size], - ) - - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel) - - return ( - - - - - {properties.children} - - - - ) -}) - -const box3Helper = new Box3() -const smallValue = new Vector3().setScalar(0.001) - -const propertyKeys = ['depthAlign'] as const - -/** - * normalizes the content so it has a height of 1 - */ -function useNormalizedContent( - collection: ManagerCollection, - ref: RefObject, - rootGroupRef: RefObject, - rootCameraDistance: CameraDistanceRef, - parentClippingRect: Signal | undefined, - orderInfo: OrderInfo, -): Signal { - const sizeSignal = useMemo(() => signal(new Vector3(1, 1, 1)), []) - const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) - const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) - useEffect(() => { - const group = ref.current - if (group == null) { - return - } - group.traverse((object) => { - if (object instanceof Mesh) { - setupRenderOrder(object, rootCameraDistance, orderInfo) - object.material.clippingPlanes = clippingPlanes - object.material.needsUpdate = true - object.raycast = makeClippedRaycast(object, object.raycast, rootGroupRef, parentClippingRect, orderInfo) - } - }) - const parent = group.parent - parent?.remove(group) - box3Helper.setFromObject(group) - const size = new Vector3() - const center = new Vector3() - box3Helper.getSize(size).max(smallValue) - sizeSignal.value = size - group.scale.set(1, 1, 1).divide(size) - if (parent != null) { - parent.add(group) - } - box3Helper.getCenter(center) - return effect(() => { - const get = getPropertySignal.value - if (get == null) { - return - } - group.position.copy(center).negate() - group.position.z -= alignmentZMap[get('depthAlign') ?? 'back'] * size.z - group.position.divide(size) - group.updateMatrix() - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getPropertySignal, rootCameraDistance, clippingPlanes, rootGroupRef]) - - return sizeSignal -} diff --git a/packages/uikit/src/react/custom.tsx b/packages/uikit/src/react/custom.tsx deleted file mode 100644 index 820aa3ca..00000000 --- a/packages/uikit/src/react/custom.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { forwardRef, ReactNode, useEffect, useRef } from 'react' -import { YogaProperties } from '../flex/node.js' -import { useFlexNode, FlexProvider } from './react.js' -import { applyHoverProperties } from '../hover.js' -import { InteractionGroup, ShadowProperties } from '../panel/react.js' -import { createCollection, finalizeCollection, WithReactive } from '../properties/utils.js' -import { useRootGroupRef } from '../utils.js' -import { FrontSide, Group, Material, Mesh } from 'three' -import { - ComponentInternals, - LayoutListeners, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, - ViewportListeners, - WithConditionals, -} from './utils.js' -import { panelGeometry } from '../panel/utils.js' -import { useGlobalClippingPlanes, useIsClipped, useParentClippingRect } from '../clipping.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' -import { flexAliasPropertyTransformation, WithAllAliases } from '../properties/alias.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { addToMerged, WithClasses } from '../properties/default.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { ElementType, setupRenderOrder, useOrderInfo, ZIndexOffset } from '../order.js' -import { effect } from '@preact/signals-core' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' - -export type CustomContainerProperties = WithConditionals< - WithClasses>> -> - -export const CustomContainer = forwardRef< - ComponentInternals, - { - children?: ReactNode - zIndexOffset?: ZIndexOffset - customDepthMaterial?: Material - customDistanceMaterial?: Material - } & CustomContainerProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - - const parentClippingRect = useParentClippingRect() - const rootGroupRef = useRootGroupRef() - const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) - - const orderInfo = useOrderInfo(ElementType.Custom, properties.zIndexOffset, undefined) - - const meshRef = useRef(null) - - const globalMatrix = useGlobalMatrix(transformMatrix) - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - - useEffect(() => { - const mesh = meshRef.current - if (mesh == null) { - return - } - - mesh.raycast = makeClippedRaycast(mesh, mesh.raycast, rootGroupRef, parentClippingRect, orderInfo) - setupRenderOrder(mesh, node.cameraDistance, orderInfo) - - if (mesh.material instanceof Material) { - mesh.material.clippingPlanes = clippingPlanes - mesh.material.needsUpdate = true - mesh.material.shadowSide = FrontSide - } - - const unsubscribeScale = effect(() => { - const [width, height] = node.size.value - mesh.scale.set(width * node.pixelSize, height * node.pixelSize, 1) - mesh.updateMatrix() - }) - - const unsubscribeVisibile = effect(() => void (mesh.visible = !isClipped.value)) - - return () => { - unsubscribeScale() - unsubscribeVisibile() - } - }, [clippingPlanes, node, isClipped, parentClippingRect, orderInfo, rootGroupRef]) - - //apply all properties - addToMerged(collection, properties) - applyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = applyHoverProperties(collection, properties) - const activeHandlers = applyActiveProperties(collection, properties) - finalizeCollection(collection) - - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - - useComponentInternals(ref, node, meshRef) - - return ( - - - {properties.children} - - - ) -}) diff --git a/packages/uikit/src/react/fullscreen.tsx b/packages/uikit/src/react/fullscreen.tsx deleted file mode 100644 index d2dcdf98..00000000 --- a/packages/uikit/src/react/fullscreen.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { ReactNode, forwardRef, useEffect, useMemo, useRef } from 'react' -import { DEFAULT_PIXEL_SIZE, Root, RootProperties } from './root.js' -import { batch, signal } from '@preact/signals-core' -import { RootState, createPortal, useFrame, useStore, useThree } from '@react-three/fiber' -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ScrollListeners } from '../scroll.js' -import { ComponentInternals, LayoutListeners } from './utils.js' -import { Group, PerspectiveCamera } from 'three' - -export const Fullscreen = forwardRef< - ComponentInternals, - RootProperties & { - children?: ReactNode - precision?: number - attachCamera?: boolean - } & EventHandlers & - LayoutListeners & - ScrollListeners ->((properties, ref) => { - const store = useStore() - const [sizeX, sizeY] = useMemo(() => { - const { width, height } = store.getState().size - return [signal(width * DEFAULT_PIXEL_SIZE), signal(height * DEFAULT_PIXEL_SIZE)] as const - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - useEffect(() => { - const fn = (state: RootState) => { - batch(() => { - sizeX.value = state.size.width * DEFAULT_PIXEL_SIZE - sizeY.value = state.size.height * DEFAULT_PIXEL_SIZE - }) - } - fn(store.getState()) - return store.subscribe(fn) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [store]) - const camera = useThree((s) => s.camera) - const groupRef = useRef(null) - useFrame(() => { - if (groupRef.current == null) { - return - } - let distance = 1 - if (camera instanceof PerspectiveCamera) { - distance = sizeY.peek() / (2 * Math.tan((camera.fov / 360) * Math.PI)) - } - groupRef.current.position.z = -distance - groupRef.current.updateMatrix() - }) - const attachCamera = properties.attachCamera ?? true - return ( - <> - {attachCamera && } - {createPortal( - - - {properties.children} - - , - camera, - )} - - ) -}) diff --git a/packages/uikit/src/react/icon.tsx b/packages/uikit/src/react/icon.tsx deleted file mode 100644 index 158f7c57..00000000 --- a/packages/uikit/src/react/icon.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ReactNode, forwardRef, useMemo, useRef } from 'react' -import { useFlexNode } from './react.js' -import { - InteractionGroup, - MaterialClass, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { createCollection, finalizeCollection, useGetBatchedProperties, writeCollection } from '../properties/utils.js' -import { useSignalEffect, fitNormalizedContentInside, useRootGroupRef } from '../utils.js' -import { Color, Group, Mesh, MeshBasicMaterial, ShapeGeometry } from 'three' -import { applyHoverProperties } from '../hover.js' -import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js' -import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { useGlobalClippingPlanes, useIsClipped, useParentClippingRect } from '../clipping.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' -import { flexAliasPropertyTransformation, panelAliasPropertyTransformation } from '../properties/alias.js' -import { useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { addToMerged } from '../properties/default.js' -import { SvgProperties, AppearanceProperties } from './svg.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { ElementType, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' - -const colorHelper = new Color() - -const propertyKeys = ['color', 'opacity'] as const - -const loader = new SVGLoader() - -export const SvgIconFromText = forwardRef< - ComponentInternals, - { - children?: ReactNode - text: string - svgWidth: number - svgHeight: number - zIndexOffset?: ZIndexOffset - materialClass?: MaterialClass - panelMaterialClass?: MaterialClass - } & SvgProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - - const rootGroupRef = useRootGroupRef() - const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) - - const orderInfo = useOrderInfo(ElementType.Svg, undefined, undefined, backgroundOrderInfo) - const svgGroup = useMemo(() => { - const group = new Group() - group.matrixAutoUpdate = false - const result = loader.parse(properties.text) - - for (const path of result.paths) { - const shapes = SVGLoader.createShapes(path) - const material = new (properties.materialClass ?? MeshBasicMaterial)() - material.transparent = true - material.depthWrite = false - material.toneMapped = false - material.clippingPlanes = clippingPlanes - - for (const shape of shapes) { - const geometry = new ShapeGeometry(shape) - geometry.computeBoundingBox() - const mesh = new Mesh(geometry, material) - mesh.matrixAutoUpdate = false - mesh.raycast = makeClippedRaycast(mesh, mesh.raycast, rootGroupRef, parentClippingRect, orderInfo) - setupRenderOrder(mesh, node.cameraDistance, orderInfo) - mesh.userData.color = path.color - mesh.scale.y = -1 - mesh.updateMatrix() - group.add(mesh) - } - } - - return group - }, [properties.text, properties.materialClass, clippingPlanes, rootGroupRef, parentClippingRect, node, orderInfo]) - - const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) - useSignalEffect(() => { - 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) - } else if (colorRepresentation != null) { - color = colorHelper.set(colorRepresentation) - } - svgGroup.traverse((object) => { - if (!(object instanceof Mesh)) { - return - } - object.receiveShadow = properties.receiveShadow ?? false - object.castShadow = properties.castShadow ?? false - const material: MeshBasicMaterial = object.material - material.color.copy(color ?? object.userData.color) - material.opacity = opacity ?? 1 - }) - }, [svgGroup, properties.color, properties.receiveShadow, properties.castShadow]) - - //apply all properties - writeCollection(collection, 'width', properties.svgWidth) - writeCollection(collection, 'height', properties.svgHeight) - addToMerged(collection, properties) - applyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = applyHoverProperties(collection, properties) - const activeHandlers = applyActiveProperties(collection, properties) - writeCollection(collection, 'aspectRatio', properties.svgWidth / properties.svgHeight) - finalizeCollection(collection) - - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - - useSignalEffect(() => { - const aspectRatio = properties.svgWidth / properties.svgHeight - const [offsetX, offsetY, scale] = fitNormalizedContentInside( - node.size, - node.paddingInset, - node.borderInset, - node.pixelSize, - properties.svgWidth / properties.svgHeight, - ) - svgGroup.position.set(offsetX - (scale * aspectRatio) / 2, offsetY + scale / 2, 0) - svgGroup.scale.setScalar(scale / properties.svgHeight) - svgGroup.updateMatrix() - }, [node, svgGroup, properties.svgWidth, properties.svgHeight]) - - useSignalEffect(() => void (svgGroup.visible = !isClipped.value), []) - - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel) - - return ( - - - - - ) -}) diff --git a/packages/uikit/src/react/image.tsx b/packages/uikit/src/react/image.tsx deleted file mode 100644 index 3f8e5204..00000000 --- a/packages/uikit/src/react/image.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { Group, Mesh, SRGBColorSpace, Texture, TextureLoader, Vector2Tuple } from 'three' -import { forwardRef, useMemo, useRef } from 'react' -import { useResourceWithParams, useRootGroupRef, useSignalEffect } from '../utils.js' -import { Signal, computed } from '@preact/signals-core' -import { Inset, YogaProperties } from '../flex/node.js' -import { panelGeometry } from '../panel/utils.js' -import { InteractionGroup, MaterialClass, ShadowProperties, usePanelMaterials } from '../panel/react.js' -import { useFlexNode } from './react.js' -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { applyHoverProperties } from '../hover.js' -import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - WithConditionals, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { useGlobalClippingPlanes, useIsClipped, useParentClippingRect } from '../clipping.js' -import { makeClippedRaycast, makePanelRaycast } from '../panel/interaction-panel-mesh.js' -import { PanelProperties } from '../panel/instanced-panel.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { - ManagerCollection, - PropertyKeyTransformation, - WithReactive, - createCollection, - finalizeCollection, - useGetBatchedProperties, - writeCollection, -} from '../properties/utils.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, addToMerged } from '../properties/default.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { ElementType, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' - -export type ImageFit = 'cover' | 'fill' -const FIT_DEFAULT: ImageFit = 'fill' - -export type ImageProperties = WithConditionals< - WithClasses< - WithAllAliases< - WithReactive< - YogaProperties & - Omit & { - opacity?: number - } & TransformProperties & - ImageFitProperties - > - > - > -> - -export type ImageFitProperties = { - fit?: ImageFit -} - -const materialPropertyTransformation: PropertyKeyTransformation = (key, value, hasProperty, setProperty) => { - if (key === 'opacity') { - setProperty('backgroundOpacity', value) - return - } - panelAliasPropertyTransformation(key, value, hasProperty, setProperty) -} - -export const Image = forwardRef< - ComponentInternals, - ImageProperties & { - src?: string | Signal | Texture | Signal - materialClass?: MaterialClass - zIndexOffset?: ZIndexOffset - keepAspectRatio?: boolean - } & EventHandlers & - LayoutListeners & - ViewportListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const texture = useResourceWithParams(loadTexture, properties.src) - const aspectRatio = useMemo( - () => - computed(() => { - const tex = texture.value - if (tex == null) { - return undefined - } - const image = tex.source.data as { width: number; height: number } - return image.width / image.height - }), - [texture], - ) - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - useTextureFit(collection, texture, node.borderInset, node.size) - const transformMatrix = useTransformMatrix(collection, node) - const parentClippingRect = useParentClippingRect() - const rootGroupRef = useRootGroupRef() - const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) - const globalMatrix = useGlobalMatrix(transformMatrix) - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - const materials = usePanelMaterials( - collection, - node.size, - node.borderInset, - isClipped, - properties.materialClass, - clippingPlanes, - materialPropertyTransformation, - ) - const orderInfo = useOrderInfo(ElementType.Image, properties.zIndexOffset, undefined) - const mesh = useMemo(() => { - const [material, depthMaterial, distanceMaterial] = materials - const result = new Mesh(panelGeometry, material) - result.matrixAutoUpdate = false - result.castShadow = properties.castShadow ?? false - result.receiveShadow = properties.receiveShadow ?? false - result.customDepthMaterial = depthMaterial - result.customDistanceMaterial = distanceMaterial - result.raycast = makeClippedRaycast(result, makePanelRaycast(result), rootGroupRef, parentClippingRect, orderInfo) - setupRenderOrder(result, node.cameraDistance, orderInfo) - return result - }, [node, materials, rootGroupRef, parentClippingRect, orderInfo, properties.receiveShadow, properties.castShadow]) - - //apply all properties - addToMerged(collection, properties) - applyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = applyHoverProperties(collection, properties) - const activeHandlers = applyActiveProperties(collection, properties) - writeCollection(collection, 'backgroundColor', 0xffffff) - if (properties.keepAspectRatio ?? true) { - writeCollection(collection, 'aspectRatio', aspectRatio) - } - finalizeCollection(collection) - - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - - useSignalEffect(() => { - const map = texture.value ?? null - if ((mesh.material as any).map === map) { - return - } - ;(mesh.material as any).map = map - mesh.material.needsUpdate = true - }, [mesh, texture]) - - useSignalEffect(() => { - const [width, height] = node.size.value - mesh.scale.set(width * node.pixelSize, height * node.pixelSize, 1) - mesh.updateMatrix() - }, [mesh]) - - useSignalEffect(() => void (mesh.visible = !isClipped.value), [mesh, isClipped]) - - useComponentInternals(ref, node, mesh) - - return ( - - - - ) -}) - -const propertyKeys = ['fit'] as const - -function useTextureFit( - collection: ManagerCollection, - textureSignal: Signal, - borderInset: Signal, - size: Signal, -): void { - const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) - useSignalEffect(() => { - const get = getPropertySignal.value - const texture = textureSignal.value - if (texture == null || get == null) { - return - } - const fit = get('fit') ?? FIT_DEFAULT - texture.matrix.identity() - - if (fit === 'fill' || texture == null) { - transformInsideBorder(borderInset, size, texture) - return - } - - const { width: textureWidth, height: textureHeight } = texture.source.data as { width: number; height: number } - const textureRatio = textureWidth / textureHeight - - const [width, height] = size.value - const [top, right, bottom, left] = borderInset.value - const boundsRatioValue = (width - left - right) / (height - top - bottom) - - if (textureRatio > boundsRatioValue) { - texture.matrix - .translate(-(0.5 * (boundsRatioValue - textureRatio)) / boundsRatioValue, 0) - .scale(boundsRatioValue / textureRatio, 1) - } else { - texture.matrix - .translate(0, -(0.5 * (textureRatio - boundsRatioValue)) / textureRatio) - .scale(1, textureRatio / boundsRatioValue) - } - transformInsideBorder(borderInset, size, texture) - }, [textureSignal, borderInset, size]) -} - -function transformInsideBorder(borderInset: Signal, size: Signal, texture: Texture): void { - const [outerWidth, outerHeight] = size.value - const [top, right, bottom, left] = borderInset.value - - const width = outerWidth - left - right - const height = outerHeight - top - bottom - - texture.matrix - .translate(-1 + (left + width) / outerWidth, -1 + (top + height) / outerHeight) - .scale(outerWidth / width, outerHeight / height) -} - -const textureLoader = new TextureLoader() - -async function loadTexture(src?: string | Texture) { - if (src == null) { - return Promise.resolve(undefined) - } - if (src instanceof Texture) { - return Promise.resolve(src) - } - try { - const texture = await textureLoader.loadAsync(src) - texture.colorSpace = SRGBColorSpace - texture.matrixAutoUpdate = false - return texture - } catch (error) { - console.error(error) - return undefined - } -} diff --git a/packages/uikit/src/react/index.ts b/packages/uikit/src/react/index.ts deleted file mode 100644 index bb966051..00000000 --- a/packages/uikit/src/react/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './content.js' -export * from './container.js' -export * from './custom.js' -export * from './image.js' -export * from './root.js' -export * from './svg.js' -export * from './text.js' -export * from './fullscreen.js' -export * from './icon.js' -export * from './suspending.js' -export * from './portal.js' diff --git a/packages/uikit/src/react/input.tsx b/packages/uikit/src/react/input.tsx deleted file mode 100644 index 5041fc2b..00000000 --- a/packages/uikit/src/react/input.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { YogaProperties } from '../flex/node.js' -import { applyHoverProperties } from '../hover.js' -import { PanelProperties } from '../panel/instanced-panel.js' -import { InteractionGroup, MaterialClass, useInstancedPanel, useInteractionPanel } from '../panel/react.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { WithClasses, addToMerged } from '../properties/default.js' -import { WithReactive, createCollection, finalizeCollection, writeCollection } from '../properties/utils.js' -import { ScrollListeners } from '../scroll.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - WithConditionals, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react' -import { useParentClippingRect, useIsClipped } from '../clipping.js' -import { useFlexNode } from './react.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { InstancedTextProperties, useInstancedText } from '../text/react.js' -import { Signal, signal } from '@preact/signals-core' -import { useRootGroupRef, useSignalEffect } from '../utils.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { Group } from 'three' -import { ElementType, ZIndexOffset, useOrderInfo } from '../order.js' -/* -export type TextProperties = WithConditionals< - WithClasses< - WithAllAliases> - > -> - -export const Text = forwardRef< - ComponentInternals, - { - value?: string | Signal - defaultValue?: string - onValueChange?: (value: string) => void - panelMaterialClass?: MaterialClass - zIndexOffset?: ZIndexOffset - } & TextProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ScrollListeners ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const rootGroupRef = useRootGroupRef() - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - properties.panelMaterialClass, - panelAliasPropertyTransformation, - ) - const orderInfo = useOrderInfo(ElementType.Text, undefined, backgroundOrderInfo) - const text = useMemo(() => signal(''), []) - const measureFunc = useInstancedText(collection, text, globalMatrix, node, isClipped, parentClippingRect, orderInfo) - - useApplyProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - writeCollection(collection, 'measureFunc', measureFunc) - finalizeCollection(collection) - - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel) - - // eslint-disable-next-line react-hooks/exhaustive-deps - const defaultValue = useMemo(() => signal(properties.defaultValue ?? ''), []) - const value = useMemo(() => properties.value ?? defaultValue, [properties.value, defaultValue]) - - const { onValueChange } = properties - const isControlled = properties.value != null - - useSignalEffect(() => { - el.value = value instanceof Signal ? value.value : value - text.value = el.value - }, [value]) - - return ( - - { - if (e.defaultPrevented) { - return - } - const buttons = e.buttons - const content = this.text - if (content == null) { - return - } - - // left click not held (i.e. not dragging) - const dragging = buttons === 1 || buttons === 3 - if (!dragging || !content) { - return - } - - const idx = getCursorPosition(e) - let start: number, end: number, dir: 'forward' | 'backward' | 'none' - - const caret = this.dragStartPoint ?? 0 - if (idx < caret) { - start = idx - end = caret - dir = 'backward' - } else if (idx > caret) { - start = caret - end = idx - dir = 'forward' - } else { - start = end = idx - dir = 'none' - } - this.setSelection([start, end]) - el.setSelectionRange(start, end, dir) - }} - onPointerDown={(e) => { - if (e.defaultPrevented) { - return - } - const idx = getCursorPosition(e) - this.dragStartPoint = idx - this.setFocus(true) - this.setSelection([idx, idx]) - el.setSelectionRange(idx, idx, 'none') - }} - onDoubleClick={(e) => { - if (e.defaultPrevented) { - return - } - const text = this.text - if (text == null) { - return - } - - const caret = this.caretPosition ?? 0 - let start = 0, - end: number = text.length - - for (let i = caret; i < text.length; i++) { - if (isWhitespace(text[i])) { - end = i - break - } - } - - for (let i = caret; i > 0; i--) { - if (isWhitespace(text[i])) { - start = i > 0 ? i + 1 : i - break - } - } - - this.setSelection([start, end]) - el.setSelectionRange(start, end, 'none') - }} - > - - - - ) -}) - -function isWhitespace(str: string): boolean { - return !!str && str.trim() === '' -} - -export function useHtmlInputElement( - defaultValue: Signal, - isControlled: boolean, - onValueChange?: (value: string) => void, -): HTMLInputElement { - const element = useMemo(() => { - const result = document.createElement('input') - result.type = 'text' - const style = result.style - style.setProperty('position', 'absolute') - style.setProperty('left', '-1000vw') - style.setProperty('transform', 'absolute') - style.setProperty('touchAction', 'translateX(-50%)') - style.setProperty('pointerEvents', 'none') - style.setProperty('opacity', '0') - const onSelectChange = (e: Event) => { - if (e.target != result) { - return - } - // setTimeout ensures that we read the selection start/end values - // once they have been updated. - // If we were to read the selection on keydown events directly, - // the value would be before the selection change from the keyboard - // event happened. - setTimeout(() => { - const { selectionStart, selectionEnd } = result - if (selectionStart == null || selectionEnd == null) { - return - } - this.setSelection([selectionStart, selectionEnd]) - }, 0) - } - result.addEventListener('input', (e) => { - if (e.target != result) { - return - } - if (!isControlled) { - defaultValue.value = result.value - } - onValueChange?.(result.value) - }) - result.addEventListener('keyup', onSelectChange) - result.addEventListener('keydown', onSelectChange) - result.addEventListener('selectionchange', onSelectChange) - document.body.appendChild(result) - return result - }, [defaultValue, isControlled, onValueChange]) - useEffect(() => () => element.remove(), [element]) - return element -} -*/ diff --git a/packages/uikit/src/react/portal.tsx b/packages/uikit/src/react/portal.tsx deleted file mode 100644 index ea5993a2..00000000 --- a/packages/uikit/src/react/portal.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { effect } from '@preact/signals-core' -import { - ComponentPropsWithoutRef, - ReactNode, - RefObject, - forwardRef, - useEffect, - useImperativeHandle, - useMemo, - useRef, -} from 'react' -import { HalfFloatType, LinearFilter, Scene, WebGLRenderTarget } from 'three' -import { Image } from './image.js' -import { InjectState, RootState, createPortal, useFrame, useStore } from '@react-three/fiber' -import type { DomEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' -import type { ComponentInternals } from './utils.js' - -export const Portal = forwardRef< - ComponentInternals, - { - frames?: number - renderPriority?: number - eventPriority?: number - resolution?: number - children?: ReactNode - } & Omit, 'src' | 'fit'> ->(({ children, resolution = 1, frames = Infinity, renderPriority = 0, eventPriority = 0, ...props }, ref) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - const fbo = useMemo( - () => - new WebGLRenderTarget(1, 1, { - minFilter: LinearFilter, - magFilter: LinearFilter, - type: HalfFloatType, - }), - [], - ) - const imageRef = useRef(null) - const injectState = useMemo( - () => ({ - events: { compute: uvCompute.bind(null, imageRef), priority: eventPriority }, - size: { width: 1, height: 1, left: 0, top: 0 }, - }), - [eventPriority], - ) - useEffect(() => { - if (imageRef.current == null) { - return - } - const { size } = imageRef.current - const unsubscribeSetSize = effect(() => { - const [width, height] = size.value - fbo.setSize(width, height) - injectState.size!.width = width - injectState.size!.height = height - }) - return () => { - unsubscribeSetSize() - fbo.dispose() - } - }, [fbo, injectState]) - useImperativeHandle(ref, () => imageRef.current!, []) - const vScene = useMemo(() => new Scene(), []) - return ( - <> - {createPortal( - - {children} - {/* Without an element that receives pointer events state.pointer will always be 0/0 */} - null} /> - , - vScene, - injectState, - )} - - - ) -}) - -function uvCompute( - { current }: RefObject, - event: DomEvent, - state: RootState, - previous?: RootState, -) { - if (current == null || previous == null) { - return false - } - // First we call the previous state-onion-layers compute, this is what makes it possible to nest portals - if (!previous.raycaster.camera) previous.events.compute?.(event, previous, previous.previousRoot?.getState()) - // We run a quick check against the parent, if it isn't hit there's no need to raycast at all - const [intersection] = previous.raycaster.intersectObject(current.interactionPanel) - if (!intersection) return false - // We take that hits uv coords, set up this layers raycaster, et voilà, we have raycasting on arbitrary surfaces - const uv = intersection.uv - if (!uv) return false - state.raycaster.setFromCamera(state.pointer.set(uv.x * 2 - 1, uv.y * 2 - 1), state.camera) -} - -function ChildrenToFBO({ - frames, - renderPriority, - children, - fbo, - imageRef, -}: { - frames: number - renderPriority: number - children: ReactNode - fbo: WebGLRenderTarget - imageRef: RefObject -}) { - const store = useStore() - useEffect(() => { - if (imageRef.current == null) { - return - } - const { size } = imageRef.current - return effect(() => { - const [width, height] = size.value - store.setState({ size: { width, height, top: 0, left: 0 } }) - }) - }) - let count = 0 - let oldAutoClear - let oldXrEnabled - useFrame((state) => { - if (frames === Infinity || count < frames) { - oldAutoClear = state.gl.autoClear - oldXrEnabled = state.gl.xr.enabled - state.gl.autoClear = true - state.gl.xr.enabled = false - state.gl.setRenderTarget(fbo) - state.gl.render(state.scene, state.camera) - state.gl.setRenderTarget(null) - state.gl.autoClear = oldAutoClear - state.gl.xr.enabled = oldXrEnabled - count++ - } - }, renderPriority) - return <>{children} -} diff --git a/packages/uikit/src/react/react.ts b/packages/uikit/src/react/react.ts deleted file mode 100644 index 566b4b5e..00000000 --- a/packages/uikit/src/react/react.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createContext, useContext, useCallback, useRef } from 'react' -import { FlexNode } from '../flex/node.js' -import { useFrame } from '@react-three/fiber' - -const FlexContext = createContext(null as any) - -export const FlexProvider = FlexContext.Provider - -export function useParentFlexNode() { - return useContext(FlexContext) -} - -export function useDeferredRequestLayoutCalculation(): (node: FlexNode) => void { - let requestedNodeRef = useRef(undefined) - useFrame(() => { - if (requestedNodeRef.current == null) { - return - } - requestedNodeRef.current.calculateLayout() - requestedNodeRef.current = undefined - }) - return useCallback((node) => { - if (requestedNodeRef.current != null || node['yogaNode'] == null) { - return - } - requestedNodeRef.current = node - }, []) -} diff --git a/packages/uikit/src/react/root.tsx b/packages/uikit/src/react/root.tsx deleted file mode 100644 index 6a59136a..00000000 --- a/packages/uikit/src/react/root.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { ReactNode, forwardRef, useEffect, useMemo, useRef } from 'react' -import { FlexNode, YogaProperties } from '../flex/node.js' -import { RootGroupProvider, alignmentXMap, alignmentYMap, useLoadYoga } from '../utils.js' -import { - InstancedPanelProvider, - InteractionGroup, - MaterialClass, - ShadowProperties, - useGetInstancedPanelGroup, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { WithReactive, createCollection, finalizeCollection, writeCollection } from '../properties/utils.js' -import { FlexProvider, useDeferredRequestLayoutCalculation } from './react.js' -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ReadonlySignal, Signal, computed } from '@preact/signals-core' -import { Group, Matrix4, Plane, Vector2Tuple, Vector3 } from 'three' -import { useFrame, useThree } from '@react-three/fiber' -import { applyHoverProperties } from '../hover.js' -import { - LayoutListeners, - useLayoutListeners, - MatrixProvider, - ComponentInternals, - useComponentInternals, - WithConditionals, -} from './utils.js' -import { ClippingRectProvider, computeClippingRect } from '../clipping.js' -import { - ScrollGroup, - ScrollHandler, - ScrollListeners, - ScrollbarProperties, - useGlobalScrollMatrix, - useScrollPosition, - createScrollbars, -} from '../scroll.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, addToMerged } from '../properties/default.js' -import { InstancedGlyphProvider, useGetInstancedGlyphGroup } from '../text/react.js' -import { PanelProperties } from '../panel/instanced-panel.js' -import { RootSizeProvider, useApplyResponsiveProperties } from '../responsive.js' -import { ElementType, OrderInfoProvider, patchRenderOrder, useOrderInfo } from '../order.js' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' - -export const DEFAULT_PRECISION = 0.1 -export const DEFAULT_PIXEL_SIZE = 0.002 - -export function useRootLayout() {} - -export type RootProperties = WithConditionals< - WithClasses< - WithAllAliases< - WithReactive & TransformProperties & PanelProperties> & - ScrollbarProperties - > - > -> - -const planeHelper = new Plane() -const vectorHelper = new Vector3() - -export const Root = forwardRef< - ComponentInternals, - RootProperties & { - children?: ReactNode - precision?: number - anchorX?: keyof typeof alignmentXMap - anchorY?: keyof typeof alignmentYMap - pixelSize?: number - panelMaterialClass?: MaterialClass - } & WithReactive<{ - sizeX?: number - sizeY?: number - }> & - EventHandlers & - LayoutListeners & - ScrollListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const renderer = useThree((state) => state.gl) - - useEffect(() => patchRenderOrder(renderer), [renderer]) - const { sizeX, sizeY } = properties - const [precision, pixelSize] = useMemo( - () => [properties.precision ?? DEFAULT_PRECISION, properties.pixelSize ?? DEFAULT_PIXEL_SIZE], - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ) - const yoga = useLoadYoga() - const distanceToCameraRef = useMemo(() => ({ current: 0 }), []) - const groupRef = useRef(null) - const requestLayout = useDeferredRequestLayoutCalculation() - const node = useMemo( - () => new FlexNode(groupRef, distanceToCameraRef, yoga, precision, pixelSize, requestLayout, undefined), - // eslint-disable-next-line react-hooks/exhaustive-deps - [requestLayout, groupRef, yoga], - ) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - useEffect(() => () => node.destroy(), [node]) - - const transformMatrix = useTransformMatrix(collection, node) - - const groupsContainer = useMemo(() => { - const result = new Group() - result.matrixAutoUpdate = false - return result - }, []) - const getPanelGroup = useGetInstancedPanelGroup(pixelSize, node.cameraDistance, groupsContainer) - const getGylphGroup = useGetInstancedGlyphGroup(pixelSize, node.cameraDistance, groupsContainer) - - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const orderInfo = useOrderInfo(ElementType.Panel, undefined, groupDeps) - - const rootMatrix = useRootMatrix(transformMatrix, node.size, pixelSize, properties) - const scrollPosition = useScrollPosition() - const globalScrollMatrix = useGlobalScrollMatrix(scrollPosition, node, rootMatrix) - createScrollbars( - collection, - scrollPosition, - node, - rootMatrix, - undefined, - properties.scrollbarPanelMaterialClass, - undefined, - orderInfo, - getPanelGroup, - ) - - useInstancedPanel( - collection, - rootMatrix, - node.size, - undefined, - node.borderInset, - undefined, - orderInfo, - undefined, - groupDeps, - panelAliasPropertyTransformation, - getPanelGroup, - ) - - //apply all properties - addToMerged(collection, properties) - applyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties, node.size) - const hoverHandlers = applyHoverProperties(collection, properties) - const activeHandlers = applyActiveProperties(collection, properties) - writeCollection(collection, 'width', useDivide(sizeX, pixelSize)) - writeCollection(collection, 'height', useDivide(sizeY, pixelSize)) - finalizeCollection(collection) - - const clippingRect = computeClippingRect(rootMatrix, node.size, node.borderInset, node.overflow, node, undefined) - useLayoutListeners(properties, node.size) - - const internactionPanel = useInteractionPanel(node.size, node, orderInfo, groupRef) - - useComponentInternals(ref, node, internactionPanel, scrollPosition) - - useFrame(({ camera }) => { - planeHelper.normal.set(0, 0, 1) - planeHelper.constant = 0 - planeHelper.applyMatrix4(internactionPanel.matrixWorld) - vectorHelper.setFromMatrixPosition(camera.matrixWorld) - distanceToCameraRef.current = planeHelper.distanceToPoint(vectorHelper) - }) - - return ( - <> - - - - - - - - - - - - - - {properties.children} - - - - - - - - - - - ) -}) - -function useDivide( - size: number | Signal | undefined, - pixelSize: number, -): ReadonlySignal | number | undefined { - return useMemo( - () => - size === undefined - ? undefined - : size instanceof Signal - ? computed(() => { - const s = size.value - if (s == null) { - return undefined - } - return s / pixelSize - }) - : size / pixelSize, - [size, pixelSize], - ) -} - -const matrixHelper = new Matrix4() - -function useRootMatrix( - matrix: Signal, - size: Signal, - pixelSize: number, - { - anchorX = 'center', - anchorY = 'center', - }: { - anchorX?: keyof typeof alignmentXMap - anchorY?: keyof typeof alignmentYMap - }, -) { - return useMemo( - () => - computed(() => { - const [width, height] = size.value - return matrix.value - ?.clone() - .premultiply( - matrixHelper.makeTranslation( - alignmentXMap[anchorX] * width * pixelSize, - alignmentYMap[anchorY] * height * pixelSize, - 0, - ), - ) - }), - [matrix, size, anchorX, anchorY, pixelSize], - ) -} diff --git a/packages/uikit/src/react/suspending.tsx b/packages/uikit/src/react/suspending.tsx deleted file mode 100644 index 3ae61726..00000000 --- a/packages/uikit/src/react/suspending.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ComponentPropsWithoutRef } from 'react' -import { Image } from './image.js' -import { useLoader } from '@react-three/fiber' -import { TextureLoader } from 'three' - -export function SuspendingImage({ - src, - ...props -}: Omit, 'src'> & { src: string }) { - const texture = useLoader(TextureLoader, src) - return -} diff --git a/packages/uikit/src/react/svg.tsx b/packages/uikit/src/react/svg.tsx deleted file mode 100644 index cfe6f672..00000000 --- a/packages/uikit/src/react/svg.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ReactNode, RefObject, forwardRef, useMemo, useRef } from 'react' -import { YogaProperties } from '../flex/node.js' -import { useFlexNode } from './react.js' -import { - InteractionGroup, - MaterialClass, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { - WithReactive, - createCollection, - finalizeCollection, - useGetBatchedProperties, - writeCollection, -} from '../properties/utils.js' -import { useResourceWithParams, useSignalEffect, fitNormalizedContentInside, useRootGroupRef } from '../utils.js' -import { Box3, Color, Group, Mesh, MeshBasicMaterial, Plane, ShapeGeometry, Vector3 } from 'three' -import { computed, ReadonlySignal, Signal } from '@preact/signals-core' -import { applyHoverProperties } from '../hover.js' -import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js' -import { Color as ColorRepresentation } from '@react-three/fiber' -import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - WithConditionals, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { ClippingRect, useGlobalClippingPlanes, useIsClipped, useParentClippingRect } from '../clipping.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' -import { PanelProperties } from '../panel/instanced-panel.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, addToMerged } from '../properties/default.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { CameraDistanceRef, ElementType, OrderInfo, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' - -export type SvgProperties = WithConditionals< - WithClasses< - WithAllAliases> - > -> - -export type AppearanceProperties = { - opacity?: number - color?: ColorRepresentation -} - -const loader = new SVGLoader() - -const box3Helper = new Box3() -const vectorHelper = new Vector3() - -async function loadSvg( - url: string, - cameraDistance: CameraDistanceRef, - MaterialClass: MaterialClass = MeshBasicMaterial, - clippingPlanes: Array, - clippedRect: Signal | undefined, - rootGroupRef: RefObject, - orderInfo: OrderInfo, -) { - const object = new Group() - object.matrixAutoUpdate = false - const result = await loader.loadAsync(url) - box3Helper.makeEmpty() - for (const path of result.paths) { - const shapes = SVGLoader.createShapes(path) - const material = new MaterialClass() - material.transparent = true - material.depthWrite = false - material.toneMapped = false - material.clippingPlanes = clippingPlanes - - for (const shape of shapes) { - const geometry = new ShapeGeometry(shape) - geometry.computeBoundingBox() - box3Helper.union(geometry.boundingBox!) - const mesh = new Mesh(geometry, material) - mesh.matrixAutoUpdate = false - mesh.raycast = makeClippedRaycast(mesh, mesh.raycast, rootGroupRef, clippedRect, orderInfo) - setupRenderOrder(mesh, cameraDistance, orderInfo) - mesh.userData.color = path.color - mesh.scale.y = -1 - mesh.updateMatrix() - object.add(mesh) - } - } - box3Helper.getSize(vectorHelper) - const aspectRatio = vectorHelper.x / vectorHelper.y - const scale = 1 / vectorHelper.y - object.scale.set(1, 1, 1).multiplyScalar(scale) - box3Helper.getCenter(vectorHelper) - vectorHelper.y *= -1 - object.position.copy(vectorHelper).negate().multiplyScalar(scale) - object.updateMatrix() - - return Object.assign(object, { aspectRatio }) -} - -const colorHelper = new Color() - -const propertyKeys = ['color', 'opacity'] as const - -export const Svg = forwardRef< - ComponentInternals, - { - zIndexOffset?: ZIndexOffset - children?: ReactNode - src: string | ReadonlySignal - materialClass?: MaterialClass - panelMaterialClass?: MaterialClass - } & SvgProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - - const rootGroupRef = useRootGroupRef() - const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) - const orderInfo = useOrderInfo(ElementType.Svg, undefined, undefined, backgroundOrderInfo) - const svgObject = useResourceWithParams( - loadSvg, - properties.src, - node.cameraDistance, - properties.materialClass, - clippingPlanes, - parentClippingRect, - rootGroupRef, - orderInfo, - ) - - const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) - useSignalEffect(() => { - 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) - } else if (colorRepresentation != null) { - color = colorHelper.set(colorRepresentation) - } - svgObject.value?.traverse((object) => { - if (!(object instanceof Mesh)) { - return - } - object.receiveShadow = properties.receiveShadow ?? false - object.castShadow = properties.castShadow ?? false - const material: MeshBasicMaterial = object.material - material.color.copy(color ?? object.userData.color) - material.opacity = opacity ?? 1 - }) - }, [svgObject, properties.color, properties.receiveShadow, properties.castShadow]) - const aspectRatio = useMemo(() => computed(() => svgObject.value?.aspectRatio), [svgObject]) - - //apply all properties - addToMerged(collection, properties) - applyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = applyHoverProperties(collection, properties) - const activeHandlers = applyActiveProperties(collection, properties) - writeCollection(collection, 'aspectRatio', aspectRatio) - finalizeCollection(collection) - - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - - const centerGroup = useMemo(() => { - const group = new Group() - group.matrixAutoUpdate = false - return group - }, []) - - useSignalEffect(() => { - const [offsetX, offsetY, scale] = fitNormalizedContentInside( - node.size, - node.paddingInset, - node.borderInset, - node.pixelSize, - svgObject.value?.aspectRatio ?? 1, - ) - centerGroup.position.set(offsetX, offsetY, 0) - centerGroup.scale.setScalar(scale) - centerGroup.updateMatrix() - }, [node, svgObject]) - - useSignalEffect(() => { - const object = svgObject.value - if (object == null) { - return - } - centerGroup.add(object) - return () => centerGroup.remove(object) - }, [svgObject, centerGroup]) - - useSignalEffect(() => void (centerGroup.visible = !isClipped.value), []) - - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel) - - return ( - - - - - ) -}) diff --git a/packages/uikit/src/react/text.tsx b/packages/uikit/src/react/text.tsx deleted file mode 100644 index 2faeb92c..00000000 --- a/packages/uikit/src/react/text.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { YogaProperties } from '../flex/node.js' -import { applyHoverProperties } from '../hover.js' -import { PanelProperties } from '../panel/instanced-panel.js' -import { - InteractionGroup, - MaterialClass, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { WithClasses, addToMerged } from '../properties/default.js' -import { WithReactive, createCollection, finalizeCollection, writeCollection } from '../properties/utils.js' -import { ScrollListeners } from '../scroll.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - WithConditionals, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { forwardRef, useRef } from 'react' -import { useParentClippingRect, useIsClipped } from '../clipping.js' -import { useFlexNode } from './react.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { InstancedTextProperties, useInstancedText } from '../text/react.js' -import { ReadonlySignal } from '@preact/signals-core' -import { useRootGroupRef } from '../utils.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { Group } from 'three' -import { ElementType, ZIndexOffset, useOrderInfo } from '../order.js' -import { applyPreferredColorSchemeProperties } from '../dark.js' -import { applyActiveProperties } from '../active.js' - -export type TextProperties = WithConditionals< - WithClasses< - WithAllAliases> - > -> - -export const Text = forwardRef< - ComponentInternals, - { - children: string | ReadonlySignal | Array> - panelMaterialClass?: MaterialClass - zIndexOffset?: ZIndexOffset - } & TextProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ScrollListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const rootGroupRef = useRootGroupRef() - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - const orderInfo = useOrderInfo(ElementType.Text, undefined, undefined, backgroundOrderInfo) - const measureFunc = useInstancedText( - collection, - properties.children, - globalMatrix, - node, - isClipped, - parentClippingRect, - orderInfo, - ) - - addToMerged(collection, properties) - applyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = applyHoverProperties(collection, properties) - const activeHandlers = applyActiveProperties(collection, properties) - writeCollection(collection, 'measureFunc', measureFunc) - finalizeCollection(collection) - - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel) - - return ( - - - - ) -}) diff --git a/packages/uikit/src/react/utils.ts b/packages/uikit/src/react/utils.ts deleted file mode 100644 index e6920937..00000000 --- a/packages/uikit/src/react/utils.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ReadonlySignal, Signal, computed, effect } from '@preact/signals-core' -import { useMemo, useEffect, createContext, useContext, useImperativeHandle, ForwardedRef, RefObject } from 'react' -import { Group, Matrix4, Mesh, Vector2Tuple } from 'three' -import { FlexNode, Inset } from '../flex/node.js' -import { WithHover } from '../hover.js' -import { WithResponsive } from '../responsive.js' -import { WithPreferredColorScheme } from '../dark.js' -import { WithActive } from '../active.js' - -export type WithConditionals = WithHover & WithResponsive & WithPreferredColorScheme & WithActive - -export type ComponentInternals = { - pixelSize: number - size: ReadonlySignal - center: ReadonlySignal - borderInset: ReadonlySignal - paddingInset: ReadonlySignal - scrollPosition?: Signal - interactionPanel: Mesh -} - -export function useComponentInternals( - ref: ForwardedRef, - node: FlexNode, - interactionPanel: Mesh | RefObject, - scrollPosition?: Signal, -): void { - useImperativeHandle( - ref, - () => ({ - borderInset: node.borderInset, - paddingInset: node.paddingInset, - pixelSize: node.pixelSize, - center: node.relativeCenter, - size: node.size, - interactionPanel: interactionPanel instanceof Mesh ? interactionPanel : interactionPanel.current!, - scrollPosition, - }), - [interactionPanel, node, scrollPosition], - ) -} - -const MatrixContext = createContext>(null as any) - -export const MatrixProvider = MatrixContext.Provider - -const RootGroupRefContext = createContext>(null as any) - -export function useRootGroupRef() { - return useContext(RootGroupRefContext) -} - -export const RootGroupProvider = RootGroupRefContext.Provider diff --git a/packages/uikit/src/responsive.ts b/packages/uikit/src/responsive.ts index e028c8e1..267c7490 100644 --- a/packages/uikit/src/responsive.ts +++ b/packages/uikit/src/responsive.ts @@ -1,9 +1,7 @@ import { Signal } from '@preact/signals-core' -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' import { Vector2Tuple } from 'three' -import { MergedProperties } from './properties/merged.js' +import { PropertyTransformers } from './properties/merged.js' const breakPoints = { sm: 640, @@ -12,35 +10,20 @@ const breakPoints = { xl: 1280, '2xl': 1536, } -const breakPointKeys = Object.keys(breakPoints) +const breakPointKeys = Object.keys(breakPoints) as Array const breakPointKeysLength = breakPointKeys.length export type WithResponsive = T & { [Key in keyof typeof breakPoints]?: T } -export function applyResponsiveProperties( - merged: MergedProperties, - defaultProperties: AllOptionalProperties | undefined, - properties: WithClasses> & EventHandlers, - rootSize: Signal, -): void { - const translator = { - sm: createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints.sm), - md: createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints.md), - lg: createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints.lg), - xl: createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints.xl), - '2xl': createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints['2xl']), +export function createResponsivePropertyTransformers(rootSize: Signal): PropertyTransformers { + const transformers: PropertyTransformers = {} + + for (let i = 0; i < breakPointKeysLength; i++) { + const key = breakPointKeys[i] + transformers[key] = createConditionalPropertyTranslator(() => rootSize.value[0] > breakPoints[key]) } - traverseProperties(defaultProperties, properties, (p) => { - for (let i = 0; i < breakPointKeysLength; i++) { - const key = breakPointKeys[i] as keyof typeof breakPoints - const properties = p[key] - if (properties == null) { - continue - } - translator[key](merged, properties) - } - }) + return transformers } diff --git a/packages/uikit/src/scroll.ts b/packages/uikit/src/scroll.ts index fade9a94..ac0b0fc5 100644 --- a/packages/uikit/src/scroll.ts +++ b/packages/uikit/src/scroll.ts @@ -1,18 +1,23 @@ import { ReadonlySignal, Signal, computed, effect, signal } from '@preact/signals-core' -import { EventHandlers, ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { Group, Matrix4, MeshBasicMaterial, Object3D, Vector2, Vector2Tuple, Vector3, Vector4Tuple } from 'three' +import { Matrix4, Vector2, Vector2Tuple, Vector3, Vector4Tuple } from 'three' import { FlexNode, Inset } from './flex/node.js' -import { Color as ColorRepresentation, useFrame } from '@react-three/fiber' -import { Subscriptions } from './utils.js' -import { GetInstancedPanelGroup, PanelGroupDependencies } from './panel/react.js' +import { ColorRepresentation, Subscriptions } from './utils.js' import { ClippingRect } from './clipping.js' import { clamp } from 'three/src/math/MathUtils.js' -import { InstancedPanel, PanelProperties } from './panel/instanced-panel.js' +import { PanelProperties, createInstancedPanel } from './panel/instanced-panel.js' import { ElementType, OrderInfo, computeOrderInfo } from './order.js' import { createGetBatchedProperties } from './properties/batched.js' import { MergedProperties } from './properties/merged.js' import { MaterialClass } from './panel/panel-material.js' import { WithReactive } from './properties/default.js' +import { + PanelGroupDependencies, + PanelGroupManager, + computePanelGroupDependencies, +} from './panel/instanced-panel-group.js' +import { Object3DRef } from './context.js' +import { ScrollListeners } from './listeners.js' +import { EventHandlers, ThreeEvent } from './events.js' const distanceHelper = new Vector3() const localPointHelper = new Vector3() @@ -22,18 +27,14 @@ export type ScrollEventHandlers = Pick< 'onPointerDown' | 'onPointerUp' | 'onPointerMove' | 'onWheel' | 'onPointerLeave' > -export type ScrollListeners = { - onScroll?: (scrollX: number, scrollY: number, event?: ThreeEvent) => void -} - export function createScrollPosition() { return signal([0, 0]) } export function computeGlobalScrollMatrix( scrollPosition: Signal, - node: FlexNode, globalMatrix: Signal, + pixelSize: number, ) { return computed(() => { const global = globalMatrix.value @@ -41,27 +42,27 @@ export function computeGlobalScrollMatrix( return undefined } const [scrollX, scrollY] = scrollPosition.value - const { pixelSize } = node return new Matrix4().makeTranslation(-scrollX * pixelSize, scrollY * pixelSize, 0).premultiply(global) }) } -export function setupScrollGroup(node: FlexNode, scrollPosition: Signal, ref: { current: Object3D }) { +export function applyScrollPosition(object: Object3DRef, scrollPosition: Signal, pixelSize: number) { return effect(() => { const [scrollX, scrollY] = scrollPosition.value - const { pixelSize } = node - ref.current?.position.set(-scrollX * pixelSize, scrollY * pixelSize, 0) - ref.current?.updateMatrix() + object.current?.position.set(-scrollX * pixelSize, scrollY * pixelSize, 0) + object.current?.updateMatrix() }) } export function setupScrollHandler( node: FlexNode, scrollPosition: Signal, - ref: { current: Object3D }, - onScrollRef: { current: ScrollListeners['onScroll'] }, - onFrames: Array<(delta: number) => void>, -): Signal { + object: Object3DRef, + listeners: ScrollListeners, + pixelSize: number, + scrollHandlers: Signal, + subscriptions: Subscriptions, +) { const isScrollable = computed(() => node.scrollable.value.some((scrollable) => scrollable)) const downPointerMap = new Map() @@ -101,11 +102,60 @@ export function setupScrollHandler( } if (x != newX || y != newY) { scrollPosition.value = [newX, newY] - onScrollRef.current?.(...scrollPosition.value, event) + listeners.onScroll?.(...scrollPosition.value, event) } } - onFrames.push((deltaTime) => { + subscriptions.push( + effect(() => { + if (!isScrollable.value) { + scrollHandlers.value = undefined + } + scrollHandlers.value = { + onPointerDown: ({ nativeEvent, point }) => { + let interaction = downPointerMap.get(nativeEvent.pointerId) + if (interaction == null) { + downPointerMap.set(nativeEvent.pointerId, (interaction = { timestamp: 0, point: new Vector3() })) + } + interaction.timestamp = performance.now() / 1000 + object.current!.worldToLocal(interaction.point.copy(point)) + }, + onPointerUp: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), + onPointerLeave: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), + onPointerCancel: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), + onContextMenu: (e) => e.nativeEvent.preventDefault(), + onPointerMove: (event) => { + const prevInteraction = downPointerMap.get(event.nativeEvent.pointerId) + + if (prevInteraction == null) { + return + } + object.current!.worldToLocal(localPointHelper.copy(event.point)) + distanceHelper.copy(localPointHelper).sub(prevInteraction.point).divideScalar(pixelSize) + const timestamp = performance.now() / 1000 + const deltaTime = timestamp - prevInteraction.timestamp + + prevInteraction.point.copy(localPointHelper) + prevInteraction.timestamp = timestamp + + if (event.defaultPrevented) { + return + } + + scroll(event, -distanceHelper.x, distanceHelper.y, deltaTime, true) + }, + onWheel: (event) => { + if (event.defaultPrevented) { + return + } + const { nativeEvent } = event + scroll(event, nativeEvent.deltaX, nativeEvent.deltaY, undefined, false) + }, + } + }), + ) + + return (delta: number) => { if (downPointerMap.size > 0) { return } @@ -118,8 +168,8 @@ export function setupScrollHandler( deltaX += outsideDistance(x, 0, maxX ?? 0) * -0.3 deltaY += outsideDistance(y, 0, maxY ?? 0) * -0.3 - deltaX += scrollVelocity.x * deltaTime - deltaY += scrollVelocity.y * deltaTime + deltaX += scrollVelocity.x * delta + deltaY += scrollVelocity.y * delta scrollVelocity.multiplyScalar(0.9) //damping scroll factor @@ -135,53 +185,7 @@ export function setupScrollHandler( return } scroll(undefined, deltaX, deltaY, undefined, true) - }) - - return computed(() => { - if (!isScrollable.value) { - return {} - } - return { - onPointerDown: (event) => { - let interaction = downPointerMap.get(event.pointerId) - if (interaction == null) { - downPointerMap.set(event.pointerId, (interaction = { timestamp: 0, point: new Vector3() })) - } - interaction.timestamp = performance.now() / 1000 - ref.current!.worldToLocal(interaction.point.copy(event.point)) - }, - onPointerUp: (event) => downPointerMap.delete(event.pointerId), - onPointerLeave: (event) => downPointerMap.delete(event.pointerId), - onPointerCancel: (event) => downPointerMap.delete(event.pointerId), - onContextMenu: (e) => e.nativeEvent.preventDefault(), - onPointerMove: (event) => { - const prevInteraction = downPointerMap.get(event.pointerId) - - if (prevInteraction == null) { - return - } - ref.current!.worldToLocal(localPointHelper.copy(event.point)) - distanceHelper.copy(localPointHelper).sub(prevInteraction.point).divideScalar(node.pixelSize) - const timestamp = performance.now() / 1000 - const deltaTime = timestamp - prevInteraction.timestamp - - prevInteraction.point.copy(localPointHelper) - prevInteraction.timestamp = timestamp - - if (event.defaultPrevented) { - return - } - - scroll(event, -distanceHelper.x, distanceHelper.y, deltaTime, true) - }, - onWheel: (event) => { - if (event.defaultPrevented) { - return - } - scroll(event, event.deltaX, event.deltaY, undefined, false) - }, - } - }) + } } const wasScrolledSymbol = Symbol('was-scrolled') @@ -279,17 +283,12 @@ export function createScrollbars( node: FlexNode, globalMatrix: Signal, isClipped: Signal | undefined, - materialClass: MaterialClass | undefined, parentClippingRect: Signal | undefined, orderInfo: Signal, - getGroup: GetInstancedPanelGroup, + panelGroupManager: PanelGroupManager, subscriptions: Subscriptions, ): void { - const groupDeps: PanelGroupDependencies = { - materialClass: materialClass ?? MeshBasicMaterial, - castShadow: false, - receiveShadow: false, - } + const groupDeps = computePanelGroupDependencies(propertiesSignal) const scrollbarOrderInfo = computeOrderInfo(propertiesSignal, ElementType.Panel, groupDeps, orderInfo) const getScrollbarWidth = createGetBatchedProperties(propertiesSignal, scrollbarWidthPropertyKeys) @@ -302,11 +301,11 @@ export function createScrollbars( scrollPosition, node, globalMatrix, + groupDeps, isClipped, - materialClass, parentClippingRect, scrollbarOrderInfo, - getGroup, + panelGroupManager, getScrollbarWidth, borderSize, subscriptions, @@ -317,11 +316,11 @@ export function createScrollbars( scrollPosition, node, globalMatrix, + groupDeps, isClipped, - materialClass, parentClippingRect, scrollbarOrderInfo, - getGroup, + panelGroupManager, getScrollbarWidth, borderSize, subscriptions, @@ -345,7 +344,7 @@ function createScrollbar( isClipped: Signal | undefined, parentClippingRect: Signal | undefined, orderInfo: Signal, - getGroup: GetInstancedPanelGroup, + panelGroupManager: PanelGroupManager, get: (key: string) => unknown, borderSize: ReadonlySignal, subscriptions: Subscriptions, @@ -363,23 +362,19 @@ function createScrollbar( const scrollbarPosition = computed(() => (scrollbarTransformation.value?.slice(0, 2) ?? [0, 0]) as Vector2Tuple) const scrollbarSize = computed(() => (scrollbarTransformation.value?.slice(2, 4) ?? [0, 0]) as Vector2Tuple) - subscriptions.push( - effect(() => { - const panel = new InstancedPanel( - propertiesSignal, - getGroup, - orderInfo, - panelGroupDependencies, - globalMatrix, - scrollbarSize, - scrollbarPosition, - borderSize, - parentClippingRect, - isClipped, - subscriptions, - scrollbarPanelPropertyRename, - ) - }), + createInstancedPanel( + propertiesSignal, + orderInfo, + panelGroupDependencies, + panelGroupManager, + globalMatrix, + scrollbarSize, + scrollbarPosition, + borderSize, + parentClippingRect, + isClipped, + subscriptions, + scrollbarPanelPropertyRename, ) } diff --git a/packages/uikit/src/text/font.ts b/packages/uikit/src/text/font.ts index c5b4ed43..9ed02949 100644 --- a/packages/uikit/src/text/font.ts +++ b/packages/uikit/src/text/font.ts @@ -1,4 +1,99 @@ -import { Texture, TypedArray } from 'three' +import { Signal, effect, signal } from '@preact/signals-core' +import { Texture, TypedArray, WebGLRenderer } from 'three' +import { createGetBatchedProperties } from '../properties/batched' +import { MergedProperties } from '../properties/merged' +import { Subscriptions } from '../utils' +import { loadCachedFont } from './cache' + +export type FontFamilyUrls = Partial> + +export type FontFamilies = Record + +const fontWeightNames = { + thin: 100, + 'extra-light': 200, + light: 300, + normal: 400, + medium: 500, + 'semi-bold': 600, + bold: 700, + 'extra-bold': 800, + black: 900, + 'extra-black': 950, +} + +export type FontWeight = keyof typeof fontWeightNames | number + +const fontKeys = ['fontFamily', 'fontWeight'] + +export type FontFamilyProperties = { fontFamily?: string; fontWeight?: FontWeight } + +const defaultFontFamilyUrls = { + inter: { + light: 'https://pmndrs.github.io/uikit/fonts/inter-light.json', + normal: 'https://pmndrs.github.io/uikit/fonts/inter-normal.json', + medium: 'https://pmndrs.github.io/uikit/fonts/inter-medium.json', + 'semi-bold': 'https://pmndrs.github.io/uikit/fonts/inter-semi-bold.json', + bold: 'https://pmndrs.github.io/uikit/fonts/inter-bold.json', + }, +} satisfies FontFamilies + +export function computeFont( + properties: Signal, + fontFamilies: FontFamilies = defaultFontFamilyUrls, + renderer: WebGLRenderer, + subscriptions: Subscriptions, +): Signal { + const result = signal(undefined) + const get = createGetBatchedProperties(properties, fontKeys) + subscriptions.push( + effect(() => { + let fontWeight = (get('fontWeight') as FontWeight) ?? 'normal' + if (typeof fontWeight === 'string') { + fontWeight = fontWeightNames[fontWeight] + } + let fontFamily = get('fontFamily') as string + if (fontFamily == null) { + fontFamily = Object.keys(fontFamilies)[0] + } + const url = getMatchingFontUrl(fontFamilies[fontFamily], fontWeight) + let canceled = false + loadCachedFont(url, renderer, (font) => (canceled ? undefined : (result.value = font))) + return () => (canceled = true) + }), + ) + return result +} + +function getMatchingFontUrl(fontFamily: FontFamilyUrls, weight: number): string { + let distance = Infinity + let result: string | undefined + for (const fontWeight in fontFamily) { + const d = Math.abs(weight - getWeightNumber(fontWeight)) + if (d === 0) { + return fontFamily[fontWeight]! + } + if (d < distance) { + distance = d + result = fontFamily[fontWeight] + } + } + if (result == null) { + throw new Error(`font family has no entries ${fontFamily}`) + } + return result +} + +function getWeightNumber(value: string): number { + if (value in fontWeightNames) { + return fontWeightNames[value as keyof typeof fontWeightNames] + } + const number = parseFloat(value) + if (isNaN(number)) { + throw new Error(`invalid font weight "${value}"`) + } + return number +} export type FontInfo = { pages: Array diff --git a/packages/uikit/src/text/index.ts b/packages/uikit/src/text/index.ts new file mode 100644 index 00000000..3aaf540d --- /dev/null +++ b/packages/uikit/src/text/index.ts @@ -0,0 +1,5 @@ +export * from './utils' +export * from './wrapper/index' +export * from './font' +export * from './layout' +export * from './render/index' diff --git a/packages/uikit/src/text/layout.ts b/packages/uikit/src/text/layout.ts index 92e4ebd7..edc1f8bc 100644 --- a/packages/uikit/src/text/layout.ts +++ b/packages/uikit/src/text/layout.ts @@ -1,6 +1,11 @@ import { BreakallWrapper, NowrapWrapper, WordWrapper } from './wrapper/index.js' import { Font } from './font.js' import { getGlyphLayoutHeight } from './utils.js' +import { Signal, computed } from '@preact/signals-core' +import { MeasureFunction, MeasureMode } from 'yoga-layout' +import { createGetBatchedProperties } from '../properties/batched.js' +import { MergedProperties } from '../properties/merged.js' +import { readReactive } from '../utils.js' export type GlyphLayoutLine = { start: number; end: number; width: number; whitespaces: number } @@ -21,6 +26,46 @@ export type GlyphLayoutProperties = { wordBreak: keyof typeof wrappers } +const glyphPropertyKeys = ['fontSize', 'letterSpacing', 'lineHeight', 'wordBreak'] + +export function computeMeasureFunc( + properties: Signal, + fontSignal: Signal, + textSignal: Signal | Array | string>>, + propertiesRef: { current: GlyphLayoutProperties | undefined }, +) { + const get = createGetBatchedProperties(properties, glyphPropertyKeys) + return computed(() => { + const font = fontSignal.value + if (font == null) { + return undefined + } + const textSignalValue = textSignal.value + const text = Array.isArray(textSignalValue) + ? textSignalValue.map((t) => readReactive(t)).join('') + : readReactive(textSignalValue) + const letterSpacing = (get('letterSpacing') as number) ?? 0 + const lineHeight = (get('lineHeight') as number) ?? 1.2 + const fontSize = (get('fontSize') as number) ?? 16 + const wordBreak = (get('wordBreak') as GlyphLayoutProperties['wordBreak']) ?? 'break-word' + + return (width, widthMode) => { + const availableWidth = widthMode === MeasureMode.Undefined ? undefined : width + return measureGlyphLayout( + (propertiesRef.current = { + font, + fontSize, + letterSpacing, + lineHeight, + text, + wordBreak, + }), + availableWidth, + ) + } + }) +} + const wrappers = { 'keep-all': NowrapWrapper, 'break-all': BreakallWrapper, diff --git a/packages/uikit/src/text/react.ts b/packages/uikit/src/text/react.ts deleted file mode 100644 index 1f6894c6..00000000 --- a/packages/uikit/src/text/react.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { ReadonlySignal, Signal, computed, effect, signal } from '@preact/signals-core' -import { InstancedText, TextAlignProperties, TextAppearanceProperties } from './render/instanced-text.js' -import { InstancedGlyphGroup } from './render/instanced-glyph-group.js' -import { FlexNode } from '../flex/node.js' -import { Group, Matrix4, WebGLRenderer } from 'three' -import { ClippingRect } from '../clipping.js' -import { Subscriptions, readReactive } from '../utils.js' -import { loadCachedFont } from './cache.js' -import { MeasureFunction, MeasureMode } from 'yoga-layout/wasm-async' -import { Font } from './font.js' -import { GlyphLayout, GlyphLayoutProperties, buildGlyphLayout, measureGlyphLayout } from './layout.js' -import { useFrame, useThree } from '@react-three/fiber' -import { CameraDistanceRef, ElementType, OrderInfo } from '../order.js' -import { MergedProperties } from '../properties/merged.js' -import { createGetBatchedProperties } from '../properties/batched.js' - -export type GetInstancedGlyphGroup = (majorIndex: number, font: Font) => InstancedGlyphGroup - -export function createGetInstancedGlyphGroup( - pixelSize: number, - cameraDistance: CameraDistanceRef, - groupsContainer: Group, -) { - const map = new Map>() - const getGroup: GetInstancedGlyphGroup = (majorIndex, font) => { - let groups = map.get(font) - if (groups == null) { - map.set(font, (groups = new Map())) - } - let group = groups?.get(majorIndex) - if (group == null) { - groups.set( - majorIndex, - (group = new InstancedGlyphGroup(font, pixelSize, cameraDistance, { - majorIndex, - elementType: ElementType.Text, - minorIndex: 0, - })), - ) - groupsContainer.add(group) - } - return group - } - - useFrame((_, delta) => { - for (const groups of map.values()) { - for (const group of groups.values()) { - group.onFrame(delta) - } - } - }) - - return getGroup -} - -export type FontFamilyUrls = Partial> - -export type FontFamilies = Record - -const fontWeightNames = { - thin: 100, - 'extra-light': 200, - light: 300, - normal: 400, - medium: 500, - 'semi-bold': 600, - bold: 700, - 'extra-bold': 800, - black: 900, - 'extra-black': 950, -} - -export type FontWeight = keyof typeof fontWeightNames | number - -const alignPropertyKeys = ['horizontalAlign', 'verticalAlign'] -const appearancePropertyKeys = ['color', 'opacity'] -const glyphPropertyKeys = ['fontSize', 'letterSpacing', 'lineHeight', 'wordBreak'] - -export type InstancedTextProperties = TextAlignProperties & - TextAppearanceProperties & - Omit & - FontFamilyProperties - -export function createInstancedText( - properties: Signal, - text: string | ReadonlySignal | Array>, - matrix: Signal, - node: FlexNode, - isHidden: Signal | undefined, - parentClippingRect: Signal | undefined, - orderInfo: OrderInfo, - fontFamilies: FontFamilies | undefined, - renderer: WebGLRenderer, - getGroup: GetInstancedGlyphGroup, - subscriptions: Subscriptions, -) { - const fontSignal = computeFont(properties, fontFamilies, renderer, subscriptions) - // eslint-disable-next-line react-hooks/exhaustive-deps - const textSignal = signal | Array>>(text) - let layoutPropertiesRef: { current: GlyphLayoutProperties | undefined } = { current: undefined } - - const measureFunc = computeMeasureFunc(properties, fontSignal, textSignal, layoutPropertiesRef) - - const getAlign = createGetBatchedProperties(properties, alignPropertyKeys) - const getAppearance = createGetBatchedProperties(properties, appearancePropertyKeys) - - const layoutSignal = signal(undefined) - subscriptions.push( - node.addLayoutChangeListener(() => { - const layoutProperties = layoutPropertiesRef.current - if (layoutProperties == null) { - return - } - const { size, paddingInset, borderInset } = node - const [width, height] = size.value - const [pTop, pRight, pBottom, pLeft] = paddingInset.value - const [bTop, bRight, bBottom, bLeft] = borderInset.value - const actualWidth = width - pRight - pLeft - bRight - bLeft - const actualheight = height - pTop - pBottom - bTop - bBottom - layoutSignal.value = buildGlyphLayout(layoutProperties, actualWidth, actualheight) - }), - ) - - subscriptions.push( - effect(() => { - const font = fontSignal.value - if (font == null) { - return - } - const instancedText = new InstancedText( - getGroup(orderInfo.majorIndex, font), - getAlign, - getAppearance, - layoutSignal, - matrix, - isHidden, - parentClippingRect, - ) - return () => instancedText.destroy() - }), - ) - - return measureFunc -} - -const fontKeys = ['fontFamily', 'fontWeight'] - -export type FontFamilyProperties = { fontFamily?: string; fontWeight?: FontWeight } - -const defaultFontFamilyUrls = { - inter: { - light: 'https://pmndrs.github.io/uikit/fonts/inter-light.json', - normal: 'https://pmndrs.github.io/uikit/fonts/inter-normal.json', - medium: 'https://pmndrs.github.io/uikit/fonts/inter-medium.json', - 'semi-bold': 'https://pmndrs.github.io/uikit/fonts/inter-semi-bold.json', - bold: 'https://pmndrs.github.io/uikit/fonts/inter-bold.json', - }, -} satisfies FontFamilies - -export function computeFont( - properties: Signal, - fontFamilies: FontFamilies = defaultFontFamilyUrls, - renderer: WebGLRenderer, - subscriptions: Subscriptions, -): Signal { - const result = signal(undefined) - const get = createGetBatchedProperties(properties, fontKeys) - subscriptions.push( - effect(() => { - let fontWeight = (get('fontWeight') as FontWeight) ?? 'normal' - if (typeof fontWeight === 'string') { - fontWeight = fontWeightNames[fontWeight] - } - let fontFamily = get('fontFamily') as string - if (fontFamily == null) { - fontFamily = Object.keys(fontFamilies)[0] - } - const url = getMatchingFontUrl(fontFamilies[fontFamily], fontWeight) - let canceled = false - loadCachedFont(url, renderer, (font) => (canceled ? undefined : (result.value = font))) - return () => (canceled = true) - }), - ) - return result -} - -function getMatchingFontUrl(fontFamily: FontFamilyUrls, weight: number): string { - let distance = Infinity - let result: string | undefined - for (const fontWeight in fontFamily) { - const d = Math.abs(weight - getWeightNumber(fontWeight)) - if (d === 0) { - return fontFamily[fontWeight]! - } - if (d < distance) { - distance = d - result = fontFamily[fontWeight] - } - } - if (result == null) { - throw new Error(`font family has no entries ${fontFamily}`) - } - return result -} - -function getWeightNumber(value: string): number { - if (value in fontWeightNames) { - return fontWeightNames[value as keyof typeof fontWeightNames] - } - const number = parseFloat(value) - if (isNaN(number)) { - throw new Error(`invalid font weight "${value}"`) - } - return number -} - -export function computeMeasureFunc( - properties: Signal, - fontSignal: Signal, - textSignal: Signal | Array | string>>, - propertiesRef: { current: GlyphLayoutProperties | undefined }, -) { - const get = createGetBatchedProperties(properties, glyphPropertyKeys) - return computed(() => { - const font = fontSignal.value - if (font == null) { - return undefined - } - const textSignalValue = textSignal.value - const text = Array.isArray(textSignalValue) - ? textSignalValue.map((t) => readReactive(t)).join('') - : readReactive(textSignalValue) - const letterSpacing = (get('letterSpacing') as number) ?? 0 - const lineHeight = (get('lineHeight') as number) ?? 1.2 - const fontSize = (get('fontSize') as number) ?? 16 - const wordBreak = (get('wordBreak') as GlyphLayoutProperties['wordBreak']) ?? 'break-word' - - return (width, widthMode) => { - const availableWidth = widthMode === MeasureMode.Undefined ? undefined : width - return measureGlyphLayout( - (propertiesRef.current = { - font, - fontSize, - letterSpacing, - lineHeight, - text, - wordBreak, - }), - availableWidth, - ) - } - }) -} diff --git a/packages/uikit/src/text/render/index.ts b/packages/uikit/src/text/render/index.ts new file mode 100644 index 00000000..4694bb8d --- /dev/null +++ b/packages/uikit/src/text/render/index.ts @@ -0,0 +1,5 @@ +export * from './instanced-gylph-material' +export * from './instanced-glyph-mesh' +export * from './instanced-glyph' +export * from './instanced-glyph-group' +export * from './instanced-text' diff --git a/packages/uikit/src/text/render/instanced-glyph-group.ts b/packages/uikit/src/text/render/instanced-glyph-group.ts index 385192d9..93f80baa 100644 --- a/packages/uikit/src/text/render/instanced-glyph-group.ts +++ b/packages/uikit/src/text/render/instanced-glyph-group.ts @@ -1,11 +1,48 @@ -import { DynamicDrawUsage, Group, InstancedBufferAttribute, Material, TypedArray } from 'three' +import { DynamicDrawUsage, InstancedBufferAttribute, Material, TypedArray } from 'three' import { InstancedGlyph } from './instanced-glyph.js' import { InstancedGlyphMesh } from './instanced-glyph-mesh.js' import { InstancedGlyphMaterial } from './instanced-gylph-material.js' import { Font } from '../font.js' -import { CameraDistanceRef, OrderInfo, setupRenderOrder } from '../../order.js' +import { ElementType, OrderInfo, WithCameraDistance, setupRenderOrder } from '../../order.js' +import { Object3DRef } from '../../context.js' -export class InstancedGlyphGroup extends Group { +export class GlyphGroupManager { + private map = new Map>() + constructor( + private pixelSize: number, + private rootCameraDistance: WithCameraDistance, + private object: Object3DRef, + ) {} + + getGroup(majorIndex: number, font: Font) { + let groups = this.map.get(font) + if (groups == null) { + this.map.set(font, (groups = new Map())) + } + let glyphGroup = groups?.get(majorIndex) + if (glyphGroup == null) { + groups.set( + majorIndex, + (glyphGroup = new InstancedGlyphGroup(this.object, font, this.pixelSize, this.rootCameraDistance, { + majorIndex, + elementType: ElementType.Text, + minorIndex: 0, + })), + ) + } + return glyphGroup + } + + onFrame = (delta: number) => { + for (const groups of this.map.values()) { + for (const group of groups.values()) { + group.onFrame(delta) + } + } + } +} + +export class InstancedGlyphGroup { public instanceMatrix!: InstancedBufferAttribute public instanceUV!: InstancedBufferAttribute public instanceRGBA!: InstancedBufferAttribute @@ -21,12 +58,12 @@ export class InstancedGlyphGroup extends Group { private timeTillDecimate?: number constructor( + private object: Object3DRef, font: Font, public readonly pixelSize: number, - private readonly cameraDistance: CameraDistanceRef, + private readonly rootCameraDistance: WithCameraDistance, private orderInfo: OrderInfo, ) { - super() this.material = new InstancedGlyphMaterial(font) } @@ -94,11 +131,13 @@ export class InstancedGlyphGroup extends Group { onFrame(delta: number): void { const requiredSize = this.glyphs.length - this.holeIndicies.length + this.requestedGlyphs.length + if (this.mesh != null) { + this.mesh.visible = requiredSize > 0 + } + if (requiredSize === 0) { - this.visible = false return } - this.visible = true const availableSize = this.instanceMatrix?.count ?? 0 @@ -125,6 +164,7 @@ export class InstancedGlyphGroup extends Group { this.glyphs[indexOffset + i] = glyph } this.mesh!.count += requestedGlyphsLength + this.mesh!.visible = true this.requestedGlyphs.length = 0 } @@ -174,14 +214,14 @@ export class InstancedGlyphGroup extends Group { this.holeIndicies.length = 0 //destroying the old mesh - this.remove(oldMesh) + this.object.current?.remove(oldMesh) oldMesh.dispose() } //finalizing the new mesh - setupRenderOrder(this.mesh, this.cameraDistance, this.orderInfo) + setupRenderOrder(this.mesh, this.rootCameraDistance, this.orderInfo) this.mesh.count = this.glyphs.length - this.add(this.mesh) + this.object.current?.add(this.mesh) } } diff --git a/packages/uikit/src/text/render/instanced-glyph.ts b/packages/uikit/src/text/render/instanced-glyph.ts index a95fe04a..c5d0ed13 100644 --- a/packages/uikit/src/text/render/instanced-glyph.ts +++ b/packages/uikit/src/text/render/instanced-glyph.ts @@ -1,13 +1,89 @@ -import { Matrix4 } from 'three' -import { InstancedGlyphGroup } from './instanced-glyph-group.js' -import { Color as ColorRepresentation } from '@react-three/fiber' -import { colorToBuffer } from '../../utils.js' +import { Matrix4, WebGLRenderer } from 'three' +import { GlyphGroupManager, InstancedGlyphGroup } from './instanced-glyph-group.js' +import { ColorRepresentation, Subscriptions, colorToBuffer } from '../../utils.js' import { ClippingRect, defaultClippingData } from '../../clipping.js' -import { GlyphInfo, glyphIntoToUV } from '../font.js' +import { FontFamilies, FontFamilyProperties, GlyphInfo, computeFont, glyphIntoToUV } from '../font.js' +import { Signal, ReadonlySignal, signal, effect } from '@preact/signals-core' +import { FlexNode } from '../../flex/node.js' +import { OrderInfo } from '../../order.js' +import { createGetBatchedProperties } from '../../properties/batched.js' +import { MergedProperties } from '../../properties/merged.js' +import { GlyphLayoutProperties, GlyphLayout, buildGlyphLayout, computeMeasureFunc } from '../layout.js' +import { TextAlignProperties, TextAppearanceProperties, InstancedText } from './instanced-text.js' const helperMatrix1 = new Matrix4() const helperMatrix2 = new Matrix4() +const alignPropertyKeys = ['horizontalAlign', 'verticalAlign'] +const appearancePropertyKeys = ['color', 'opacity'] + +export type InstancedTextProperties = TextAlignProperties & + TextAppearanceProperties & + Omit & + FontFamilyProperties + +export function createInstancedText( + properties: Signal, + text: string | ReadonlySignal | Array>, + matrix: Signal, + node: FlexNode, + isHidden: Signal | undefined, + parentClippingRect: Signal | undefined, + orderInfo: OrderInfo, + fontFamilies: FontFamilies | undefined, + renderer: WebGLRenderer, + glyphGroupManager: GlyphGroupManager, + subscriptions: Subscriptions, +) { + const fontSignal = computeFont(properties, fontFamilies, renderer, subscriptions) + // eslint-disable-next-line react-hooks/exhaustive-deps + const textSignal = signal | Array>>(text) + let layoutPropertiesRef: { current: GlyphLayoutProperties | undefined } = { current: undefined } + + const measureFunc = computeMeasureFunc(properties, fontSignal, textSignal, layoutPropertiesRef) + + const getAlign = createGetBatchedProperties(properties, alignPropertyKeys) + const getAppearance = createGetBatchedProperties(properties, appearancePropertyKeys) + + const layoutSignal = signal(undefined) + subscriptions.push( + node.addLayoutChangeListener(() => { + const layoutProperties = layoutPropertiesRef.current + if (layoutProperties == null) { + return + } + const { size, paddingInset, borderInset } = node + const [width, height] = size.value + const [pTop, pRight, pBottom, pLeft] = paddingInset.value + const [bTop, bRight, bBottom, bLeft] = borderInset.value + const actualWidth = width - pRight - pLeft - bRight - bLeft + const actualheight = height - pTop - pBottom - bTop - bBottom + layoutSignal.value = buildGlyphLayout(layoutProperties, actualWidth, actualheight) + }), + ) + + subscriptions.push( + effect(() => { + const font = fontSignal.value + if (font == null) { + return + } + const instancedText = new InstancedText( + glyphGroupManager.getGroup(orderInfo.majorIndex, font), + getAlign, + getAppearance, + layoutSignal, + matrix, + isHidden, + parentClippingRect, + ) + return () => instancedText.destroy() + }), + ) + + return measureFunc +} + /** * renders an initially specified glyph */ diff --git a/packages/uikit/src/text/render/instanced-text.ts b/packages/uikit/src/text/render/instanced-text.ts index 795149f4..795ccac7 100644 --- a/packages/uikit/src/text/render/instanced-text.ts +++ b/packages/uikit/src/text/render/instanced-text.ts @@ -1,9 +1,8 @@ import { Signal, effect } from '@preact/signals-core' import { InstancedGlyph } from './instanced-glyph.js' -import { Color as ColorRepresentation } from '@react-three/fiber' import { Matrix4 } from 'three' import { ClippingRect } from '../../clipping.js' -import { alignmentXMap, alignmentYMap } from '../../utils.js' +import { ColorRepresentation, alignmentXMap, alignmentYMap } from '../../utils.js' import { getGlyphLayoutHeight, getGlyphOffsetX, diff --git a/packages/uikit/src/text/wrapper/index.ts b/packages/uikit/src/text/wrapper/index.ts index 9687e488..67c36784 100644 --- a/packages/uikit/src/text/wrapper/index.ts +++ b/packages/uikit/src/text/wrapper/index.ts @@ -1,4 +1,4 @@ -import { GlyphLayoutLine, GlyphLayoutProperties } from '../layout.js' +import type { GlyphLayoutLine, GlyphLayoutProperties } from '../layout.js' export type GlyphWrapper = ( layout: GlyphLayoutProperties, diff --git a/packages/uikit/src/transform.ts b/packages/uikit/src/transform.ts index 41c2baeb..ec878bbc 100644 --- a/packages/uikit/src/transform.ts +++ b/packages/uikit/src/transform.ts @@ -1,9 +1,10 @@ -import { Signal, computed } from '@preact/signals-core' +import { Signal, computed, effect } from '@preact/signals-core' import { Euler, Matrix4, Quaternion, Vector3, Vector3Tuple } from 'three' import { FlexNode } from './flex/node.js' -import { alignmentXMap, alignmentYMap } from './utils.js' +import { Subscriptions, alignmentXMap, alignmentYMap } from './utils.js' import { createGetBatchedProperties } from './properties/batched.js' import { MergedProperties } from './properties/merged.js' +import { Object3DRef } from './context.js' export type TransformProperties = { transformTranslateX?: number @@ -51,6 +52,7 @@ function toQuaternion([x, y, z]: Vector3Tuple): Quaternion { export function computeTransformMatrix( propertiesSignal: Signal, node: FlexNode, + pixelSize: number, renameOutput?: Record, ): Signal { //B * O^-1 * T * O @@ -59,7 +61,7 @@ export function computeTransformMatrix( //T = transform matrix (translate, rotate, scale) const get = createGetBatchedProperties(propertiesSignal, propertyKeys, renameOutput) return computed(() => { - const { pixelSize, relativeCenter } = node + const { relativeCenter } = node const [x, y] = relativeCenter.value const result = new Matrix4().makeTranslation(x * pixelSize, y * pixelSize, 0) @@ -91,3 +93,19 @@ export function computeTransformMatrix( return result }) } + +export function applyTransform( + object: Object3DRef, + transformMatrix: Signal, + subscriptions: Subscriptions, +) { + subscriptions.push( + effect(() => { + if (transformMatrix.value == null) { + object.current?.matrix.elements.fill(0) + return + } + object.current?.matrix.copy(transformMatrix.value) + }), + ) +} diff --git a/packages/uikit/src/utils.ts b/packages/uikit/src/utils.ts index 2c30d6c2..89817fd6 100644 --- a/packages/uikit/src/utils.ts +++ b/packages/uikit/src/utils.ts @@ -1,10 +1,10 @@ -import { computed, effect, Signal, signal } from '@preact/signals-core' -import { Vector2Tuple, BufferAttribute, Color } from 'three' -import { Color as ColorRepresentation } from '@react-three/fiber' +import { computed, Signal, signal } from '@preact/signals-core' +import { Vector2Tuple, BufferAttribute, Color, Vector3Tuple } from 'three' import { Inset } from './flex/node.js' import { Yoga, loadYoga as loadYogaImpl } from 'yoga-layout/wasm-async' import { MergedProperties } from './properties/merged.js' -import { Properties } from './properties/default.js' + +export type ColorRepresentation = Color | string | number | Vector3Tuple export type Subscriptions = Array<() => void> @@ -13,6 +13,7 @@ export function unsubscribeSubscriptions(subscriptions: Subscriptions): void { for (let i = 0; i < length; i++) { subscriptions[i]() } + subscriptions.length = 0 } export const alignmentXMap = { left: 0.5, center: 0, right: -0.5 } @@ -79,9 +80,12 @@ export function readReactive(value: T | Signal): T { export function createConditionalPropertyTranslator(condition: () => boolean) { const signalMap = new Map>() - return (merged: MergedProperties, properties: Properties) => { + return (properties: unknown, merged: MergedProperties) => { + if (typeof properties != 'object') { + throw new Error(`Invalid properties "${properties}"`) + } for (const key in properties) { - const value = properties[key] + const value = properties[key as never] if (value === undefined) { return } diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts new file mode 100644 index 00000000..484cbe6a --- /dev/null +++ b/packages/uikit/src/vanilla/container.ts @@ -0,0 +1,84 @@ +import { Object3D } from 'three' +import { WithContext } from '../context' +import { + ContainerProperties, + createContainer, + createContainerPropertyTransfomers, + updateContainerProperties, +} from '../components/container' +import { createListeners, updateListeners } from '../listeners' +import { Signal, effect, signal } from '@preact/signals-core' +import { MergedProperties, PropertyTransformers } from '../properties/merged' +import { Component, BindEventHandlers } from './utils' +import { Subscriptions, unsubscribeSubscriptions } from '../utils' +import { AllOptionalProperties } from '../properties/default' +import { createInteractionPanel } from '../panel/instanced-panel-mesh' +import { EventHandlers } from '../events' + +export class Container extends Object3D { + private propertiesSignal: Signal + private container: Object3D + private subscriptions: Subscriptions = [] + private propertySubscriptions: Subscriptions = [] + private listeners = createListeners() + private propertyTransformers: PropertyTransformers + private hoveredSignal = signal>([]) + private activeSignal = signal>([]) + + public readonly bindEventHandlers: BindEventHandlers + public readonly ctx: WithContext + + constructor(parent: Component, properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { + super() + const scrollHandlers = signal({}) + const rootSize = parent.ctx.root.node.size + this.propertyTransformers = createContainerPropertyTransfomers(rootSize, this.hoveredSignal, this.activeSignal) + this.bindEventHandlers = parent.bindEventHandlers + this.container = new Object3D() + this.container.matrixAutoUpdate = false + this.container.add(this) + this.matrixAutoUpdate = false + parent.add(this.container) + this.propertiesSignal = signal(undefined as any) + this.setProperties(properties, defaultProperties) + this.ctx = createContainer( + this.propertiesSignal, + { current: this.container }, + { current: this }, + parent.ctx, + scrollHandlers, + this.listeners, + this.subscriptions, + ) + const interactionPanel = createInteractionPanel( + this.ctx.node.size, + this.ctx.root.pixelSize, + this.ctx.orderInfo, + parent.ctx.clippingRect, + this.ctx.root.object, + this.subscriptions, + ) + this.container.add(interactionPanel) + this.subscriptions.push(effect(() => this.bindEventHandlers(interactionPanel, scrollHandlers.value))) + } + + setProperties(properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { + const handlers = updateContainerProperties( + this.propertiesSignal, + properties, + defaultProperties, + this.hoveredSignal, + this.activeSignal, + this.propertyTransformers, + this.propertySubscriptions, + ) + this.bindEventHandlers(this.container, handlers) + updateListeners(this.listeners, properties) + } + + destroy() { + this.container.parent?.remove(this.container) + unsubscribeSubscriptions(this.propertySubscriptions) + unsubscribeSubscriptions(this.subscriptions) + } +} diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts new file mode 100644 index 00000000..85516b16 --- /dev/null +++ b/packages/uikit/src/vanilla/image.ts @@ -0,0 +1,98 @@ +import { Object3D, Object3DEventMap, Texture, Vector2Tuple } from 'three' +import { WithContext } from '../context' +import { + InheritableImageProperties, + createImage, + createImageMesh, + computeTextureAspectRatio, + createImagePropertyTransformers, + loadImageTexture, + updateImageProperties, + ImageProperties, +} from '../components/image' +import { Listeners, createListeners, updateListeners } from '../listeners' +import { Signal, effect, signal } from '@preact/signals-core' +import { MergedProperties, PropertyTransformers } from '../properties/merged' +import { Component, BindEventHandlers } from './utils' +import { Subscriptions, unsubscribeSubscriptions } from '../utils' +import { AllOptionalProperties } from '../properties/default' +import { EventHandlers } from '../events' + +export class Image extends Object3D { + private propertiesSignal: Signal + private container: Object3D + private subscriptions: Subscriptions = [] + private propertySubscriptions: Subscriptions = [] + private imageSubscriptions: Subscriptions = [] + private listeners = createListeners() + private propertyTransformers: PropertyTransformers + private hoveredSignal = signal>([]) + private activeSignal = signal>([]) + private texture = signal(undefined) + private textureAspectRatio: Signal + + public readonly bindEventHandlers: BindEventHandlers + public readonly ctx: WithContext + private prevSrc?: ImageProperties['src'] + + constructor(parent: Component, properties: ImageProperties, defaultProperties?: AllOptionalProperties) { + super() + this.textureAspectRatio = computeTextureAspectRatio(this.texture) + const scrollHandlers = signal({}) + const rootSize = parent.ctx.root.node.size + this.propertyTransformers = createImagePropertyTransformers( + rootSize, + this.hoveredSignal, + this.activeSignal, + this.textureAspectRatio, + ) + this.bindEventHandlers = parent.bindEventHandlers + this.container = new Object3D() + this.container.matrixAutoUpdate = false + this.container.add(this) + this.matrixAutoUpdate = false + parent.add(this.container) + this.propertiesSignal = signal(undefined as any) + this.setProperties(properties, defaultProperties) + this.ctx = createImage( + this.propertiesSignal, + { current: this.container }, + { current: this }, + parent.ctx, + scrollHandlers, + this.listeners, + this.subscriptions, + ) + const mesh = createImageMesh(this.propertiesSignal, this.texture, parent.ctx, this.ctx, this.subscriptions) + this.container.add(mesh) + this.subscriptions.push(effect(() => this.bindEventHandlers(mesh, scrollHandlers.value))) + } + + setProperties(properties: ImageProperties, defaultProperties?: AllOptionalProperties) { + if (properties.src != this.prevSrc) { + unsubscribeSubscriptions(this.imageSubscriptions) + loadImageTexture(this.texture, properties.src, this.imageSubscriptions) + this.prevSrc = properties.src + } + unsubscribeSubscriptions(this.propertySubscriptions) + const handlers = updateImageProperties( + this.propertiesSignal, + this.textureAspectRatio, + properties, + defaultProperties, + this.hoveredSignal, + this.activeSignal, + this.propertyTransformers, + this.propertySubscriptions, + ) + this.bindEventHandlers(this.container, handlers) + updateListeners(this.listeners, properties) + } + + destroy() { + this.container.parent?.remove(this.container) + unsubscribeSubscriptions(this.imageSubscriptions) + unsubscribeSubscriptions(this.propertySubscriptions) + unsubscribeSubscriptions(this.subscriptions) + } +} diff --git a/packages/uikit/src/vanilla/index.ts b/packages/uikit/src/vanilla/index.ts new file mode 100644 index 00000000..b55e9e23 --- /dev/null +++ b/packages/uikit/src/vanilla/index.ts @@ -0,0 +1,3 @@ +export * from './container' +export * from './root' +export * from './image' diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts new file mode 100644 index 00000000..d7575cfa --- /dev/null +++ b/packages/uikit/src/vanilla/root.ts @@ -0,0 +1,98 @@ +import { Camera, Object3D, Vector2Tuple } from 'three' +import { WithContext } from '../context' +import { createListeners, updateListeners } from '../listeners' +import { Signal, effect, signal } from '@preact/signals-core' +import { MergedProperties, PropertyTransformers } from '../properties/merged' +import { BindEventHandlers } from './utils' +import { Subscriptions, unsubscribeSubscriptions } from '../utils' +import { AllOptionalProperties } from '../properties/default' +import { createInteractionPanel } from '../panel/instanced-panel-mesh' +import { updateRootProperties, createRoot, createRootPropertyTransformers, RootProperties } from '../components/root' +import { EventHandlers } from '../events' + +export class Root extends Object3D { + private propertiesSignal: Signal + private container: Object3D + private subscriptions: Subscriptions = [] + private propertySubscriptions: Subscriptions = [] + private listeners = createListeners() + private scrollHandlers = signal({}) + private onFrameSet = new Set<(delta: number) => void>() + private propertyTransformers: PropertyTransformers + private hoveredSignal = signal>([]) + private activeSignal = signal>([]) + + public readonly ctx: WithContext + + constructor( + camera: Camera | (() => Camera), + object: Object3D, + properties: RootProperties, + defaultProperties?: AllOptionalProperties, + public readonly bindEventHandlers: BindEventHandlers = () => {}, + ) { + super() + const rootSize = signal([0, 0]) + this.propertyTransformers = createRootPropertyTransformers( + rootSize, + this.hoveredSignal, + this.activeSignal, + properties.pixelSize, + ) + this.container = new Object3D() + this.container.matrixAutoUpdate = false + this.container.add(this) + this.matrixAutoUpdate = false + object.add(this.container) + this.propertiesSignal = signal(undefined as any) + this.setProperties(properties, defaultProperties) + this.ctx = createRoot( + this.propertiesSignal, + rootSize, + { current: this.container }, + { current: this }, + this.scrollHandlers, + this.listeners, + properties.pixelSize, + this.onFrameSet, + typeof camera === 'function' ? camera : () => camera, + this.subscriptions, + ) + const interactionPanel = createInteractionPanel( + this.ctx.node.size, + this.ctx.root.pixelSize, + this.ctx.orderInfo, + undefined, + this.ctx.root.object, + this.subscriptions, + ) + this.container.add(interactionPanel) + this.subscriptions.push(effect(() => this.bindEventHandlers(interactionPanel, this.scrollHandlers.value))) + } + + update(delta: number) { + for (const onFrame of this.onFrameSet) { + onFrame(delta) + } + } + + setProperties(properties: RootProperties, defaultProperties?: AllOptionalProperties) { + const handlers = updateRootProperties( + this.propertiesSignal, + properties, + defaultProperties, + this.hoveredSignal, + this.activeSignal, + this.propertyTransformers, + this.propertySubscriptions, + ) + this.bindEventHandlers(this.container, handlers) + updateListeners(this.listeners, properties) + } + + destroy() { + this.container.parent?.remove(this.container) + unsubscribeSubscriptions(this.propertySubscriptions) + unsubscribeSubscriptions(this.subscriptions) + } +} diff --git a/packages/uikit/src/vanilla/utils.ts b/packages/uikit/src/vanilla/utils.ts new file mode 100644 index 00000000..dc944e82 --- /dev/null +++ b/packages/uikit/src/vanilla/utils.ts @@ -0,0 +1,9 @@ +import { Object3D } from 'three' +import { Container } from './container' +import { Root } from './root' +import { Image } from './image' +import { EventHandlers } from '../events' + +export type Component = Container | Root | Image + +export type BindEventHandlers = (object: Object3D, handlers: EventHandlers) => void diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4529b155..0acdbb5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,7 +106,7 @@ importers: version: 1.3.0(@react-three/fiber@8.15.13)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../packages/uikit + version: link:../../packages/react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../packages/icons/lucide @@ -154,7 +154,7 @@ importers: version: 1.3.0(@react-three/fiber@8.15.13)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../packages/uikit + version: link:../../packages/react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../packages/icons/lucide @@ -196,7 +196,7 @@ importers: version: 2.16.0(@react-three/fiber@8.15.13)(@types/three@0.161.2)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../packages/uikit + version: link:../../packages/react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../packages/icons/lucide @@ -238,7 +238,7 @@ importers: version: 8.15.13(react-dom@18.2.0)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../packages/uikit + version: link:../../packages/react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../packages/icons/lucide @@ -265,7 +265,7 @@ importers: version: 8.15.13(react-dom@18.2.0)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../packages/uikit + version: link:../../packages/react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../packages/icons/lucide @@ -289,7 +289,7 @@ importers: version: 2.16.0(@react-three/fiber@8.15.13)(@types/three@0.161.2)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../packages/uikit + version: link:../../packages/react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../packages/icons/lucide @@ -322,7 +322,7 @@ importers: version: 8.15.13(react-dom@18.2.0)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../packages/uikit + version: link:../../packages/react react: specifier: ^18.2.0 version: 18.2.0 @@ -337,6 +337,22 @@ importers: specifier: ^0.161.0 version: 0.161.2 + examples/vanilla: + dependencies: + '@react-three/uikit': + specifier: workspace:^ + version: link:../../packages/react + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + three: + specifier: ^0.161.0 + version: 0.161.0 + devDependencies: + '@types/three': + specifier: ^0.161.0 + version: 0.161.2 + packages/fonts: dependencies: msdf-bmfont-xml: @@ -350,7 +366,7 @@ importers: dependencies: '@react-three/uikit': specifier: workspace:^ - version: link:../../uikit + version: link:../../react devDependencies: '@types/node': specifier: ^20.11.0 @@ -375,7 +391,7 @@ importers: version: 8.15.13(react-dom@18.2.0)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../uikit + version: link:../../react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../icons/lucide @@ -403,7 +419,7 @@ importers: version: 8.15.13(react-dom@18.2.0)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../uikit + version: link:../../react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../icons/lucide @@ -417,29 +433,14 @@ importers: specifier: ^0.161.0 version: 0.161.0 - packages/uikit: + packages/react: dependencies: '@preact/signals-core': specifier: ^1.5.1 version: 1.5.1 - chalk: - specifier: ^5.3.0 - version: 5.3.0 - commander: - specifier: ^12.0.0 - version: 12.0.0 - ora: - specifier: ^8.0.1 - version: 8.0.1 - prompts: - specifier: ^2.4.2 - version: 2.4.2 - yoga-layout: - specifier: ^2.0.1 - version: 2.0.1 - zod: - specifier: ^3.22.4 - version: 3.22.4 + '@vanilla-three/uikit': + specifier: workspace:^ + version: link:../uikit devDependencies: '@react-three/drei': specifier: ^9.96.1 @@ -447,9 +448,6 @@ importers: '@react-three/fiber': specifier: ^8.15.13 version: 8.15.13(react-dom@18.2.0)(react@18.2.0)(three@0.161.0) - '@types/node': - specifier: ^20.11.0 - version: 20.11.0 '@types/prompts': specifier: ^2.4.9 version: 2.4.9 @@ -475,6 +473,22 @@ importers: specifier: ^0.161.0 version: 0.161.0 + packages/uikit: + dependencies: + '@preact/signals-core': + specifier: ^1.5.1 + version: 1.5.1 + yoga-layout: + specifier: ^2.0.1 + version: 2.0.1 + devDependencies: + '@types/three': + specifier: ^0.161.0 + version: 0.161.2 + three: + specifier: ^0.161.0 + version: 0.161.0 + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -2775,11 +2789,6 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: false - /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -3177,11 +3186,6 @@ packages: ansi-styles: 4.3.0 supports-color: 7.2.0 - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: false - /check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: @@ -3227,24 +3231,12 @@ packages: engines: {node: '>=6'} dev: false - /cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - restore-cursor: 4.0.0 - dev: false - /cli-progress@3.12.0: resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} engines: {node: '>=4'} dependencies: string-width: 4.2.3 - /cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - dev: false - /cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: @@ -3309,11 +3301,6 @@ packages: delayed-stream: 1.0.0 dev: false - /commander@12.0.0: - resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==} - engines: {node: '>=18'} - dev: false - /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: false @@ -3547,10 +3534,6 @@ packages: resolution: {integrity: sha512-4nToZ5jlPO14W82NkF32wyjhYqQByVaDmLy4J2/tYcAbJfgO2TKJC780Az1V13gzq4l73CJ0yuyalpXvxXXD9A==} dev: true - /emoji-regex@10.3.0: - resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} - dev: false - /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4175,11 +4158,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - /get-east-asian-width@1.2.0: - resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} - engines: {node: '>=18'} - dev: false - /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true @@ -4616,11 +4594,6 @@ packages: is-path-inside: 3.0.3 dev: false - /is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - dev: false - /is-invalid-path@1.0.2: resolution: {integrity: sha512-6KLcFrPCEP3AFXMfnWrIFkZpYNBVzZAoBJJDEZKtI3LXkaDjM3uFMJQjxiizUuZTZ9Oh9FNv/soXbx5TcpaDmA==} engines: {node: '>=6.0'} @@ -4718,16 +4691,6 @@ packages: engines: {node: '>=10'} dev: true - /is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - dev: false - - /is-unicode-supported@2.0.0: - resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} - engines: {node: '>=18'} - dev: false - /is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} dev: true @@ -4893,6 +4856,7 @@ packages: /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + dev: true /ktx-parse@0.4.5: resolution: {integrity: sha512-MK3FOody4TXbFf8Yqv7EBbySw7aPvEcPX++Ipt6Sox+/YMFvR5xaTyhfNSk1AEmMy+RYIw81ctN4IMxCB8OAlg==} @@ -4961,14 +4925,6 @@ packages: is-unicode-supported: 0.1.0 dev: true - /log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} - dependencies: - chalk: 5.3.0 - is-unicode-supported: 1.3.0 - dev: false - /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -5115,11 +5071,6 @@ packages: engines: {node: '>=4'} hasBin: true - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: false - /mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -5422,13 +5373,6 @@ packages: dependencies: wrappy: 1.0.2 - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 - dev: false - /opentype.js@0.11.0: resolution: {integrity: sha512-Z9NkAyQi/iEKQYzCSa7/VJSqVIs33wknw8Z8po+DzuRUAqivJ+hJZ94mveg3xIeKwLreJdWTMyEO7x1K13l41Q==} hasBin: true @@ -5458,21 +5402,6 @@ packages: type-check: 0.4.0 dev: true - /ora@8.0.1: - resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} - engines: {node: '>=18'} - dependencies: - chalk: 5.3.0 - cli-cursor: 4.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 2.0.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.1.0 - strip-ansi: 7.1.0 - dev: false - /oslllo-potrace@2.0.1: resolution: {integrity: sha512-XDsVIUfwXnylngcbecF/6gBHdtFgEnqDt0a9WKqXIo/jPe2AkZkmi6bNaNb9OwlAgoIjy0b1Hi6odPEqztPszg==} dependencies: @@ -5705,14 +5634,6 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - /prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - dev: false - /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -6003,14 +5924,6 @@ packages: lowercase-keys: 1.0.1 dev: false - /restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: false - /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -6205,10 +6118,6 @@ packages: is-arrayish: 0.3.2 dev: false - /sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: false - /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -6260,11 +6169,6 @@ packages: /stats.js@0.17.0: resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} - /stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} - dev: false - /streamx@2.16.1: resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} dependencies: @@ -6282,15 +6186,6 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - /string-width@7.1.0: - resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} - engines: {node: '>=18'} - dependencies: - emoji-regex: 10.3.0 - get-east-asian-width: 1.2.0 - strip-ansi: 7.1.0 - dev: false - /string.prototype.codepointat@0.2.1: resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} dev: false @@ -6345,13 +6240,6 @@ packages: dependencies: ansi-regex: 5.0.1 - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - dev: false - /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -7098,10 +6986,6 @@ packages: resolution: {integrity: sha512-tT/oChyDXelLo2A+UVnlW9GU7CsvFMaEnd9kVFsaiCQonFAXd3xrHhkLYu+suwwosrAEQ746xBU+HvYtm1Zs2Q==} dev: false - /zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - dev: false - /zstddec@0.0.2: resolution: {integrity: sha512-DCo0oxvcvOTGP/f5FA6tz2Z6wF+FIcEApSTu0zV5sQgn9hoT5lZ9YRAKUraxt9oP7l4e8TnNdi8IZTCX6WCkwA==} dev: false From 7193b0383f5ec5cfb719ce3a8f3bf1e150950273 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Fri, 22 Mar 2024 21:09:43 +0100 Subject: [PATCH 04/20] createState --- .gitignore | 4 +- examples/uikit/src/App.tsx | 288 +++++++++--------- examples/uikit/vite.config.ts | 2 +- examples/vanilla/index.ts | 13 +- examples/vanilla/package.json | 2 +- packages/react/src/container.tsx | 71 +---- packages/react/src/image.tsx | 91 ++++++ packages/react/src/index.ts | 1 + packages/react/src/root.tsx | 10 +- packages/uikit/src/components/container.ts | 82 +++-- packages/uikit/src/components/image.ts | 8 +- packages/uikit/src/context.ts | 7 + .../uikit/src/panel/instanced-panel-mesh.ts | 26 +- packages/uikit/src/properties/merged.ts | 4 + packages/uikit/src/vanilla/container.ts | 85 ++---- packages/uikit/src/vanilla/image.ts | 7 +- packages/uikit/src/vanilla/index.ts | 6 + packages/uikit/src/vanilla/root.ts | 2 - packages/uikit/src/vanilla/utils.ts | 9 - pnpm-lock.yaml | 4 +- 20 files changed, 389 insertions(+), 333 deletions(-) delete mode 100644 packages/uikit/src/vanilla/utils.ts diff --git a/.gitignore b/.gitignore index 8982f6f3..7f3358ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ node_modules dist package-lock.json -.DS_Store -*.js -*.d.ts \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/examples/uikit/src/App.tsx b/examples/uikit/src/App.tsx index b082c8fb..78bb3ac7 100644 --- a/examples/uikit/src/App.tsx +++ b/examples/uikit/src/App.tsx @@ -1,157 +1,153 @@ -import { Suspense, useMemo, useState } from 'react' import { Canvas } from '@react-three/fiber' -import { Gltf, Box, PerspectiveCamera, RenderTexture } from '@react-three/drei' -import { signal } from '@preact/signals-core' -import { - DefaultProperties, - Container, - Content, - CustomContainer, - Svg, - Text, - Image, - Fullscreen, - Portal, - SuspendingImage, -} from '@react-three/uikit' -import { Texture } from 'three' -import { Skeleton } from '../../../packages/kits/default/skeleton' +import { Gltf } from '@react-three/drei' +import { Container, Root, Image } from '@react-three/uikit' export default function App() { - const texture = useMemo(() => signal(undefined), []) - const [show, setShow] = useState(false) - const s = useMemo(() => signal(5), []) - const x = useMemo(() => signal('red'), []) - const t = useMemo(() => signal('X X\nX X'), []) return ( - + - (texture.value = t ?? undefined)}> - - - - - - - - - - - Escribe algo... - - - Escribe algo... - - - - - { - t.value += 'X' - setShow((s) => !s) - }} - width="100%" - backgroundOpacity={0.5} - backgroundColor="black" - fontSize={30} - verticalAlign="bottom" - horizontalAlign="block" - cursor="pointer" - > - {t} - more - - (x.value = hover ? 'yellow' : undefined)} - backgroundColor={x} - borderColor="white" - borderBend={1} - border={20} - borderRadius={30} - width={300} - height={100} - /> - - - - console.log(w, h)} - keepAspectRatio={false} - borderRight={100} - > - - - - - - - - - - }> - - - - - - - Hello world - - - {show ? ( - - (s.value += 10)} - backgroundColor="yellow" - width={300} - minHeight={300} - height={300} - /> - - - ) : undefined} - + + + + + ) } + +/* + + + + + + + + + Escribe algo... + + + Escribe algo... + + + + + { + t.value += 'X' + setShow((s) => !s) + }} + width="100%" + backgroundOpacity={0.5} + backgroundColor="black" + fontSize={30} + verticalAlign="bottom" + horizontalAlign="block" + cursor="pointer" + > + {t} + more + + (x.value = hover ? 'yellow' : undefined)} + backgroundColor={x} + borderColor="white" + borderBend={1} + border={20} + borderRadius={30} + width={300} + height={100} + /> + + + + console.log(w, h)} + keepAspectRatio={false} + borderRight={100} + > + + + + + + + + + + }> + + + + + + + Hello world + + +{show ? ( + + (s.value += 10)} + backgroundColor="yellow" + width={300} + minHeight={300} + height={300} + /> + + +) : undefined} +*/ diff --git a/examples/uikit/vite.config.ts b/examples/uikit/vite.config.ts index 9a823cca..d801f5a1 100644 --- a/examples/uikit/vite.config.ts +++ b/examples/uikit/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ resolve: { alias: [ { find: '@', replacement: path.resolve(__dirname, '../../packages/kits/default') }, - { find: '@react-three/uikit', replacement: path.resolve(__dirname, '../../packages/uikit/src/index.ts') }, + { find: '@react-three/uikit', replacement: path.resolve(__dirname, '../../packages/react/src/index.ts') }, ], }, }) diff --git a/examples/vanilla/index.ts b/examples/vanilla/index.ts index 96804fdd..7d220252 100644 --- a/examples/vanilla/index.ts +++ b/examples/vanilla/index.ts @@ -1,5 +1,5 @@ import { BoxGeometry, Mesh, MeshNormalMaterial, PerspectiveCamera, Scene, WebGLRenderer } from 'three' -import { patchRenderOrder, Container, Root, Image } from '@react-three/uikit' +import { patchRenderOrder, Container, Root, Image } from '@vanilla-three/uikit' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' // init @@ -21,9 +21,16 @@ const root = new Root(camera, scene, { sizeY: 0.5, backgroundColor: 'red', }) -new Image(root, { flexBasis: 0, flexGrow: 1, border: 10, borderColor: 'green', src: 'https://picsum.photos/300/300' }) new Container(root, { flexGrow: 1, backgroundColor: 'blue' }) -new Container(root, { flexGrow: 1, backgroundColor: 'green' }) +const x = new Container(root, { padding: 30, flexGrow: 1, backgroundColor: 'green' }) +new Image(x, { + keepAspectRatio: false, + borderRadius: 1000, + height: '100%', + flexBasis: 0, + flexGrow: 1, + src: 'https://picsum.photos/300/300', +}) const geometry = new BoxGeometry(0.2, 0.2, 0.2) const material = new MeshNormalMaterial() diff --git a/examples/vanilla/package.json b/examples/vanilla/package.json index 95ecaca8..e2885454 100644 --- a/examples/vanilla/package.json +++ b/examples/vanilla/package.json @@ -1,7 +1,7 @@ { "type": "module", "dependencies": { - "@react-three/uikit": "workspace:^", + "@vanilla-three/uikit": "workspace:^", "react-dom": "^18.2.0", "three": "^0.161.0" }, diff --git a/packages/react/src/container.tsx b/packages/react/src/container.tsx index d13bd444..d917994e 100644 --- a/packages/react/src/container.tsx +++ b/packages/react/src/container.tsx @@ -4,19 +4,13 @@ import { Object3D } from 'three' import { ParentProvider, useParent } from './context' import { AddHandlers, AddScrollHandler } from './utilts' import { - createContainer, createInteractionPanel, - createListeners, - MergedProperties, - Subscriptions, - unsubscribeSubscriptions, - updateListeners, - EventHandlers as CoreEventHandlers, updateContainerProperties, - createContainerPropertyTransfomers, ContainerProperties, + createContainerState, + cleanContainerState, + createContainerContext, } from '@vanilla-three/uikit/internals' -import { signal } from '@preact/signals-core' import { useDefaultProperties } from './default' export const Container: ( @@ -26,65 +20,24 @@ export const Container: ( EventHandlers, ) => ReactNode = forwardRef((properties, ref) => { //TODO: ComponentInternals - const outerRef = useRef(null) - const innerRef = useRef(null) const parent = useParent() - const defaultProperties = useDefaultProperties() - const propertiesSignal = useMemo(() => signal(undefined as any), []) - const hoveredSignal = useMemo(() => signal>([]), []) - const activeSignal = useMemo(() => signal>([]), []) - const tranformers = useMemo( - () => createContainerPropertyTransfomers(parent.root.node.size, hoveredSignal, activeSignal), - [parent, hoveredSignal, activeSignal], - ) - const propertiesSubscriptions = useMemo(() => [], []) - unsubscribeSubscriptions(propertiesSubscriptions) - const handlers = updateContainerProperties( - propertiesSignal, - properties, - defaultProperties, - hoveredSignal, - activeSignal, - tranformers, - propertiesSubscriptions, - ) - - const listeners = useMemo(() => createListeners(), []) - updateListeners(listeners, properties) + const state = useMemo(() => createContainerState(parent.root.node.size), [parent]) + useEffect(() => () => cleanContainerState(state), [state]) - const scrollHandlers = useMemo(() => signal({}), []) + const defaultProperties = useDefaultProperties() + const handlers = updateContainerProperties(state, properties, defaultProperties) - const subscriptions = useMemo(() => [], []) - const ctx = useMemo( - () => createContainer(propertiesSignal, outerRef, innerRef, parent, scrollHandlers, listeners, subscriptions), - [listeners, parent, propertiesSignal, scrollHandlers, subscriptions], - ) - useEffect( - () => () => { - unsubscribeSubscriptions(propertiesSubscriptions) - unsubscribeSubscriptions(subscriptions) - }, - [propertiesSubscriptions, subscriptions], - ) + const outerRef = useRef(null) + const innerRef = useRef(null) + const ctx = useMemo(() => createContainerContext(state, outerRef, innerRef, parent), [parent, state]) //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) - const interactionPanel = useMemo( - () => - createInteractionPanel( - ctx.node.size, - parent.root.pixelSize, - ctx.orderInfo, - parent.clippingRect, - ctx.root.object, - subscriptions, - ), - [ctx, parent, subscriptions], - ) + const interactionPanel = useMemo(() => createInteractionPanel(ctx, parent, state.subscriptions), [ctx, parent, state]) return ( - + diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx index e69de29b..f8af71fa 100644 --- a/packages/react/src/image.tsx +++ b/packages/react/src/image.tsx @@ -0,0 +1,91 @@ +import { signal } from '@preact/signals-core' +import { + createListeners, + computeTextureAspectRatio, + EventHandlers, + createImagePropertyTransformers, + createImage, + createImageMesh, + loadImageTexture, + updateImageProperties, + updateListeners, + ImageProperties, + Subscriptions, + unsubscribeSubscriptions, +} from '@vanilla-three/uikit/internals' +import { ReactNode, forwardRef, useEffect, useMemo, useRef } from 'react' +import { Object3D, Texture } from 'three' +import { AddHandlers, AddScrollHandler } from './utilts' +import { ParentProvider, useParent } from './context' +import { useDefaultProperties } from './default' + +export const Image: (props: ImageProperties & { children?: ReactNode }) => ReactNode = forwardRef((properties, ref) => { + const texture = useMemo(() => signal(undefined), []) + const imageSubscriptions = useMemo(() => [], []) + const srcRef = useRef(undefined) + if (properties.src != srcRef.current) { + unsubscribeSubscriptions(imageSubscriptions) + loadImageTexture(texture, properties.src, imageSubscriptions) + srcRef.current = properties.src + } + + const scrollHandlers = useMemo(() => signal({}), []) + const parent = useParent() + const hoveredSignal = useMemo(() => signal>([]), []) + const activeSignal = useMemo(() => signal>([]), []) + const tranformers = useMemo( + () => createImagePropertyTransformers(parent.root.node.size, hoveredSignal, activeSignal), + [activeSignal, hoveredSignal, parent], + ) + const propertiesSignal = useMemo(() => signal(undefined as any), []) + const defaultProperties = useDefaultProperties() + const propertySubscriptions = useMemo(() => [], []) + const textureAspectRatio = useMemo(() => computeTextureAspectRatio(texture), [texture]) + unsubscribeSubscriptions(propertySubscriptions) + const handlers = updateImageProperties( + propertiesSignal, + textureAspectRatio, + properties, + defaultProperties, + hoveredSignal, + activeSignal, + tranformers, + propertySubscriptions, + ) + + const listeners = useMemo(() => createListeners(), []) + updateListeners(listeners, properties) + + const outerRef = useRef(null) + const innerRef = useRef(null) + const subscriptions = useMemo(() => [], []) + const ctx = useMemo( + () => createImage(propertiesSignal, outerRef, innerRef, parent, scrollHandlers, listeners, subscriptions), + [listeners, parent, propertiesSignal, scrollHandlers, subscriptions], + ) + + const mesh = useMemo( + () => createImageMesh(propertiesSignal, texture, parent, ctx, subscriptions), + [ctx, parent, propertiesSignal, subscriptions, texture], + ) + + useEffect( + () => () => { + unsubscribeSubscriptions(imageSubscriptions) + unsubscribeSubscriptions(propertySubscriptions) + unsubscribeSubscriptions(subscriptions) + }, + [imageSubscriptions, propertySubscriptions, subscriptions], + ) + + return ( + + + + + + {properties.children} + + + ) +}) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 7c3b2b26..18e721c1 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -14,3 +14,4 @@ export { export { DefaultProperties } from './default.js' export * from './container.js' export * from './root.js' +export * from './image.js' diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx index 433ccbee..12ffe597 100644 --- a/packages/react/src/root.tsx +++ b/packages/react/src/root.tsx @@ -41,8 +41,8 @@ export const Root: ( () => createRootPropertyTransformers(rootSize, hoveredSignal, activeSignal), [rootSize, hoveredSignal, activeSignal], ) - const propertiesSubscriptions = useMemo(() => [], []) - unsubscribeSubscriptions(propertiesSubscriptions) + const propertySubscriptions = useMemo(() => [], []) + unsubscribeSubscriptions(propertySubscriptions) const handlers = updateRootProperties( propertiesSignal, properties, @@ -50,7 +50,7 @@ export const Root: ( hoveredSignal, activeSignal, tranformers, - propertiesSubscriptions, + propertySubscriptions, ) const listeners = useMemo(() => createListeners(), []) @@ -79,10 +79,10 @@ export const Root: ( ) useEffect( () => () => { - unsubscribeSubscriptions(propertiesSubscriptions) + unsubscribeSubscriptions(propertySubscriptions) unsubscribeSubscriptions(subscriptions) }, - [propertiesSubscriptions, subscriptions], + [propertySubscriptions, subscriptions], ) useFrame((_, delta) => { diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 82e6f62d..e8741751 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -17,11 +17,17 @@ import { createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, ZIndexOffset, computeOrderInfo } from '../order.js' import { preferredColorSchemePropertyTransformers } from '../dark.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { Signal } from '@preact/signals-core' +import { Signal, signal } from '@preact/signals-core' import { WithConditionals, computeGlobalMatrix } from './utils.js' import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' -import { MergedProperties, PropertyTransformers } from '../properties/merged.js' -import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' +import { MergedProperties } from '../properties/merged.js' +import { + Listeners, + createListeners, + setupLayoutListeners, + setupViewportListeners, + updateListeners, +} from '../listeners.js' import { Object3DRef, WithContext } from '../context.js' import { ShadowProperties, computePanelGroupDependencies } from '../panel/instanced-panel-group.js' import { cloneHandlers } from '../panel/instanced-panel-mesh.js' @@ -47,14 +53,33 @@ export type InheritableContainerProperties = WithConditionals< export type ContainerProperties = InheritableContainerProperties & Listeners & EventHandlers -export function createContainer( - propertiesSignal: Signal, +export type ContainerState = ReturnType + +export function createContainerState(rootSize: Signal) { + const hoveredSignal = signal>([]) + const activeSignal = signal>([]) + return { + scrollHandlers: signal({}), + propertiesSignal: signal(undefined as any), + subscriptions: [] as Subscriptions, + propertySubscriptions: [] as Subscriptions, + listeners: createListeners(), + hoveredSignal, + activeSignal, + propertyTransformers: { + ...preferredColorSchemePropertyTransformers, + ...createResponsivePropertyTransformers(rootSize), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + }, + } +} + +export function createContainerContext( + { subscriptions, propertiesSignal, listeners, scrollHandlers }: ContainerState, object: Object3DRef, childrenContainer: Object3DRef, parent: WithContext, - scrollHandlers: Signal, - listeners: Listeners, - subscriptions: Subscriptions, ): WithContext { const node = parent.node.createChild(propertiesSignal, object, subscriptions) parent.node.addChild(node) @@ -138,37 +163,36 @@ export function createContainer( } } -export function createContainerPropertyTransfomers( - rootSize: Signal, - hoveredSignal: Signal>, - activeSignal: Signal>, -): PropertyTransformers { - return { - ...preferredColorSchemePropertyTransformers, - ...createResponsivePropertyTransformers(rootSize), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - } -} - export function updateContainerProperties( - propertiesSignal: Signal, + { + activeSignal, + hoveredSignal, + propertiesSignal, + propertySubscriptions, + propertyTransformers, + listeners, + }: ContainerState, properties: Properties, defaultProperties: AllOptionalProperties | undefined, - hoveredSignal: Signal>, - activeSignal: Signal>, - transformers: PropertyTransformers, - propertiesSubscriptions: Subscriptions, ) { //build merged properties - const merged = new MergedProperties(transformers) + const merged = new MergedProperties(propertyTransformers) merged.addAll(defaultProperties, properties) propertiesSignal.value = merged //build handlers const handlers = cloneHandlers(properties) - unsubscribeSubscriptions(propertiesSubscriptions) - addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal, propertiesSubscriptions) + unsubscribeSubscriptions(propertySubscriptions) + addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal, propertySubscriptions) addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + + //update listeners + updateListeners(listeners, properties) + return handlers } + +export function cleanContainerState(state: ContainerState) { + unsubscribeSubscriptions(state.propertySubscriptions) + unsubscribeSubscriptions(state.subscriptions) +} diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index afd56318..29d710f0 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -156,10 +156,14 @@ export function createImagePropertyTransformers( rootSize: Signal, hoveredSignal: Signal>, activeSignal: Signal>, - textureAspectRatio: Signal, ): PropertyTransformers { return { - keepAspectRatio: (value, target) => target.add('aspectRatio', value === false ? undefined : textureAspectRatio), + keepAspectRatio: (value, target) => { + if (value !== false) { + return + } + target.remove('aspectRatio') + }, ...preferredColorSchemePropertyTransformers, ...createResponsivePropertyTransformers(rootSize), ...createHoverPropertyTransformers(hoveredSignal), diff --git a/packages/uikit/src/context.ts b/packages/uikit/src/context.ts index 66ea6f73..b539338d 100644 --- a/packages/uikit/src/context.ts +++ b/packages/uikit/src/context.ts @@ -5,6 +5,11 @@ import { ClippingRect } from './clipping' import { OrderInfo, WithCameraDistance } from './order' import { GlyphGroupManager } from './text/render/instanced-glyph-group' import { PanelGroupManager } from './panel/instanced-panel-group' +import { EventHandlers } from './events' + +export type BindEventHandlers = (object: Object3D, handlers: EventHandlers) => void + +export type UnbindEventHandlers = (object: Object3D, handlers: EventHandlers) => void export type WithContext = ElementContext & Readonly<{ root: RootContext }> @@ -16,6 +21,8 @@ export type RootContext = WithCameraDistance & panelGroupManager: PanelGroupManager pixelSize: number onFrameSet: Set<(delta: number) => void> + bindEventHandlers: BindEventHandlers + unbindEventHandlers: UnbindEventHandlers }> & ElementContext diff --git a/packages/uikit/src/panel/instanced-panel-mesh.ts b/packages/uikit/src/panel/instanced-panel-mesh.ts index fd2e7885..20e3a28e 100644 --- a/packages/uikit/src/panel/instanced-panel-mesh.ts +++ b/packages/uikit/src/panel/instanced-panel-mesh.ts @@ -1,29 +1,31 @@ -import { Box3, InstancedBufferAttribute, Mesh, Object3DEventMap, Sphere, Vector2Tuple } from 'three' +import { Box3, InstancedBufferAttribute, Mesh, Object3DEventMap, Sphere } from 'three' import { createPanelGeometry, panelGeometry } from './utils.js' import { instancedPanelDepthMaterial, instancedPanelDistanceMaterial } from './panel-material.js' -import { Signal, effect } from '@preact/signals-core' -import { ClippingRect } from '../clipping.js' -import { OrderInfo } from '../order.js' +import { effect } from '@preact/signals-core' import { Subscriptions } from '../utils.js' import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' -import { Object3DRef } from '../context.js' +import { WithContext } from '../context.js' import { EventHandlers, ThreeEvent } from '../events.js' export function createInteractionPanel( - size: Signal, - pixelSize: number, - orderInfo: Signal, - parentClippingRect: Signal | undefined, - rootObject: Object3DRef, + context: WithContext, + parentContext: WithContext, subscriptions: Subscriptions, ): Mesh { const panel = new Mesh(panelGeometry) panel.matrixAutoUpdate = false - panel.raycast = makeClippedRaycast(panel, makePanelRaycast(panel), rootObject, parentClippingRect, orderInfo) + panel.raycast = makeClippedRaycast( + panel, + makePanelRaycast(panel), + context.root.object, + parentContext.clippingRect, + context.orderInfo, + ) panel.visible = false subscriptions.push( effect(() => { - const [width, height] = size.value + const [width, height] = context.node.size.value + const pixelSize = context.root.pixelSize panel.scale.set(width * pixelSize, height * pixelSize, 1) panel.updateMatrix() }), diff --git a/packages/uikit/src/properties/merged.ts b/packages/uikit/src/properties/merged.ts index 1966936c..c73b0112 100644 --- a/packages/uikit/src/properties/merged.ts +++ b/packages/uikit/src/properties/merged.ts @@ -9,6 +9,10 @@ export class MergedProperties { constructor(private transformers: PropertyTransformers) {} + remove(key: string) { + this.propertyMap.delete(key) + } + add(key: string, value: unknown) { if (value === undefined) { //only adding non undefined values to the properties diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts index 484cbe6a..688a417f 100644 --- a/packages/uikit/src/vanilla/container.ts +++ b/packages/uikit/src/vanilla/container.ts @@ -2,83 +2,62 @@ import { Object3D } from 'three' import { WithContext } from '../context' import { ContainerProperties, - createContainer, - createContainerPropertyTransfomers, + ContainerState, + cleanContainerState, + createContainerContext, + createContainerState, updateContainerProperties, } from '../components/container' -import { createListeners, updateListeners } from '../listeners' -import { Signal, effect, signal } from '@preact/signals-core' -import { MergedProperties, PropertyTransformers } from '../properties/merged' -import { Component, BindEventHandlers } from './utils' -import { Subscriptions, unsubscribeSubscriptions } from '../utils' +import { effect } from '@preact/signals-core' import { AllOptionalProperties } from '../properties/default' import { createInteractionPanel } from '../panel/instanced-panel-mesh' import { EventHandlers } from '../events' +import { Component } from '.' export class Container extends Object3D { - private propertiesSignal: Signal - private container: Object3D - private subscriptions: Subscriptions = [] - private propertySubscriptions: Subscriptions = [] - private listeners = createListeners() - private propertyTransformers: PropertyTransformers - private hoveredSignal = signal>([]) - private activeSignal = signal>([]) - - public readonly bindEventHandlers: BindEventHandlers public readonly ctx: WithContext + private state: ContainerState + private container: Object3D + private prevHandlers?: EventHandlers constructor(parent: Component, properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { super() - const scrollHandlers = signal({}) - const rootSize = parent.ctx.root.node.size - this.propertyTransformers = createContainerPropertyTransfomers(rootSize, this.hoveredSignal, this.activeSignal) - this.bindEventHandlers = parent.bindEventHandlers + + //setting up the threejs elements this.container = new Object3D() this.container.matrixAutoUpdate = false this.container.add(this) this.matrixAutoUpdate = false parent.add(this.container) - this.propertiesSignal = signal(undefined as any) + + //setting up the container + this.state = createContainerState(parent.ctx.root.node.size) this.setProperties(properties, defaultProperties) - this.ctx = createContainer( - this.propertiesSignal, - { current: this.container }, - { current: this }, - parent.ctx, - scrollHandlers, - this.listeners, - this.subscriptions, - ) - const interactionPanel = createInteractionPanel( - this.ctx.node.size, - this.ctx.root.pixelSize, - this.ctx.orderInfo, - parent.ctx.clippingRect, - this.ctx.root.object, - this.subscriptions, - ) + this.ctx = createContainerContext(this.state, { current: this.container }, { current: this }, parent.ctx) + + //setup scrolling & events + const interactionPanel = createInteractionPanel(this.ctx, parent.ctx, this.state.subscriptions) this.container.add(interactionPanel) - this.subscriptions.push(effect(() => this.bindEventHandlers(interactionPanel, scrollHandlers.value))) + this.state.subscriptions.push( + effect(() => { + const scrollHandlers = this.state.scrollHandlers.value + this.ctx.root.bindEventHandlers(interactionPanel, scrollHandlers) + return () => this.ctx.root.unbindEventHandlers(interactionPanel, scrollHandlers) + }), + ) } setProperties(properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { - const handlers = updateContainerProperties( - this.propertiesSignal, - properties, - defaultProperties, - this.hoveredSignal, - this.activeSignal, - this.propertyTransformers, - this.propertySubscriptions, - ) - this.bindEventHandlers(this.container, handlers) - updateListeners(this.listeners, properties) + if (this.prevHandlers != null) { + this.ctx.root.unbindEventHandlers(this.container, this.prevHandlers) + } + const handlers = updateContainerProperties(this.state, properties, defaultProperties) + this.ctx.root.bindEventHandlers(this.container, handlers) + this.prevHandlers = handlers } destroy() { this.container.parent?.remove(this.container) - unsubscribeSubscriptions(this.propertySubscriptions) - unsubscribeSubscriptions(this.subscriptions) + cleanContainerState(this.state) } } diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index 85516b16..78f3f304 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -40,12 +40,7 @@ export class Image extends Object3D { this.textureAspectRatio = computeTextureAspectRatio(this.texture) const scrollHandlers = signal({}) const rootSize = parent.ctx.root.node.size - this.propertyTransformers = createImagePropertyTransformers( - rootSize, - this.hoveredSignal, - this.activeSignal, - this.textureAspectRatio, - ) + this.propertyTransformers = createImagePropertyTransformers(rootSize, this.hoveredSignal, this.activeSignal) this.bindEventHandlers = parent.bindEventHandlers this.container = new Object3D() this.container.matrixAutoUpdate = false diff --git a/packages/uikit/src/vanilla/index.ts b/packages/uikit/src/vanilla/index.ts index b55e9e23..799b71d3 100644 --- a/packages/uikit/src/vanilla/index.ts +++ b/packages/uikit/src/vanilla/index.ts @@ -1,3 +1,9 @@ +import type { Container } from './container' +import type { Root } from './root' +import type { Image } from './image' + +export type Component = Container | Root | Image + export * from './container' export * from './root' export * from './image' diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index d7575cfa..491eb2c4 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -3,7 +3,6 @@ import { WithContext } from '../context' import { createListeners, updateListeners } from '../listeners' import { Signal, effect, signal } from '@preact/signals-core' import { MergedProperties, PropertyTransformers } from '../properties/merged' -import { BindEventHandlers } from './utils' import { Subscriptions, unsubscribeSubscriptions } from '../utils' import { AllOptionalProperties } from '../properties/default' import { createInteractionPanel } from '../panel/instanced-panel-mesh' @@ -29,7 +28,6 @@ export class Root extends Object3D { object: Object3D, properties: RootProperties, defaultProperties?: AllOptionalProperties, - public readonly bindEventHandlers: BindEventHandlers = () => {}, ) { super() const rootSize = signal([0, 0]) diff --git a/packages/uikit/src/vanilla/utils.ts b/packages/uikit/src/vanilla/utils.ts deleted file mode 100644 index dc944e82..00000000 --- a/packages/uikit/src/vanilla/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Object3D } from 'three' -import { Container } from './container' -import { Root } from './root' -import { Image } from './image' -import { EventHandlers } from '../events' - -export type Component = Container | Root | Image - -export type BindEventHandlers = (object: Object3D, handlers: EventHandlers) => void diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0acdbb5e..8bbf29af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -339,9 +339,9 @@ importers: examples/vanilla: dependencies: - '@react-three/uikit': + '@vanilla-three/uikit': specifier: workspace:^ - version: link:../../packages/react + version: link:../../packages/uikit react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) From 90d871a49bbf849b55c6cf8ccd2854626b1e9dd7 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Mon, 25 Mar 2024 21:18:07 +0100 Subject: [PATCH 05/20] move vanilla and react closer together --- examples/uikit/src/App.tsx | 4 +- examples/vanilla/index.ts | 34 ++-- packages/react/package.json | 2 +- packages/react/src/container.tsx | 30 +-- packages/react/src/image.tsx | 85 ++------- packages/react/src/responsive.ts | 5 +- packages/react/src/root.tsx | 91 ++------- packages/react/src/utilts.tsx | 6 +- packages/uikit/src/active.ts | 2 + packages/uikit/src/components/container.ts | 166 ++++++++-------- packages/uikit/src/components/image.ts | 177 +++++++++--------- packages/uikit/src/components/root.ts | 113 ++++++----- packages/uikit/src/context.ts | 7 - packages/uikit/src/hover.ts | 9 +- packages/uikit/src/listeners.ts | 21 +-- .../uikit/src/panel/instanced-panel-mesh.ts | 23 +-- packages/uikit/src/scroll.ts | 4 +- packages/uikit/src/vanilla/container.ts | 70 +++---- packages/uikit/src/vanilla/image.ts | 93 +++------ packages/uikit/src/vanilla/index.ts | 2 +- packages/uikit/src/vanilla/root.ts | 97 +++------- packages/uikit/src/vanilla/utils.ts | 38 ++++ 22 files changed, 417 insertions(+), 662 deletions(-) create mode 100644 packages/uikit/src/vanilla/utils.ts diff --git a/examples/uikit/src/App.tsx b/examples/uikit/src/App.tsx index 78bb3ac7..7d9e06f2 100644 --- a/examples/uikit/src/App.tsx +++ b/examples/uikit/src/App.tsx @@ -15,11 +15,11 @@ export default function App() { diff --git a/examples/vanilla/index.ts b/examples/vanilla/index.ts index 7d220252..d5f6b643 100644 --- a/examples/vanilla/index.ts +++ b/examples/vanilla/index.ts @@ -1,4 +1,4 @@ -import { BoxGeometry, Mesh, MeshNormalMaterial, PerspectiveCamera, Scene, WebGLRenderer } from 'three' +import { PerspectiveCamera, Scene, WebGLRenderer } from 'three' import { patchRenderOrder, Container, Root, Image } from '@vanilla-three/uikit' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' @@ -13,14 +13,22 @@ const canvas = document.getElementById('root') as HTMLCanvasElement const controls = new OrbitControls(camera, canvas) //UI -const root = new Root(camera, scene, { - flexDirection: 'row', - gap: 10, - padding: 10, - sizeX: 1, - sizeY: 0.5, - backgroundColor: 'red', -}) +const root = new Root( + { + bindEventHandlers(object, handlers) {}, + unbindEventHandlers(object, handlers) {}, + }, + camera, + scene, + { + flexDirection: 'row', + gap: 10, + padding: 10, + sizeX: 1, + sizeY: 0.5, + backgroundColor: 'red', + }, +) new Container(root, { flexGrow: 1, backgroundColor: 'blue' }) const x = new Container(root, { padding: 30, flexGrow: 1, backgroundColor: 'green' }) new Image(x, { @@ -32,12 +40,6 @@ new Image(x, { src: 'https://picsum.photos/300/300', }) -const geometry = new BoxGeometry(0.2, 0.2, 0.2) -const material = new MeshNormalMaterial() - -const mesh = new Mesh(geometry, material) -scene.add(mesh) - const renderer = new WebGLRenderer({ antialias: true, canvas }) renderer.setAnimationLoop(animation) renderer.localClippingEnabled = true @@ -58,8 +60,6 @@ let prev: number | undefined function animation(time: number) { const delta = prev == null ? 0 : time - prev prev = time - mesh.rotation.x = time / 2000 - mesh.rotation.y = time / 1000 root.update(delta) controls.update(delta) diff --git a/packages/react/package.json b/packages/react/package.json index a0bf0ad4..12d80173 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -30,7 +30,7 @@ "build": "tsc", "check:prettier": "prettier --check src scripts tests", "check:eslint": "eslint 'src/**/*.{tsx,ts}'", - "fix:prettier": "prettier --write src scripts tests", + "fix:prettier": "prettier --write src", "fix:eslint": "eslint 'src/**/*.{tsx,ts}' --fix" }, "peerDependencies": { diff --git a/packages/react/src/container.tsx b/packages/react/src/container.tsx index d917994e..1f7a41d6 100644 --- a/packages/react/src/container.tsx +++ b/packages/react/src/container.tsx @@ -3,14 +3,7 @@ import { forwardRef, ReactNode, useEffect, useMemo, useRef } from 'react' import { Object3D } from 'three' import { ParentProvider, useParent } from './context' import { AddHandlers, AddScrollHandler } from './utilts' -import { - createInteractionPanel, - updateContainerProperties, - ContainerProperties, - createContainerState, - cleanContainerState, - createContainerContext, -} from '@vanilla-three/uikit/internals' +import { ContainerProperties, createContainer, destroyContainer } from '@vanilla-three/uikit/internals' import { useDefaultProperties } from './default' export const Container: ( @@ -21,27 +14,22 @@ export const Container: ( ) => ReactNode = forwardRef((properties, ref) => { //TODO: ComponentInternals const parent = useParent() - const state = useMemo(() => createContainerState(parent.root.node.size), [parent]) - useEffect(() => () => cleanContainerState(state), [state]) - - const defaultProperties = useDefaultProperties() - const handlers = updateContainerProperties(state, properties, defaultProperties) - const outerRef = useRef(null) const innerRef = useRef(null) - const ctx = useMemo(() => createContainerContext(state, outerRef, innerRef, parent), [parent, state]) + const defaultProperties = useDefaultProperties() + // eslint-disable-next-line react-hooks/exhaustive-deps + const internals = useMemo(() => createContainer(parent, properties, defaultProperties, outerRef, innerRef), [parent]) + useEffect(() => () => destroyContainer(internals), [internals]) //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) - const interactionPanel = useMemo(() => createInteractionPanel(ctx, parent, state.subscriptions), [ctx, parent, state]) - return ( - - - + + + - {properties.children} + {properties.children} ) diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx index f8af71fa..c2835cb9 100644 --- a/packages/react/src/image.tsx +++ b/packages/react/src/image.tsx @@ -1,90 +1,29 @@ -import { signal } from '@preact/signals-core' -import { - createListeners, - computeTextureAspectRatio, - EventHandlers, - createImagePropertyTransformers, - createImage, - createImageMesh, - loadImageTexture, - updateImageProperties, - updateListeners, - ImageProperties, - Subscriptions, - unsubscribeSubscriptions, -} from '@vanilla-three/uikit/internals' +import { createImage, ImageProperties, destroyImage } from '@vanilla-three/uikit/internals' import { ReactNode, forwardRef, useEffect, useMemo, useRef } from 'react' -import { Object3D, Texture } from 'three' +import { Object3D } from 'three' import { AddHandlers, AddScrollHandler } from './utilts' import { ParentProvider, useParent } from './context' import { useDefaultProperties } from './default' export const Image: (props: ImageProperties & { children?: ReactNode }) => ReactNode = forwardRef((properties, ref) => { - const texture = useMemo(() => signal(undefined), []) - const imageSubscriptions = useMemo(() => [], []) - const srcRef = useRef(undefined) - if (properties.src != srcRef.current) { - unsubscribeSubscriptions(imageSubscriptions) - loadImageTexture(texture, properties.src, imageSubscriptions) - srcRef.current = properties.src - } - - const scrollHandlers = useMemo(() => signal({}), []) + //TODO: ComponentInternals const parent = useParent() - const hoveredSignal = useMemo(() => signal>([]), []) - const activeSignal = useMemo(() => signal>([]), []) - const tranformers = useMemo( - () => createImagePropertyTransformers(parent.root.node.size, hoveredSignal, activeSignal), - [activeSignal, hoveredSignal, parent], - ) - const propertiesSignal = useMemo(() => signal(undefined as any), []) - const defaultProperties = useDefaultProperties() - const propertySubscriptions = useMemo(() => [], []) - const textureAspectRatio = useMemo(() => computeTextureAspectRatio(texture), [texture]) - unsubscribeSubscriptions(propertySubscriptions) - const handlers = updateImageProperties( - propertiesSignal, - textureAspectRatio, - properties, - defaultProperties, - hoveredSignal, - activeSignal, - tranformers, - propertySubscriptions, - ) - - const listeners = useMemo(() => createListeners(), []) - updateListeners(listeners, properties) - const outerRef = useRef(null) const innerRef = useRef(null) - const subscriptions = useMemo(() => [], []) - const ctx = useMemo( - () => createImage(propertiesSignal, outerRef, innerRef, parent, scrollHandlers, listeners, subscriptions), - [listeners, parent, propertiesSignal, scrollHandlers, subscriptions], - ) - - const mesh = useMemo( - () => createImageMesh(propertiesSignal, texture, parent, ctx, subscriptions), - [ctx, parent, propertiesSignal, subscriptions, texture], - ) + const defaultProperties = useDefaultProperties() + // eslint-disable-next-line react-hooks/exhaustive-deps + const internals = useMemo(() => createImage(parent, properties, defaultProperties, outerRef, innerRef), [parent]) + useEffect(() => () => destroyImage(internals), [internals]) - useEffect( - () => () => { - unsubscribeSubscriptions(imageSubscriptions) - unsubscribeSubscriptions(propertySubscriptions) - unsubscribeSubscriptions(subscriptions) - }, - [imageSubscriptions, propertySubscriptions, subscriptions], - ) + //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) return ( - - - + + + - {properties.children} + {properties.children} ) diff --git a/packages/react/src/responsive.ts b/packages/react/src/responsive.ts index 493e6839..8e771c3d 100644 --- a/packages/react/src/responsive.ts +++ b/packages/react/src/responsive.ts @@ -1,4 +1 @@ - -export function useRootSize() { - -} \ No newline at end of file +export function useRootSize() {} diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx index 12ffe597..fd025e8e 100644 --- a/packages/react/src/root.tsx +++ b/packages/react/src/root.tsx @@ -3,23 +3,9 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/eve import { forwardRef, ReactNode, useEffect, useMemo, useRef } from 'react' import { ParentProvider } from './context' import { AddHandlers, AddScrollHandler } from './utilts' -import { - MergedProperties, - RootProperties, - Subscriptions, - createInteractionPanel, - createListeners, - patchRenderOrder, - unsubscribeSubscriptions, - updateListeners, - createRoot, - updateRootProperties, - createRootPropertyTransformers, - EventHandlers as CoreEventHandlers, -} from '@vanilla-three/uikit/internals' -import { signal } from '@preact/signals-core' -import { Object3D, Vector2Tuple } from 'three' +import { RootProperties, patchRenderOrder, createRoot, destroyRoot } from '@vanilla-three/uikit/internals' import { useDefaultProperties } from './default' +import { Object3D } from 'three' export const Root: ( props: RootProperties & { @@ -29,83 +15,32 @@ export const Root: ( const renderer = useThree((state) => state.gl) useEffect(() => patchRenderOrder(renderer), [renderer]) - + const store = useStore() const outerRef = useRef(null) const innerRef = useRef(null) const defaultProperties = useDefaultProperties() - const propertiesSignal = useMemo(() => signal(undefined as any), []) - const rootSize = useMemo(() => signal([0, 0]), []) - const hoveredSignal = useMemo(() => signal>([]), []) - const activeSignal = useMemo(() => signal>([]), []) - const tranformers = useMemo( - () => createRootPropertyTransformers(rootSize, hoveredSignal, activeSignal), - [rootSize, hoveredSignal, activeSignal], - ) - const propertySubscriptions = useMemo(() => [], []) - unsubscribeSubscriptions(propertySubscriptions) - const handlers = updateRootProperties( - propertiesSignal, - properties, - defaultProperties, - hoveredSignal, - activeSignal, - tranformers, - propertySubscriptions, - ) - - const listeners = useMemo(() => createListeners(), []) - updateListeners(listeners, properties) - - const scrollHandlers = useMemo(() => signal({}), []) - - const subscriptions = useMemo(() => [], []) - const onFrameSet = useMemo(() => new Set<(delta: number) => void>(), []) - const store = useStore() - const ctx = useMemo( - () => - createRoot( - propertiesSignal, - rootSize, - outerRef, - innerRef, - scrollHandlers, - listeners, - properties.pixelSize, - onFrameSet, - () => store.getState().camera, - subscriptions, - ), - [listeners, onFrameSet, properties.pixelSize, propertiesSignal, rootSize, scrollHandlers, store, subscriptions], - ) - useEffect( - () => () => { - unsubscribeSubscriptions(propertySubscriptions) - unsubscribeSubscriptions(subscriptions) - }, - [propertySubscriptions, subscriptions], + const internals = useMemo( + () => createRoot(properties, defaultProperties, outerRef, innerRef, () => store.getState().camera), + // eslint-disable-next-line react-hooks/exhaustive-deps + [store], ) + useEffect(() => () => destroyRoot(internals), [internals]) useFrame((_, delta) => { - for (const onFrame of onFrameSet) { + for (const onFrame of internals.onFrameSet) { onFrame(delta) } }) //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) - const interactionPanel = useMemo( - () => - createInteractionPanel(ctx.node.size, ctx.pixelSize, ctx.orderInfo, undefined, ctx.root.object, subscriptions), - [ctx, subscriptions], - ) - return ( - - - + + + - {properties.children} + {properties.children} ) diff --git a/packages/react/src/utilts.tsx b/packages/react/src/utilts.tsx index 98863d71..24389fab 100644 --- a/packages/react/src/utilts.tsx +++ b/packages/react/src/utilts.tsx @@ -3,8 +3,10 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/eve import { ReactNode, forwardRef, useEffect, useMemo, useState } from 'react' import { Object3D } from 'three' -export const AddHandlers = forwardRef( - ({ handlers, children }, ref) => { +export const AddHandlers = forwardRef; children?: ReactNode }>( + ({ handlers: handlersSignal, children }, ref) => { + const [handlers, setHandlers] = useState(() => handlersSignal.value) + useSignalEffect(() => setHandlers(handlersSignal.value), [handlersSignal]) return ( {children} diff --git a/packages/uikit/src/active.ts b/packages/uikit/src/active.ts index 4be27270..954c44e0 100644 --- a/packages/uikit/src/active.ts +++ b/packages/uikit/src/active.ts @@ -48,6 +48,8 @@ export function addActiveHandlers( addHandler('onPointerLeave', target, onLeave) } +//TODO: this does not work because active: { ... } should always overwrite even properties after + export function createActivePropertyTransfomers(activeSignal: Signal>) { return { active: createConditionalPropertyTranslator(() => activeSignal.value.length > 0), diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index e8741751..fe266faf 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -1,5 +1,5 @@ import { YogaProperties } from '../flex/node.js' -import { addHoverHandlers, createHoverPropertyTransformers } from '../hover.js' +import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { computeIsClipped, computeClippingRect } from '../clipping.js' import { ScrollbarProperties, @@ -17,20 +17,14 @@ import { createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, ZIndexOffset, computeOrderInfo } from '../order.js' import { preferredColorSchemePropertyTransformers } from '../dark.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { Signal, signal } from '@preact/signals-core' +import { Signal, computed, signal } from '@preact/signals-core' import { WithConditionals, computeGlobalMatrix } from './utils.js' import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' import { MergedProperties } from '../properties/merged.js' -import { - Listeners, - createListeners, - setupLayoutListeners, - setupViewportListeners, - updateListeners, -} from '../listeners.js' +import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' import { Object3DRef, WithContext } from '../context.js' import { ShadowProperties, computePanelGroupDependencies } from '../panel/instanced-panel-group.js' -import { cloneHandlers } from '../panel/instanced-panel-mesh.js' +import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { MaterialClass } from '../panel/panel-material.js' import { Vector2Tuple } from 'three' import { EventHandlers } from '../events.js' @@ -53,73 +47,74 @@ export type InheritableContainerProperties = WithConditionals< export type ContainerProperties = InheritableContainerProperties & Listeners & EventHandlers -export type ContainerState = ReturnType - -export function createContainerState(rootSize: Signal) { +export function createContainer( + parentContext: WithContext, + properties: ContainerProperties, + defaultProperties: AllOptionalProperties | undefined, + object: Object3DRef, + childrenContainer: Object3DRef, +) { const hoveredSignal = signal>([]) const activeSignal = signal>([]) - return { - scrollHandlers: signal({}), - propertiesSignal: signal(undefined as any), - subscriptions: [] as Subscriptions, - propertySubscriptions: [] as Subscriptions, - listeners: createListeners(), - hoveredSignal, - activeSignal, - propertyTransformers: { - ...preferredColorSchemePropertyTransformers, - ...createResponsivePropertyTransformers(rootSize), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - }, + const subscriptions = [] as Subscriptions + setupCursorCleanup(hoveredSignal, subscriptions) + + const propertyTransformers = { + ...preferredColorSchemePropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), } -} -export function createContainerContext( - { subscriptions, propertiesSignal, listeners, scrollHandlers }: ContainerState, - object: Object3DRef, - childrenContainer: Object3DRef, - parent: WithContext, -): WithContext { - const node = parent.node.createChild(propertiesSignal, object, subscriptions) - parent.node.addChild(node) + const scrollHandlers = signal({}) + const propertiesSignal = signal(properties) + const defaultPropertiesSignal = signal(defaultProperties) - const transformMatrix = computeTransformMatrix(propertiesSignal, node, parent.root.pixelSize) + const mergedProperties = computed(() => { + const merged = new MergedProperties(propertyTransformers) + merged.addAll(defaultPropertiesSignal.value, propertiesSignal.value) + return merged + }) + + const node = parentContext.node.createChild(mergedProperties, object, subscriptions) + parentContext.node.addChild(node) + + const transformMatrix = computeTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) applyTransform(object, transformMatrix, subscriptions) - const globalMatrix = computeGlobalMatrix(parent.matrix, transformMatrix) + const globalMatrix = computeGlobalMatrix(parentContext.matrix, transformMatrix) - const isClipped = computeIsClipped(parent.clippingRect, globalMatrix, node.size, parent.root.pixelSize) - const groupDeps = computePanelGroupDependencies(propertiesSignal) + const isClipped = computeIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) + const groupDeps = computePanelGroupDependencies(mergedProperties) - const orderInfo = computeOrderInfo(propertiesSignal, ElementType.Panel, groupDeps, parent.orderInfo) + const orderInfo = computeOrderInfo(mergedProperties, ElementType.Panel, groupDeps, parentContext.orderInfo) createInstancedPanel( - propertiesSignal, + mergedProperties, orderInfo, groupDeps, - parent.root.panelGroupManager, + parentContext.root.panelGroupManager, globalMatrix, node.size, undefined, node.borderInset, - parent.clippingRect, + parentContext.clippingRect, isClipped, subscriptions, ) const scrollPosition = createScrollPosition() - applyScrollPosition(childrenContainer, scrollPosition, parent.root.pixelSize) - const matrix = computeGlobalScrollMatrix(scrollPosition, globalMatrix, parent.root.pixelSize) + applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) + const matrix = computeGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) createScrollbars( - propertiesSignal, + mergedProperties, scrollPosition, node, globalMatrix, isClipped, - parent.clippingRect, + parentContext.clippingRect, orderInfo, - parent.root.panelGroupManager, + parentContext.root.panelGroupManager, subscriptions, ) @@ -128,71 +123,60 @@ export function createContainerContext( node.size, node.borderInset, node.overflow, - parent.root.pixelSize, - parent.clippingRect, + parentContext.root.pixelSize, + parentContext.clippingRect, ) - setupLayoutListeners(listeners, node.size, subscriptions) - setupViewportListeners(listeners, isClipped, subscriptions) + setupLayoutListeners(propertiesSignal, node.size, subscriptions) + setupViewportListeners(propertiesSignal, isClipped, subscriptions) const onScrollFrame = setupScrollHandler( node, scrollPosition, object, - listeners, - parent.root.pixelSize, + propertiesSignal, + parentContext.root.pixelSize, scrollHandlers, subscriptions, ) - parent.root.onFrameSet.add(onScrollFrame) + parentContext.root.onFrameSet.add(onScrollFrame) subscriptions.push(() => { - parent.root.onFrameSet.delete(onScrollFrame) - parent.node.removeChild(node) + parentContext.root.onFrameSet.delete(onScrollFrame) + parentContext.node.removeChild(node) node.destroy() }) return { + scrollHandlers, isClipped, clippingRect, matrix, node, object, orderInfo, - root: parent.root, - } -} - -export function updateContainerProperties( - { - activeSignal, - hoveredSignal, + root: parentContext.root, propertiesSignal, - propertySubscriptions, - propertyTransformers, - listeners, - }: ContainerState, - properties: Properties, - defaultProperties: AllOptionalProperties | undefined, -) { - //build merged properties - const merged = new MergedProperties(propertyTransformers) - merged.addAll(defaultProperties, properties) - propertiesSignal.value = merged - - //build handlers - const handlers = cloneHandlers(properties) - unsubscribeSubscriptions(propertySubscriptions) - addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal, propertySubscriptions) - addActiveHandlers(handlers, properties, defaultProperties, activeSignal) - - //update listeners - updateListeners(listeners, properties) - - return handlers + defaultPropertiesSignal, + interactionPanel: createInteractionPanel( + node, + orderInfo, + parentContext.root, + parentContext.clippingRect, + subscriptions, + ), + handlers: computed(() => { + const properties = propertiesSignal.value + const defaultProperties = defaultPropertiesSignal.value + const handlers = cloneHandlers(properties) + addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal) + addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + return handlers + }), + subscriptions, + } } -export function cleanContainerState(state: ContainerState) { - unsubscribeSubscriptions(state.propertySubscriptions) - unsubscribeSubscriptions(state.subscriptions) +export function destroyContainer(container: ReturnType) { + unsubscribeSubscriptions(container.subscriptions) } diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index 29d710f0..e3418d1c 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -1,4 +1,4 @@ -import { Signal, computed, effect } from '@preact/signals-core' +import { Signal, computed, effect, signal } from '@preact/signals-core' import { Mesh, MeshBasicMaterial, PlaneGeometry, SRGBColorSpace, Texture, TextureLoader, Vector2Tuple } from 'three' import { Listeners } from '..' import { Object3DRef, WithContext } from '../context' @@ -20,7 +20,7 @@ import { import { TransformProperties, applyTransform, computeTransformMatrix } from '../transform' import { WithConditionals, computeGlobalMatrix, loadResourceWithParams } from './utils' import { MergedProperties, PropertyTransformers } from '../properties/merged' -import { Subscriptions, unsubscribeSubscriptions } from '../utils' +import { Subscriptions, readReactive, unsubscribeSubscriptions } from '../utils' import { computeIsPanelVisible, panelGeometry } from '../panel/utils' import { setupImmediateProperties } from '../properties/immediate' import { makeClippedRaycast, makePanelRaycast } from '../panel/interaction-panel-mesh' @@ -33,7 +33,7 @@ import { import { setupLayoutListeners, setupViewportListeners } from '../listeners' import { createGetBatchedProperties } from '../properties/batched' import { addActiveHandlers, createActivePropertyTransfomers } from '../active' -import { addHoverHandlers, createHoverPropertyTransformers } from '../hover' +import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover' import { cloneHandlers } from '../panel/instanced-panel-mesh' import { preferredColorSchemePropertyTransformers } from '../dark' import { createResponsivePropertyTransformers } from '../responsive' @@ -66,38 +66,78 @@ export type ImageProperties = InheritableImageProperties & Listeners & EventHand const shadowProperties = ['castShadow', 'receiveShadow'] export function createImage( - propertiesSignal: Signal, + parentContext: WithContext, + properties: ImageProperties, + defaultProperties: AllOptionalProperties | undefined, object: Object3DRef, childrenContainer: Object3DRef, - parent: WithContext, - scrollHandlers: Signal, - listeners: Listeners, - subscriptions: Subscriptions, -): WithContext { - const node = parent.node.createChild(propertiesSignal, object, subscriptions) - parent.node.addChild(node) +) { + const subscriptions: Subscriptions = [] + const texture = signal(undefined) + const hoveredSignal = signal>([]) + const activeSignal = signal>([]) + setupCursorCleanup(hoveredSignal, subscriptions) + const scrollHandlers = signal({}) + const propertiesSignal = signal(properties) + const defaultPropertiesSignal = signal(defaultProperties) + + const src = computed(() => readReactive(propertiesSignal.value.src)) + loadResourceWithParams(texture, loadTextureImpl, subscriptions, src) + + const textureAspectRatio = computed(() => { + const tex = texture.value + if (tex == null) { + return undefined + } + const image = tex.source.data as { width: number; height: number } + return image.width / image.height + }) + + const propertyTransformers: PropertyTransformers = { + keepAspectRatio: (value, target) => { + if (value !== false) { + return + } + target.remove('aspectRatio') + }, + ...preferredColorSchemePropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + } - const transformMatrix = computeTransformMatrix(propertiesSignal, node, parent.root.pixelSize) + const mergedProperties = computed(() => { + const merged = new MergedProperties(propertyTransformers) + merged.add('backgroundColor', 0xffffff) + merged.add('aspectRatio', textureAspectRatio) + merged.addAll(defaultPropertiesSignal.value, propertiesSignal.value) + return merged + }) + + const node = parentContext.node.createChild(mergedProperties, object, subscriptions) + parentContext.node.addChild(node) + + const transformMatrix = computeTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) applyTransform(object, transformMatrix, subscriptions) - const globalMatrix = computeGlobalMatrix(parent.matrix, transformMatrix) + const globalMatrix = computeGlobalMatrix(parentContext.matrix, transformMatrix) - const isClipped = computeIsClipped(parent.clippingRect, globalMatrix, node.size, parent.root.pixelSize) + const isClipped = computeIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) - const orderInfo = computeOrderInfo(propertiesSignal, ElementType.Image, undefined, parent.orderInfo) + const orderInfo = computeOrderInfo(mergedProperties, ElementType.Image, undefined, parentContext.orderInfo) const scrollPosition = createScrollPosition() - applyScrollPosition(childrenContainer, scrollPosition, parent.root.pixelSize) - const matrix = computeGlobalScrollMatrix(scrollPosition, globalMatrix, parent.root.pixelSize) + applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) + const matrix = computeGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) createScrollbars( - propertiesSignal, + mergedProperties, scrollPosition, node, globalMatrix, isClipped, - parent.clippingRect, + parentContext.clippingRect, orderInfo, - parent.root.panelGroupManager, + parentContext.root.panelGroupManager, subscriptions, ) @@ -106,96 +146,59 @@ export function createImage( node.size, node.borderInset, node.overflow, - parent.root.pixelSize, - parent.clippingRect, + parentContext.root.pixelSize, + parentContext.clippingRect, ) - setupLayoutListeners(listeners, node.size, subscriptions) - setupViewportListeners(listeners, isClipped, subscriptions) + setupLayoutListeners(propertiesSignal, node.size, subscriptions) + setupViewportListeners(propertiesSignal, isClipped, subscriptions) const onScrollFrame = setupScrollHandler( node, scrollPosition, object, - listeners, - parent.root.pixelSize, + propertiesSignal, + parentContext.root.pixelSize, scrollHandlers, subscriptions, ) - parent.root.onFrameSet.add(onScrollFrame) + parentContext.root.onFrameSet.add(onScrollFrame) subscriptions.push(() => { - parent.root.onFrameSet.delete(onScrollFrame) - parent.node.removeChild(node) + parentContext.root.onFrameSet.delete(onScrollFrame) + parentContext.node.removeChild(node) node.destroy() }) - return { + const ctx: WithContext = { isClipped, clippingRect, matrix, node, object, orderInfo, - root: parent.root, + root: parentContext.root, } -} - -export function computeTextureAspectRatio(texture: Signal) { - return computed(() => { - const tex = texture.value - if (tex == null) { - return undefined - } - const image = tex.source.data as { width: number; height: number } - return image.width / image.height + return Object.assign(ctx, { + subscriptions, + scrollHandlers, + propertiesSignal, + defaultPropertiesSignal, + handlers: computed(() => { + const handlers = cloneHandlers(properties) + addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal) + addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + return handlers + }), + mesh: createImageMesh(mergedProperties, texture, parentContext, ctx, subscriptions), }) } -export function createImagePropertyTransformers( - rootSize: Signal, - hoveredSignal: Signal>, - activeSignal: Signal>, -): PropertyTransformers { - return { - keepAspectRatio: (value, target) => { - if (value !== false) { - return - } - target.remove('aspectRatio') - }, - ...preferredColorSchemePropertyTransformers, - ...createResponsivePropertyTransformers(rootSize), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - } -} - -export function updateImageProperties( - propertiesSignal: Signal, - textureAspectRatio: Signal, - properties: Properties, - defaultProperties: AllOptionalProperties | undefined, - hoveredSignal: Signal>, - activeSignal: Signal>, - transformers: PropertyTransformers, - subscriptions: Subscriptions, -) { - //build merged properties - const merged = new MergedProperties(transformers) - merged.add('backgroundColor', 0xffffff) - merged.add('aspectRatio', textureAspectRatio) - merged.addAll(defaultProperties, properties) - propertiesSignal.value = merged - - //build handlers - const handlers = cloneHandlers(properties) - addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal, subscriptions) - addActiveHandlers(handlers, properties, defaultProperties, activeSignal) - return handlers +export function destroyImage(internals: ReturnType) { + unsubscribeSubscriptions(internals.subscriptions) } -export function createImageMesh( +function createImageMesh( propertiesSignal: Signal, texture: Signal, parent: WithContext, @@ -303,14 +306,6 @@ function transformInsideBorder(borderInset: Signal, size: Signal, - src: Signal | string, - subscriptions: Subscriptions, -): void { - loadResourceWithParams(target, loadTextureImpl, subscriptions, src) -} - const textureLoader = new TextureLoader() async function loadTextureImpl(src?: string | Texture) { diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index 8af1441d..db874375 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -26,8 +26,8 @@ import { GlyphGroupManager } from '../text/render/instanced-glyph-group' import { createGetBatchedProperties } from '../properties/batched' import { addActiveHandlers, createActivePropertyTransfomers } from '../active' import { preferredColorSchemePropertyTransformers } from '../dark' -import { addHoverHandlers, createHoverPropertyTransformers } from '../hover' -import { cloneHandlers } from '../panel/instanced-panel-mesh' +import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover' +import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh' import { createResponsivePropertyTransformers } from '../responsive' import { EventHandlers } from '../events' @@ -65,22 +65,42 @@ const planeHelper = new Plane() const notClipped = signal(false) export function createRoot( - propertiesSignal: Signal, - rootSize: Signal, + properties: RootProperties, + defaultProperties: AllOptionalProperties | undefined, object: Object3DRef, childrenContainer: Object3DRef, - scrollHandlers: Signal, - listeners: Listeners, - pixelSize: number | undefined, - onFrameSet: Set<(delta: number) => void>, getCamera: () => Camera, - subscriptions: Subscriptions, -): RootContext & WithContext { - pixelSize ??= DEFAULT_PIXEL_SIZE +) { + const rootSize = signal([0, 0]) + const hoveredSignal = signal>([]) + const activeSignal = signal>([]) + const subscriptions = [] as Subscriptions + setupCursorCleanup(hoveredSignal, subscriptions) + const pixelSize = properties.pixelSize ?? DEFAULT_PIXEL_SIZE + + const transformers: PropertyTransformers = { + ...createSizeTranslator(pixelSize, 'sizeX', 'width'), + ...createSizeTranslator(pixelSize, 'sizeY', 'height'), + ...preferredColorSchemePropertyTransformers, + ...createResponsivePropertyTransformers(rootSize), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + } + + const scrollHandlers = signal({}) + const propertiesSignal = signal(properties) + const defaultPropertiesSignal = signal(defaultProperties) + const onFrameSet = new Set<(delta: number) => void>() + + const mergedProperties = computed(() => { + const merged = new MergedProperties(transformers) + merged.addAll(defaultProperties, properties) + return merged + }) const requestCalculateLayout = createDeferredRequestLayoutCalculation(onFrameSet, subscriptions) const node = new FlexNode( - propertiesSignal, + mergedProperties, rootSize, object, loadYoga(), @@ -91,13 +111,13 @@ export function createRoot( ) subscriptions.push(() => node.destroy()) - const transformMatrix = computeTransformMatrix(propertiesSignal, node, pixelSize) - const rootMatrix = computeRootMatrix(propertiesSignal, transformMatrix, node.size, pixelSize) + const transformMatrix = computeTransformMatrix(mergedProperties, node, pixelSize) + const rootMatrix = computeRootMatrix(mergedProperties, transformMatrix, node.size, pixelSize) applyTransform(object, transformMatrix, subscriptions) - const groupDeps = computePanelGroupDependencies(propertiesSignal) + const groupDeps = computePanelGroupDependencies(mergedProperties) - const orderInfo = computeOrderInfo(propertiesSignal, ElementType.Panel, groupDeps, undefined) + const orderInfo = computeOrderInfo(mergedProperties, ElementType.Panel, groupDeps, undefined) const ctx: WithCameraDistance = { cameraDistance: 0 } @@ -120,7 +140,7 @@ export function createRoot( subscriptions.push(() => onFrameSet.delete(onCameraDistanceFrame)) createInstancedPanel( - propertiesSignal, + mergedProperties, orderInfo, groupDeps, panelGroupManager, @@ -137,7 +157,7 @@ export function createRoot( applyScrollPosition(childrenContainer, scrollPosition, pixelSize) const matrix = computeGlobalScrollMatrix(scrollPosition, rootMatrix, pixelSize) createScrollbars( - propertiesSignal, + mergedProperties, scrollPosition, node, rootMatrix, @@ -150,13 +170,13 @@ export function createRoot( const clippingRect = computeClippingRect(rootMatrix, node.size, node.borderInset, node.overflow, pixelSize, undefined) - setupLayoutListeners(listeners, node.size, subscriptions) + setupLayoutListeners(propertiesSignal, node.size, subscriptions) const onScrollFrame = setupScrollHandler( node, scrollPosition, object, - listeners, + propertiesSignal, pixelSize, scrollHandlers, subscriptions, @@ -181,45 +201,24 @@ export function createRoot( pixelSize, }) - return Object.assign(rootCtx, { root: rootCtx }) -} - -export function createRootPropertyTransformers( - rootSize: Signal, - hoveredSignal: Signal>, - activeSignal: Signal>, - pixelSize: number = DEFAULT_PIXEL_SIZE, -): PropertyTransformers { - return { - ...createSizeTranslator(pixelSize, 'sizeX', 'width'), - ...createSizeTranslator(pixelSize, 'sizeY', 'height'), - ...preferredColorSchemePropertyTransformers, - ...createResponsivePropertyTransformers(rootSize), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - } + return Object.assign(rootCtx, { + subscriptions, + propertiesSignal, + defaultPropertiesSignal, + scrollHandlers, + interactionPanel: createInteractionPanel(node, orderInfo, rootCtx, undefined, subscriptions), + handlers: computed(() => { + const handlers = cloneHandlers(properties) + addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal) + addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + return handlers + }), + root: rootCtx, + }) } -export function updateRootProperties( - propertiesSignal: Signal, - properties: Properties, - defaultProperties: AllOptionalProperties | undefined, - hoveredSignal: Signal>, - activeSignal: Signal>, - transformers: PropertyTransformers, - propertiesSubscriptions: Subscriptions, -) { - //build merged properties - const merged = new MergedProperties(transformers) - merged.addAll(defaultProperties, properties) - propertiesSignal.value = merged - - //build handlers - const handlers = cloneHandlers(properties) - unsubscribeSubscriptions(propertiesSubscriptions) - addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal, propertiesSubscriptions) - addActiveHandlers(handlers, properties, defaultProperties, activeSignal) - return handlers +export function destroyRoot(internals: ReturnType) { + unsubscribeSubscriptions(internals.subscriptions) } function createDeferredRequestLayoutCalculation( diff --git a/packages/uikit/src/context.ts b/packages/uikit/src/context.ts index b539338d..66ea6f73 100644 --- a/packages/uikit/src/context.ts +++ b/packages/uikit/src/context.ts @@ -5,11 +5,6 @@ import { ClippingRect } from './clipping' import { OrderInfo, WithCameraDistance } from './order' import { GlyphGroupManager } from './text/render/instanced-glyph-group' import { PanelGroupManager } from './panel/instanced-panel-group' -import { EventHandlers } from './events' - -export type BindEventHandlers = (object: Object3D, handlers: EventHandlers) => void - -export type UnbindEventHandlers = (object: Object3D, handlers: EventHandlers) => void export type WithContext = ElementContext & Readonly<{ root: RootContext }> @@ -21,8 +16,6 @@ export type RootContext = WithCameraDistance & panelGroupManager: PanelGroupManager pixelSize: number onFrameSet: Set<(delta: number) => void> - bindEventHandlers: BindEventHandlers - unbindEventHandlers: UnbindEventHandlers }> & ElementContext diff --git a/packages/uikit/src/hover.ts b/packages/uikit/src/hover.ts index 82a9baa5..b74d0aa4 100644 --- a/packages/uikit/src/hover.ts +++ b/packages/uikit/src/hover.ts @@ -12,16 +12,17 @@ export type WithHover = T & { } export type HoverEventHandlers = Pick +export function setupCursorCleanup(hoveredSignal: Signal>, subscriptions: Subscriptions) { + //cleanup cursor effect + subscriptions.push(() => unsetCursorType(hoveredSignal)) +} + export function addHoverHandlers( target: EventHandlers, properties: WithHover<{}>, defaultProperties: AllOptionalProperties | undefined, hoveredSignal: Signal>, - subscriptions: Subscriptions, ): void { - //cleanup cursor effect - subscriptions.push(() => unsetCursorType(hoveredSignal)) - let hoverPropertiesExist = false traverseProperties(defaultProperties, properties, (p) => { if ('hover' in p) { diff --git a/packages/uikit/src/listeners.ts b/packages/uikit/src/listeners.ts index e893d959..289d1a5d 100644 --- a/packages/uikit/src/listeners.ts +++ b/packages/uikit/src/listeners.ts @@ -5,19 +5,6 @@ import { ThreeEvent } from './events' export type Listeners = ScrollListeners & LayoutListeners & ViewportListeners -export function createListeners(): Listeners { - return {} -} - -export function updateListeners( - target: ScrollListeners & LayoutListeners & ViewportListeners, - { onIsInViewportChange, onScroll, onSizeChange }: ScrollListeners & LayoutListeners & ViewportListeners, -): void { - target.onIsInViewportChange = onIsInViewportChange - target.onScroll = onScroll - target.onSizeChange = onSizeChange -} - export type ScrollListeners = { onScroll?: (scrollX: number, scrollY: number, event?: ThreeEvent) => void } @@ -31,7 +18,7 @@ export type ViewportListeners = { } export function setupLayoutListeners( - listeners: LayoutListeners, + listeners: Signal, size: Signal, subscriptions: Subscriptions, ) { @@ -43,13 +30,13 @@ export function setupLayoutListeners( first = false return } - listeners.onSizeChange?.(...s) + listeners.peek().onSizeChange?.(...s) }), ) } export function setupViewportListeners( - listeners: ViewportListeners, + listeners: Signal, isClipped: Signal, subscriptions: Subscriptions, ) { @@ -61,7 +48,7 @@ export function setupViewportListeners( first = false return } - listeners.onIsInViewportChange?.(isInViewport) + listeners.peek().onIsInViewportChange?.(isInViewport) }), ) } diff --git a/packages/uikit/src/panel/instanced-panel-mesh.ts b/packages/uikit/src/panel/instanced-panel-mesh.ts index 20e3a28e..cae067ed 100644 --- a/packages/uikit/src/panel/instanced-panel-mesh.ts +++ b/packages/uikit/src/panel/instanced-panel-mesh.ts @@ -1,31 +1,28 @@ import { Box3, InstancedBufferAttribute, Mesh, Object3DEventMap, Sphere } from 'three' import { createPanelGeometry, panelGeometry } from './utils.js' import { instancedPanelDepthMaterial, instancedPanelDistanceMaterial } from './panel-material.js' -import { effect } from '@preact/signals-core' +import { Signal, effect } from '@preact/signals-core' import { Subscriptions } from '../utils.js' import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' -import { WithContext } from '../context.js' import { EventHandlers, ThreeEvent } from '../events.js' +import { OrderInfo } from '../order.js' +import { ClippingRect, FlexNode, RootContext } from '../internals.js' export function createInteractionPanel( - context: WithContext, - parentContext: WithContext, + node: FlexNode, + orderInfo: Signal, + rootContext: RootContext, + parentClippingRect: Signal | undefined, subscriptions: Subscriptions, ): Mesh { const panel = new Mesh(panelGeometry) panel.matrixAutoUpdate = false - panel.raycast = makeClippedRaycast( - panel, - makePanelRaycast(panel), - context.root.object, - parentContext.clippingRect, - context.orderInfo, - ) + panel.raycast = makeClippedRaycast(panel, makePanelRaycast(panel), rootContext.object, parentClippingRect, orderInfo) panel.visible = false subscriptions.push( effect(() => { - const [width, height] = context.node.size.value - const pixelSize = context.root.pixelSize + const [width, height] = node.size.value + const pixelSize = rootContext.pixelSize panel.scale.set(width * pixelSize, height * pixelSize, 1) panel.updateMatrix() }), diff --git a/packages/uikit/src/scroll.ts b/packages/uikit/src/scroll.ts index ac0b0fc5..7a7ecc93 100644 --- a/packages/uikit/src/scroll.ts +++ b/packages/uikit/src/scroll.ts @@ -58,7 +58,7 @@ export function setupScrollHandler( node: FlexNode, scrollPosition: Signal, object: Object3DRef, - listeners: ScrollListeners, + listeners: Signal, pixelSize: number, scrollHandlers: Signal, subscriptions: Subscriptions, @@ -102,7 +102,7 @@ export function setupScrollHandler( } if (x != newX || y != newY) { scrollPosition.value = [newX, newY] - listeners.onScroll?.(...scrollPosition.value, event) + listeners.peek().onScroll?.(...scrollPosition.value, event) } } diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts index 688a417f..e06a59af 100644 --- a/packages/uikit/src/vanilla/container.ts +++ b/packages/uikit/src/vanilla/container.ts @@ -1,63 +1,49 @@ import { Object3D } from 'three' -import { WithContext } from '../context' -import { - ContainerProperties, - ContainerState, - cleanContainerState, - createContainerContext, - createContainerState, - updateContainerProperties, -} from '../components/container' -import { effect } from '@preact/signals-core' -import { AllOptionalProperties } from '../properties/default' -import { createInteractionPanel } from '../panel/instanced-panel-mesh' -import { EventHandlers } from '../events' +import { ContainerProperties, createContainer, destroyContainer } from '../components/container' +import { AllOptionalProperties, Properties } from '../properties/default' import { Component } from '.' +import { EventConfig, bindHandlers } from './utils' +import { batch } from '@preact/signals-core' export class Container extends Object3D { - public readonly ctx: WithContext - private state: ContainerState - private container: Object3D - private prevHandlers?: EventHandlers + private object: Object3D + public readonly internals: ReturnType + public readonly eventConfig: EventConfig constructor(parent: Component, properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { super() + this.eventConfig = parent.eventConfig //setting up the threejs elements - this.container = new Object3D() - this.container.matrixAutoUpdate = false - this.container.add(this) + this.object = new Object3D() + this.object.matrixAutoUpdate = false + this.object.add(this) this.matrixAutoUpdate = false - parent.add(this.container) + parent.add(this.object) //setting up the container - this.state = createContainerState(parent.ctx.root.node.size) - this.setProperties(properties, defaultProperties) - this.ctx = createContainerContext(this.state, { current: this.container }, { current: this }, parent.ctx) + this.internals = createContainer( + parent.internals, + properties, + defaultProperties, + { current: this.object }, + { current: this }, + ) //setup scrolling & events - const interactionPanel = createInteractionPanel(this.ctx, parent.ctx, this.state.subscriptions) - this.container.add(interactionPanel) - this.state.subscriptions.push( - effect(() => { - const scrollHandlers = this.state.scrollHandlers.value - this.ctx.root.bindEventHandlers(interactionPanel, scrollHandlers) - return () => this.ctx.root.unbindEventHandlers(interactionPanel, scrollHandlers) - }), - ) + this.add(this.internals.interactionPanel) + bindHandlers(this.internals, this, this.internals.interactionPanel, this.eventConfig) } - setProperties(properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { - if (this.prevHandlers != null) { - this.ctx.root.unbindEventHandlers(this.container, this.prevHandlers) - } - const handlers = updateContainerProperties(this.state, properties, defaultProperties) - this.ctx.root.bindEventHandlers(this.container, handlers) - this.prevHandlers = handlers + setProperties(properties: Properties, defaultProperties?: AllOptionalProperties) { + batch(() => { + this.internals.propertiesSignal.value = properties + this.internals.defaultPropertiesSignal.value = defaultProperties + }) } destroy() { - this.container.parent?.remove(this.container) - cleanContainerState(this.state) + this.object.parent?.remove(this.object) + destroyContainer(this.internals) } } diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index 78f3f304..ff7b1d2c 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -1,93 +1,46 @@ -import { Object3D, Object3DEventMap, Texture, Vector2Tuple } from 'three' -import { WithContext } from '../context' -import { - InheritableImageProperties, - createImage, - createImageMesh, - computeTextureAspectRatio, - createImagePropertyTransformers, - loadImageTexture, - updateImageProperties, - ImageProperties, -} from '../components/image' -import { Listeners, createListeners, updateListeners } from '../listeners' -import { Signal, effect, signal } from '@preact/signals-core' -import { MergedProperties, PropertyTransformers } from '../properties/merged' -import { Component, BindEventHandlers } from './utils' -import { Subscriptions, unsubscribeSubscriptions } from '../utils' +import { Object3D } from 'three' +import { ImageProperties, createImage, destroyImage } from '../components/image' import { AllOptionalProperties } from '../properties/default' -import { EventHandlers } from '../events' +import { Component } from '.' +import { EventConfig, bindHandlers } from './utils' +import { batch } from '@preact/signals-core' export class Image extends Object3D { - private propertiesSignal: Signal - private container: Object3D - private subscriptions: Subscriptions = [] - private propertySubscriptions: Subscriptions = [] - private imageSubscriptions: Subscriptions = [] - private listeners = createListeners() - private propertyTransformers: PropertyTransformers - private hoveredSignal = signal>([]) - private activeSignal = signal>([]) - private texture = signal(undefined) - private textureAspectRatio: Signal + public readonly internals: ReturnType + public readonly eventConfig: EventConfig - public readonly bindEventHandlers: BindEventHandlers - public readonly ctx: WithContext - private prevSrc?: ImageProperties['src'] + private container: Object3D constructor(parent: Component, properties: ImageProperties, defaultProperties?: AllOptionalProperties) { super() - this.textureAspectRatio = computeTextureAspectRatio(this.texture) - const scrollHandlers = signal({}) - const rootSize = parent.ctx.root.node.size - this.propertyTransformers = createImagePropertyTransformers(rootSize, this.hoveredSignal, this.activeSignal) - this.bindEventHandlers = parent.bindEventHandlers + this.eventConfig = parent.eventConfig this.container = new Object3D() this.container.matrixAutoUpdate = false this.container.add(this) this.matrixAutoUpdate = false parent.add(this.container) - this.propertiesSignal = signal(undefined as any) - this.setProperties(properties, defaultProperties) - this.ctx = createImage( - this.propertiesSignal, - { current: this.container }, + this.internals = createImage( + parent.internals, + properties, + defaultProperties, { current: this }, - parent.ctx, - scrollHandlers, - this.listeners, - this.subscriptions, + { current: this.container }, ) - const mesh = createImageMesh(this.propertiesSignal, this.texture, parent.ctx, this.ctx, this.subscriptions) - this.container.add(mesh) - this.subscriptions.push(effect(() => this.bindEventHandlers(mesh, scrollHandlers.value))) + this.setProperties(properties, defaultProperties) + + this.container.add(this.internals.mesh) + bindHandlers(this.internals, this, this.internals.mesh, this.eventConfig) } setProperties(properties: ImageProperties, defaultProperties?: AllOptionalProperties) { - if (properties.src != this.prevSrc) { - unsubscribeSubscriptions(this.imageSubscriptions) - loadImageTexture(this.texture, properties.src, this.imageSubscriptions) - this.prevSrc = properties.src - } - unsubscribeSubscriptions(this.propertySubscriptions) - const handlers = updateImageProperties( - this.propertiesSignal, - this.textureAspectRatio, - properties, - defaultProperties, - this.hoveredSignal, - this.activeSignal, - this.propertyTransformers, - this.propertySubscriptions, - ) - this.bindEventHandlers(this.container, handlers) - updateListeners(this.listeners, properties) + batch(() => { + this.internals.propertiesSignal.value = properties + this.internals.defaultPropertiesSignal.value = defaultProperties + }) } destroy() { this.container.parent?.remove(this.container) - unsubscribeSubscriptions(this.imageSubscriptions) - unsubscribeSubscriptions(this.propertySubscriptions) - unsubscribeSubscriptions(this.subscriptions) + destroyImage(this.internals) } } diff --git a/packages/uikit/src/vanilla/index.ts b/packages/uikit/src/vanilla/index.ts index 799b71d3..7bdb136b 100644 --- a/packages/uikit/src/vanilla/index.ts +++ b/packages/uikit/src/vanilla/index.ts @@ -2,7 +2,7 @@ import type { Container } from './container' import type { Root } from './root' import type { Image } from './image' -export type Component = Container | Root | Image +export type Component = Container | Root export * from './container' export * from './root' diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index 491eb2c4..93d88f03 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -1,96 +1,55 @@ -import { Camera, Object3D, Vector2Tuple } from 'three' -import { WithContext } from '../context' -import { createListeners, updateListeners } from '../listeners' -import { Signal, effect, signal } from '@preact/signals-core' -import { MergedProperties, PropertyTransformers } from '../properties/merged' -import { Subscriptions, unsubscribeSubscriptions } from '../utils' +import { Camera, Object3D } from 'three' +import { batch } from '@preact/signals-core' import { AllOptionalProperties } from '../properties/default' -import { createInteractionPanel } from '../panel/instanced-panel-mesh' -import { updateRootProperties, createRoot, createRootPropertyTransformers, RootProperties } from '../components/root' -import { EventHandlers } from '../events' +import { createRoot, destroyRoot, RootProperties } from '../components/root' +import { EventConfig, bindHandlers } from './utils' export class Root extends Object3D { - private propertiesSignal: Signal - private container: Object3D - private subscriptions: Subscriptions = [] - private propertySubscriptions: Subscriptions = [] - private listeners = createListeners() - private scrollHandlers = signal({}) - private onFrameSet = new Set<(delta: number) => void>() - private propertyTransformers: PropertyTransformers - private hoveredSignal = signal>([]) - private activeSignal = signal>([]) - - public readonly ctx: WithContext + public readonly internals: ReturnType + private object: Object3D constructor( + public readonly eventConfig: EventConfig, camera: Camera | (() => Camera), - object: Object3D, + parent: Object3D, properties: RootProperties, defaultProperties?: AllOptionalProperties, ) { super() - const rootSize = signal([0, 0]) - this.propertyTransformers = createRootPropertyTransformers( - rootSize, - this.hoveredSignal, - this.activeSignal, - properties.pixelSize, - ) - this.container = new Object3D() - this.container.matrixAutoUpdate = false - this.container.add(this) + this.object = new Object3D() + this.object.matrixAutoUpdate = false + this.object.add(this) this.matrixAutoUpdate = false - object.add(this.container) - this.propertiesSignal = signal(undefined as any) - this.setProperties(properties, defaultProperties) - this.ctx = createRoot( - this.propertiesSignal, - rootSize, - { current: this.container }, + parent.add(this.object) + + this.internals = createRoot( + properties, + defaultProperties, + { current: this }, { current: this }, - this.scrollHandlers, - this.listeners, - properties.pixelSize, - this.onFrameSet, typeof camera === 'function' ? camera : () => camera, - this.subscriptions, ) - const interactionPanel = createInteractionPanel( - this.ctx.node.size, - this.ctx.root.pixelSize, - this.ctx.orderInfo, - undefined, - this.ctx.root.object, - this.subscriptions, - ) - this.container.add(interactionPanel) - this.subscriptions.push(effect(() => this.bindEventHandlers(interactionPanel, this.scrollHandlers.value))) + + //setup scrolling & events + this.add(this.internals.interactionPanel) + bindHandlers(this.internals, this, this.internals.interactionPanel, this.eventConfig) } update(delta: number) { - for (const onFrame of this.onFrameSet) { + for (const onFrame of this.internals.onFrameSet) { onFrame(delta) } } setProperties(properties: RootProperties, defaultProperties?: AllOptionalProperties) { - const handlers = updateRootProperties( - this.propertiesSignal, - properties, - defaultProperties, - this.hoveredSignal, - this.activeSignal, - this.propertyTransformers, - this.propertySubscriptions, - ) - this.bindEventHandlers(this.container, handlers) - updateListeners(this.listeners, properties) + batch(() => { + this.internals.propertiesSignal.value = properties + this.internals.defaultPropertiesSignal.value = defaultProperties + }) } destroy() { - this.container.parent?.remove(this.container) - unsubscribeSubscriptions(this.propertySubscriptions) - unsubscribeSubscriptions(this.subscriptions) + this.object.parent?.remove(this.object) + destroyRoot(this.internals) } } diff --git a/packages/uikit/src/vanilla/utils.ts b/packages/uikit/src/vanilla/utils.ts new file mode 100644 index 00000000..326b2706 --- /dev/null +++ b/packages/uikit/src/vanilla/utils.ts @@ -0,0 +1,38 @@ +import { Signal, effect } from '@preact/signals-core' +import { Subscriptions } from '../utils' +import { EventHandlers } from '../events' +import { Mesh, Object3D } from 'three' +import { RootContext } from '../context' + +export type EventConfig = { + bindEventHandlers: (object: Object3D, handlers: EventHandlers) => void + unbindEventHandlers: (object: Object3D, handlers: EventHandlers) => void +} + +export function bindHandlers( + { + scrollHandlers, + handlers, + subscriptions, + }: { + scrollHandlers: Signal + handlers: Signal + subscriptions: Subscriptions + }, + container: Object3D, + mesh: Mesh, + eventConfig: EventConfig, +) { + subscriptions.push( + effect(() => { + const { value } = handlers + eventConfig.bindEventHandlers(container, value) + return () => eventConfig.unbindEventHandlers(container, value) + }), + effect(() => { + const { value } = scrollHandlers + eventConfig.bindEventHandlers(mesh, value) + return () => eventConfig.unbindEventHandlers(mesh, value) + }), + ) +} From 7e2ef828d45b56d3b2664b24160d95ca8b925608 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Wed, 27 Mar 2024 13:17:48 +0100 Subject: [PATCH 06/20] example: added interaction example --- examples/vanilla/index.ts | 33 +++++++++++++++++++++++--- packages/react/src/image.tsx | 2 +- packages/uikit/package.json | 1 + packages/uikit/src/components/image.ts | 2 +- packages/uikit/src/vanilla/image.ts | 4 ++-- pnpm-lock.yaml | 3 +++ 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/examples/vanilla/index.ts b/examples/vanilla/index.ts index d5f6b643..8b021c8e 100644 --- a/examples/vanilla/index.ts +++ b/examples/vanilla/index.ts @@ -1,6 +1,7 @@ import { PerspectiveCamera, Scene, WebGLRenderer } from 'three' import { patchRenderOrder, Container, Root, Image } from '@vanilla-three/uikit' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' +import { EventHandlers } from '@vanilla-three/uikit/internals' // init @@ -12,11 +13,31 @@ const scene = new Scene() const canvas = document.getElementById('root') as HTMLCanvasElement const controls = new OrbitControls(camera, canvas) +function handlerToEventName(key: string) { + return key[2].toLocaleLowerCase() + key.slice(3) +} + //UI const root = new Root( { - bindEventHandlers(object, handlers) {}, - unbindEventHandlers(object, handlers) {}, + bindEventHandlers(object, handlers) { + for (const key in handlers) { + const handler = handlers[key as keyof EventHandlers] + if (handler == null) { + continue + } + object.addEventListener(handlerToEventName(key), handler as any) + } + }, + unbindEventHandlers(object, handlers) { + for (const key in handlers) { + const handler = handlers[key as keyof EventHandlers] + if (handler == null) { + continue + } + object.removeEventListener(handlerToEventName(key), handler as any) + } + }, }, camera, scene, @@ -30,7 +51,13 @@ const root = new Root( }, ) new Container(root, { flexGrow: 1, backgroundColor: 'blue' }) -const x = new Container(root, { padding: 30, flexGrow: 1, backgroundColor: 'green' }) +const x = new Container(root, { + padding: 30, + flexGrow: 1, + backgroundColor: 'green', + hover: { backgroundColor: 'yellow' }, +}) +x.dispatchEvent({ type: 'pointerOver', target: x, nativeEvent: { pointerId: 1 } } as any) new Image(x, { keepAspectRatio: false, borderRadius: 1000, diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx index c2835cb9..39ba2c4a 100644 --- a/packages/react/src/image.tsx +++ b/packages/react/src/image.tsx @@ -20,7 +20,7 @@ export const Image: (props: ImageProperties & { children?: ReactNode }) => React return ( - + {properties.children} diff --git a/packages/uikit/package.json b/packages/uikit/package.json index 3303bc18..2b18d7fc 100644 --- a/packages/uikit/package.json +++ b/packages/uikit/package.json @@ -42,6 +42,7 @@ "yoga-layout": "^2.0.1" }, "devDependencies": { + "@types/node": "^20.11.0", "@types/three": "^0.161.0", "three": "^0.161.0" } diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index e3418d1c..62cd96f1 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -190,7 +190,7 @@ export function createImage( addActiveHandlers(handlers, properties, defaultProperties, activeSignal) return handlers }), - mesh: createImageMesh(mergedProperties, texture, parentContext, ctx, subscriptions), + interactionPanel: createImageMesh(mergedProperties, texture, parentContext, ctx, subscriptions), }) } diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index ff7b1d2c..45cd841f 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -28,8 +28,8 @@ export class Image extends Object3D { ) this.setProperties(properties, defaultProperties) - this.container.add(this.internals.mesh) - bindHandlers(this.internals, this, this.internals.mesh, this.eventConfig) + this.container.add(this.internals.interactionPanel) + bindHandlers(this.internals, this, this.internals.interactionPanel, this.eventConfig) } setProperties(properties: ImageProperties, defaultProperties?: AllOptionalProperties) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8bbf29af..754c9c6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -482,6 +482,9 @@ importers: specifier: ^2.0.1 version: 2.0.1 devDependencies: + '@types/node': + specifier: ^20.11.0 + version: 20.11.0 '@types/three': specifier: ^0.161.0 version: 0.161.2 From dce5f0f6eb2ad32ec7bf10ea9f41f581b2125f50 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Mon, 1 Apr 2024 18:58:03 +0200 Subject: [PATCH 07/20] add dependencies to the react package --- packages/react/package.json | 7 +- pnpm-lock.yaml | 147 +++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 2 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 12d80173..b7ca9bf7 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -39,7 +39,12 @@ }, "dependencies": { "@preact/signals-core": "^1.5.1", - "@vanilla-three/uikit": "workspace:^" + "@vanilla-three/uikit": "workspace:^", + "chalk": "^5.3.0", + "commander": "^12.0.0", + "ora": "^8.0.1", + "prompts": "^2.4.2", + "zod": "^3.22.4" }, "devDependencies": { "@react-three/drei": "^9.96.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 754c9c6f..42ca8409 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -441,6 +441,21 @@ importers: '@vanilla-three/uikit': specifier: workspace:^ version: link:../uikit + chalk: + specifier: ^5.3.0 + version: 5.3.0 + commander: + specifier: ^12.0.0 + version: 12.0.0 + ora: + specifier: ^8.0.1 + version: 8.0.1 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@react-three/drei': specifier: ^9.96.1 @@ -2792,6 +2807,11 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -3189,6 +3209,11 @@ packages: ansi-styles: 4.3.0 supports-color: 7.2.0 + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + /check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: @@ -3234,12 +3259,24 @@ packages: engines: {node: '>=6'} dev: false + /cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: false + /cli-progress@3.12.0: resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} engines: {node: '>=4'} dependencies: string-width: 4.2.3 + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: false + /cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: @@ -3304,6 +3341,11 @@ packages: delayed-stream: 1.0.0 dev: false + /commander@12.0.0: + resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==} + engines: {node: '>=18'} + dev: false + /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: false @@ -3537,6 +3579,10 @@ packages: resolution: {integrity: sha512-4nToZ5jlPO14W82NkF32wyjhYqQByVaDmLy4J2/tYcAbJfgO2TKJC780Az1V13gzq4l73CJ0yuyalpXvxXXD9A==} dev: true + /emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4161,6 +4207,11 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + /get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + dev: false + /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true @@ -4597,6 +4648,11 @@ packages: is-path-inside: 3.0.3 dev: false + /is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: false + /is-invalid-path@1.0.2: resolution: {integrity: sha512-6KLcFrPCEP3AFXMfnWrIFkZpYNBVzZAoBJJDEZKtI3LXkaDjM3uFMJQjxiizUuZTZ9Oh9FNv/soXbx5TcpaDmA==} engines: {node: '>=6.0'} @@ -4694,6 +4750,16 @@ packages: engines: {node: '>=10'} dev: true + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: false + + /is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + dev: false + /is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} dev: true @@ -4859,7 +4925,6 @@ packages: /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - dev: true /ktx-parse@0.4.5: resolution: {integrity: sha512-MK3FOody4TXbFf8Yqv7EBbySw7aPvEcPX++Ipt6Sox+/YMFvR5xaTyhfNSk1AEmMy+RYIw81ctN4IMxCB8OAlg==} @@ -4928,6 +4993,14 @@ packages: is-unicode-supported: 0.1.0 dev: true + /log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -5074,6 +5147,11 @@ packages: engines: {node: '>=4'} hasBin: true + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: false + /mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -5376,6 +5454,13 @@ packages: dependencies: wrappy: 1.0.2 + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: false + /opentype.js@0.11.0: resolution: {integrity: sha512-Z9NkAyQi/iEKQYzCSa7/VJSqVIs33wknw8Z8po+DzuRUAqivJ+hJZ94mveg3xIeKwLreJdWTMyEO7x1K13l41Q==} hasBin: true @@ -5405,6 +5490,21 @@ packages: type-check: 0.4.0 dev: true + /ora@8.0.1: + resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} + engines: {node: '>=18'} + dependencies: + chalk: 5.3.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.0.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.1.0 + strip-ansi: 7.1.0 + dev: false + /oslllo-potrace@2.0.1: resolution: {integrity: sha512-XDsVIUfwXnylngcbecF/6gBHdtFgEnqDt0a9WKqXIo/jPe2AkZkmi6bNaNb9OwlAgoIjy0b1Hi6odPEqztPszg==} dependencies: @@ -5637,6 +5737,14 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: false + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -5927,6 +6035,14 @@ packages: lowercase-keys: 1.0.1 dev: false + /restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -6121,6 +6237,10 @@ packages: is-arrayish: 0.3.2 dev: false + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -6172,6 +6292,11 @@ packages: /stats.js@0.17.0: resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + /stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + dev: false + /streamx@2.16.1: resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} dependencies: @@ -6189,6 +6314,15 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width@7.1.0: + resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} + engines: {node: '>=18'} + dependencies: + emoji-regex: 10.3.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + dev: false + /string.prototype.codepointat@0.2.1: resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} dev: false @@ -6243,6 +6377,13 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -6989,6 +7130,10 @@ packages: resolution: {integrity: sha512-tT/oChyDXelLo2A+UVnlW9GU7CsvFMaEnd9kVFsaiCQonFAXd3xrHhkLYu+suwwosrAEQ746xBU+HvYtm1Zs2Q==} dev: false + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false + /zstddec@0.0.2: resolution: {integrity: sha512-DCo0oxvcvOTGP/f5FA6tz2Z6wF+FIcEApSTu0zV5sQgn9hoT5lZ9YRAKUraxt9oP7l4e8TnNdi8IZTCX6WCkwA==} dev: false From 5937f3b38e6139122d0709ab3337c077a5933d4b Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Tue, 2 Apr 2024 20:24:06 +0200 Subject: [PATCH 08/20] improve panel properties system --- examples/vanilla/vite.config.ts | 11 +- packages/react/src/container.tsx | 6 +- packages/react/src/font.tsx | 18 +- packages/react/src/image.tsx | 6 +- packages/react/src/input.tsx | 643 +++++++++--------- packages/react/src/ref.ts | 62 +- packages/react/src/root.tsx | 6 +- packages/uikit/src/allocation/index.ts | 2 +- packages/uikit/src/caret.ts | 135 ++-- packages/uikit/src/clipping.ts | 6 +- packages/uikit/src/components/container.ts | 43 +- packages/uikit/src/components/image.ts | 165 +++-- packages/uikit/src/components/index.ts | 8 +- packages/uikit/src/components/root.ts | 88 +-- packages/uikit/src/components/utils.tsx | 12 +- packages/uikit/src/context.ts | 10 +- packages/uikit/src/flex/node.ts | 4 +- packages/uikit/src/focus.ts | 22 +- packages/uikit/src/internals.ts | 31 +- packages/uikit/src/order.ts | 19 +- packages/uikit/src/panel/index.ts | 10 +- .../uikit/src/panel/instanced-panel-group.ts | 45 +- packages/uikit/src/panel/instanced-panel.ts | 103 +-- packages/uikit/src/panel/panel-material.ts | 271 +++++--- packages/uikit/src/panel/utils.ts | 40 +- packages/uikit/src/properties/batched.ts | 22 +- packages/uikit/src/properties/default.ts | 6 +- packages/uikit/src/properties/immediate.ts | 2 +- packages/uikit/src/properties/index.ts | 10 +- packages/uikit/src/properties/merged.ts | 4 +- packages/uikit/src/scroll.ts | 106 +-- packages/uikit/src/selection.ts | 149 ++-- packages/uikit/src/text/font.ts | 18 +- packages/uikit/src/text/index.ts | 10 +- packages/uikit/src/text/layout.ts | 6 +- packages/uikit/src/text/render/index.ts | 10 +- .../uikit/src/text/render/instanced-glyph.ts | 34 +- .../uikit/src/text/render/instanced-text.ts | 6 +- packages/uikit/src/transform.ts | 17 +- packages/uikit/src/utils.ts | 29 +- packages/uikit/src/vanilla/container.ts | 8 +- packages/uikit/src/vanilla/image.ts | 8 +- packages/uikit/src/vanilla/index.ts | 8 +- packages/uikit/src/vanilla/root.ts | 6 +- packages/uikit/src/vanilla/utils.ts | 5 +- pnpm-lock.yaml | 22 +- 46 files changed, 1105 insertions(+), 1147 deletions(-) diff --git a/examples/vanilla/vite.config.ts b/examples/vanilla/vite.config.ts index 93888ac4..854db0f0 100644 --- a/examples/vanilla/vite.config.ts +++ b/examples/vanilla/vite.config.ts @@ -1,13 +1,20 @@ import { defineConfig } from 'vite' import path from 'path' -import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ resolve: { alias: [ { find: '@', replacement: path.resolve(__dirname, '../../packages/kits/default') }, - { find: '@react-three/uikit', replacement: path.resolve(__dirname, '../../packages/uikit/src/index.ts') }, + { find: '@vanilla-three/uikit', replacement: path.resolve(__dirname, '../../packages/uikit/src/index.ts') }, ], }, + optimizeDeps: { + esbuildOptions: { + target: 'esnext', + }, + }, + build: { + target: 'esnext', + }, }) diff --git a/packages/react/src/container.tsx b/packages/react/src/container.tsx index 1f7a41d6..abf00072 100644 --- a/packages/react/src/container.tsx +++ b/packages/react/src/container.tsx @@ -1,10 +1,10 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' import { forwardRef, ReactNode, useEffect, useMemo, useRef } from 'react' import { Object3D } from 'three' -import { ParentProvider, useParent } from './context' -import { AddHandlers, AddScrollHandler } from './utilts' +import { ParentProvider, useParent } from './context.js' +import { AddHandlers, AddScrollHandler } from './utilts.js' import { ContainerProperties, createContainer, destroyContainer } from '@vanilla-three/uikit/internals' -import { useDefaultProperties } from './default' +import { useDefaultProperties } from './default.js' export const Container: ( props: { diff --git a/packages/react/src/font.tsx b/packages/react/src/font.tsx index b354ce2b..b26fdc6f 100644 --- a/packages/react/src/font.tsx +++ b/packages/react/src/font.tsx @@ -1,10 +1,10 @@ export function FontFamilyProvider(properties: { - [Key in T]: Key extends 'children' ? ReactNode : FontFamilyUrls - }) { - let { children, ...fontFamilies } = properties as any - const existinFontFamilyUrls = useContext(FontFamiliesContext) - if (existinFontFamilyUrls != null) { - fontFamilies = { ...existinFontFamilyUrls, ...fontFamilies } - } - return {children} - } \ No newline at end of file + [Key in T]: Key extends 'children' ? ReactNode : FontFamilyUrls +}) { + let { children, ...fontFamilies } = properties as any + const existinFontFamilyUrls = useContext(FontFamiliesContext) + if (existinFontFamilyUrls != null) { + fontFamilies = { ...existinFontFamilyUrls, ...fontFamilies } + } + return {children} +} diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx index 39ba2c4a..7321339f 100644 --- a/packages/react/src/image.tsx +++ b/packages/react/src/image.tsx @@ -1,9 +1,9 @@ import { createImage, ImageProperties, destroyImage } from '@vanilla-three/uikit/internals' import { ReactNode, forwardRef, useEffect, useMemo, useRef } from 'react' import { Object3D } from 'three' -import { AddHandlers, AddScrollHandler } from './utilts' -import { ParentProvider, useParent } from './context' -import { useDefaultProperties } from './default' +import { AddHandlers, AddScrollHandler } from './utilts.js' +import { ParentProvider, useParent } from './context.js' +import { useDefaultProperties } from './default.js' export const Image: (props: ImageProperties & { children?: ReactNode }) => ReactNode = forwardRef((properties, ref) => { //TODO: ComponentInternals diff --git a/packages/react/src/input.tsx b/packages/react/src/input.tsx index 46a5ecd7..23ec59e4 100644 --- a/packages/react/src/input.tsx +++ b/packages/react/src/input.tsx @@ -1,329 +1,328 @@ import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, - } from './utils.js' - import { - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - PointerEvent as ReactPointerEvent, - } from 'react' - import { ReadonlySignal, Signal, signal } from '@preact/signals-core' - import { readReactive, useRootGroupRef, useSignalEffect } from '../utils.js' - import { TextProperties } from './text.js' - import { Group, Vector2, Vector2Tuple, Vector3Tuple } from 'three' - import { InstancedText } from '../text/render/instanced-text.js' - import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' - import { MaterialClass } from '../index.js' - import { ElementType, ZIndexOffset, useOrderInfo } from '../order.js' - import { - InteractionGroup, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, - } from '../panel/react.js' - import { ScrollListeners } from '../scroll.js' - import { WithFocus, useApplyFocusProperties } from '../focus.js' - import { useApplyActiveProperties } from '../active.js' - import { useCaret } from '../caret.js' - import { useParentClippingRect, useIsClipped } from '../clipping.js' - import { useApplyPreferredColorSchemeProperties } from '../dark.js' - import { useFlexNode } from '../flex/react.js' - import { useApplyHoverProperties } from '../hover.js' - import { flexAliasPropertyTransformation, panelAliasPropertyTransformation } from '../properties/alias.js' - import { useApplyProperties } from '../properties/default.js' - import { useImmediateProperties } from '../properties/immediate.js' - import { createCollection, writeCollection, finalizeCollection } from '../properties/utils.js' - import { useApplyResponsiveProperties } from '../responsive.js' - import { SelectionBoxes, useSelection } from '../selection.js' - import { useInstancedText } from '../text/react.js' - import { useTransformMatrix } from '../transform.js' - import { FlexNode } from '../flex/node.js' - - export type InputProperties = WithFocus - - export type InputInternals = ComponentInternals & { readonly value: string | ReadonlySignal; focus: () => void } - - const cancelSet = new Set() - - function cancelBlur(event: PointerEvent) { - cancelSet.add(event) - } - - export const canvasInputProps = { - onPointerDown: (e: ReactPointerEvent) => { - if (!(document.activeElement instanceof HTMLElement)) { - return + ComponentInternals, + LayoutListeners, + ViewportListeners, + useGlobalMatrix, + useLayoutListeners, + useViewportListeners, +} from './utils.js' +import { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + PointerEvent as ReactPointerEvent, +} from 'react' +import { ReadonlySignal, Signal, signal } from '@preact/signals-core' +import { readReactive, useRootGroupRef, useSignalEffect } from '../utils.js' +import { TextProperties } from './text.js' +import { Group, Vector2, Vector2Tuple, Vector3Tuple } from 'three' +import { InstancedText } from '../text/render/instanced-text.js' +import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' +import { MaterialClass } from '../index.js' +import { ElementType, ZIndexOffset, useOrderInfo } from '../order.js' +import { + InteractionGroup, + ShadowProperties, + useInstancedPanel, + useInteractionPanel, + usePanelGroupDependencies, +} from '../panel/react.js' +import { ScrollListeners } from '../scroll.js' +import { WithFocus, useApplyFocusProperties } from '../focus.js' +import { useApplyActiveProperties } from '../active.js' +import { useCaret } from '../caret.js' +import { useParentClippingRect, useIsClipped } from '../clipping.js' +import { useApplyPreferredColorSchemeProperties } from '../dark.js' +import { useFlexNode } from '../flex/react.js' +import { useApplyHoverProperties } from '../hover.js' +import { flexAliasPropertyTransformation, panelAliasPropertyTransformation } from '../properties/alias.js' +import { useApplyProperties } from '../properties/default.js' +import { useImmediateProperties } from '../properties/immediate.js' +import { createCollection, writeCollection, finalizeCollection } from '../properties/utils.js' +import { useApplyResponsiveProperties } from '../responsive.js' +import { SelectionBoxes, useSelection } from '../selection.js' +import { useInstancedText } from '../text/react.js' +import { useTransformMatrix } from '../transform.js' +import { FlexNode } from '../flex/node.js' + +export type InputProperties = WithFocus + +export type InputInternals = ComponentInternals & { readonly value: string | ReadonlySignal; focus: () => void } + +const cancelSet = new Set() + +function cancelBlur(event: PointerEvent) { + cancelSet.add(event) +} + +export const canvasInputProps = { + onPointerDown: (e: ReactPointerEvent) => { + if (!(document.activeElement instanceof HTMLElement)) { + return + } + if (!cancelSet.has(e.nativeEvent)) { + return + } + cancelSet.delete(e.nativeEvent) + e.preventDefault() + }, +} + +export const Input = forwardRef< + InputInternals, + { + panelMaterialClass?: MaterialClass + zIndexOffset?: ZIndexOffset + multiline?: boolean + value?: string | Signal + defaultValue?: string + onValueChange?: (value: string) => void + tabIndex?: number + disabled?: boolean + } & InputProperties & + EventHandlers & + LayoutListeners & + ViewportListeners & + ScrollListeners & + ShadowProperties +>((properties, ref) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + const defaultValue = useMemo(() => signal(properties.defaultValue ?? ''), []) + const value = useMemo(() => properties.value ?? defaultValue, [properties.value, defaultValue]) + + const onValueChangeRef = useRef(properties.onValueChange) + onValueChangeRef.current = properties.onValueChange + + const startCharIndex = useRef(undefined) + + const isControlled = properties.value != null + const onChange = useCallback( + (value: string) => { + if (!isControlled) { + defaultValue.value = value } - if (!cancelSet.has(e.nativeEvent)) { + onValueChangeRef.current?.(value) + }, + [defaultValue, isControlled], + ) + const selectionRange = useMemo(() => signal(undefined), []) + const element = useHtmlInputElement(value, selectionRange, onChange, properties.multiline) + element.tabIndex = properties.tabIndex ?? 0 + element.disabled = properties.disabled ?? false + + // eslint-disable-next-line react-hooks/exhaustive-deps + const hasFocusSignal = useMemo(() => signal(document.activeElement === element), []) + useEffect(() => { + const updateFocus = () => (hasFocusSignal.value = document.activeElement === element) + element.addEventListener('focus', updateFocus) + element.addEventListener('blur', updateFocus) + return () => { + element.removeEventListener('focus', updateFocus) + element.removeEventListener('blur', updateFocus) + } + }, [element, hasFocusSignal]) + const setFocus = useCallback( + (focus: boolean) => { + if (hasFocusSignal.peek() === focus) { return } - cancelSet.delete(e.nativeEvent) - e.preventDefault() - }, - } - - export const Input = forwardRef< - InputInternals, - { - panelMaterialClass?: MaterialClass - zIndexOffset?: ZIndexOffset - multiline?: boolean - value?: string | Signal - defaultValue?: string - onValueChange?: (value: string) => void - tabIndex?: number - disabled?: boolean - } & InputProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ScrollListeners & - ShadowProperties - >((properties, ref) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - const defaultValue = useMemo(() => signal(properties.defaultValue ?? ''), []) - const value = useMemo(() => properties.value ?? defaultValue, [properties.value, defaultValue]) - - const onValueChangeRef = useRef(properties.onValueChange) - onValueChangeRef.current = properties.onValueChange - - const startCharIndex = useRef(undefined) - - const isControlled = properties.value != null - const onChange = useCallback( - (value: string) => { - if (!isControlled) { - defaultValue.value = value - } - onValueChangeRef.current?.(value) - }, - [defaultValue, isControlled], - ) - const selectionRange = useMemo(() => signal(undefined), []) - const element = useHtmlInputElement(value, selectionRange, onChange, properties.multiline) - element.tabIndex = properties.tabIndex ?? 0 - element.disabled = properties.disabled ?? false - - // eslint-disable-next-line react-hooks/exhaustive-deps - const hasFocusSignal = useMemo(() => signal(document.activeElement === element), []) - useEffect(() => { - const updateFocus = () => (hasFocusSignal.value = document.activeElement === element) - element.addEventListener('focus', updateFocus) - element.addEventListener('blur', updateFocus) - return () => { - element.removeEventListener('focus', updateFocus) - element.removeEventListener('blur', updateFocus) + if (focus) { + element.focus() + } else { + element.blur() } - }, [element, hasFocusSignal]) - const setFocus = useCallback( - (focus: boolean) => { - if (hasFocusSignal.peek() === focus) { - return - } - if (focus) { - element.focus() - } else { - element.blur() - } - }, - [hasFocusSignal, element], - ) - - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const rootGroupRef = useRootGroupRef() - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - const selectionBoxes = useMemo(() => signal([]), []) - const caretPosition = useMemo(() => signal(undefined), []) - const selectionOrderInfo = useSelection( - globalMatrix, - selectionBoxes, - isClipped, - backgroundOrderInfo, - parentClippingRect, - ) - useCaret(collection, globalMatrix, caretPosition, isClipped, backgroundOrderInfo, parentClippingRect) - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - const instancedTextRef = useRef() - const measureFunc = useInstancedText( - collection, + }, + [hasFocusSignal, element], + ) + + const collection = createCollection() + const groupRef = useRef(null) + const node = useFlexNode(groupRef) + useImmediateProperties(collection, node, flexAliasPropertyTransformation) + const transformMatrix = useTransformMatrix(collection, node) + const rootGroupRef = useRootGroupRef() + const globalMatrix = useGlobalMatrix(transformMatrix) + const parentClippingRect = useParentClippingRect() + const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) + useLayoutListeners(properties, node.size) + useViewportListeners(properties, isClipped) + const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) + const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) + useInstancedPanel( + collection, + globalMatrix, + node.size, + undefined, + node.borderInset, + isClipped, + backgroundOrderInfo, + parentClippingRect, + groupDeps, + panelAliasPropertyTransformation, + ) + const selectionBoxes = useMemo(() => signal([]), []) + const caretPosition = useMemo(() => signal(undefined), []) + const selectionOrderInfo = useSelection( + globalMatrix, + selectionBoxes, + isClipped, + backgroundOrderInfo, + parentClippingRect, + ) + useCaret(collection, globalMatrix, caretPosition, isClipped, backgroundOrderInfo, parentClippingRect) + const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) + const instancedTextRef = useRef() + const measureFunc = useInstancedText( + collection, + value, + globalMatrix, + node, + isClipped, + parentClippingRect, + selectionOrderInfo, + selectionRange, + selectionBoxes, + caretPosition, + instancedTextRef, + ) + + const disabled = properties.disabled ?? false + + useApplyProperties(collection, properties) + useApplyPreferredColorSchemeProperties(collection, properties) + useApplyResponsiveProperties(collection, properties) + const hoverHandlers = useApplyHoverProperties(collection, properties, disabled ? undefined : 'text') + const activeHandlers = useApplyActiveProperties(collection, properties) + useApplyFocusProperties(collection, properties, hasFocusSignal) + writeCollection(collection, 'measureFunc', measureFunc) + finalizeCollection(collection) + + useImperativeHandle( + ref, + () => ({ + focus: () => setFocus(true), value, - globalMatrix, - node, - isClipped, - parentClippingRect, - selectionOrderInfo, - selectionRange, - selectionBoxes, - caretPosition, - instancedTextRef, - ) - - const disabled = properties.disabled ?? false - - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties, disabled ? undefined : 'text') - const activeHandlers = useApplyActiveProperties(collection, properties) - useApplyFocusProperties(collection, properties, hasFocusSignal) - writeCollection(collection, 'measureFunc', measureFunc) - finalizeCollection(collection) - - useImperativeHandle( - ref, - () => ({ - focus: () => setFocus(true), - value, - borderInset: node.borderInset, - paddingInset: node.paddingInset, - pixelSize: node.pixelSize, - center: node.relativeCenter, - size: node.size, - interactionPanel, - }), - [interactionPanel, node, value, setFocus], - ) - - return ( - { - if (e.defaultPrevented || e.uv == null || instancedTextRef.current == null) { - return - } - cancelBlur(e.nativeEvent) - e.stopPropagation() - const charIndex = uvToCharIndex(node, e.uv, instancedTextRef.current) - startCharIndex.current = charIndex - - setTimeout(() => { - setFocus(true) - selectionRange.value = [charIndex, charIndex] - element.setSelectionRange(charIndex, charIndex) - }) - }, - onPointerUp: (e) => { - startCharIndex.current = undefined - }, - onPointerLeave: (e) => { - startCharIndex.current = undefined - }, - onPointerMove: (e) => { - if (startCharIndex.current == null || e.uv == null || instancedTextRef.current == null) { - return - } - e.stopPropagation() - const charIndex = uvToCharIndex(node, e.uv, instancedTextRef.current) - - const start = Math.min(startCharIndex.current, charIndex) - const end = Math.max(startCharIndex.current, charIndex) - const direction = startCharIndex.current < charIndex ? 'forward' : 'backward' - - setTimeout(() => { - setFocus(true) - selectionRange.value = [start, end] - element.setSelectionRange(start, end, direction) - }) - }, - } - } - hoverHandlers={hoverHandlers} - activeHandlers={activeHandlers} - > - - - ) - }) - - export function useHtmlInputElement( - value: string | Signal, - selectionRange: Signal, - onChange?: (value: string) => void, - multiline: boolean = false, - ): HTMLInputElement | HTMLTextAreaElement { - const element = useMemo(() => { - const result = document.createElement(multiline ? 'textarea' : 'input') - const style = result.style - style.setProperty('position', 'absolute') - style.setProperty('left', '-1000vw') - style.setProperty('pointerEvents', 'none') - style.setProperty('opacity', '0') - result.addEventListener('input', () => { - onChange?.(result.value) - updateSelection() - }) - const updateSelection = () => { - const { selectionStart, selectionEnd } = result - if (selectionStart == null || selectionEnd == null) { - selectionRange.value = undefined - return - } - const current = selectionRange.peek() - if (current != null && current[0] === selectionStart && current[1] === selectionEnd) { - return - } - selectionRange.value = [selectionStart, selectionEnd] + borderInset: node.borderInset, + paddingInset: node.paddingInset, + pixelSize: node.pixelSize, + center: node.relativeCenter, + size: node.size, + interactionPanel, + }), + [interactionPanel, node, value, setFocus], + ) + + return ( + { + if (e.defaultPrevented || e.uv == null || instancedTextRef.current == null) { + return + } + cancelBlur(e.nativeEvent) + e.stopPropagation() + const charIndex = uvToCharIndex(node, e.uv, instancedTextRef.current) + startCharIndex.current = charIndex + + setTimeout(() => { + setFocus(true) + selectionRange.value = [charIndex, charIndex] + element.setSelectionRange(charIndex, charIndex) + }) + }, + onPointerUp: (e) => { + startCharIndex.current = undefined + }, + onPointerLeave: (e) => { + startCharIndex.current = undefined + }, + onPointerMove: (e) => { + if (startCharIndex.current == null || e.uv == null || instancedTextRef.current == null) { + return + } + e.stopPropagation() + const charIndex = uvToCharIndex(node, e.uv, instancedTextRef.current) + + const start = Math.min(startCharIndex.current, charIndex) + const end = Math.max(startCharIndex.current, charIndex) + const direction = startCharIndex.current < charIndex ? 'forward' : 'backward' + + setTimeout(() => { + setFocus(true) + selectionRange.value = [start, end] + element.setSelectionRange(start, end, direction) + }) + }, + } + } + hoverHandlers={hoverHandlers} + activeHandlers={activeHandlers} + > + + + ) +}) + +export function useHtmlInputElement( + value: string | Signal, + selectionRange: Signal, + onChange?: (value: string) => void, + multiline: boolean = false, +): HTMLInputElement | HTMLTextAreaElement { + const element = useMemo(() => { + const result = document.createElement(multiline ? 'textarea' : 'input') + const style = result.style + style.setProperty('position', 'absolute') + style.setProperty('left', '-1000vw') + style.setProperty('pointerEvents', 'none') + style.setProperty('opacity', '0') + result.addEventListener('input', () => { + onChange?.(result.value) + updateSelection() + }) + const updateSelection = () => { + const { selectionStart, selectionEnd } = result + if (selectionStart == null || selectionEnd == null) { + selectionRange.value = undefined + return + } + const current = selectionRange.peek() + if (current != null && current[0] === selectionStart && current[1] === selectionEnd) { + return } - result.addEventListener('keydown', updateSelection) - result.addEventListener('keyup', updateSelection) - result.addEventListener('blur', () => (selectionRange.value = undefined)) - document.body.appendChild(result) - return result - }, [onChange, selectionRange, multiline]) - useSignalEffect(() => { - element.value = readReactive(value) - }, [value]) - useEffect(() => () => element.remove(), [element]) - return element - } - - function uvToCharIndex( - { size, borderInset, paddingInset }: FlexNode, - uv: Vector2, - instancedText: InstancedText, - ): number { - const [width, height] = size.peek() - const [bTop, , , bLeft] = borderInset.peek() - const [pTop, , , pLeft] = paddingInset.peek() - const x = uv.x * width - bLeft - pLeft - const y = -uv.y * height + bTop + pTop - return instancedText.getCharIndex(x, y) - } - \ No newline at end of file + selectionRange.value = [selectionStart, selectionEnd] + } + result.addEventListener('keydown', updateSelection) + result.addEventListener('keyup', updateSelection) + result.addEventListener('blur', () => (selectionRange.value = undefined)) + document.body.appendChild(result) + return result + }, [onChange, selectionRange, multiline]) + useSignalEffect(() => { + element.value = readReactive(value) + }, [value]) + useEffect(() => () => element.remove(), [element]) + return element +} + +function uvToCharIndex( + { size, borderInset, paddingInset }: FlexNode, + uv: Vector2, + instancedText: InstancedText, +): number { + const [width, height] = size.peek() + const [bTop, , , bLeft] = borderInset.peek() + const [pTop, , , pLeft] = paddingInset.peek() + const x = uv.x * width - bLeft - pLeft + const y = -uv.y * height + bTop + pTop + return instancedText.getCharIndex(x, y) +} diff --git a/packages/react/src/ref.ts b/packages/react/src/ref.ts index fab9cdc7..23935060 100644 --- a/packages/react/src/ref.ts +++ b/packages/react/src/ref.ts @@ -1,33 +1,33 @@ +import { ReadonlySignal, Signal } from '@preact/signals-core' +import { Inset, FlexNode } from '@vanilla-three/uikit/internals' +import { utils } from 'mocha' +import { ForwardedRef, RefObject, useImperativeHandle } from 'react' +import { Vector2Tuple, Mesh } from 'three' export type ComponentInternals = { - pixelSize: number - size: ReadonlySignal - center: ReadonlySignal - borderInset: ReadonlySignal - paddingInset: ReadonlySignal - scrollPosition?: Signal - maxScrollPosition?: Signal> - interactionPanel: Mesh - } - - export function useComponentInternals( - ref: ForwardedRef, - node: FlexNode, - interactionPanel: Mesh | RefObject, - scrollPosition?: Signal, - ): void { - useImperativeHandle( - ref, - () => ({ - borderInset: node.borderInset, - paddingInset: node.paddingInset, - pixelSize: node.pixelSize, - center: node.relativeCenter, - maxScrollPosition: node.maxScrollPosition, - size: node.size, - interactionPanel: interactionPanel instanceof Mesh ? interactionPanel : interactionPanel.current!, - scrollPosition, - >>>>>>> main:packages/uikit/src/components/utils.tsx - }), - ) - } \ No newline at end of file + pixelSize: number + size: ReadonlySignal + center: ReadonlySignal + borderInset: ReadonlySignal + paddingInset: ReadonlySignal + scrollPosition?: Signal + maxScrollPosition?: Signal> + interactionPanel: Mesh +} + +export function useComponentInternals( + ref: ForwardedRef, + node: FlexNode, + interactionPanel: Mesh | RefObject, + scrollPosition?: Signal, +): void { + useImperativeHandle(ref, () => ({ + borderInset: node.borderInset, + paddingInset: node.paddingInset, + center: node.relativeCenter, + maxScrollPosition: node.maxScrollPosition, + size: node.size, + interactionPanel: interactionPanel instanceof Mesh ? interactionPanel : interactionPanel.current!, + scrollPosition, + })) +} diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx index fd025e8e..28edf5fa 100644 --- a/packages/react/src/root.tsx +++ b/packages/react/src/root.tsx @@ -1,10 +1,10 @@ import { useFrame, useStore, useThree } from '@react-three/fiber' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' import { forwardRef, ReactNode, useEffect, useMemo, useRef } from 'react' -import { ParentProvider } from './context' -import { AddHandlers, AddScrollHandler } from './utilts' +import { ParentProvider } from './context.js' +import { AddHandlers, AddScrollHandler } from './utilts.js' import { RootProperties, patchRenderOrder, createRoot, destroyRoot } from '@vanilla-three/uikit/internals' -import { useDefaultProperties } from './default' +import { useDefaultProperties } from './default.js' import { Object3D } from 'three' export const Root: ( diff --git a/packages/uikit/src/allocation/index.ts b/packages/uikit/src/allocation/index.ts index 6c07d138..16a36642 100644 --- a/packages/uikit/src/allocation/index.ts +++ b/packages/uikit/src/allocation/index.ts @@ -1 +1 @@ -export * from './sorted-buckets' +export * from './sorted-buckets.js' diff --git a/packages/uikit/src/caret.ts b/packages/uikit/src/caret.ts index 0f78af12..273120c1 100644 --- a/packages/uikit/src/caret.ts +++ b/packages/uikit/src/caret.ts @@ -1,90 +1,85 @@ import { Signal, computed, effect, signal } from '@preact/signals-core' import { Matrix4, Vector3Tuple } from 'three' import { ClippingRect } from './clipping.js' -import { ElementType, OrderInfo, useOrderInfo } from './order.js' -import { GetInstancedPanelGroup, useGetInstancedPanelGroup, usePanelGroupDependencies } from './panel/react.js' -import { useEffect, useMemo } from 'react' -import { InstancedPanel } from './panel/instanced-panel.js' +import { ElementType, OrderInfo, computedOrderInfo } from './order.js' import { Inset } from './flex/index.js' -import { ManagerCollection, PropertyTransformation } from './properties/utils.js' -import { useImmediateProperties } from './properties/immediate.js' -import { useBatchedProperties } from './properties/batched.js' +import { createInstancedPanel } from './panel/instanced-panel.js' +import { + MergedProperties, + PanelGroupManager, + PanelMaterialConfig, + Subscriptions, + createPanelMaterialConfig, +} from './internals.js' const noBorder = signal([0, 0, 0, 0]) const CARET_WIDTH = 1.5 -const caretPropertyTransformation: PropertyTransformation = (key, value, hasProperty, setProperty) => { - if (key != 'color') { - return - } - setProperty('backgroundColor', value) +let caretMaterialConfig: PanelMaterialConfig | undefined +function getCaretMaterialConfig() { + caretMaterialConfig ??= createPanelMaterialConfig( + { + backgroundColor: 'color', + backgroundOpacity: 'opacity', + }, + { + backgroundColor: 0xffffff, + }, + ) + return caretMaterialConfig } export function useCaret( - collection: ManagerCollection, + propertiesSignal: Signal, matrix: Signal, caretPosition: Signal, isHidden: Signal | undefined, - parentOrderInfo: OrderInfo, + parentOrderInfo: Signal, parentClippingRect: Signal | undefined, - providedGetGroup?: GetInstancedPanelGroup, + panelGroupManager: PanelGroupManager, + subscriptions: Subscriptions, ) { - // eslint-disable-next-line react-hooks/rules-of-hooks - const getGroup = providedGetGroup ?? useGetInstancedPanelGroup() - const groupDeps = usePanelGroupDependencies(undefined, { castShadow: false, receiveShadow: false }) - const orderInfo = useOrderInfo(ElementType.Panel, undefined, groupDeps, parentOrderInfo) - const blinkingCaretPosition = useMemo(() => signal(undefined), []) - const unsubscribeBlink = useMemo( - () => - effect(() => { - const pos = caretPosition.value - if (pos == null) { - blinkingCaretPosition.value = undefined - } - blinkingCaretPosition.value = pos - const ref = setInterval( - () => (blinkingCaretPosition.value = blinkingCaretPosition.peek() == null ? pos : undefined), - 500, - ) - return () => clearInterval(ref) - }), - [blinkingCaretPosition, caretPosition], + const orderInfo = computedOrderInfo(undefined, ElementType.Panel, undefined, parentOrderInfo) + const blinkingCaretPosition = signal(undefined) + subscriptions.push( + effect(() => { + const pos = caretPosition.value + if (pos == null) { + blinkingCaretPosition.value = undefined + } + blinkingCaretPosition.value = pos + const ref = setInterval( + () => (blinkingCaretPosition.value = blinkingCaretPosition.peek() == null ? pos : undefined), + 500, + ) + return () => clearInterval(ref) + }), ) - useEffect(() => unsubscribeBlink, [unsubscribeBlink]) - const panel = useMemo( - () => - new InstancedPanel( - getGroup(orderInfo.majorIndex, groupDeps), - matrix, - computed(() => { - const size = blinkingCaretPosition.value - if (size == null) { - return [0, 0] - } - return [CARET_WIDTH, size[2]] - }), - computed(() => { - const position = blinkingCaretPosition.value - if (position == null) { - return [0, 0] - } - return [position[0] - CARET_WIDTH / 2, position[1]] - }), - noBorder, - parentClippingRect, - isHidden, - orderInfo.minorIndex, - ), - [getGroup, orderInfo, groupDeps, matrix, parentClippingRect, isHidden, blinkingCaretPosition], + createInstancedPanel( + propertiesSignal, + orderInfo, + undefined, + panelGroupManager, + matrix, + computed(() => { + const size = blinkingCaretPosition.value + if (size == null) { + return [0, 0] + } + return [CARET_WIDTH, size[2]] + }), + computed(() => { + const position = blinkingCaretPosition.value + if (position == null) { + return [0, 0] + } + return [position[0] - CARET_WIDTH / 2, position[1]] + }), + noBorder, + parentClippingRect, + isHidden, + getCaretMaterialConfig(), + subscriptions, ) - const startIndex = collection.length - useImmediateProperties(collection, panel, caretPropertyTransformation) - useBatchedProperties(collection, panel, caretPropertyTransformation) - //setting default color to text default color (0xffffff) - const collectionLength = collection.length - for (let i = startIndex; i < collectionLength; i++) { - collection[i].add('color', 0xffffff) - } - useEffect(() => () => panel.destroy(), [panel]) } diff --git a/packages/uikit/src/clipping.ts b/packages/uikit/src/clipping.ts index 51e5a7ac..e440fc91 100644 --- a/packages/uikit/src/clipping.ts +++ b/packages/uikit/src/clipping.ts @@ -2,7 +2,7 @@ import { Signal, computed } from '@preact/signals-core' import { Group, Matrix4, Plane, Vector3 } from 'three' import type { Vector2Tuple } from 'three' import { Inset } from './flex/node.js' -import { Overflow } from 'yoga-layout/wasm-async' +import { Overflow } from 'yoga-layout' import { Object3DRef } from './context.js' const dotLt45deg = Math.cos((45 / 180) * Math.PI) @@ -94,7 +94,7 @@ const multiplier = [ [-0.5, 0.5], ] -export function computeIsClipped( +export function computedIsClipped( parentClippingRect: Signal | undefined, globalMatrix: Signal, size: Signal, @@ -132,7 +132,7 @@ export function computeIsClipped( }) } -export function computeClippingRect( +export function computedClippingRect( globalMatrix: Signal, size: Signal, borderInset: Signal, diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index fe266faf..8d9d1de0 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -1,33 +1,32 @@ import { YogaProperties } from '../flex/node.js' import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' -import { computeIsClipped, computeClippingRect } from '../clipping.js' +import { computedIsClipped, computedClippingRect } from '../clipping.js' import { ScrollbarProperties, applyScrollPosition, - computeGlobalScrollMatrix, + computedGlobalScrollMatrix, createScrollPosition, createScrollbars, setupScrollHandler, } from '../scroll.js' import { WithAllAliases } from '../properties/alias.js' import { PanelProperties, createInstancedPanel } from '../panel/instanced-panel.js' -import { TransformProperties, applyTransform, computeTransformMatrix } from '../transform.js' -import { AllOptionalProperties, Properties, WithClasses, WithReactive } from '../properties/default.js' +import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' +import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' import { createResponsivePropertyTransformers } from '../responsive.js' -import { ElementType, ZIndexOffset, computeOrderInfo } from '../order.js' +import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js' import { preferredColorSchemePropertyTransformers } from '../dark.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { Signal, computed, signal } from '@preact/signals-core' -import { WithConditionals, computeGlobalMatrix } from './utils.js' +import { computed, signal } from '@preact/signals-core' +import { WithConditionals, computedGlobalMatrix } from './utils.js' import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' import { MergedProperties } from '../properties/merged.js' import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' import { Object3DRef, WithContext } from '../context.js' -import { ShadowProperties, computePanelGroupDependencies } from '../panel/instanced-panel-group.js' +import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' -import { MaterialClass } from '../panel/panel-material.js' -import { Vector2Tuple } from 'three' import { EventHandlers } from '../events.js' +import { getDefaultPanelMaterialConfig } from '../internals.js' export type InheritableContainerProperties = WithConditionals< WithClasses< @@ -35,11 +34,10 @@ export type InheritableContainerProperties = WithConditionals< WithReactive< YogaProperties & PanelProperties & - TransformProperties & { - zIndexOffset?: ZIndexOffset - panelMaterialClass?: MaterialClass - } & ScrollbarProperties & - ShadowProperties + ZIndexProperties & + TransformProperties & + ScrollbarProperties & + PanelGroupProperties > > > @@ -79,15 +77,15 @@ export function createContainer( const node = parentContext.node.createChild(mergedProperties, object, subscriptions) parentContext.node.addChild(node) - const transformMatrix = computeTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) + const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) applyTransform(object, transformMatrix, subscriptions) - const globalMatrix = computeGlobalMatrix(parentContext.matrix, transformMatrix) + const globalMatrix = computedGlobalMatrix(parentContext.matrix, transformMatrix) - const isClipped = computeIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) - const groupDeps = computePanelGroupDependencies(mergedProperties) + const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) + const groupDeps = computedPanelGroupDependencies(mergedProperties) - const orderInfo = computeOrderInfo(mergedProperties, ElementType.Panel, groupDeps, parentContext.orderInfo) + const orderInfo = computedOrderInfo(mergedProperties, ElementType.Panel, groupDeps, parentContext.orderInfo) createInstancedPanel( mergedProperties, @@ -100,12 +98,13 @@ export function createContainer( node.borderInset, parentContext.clippingRect, isClipped, + getDefaultPanelMaterialConfig(), subscriptions, ) const scrollPosition = createScrollPosition() applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) - const matrix = computeGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) + const matrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) createScrollbars( mergedProperties, scrollPosition, @@ -118,7 +117,7 @@ export function createContainer( subscriptions, ) - const clippingRect = computeClippingRect( + const clippingRect = computedClippingRect( globalMatrix, node.size, node.borderInset, diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index 929c55ff..aec5a252 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -9,52 +9,44 @@ import { TextureLoader, Vector2Tuple, } from 'three' -import { Listeners } from '..' -import { Object3DRef, WithContext } from '../context' -import { Inset, YogaProperties } from '../flex' -import { ElementType, ZIndexOffset, computeOrderInfo, setupRenderOrder } from '../order' -import { PanelProperties } from '../panel/instanced-panel' -import { ShadowProperties } from '../panel/instanced-panel-group' -import { - MaterialClass, - PanelDepthMaterial, - PanelDistanceMaterial, - applyPropsToMaterialData, - createPanelMaterial, - panelMaterialDefaultData, - setupPanelMaterials, -} from '../panel/panel-material' -import { WithAllAliases } from '../properties/alias' -import { AllOptionalProperties, Properties, WithClasses, WithReactive } from '../properties/default' +import { Listeners } from '../index.js' +import { Object3DRef, WithContext } from '../context.js' +import { Inset, YogaProperties } from '../flex/index.js' +import { ElementType, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' +import { PanelProperties } from '../panel/instanced-panel.js' +import { PanelDepthMaterial, PanelDistanceMaterial, createPanelMaterial } from '../panel/panel-material.js' +import { WithAllAliases } from '../properties/alias.js' +import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' import { ScrollbarProperties, applyScrollPosition, - computeGlobalScrollMatrix, + computedGlobalScrollMatrix, createScrollPosition, createScrollbars, setupScrollHandler, -} from '../scroll' -import { TransformProperties, applyTransform, computeTransformMatrix } from '../transform' -import { WithConditionals, computeGlobalMatrix, loadResourceWithParams } from './utils' -import { MergedProperties, PropertyTransformers } from '../properties/merged' -import { Subscriptions, readReactive, unsubscribeSubscriptions } from '../utils' -import { computeIsPanelVisible, panelGeometry } from '../panel/utils' -import { setupImmediateProperties } from '../properties/immediate' -import { makeClippedRaycast, makePanelRaycast } from '../panel/interaction-panel-mesh' +} from '../scroll.js' +import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' +import { WithConditionals, computedGlobalMatrix, loadResourceWithParams } from './utils.js' +import { MergedProperties, PropertyTransformers } from '../properties/merged.js' +import { Subscriptions, readReactive, unsubscribeSubscriptions } from '../utils.js' +import { panelGeometry } from '../panel/utils.js' +import { setupImmediateProperties } from '../properties/immediate.js' +import { makeClippedRaycast, makePanelRaycast } from '../panel/interaction-panel-mesh.js' import { - computeIsClipped, - computeClippingRect, + computedIsClipped, + computedClippingRect, createGlobalClippingPlanes, updateGlobalClippingPlanes, -} from '../clipping' -import { setupLayoutListeners, setupViewportListeners } from '../listeners' -import { createGetBatchedProperties } from '../properties/batched' -import { addActiveHandlers, createActivePropertyTransfomers } from '../active' -import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover' -import { cloneHandlers } from '../panel/instanced-panel-mesh' -import { preferredColorSchemePropertyTransformers } from '../dark' -import { createResponsivePropertyTransformers } from '../responsive' -import { EventHandlers } from '../events' +} from '../clipping.js' +import { setupLayoutListeners, setupViewportListeners } from '../listeners.js' +import { createGetBatchedProperties } from '../properties/batched.js' +import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' +import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { cloneHandlers } from '../panel/instanced-panel-mesh.js' +import { preferredColorSchemePropertyTransformers } from '../dark.js' +import { createResponsivePropertyTransformers } from '../responsive.js' +import { EventHandlers } from '../events.js' +import { PanelGroupProperties, PanelMaterialConfig, createPanelMaterialConfig } from '../internals.js' export type ImageFit = 'cover' | 'fill' const FIT_DEFAULT: ImageFit = 'fill' @@ -64,14 +56,13 @@ export type InheritableImageProperties = WithConditionals< WithAllAliases< WithReactive< YogaProperties & + ZIndexProperties & Omit & { opacity?: number fit?: ImageFit - panelMaterialClass?: MaterialClass - zIndexOffset?: ZIndexOffset keepAspectRatio?: boolean } & TransformProperties & - ShadowProperties + PanelGroupProperties > & ScrollbarProperties > @@ -80,8 +71,6 @@ export type InheritableImageProperties = WithConditionals< export type ImageProperties = InheritableImageProperties & Listeners & EventHandlers & { src: Signal | string } -const shadowProperties = ['castShadow', 'receiveShadow'] - export function createImage( parentContext: WithContext, properties: ImageProperties, @@ -134,19 +123,19 @@ export function createImage( const node = parentContext.node.createChild(mergedProperties, object, subscriptions) parentContext.node.addChild(node) - const transformMatrix = computeTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) + const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) applyTransform(object, transformMatrix, subscriptions) - const globalMatrix = computeGlobalMatrix(parentContext.matrix, transformMatrix) + const globalMatrix = computedGlobalMatrix(parentContext.matrix, transformMatrix) - const isClipped = computeIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) + const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) const isHidden = computed(() => isClipped.value || texture.value == null) - const orderInfo = computeOrderInfo(mergedProperties, ElementType.Image, undefined, parentContext.orderInfo) + const orderInfo = computedOrderInfo(mergedProperties, ElementType.Image, undefined, parentContext.orderInfo) const scrollPosition = createScrollPosition() applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) - const matrix = computeGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) + const matrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) createScrollbars( mergedProperties, scrollPosition, @@ -159,7 +148,7 @@ export function createImage( subscriptions, ) - const clippingRect = computeClippingRect( + const clippingRect = computedClippingRect( globalMatrix, node.size, node.borderInset, @@ -215,6 +204,26 @@ export function destroyImage(internals: ReturnType) { unsubscribeSubscriptions(internals.subscriptions) } +let imageMaterialConfig: PanelMaterialConfig | undefined +function getImageMaterialConfig() { + imageMaterialConfig ??= createPanelMaterialConfig( + { + borderBend: 'borderBend', + borderBottomLeftRadius: 'borderBottomLeftRadius', + borderBottomRightRadius: 'borderBottomRightRadius', + borderColor: 'borderColor', + borderOpacity: 'borderOpacity', + borderTopLeftRadius: 'borderTopLeftRadius', + borderTopRightRadius: 'borderTopRightRadius', + backgroundOpacity: 'opacity', + }, + { + backgroundColor: 0xffffff, + }, + ) + return imageMaterialConfig +} + function createImageMesh( propertiesSignal: Signal, texture: Signal, @@ -229,15 +238,8 @@ function createImageMesh( const updateClippingPlanes = () => updateGlobalClippingPlanes(clippingRect, root.object, clippingPlanes) root.onFrameSet.add(updateClippingPlanes) subscriptions.push(() => root.onFrameSet.delete(updateClippingPlanes)) - setupPanelMaterials(propertiesSignal, mesh, node.size, node.borderInset, isHidden, clippingPlanes, subscriptions) - const isVisible = computeIsPanelVisible(propertiesSignal, node.borderInset, node.size, isHidden, 0xffffff) - setupImmediateProperties( - propertiesSignal, - isVisible, - (key) => shadowProperties.includes(key), - (key, value) => (mesh[key as 'castShadow' | 'receiveShadow'] = (value as boolean | undefined) ?? false), - subscriptions, - ) + const isVisible = getImageMaterialConfig().computedIsVisibile(propertiesSignal, node.borderInset, node.size, isHidden) + setupImageMaterials(propertiesSignal, mesh, node.size, node.borderInset, isVisible, clippingPlanes, subscriptions) mesh.raycast = makeClippedRaycast(mesh, makePanelRaycast(mesh), root.object, parent.clippingRect, orderInfo) subscriptions.push(effect(() => setupRenderOrder(mesh, root, orderInfo.value))) @@ -344,25 +346,16 @@ async function loadTextureImpl(src?: string | Texture) { } } -const panelMaterialClassKey = ['panelMaterialClass'] - -//TODO: rename setter: opacity => backgroundOpacity and remove backgroundColor -//TODO: allow providing own default material data - -const imageMaterialDefaultData = [...panelMaterialDefaultData] -imageMaterialDefaultData[4] = 1 -imageMaterialDefaultData[5] = 1 -imageMaterialDefaultData[6] = 1 +const panelMaterialClassKey = ['panelMaterialClass'] as const -export function setupPanelMaterials( +function setupImageMaterials( propertiesSignal: Signal, target: Mesh, size: Signal, borderInset: Signal, - isClipped: Signal, + isVisible: Signal, clippingPlanes: Array, subscriptions: Subscriptions, - renameOutput?: Record, ) { const data = new Float32Array(16) const info = { data: data, type: 'normal' } as const @@ -371,13 +364,39 @@ export function setupPanelMaterials( target.customDepthMaterial.clippingPlanes = clippingPlanes target.customDistanceMaterial.clippingPlanes = clippingPlanes - const get = createGetBatchedProperties(propertiesSignal, panelMaterialClassKey) + const get = createGetBatchedProperties(propertiesSignal, panelMaterialClassKey) subscriptions.push( effect(() => { - const materialClass = get('panelMaterialClass') as MaterialClass | undefined - target.material = createPanelMaterial(materialClass ?? MeshBasicMaterial, info) + target.material = createPanelMaterial(get('panelMaterialClass') ?? MeshBasicMaterial, info) target.material.clippingPlanes = clippingPlanes }), + effect(() => (target.castShadow = get('castShadow') ?? false)), + effect(() => (target.receiveShadow = get('receiveShadow') ?? false)), + ) + + const imageMaterialConfig = getImageMaterialConfig() + const internalSubscriptions: Array<() => void> = [] + subscriptions.push( + effect(() => { + if (!isVisible.value) { + return + } + + data.set(imageMaterialConfig.defaultData) + + internalSubscriptions.push( + effect(() => data.set(size.value, 13)), + effect(() => data.set(borderInset.value, 0)), + ) + return () => unsubscribeSubscriptions(internalSubscriptions) + }), + ) + const setters = imageMaterialConfig.setters + setupImmediateProperties( + propertiesSignal, + isVisible, + imageMaterialConfig.hasProperty, + (key, value) => setters[key](data, 0, value as any, size, undefined), + subscriptions, ) - applyPropsToMaterialData(propertiesSignal, data, size, borderInset, isClipped, [], subscriptions, renameOutput) } diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/components/index.ts index 6d6ab319..83b92f53 100644 --- a/packages/uikit/src/components/index.ts +++ b/packages/uikit/src/components/index.ts @@ -1,4 +1,4 @@ -export * from './container' -export * from './image' -export * from './root' -export * from './utils' +export * from './container.js' +export * from './image.js' +export * from './root.js' +export * from './utils.js' diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index db874375..9526bae0 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -1,35 +1,39 @@ import { Signal, computed, signal } from '@preact/signals-core' -import { Object3DRef, RootContext, WithContext } from '../context' -import { FlexNode, YogaProperties } from '../flex' -import { LayoutListeners, Listeners, ScrollListeners, setupLayoutListeners } from '../listeners' -import { PanelProperties, createInstancedPanel } from '../panel/instanced-panel' -import { PanelGroupManager, ShadowProperties, computePanelGroupDependencies } from '../panel/instanced-panel-group' -import { MaterialClass } from '../panel/panel-material' -import { WithAllAliases } from '../properties/alias' -import { AllOptionalProperties, Properties, WithClasses, WithReactive } from '../properties/default' -import { MergedProperties, PropertyTransformers } from '../properties/merged' +import { Object3DRef, RootContext } from '../context.js' +import { FlexNode, YogaProperties } from '../flex/index.js' +import { LayoutListeners, ScrollListeners, setupLayoutListeners } from '../listeners.js' +import { PanelProperties, createInstancedPanel } from '../panel/instanced-panel.js' +import { + PanelGroupManager, + PanelGroupProperties, + computedPanelGroupDependencies, +} from '../panel/instanced-panel-group.js' +import { WithAllAliases } from '../properties/alias.js' +import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' +import { MergedProperties, PropertyTransformers } from '../properties/merged.js' import { ScrollbarProperties, applyScrollPosition, - computeGlobalScrollMatrix, + computedGlobalScrollMatrix, createScrollPosition, createScrollbars, setupScrollHandler, -} from '../scroll' -import { TransformProperties, applyTransform, computeTransformMatrix } from '../transform' -import { Subscriptions, alignmentXMap, alignmentYMap, loadYoga, readReactive, unsubscribeSubscriptions } from '../utils' -import { WithConditionals } from './utils' -import { computeClippingRect } from '../clipping' -import { computeOrderInfo, ElementType, WithCameraDistance } from '../order' +} from '../scroll.js' +import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' +import { Subscriptions, alignmentXMap, alignmentYMap, readReactive, unsubscribeSubscriptions } from '../utils.js' +import { WithConditionals } from './utils.js' +import { computedClippingRect } from '../clipping.js' +import { computedOrderInfo, ElementType, WithCameraDistance } from '../order.js' import { Camera, Matrix4, Plane, Vector2Tuple, Vector3 } from 'three' -import { GlyphGroupManager } from '../text/render/instanced-glyph-group' -import { createGetBatchedProperties } from '../properties/batched' -import { addActiveHandlers, createActivePropertyTransfomers } from '../active' -import { preferredColorSchemePropertyTransformers } from '../dark' -import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover' -import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh' -import { createResponsivePropertyTransformers } from '../responsive' -import { EventHandlers } from '../events' +import { GlyphGroupManager } from '../text/render/instanced-glyph-group.js' +import { createGetBatchedProperties } from '../properties/batched.js' +import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' +import { preferredColorSchemePropertyTransformers } from '../dark.js' +import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { createResponsivePropertyTransformers } from '../responsive.js' +import { EventHandlers } from '../events.js' +import { getDefaultPanelMaterialConfig } from '../internals.js' export type InheritableRootProperties = WithConditionals< WithClasses< @@ -39,8 +43,7 @@ export type InheritableRootProperties = WithConditionals< TransformProperties & PanelProperties & ScrollbarProperties & - ShadowProperties & { - panelMaterialClass?: MaterialClass + PanelGroupProperties & { sizeX?: number sizeY?: number anchorX?: keyof typeof alignmentXMap @@ -99,25 +102,16 @@ export function createRoot( }) const requestCalculateLayout = createDeferredRequestLayoutCalculation(onFrameSet, subscriptions) - const node = new FlexNode( - mergedProperties, - rootSize, - object, - loadYoga(), - 0.01, - requestCalculateLayout, - undefined, - subscriptions, - ) + const node = new FlexNode(mergedProperties, rootSize, object, requestCalculateLayout, undefined, subscriptions) subscriptions.push(() => node.destroy()) - const transformMatrix = computeTransformMatrix(mergedProperties, node, pixelSize) - const rootMatrix = computeRootMatrix(mergedProperties, transformMatrix, node.size, pixelSize) + const transformMatrix = computedTransformMatrix(mergedProperties, node, pixelSize) + const rootMatrix = computedRootMatrix(mergedProperties, transformMatrix, node.size, pixelSize) applyTransform(object, transformMatrix, subscriptions) - const groupDeps = computePanelGroupDependencies(mergedProperties) + const groupDeps = computedPanelGroupDependencies(mergedProperties) - const orderInfo = computeOrderInfo(mergedProperties, ElementType.Panel, groupDeps, undefined) + const orderInfo = computedOrderInfo(undefined, ElementType.Panel, groupDeps, undefined) const ctx: WithCameraDistance = { cameraDistance: 0 } @@ -150,12 +144,13 @@ export function createRoot( node.borderInset, undefined, undefined, + getDefaultPanelMaterialConfig(), subscriptions, ) const scrollPosition = createScrollPosition() applyScrollPosition(childrenContainer, scrollPosition, pixelSize) - const matrix = computeGlobalScrollMatrix(scrollPosition, rootMatrix, pixelSize) + const matrix = computedGlobalScrollMatrix(scrollPosition, rootMatrix, pixelSize) createScrollbars( mergedProperties, scrollPosition, @@ -168,7 +163,14 @@ export function createRoot( subscriptions, ) - const clippingRect = computeClippingRect(rootMatrix, node.size, node.borderInset, node.overflow, pixelSize, undefined) + const clippingRect = computedClippingRect( + rootMatrix, + node.size, + node.borderInset, + node.overflow, + pixelSize, + undefined, + ) setupLayoutListeners(propertiesSignal, node.size, subscriptions) @@ -269,7 +271,7 @@ const matrixHelper = new Matrix4() const keys = ['anchorX', 'anchorY'] -function computeRootMatrix( +function computedRootMatrix( propertiesSignal: Signal, matrix: Signal, size: Signal, diff --git a/packages/uikit/src/components/utils.tsx b/packages/uikit/src/components/utils.tsx index d207b5f1..17948061 100644 --- a/packages/uikit/src/components/utils.tsx +++ b/packages/uikit/src/components/utils.tsx @@ -1,12 +1,12 @@ import { Signal, computed, effect, signal } from '@preact/signals-core' import { Matrix4 } from 'three' -import { WithActive } from '../active' -import { WithPreferredColorScheme } from '../dark' -import { WithHover } from '../hover' -import { WithResponsive } from '../responsive' -import { Subscriptions } from '../utils' +import { WithActive } from '../active.js' +import { WithPreferredColorScheme } from '../dark.js' +import { WithHover } from '../hover.js' +import { WithResponsive } from '../responsive.js' +import { Subscriptions } from '../utils.js' -export function computeGlobalMatrix( +export function computedGlobalMatrix( parentMatrix: Signal, localMatrix: Signal, ): Signal { diff --git a/packages/uikit/src/context.ts b/packages/uikit/src/context.ts index 537fe32d..415bd8cf 100644 --- a/packages/uikit/src/context.ts +++ b/packages/uikit/src/context.ts @@ -1,10 +1,10 @@ import { Signal } from '@preact/signals-core' -import { FlexNode } from './flex' +import { FlexNode } from './flex/index.js' import { Matrix4, Object3D } from 'three' -import { ClippingRect } from './clipping' -import { OrderInfo, WithCameraDistance } from './order' -import { GlyphGroupManager } from './text/render/instanced-glyph-group' -import { PanelGroupManager } from './panel/instanced-panel-group' +import { ClippingRect } from './clipping.js' +import { OrderInfo, WithCameraDistance } from './order.js' +import { GlyphGroupManager } from './text/render/instanced-glyph-group.js' +import { PanelGroupManager } from './panel/instanced-panel-group.js' export type WithContext = ElementContext & Readonly<{ root: RootContext }> diff --git a/packages/uikit/src/flex/node.ts b/packages/uikit/src/flex/node.ts index 926d8b0d..9ea9e167 100644 --- a/packages/uikit/src/flex/node.ts +++ b/packages/uikit/src/flex/node.ts @@ -1,6 +1,6 @@ import { Object3D, Vector2Tuple } from 'three' import { Signal, batch, computed, effect, signal } from '@preact/signals-core' -import { Edge, Node, Yoga, Overflow } from 'yoga-layout' +import Yoga, { Edge, Node, Overflow } from 'yoga-layout' import { setter } from './setter.js' import { Subscriptions } from '../utils.js' import { setupImmediateProperties } from '../properties/immediate.js' @@ -44,7 +44,6 @@ export class FlexNode { propertiesSignal: Signal, public readonly size = signal([0, 0]), private object: Object3DRef, - public readonly yoga: Signal, requestCalculateLayout: (node: FlexNode) => void, public readonly anyAncestorScrollable: Signal<[boolean, boolean]> | undefined, subscriptions: Subscriptions, @@ -96,7 +95,6 @@ export class FlexNode { propertiesSignal, undefined, object, - this.yoga, this.requestCalculateLayout, computed(() => { const [ancestorX, ancestorY] = this.anyAncestorScrollable?.value ?? [false, false] diff --git a/packages/uikit/src/focus.ts b/packages/uikit/src/focus.ts index bc34e310..d7e8bdda 100644 --- a/packages/uikit/src/focus.ts +++ b/packages/uikit/src/focus.ts @@ -1,26 +1,14 @@ -import { useMemo } from 'react' -import { WithClasses, useTraverseProperties } from './properties/default.js' -import { ManagerCollection, Properties } from './properties/utils.js' import { createConditionalPropertyTranslator } from './utils.js' import { Signal } from '@preact/signals-core' +import { PropertyTransformers } from './internals.js' export type WithFocus = T & { focus?: T onFocusChange?: (focus: boolean) => void } -export function useApplyFocusProperties( - collection: ManagerCollection, - properties: WithClasses>, - hasFocusSignal: Signal, -): void { - // eslint-disable-next-line react-hooks/exhaustive-deps - const translate = useMemo(() => createConditionalPropertyTranslator(() => hasFocusSignal.value), [hasFocusSignal]) - - useTraverseProperties(properties, (p) => { - if (p.focus == null) { - return - } - translate(collection, p.focus) - }) +export function createFocusPropertyTransformers(hasFocusSignal: Signal>): PropertyTransformers { + return { + hover: createConditionalPropertyTranslator(() => hasFocusSignal.value.length > 0), + } } diff --git a/packages/uikit/src/internals.ts b/packages/uikit/src/internals.ts index b23a8526..97b39b89 100644 --- a/packages/uikit/src/internals.ts +++ b/packages/uikit/src/internals.ts @@ -1,19 +1,18 @@ -export * from './utils' -export * from './order' -export * from './listeners' -export * from './scroll' -export * from './transform' -export * from './clipping' -export * from './properties/index' -export * from './allocation/index' -export * from './flex/index' -export * from './hover' -export * from './hover' -export * from './dark' -export * from './responsive' -export * from './text/index' -export * from './panel/index' -export * from './components/index' +export * from './properties/index.js' +export * from './utils.js' +export * from './order.js' +export * from './listeners.js' +export * from './transform.js' +export * from './clipping.js' +export * from './allocation/index.js' +export * from './flex/index.js' +export * from './hover.js' +export * from './dark.js' +export * from './responsive.js' +export * from './text/index.js' +export * from './panel/index.js' +export * from './scroll.js' +export * from './components/index.js' export type * from './events' export type * from './context' diff --git a/packages/uikit/src/order.ts b/packages/uikit/src/order.ts index 5d2c06cb..882d92ca 100644 --- a/packages/uikit/src/order.ts +++ b/packages/uikit/src/order.ts @@ -1,7 +1,7 @@ import { Signal, computed } from '@preact/signals-core' import { RenderItem, WebGLRenderer } from 'three' -import { MergedProperties } from './properties/merged' -import { createGetBatchedProperties } from './properties/batched' +import { MergedProperties } from './properties/merged.js' +import { createGetBatchedProperties } from './properties/batched.js' export type WithCameraDistance = { cameraDistance: number } @@ -62,21 +62,26 @@ function compareOrderInfo(o1: OrderInfo, o2: OrderInfo): number { return o1.minorIndex - o2.minorIndex } +export type ZIndexProperties = { + zIndexOffset?: ZIndexOffset +} + export type ZIndexOffset = { major?: number; minor?: number } | number -const propertyKeys = ['zIndexOffset'] +const propertyKeys = ['zIndexOffset'] as const -export function computeOrderInfo( - propertiesSignal: Signal, +export function computedOrderInfo( + propertiesSignal: Signal | undefined, type: ElementType, instancedGroupDependencies: Record | undefined, parentOrderInfoSignal: Signal | undefined, ): Signal { - const get = createGetBatchedProperties(propertiesSignal, propertyKeys) + const get = + propertiesSignal == null ? undefined : createGetBatchedProperties(propertiesSignal, propertyKeys) return computed(() => { const parentOrderInfo = parentOrderInfoSignal?.value - const offset = get('zIndexOffset') as ZIndexOffset + const offset = get?.('zIndexOffset') const majorOffset = typeof offset === 'number' ? offset : offset?.major ?? 0 const minorOffset = typeof offset === 'number' ? 0 : offset?.minor ?? 0 diff --git a/packages/uikit/src/panel/index.ts b/packages/uikit/src/panel/index.ts index 4b75dab2..4197447d 100644 --- a/packages/uikit/src/panel/index.ts +++ b/packages/uikit/src/panel/index.ts @@ -1,5 +1,5 @@ -export * from './utils' -export * from './interaction-panel-mesh' -export * from './panel-material' -export * from './instanced-panel-mesh' -export * from './instanced-panel-group' +export * from './utils.js' +export * from './interaction-panel-mesh.js' +export * from './panel-material.js' +export * from './instanced-panel-mesh.js' +export * from './instanced-panel-group.js' diff --git a/packages/uikit/src/panel/instanced-panel-group.ts b/packages/uikit/src/panel/instanced-panel-group.ts index b64054c0..ce031920 100644 --- a/packages/uikit/src/panel/instanced-panel-group.ts +++ b/packages/uikit/src/panel/instanced-panel-group.ts @@ -6,7 +6,7 @@ import { updateSortedBucketsAllocation, resizeSortedBucketsSpace, } from '../allocation/sorted-buckets.js' -import { MaterialClass, createPanelMaterial, panelMaterialDefaultData } from './panel-material.js' +import { MaterialClass, createPanelMaterial } from './panel-material.js' import { InstancedPanel } from './instanced-panel.js' import { InstancedPanelMesh } from './instanced-panel-mesh.js' import { ElementType, OrderInfo, WithCameraDistance, setupRenderOrder } from '../order.js' @@ -15,22 +15,24 @@ import { createGetBatchedProperties } from '../properties/batched.js' import { MergedProperties } from '../properties/merged.js' import { Object3DRef } from '../context.js' -export type PanelGroupDependencies = { - materialClass: MaterialClass -} & ShadowProperties +export type PanelGroupProperties = { + panelMaterialClass?: MaterialClass + receiveShadow?: boolean + castShadow?: boolean +} -const propertyKeys = ['materialClass', 'castShadow', 'receiveShadow'] +const propertyKeys = ['panelMaterialClass', 'castShadow', 'receiveShadow'] as const -export function computePanelGroupDependencies(propertiesSignal: Signal) { - const get = createGetBatchedProperties(propertiesSignal, propertyKeys) - return computed(() => ({ - materialClass: (get('materialClass') as MaterialClass | undefined) ?? MeshBasicMaterial, - castShadow: get('castShadow') as boolean | undefined, - receiveShadow: get('receiveShadow') as boolean | undefined, +export function computedPanelGroupDependencies(propertiesSignal: Signal) { + const get = createGetBatchedProperties(propertiesSignal, propertyKeys) + return computed(() => ({ + panelMaterialClass: get('panelMaterialClass'), + castShadow: get('castShadow'), + receiveShadow: get('receiveShadow'), })) } -export type ShadowProperties = { receiveShadow?: boolean; castShadow?: boolean } +const defaultDependencies: PanelGroupProperties = { panelMaterialClass: MeshBasicMaterial } export class PanelGroupManager { private map = new Map>() @@ -41,15 +43,18 @@ export class PanelGroupManager { private object: Object3DRef, ) {} - getGroup(majorIndex: number, { materialClass, receiveShadow, castShadow }: PanelGroupDependencies) { - let groups = this.map.get(materialClass) + getGroup( + majorIndex: number, + { panelMaterialClass = MeshBasicMaterial, receiveShadow, castShadow }: PanelGroupProperties = defaultDependencies, + ) { + let groups = this.map.get(panelMaterialClass) if (groups == null) { - this.map.set(materialClass, (groups = new Map())) + this.map.set(panelMaterialClass, (groups = new Map())) } const key = (majorIndex << 2) + ((receiveShadow ? 1 : 0) << 1) + (castShadow ? 1 : 0) let panelGroup = groups.get(key) if (panelGroup == null) { - const material = createPanelMaterial(materialClass, { type: 'instanced' }) + const material = createPanelMaterial(panelMaterialClass, { type: 'instanced' }) groups.set( key, (panelGroup = new InstancedPanelGroup( @@ -90,9 +95,11 @@ export class InstancedPanelGroup { private bufferElementSize: number = 0 private timeToNextUpdate: number | undefined + public instanceDataOnUpdate!: InstancedBufferAttribute['addUpdateRange'] + private activateElement = (element: InstancedPanel, bucket: Bucket, indexInBucket: number) => { const index = bucket.offset + indexInBucket - this.instanceData.set(panelMaterialDefaultData, 16 * index) + this.instanceData.set(element.materialConfig.defaultData, 16 * index) this.instanceData.addUpdateRange(16 * index, 16) this.instanceData.needsUpdate = true element.activate(bucket, indexInBucket) @@ -220,6 +227,10 @@ export class InstancedPanelGroup { dataArray.set(this.instanceData.array.subarray(0, dataArray.length)) } this.instanceData = new InstancedBufferAttribute(dataArray, 16, false) + this.instanceDataOnUpdate = (start, count) => { + this.instanceData.addUpdateRange(start, count) + this.instanceData.needsUpdate = true + } this.instanceData.setUsage(DynamicDrawUsage) const clippingArray = new Float32Array(this.bufferElementSize * 16) if (this.instanceClipping != null) { diff --git a/packages/uikit/src/panel/instanced-panel.ts b/packages/uikit/src/panel/instanced-panel.ts index 5e24512e..77d784df 100644 --- a/packages/uikit/src/panel/instanced-panel.ts +++ b/packages/uikit/src/panel/instanced-panel.ts @@ -1,15 +1,14 @@ import { Signal, signal, effect } from '@preact/signals-core' -import { InstancedBufferAttribute, Matrix4, Vector2Tuple } from 'three' +import { Matrix4, Vector2Tuple } from 'three' import { Bucket } from '../allocation/sorted-buckets.js' import { ClippingRect, defaultClippingData } from '../clipping.js' import { Inset } from '../flex/node.js' -import { InstancedPanelGroup, PanelGroupManager, PanelGroupDependencies } from './instanced-panel-group.js' -import { panelDefaultColor } from './panel-material.js' -import { ColorRepresentation, Subscriptions, colorToBuffer, unsubscribeSubscriptions } from '../utils.js' -import { computeIsPanelVisible, setBorderRadius } from './utils.js' +import { InstancedPanelGroup, PanelGroupManager, PanelGroupProperties } from './instanced-panel-group.js' +import { ColorRepresentation, Subscriptions, unsubscribeSubscriptions } from '../utils.js' import { MergedProperties } from '../properties/merged.js' import { setupImmediateProperties } from '../properties/immediate.js' import { OrderInfo } from '../order.js' +import { PanelMaterialConfig } from './index.js' export type PanelProperties = { borderTopLeftRadius?: number @@ -26,7 +25,7 @@ export type PanelProperties = { export function createInstancedPanel( propertiesSignal: Signal, orderInfo: Signal, - panelGroupDependencies: Signal, + panelGroupDependencies: Signal | undefined, panelGroupManager: PanelGroupManager, matrix: Signal, size: Signal, @@ -34,13 +33,13 @@ export function createInstancedPanel( borderInset: Signal, clippingRect: Signal | undefined, isHidden: Signal | undefined, - outerSubscriptions: Subscriptions, - renameOutput?: Record, + materialConfig: PanelMaterialConfig, + subscriptions: Subscriptions, ) { - outerSubscriptions.push( + subscriptions.push( effect(() => { - const subscriptions: Subscriptions = [] - const group = panelGroupManager.getGroup(orderInfo.value.majorIndex, panelGroupDependencies.value) + const innerSubscriptions: Subscriptions = [] + const group = panelGroupManager.getGroup(orderInfo.value.majorIndex, panelGroupDependencies?.value) new InstancedPanel( propertiesSignal, group, @@ -51,53 +50,14 @@ export function createInstancedPanel( borderInset, clippingRect, isHidden, - outerSubscriptions, - renameOutput, + materialConfig, + innerSubscriptions, ) - return () => unsubscribeSubscriptions(subscriptions) + return () => unsubscribeSubscriptions(innerSubscriptions) }), ) } -const instancedPanelMaterialSetters: { - [Key in keyof PanelProperties]-?: ( - group: InstancedPanelGroup, - index: number, - value: PanelProperties[Key], - size: Signal, - ) => void -} = { - //0-3 = borderSizes - - //4-6 = background color - backgroundColor: (m, i, p) => colorToBuffer(m.instanceData, i, p ?? panelDefaultColor, 4), - - //7 - borderBottomLeftRadius: (m, i, p, { value }) => writeBorderRadius(m.instanceData, i, 7, 0, p, value[1]), - borderBottomRightRadius: (m, i, p, { value }) => writeBorderRadius(m.instanceData, i, 7, 1, p, value[1]), - borderTopRightRadius: (m, i, p, { value }) => writeBorderRadius(m.instanceData, i, 7, 2, p, value[1]), - borderTopLeftRadius: (m, i, p, { value }) => writeBorderRadius(m.instanceData, i, 7, 3, p, value[1]), - - //8-10 = border color - borderColor: (m, i, p) => colorToBuffer(m.instanceData, i, p ?? 0xffffff, 8), - //11 - borderBend: (m, i, p) => writeComponent(m.instanceData, i, 11, p ?? 0), - //12 - borderOpacity: (m, i, p) => writeComponent(m.instanceData, i, 12, p ?? 1), - - //13 = width - //14 = height - - //15 - backgroundOpacity: (m, i, p) => writeComponent(m.instanceData, i, 15, p ?? -1), -} - -function hasImmediateProperty(key: string): boolean { - return key in instancedPanelMaterialSetters -} - -export type InstancedPanelSetter = (typeof instancedPanelMaterialSetters)[keyof typeof instancedPanelMaterialSetters] - const matrixHelper1 = new Matrix4() const matrixHelper2 = new Matrix4() @@ -121,29 +81,25 @@ export class InstancedPanel { private readonly borderInset: Signal, private readonly clippingRect: Signal | undefined, isHidden: Signal | undefined, + public readonly materialConfig: PanelMaterialConfig, subscriptions: Subscriptions, - renameOutput?: Record, ) { + const setters = materialConfig.setters setupImmediateProperties( propertiesSignal, this.active, - hasImmediateProperty, + materialConfig.hasProperty, (key, value) => { const index = this.getIndexInBuffer() if (index == null) { return } - instancedPanelMaterialSetters[key as keyof typeof instancedPanelMaterialSetters]( - this.group, - index, - value as any, - this.size, - ) + const { instanceData, instanceDataOnUpdate: instanceDataAddUpdateRange } = this.group + setters[key](instanceData.array, instanceData.itemSize * index, value, size, instanceDataAddUpdateRange) }, subscriptions, - renameOutput, ) - const isVisible = computeIsPanelVisible(propertiesSignal, borderInset, size, isHidden, renameOutput) + const isVisible = materialConfig.computedIsVisibile(propertiesSignal, borderInset, size, isHidden) subscriptions.push( effect(() => { if (isVisible.value) { @@ -262,24 +218,3 @@ export class InstancedPanel { this.unsubscribeList.length = 0 } } - -function writeBorderRadius( - buffer: InstancedBufferAttribute, - index: number, - component: number, - indexInFloat: number, - value: number | undefined, - height: number, -): void { - const bufferIndex = index * buffer.itemSize + component - buffer.addUpdateRange(bufferIndex, 1) - setBorderRadius(buffer.array, bufferIndex, indexInFloat, value, height) - buffer.needsUpdate = true -} - -function writeComponent(buffer: InstancedBufferAttribute, index: number, component: number, value: number): void { - const bufferIndex = index * buffer.itemSize + component - buffer.addUpdateRange(bufferIndex, 1) - buffer.array[bufferIndex] = value - buffer.needsUpdate = true -} diff --git a/packages/uikit/src/panel/panel-material.ts b/packages/uikit/src/panel/panel-material.ts index d0b11e80..a81ce0df 100644 --- a/packages/uikit/src/panel/panel-material.ts +++ b/packages/uikit/src/panel/panel-material.ts @@ -2,154 +2,205 @@ import { Color, FrontSide, Material, - Mesh, - MeshBasicMaterial, MeshDepthMaterial, MeshDistanceMaterial, - Plane, RGBADepthPacking, + TypedArray, Vector2Tuple, WebGLProgramParametersWithUniforms, WebGLRenderer, } from 'three' -import { Constructor, computeIsPanelVisible, setBorderRadius } from './utils.js' -import { Signal, effect } from '@preact/signals-core' -import { Inset } from '../flex/node.js' -import type { PanelProperties } from './instanced-panel.js' -import { setupImmediateProperties } from '../properties/immediate.js' -import { MergedProperties } from '../properties/merged.js' -import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' +import { Constructor, setBorderRadius } from './utils.js' +import { Signal, computed } from '@preact/signals-core' +import { ColorRepresentation, Inset, MergedProperties, createGetBatchedProperties } from '../internals.js' export type MaterialClass = { new (...args: Array): Material } type InstanceOf = T extends { new (): infer K } ? K : never -const colorHelper = new Color() +const noColor = new Color(-1, -1, -1) + +const defaultDefaults = { + backgroundColor: noColor as ColorRepresentation, + backgroundOpacity: -1, + borderColor: 0xffffff as ColorRepresentation, + borderBottomLeftRadius: 0, + borderTopLeftRadius: 0, + borderBottomRightRadius: 0, + borderTopRightRadius: 0, + borderBend: 0, + borderOpacity: 1, +} satisfies { [Key in keyof typeof materialSetters]: unknown } + +export type PanelMaterialConfig = ReturnType + +let defaultPanelMaterialConfig: PanelMaterialConfig | undefined +export function getDefaultPanelMaterialConfig() { + if (defaultPanelMaterialConfig == null) { + const defaultPanelMaterialKeys = {} as { [Key in keyof typeof defaultDefaults]: string } + for (const key in defaultDefaults) { + defaultPanelMaterialKeys[key as keyof typeof defaultDefaults] = key + } + defaultPanelMaterialConfig = createPanelMaterialConfig(defaultPanelMaterialKeys) + } + return defaultPanelMaterialConfig +} -export const panelDefaultColor = new Color(-1, -1, -1) +export function createPanelMaterialConfig( + keys: { [Key in keyof typeof materialSetters]?: string }, + overrideDefaults?: { + [Key in Exclude< + keyof typeof defaultDefaults, + 'borderBottomLeftRadius' | 'borderTopLeftRadius' | 'borderBottomRightRadius' | 'borderTopRightRadius' + >]?: (typeof defaultDefaults)[Key] + }, +) { + const defaults = { ...defaultDefaults, ...overrideDefaults } + + const setters: { + [Key in string]: ( + data: TypedArray, + offset: number, + value: unknown, + size: Signal, + onUpdate: ((start: number, count: number) => void) | undefined, + ) => void + } = {} + for (const key in keys) { + const fn = materialSetters[key as keyof typeof materialSetters] + const defaultValue = defaults[key as keyof typeof materialSetters] + setters[keys[key as keyof typeof materialSetters]!] = (data, offset, value, size, onUpdate) => + fn(data, offset, (value ?? defaultValue) as any, size, onUpdate) + } -export const panelMaterialSetters: { - [Key in keyof PanelProperties]-?: ( - data: Float32Array, - value: PanelProperties[Key], - size: Signal, - ) => void -} = { + const visibleProperties = [keys.backgroundColor, keys.backgroundOpacity, keys.borderOpacity].filter(filterNull) + const defaultData = new Float32Array(16) //filled with 0s by default + writeColor(defaultData, 4, defaults.backgroundColor, undefined) + writeColor(defaultData, 8, defaults.borderColor, undefined) + defaultData[11] = defaults.borderBend + defaultData[12] = defaults.borderOpacity + defaultData[15] = defaults.backgroundOpacity + return { + hasProperty: (key: string) => key in setters, + defaultData, + setters, + computedIsVisibile: ( + propertiesSignal: Signal, + borderInset: Signal, + size: Signal, + isHidden: Signal | undefined, + ) => { + const get = createGetBatchedProperties(propertiesSignal, visibleProperties) + return computed(() => { + const borderOpacity = + keys.borderOpacity == null + ? defaults.borderOpacity + : (get(keys.borderOpacity) as number) ?? defaults.borderOpacity + const backgroundOpacity = + keys.backgroundOpacity == null + ? defaults.backgroundOpacity + : (get(keys.backgroundOpacity) as number) ?? defaults.backgroundOpacity + const backgroundColor = + keys.backgroundColor == null + ? defaults.backgroundColor + : (get(keys.backgroundColor) as ColorRepresentation) ?? defaults.backgroundColor + const borderVisible = borderInset.value.some((s) => s > 0) && borderOpacity > 0 + const [width, height] = size.value + const backgroundVisible = + width > 0 && height > 0 && (backgroundOpacity === -1 || backgroundOpacity > 0) && backgroundColor != noColor + + if (!backgroundVisible && !borderVisible) { + return false + } + + if (isHidden == null) { + return true + } + + return !isHidden.value + }) + }, + } +} + +const materialSetters = { //0-3 = borderSizes //4-6 = background color - backgroundColor: (d, p) => - (Array.isArray(p) ? colorHelper.setRGB(...p) : colorHelper.set(p ?? panelDefaultColor)).toArray(d, 4), + backgroundColor: (d, o, p: ColorRepresentation, _, u) => writeColor(d, o + 4, p, u), //7 = border radiuses - borderBottomLeftRadius: (d, p, size) => setBorderRadius(d, 7, 0, p, size.value[1]), - borderBottomRightRadius: (d, p, size) => setBorderRadius(d, 7, 1, p, size.value[1]), - borderTopRightRadius: (d, p, size) => setBorderRadius(d, 7, 2, p, size.value[1]), - borderTopLeftRadius: (d, p, size) => setBorderRadius(d, 7, 3, p, size.value[1]), + borderBottomLeftRadius: (d, o, p: number, { value: [, h] }, u) => writeBorderRadius(d, o + 7, 0, p, h, u), + borderBottomRightRadius: (d, o, p: number, { value: [, h] }, u) => writeBorderRadius(d, o + 7, 1, p, h, u), + borderTopRightRadius: (d, o, p: number, { value: [, h] }, u) => writeBorderRadius(d, o + 7, 2, p, h, u), + borderTopLeftRadius: (d, o, p: number, { value: [, h] }, u) => writeBorderRadius(d, o + 7, 3, p, h, u), //8 - 10 = border color - borderColor: (d, p) => (Array.isArray(p) ? colorHelper.setRGB(...p) : colorHelper.set(p ?? 0xffffff)).toArray(d, 8), + borderColor: (d, o, p: number, _, u) => writeColor(d, o + 8, p, u), //11 - borderBend: (d, p) => (d[11] = p ?? 0), + borderBend: (d, o, p: number, _, u) => writeComponent(d, o + 11, p, u), //12 - borderOpacity: (d, p) => (d[12] = p ?? 1), + borderOpacity: (d, o, p: number, _, u) => writeComponent(d, o + 12, p, u), //13 = width //14 = height //15 - backgroundOpacity: (d, p) => (d[15] = p ?? -1), + backgroundOpacity: (d, o, p: number, _, u) => writeComponent(d, o + 15, p, u), +} as const satisfies { + [Key in string]: ( + data: TypedArray, + offset: number, + value: any, + size: Signal, + onUpdate: ((start: number, count: number) => void) | undefined, + ) => void } -export type PanelSetter = (typeof panelMaterialSetters)[keyof typeof panelMaterialSetters] +function filterNull(value: T | undefined | null): value is T { + return value != null +} -export type PanelMaterial = InstanceOf> +function writeBorderRadius( + data: TypedArray, + offset: number, + indexInFloat: number, + value: any, + height: number, + onUpdate: ((start: number, count: number) => void) | undefined, +): void { + setBorderRadius(data, offset, indexInFloat, value, height) + onUpdate?.(offset, 1) +} -export const panelMaterialDefaultData = [ - 0, - 0, - 0, - 0, //border sizes - -1, - -1, - -1, //background color - 0, //border radiuses - 1, - 1, - 1, //border color - 0, //border bend - 1, //border opacity - 1, //width - 1, //height - -1, //background opacity -] - -function hasImmediateProperty(key: string): boolean { - return key in panelMaterialSetters +function writeComponent( + data: TypedArray, + offset: number, + value: any, + onUpdate: ((start: number, count: number) => void) | undefined, +): void { + data[offset] = value + onUpdate?.(offset, 1) } -export function applyPropsToMaterialData( - propertiesSignal: Signal, - data: Float32Array, - size: Signal, - borderInset: Signal, - isClipped: Signal, - materials: ReadonlyArray, - subscriptions: Subscriptions, -) { - const internalSubscriptions: Array<() => void> = [] - const isVisible = computeIsPanelVisible(propertiesSignal, borderInset, size, isClipped, renameOutput) - let visible = false - const materialsLength = materials.length - const syncVisible = () => { - for (let i = 0; i < materialsLength; i++) { - materials[i].visible = visible - } - } - const deactivate = () => { - if (!visible) { - return - } +const colorHelper = new Color() - visible = false - syncVisible() - unsubscribeSubscriptions(internalSubscriptions) +export function writeColor( + target: TypedArray, + offset: number, + color: ColorRepresentation, + onUpdate: ((start: number, count: number) => void) | undefined, +) { + if (Array.isArray(color)) { + target.set(color, offset) + } else { + colorHelper.set(color).toArray(target, offset) } - subscriptions.push( - effect(() => { - if (!isVisible.value) { - deactivate() - return - } - if (visible) { - return - } - - visible = true - syncVisible() - - data.set(panelMaterialDefaultData) - - internalSubscriptions.push( - effect(() => data.set(size.value, 13)), - effect(() => data.set(borderInset.value, 0)), - ) - }), - ) - subscriptions.push(deactivate) - setupImmediateProperties( - propertiesSignal, - isVisible, - hasImmediateProperty, - (key, value) => { - const setter = panelMaterialSetters[key as keyof typeof panelMaterialSetters] - setter(data, value as any, size) - }, - subscriptions, - renameOutput, - ) + onUpdate?.(offset, 3) } +export type PanelMaterial = InstanceOf> + export type PanelMaterialInfo = { type: 'instanced' } | { type: 'normal'; data: Float32Array } export function createPanelMaterial>(MaterialClass: T, info: PanelMaterialInfo) { diff --git a/packages/uikit/src/panel/utils.ts b/packages/uikit/src/panel/utils.ts index 9a9bbd66..60ed266b 100644 --- a/packages/uikit/src/panel/utils.ts +++ b/packages/uikit/src/panel/utils.ts @@ -1,10 +1,5 @@ -import { Signal, computed } from '@preact/signals-core' -import { BufferAttribute, PlaneGeometry, TypedArray, Vector2Tuple } from 'three' +import { BufferAttribute, PlaneGeometry, TypedArray } from 'three' import { clamp } from 'three/src/math/MathUtils.js' -import { Inset } from '../flex/node.js' -import { createGetBatchedProperties } from '../properties/batched.js' -import { MergedProperties } from '../properties/merged.js' -import { ColorRepresentation } from '../utils.js' export type Constructor = new (...args: any[]) => T export type FirstConstructorParameter any> = T extends new ( @@ -34,42 +29,11 @@ export function setComponentInFloat(from: number, index: number, value: number): export const panelGeometry = createPanelGeometry() -const visibleProperties = ['borderOpacity', 'backgroundColor', 'backgroundOpacity'] - -export function computeIsPanelVisible( - propertiesSignal: Signal, - borderInset: Signal, - size: Signal, - isHidden: Signal | undefined, - defaultBackgroundColor?: ColorRepresentation, -) { - const get = createGetBatchedProperties(propertiesSignal, visibleProperties) - return computed(() => { - const borderOpacity = get('borderOpacity') as number - const backgroundOpacity = get('backgroundOpacity') as number - const backgroundColor = defaultBackgroundColor ?? (get('backgroundColor') as ColorRepresentation) - const borderVisible = borderInset.value.some((s) => s > 0) && (borderOpacity == null || borderOpacity > 0) - const [width, height] = size.value - const backgroundVisible = - width > 0 && height > 0 && (backgroundOpacity == null || backgroundOpacity > 0) && backgroundColor != null - - if (!backgroundVisible && !borderVisible) { - return false - } - - if (isHidden == null) { - return true - } - - return !isHidden.value - }) -} - export function setBorderRadius( data: TypedArray, indexInData: number, indexInFloat: number, - value: number | undefined, + value: number, height: number, ) { data[indexInData] = setComponentInFloat( diff --git a/packages/uikit/src/properties/batched.ts b/packages/uikit/src/properties/batched.ts index 139a0c26..2649032f 100644 --- a/packages/uikit/src/properties/batched.ts +++ b/packages/uikit/src/properties/batched.ts @@ -1,19 +1,14 @@ import { Signal, computed } from '@preact/signals-core' import { MergedProperties } from './merged.js' -export type GetBatchedProperties = (key: string) => unknown +export type GetBatchedProperties = (key: K) => T[K] -export function createGetBatchedProperties( +export function createGetBatchedProperties( propertiesSignal: Signal, - keys: Array, - renameOutput?: Record, -): GetBatchedProperties { - const reverseRenameMap: Record = {} - for (const key in renameOutput) { - reverseRenameMap[renameOutput[key]] = key - } + keys: ReadonlyArray, +): GetBatchedProperties { let currentProperties: MergedProperties | undefined - const hasPropertiy = (key: string) => keys.includes(key) + const hasPropertiy = (key: string) => keys.includes(key as any) const computedProperties = computed(() => { const newProperties = propertiesSignal.value if (!newProperties.filterIsEqual(hasPropertiy, currentProperties)) { @@ -23,10 +18,5 @@ export function createGetBatchedProperties( //due to the referencial equality check, the computed value only updates when filterIsEqual returns false return currentProperties }) - return (key) => { - if (key in reverseRenameMap) { - key = reverseRenameMap[key] - } - return computedProperties.value?.read(key) - } + return (key) => computedProperties.value?.read(key as any) as any } diff --git a/packages/uikit/src/properties/default.ts b/packages/uikit/src/properties/default.ts index be7abe1a..32ac1332 100644 --- a/packages/uikit/src/properties/default.ts +++ b/packages/uikit/src/properties/default.ts @@ -1,7 +1,7 @@ import { ReadonlySignal } from '@preact/signals-core' -import { InheritableContainerProperties } from '../components/container.js' -import { InheritableRootProperties } from '../components/root.js' -import { InheritableImageProperties } from '../components/image.js' +import type { InheritableContainerProperties } from '../components/container.js' +import type { InheritableRootProperties } from '../components/root.js' +import type { InheritableImageProperties } from '../components/image.js' export type AllOptionalProperties = | InheritableContainerProperties diff --git a/packages/uikit/src/properties/immediate.ts b/packages/uikit/src/properties/immediate.ts index 441df8d3..af303090 100644 --- a/packages/uikit/src/properties/immediate.ts +++ b/packages/uikit/src/properties/immediate.ts @@ -1,6 +1,6 @@ import { Signal, effect, untracked } from '@preact/signals-core' import { MergedProperties } from './merged.js' -import { Subscriptions } from '../utils.js' +import type { Subscriptions } from '../utils.js' type PropertySubscriptions = Record void> diff --git a/packages/uikit/src/properties/index.ts b/packages/uikit/src/properties/index.ts index c334eb5e..96fad7b4 100644 --- a/packages/uikit/src/properties/index.ts +++ b/packages/uikit/src/properties/index.ts @@ -1,5 +1,5 @@ -export * from './alias' -export * from './batched' -export * from './default' -export * from './immediate' -export * from './merged' +export * from './alias.js' +export * from './batched.js' +export * from './default.js' +export * from './immediate.js' +export * from './merged.js' diff --git a/packages/uikit/src/properties/merged.ts b/packages/uikit/src/properties/merged.ts index c73b0112..dd680f8f 100644 --- a/packages/uikit/src/properties/merged.ts +++ b/packages/uikit/src/properties/merged.ts @@ -1,6 +1,6 @@ import { Signal } from '@preact/signals-core' -import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './default' -import { AllAliases, allAliases } from './alias' +import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './default.js' +import { AllAliases, allAliases } from './alias.js' export type PropertyTransformers = Record void> diff --git a/packages/uikit/src/scroll.ts b/packages/uikit/src/scroll.ts index b3b23607..ee1fb399 100644 --- a/packages/uikit/src/scroll.ts +++ b/packages/uikit/src/scroll.ts @@ -5,15 +5,15 @@ import { ColorRepresentation, Subscriptions } from './utils.js' import { ClippingRect } from './clipping.js' import { clamp } from 'three/src/math/MathUtils.js' import { PanelProperties, createInstancedPanel } from './panel/instanced-panel.js' -import { ElementType, OrderInfo, computeOrderInfo } from './order.js' -import { createGetBatchedProperties } from './properties/batched.js' +import { ElementType, OrderInfo, computedOrderInfo } from './order.js' +import { GetBatchedProperties, createGetBatchedProperties } from './properties/batched.js' import { MergedProperties } from './properties/merged.js' -import { MaterialClass } from './panel/panel-material.js' +import { MaterialClass, PanelMaterialConfig, createPanelMaterialConfig } from './panel/panel-material.js' import { WithReactive } from './properties/default.js' import { - PanelGroupDependencies, + PanelGroupProperties, PanelGroupManager, - computePanelGroupDependencies, + computedPanelGroupDependencies, } from './panel/instanced-panel-group.js' import { Object3DRef } from './context.js' import { ScrollListeners } from './listeners.js' @@ -31,7 +31,7 @@ export function createScrollPosition() { return signal([0, 0]) } -export function computeGlobalScrollMatrix( +export function computedGlobalScrollMatrix( scrollPosition: Signal, globalMatrix: Signal, pixelSize: number, @@ -239,44 +239,39 @@ function outsideDistance(value: number, min: number, max: number): number { return 0 } +export type ScrollbarWidthProperties = { + scrollbarWidth?: number +} + +export type ScrollbarBorderSizeProperties = { + scrollbarBorderRight?: number + scrollbarBorderTop?: number + scrollbarBorderLeft?: number + scrollbarBorderBottom?: number +} + export type ScrollbarProperties = { scrollbarPanelMaterialClass?: MaterialClass } & WithReactive< { - scrollbarWidth?: number scrollbarOpacity?: number scrollbarColor?: ColorRepresentation - scrollbarBorderRight?: number - scrollbarBorderTop?: ColorRepresentation - scrollbarBorderLeft?: ColorRepresentation - scrollbarBorderBottom?: ColorRepresentation - } & { - [Key in `scrollbar${Capitalize< - keyof Omit - >}`]: PanelProperties - } + } & ScrollbarWidthProperties & + ScrollbarBorderSizeProperties & { + [Key in `scrollbar${Capitalize< + keyof Omit + >}`]: PanelProperties + } > -const scrollbarPanelPropertyRename = { - scrollbarColor: 'backgroundColor', - scrollbarBorderBottomLeftRadius: 'borderBottomLeftRadius', - scrollbarBorderBottomRightRadius: 'borderBottomRightRadius', - scrollbarBorderTopRightRadius: 'borderTopRightRadius', - scrollbarBorderTopLeftRadius: 'borderTopLeftRadius', - scrollbarBorderColor: 'borderColor', - scrollbarBorderBend: 'borderBend', - scrollbarBorderOpacity: 'borderOpacity', - scrollbarOpacity: 'backgroundOpacity', -} - -const scrollbarWidthPropertyKeys = ['scrollbarWidth'] +const scrollbarWidthPropertyKeys = ['scrollbarWidth'] as const const scrollbarBorderPropertyKeys = [ 'scrollbarBorderLeft', 'scrollbarBorderRight', 'scrollbarBorderTop', 'scrollbarBorderBottom', -] +] as const export function createScrollbars( propertiesSignal: Signal, @@ -289,12 +284,18 @@ export function createScrollbars( panelGroupManager: PanelGroupManager, subscriptions: Subscriptions, ): void { - const groupDeps = computePanelGroupDependencies(propertiesSignal) - const scrollbarOrderInfo = computeOrderInfo(propertiesSignal, ElementType.Panel, groupDeps, orderInfo) + const groupDeps = computedPanelGroupDependencies(propertiesSignal) + const scrollbarOrderInfo = computedOrderInfo(undefined, ElementType.Panel, groupDeps, orderInfo) - const getScrollbarWidth = createGetBatchedProperties(propertiesSignal, scrollbarWidthPropertyKeys) - const getBorder = createGetBatchedProperties(propertiesSignal, scrollbarBorderPropertyKeys) - const borderSize = computed(() => scrollbarBorderPropertyKeys.map((key) => (getBorder(key) as number) ?? 0) as Inset) + const getScrollbarWidth = createGetBatchedProperties( + propertiesSignal, + scrollbarWidthPropertyKeys, + ) + const getBorder = createGetBatchedProperties( + propertiesSignal, + scrollbarBorderPropertyKeys, + ) + const borderSize = computed(() => scrollbarBorderPropertyKeys.map((key) => getBorder(key) ?? 0) as Inset) createScrollbar( propertiesSignal, @@ -326,13 +327,28 @@ export function createScrollbars( borderSize, subscriptions, ) +} - //TODO: setting the scrollbar color and opacity default for all property managers of the instanced panel - /*const collectionLength = collection.length - for (let i = startIndex; i < collectionLength; i++) { - collection[i].add('scrollbarColor', 0xffffff) - collection[i].add('scrollbarOpacity', 1) - }*/ +let scrollbarMaterialConfig: PanelMaterialConfig | undefined +function getScrollbarMaterialConfig() { + scrollbarMaterialConfig ??= createPanelMaterialConfig( + { + backgroundColor: 'scrollbarColor', + borderBottomLeftRadius: 'scrollbarBorderBottomLeftRadius', + borderBottomRightRadius: 'scrollbarBorderBottomRightRadius', + borderTopRightRadius: 'scrollbarBorderTopRightRadius', + borderTopLeftRadius: 'scrollbarBorderTopLeftRadius', + borderColor: 'scrollbarBorderColor', + borderBend: 'scrollbarBorderBend', + borderOpacity: 'scrollbarBorderOpacity', + backgroundOpacity: 'scrollbarOpacity', + }, + { + backgroundColor: 0xffffff, + backgroundOpacity: 1, + }, + ) + return scrollbarMaterialConfig } function createScrollbar( @@ -341,19 +357,19 @@ function createScrollbar( scrollPosition: Signal, node: FlexNode, globalMatrix: Signal, - panelGroupDependencies: Signal, + panelGroupDependencies: Signal, isClipped: Signal | undefined, parentClippingRect: Signal | undefined, orderInfo: Signal, panelGroupManager: PanelGroupManager, - get: (key: string) => unknown, + get: GetBatchedProperties, borderSize: ReadonlySignal, subscriptions: Subscriptions, ) { const scrollbarTransformation = computed(() => { return computeScrollbarTransformation( mainIndex, - (get('scrollbarWidth') as number) ?? 10, + get('scrollbarWidth') ?? 10, node.size.value, node.maxScrollPosition.value, node.borderInset.value, @@ -374,8 +390,8 @@ function createScrollbar( borderSize, parentClippingRect, isClipped, + getScrollbarMaterialConfig(), subscriptions, - scrollbarPanelPropertyRename, ) } diff --git a/packages/uikit/src/selection.ts b/packages/uikit/src/selection.ts index b811b297..d5e54ec1 100644 --- a/packages/uikit/src/selection.ts +++ b/packages/uikit/src/selection.ts @@ -1,96 +1,103 @@ import { Signal, effect, signal } from '@preact/signals-core' -import { GetInstancedPanelGroup, useGetInstancedPanelGroup, usePanelGroupDependencies } from './panel/react.js' -import { useEffect, useMemo } from 'react' -import { InstancedPanel } from './panel/instanced-panel.js' +import { useMemo } from 'react' +import { createInstancedPanel } from './panel/instanced-panel.js' import { Matrix4, Vector2Tuple } from 'three' import { ClippingRect } from './clipping.js' -import { ElementType, OrderInfo, useOrderInfo } from './order.js' +import { ElementType, OrderInfo, computedOrderInfo } from './order.js' import { Inset } from './flex/index.js' +import { + MergedProperties, + PanelGroupManager, + PanelMaterialConfig, + Subscriptions, + createPanelMaterialConfig, + unsubscribeSubscriptions, +} from './internals.js' const noBorder = signal([0, 0, 0, 0]) export type SelectionBoxes = Array<{ size: Vector2Tuple; position: Vector2Tuple }> +let selectionMaterialConfig: PanelMaterialConfig | undefined +function getSelectionMaterialConfig() { + selectionMaterialConfig ??= createPanelMaterialConfig( + { + backgroundColor: 'selectionColor', + backgroundOpacity: 'selectionOpacity', + }, + { + backgroundColor: 0xb4d7ff, + backgroundOpacity: 1, + }, + ) + return selectionMaterialConfig +} + export function useSelection( + propertiesSignal: Signal, matrix: Signal, selectionBoxes: Signal, isHidden: Signal | undefined, - parentOrderInfo: OrderInfo, + parentOrderInfo: Signal, parentClippingRect: Signal | undefined, - providedGetGroup?: GetInstancedPanelGroup, -): OrderInfo { - // eslint-disable-next-line react-hooks/rules-of-hooks - const getGroup = providedGetGroup ?? useGetInstancedPanelGroup() + panelGroupManager: PanelGroupManager, + subscriptions: Subscriptions, +): Signal { const panels = useMemo< - Array<{ panel: InstancedPanel; size: Signal; offset: Signal; unsubscribe: () => void }> + Array<{ + size: Signal + offset: Signal + panelSubscriptions: Subscriptions + }> >(() => [], []) - const groupDeps = usePanelGroupDependencies(undefined, { castShadow: false, receiveShadow: false }) - const orderInfo = useOrderInfo(ElementType.Panel, undefined, groupDeps, parentOrderInfo) - const unsubscribe = useMemo( - () => - effect(() => { - const selections = selectionBoxes.value - const selectionsLength = selections.length - for (let i = 0; i < selectionsLength; i++) { - let panelData = panels[i] - if (panelData == null) { - const size = signal([0, 0]) - const offset = signal([0, 0]) - const panel = new InstancedPanel( - getGroup(orderInfo.majorIndex, groupDeps), - matrix, - size, - offset, - noBorder, - parentClippingRect, - isHidden, - orderInfo.minorIndex, - ) - panel.getProperty.value = (key) => { - if (key === 'backgroundColor') { - return 0xb4d7ff as any - } - if (key === 'backgroundOpacity') { - return 1 - } - return undefined - } - const unsubscribe = effect(() => { - if (panel.active.value) { - panel.setProperty('backgroundColor', 0xb4d7ff) - panel.setProperty('backgroundOpacity', 1) - } - }) - panels[i] = panelData = { - unsubscribe, - panel, - offset, - size, - } + const orderInfo = computedOrderInfo(undefined, ElementType.Panel, undefined, parentOrderInfo) + + subscriptions.push( + effect(() => { + const selections = selectionBoxes.value + const selectionsLength = selections.length + for (let i = 0; i < selectionsLength; i++) { + let panelData = panels[i] + if (panelData == null) { + const size = signal([0, 0]) + const offset = signal([0, 0]) + const panelSubscriptions: Subscriptions = [] + createInstancedPanel( + propertiesSignal, + orderInfo, + undefined, + panelGroupManager, + matrix, + size, + offset, + noBorder, + parentClippingRect, + isHidden, + getSelectionMaterialConfig(), + panelSubscriptions, + ) + panels[i] = panelData = { + panelSubscriptions, + offset, + size, } - const selection = selections[i] - panelData.size.value = selection.size - panelData.offset.value = selection.position } - const panelsLength = panels.length - for (let i = selectionsLength; i < panelsLength; i++) { - panels[i].unsubscribe() - panels[i].panel.destroy() - } - panels.length = selectionsLength - }), - [selectionBoxes, panels, getGroup, orderInfo, groupDeps, matrix, parentClippingRect, isHidden], - ) - useEffect( - () => () => { - unsubscribe() + const selection = selections[i] + panelData.size.value = selection.size + panelData.offset.value = selection.position + } + const panelsLength = panels.length + for (let i = selectionsLength; i < panelsLength; i++) { + unsubscribeSubscriptions(panels[i].panelSubscriptions) + } + panels.length = selectionsLength + }), + () => { const panelsLength = panels.length for (let i = 0; i < panelsLength; i++) { - panels[i].unsubscribe() - panels[i].panel.destroy() + unsubscribeSubscriptions(panels[i].panelSubscriptions) } }, - [unsubscribe, panels], ) return orderInfo } diff --git a/packages/uikit/src/text/font.ts b/packages/uikit/src/text/font.ts index 9ed02949..f51de6bb 100644 --- a/packages/uikit/src/text/font.ts +++ b/packages/uikit/src/text/font.ts @@ -1,9 +1,9 @@ import { Signal, effect, signal } from '@preact/signals-core' import { Texture, TypedArray, WebGLRenderer } from 'three' -import { createGetBatchedProperties } from '../properties/batched' -import { MergedProperties } from '../properties/merged' -import { Subscriptions } from '../utils' -import { loadCachedFont } from './cache' +import { createGetBatchedProperties } from '../properties/batched.js' +import { MergedProperties } from '../properties/merged.js' +import { Subscriptions } from '../utils.js' +import { loadCachedFont } from './cache.js' export type FontFamilyUrls = Partial> @@ -24,7 +24,7 @@ const fontWeightNames = { export type FontWeight = keyof typeof fontWeightNames | number -const fontKeys = ['fontFamily', 'fontWeight'] +const fontKeys = ['fontFamily', 'fontWeight'] as const export type FontFamilyProperties = { fontFamily?: string; fontWeight?: FontWeight } @@ -38,21 +38,21 @@ const defaultFontFamilyUrls = { }, } satisfies FontFamilies -export function computeFont( +export function computedFont( properties: Signal, fontFamilies: FontFamilies = defaultFontFamilyUrls, renderer: WebGLRenderer, subscriptions: Subscriptions, ): Signal { const result = signal(undefined) - const get = createGetBatchedProperties(properties, fontKeys) + const get = createGetBatchedProperties(properties, fontKeys) subscriptions.push( effect(() => { - let fontWeight = (get('fontWeight') as FontWeight) ?? 'normal' + let fontWeight = get('fontWeight') ?? 'normal' if (typeof fontWeight === 'string') { fontWeight = fontWeightNames[fontWeight] } - let fontFamily = get('fontFamily') as string + let fontFamily = get('fontFamily') if (fontFamily == null) { fontFamily = Object.keys(fontFamilies)[0] } diff --git a/packages/uikit/src/text/index.ts b/packages/uikit/src/text/index.ts index 3aaf540d..7fcfae2b 100644 --- a/packages/uikit/src/text/index.ts +++ b/packages/uikit/src/text/index.ts @@ -1,5 +1,5 @@ -export * from './utils' -export * from './wrapper/index' -export * from './font' -export * from './layout' -export * from './render/index' +export * from './utils.js' +export * from './wrapper/index.js' +export * from './font.js' +export * from './layout.js' +export * from './render/index.js' diff --git a/packages/uikit/src/text/layout.ts b/packages/uikit/src/text/layout.ts index 288aee54..f557a6a2 100644 --- a/packages/uikit/src/text/layout.ts +++ b/packages/uikit/src/text/layout.ts @@ -32,15 +32,15 @@ export type GlyphLayoutProperties = { wordBreak: keyof typeof wrappers } -const glyphPropertyKeys = ['fontSize', 'letterSpacing', 'lineHeight', 'wordBreak'] +const glyphPropertyKeys = ['fontSize', 'letterSpacing', 'lineHeight', 'wordBreak'] as const -export function computeMeasureFunc( +export function computedMeasureFunc( properties: Signal, fontSignal: Signal, textSignal: Signal | Array | string>>, propertiesRef: { current: GlyphLayoutProperties | undefined }, ) { - const get = createGetBatchedProperties(properties, glyphPropertyKeys) + const get = createGetBatchedProperties(properties, glyphPropertyKeys) return computed(() => { const font = fontSignal.value if (font == null) { diff --git a/packages/uikit/src/text/render/index.ts b/packages/uikit/src/text/render/index.ts index 4694bb8d..5429c084 100644 --- a/packages/uikit/src/text/render/index.ts +++ b/packages/uikit/src/text/render/index.ts @@ -1,5 +1,5 @@ -export * from './instanced-gylph-material' -export * from './instanced-glyph-mesh' -export * from './instanced-glyph' -export * from './instanced-glyph-group' -export * from './instanced-text' +export * from './instanced-gylph-material.js' +export * from './instanced-glyph-mesh.js' +export * from './instanced-glyph.js' +export * from './instanced-glyph-group.js' +export * from './instanced-text.js' diff --git a/packages/uikit/src/text/render/instanced-glyph.ts b/packages/uikit/src/text/render/instanced-glyph.ts index d6132e74..651e7606 100644 --- a/packages/uikit/src/text/render/instanced-glyph.ts +++ b/packages/uikit/src/text/render/instanced-glyph.ts @@ -1,21 +1,23 @@ -import { Matrix4, WebGLRenderer } from 'three' +import { Matrix4, Vector2Tuple, Vector3Tuple, WebGLRenderer } from 'three' import { GlyphGroupManager, InstancedGlyphGroup } from './instanced-glyph-group.js' -import { ColorRepresentation, Subscriptions, colorToBuffer } from '../../utils.js' +import { ColorRepresentation, Subscriptions } from '../../utils.js' import { ClippingRect, defaultClippingData } from '../../clipping.js' -import { FontFamilies, FontFamilyProperties, GlyphInfo, computeFont, glyphIntoToUV } from '../font.js' +import { FontFamilies, FontFamilyProperties, GlyphInfo, computedFont, glyphIntoToUV } from '../font.js' import { Signal, ReadonlySignal, signal, effect } from '@preact/signals-core' import { FlexNode } from '../../flex/node.js' import { OrderInfo } from '../../order.js' import { createGetBatchedProperties } from '../../properties/batched.js' import { MergedProperties } from '../../properties/merged.js' -import { GlyphLayoutProperties, GlyphLayout, buildGlyphLayout, computeMeasureFunc } from '../layout.js' +import { GlyphLayoutProperties, GlyphLayout, buildGlyphLayout, computedMeasureFunc } from '../layout.js' import { TextAlignProperties, TextAppearanceProperties, InstancedText } from './instanced-text.js' +import { SelectionBoxes } from '../../selection.js' +import { writeColor } from '../../internals.js' const helperMatrix1 = new Matrix4() const helperMatrix2 = new Matrix4() -const alignPropertyKeys = ['horizontalAlign', 'verticalAlign'] -const appearancePropertyKeys = ['color', 'opacity'] +const alignPropertyKeys = ['horizontalAlign', 'verticalAlign'] as const +const appearancePropertyKeys = ['color', 'opacity'] as const export type InstancedTextProperties = TextAlignProperties & TextAppearanceProperties & @@ -33,17 +35,20 @@ export function createInstancedText( fontFamilies: FontFamilies | undefined, renderer: WebGLRenderer, glyphGroupManager: GlyphGroupManager, + selectionRange: Signal | undefined, + selectionBoxes: Signal | undefined, + caretPosition: Signal | undefined, subscriptions: Subscriptions, ) { - const fontSignal = computeFont(properties, fontFamilies, renderer, subscriptions) + const fontSignal = computedFont(properties, fontFamilies, renderer, subscriptions) // eslint-disable-next-line react-hooks/exhaustive-deps const textSignal = signal | Array>>(text) let layoutPropertiesRef: { current: GlyphLayoutProperties | undefined } = { current: undefined } - const measureFunc = computeMeasureFunc(properties, fontSignal, textSignal, layoutPropertiesRef) + const measureFunc = computedMeasureFunc(properties, fontSignal, textSignal, layoutPropertiesRef) - const getAlign = createGetBatchedProperties(properties, alignPropertyKeys) - const getAppearance = createGetBatchedProperties(properties, appearancePropertyKeys) + const getAlign = createGetBatchedProperties(properties, alignPropertyKeys) + const getAppearance = createGetBatchedProperties(properties, appearancePropertyKeys) const layoutSignal = signal(undefined) subscriptions.push( @@ -76,6 +81,9 @@ export function createInstancedText( matrix, isHidden, parentClippingRect, + selectionRange, + selectionBoxes, + caretPosition, ) return () => instancedText.destroy() }), @@ -164,7 +172,11 @@ export class InstancedGlyph { if (this.index == null) { return } - colorToBuffer(this.group.instanceRGBA, this.index, color) + const { instanceRGBA } = this.group + const offset = instanceRGBA.itemSize * this.index + writeColor(instanceRGBA.array, offset, color, undefined) + instanceRGBA.addUpdateRange(offset, 3) + instanceRGBA.needsUpdate = true } updateOpacity(opacity: number): void { diff --git a/packages/uikit/src/text/render/instanced-text.ts b/packages/uikit/src/text/render/instanced-text.ts index a6589ec5..4b04ef61 100644 --- a/packages/uikit/src/text/render/instanced-text.ts +++ b/packages/uikit/src/text/render/instanced-text.ts @@ -40,8 +40,8 @@ export class InstancedText { constructor( private group: InstancedGlyphGroup, - private getAlignment: GetBatchedProperties, - private getAppearance: GetBatchedProperties, + private getAlignment: GetBatchedProperties, + private getAppearance: GetBatchedProperties, private layoutSignal: Signal, private matrix: Signal, isHidden: Signal | undefined, @@ -70,7 +70,7 @@ export class InstancedText { } public getCharIndex(x: number, y: number): number { - const verticalAlign = untracked(() => this.getAlignmentProperties.value?.('verticalAlign') ?? 'top') + const verticalAlign = untracked(() => this.getAlignment('verticalAlign') ?? 'top') const layout = this.lastLayout if (layout == null) { return 0 diff --git a/packages/uikit/src/transform.ts b/packages/uikit/src/transform.ts index ec878bbc..f3910481 100644 --- a/packages/uikit/src/transform.ts +++ b/packages/uikit/src/transform.ts @@ -35,7 +35,7 @@ const sX = 'transformScaleX' const sY = 'transformScaleY' const sZ = 'transformScaleZ' -const propertyKeys = [tX, tY, tZ, rX, rY, rZ, sX, sY, sZ] +const propertyKeys = [tX, tY, tZ, rX, rY, rZ, sX, sY, sZ] as const const tHelper = new Vector3() const sHelper = new Vector3() @@ -49,24 +49,23 @@ function toQuaternion([x, y, z]: Vector3Tuple): Quaternion { return quaternionHelper.setFromEuler(eulerHelper.set(x * toRad, y * toRad, z * toRad)) } -export function computeTransformMatrix( +export function computedTransformMatrix( propertiesSignal: Signal, node: FlexNode, pixelSize: number, - renameOutput?: Record, ): Signal { //B * O^-1 * T * O //B = bound transformation matrix //O = matrix to transform the origin for matrix T //T = transform matrix (translate, rotate, scale) - const get = createGetBatchedProperties(propertiesSignal, propertyKeys, renameOutput) + const get = createGetBatchedProperties(propertiesSignal, propertyKeys) return computed(() => { const { relativeCenter } = node const [x, y] = relativeCenter.value const result = new Matrix4().makeTranslation(x * pixelSize, y * pixelSize, 0) - const tOriginX = (get('transformOriginX') ?? 'center') as keyof typeof alignmentXMap - const tOriginY = (get('transformOriginY') ?? 'center') as keyof typeof alignmentYMap + const tOriginX = get('transformOriginX') ?? 'center' + const tOriginY = get('transformOriginY') ?? 'center' let originCenter = true if (tOriginX != 'center' || tOriginY != 'center') { @@ -77,9 +76,9 @@ export function computeTransformMatrix( originVector.negate() } - const r: Vector3Tuple = [(get(rX) as number) ?? 0, (get(rY) as number) ?? 0, (get(rZ) as number) ?? 0] - const t: Vector3Tuple = [(get(tX) as number) ?? 0, -((get(tY) as number) ?? 0), (get(tZ) as number) ?? 0] - const s: Vector3Tuple = [(get(sX) as number) ?? 1, (get(sY) as number) ?? 1, (get(sZ) as number) ?? 1] + const r: Vector3Tuple = [get(rX) ?? 0, get(rY) ?? 0, get(rZ) ?? 0] + const t: Vector3Tuple = [get(tX) ?? 0, -(get(tY) ?? 0), get(tZ) ?? 0] + const s: Vector3Tuple = [get(sX) ?? 1, get(sY) ?? 1, get(sZ) ?? 1] if (t.some((v) => v != 0) || r.some((v) => v != 0) || s.some((v) => v != 1)) { result.multiply( matrixHelper.compose(tHelper.fromArray(t).multiplyScalar(pixelSize), toQuaternion(r), sHelper.fromArray(s)), diff --git a/packages/uikit/src/utils.ts b/packages/uikit/src/utils.ts index 89817fd6..d16ac8f7 100644 --- a/packages/uikit/src/utils.ts +++ b/packages/uikit/src/utils.ts @@ -1,7 +1,6 @@ -import { computed, Signal, signal } from '@preact/signals-core' -import { Vector2Tuple, BufferAttribute, Color, Vector3Tuple } from 'three' +import { computed, Signal } from '@preact/signals-core' +import { Vector2Tuple, Color, Vector3Tuple } from 'three' import { Inset } from './flex/node.js' -import { Yoga, loadYoga as loadYogaImpl } from 'yoga-layout/wasm-async' import { MergedProperties } from './properties/merged.js' export type ColorRepresentation = Color | string | number | Vector3Tuple @@ -20,16 +19,6 @@ export const alignmentXMap = { left: 0.5, center: 0, right: -0.5 } export const alignmentYMap = { top: -0.5, center: 0, bottom: 0.5 } export const alignmentZMap = { back: -0.5, center: 0, front: 0.5 } -let yoga: Signal | undefined - -export function loadYoga(): Signal { - if (yoga == null) { - const result = (yoga = signal(undefined)) - loadYogaImpl().then((value) => (result.value = value)) - } - return yoga -} - /** * calculates the offsetX, offsetY, and scale to fit content with size [aspectRatio, 1] inside */ @@ -60,20 +49,6 @@ export function fitNormalizedContentInside( return [(leftInset - rightInset) * 0.5 * pixelSize, (bottomInset - topInset) * 0.5 * pixelSize, scaling] } -const colorHelper = new Color() - -export function colorToBuffer(buffer: BufferAttribute, index: number, color: ColorRepresentation, offset = 0): void { - const bufferIndex = index * buffer.itemSize + offset - buffer.addUpdateRange(bufferIndex, 3) - if (Array.isArray(color)) { - buffer.set(color, bufferIndex) - } else { - colorHelper.set(color) - colorHelper.toArray(buffer.array, bufferIndex) - } - buffer.needsUpdate = true -} - export function readReactive(value: T | Signal): T { return value instanceof Signal ? value.value : value } diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts index e06a59af..fd51fc88 100644 --- a/packages/uikit/src/vanilla/container.ts +++ b/packages/uikit/src/vanilla/container.ts @@ -1,8 +1,8 @@ import { Object3D } from 'three' -import { ContainerProperties, createContainer, destroyContainer } from '../components/container' -import { AllOptionalProperties, Properties } from '../properties/default' -import { Component } from '.' -import { EventConfig, bindHandlers } from './utils' +import { ContainerProperties, createContainer, destroyContainer } from '../components/container.js' +import { AllOptionalProperties, Properties } from '../properties/default.js' +import { Component } from './index.js' +import { EventConfig, bindHandlers } from './utils.js' import { batch } from '@preact/signals-core' export class Container extends Object3D { diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index 45cd841f..58b63e57 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -1,8 +1,8 @@ import { Object3D } from 'three' -import { ImageProperties, createImage, destroyImage } from '../components/image' -import { AllOptionalProperties } from '../properties/default' -import { Component } from '.' -import { EventConfig, bindHandlers } from './utils' +import { ImageProperties, createImage, destroyImage } from '../components/image.js' +import { AllOptionalProperties } from '../properties/default.js' +import { Component } from './index.js' +import { EventConfig, bindHandlers } from './utils.js' import { batch } from '@preact/signals-core' export class Image extends Object3D { diff --git a/packages/uikit/src/vanilla/index.ts b/packages/uikit/src/vanilla/index.ts index 7bdb136b..b3647a44 100644 --- a/packages/uikit/src/vanilla/index.ts +++ b/packages/uikit/src/vanilla/index.ts @@ -2,8 +2,8 @@ import type { Container } from './container' import type { Root } from './root' import type { Image } from './image' -export type Component = Container | Root +export type Component = Container | Root | Image -export * from './container' -export * from './root' -export * from './image' +export * from './container.js' +export * from './root.js' +export * from './image.js' diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index 93d88f03..dc2ee4e1 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -1,8 +1,8 @@ import { Camera, Object3D } from 'three' import { batch } from '@preact/signals-core' -import { AllOptionalProperties } from '../properties/default' -import { createRoot, destroyRoot, RootProperties } from '../components/root' -import { EventConfig, bindHandlers } from './utils' +import { AllOptionalProperties } from '../properties/default.js' +import { createRoot, destroyRoot, RootProperties } from '../components/root.js' +import { EventConfig, bindHandlers } from './utils.js' export class Root extends Object3D { public readonly internals: ReturnType diff --git a/packages/uikit/src/vanilla/utils.ts b/packages/uikit/src/vanilla/utils.ts index 326b2706..e4ebdbb4 100644 --- a/packages/uikit/src/vanilla/utils.ts +++ b/packages/uikit/src/vanilla/utils.ts @@ -1,8 +1,7 @@ import { Signal, effect } from '@preact/signals-core' -import { Subscriptions } from '../utils' -import { EventHandlers } from '../events' +import { Subscriptions } from '../utils.js' +import { EventHandlers } from '../events.js' import { Mesh, Object3D } from 'three' -import { RootContext } from '../context' export type EventConfig = { bindEventHandlers: (object: Object3D, handlers: EventHandlers) => void diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53b68e2f..fb45c63f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,7 +151,7 @@ importers: version: 2.16.0(@react-three/fiber@8.15.13)(@types/three@0.161.2)(react@18.2.0)(three@0.161.0) '@react-three/uikit': specifier: workspace:^ - version: link:../../packages/uikit + version: link:../../packages/react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../packages/icons/lucide @@ -421,7 +421,7 @@ importers: dependencies: '@react-three/uikit': specifier: workspace:^ - version: link:../../uikit + version: link:../../react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../icons/lucide @@ -432,12 +432,6 @@ importers: '@react-three/fiber': specifier: ^8.15.13 version: 8.15.13(react-dom@18.2.0)(react@18.2.0)(three@0.161.0) - '@react-three/uikit': - specifier: workspace:^ - version: link:../../react - '@react-three/uikit-lucide': - specifier: workspace:^ - version: link:../../icons/lucide '@types/react': specifier: ^18.2.47 version: 18.2.47 @@ -452,7 +446,7 @@ importers: dependencies: '@react-three/uikit': specifier: workspace:^ - version: link:../../uikit + version: link:../../react '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../icons/lucide @@ -466,12 +460,6 @@ importers: '@react-three/fiber': specifier: ^8.15.13 version: 8.15.13(react-dom@18.2.0)(react@18.2.0)(three@0.161.0) - '@react-three/uikit': - specifier: workspace:^ - version: link:../../react - '@react-three/uikit-lucide': - specifier: workspace:^ - version: link:../../icons/lucide '@types/react': specifier: ^18.2.47 version: 18.2.47 @@ -543,8 +531,8 @@ importers: specifier: ^1.5.1 version: 1.5.1 yoga-layout: - specifier: ^2.0.1 - version: 2.0.1 + specifier: ^3.0.2 + version: 3.0.2 devDependencies: '@types/node': specifier: ^20.11.0 From 44b088d8750532c036cbf44aafed28b5efcbfeb2 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Wed, 3 Apr 2024 13:37:24 +0200 Subject: [PATCH 09/20] fix: properties precedence --- packages/uikit/package.json | 2 +- packages/uikit/src/active.ts | 3 -- packages/uikit/src/components/container.ts | 23 +++++----- packages/uikit/src/components/image.ts | 40 ++++++++++------- packages/uikit/src/components/root.ts | 20 +++++---- packages/uikit/src/dark.ts | 2 +- packages/uikit/src/focus.ts | 1 - packages/uikit/src/properties/default.ts | 26 +++++++---- packages/uikit/src/properties/merged.ts | 21 ++++++--- packages/uikit/tests/precedence.spec.ts | 51 ++++++++++++++++++++++ 10 files changed, 131 insertions(+), 58 deletions(-) create mode 100644 packages/uikit/tests/precedence.spec.ts diff --git a/packages/uikit/package.json b/packages/uikit/package.json index 8e6cf068..602ce997 100644 --- a/packages/uikit/package.json +++ b/packages/uikit/package.json @@ -27,7 +27,7 @@ "./internals": "./dist/internals.js" }, "scripts": { - "test": "mocha ./tests/flex.spec.ts", + "test": "mocha ./tests/precedence.spec.ts", "build": "tsc -p ./tsconfig.build.json", "generate": "node --loader ts-node/esm scripts/flex-generate-setter.ts", "check:prettier": "prettier --check src scripts tests", diff --git a/packages/uikit/src/active.ts b/packages/uikit/src/active.ts index 954c44e0..aa925845 100644 --- a/packages/uikit/src/active.ts +++ b/packages/uikit/src/active.ts @@ -47,9 +47,6 @@ export function addActiveHandlers( addHandler('onPointerUp', target, onLeave) addHandler('onPointerLeave', target, onLeave) } - -//TODO: this does not work because active: { ... } should always overwrite even properties after - export function createActivePropertyTransfomers(activeSignal: Signal>) { return { active: createConditionalPropertyTranslator(() => activeSignal.value.length > 0), diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 8d9d1de0..5615fc7d 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -15,7 +15,6 @@ import { TransformProperties, applyTransform, computedTransformMatrix } from '.. import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js' -import { preferredColorSchemePropertyTransformers } from '../dark.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' import { computed, signal } from '@preact/signals-core' import { WithConditionals, computedGlobalMatrix } from './utils.js' @@ -26,10 +25,10 @@ import { Object3DRef, WithContext } from '../context.js' import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { EventHandlers } from '../events.js' -import { getDefaultPanelMaterialConfig } from '../internals.js' +import { darkPropertyTransformers, getDefaultPanelMaterialConfig, traverseProperties } from '../internals.js' -export type InheritableContainerProperties = WithConditionals< - WithClasses< +export type InheritableContainerProperties = WithClasses< + WithConditionals< WithAllAliases< WithReactive< YogaProperties & @@ -57,20 +56,20 @@ export function createContainer( const subscriptions = [] as Subscriptions setupCursorCleanup(hoveredSignal, subscriptions) - const propertyTransformers = { - ...preferredColorSchemePropertyTransformers, + const scrollHandlers = signal({}) + const propertiesSignal = signal(properties) + const defaultPropertiesSignal = signal(defaultProperties) + + const postTranslators = { + ...darkPropertyTransformers, ...createResponsivePropertyTransformers(parentContext.root.node.size), ...createHoverPropertyTransformers(hoveredSignal), ...createActivePropertyTransfomers(activeSignal), } - const scrollHandlers = signal({}) - const propertiesSignal = signal(properties) - const defaultPropertiesSignal = signal(defaultProperties) - const mergedProperties = computed(() => { - const merged = new MergedProperties(propertyTransformers) - merged.addAll(defaultPropertiesSignal.value, propertiesSignal.value) + const merged = new MergedProperties() + merged.addAll(defaultPropertiesSignal.value, propertiesSignal.value, postTranslators) return merged }) diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index aec5a252..c0af2695 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -43,16 +43,20 @@ import { createGetBatchedProperties } from '../properties/batched.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { cloneHandlers } from '../panel/instanced-panel-mesh.js' -import { preferredColorSchemePropertyTransformers } from '../dark.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { EventHandlers } from '../events.js' -import { PanelGroupProperties, PanelMaterialConfig, createPanelMaterialConfig } from '../internals.js' +import { + PanelGroupProperties, + PanelMaterialConfig, + createPanelMaterialConfig, + darkPropertyTransformers, +} from '../internals.js' export type ImageFit = 'cover' | 'fill' const FIT_DEFAULT: ImageFit = 'fill' -export type InheritableImageProperties = WithConditionals< - WithClasses< +export type InheritableImageProperties = WithClasses< + WithConditionals< WithAllAliases< WithReactive< YogaProperties & @@ -62,9 +66,9 @@ export type InheritableImageProperties = WithConditionals< fit?: ImageFit keepAspectRatio?: boolean } & TransformProperties & - PanelGroupProperties - > & - ScrollbarProperties + PanelGroupProperties & + ScrollbarProperties + > > > > @@ -99,24 +103,30 @@ export function createImage( return image.width / image.height }) - const propertyTransformers: PropertyTransformers = { + const signalMap = new Map>() + + const prePropertyTransformers: PropertyTransformers = { keepAspectRatio: (value, target) => { - if (value !== false) { - return + let signal = signalMap.get(value) + if (signal == null) { + //if keep aspect ratio is "false" => we write "null" => which overrides the previous properties and returns null + signalMap.set(value, (signal = computed(() => (readReactive(value) === false ? null : undefined)))) } - target.remove('aspectRatio') + target.add('aspectRatio', signal) }, - ...preferredColorSchemePropertyTransformers, + } + + const postTransformers = { + ...darkPropertyTransformers, ...createResponsivePropertyTransformers(parentContext.root.node.size), ...createHoverPropertyTransformers(hoveredSignal), ...createActivePropertyTransfomers(activeSignal), } const mergedProperties = computed(() => { - const merged = new MergedProperties(propertyTransformers) - merged.add('backgroundColor', 0xffffff) + const merged = new MergedProperties(prePropertyTransformers) merged.add('aspectRatio', textureAspectRatio) - merged.addAll(defaultPropertiesSignal.value, propertiesSignal.value) + merged.addAll(defaultPropertiesSignal.value, propertiesSignal.value, postTransformers) return merged }) diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index 9526bae0..42085141 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -28,18 +28,17 @@ import { Camera, Matrix4, Plane, Vector2Tuple, Vector3 } from 'three' import { GlyphGroupManager } from '../text/render/instanced-glyph-group.js' import { createGetBatchedProperties } from '../properties/batched.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { preferredColorSchemePropertyTransformers } from '../dark.js' import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { EventHandlers } from '../events.js' -import { getDefaultPanelMaterialConfig } from '../internals.js' +import { darkPropertyTransformers, getDefaultPanelMaterialConfig, traverseProperties } from '../internals.js' -export type InheritableRootProperties = WithConditionals< - WithClasses< +export type InheritableRootProperties = WithClasses< + WithConditionals< WithAllAliases< WithReactive< - Omit & + YogaProperties & TransformProperties & PanelProperties & ScrollbarProperties & @@ -81,10 +80,13 @@ export function createRoot( setupCursorCleanup(hoveredSignal, subscriptions) const pixelSize = properties.pixelSize ?? DEFAULT_PIXEL_SIZE - const transformers: PropertyTransformers = { + const preTransformers: PropertyTransformers = { ...createSizeTranslator(pixelSize, 'sizeX', 'width'), ...createSizeTranslator(pixelSize, 'sizeY', 'height'), - ...preferredColorSchemePropertyTransformers, + } + + const postTransformers = { + ...darkPropertyTransformers, ...createResponsivePropertyTransformers(rootSize), ...createHoverPropertyTransformers(hoveredSignal), ...createActivePropertyTransfomers(activeSignal), @@ -96,8 +98,8 @@ export function createRoot( const onFrameSet = new Set<(delta: number) => void>() const mergedProperties = computed(() => { - const merged = new MergedProperties(transformers) - merged.addAll(defaultProperties, properties) + const merged = new MergedProperties(preTransformers) + merged.addAll(defaultProperties, properties, postTransformers) return merged }) diff --git a/packages/uikit/src/dark.ts b/packages/uikit/src/dark.ts index b0a9b5f2..dc5967c1 100644 --- a/packages/uikit/src/dark.ts +++ b/packages/uikit/src/dark.ts @@ -33,7 +33,7 @@ export function getPreferredColorScheme() { return preferredColorScheme.peek() } -export const preferredColorSchemePropertyTransformers: PropertyTransformers = { +export const darkPropertyTransformers: PropertyTransformers = { dark: createConditionalPropertyTranslator(() => isDarkMode.value), } diff --git a/packages/uikit/src/focus.ts b/packages/uikit/src/focus.ts index d7e8bdda..1a664b90 100644 --- a/packages/uikit/src/focus.ts +++ b/packages/uikit/src/focus.ts @@ -1,6 +1,5 @@ import { createConditionalPropertyTranslator } from './utils.js' import { Signal } from '@preact/signals-core' -import { PropertyTransformers } from './internals.js' export type WithFocus = T & { focus?: T diff --git a/packages/uikit/src/properties/default.ts b/packages/uikit/src/properties/default.ts index 32ac1332..373cea85 100644 --- a/packages/uikit/src/properties/default.ts +++ b/packages/uikit/src/properties/default.ts @@ -27,16 +27,24 @@ export function traverseProperties( fn: (properties: T) => void, ): void { if (defaultProperties != null) { + traverseClasses(defaultProperties.classes as any, fn) fn(defaultProperties as T) } - const { classes } = properties - if (Array.isArray(classes)) { - const classesLength = classes.length - for (let i = 0; i < classesLength; i++) { - fn(classes[i]) - } - } else if (classes != null) { - fn(classes) - } + traverseClasses(properties.classes as any, fn) fn(properties) } + +function traverseClasses(classes: WithClasses['classes'], fn: (properties: T) => void) { + if (classes == null) { + return + } + if (!Array.isArray(classes)) { + fn(classes as T) + return + } + const classesLength = classes.length + for (let i = 0; i < classesLength; i++) { + fn(classes[i]) + } + return +} diff --git a/packages/uikit/src/properties/merged.ts b/packages/uikit/src/properties/merged.ts index dd680f8f..424645c8 100644 --- a/packages/uikit/src/properties/merged.ts +++ b/packages/uikit/src/properties/merged.ts @@ -7,18 +7,14 @@ export type PropertyTransformers = Record>>() - constructor(private transformers: PropertyTransformers) {} - - remove(key: string) { - this.propertyMap.delete(key) - } + constructor(private preTransformers?: PropertyTransformers) {} add(key: string, value: unknown) { if (value === undefined) { //only adding non undefined values to the properties return } - const transform = this.transformers[key] + const transform = this.preTransformers?.[key] if (transform != null) { transform(value, this) return @@ -146,11 +142,22 @@ export class MergedProperties { return shallodwEqual(entry1, entry2) } - addAll(defaultProperties: AllOptionalProperties | undefined, properties: WithClasses): void { + addAll( + defaultProperties: AllOptionalProperties | undefined, + properties: WithClasses, + postTransformers: PropertyTransformers, + ): void { traverseProperties(defaultProperties, properties, (p) => { for (const key in p) { this.add(key, p[key]) } + for (const key in postTransformers) { + const property = p[key] + if (property == null) { + continue + } + postTransformers[key](property, this) + } }) } } diff --git a/packages/uikit/tests/precedence.spec.ts b/packages/uikit/tests/precedence.spec.ts new file mode 100644 index 00000000..4d1489f1 --- /dev/null +++ b/packages/uikit/tests/precedence.spec.ts @@ -0,0 +1,51 @@ +import { expect } from 'chai' +import { MergedProperties } from '../src/properties/index.js' +import { Signal, signal } from '@preact/signals-core' +import { createHoverPropertyTransformers } from '../src/hover.js' + +describe('properties precedence', () => { + it('should use undefined as ignore and null as unset (without signals)', () => { + const merged = new MergedProperties() + merged.add('x', 1) + expect(merged.read('x')).to.equal(1) + merged.add('x', undefined) + expect(merged.read('x')).to.equal(1) + merged.add('x', null) + expect(merged.read('x') == null).to.true + }) + + it('should use undefined as ignore and null as unset (with signals)', () => { + const merged = new MergedProperties() + merged.add('x', signal(1)) + expect(merged.read('x')).to.equal(1) + merged.add('x', signal(undefined)) + expect(merged.read('x')).to.equal(1) + const f = signal(null) + merged.add('x', f) + expect(merged.read('x') == null).to.true + f.value = undefined + expect(merged.read('x')).to.equal(1) + }) + + it('should preserve order default classes (hover/...) -> defaults (hover/...) -> properties classes (hover/...) -> properties (class/hover/...) -> style classes (hover/...) -> styles (class/hover/...)', () => { + const merged = new MergedProperties() + merged.addAll( + { + classes: [{ height: signal(0), hover: { height: signal(1) } }, { height: signal(2) }], + height: signal(3), + hover: { height: signal(4) }, + }, + { + classes: [{ height: signal(5), hover: { height: signal(6) } }, { height: signal(7) }], + height: signal(8), + hover: { height: signal(9) }, + }, + createHoverPropertyTransformers(signal([1])), + ) + const property = merged['propertyMap'].get('height')! + expect(property.map((x) => (x instanceof Signal ? x.value : x))).to.deep.equal( + new Array(10).fill(0).map((_, i) => i), + ) + expect(merged.read('height')).to.equal(9) + }) +}) From 94312ad14f9921d08a278fe9fc10881492156c33 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Wed, 3 Apr 2024 14:46:13 +0200 Subject: [PATCH 10/20] merge/simplify event handlers --- packages/react/src/container.tsx | 24 ++-- packages/react/src/image.tsx | 25 +++-- packages/react/src/ref.ts | 46 +++++--- packages/react/src/root.tsx | 29 +++-- packages/react/src/utilts.tsx | 34 +++--- packages/uikit/src/components/container.ts | 39 +++---- packages/uikit/src/components/image.ts | 34 +++--- packages/uikit/src/components/root.ts | 35 +++--- packages/uikit/src/events.ts | 2 + packages/uikit/src/focus.ts | 2 +- .../uikit/src/panel/instanced-panel-mesh.ts | 21 ++-- packages/uikit/src/scroll.ts | 104 +++++++++--------- packages/uikit/src/vanilla/container.ts | 21 ++-- packages/uikit/src/vanilla/image.ts | 19 ++-- packages/uikit/src/vanilla/root.ts | 20 ++-- packages/uikit/src/vanilla/utils.ts | 19 +--- 16 files changed, 238 insertions(+), 236 deletions(-) diff --git a/packages/react/src/container.tsx b/packages/react/src/container.tsx index abf00072..42bff8f7 100644 --- a/packages/react/src/container.tsx +++ b/packages/react/src/container.tsx @@ -1,33 +1,33 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' -import { forwardRef, ReactNode, useEffect, useMemo, useRef } from 'react' +import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' import { Object3D } from 'three' import { ParentProvider, useParent } from './context.js' -import { AddHandlers, AddScrollHandler } from './utilts.js' +import { AddHandlers, usePropertySignals } from './utilts.js' import { ContainerProperties, createContainer, destroyContainer } from '@vanilla-three/uikit/internals' -import { useDefaultProperties } from './default.js' +import { ComponentInternals, useComponentInternals } from './ref.js' export const Container: ( props: { children?: ReactNode } & ContainerProperties & - EventHandlers, + EventHandlers & + RefAttributes, ) => ReactNode = forwardRef((properties, ref) => { - //TODO: ComponentInternals const parent = useParent() const outerRef = useRef(null) const innerRef = useRef(null) - const defaultProperties = useDefaultProperties() - // eslint-disable-next-line react-hooks/exhaustive-deps - const internals = useMemo(() => createContainer(parent, properties, defaultProperties, outerRef, innerRef), [parent]) + const propertySignals = usePropertySignals(properties) + const internals = useMemo( + () => createContainer(parent, propertySignals.properties, propertySignals.default, outerRef, innerRef), + [parent, propertySignals], + ) useEffect(() => () => destroyContainer(internals), [internals]) - //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) + useComponentInternals(ref, propertySignals.style, internals) return ( - - - + {properties.children} diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx index 7321339f..b90c504a 100644 --- a/packages/react/src/image.tsx +++ b/packages/react/src/image.tsx @@ -1,27 +1,28 @@ import { createImage, ImageProperties, destroyImage } from '@vanilla-three/uikit/internals' -import { ReactNode, forwardRef, useEffect, useMemo, useRef } from 'react' +import { ReactNode, RefAttributes, forwardRef, useEffect, useMemo, useRef } from 'react' import { Object3D } from 'three' -import { AddHandlers, AddScrollHandler } from './utilts.js' +import { AddHandlers, usePropertySignals } from './utilts.js' import { ParentProvider, useParent } from './context.js' -import { useDefaultProperties } from './default.js' +import { ComponentInternals, useComponentInternals } from './ref.js' -export const Image: (props: ImageProperties & { children?: ReactNode }) => ReactNode = forwardRef((properties, ref) => { - //TODO: ComponentInternals +export const Image: ( + props: ImageProperties & RefAttributes & { children?: ReactNode }, +) => ReactNode = forwardRef((properties, ref) => { const parent = useParent() const outerRef = useRef(null) const innerRef = useRef(null) - const defaultProperties = useDefaultProperties() - // eslint-disable-next-line react-hooks/exhaustive-deps - const internals = useMemo(() => createImage(parent, properties, defaultProperties, outerRef, innerRef), [parent]) + const propertySignals = usePropertySignals(properties) + const internals = useMemo( + () => createImage(parent, propertySignals.properties, propertySignals.default, outerRef, innerRef), + [parent, propertySignals], + ) useEffect(() => () => destroyImage(internals), [internals]) - //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) + useComponentInternals(ref, propertySignals.style, internals) return ( - - - + {properties.children} diff --git a/packages/react/src/ref.ts b/packages/react/src/ref.ts index 23935060..2db8c044 100644 --- a/packages/react/src/ref.ts +++ b/packages/react/src/ref.ts @@ -1,7 +1,6 @@ import { ReadonlySignal, Signal } from '@preact/signals-core' -import { Inset, FlexNode } from '@vanilla-three/uikit/internals' -import { utils } from 'mocha' -import { ForwardedRef, RefObject, useImperativeHandle } from 'react' +import { Inset, createContainer, createImage, createRoot } from '@vanilla-three/uikit/internals' +import { ForwardedRef, useImperativeHandle } from 'react' import { Vector2Tuple, Mesh } from 'three' export type ComponentInternals = { @@ -15,19 +14,34 @@ export type ComponentInternals = { interactionPanel: Mesh } -export function useComponentInternals( +export function useComponentInternals( ref: ForwardedRef, - node: FlexNode, - interactionPanel: Mesh | RefObject, - scrollPosition?: Signal, + styleSignal: Signal, + internals: ReturnType & { + scrollPosition?: Signal + }, ): void { - useImperativeHandle(ref, () => ({ - borderInset: node.borderInset, - paddingInset: node.paddingInset, - center: node.relativeCenter, - maxScrollPosition: node.maxScrollPosition, - size: node.size, - interactionPanel: interactionPanel instanceof Mesh ? interactionPanel : interactionPanel.current!, - scrollPosition, - })) + useImperativeHandle( + ref, + () => { + const { + scrollPosition, + node, + root: { pixelSize }, + interactionPanel, + } = internals + return { + setStyle: (style: T) => (styleSignal.value = style), + pixelSize, + borderInset: node.borderInset, + paddingInset: node.paddingInset, + center: node.relativeCenter, + maxScrollPosition: node.maxScrollPosition, + size: node.size, + interactionPanel, + scrollPosition, + } + }, + [styleSignal, internals], + ) } diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx index 28edf5fa..09af543a 100644 --- a/packages/react/src/root.tsx +++ b/packages/react/src/root.tsx @@ -1,16 +1,17 @@ import { useFrame, useStore, useThree } from '@react-three/fiber' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' -import { forwardRef, ReactNode, useEffect, useMemo, useRef } from 'react' +import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' import { ParentProvider } from './context.js' -import { AddHandlers, AddScrollHandler } from './utilts.js' +import { AddHandlers, usePropertySignals } from './utilts.js' import { RootProperties, patchRenderOrder, createRoot, destroyRoot } from '@vanilla-three/uikit/internals' -import { useDefaultProperties } from './default.js' import { Object3D } from 'three' +import { ComponentInternals, useComponentInternals } from './ref.js' export const Root: ( props: RootProperties & { children?: ReactNode - } & EventHandlers, + } & EventHandlers & + RefAttributes, ) => ReactNode = forwardRef((properties, ref) => { const renderer = useThree((state) => state.gl) @@ -18,11 +19,17 @@ export const Root: ( const store = useStore() const outerRef = useRef(null) const innerRef = useRef(null) - const defaultProperties = useDefaultProperties() + const propertySignals = usePropertySignals(properties) const internals = useMemo( - () => createRoot(properties, defaultProperties, outerRef, innerRef, () => store.getState().camera), - // eslint-disable-next-line react-hooks/exhaustive-deps - [store], + () => + createRoot( + propertySignals.properties, + propertySignals.default, + outerRef, + innerRef, + () => store.getState().camera, + ), + [store, propertySignals], ) useEffect(() => () => destroyRoot(internals), [internals]) @@ -32,13 +39,11 @@ export const Root: ( } }) - //TBD: useComponentInternals(ref, node, interactionPanel, scrollPosition) + useComponentInternals(ref, propertySignals.style, internals) return ( - - - + {properties.children} diff --git a/packages/react/src/utilts.tsx b/packages/react/src/utilts.tsx index 24389fab..98ff2eb3 100644 --- a/packages/react/src/utilts.tsx +++ b/packages/react/src/utilts.tsx @@ -1,7 +1,9 @@ -import { Signal, effect } from '@preact/signals-core' +import { Signal, effect, signal } from '@preact/signals-core' import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' import { ReactNode, forwardRef, useEffect, useMemo, useState } from 'react' import { Object3D } from 'three' +import { useDefaultProperties } from './default.js' +import { AllOptionalProperties } from '@vanilla-three/uikit/internals' export const AddHandlers = forwardRef; children?: ReactNode }>( ({ handlers: handlersSignal, children }, ref) => { @@ -15,24 +17,22 @@ export const AddHandlers = forwardRef - children?: ReactNode -}) { - const [scrollHandlers, setScrollHandlers] = useState(() => handlers.value) - useSignalEffect(() => setScrollHandlers(handlers.value), [handlers]) - return ( - - {children} - - ) -} - export function useSignalEffect(fn: () => (() => void) | void, deps: Array) { // eslint-disable-next-line react-hooks/exhaustive-deps const unsubscribe = useMemo(() => effect(fn), deps) useEffect(() => unsubscribe, [unsubscribe]) } + +export function usePropertySignals(properties: T) { + const propertySignals = useMemo( + () => ({ + style: signal(undefined), + properties: signal(undefined as any), + default: signal(undefined), + }), + [], + ) + propertySignals.properties.value = properties + propertySignals.default.value = useDefaultProperties() + return propertySignals +} diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 5615fc7d..04207d9f 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -16,14 +16,14 @@ import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/ import { createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { computed, signal } from '@preact/signals-core' +import { Signal, computed, signal } from '@preact/signals-core' import { WithConditionals, computedGlobalMatrix } from './utils.js' import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' import { MergedProperties } from '../properties/merged.js' import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' import { Object3DRef, WithContext } from '../context.js' import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' -import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { addHandlers, cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { EventHandlers } from '../events.js' import { darkPropertyTransformers, getDefaultPanelMaterialConfig, traverseProperties } from '../internals.js' @@ -46,8 +46,8 @@ export type ContainerProperties = InheritableContainerProperties & Listeners & E export function createContainer( parentContext: WithContext, - properties: ContainerProperties, - defaultProperties: AllOptionalProperties | undefined, + properties: Signal, + defaultProperties: Signal, object: Object3DRef, childrenContainer: Object3DRef, ) { @@ -56,10 +56,6 @@ export function createContainer( const subscriptions = [] as Subscriptions setupCursorCleanup(hoveredSignal, subscriptions) - const scrollHandlers = signal({}) - const propertiesSignal = signal(properties) - const defaultPropertiesSignal = signal(defaultProperties) - const postTranslators = { ...darkPropertyTransformers, ...createResponsivePropertyTransformers(parentContext.root.node.size), @@ -69,7 +65,7 @@ export function createContainer( const mergedProperties = computed(() => { const merged = new MergedProperties() - merged.addAll(defaultPropertiesSignal.value, propertiesSignal.value, postTranslators) + merged.addAll(defaultProperties.value, properties.value, postTranslators) return merged }) @@ -125,28 +121,25 @@ export function createContainer( parentContext.clippingRect, ) - setupLayoutListeners(propertiesSignal, node.size, subscriptions) - setupViewportListeners(propertiesSignal, isClipped, subscriptions) + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) - const onScrollFrame = setupScrollHandler( + const scrollHandlers = setupScrollHandler( node, scrollPosition, object, - propertiesSignal, + properties, parentContext.root.pixelSize, - scrollHandlers, + parentContext.root.onFrameSet, subscriptions, ) - parentContext.root.onFrameSet.add(onScrollFrame) subscriptions.push(() => { - parentContext.root.onFrameSet.delete(onScrollFrame) parentContext.node.removeChild(node) node.destroy() }) return { - scrollHandlers, isClipped, clippingRect, matrix, @@ -154,8 +147,7 @@ export function createContainer( object, orderInfo, root: parentContext.root, - propertiesSignal, - defaultPropertiesSignal, + scrollPosition, interactionPanel: createInteractionPanel( node, orderInfo, @@ -164,11 +156,10 @@ export function createContainer( subscriptions, ), handlers: computed(() => { - const properties = propertiesSignal.value - const defaultProperties = defaultPropertiesSignal.value - const handlers = cloneHandlers(properties) - addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal) - addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + const handlers = cloneHandlers(properties.value) + addHandlers(handlers, scrollHandlers.value) + addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) + addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) return handlers }), subscriptions, diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index c0af2695..d01c5134 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -42,7 +42,7 @@ import { setupLayoutListeners, setupViewportListeners } from '../listeners.js' import { createGetBatchedProperties } from '../properties/batched.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' -import { cloneHandlers } from '../panel/instanced-panel-mesh.js' +import { addHandlers, cloneHandlers } from '../panel/instanced-panel-mesh.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { EventHandlers } from '../events.js' import { @@ -77,8 +77,8 @@ export type ImageProperties = InheritableImageProperties & Listeners & EventHand export function createImage( parentContext: WithContext, - properties: ImageProperties, - defaultProperties: AllOptionalProperties | undefined, + properties: Signal, + defaultProperties: Signal, object: Object3DRef, childrenContainer: Object3DRef, ) { @@ -87,11 +87,8 @@ export function createImage( const hoveredSignal = signal>([]) const activeSignal = signal>([]) setupCursorCleanup(hoveredSignal, subscriptions) - const scrollHandlers = signal({}) - const propertiesSignal = signal(properties) - const defaultPropertiesSignal = signal(defaultProperties) - const src = computed(() => readReactive(propertiesSignal.value.src)) + const src = computed(() => readReactive(properties.value.src)) loadResourceWithParams(texture, loadTextureImpl, subscriptions, src) const textureAspectRatio = computed(() => { @@ -126,7 +123,7 @@ export function createImage( const mergedProperties = computed(() => { const merged = new MergedProperties(prePropertyTransformers) merged.add('aspectRatio', textureAspectRatio) - merged.addAll(defaultPropertiesSignal.value, propertiesSignal.value, postTransformers) + merged.addAll(defaultProperties.value, properties.value, postTransformers) return merged }) @@ -167,22 +164,20 @@ export function createImage( parentContext.clippingRect, ) - setupLayoutListeners(propertiesSignal, node.size, subscriptions) - setupViewportListeners(propertiesSignal, isClipped, subscriptions) + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) - const onScrollFrame = setupScrollHandler( + const scrollHandlers = setupScrollHandler( node, scrollPosition, object, - propertiesSignal, + properties, parentContext.root.pixelSize, - scrollHandlers, + parentContext.root.onFrameSet, subscriptions, ) - parentContext.root.onFrameSet.add(onScrollFrame) subscriptions.push(() => { - parentContext.root.onFrameSet.delete(onScrollFrame) parentContext.node.removeChild(node) node.destroy() }) @@ -198,12 +193,11 @@ export function createImage( return Object.assign(ctx, { subscriptions, scrollHandlers, - propertiesSignal, - defaultPropertiesSignal, handlers: computed(() => { - const handlers = cloneHandlers(properties) - addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal) - addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + const handlers = cloneHandlers(properties.value) + addHandlers(handlers, scrollHandlers.value) + addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) + addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) return handlers }), interactionPanel: createImageMesh(mergedProperties, texture, parentContext, ctx, isHidden, subscriptions), diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index 42085141..823239aa 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -1,4 +1,4 @@ -import { Signal, computed, signal } from '@preact/signals-core' +import { Signal, computed, signal, untracked } from '@preact/signals-core' import { Object3DRef, RootContext } from '../context.js' import { FlexNode, YogaProperties } from '../flex/index.js' import { LayoutListeners, ScrollListeners, setupLayoutListeners } from '../listeners.js' @@ -29,7 +29,7 @@ import { GlyphGroupManager } from '../text/render/instanced-glyph-group.js' import { createGetBatchedProperties } from '../properties/batched.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' -import { cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { addHandler, addHandlers, cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { EventHandlers } from '../events.js' import { darkPropertyTransformers, getDefaultPanelMaterialConfig, traverseProperties } from '../internals.js' @@ -67,8 +67,8 @@ const planeHelper = new Plane() const notClipped = signal(false) export function createRoot( - properties: RootProperties, - defaultProperties: AllOptionalProperties | undefined, + properties: Signal, + defaultProperties: Signal, object: Object3DRef, childrenContainer: Object3DRef, getCamera: () => Camera, @@ -78,7 +78,7 @@ export function createRoot( const activeSignal = signal>([]) const subscriptions = [] as Subscriptions setupCursorCleanup(hoveredSignal, subscriptions) - const pixelSize = properties.pixelSize ?? DEFAULT_PIXEL_SIZE + const pixelSize = untracked(() => properties.value.pixelSize ?? DEFAULT_PIXEL_SIZE) const preTransformers: PropertyTransformers = { ...createSizeTranslator(pixelSize, 'sizeX', 'width'), @@ -92,14 +92,11 @@ export function createRoot( ...createActivePropertyTransfomers(activeSignal), } - const scrollHandlers = signal({}) - const propertiesSignal = signal(properties) - const defaultPropertiesSignal = signal(defaultProperties) const onFrameSet = new Set<(delta: number) => void>() const mergedProperties = computed(() => { const merged = new MergedProperties(preTransformers) - merged.addAll(defaultProperties, properties, postTransformers) + merged.addAll(defaultProperties.value, properties.value, postTransformers) return merged }) @@ -174,19 +171,17 @@ export function createRoot( undefined, ) - setupLayoutListeners(propertiesSignal, node.size, subscriptions) + setupLayoutListeners(properties, node.size, subscriptions) - const onScrollFrame = setupScrollHandler( + const scrollHandlers = setupScrollHandler( node, scrollPosition, object, - propertiesSignal, + properties, pixelSize, - scrollHandlers, + onFrameSet, subscriptions, ) - onFrameSet.add(onScrollFrame) - subscriptions.push(() => onFrameSet.delete(onScrollFrame)) const gylphGroupManager = new GlyphGroupManager(pixelSize, ctx, object) onFrameSet.add(gylphGroupManager.onFrame) subscriptions.push(() => onFrameSet.delete(gylphGroupManager.onFrame)) @@ -207,14 +202,12 @@ export function createRoot( return Object.assign(rootCtx, { subscriptions, - propertiesSignal, - defaultPropertiesSignal, - scrollHandlers, interactionPanel: createInteractionPanel(node, orderInfo, rootCtx, undefined, subscriptions), handlers: computed(() => { - const handlers = cloneHandlers(properties) - addHoverHandlers(handlers, properties, defaultProperties, hoveredSignal) - addActiveHandlers(handlers, properties, defaultProperties, activeSignal) + const handlers = cloneHandlers(properties.value) + addHandlers(handlers, scrollHandlers.value) + addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) + addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) return handlers }), root: rootCtx, diff --git a/packages/uikit/src/events.ts b/packages/uikit/src/events.ts index 66b55cc4..3c374d3f 100644 --- a/packages/uikit/src/events.ts +++ b/packages/uikit/src/events.ts @@ -6,6 +6,8 @@ export type ThreeEvent = Intersection & { stopped?: boolean } +export type KeyToEvent = Parameters[K]>[0] + export type EventHandlers = { onClick?: (event: ThreeEvent) => void onContextMenu?: (event: ThreeEvent) => void diff --git a/packages/uikit/src/focus.ts b/packages/uikit/src/focus.ts index 1a664b90..8b35fa46 100644 --- a/packages/uikit/src/focus.ts +++ b/packages/uikit/src/focus.ts @@ -6,7 +6,7 @@ export type WithFocus = T & { onFocusChange?: (focus: boolean) => void } -export function createFocusPropertyTransformers(hasFocusSignal: Signal>): PropertyTransformers { +export function createFocusPropertyTransformers(hasFocusSignal: Signal>) { return { hover: createConditionalPropertyTranslator(() => hasFocusSignal.value.length > 0), } diff --git a/packages/uikit/src/panel/instanced-panel-mesh.ts b/packages/uikit/src/panel/instanced-panel-mesh.ts index cae067ed..28f8a7c4 100644 --- a/packages/uikit/src/panel/instanced-panel-mesh.ts +++ b/packages/uikit/src/panel/instanced-panel-mesh.ts @@ -4,7 +4,7 @@ import { instancedPanelDepthMaterial, instancedPanelDistanceMaterial } from './p import { Signal, effect } from '@preact/signals-core' import { Subscriptions } from '../utils.js' import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' -import { EventHandlers, ThreeEvent } from '../events.js' +import { EventHandlers, KeyToEvent, ThreeEvent } from '../events.js' import { OrderInfo } from '../order.js' import { ClippingRect, FlexNode, RootContext } from '../internals.js' @@ -48,26 +48,25 @@ export function cloneHandlers(handlers: EventHandlers): EventHandlers { } } -function setHandler(key: K, target: EventHandlers, fn: EventHandlers[K]) { - if (fn == null) { - return +export function addHandlers(target: EventHandlers, handlers: EventHandlers | undefined) { + for (const key in handlers) { + addHandler(key as any, target, handlers[key as never]) } - target[key] = fn } -export function addHandler( - key: Exclude, +export function addHandler( + key: K, target: EventHandlers, - handler: (event: ThreeEvent) => void, + handler: (event: KeyToEvent) => void, ): void { const existingHandler = target[key] if (existingHandler == null) { target[key] = handler return } - target[key] = (e: ThreeEvent) => { - existingHandler(e) - if (e.stopped) { + target[key] = (e: KeyToEvent) => { + existingHandler(e as any) + if ('stopped' in e && e.stopped) { return } handler(e) diff --git a/packages/uikit/src/scroll.ts b/packages/uikit/src/scroll.ts index ee1fb399..a1ae3df9 100644 --- a/packages/uikit/src/scroll.ts +++ b/packages/uikit/src/scroll.ts @@ -24,7 +24,7 @@ const localPointHelper = new Vector3() export type ScrollEventHandlers = Pick< EventHandlers, - 'onPointerDown' | 'onPointerUp' | 'onPointerMove' | 'onWheel' | 'onPointerLeave' + 'onPointerDown' | 'onPointerUp' | 'onPointerMove' | 'onWheel' | 'onPointerLeave' | 'onPointerCancel' > export function createScrollPosition() { @@ -60,7 +60,7 @@ export function setupScrollHandler( object: Object3DRef, listeners: Signal, pixelSize: number, - scrollHandlers: Signal, + onFrameSet: Set<(delta: number) => void>, subscriptions: Subscriptions, ) { const isScrollable = computed(() => node.scrollable.value.some((scrollable) => scrollable)) @@ -107,56 +107,7 @@ export function setupScrollHandler( scrollPosition.value = [newX, newY] } - subscriptions.push( - effect(() => { - if (!isScrollable.value) { - scrollHandlers.value = undefined - } - scrollHandlers.value = { - onPointerDown: ({ nativeEvent, point }) => { - let interaction = downPointerMap.get(nativeEvent.pointerId) - if (interaction == null) { - downPointerMap.set(nativeEvent.pointerId, (interaction = { timestamp: 0, point: new Vector3() })) - } - interaction.timestamp = performance.now() / 1000 - object.current!.worldToLocal(interaction.point.copy(point)) - }, - onPointerUp: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), - onPointerLeave: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), - onPointerCancel: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), - onContextMenu: (e) => e.nativeEvent.preventDefault(), - onPointerMove: (event) => { - const prevInteraction = downPointerMap.get(event.nativeEvent.pointerId) - - if (prevInteraction == null) { - return - } - object.current!.worldToLocal(localPointHelper.copy(event.point)) - distanceHelper.copy(localPointHelper).sub(prevInteraction.point).divideScalar(pixelSize) - const timestamp = performance.now() / 1000 - const deltaTime = timestamp - prevInteraction.timestamp - - prevInteraction.point.copy(localPointHelper) - prevInteraction.timestamp = timestamp - - if (event.defaultPrevented) { - return - } - - scroll(event, -distanceHelper.x, distanceHelper.y, deltaTime, true) - }, - onWheel: (event) => { - if (event.defaultPrevented) { - return - } - const { nativeEvent } = event - scroll(event, nativeEvent.deltaX, nativeEvent.deltaY, undefined, false) - }, - } - }), - ) - - return (delta: number) => { + const onFrame = (delta: number) => { if (downPointerMap.size > 0) { return } @@ -187,6 +138,55 @@ export function setupScrollHandler( } scroll(undefined, deltaX, deltaY, undefined, true) } + + onFrameSet.add(onFrame) + subscriptions.push(() => onFrameSet.delete(onFrame)) + + return computed(() => { + if (!isScrollable.value) { + return undefined + } + return { + onPointerDown: ({ nativeEvent, point }) => { + let interaction = downPointerMap.get(nativeEvent.pointerId) + if (interaction == null) { + downPointerMap.set(nativeEvent.pointerId, (interaction = { timestamp: 0, point: new Vector3() })) + } + interaction.timestamp = performance.now() / 1000 + object.current!.worldToLocal(interaction.point.copy(point)) + }, + onPointerUp: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), + onPointerLeave: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), + onPointerCancel: ({ nativeEvent }) => downPointerMap.delete(nativeEvent.pointerId), + onPointerMove: (event) => { + const prevInteraction = downPointerMap.get(event.nativeEvent.pointerId) + + if (prevInteraction == null) { + return + } + object.current!.worldToLocal(localPointHelper.copy(event.point)) + distanceHelper.copy(localPointHelper).sub(prevInteraction.point).divideScalar(pixelSize) + const timestamp = performance.now() / 1000 + const deltaTime = timestamp - prevInteraction.timestamp + + prevInteraction.point.copy(localPointHelper) + prevInteraction.timestamp = timestamp + + if (event.defaultPrevented) { + return + } + + scroll(event, -distanceHelper.x, distanceHelper.y, deltaTime, true) + }, + onWheel: (event) => { + if (event.defaultPrevented) { + return + } + const { nativeEvent } = event + scroll(event, nativeEvent.deltaX, nativeEvent.deltaY, undefined, false) + }, + } + }) } const wasScrolledSymbol = Symbol('was-scrolled') diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts index fd51fc88..cf19ddf5 100644 --- a/packages/uikit/src/vanilla/container.ts +++ b/packages/uikit/src/vanilla/container.ts @@ -3,16 +3,20 @@ import { ContainerProperties, createContainer, destroyContainer } from '../compo import { AllOptionalProperties, Properties } from '../properties/default.js' import { Component } from './index.js' import { EventConfig, bindHandlers } from './utils.js' -import { batch } from '@preact/signals-core' +import { Signal, batch, signal } from '@preact/signals-core' export class Container extends Object3D { private object: Object3D public readonly internals: ReturnType public readonly eventConfig: EventConfig + private readonly propertiesSignal: Signal + private readonly defaultPropertiesSignal: Signal + constructor(parent: Component, properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { super() - + this.propertiesSignal = signal(properties) + this.defaultPropertiesSignal = signal(defaultProperties) this.eventConfig = parent.eventConfig //setting up the threejs elements this.object = new Object3D() @@ -24,21 +28,22 @@ export class Container extends Object3D { //setting up the container this.internals = createContainer( parent.internals, - properties, - defaultProperties, + this.propertiesSignal, + this.defaultPropertiesSignal, { current: this.object }, { current: this }, ) //setup scrolling & events - this.add(this.internals.interactionPanel) - bindHandlers(this.internals, this, this.internals.interactionPanel, this.eventConfig) + const { handlers, interactionPanel, subscriptions } = this.internals + this.add(interactionPanel) + bindHandlers(handlers, interactionPanel, this.eventConfig, subscriptions) } setProperties(properties: Properties, defaultProperties?: AllOptionalProperties) { batch(() => { - this.internals.propertiesSignal.value = properties - this.internals.defaultPropertiesSignal.value = defaultProperties + this.propertiesSignal.value = properties + this.defaultPropertiesSignal.value = defaultProperties }) } diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index 58b63e57..b81ee56f 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -3,16 +3,20 @@ import { ImageProperties, createImage, destroyImage } from '../components/image. import { AllOptionalProperties } from '../properties/default.js' import { Component } from './index.js' import { EventConfig, bindHandlers } from './utils.js' -import { batch } from '@preact/signals-core' +import { Signal, batch, signal } from '@preact/signals-core' export class Image extends Object3D { public readonly internals: ReturnType public readonly eventConfig: EventConfig private container: Object3D + private readonly propertiesSignal: Signal + private readonly defaultPropertiesSignal: Signal constructor(parent: Component, properties: ImageProperties, defaultProperties?: AllOptionalProperties) { super() + this.propertiesSignal = signal(properties) + this.defaultPropertiesSignal = signal(defaultProperties) this.eventConfig = parent.eventConfig this.container = new Object3D() this.container.matrixAutoUpdate = false @@ -21,21 +25,22 @@ export class Image extends Object3D { parent.add(this.container) this.internals = createImage( parent.internals, - properties, - defaultProperties, + this.propertiesSignal, + this.defaultPropertiesSignal, { current: this }, { current: this.container }, ) this.setProperties(properties, defaultProperties) - this.container.add(this.internals.interactionPanel) - bindHandlers(this.internals, this, this.internals.interactionPanel, this.eventConfig) + const { handlers, interactionPanel, subscriptions } = this.internals + this.container.add(interactionPanel) + bindHandlers(handlers, interactionPanel, this.eventConfig, subscriptions) } setProperties(properties: ImageProperties, defaultProperties?: AllOptionalProperties) { batch(() => { - this.internals.propertiesSignal.value = properties - this.internals.defaultPropertiesSignal.value = defaultProperties + this.propertiesSignal.value = properties + this.defaultPropertiesSignal.value = defaultProperties }) } diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index dc2ee4e1..86983a56 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -1,5 +1,5 @@ import { Camera, Object3D } from 'three' -import { batch } from '@preact/signals-core' +import { Signal, batch, signal } from '@preact/signals-core' import { AllOptionalProperties } from '../properties/default.js' import { createRoot, destroyRoot, RootProperties } from '../components/root.js' import { EventConfig, bindHandlers } from './utils.js' @@ -8,6 +8,9 @@ export class Root extends Object3D { public readonly internals: ReturnType private object: Object3D + private readonly propertiesSignal: Signal + private readonly defaultPropertiesSignal: Signal + constructor( public readonly eventConfig: EventConfig, camera: Camera | (() => Camera), @@ -16,6 +19,8 @@ export class Root extends Object3D { defaultProperties?: AllOptionalProperties, ) { super() + this.propertiesSignal = signal(properties) + this.defaultPropertiesSignal = signal(defaultProperties) this.object = new Object3D() this.object.matrixAutoUpdate = false this.object.add(this) @@ -23,16 +28,17 @@ export class Root extends Object3D { parent.add(this.object) this.internals = createRoot( - properties, - defaultProperties, + this.propertiesSignal, + this.defaultPropertiesSignal, { current: this }, { current: this }, typeof camera === 'function' ? camera : () => camera, ) //setup scrolling & events - this.add(this.internals.interactionPanel) - bindHandlers(this.internals, this, this.internals.interactionPanel, this.eventConfig) + const { handlers, interactionPanel, subscriptions } = this.internals + this.add(interactionPanel) + bindHandlers(handlers, interactionPanel, this.eventConfig, subscriptions) } update(delta: number) { @@ -43,8 +49,8 @@ export class Root extends Object3D { setProperties(properties: RootProperties, defaultProperties?: AllOptionalProperties) { batch(() => { - this.internals.propertiesSignal.value = properties - this.internals.defaultPropertiesSignal.value = defaultProperties + this.propertiesSignal.value = properties + this.defaultPropertiesSignal.value = defaultProperties }) } diff --git a/packages/uikit/src/vanilla/utils.ts b/packages/uikit/src/vanilla/utils.ts index e4ebdbb4..389ac628 100644 --- a/packages/uikit/src/vanilla/utils.ts +++ b/packages/uikit/src/vanilla/utils.ts @@ -1,7 +1,7 @@ import { Signal, effect } from '@preact/signals-core' import { Subscriptions } from '../utils.js' import { EventHandlers } from '../events.js' -import { Mesh, Object3D } from 'three' +import { Object3D } from 'three' export type EventConfig = { bindEventHandlers: (object: Object3D, handlers: EventHandlers) => void @@ -9,18 +9,10 @@ export type EventConfig = { } export function bindHandlers( - { - scrollHandlers, - handlers, - subscriptions, - }: { - scrollHandlers: Signal - handlers: Signal - subscriptions: Subscriptions - }, + handlers: Signal, container: Object3D, - mesh: Mesh, eventConfig: EventConfig, + subscriptions: Subscriptions, ) { subscriptions.push( effect(() => { @@ -28,10 +20,5 @@ export function bindHandlers( eventConfig.bindEventHandlers(container, value) return () => eventConfig.unbindEventHandlers(container, value) }), - effect(() => { - const { value } = scrollHandlers - eventConfig.bindEventHandlers(mesh, value) - return () => eventConfig.unbindEventHandlers(mesh, value) - }), ) } From 7d01da47a820a7422c5b1b7fa2998f0fe15f52e7 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Wed, 3 Apr 2024 15:58:32 +0200 Subject: [PATCH 11/20] port text --- examples/vanilla/index.ts | 21 +- examples/vanilla/vite.config.ts | 4 + packages/react/src/font.tsx | 9 + packages/react/src/root.tsx | 7 +- packages/react/src/text.tsx | 159 ++++---------- packages/uikit/src/components/container.ts | 2 +- packages/uikit/src/components/index.ts | 5 +- packages/uikit/src/components/root.ts | 6 +- packages/uikit/src/components/text.ts | 206 ++++++++++++++++++ packages/uikit/src/components/utils.tsx | 1 - packages/uikit/src/context.ts | 3 +- packages/uikit/src/flex/node.ts | 37 +++- packages/uikit/src/flex/utils.ts | 21 -- packages/uikit/src/index.ts | 2 +- packages/uikit/src/internals.ts | 4 +- packages/uikit/src/order.ts | 12 +- packages/uikit/src/text/font.ts | 7 +- .../uikit/src/text/render/instanced-glyph.ts | 20 +- packages/uikit/src/vanilla/index.ts | 10 +- packages/uikit/src/vanilla/root.ts | 4 +- packages/uikit/src/vanilla/text.ts | 76 +++++++ 21 files changed, 428 insertions(+), 188 deletions(-) create mode 100644 packages/uikit/src/components/text.ts delete mode 100644 packages/uikit/src/flex/utils.ts create mode 100644 packages/uikit/src/vanilla/text.ts diff --git a/examples/vanilla/index.ts b/examples/vanilla/index.ts index 8b021c8e..689a90b0 100644 --- a/examples/vanilla/index.ts +++ b/examples/vanilla/index.ts @@ -1,12 +1,12 @@ import { PerspectiveCamera, Scene, WebGLRenderer } from 'three' -import { patchRenderOrder, Container, Root, Image } from '@vanilla-three/uikit' +import { Container, Root, Image, Text } from '@vanilla-three/uikit' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' -import { EventHandlers } from '@vanilla-three/uikit/internals' +import { EventHandlers, reversePainterSortStable } from '@vanilla-three/uikit/internals' // init -const camera = new PerspectiveCamera(70, 1, 0.01, 10) -camera.position.z = 1 +const camera = new PerspectiveCamera(70, 1, 0.01, 100) +camera.position.z = 10 const scene = new Scene() @@ -17,6 +17,8 @@ function handlerToEventName(key: string) { return key[2].toLocaleLowerCase() + key.slice(3) } +const renderer = new WebGLRenderer({ antialias: true, canvas }) + //UI const root = new Root( { @@ -40,16 +42,19 @@ const root = new Root( }, }, camera, + renderer, scene, { flexDirection: 'row', gap: 10, padding: 10, - sizeX: 1, - sizeY: 0.5, + sizeX: 15, + sizeY: 5, backgroundColor: 'red', }, ) + +new Text(root, 'Hello World', undefined, { fontSize: 50 }) new Container(root, { flexGrow: 1, backgroundColor: 'blue' }) const x = new Container(root, { padding: 30, @@ -67,13 +72,13 @@ new Image(x, { src: 'https://picsum.photos/300/300', }) -const renderer = new WebGLRenderer({ antialias: true, canvas }) renderer.setAnimationLoop(animation) renderer.localClippingEnabled = true -patchRenderOrder(renderer) +renderer.setTransparentSort(reversePainterSortStable) function updateSize() { renderer.setSize(window.innerWidth, window.innerHeight) + renderer.setPixelRatio(window.devicePixelRatio) camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() } diff --git a/examples/vanilla/vite.config.ts b/examples/vanilla/vite.config.ts index 854db0f0..1b1f0245 100644 --- a/examples/vanilla/vite.config.ts +++ b/examples/vanilla/vite.config.ts @@ -6,6 +6,10 @@ export default defineConfig({ resolve: { alias: [ { find: '@', replacement: path.resolve(__dirname, '../../packages/kits/default') }, + { + find: '@vanilla-three/uikit/internals', + replacement: path.resolve(__dirname, '../../packages/uikit/src/internals.ts'), + }, { find: '@vanilla-three/uikit', replacement: path.resolve(__dirname, '../../packages/uikit/src/index.ts') }, ], }, diff --git a/packages/react/src/font.tsx b/packages/react/src/font.tsx index b26fdc6f..ba669aaf 100644 --- a/packages/react/src/font.tsx +++ b/packages/react/src/font.tsx @@ -1,3 +1,8 @@ +import { FontFamilies, FontFamilyUrls } from '@vanilla-three/uikit/internals' +import { useContext, createContext, ReactNode } from 'react' + +const FontFamiliesContext = createContext(null as any) + export function FontFamilyProvider(properties: { [Key in T]: Key extends 'children' ? ReactNode : FontFamilyUrls }) { @@ -8,3 +13,7 @@ export function FontFamilyProvider(properties: { } return {children} } + +export function useFontFamilies(): FontFamilies | undefined { + return useContext(FontFamiliesContext) +} diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx index 09af543a..dc1926af 100644 --- a/packages/react/src/root.tsx +++ b/packages/react/src/root.tsx @@ -3,7 +3,7 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/eve import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' import { ParentProvider } from './context.js' import { AddHandlers, usePropertySignals } from './utilts.js' -import { RootProperties, patchRenderOrder, createRoot, destroyRoot } from '@vanilla-three/uikit/internals' +import { RootProperties, createRoot, destroyRoot, reversePainterSortStable } from '@vanilla-three/uikit/internals' import { Object3D } from 'three' import { ComponentInternals, useComponentInternals } from './ref.js' @@ -15,7 +15,7 @@ export const Root: ( ) => ReactNode = forwardRef((properties, ref) => { const renderer = useThree((state) => state.gl) - useEffect(() => patchRenderOrder(renderer), [renderer]) + useEffect(() => renderer.setTransparentSort(reversePainterSortStable), [renderer]) const store = useStore() const outerRef = useRef(null) const innerRef = useRef(null) @@ -28,8 +28,9 @@ export const Root: ( outerRef, innerRef, () => store.getState().camera, + renderer, ), - [store, propertySignals], + [store, propertySignals, renderer], ) useEffect(() => () => destroyRoot(internals), [internals]) diff --git a/packages/react/src/text.tsx b/packages/react/src/text.tsx index 1824e469..3d0099d6 100644 --- a/packages/react/src/text.tsx +++ b/packages/react/src/text.tsx @@ -1,122 +1,57 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { YogaProperties } from '../flex/node.js' -import { useApplyHoverProperties } from '../hover.js' -import { PanelProperties } from '../panel/instanced-panel.js' +import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' +import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' +import { Object3D } from 'three' +import { ParentProvider, useParent } from './context.js' +import { AddHandlers, usePropertySignals } from './utilts.js' import { - InteractionGroup, - MaterialClass, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' -import { WithReactive, createCollection, finalizeCollection, writeCollection } from '../properties/utils.js' -import { ScrollListeners } from '../scroll.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - WithConditionals, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { forwardRef, useRef } from 'react' -import { useParentClippingRect, useIsClipped } from '../clipping.js' -import { useFlexNode } from '../flex/react.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { InstancedTextProperties, useInstancedText } from '../text/react.js' -import { ReadonlySignal } from '@preact/signals-core' -import { useRootGroupRef } from '../utils.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { Group } from 'three' -import { ElementType, ZIndexOffset, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' - -export type TextProperties = WithConditionals< - WithClasses< - WithAllAliases> - > -> + ContainerProperties, + createContainer, + createText, + destroyContainer, + FontFamilies, +} from '@vanilla-three/uikit/internals' +import { ComponentInternals, useComponentInternals } from './ref.js' +import { Signal, signal } from '@preact/signals-core' +import { useFontFamilies } from './font.js' -export const Text = forwardRef< - ComponentInternals, - { - children: string | ReadonlySignal | Array> - panelMaterialClass?: MaterialClass - zIndexOffset?: ZIndexOffset - } & TextProperties & +export const Text: ( + props: { + children: string | Array> | Signal + } & ContainerProperties & EventHandlers & - LayoutListeners & - ViewportListeners & - ScrollListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const rootGroupRef = useRootGroupRef() - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, + RefAttributes, +) => ReactNode = forwardRef((properties, ref) => { + const parent = useParent() + const outerRef = useRef(null) + const innerRef = useRef(null) + const propertySignals = usePropertySignals(properties) + const textSignal = useMemo( + () => signal> | Signal>(undefined as any), + [], ) - const measureFunc = useInstancedText( - collection, - properties.children, - globalMatrix, - node, - isClipped, - parentClippingRect, - backgroundOrderInfo, + textSignal.value = properties.children + const fontFamilies = useMemo(() => signal(undefined as any), []) + fontFamilies.value = useFontFamilies() + const internals = useMemo( + () => + createText( + parent, + textSignal, + fontFamilies, + propertySignals.properties, + propertySignals.default, + outerRef, + innerRef, + ), + [fontFamilies, parent, propertySignals, textSignal], ) + useEffect(() => () => destroyContainer(internals), [internals]) - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) - writeCollection(collection, 'measureFunc', measureFunc) - finalizeCollection(collection) - - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel) + useComponentInternals(ref, propertySignals.style, internals) return ( - - - + + + ) }) diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 04207d9f..44675d8c 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -25,7 +25,7 @@ import { Object3DRef, WithContext } from '../context.js' import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' import { addHandlers, cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { EventHandlers } from '../events.js' -import { darkPropertyTransformers, getDefaultPanelMaterialConfig, traverseProperties } from '../internals.js' +import { darkPropertyTransformers, getDefaultPanelMaterialConfig } from '../internals.js' export type InheritableContainerProperties = WithClasses< WithConditionals< diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/components/index.ts index 83b92f53..416764a3 100644 --- a/packages/uikit/src/components/index.ts +++ b/packages/uikit/src/components/index.ts @@ -1,4 +1,5 @@ +export * from './utils.js' +export * from './root.js' export * from './container.js' export * from './image.js' -export * from './root.js' -export * from './utils.js' +export * from './text.js' diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index 823239aa..4cedc3df 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -24,7 +24,7 @@ import { Subscriptions, alignmentXMap, alignmentYMap, readReactive, unsubscribeS import { WithConditionals } from './utils.js' import { computedClippingRect } from '../clipping.js' import { computedOrderInfo, ElementType, WithCameraDistance } from '../order.js' -import { Camera, Matrix4, Plane, Vector2Tuple, Vector3 } from 'three' +import { Camera, Matrix4, Plane, Vector2Tuple, Vector3, WebGLRenderer } from 'three' import { GlyphGroupManager } from '../text/render/instanced-glyph-group.js' import { createGetBatchedProperties } from '../properties/batched.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' @@ -59,7 +59,7 @@ export type RootProperties = InheritableRootProperties & { LayoutListeners & ScrollListeners -const DEFAULT_PIXEL_SIZE = 0.002 +const DEFAULT_PIXEL_SIZE = 0.01 const vectorHelper = new Vector3() const planeHelper = new Plane() @@ -72,6 +72,7 @@ export function createRoot( object: Object3DRef, childrenContainer: Object3DRef, getCamera: () => Camera, + renderer: WebGLRenderer, ) { const rootSize = signal([0, 0]) const hoveredSignal = signal>([]) @@ -198,6 +199,7 @@ export function createRoot( orderInfo, panelGroupManager, pixelSize, + renderer, }) return Object.assign(rootCtx, { diff --git a/packages/uikit/src/components/text.ts b/packages/uikit/src/components/text.ts new file mode 100644 index 00000000..4b91c7e1 --- /dev/null +++ b/packages/uikit/src/components/text.ts @@ -0,0 +1,206 @@ +import { YogaProperties } from '../flex/node.js' +import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { computedIsClipped, computedClippingRect } from '../clipping.js' +import { + ScrollbarProperties, + applyScrollPosition, + computedGlobalScrollMatrix, + createScrollPosition, + createScrollbars, + setupScrollHandler, +} from '../scroll.js' +import { WithAllAliases } from '../properties/alias.js' +import { PanelProperties, createInstancedPanel } from '../panel/instanced-panel.js' +import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' +import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' +import { createResponsivePropertyTransformers } from '../responsive.js' +import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js' +import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' +import { Signal, computed, signal } from '@preact/signals-core' +import { WithConditionals, computedGlobalMatrix } from './utils.js' +import { Subscriptions, readReactive, unsubscribeSubscriptions } from '../utils.js' +import { MergedProperties } from '../properties/merged.js' +import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' +import { Object3DRef, WithContext } from '../context.js' +import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' +import { addHandlers, cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { EventHandlers } from '../events.js' +import { + FontFamilies, + InstancedTextProperties, + computedFont, + computedGylphGroupDependencies, + createInstancedText, + darkPropertyTransformers, + getDefaultPanelMaterialConfig, +} from '../internals.js' + +export type InheritableTextProperties = WithClasses< + WithConditionals< + WithAllAliases< + WithReactive< + YogaProperties & + PanelProperties & + ZIndexProperties & + TransformProperties & + ScrollbarProperties & + PanelGroupProperties & + InstancedTextProperties + > + > + > +> + +export type TextProperties = InheritableTextProperties & Listeners & EventHandlers + +export function createText( + parentContext: WithContext, + textSignal: Signal | Array>>, + fontFamilies: Signal | undefined, + properties: Signal, + defaultProperties: Signal, + object: Object3DRef, + childrenContainer: Object3DRef, +) { + const hoveredSignal = signal>([]) + const activeSignal = signal>([]) + const subscriptions = [] as Subscriptions + setupCursorCleanup(hoveredSignal, subscriptions) + + const postTranslators = { + ...darkPropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + } + + const mergedProperties = computed(() => { + const merged = new MergedProperties() + merged.addAll(defaultProperties.value, properties.value, postTranslators) + return merged + }) + + const node = parentContext.node.createChild(mergedProperties, object, subscriptions) + parentContext.node.addChild(node) + subscriptions.push(() => { + parentContext.node.removeChild(node) + node.destroy() + }) + + const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) + applyTransform(object, transformMatrix, subscriptions) + + const globalMatrix = computedGlobalMatrix(parentContext.matrix, transformMatrix) + + const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) + const groupDeps = computedPanelGroupDependencies(mergedProperties) + + const backgroundOrderInfo = computedOrderInfo(mergedProperties, ElementType.Panel, groupDeps, parentContext.orderInfo) + + createInstancedPanel( + mergedProperties, + backgroundOrderInfo, + groupDeps, + parentContext.root.panelGroupManager, + globalMatrix, + node.size, + undefined, + node.borderInset, + parentContext.clippingRect, + isClipped, + getDefaultPanelMaterialConfig(), + subscriptions, + ) + + const fontSignal = computedFont(mergedProperties, fontFamilies, parentContext.root.renderer, subscriptions) + const orderInfo = computedOrderInfo( + undefined, + ElementType.Text, + computedGylphGroupDependencies(fontSignal), + parentContext.orderInfo, + ) + + const measureFunc = createInstancedText( + mergedProperties, + textSignal, + globalMatrix, + node, + isClipped, + parentContext.clippingRect, + orderInfo, + fontSignal, + parentContext.root.gylphGroupManager, + undefined, + undefined, + undefined, + subscriptions, + ) + subscriptions.push(node.setMeasureFunc(measureFunc)) + + const scrollPosition = createScrollPosition() + applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) + const matrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) + createScrollbars( + mergedProperties, + scrollPosition, + node, + globalMatrix, + isClipped, + parentContext.clippingRect, + backgroundOrderInfo, + parentContext.root.panelGroupManager, + subscriptions, + ) + + const clippingRect = computedClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parentContext.root.pixelSize, + parentContext.clippingRect, + ) + + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) + + const scrollHandlers = setupScrollHandler( + node, + scrollPosition, + object, + properties, + parentContext.root.pixelSize, + parentContext.root.onFrameSet, + subscriptions, + ) + + return { + isClipped, + clippingRect, + matrix, + node, + object, + orderInfo: backgroundOrderInfo, + root: parentContext.root, + scrollPosition, + interactionPanel: createInteractionPanel( + node, + backgroundOrderInfo, + parentContext.root, + parentContext.clippingRect, + subscriptions, + ), + handlers: computed(() => { + const handlers = cloneHandlers(properties.value) + addHandlers(handlers, scrollHandlers.value) + addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) + addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) + return handlers + }), + subscriptions, + } +} + +export function destroyText(text: ReturnType) { + unsubscribeSubscriptions(text.subscriptions) +} diff --git a/packages/uikit/src/components/utils.tsx b/packages/uikit/src/components/utils.tsx index 17948061..bb5bb8ba 100644 --- a/packages/uikit/src/components/utils.tsx +++ b/packages/uikit/src/components/utils.tsx @@ -42,7 +42,6 @@ export function loadResourceWithParams>( .then((value) => (canceled ? undefined : (target.value = value))) .catch(console.error) return () => (canceled = true) - // eslint-disable-next-line react-hooks/exhaustive-deps }), ) } diff --git a/packages/uikit/src/context.ts b/packages/uikit/src/context.ts index 415bd8cf..50573a00 100644 --- a/packages/uikit/src/context.ts +++ b/packages/uikit/src/context.ts @@ -1,6 +1,6 @@ import { Signal } from '@preact/signals-core' import { FlexNode } from './flex/index.js' -import { Matrix4, Object3D } from 'three' +import { Matrix4, Object3D, WebGLRenderer } from 'three' import { ClippingRect } from './clipping.js' import { OrderInfo, WithCameraDistance } from './order.js' import { GlyphGroupManager } from './text/render/instanced-glyph-group.js' @@ -16,6 +16,7 @@ export type RootContext = WithCameraDistance & panelGroupManager: PanelGroupManager pixelSize: number onFrameSet: Set<(delta: number) => void> + renderer: WebGLRenderer }> & ElementContext diff --git a/packages/uikit/src/flex/node.ts b/packages/uikit/src/flex/node.ts index 9ea9e167..3b84fd2f 100644 --- a/packages/uikit/src/flex/node.ts +++ b/packages/uikit/src/flex/node.ts @@ -1,13 +1,12 @@ import { Object3D, Vector2Tuple } from 'three' import { Signal, batch, computed, effect, signal } from '@preact/signals-core' -import Yoga, { Edge, Node, Overflow } from 'yoga-layout' +import Yoga, { Edge, MeasureFunction, Node, Overflow } from 'yoga-layout' import { setter } from './setter.js' import { Subscriptions } from '../utils.js' import { setupImmediateProperties } from '../properties/immediate.js' import { MergedProperties } from '../properties/merged.js' import { Object3DRef } from '../context.js' -import { defaultYogaConfig } from './config.js' -import { setMeasureFunc, yogaNodeEqual } from './utils.js' +import { PointScaleFactor, defaultYogaConfig } from './config.js' export type YogaProperties = { [Key in keyof typeof setter]?: Parameters<(typeof setter)[Key]>[1] @@ -61,11 +60,7 @@ export class FlexNode { this.active, hasImmediateProperty, (key: string, value: unknown) => { - if (key === 'measureFunc') { - setMeasureFunc(this.yogaNode!, value as any) - } else { - setter[key as keyof typeof setter](this.yogaNode!, value as any) - } + setter[key as keyof typeof setter](this.yogaNode!, value as any) this.requestCalculateLayout() }, subscriptions, @@ -73,6 +68,28 @@ export class FlexNode { ) } + setMeasureFunc(func: Signal) { + return effect(() => { + if (!this.active.value) { + return + } + if (func.value == null) { + this.yogaNode!.setMeasureFunc(null) + return + } + const fn = func.value + this.yogaNode!.setMeasureFunc((width, wMode, height, hMode) => { + const result = fn(width, wMode, height, hMode) + return { + width: Math.ceil(result.width * PointScaleFactor + 1) / PointScaleFactor, + height: Math.ceil(result.height * PointScaleFactor + 1) / PointScaleFactor, + } + }) + this.yogaNode!.markDirty() + this.requestCalculateLayout() + }) + } + destroy() { this.unsubscribeYoga?.() this.yogaNode?.free() @@ -297,3 +314,7 @@ function assertNodeNotNull(val: T | undefined): T { } return val } + +function yogaNodeEqual(n1: Node, n2: Node): boolean { + return (n1 as any)['M']['O'] === (n2 as any)['M']['O'] +} diff --git a/packages/uikit/src/flex/utils.ts b/packages/uikit/src/flex/utils.ts deleted file mode 100644 index e598684c..00000000 --- a/packages/uikit/src/flex/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Node, MeasureFunction } from 'yoga-layout' -import { PointScaleFactor } from './config.js' - -export function yogaNodeEqual(n1: Node, n2: Node): boolean { - return (n1 as any)['M']['O'] === (n2 as any)['M']['O'] -} - -export function setMeasureFunc(node: Node, func: MeasureFunction | undefined): void { - if (func == null) { - node.setMeasureFunc(null) - return - } - node.setMeasureFunc((width, wMode, height, hMode) => { - const result = func(width, wMode, height, hMode) - return { - width: Math.ceil(result.width * PointScaleFactor + 1) / PointScaleFactor, - height: Math.ceil(result.height * PointScaleFactor + 1) / PointScaleFactor, - } - }) - node.markDirty() -} diff --git a/packages/uikit/src/index.ts b/packages/uikit/src/index.ts index 929be2cc..236d725d 100644 --- a/packages/uikit/src/index.ts +++ b/packages/uikit/src/index.ts @@ -1,4 +1,4 @@ -export { patchRenderOrder } from './order.js' +export { reversePainterSortStable } from './order.js' export { basedOnPreferredColorScheme, setPreferredColorScheme, diff --git a/packages/uikit/src/internals.ts b/packages/uikit/src/internals.ts index 97b39b89..501f9006 100644 --- a/packages/uikit/src/internals.ts +++ b/packages/uikit/src/internals.ts @@ -14,5 +14,5 @@ export * from './panel/index.js' export * from './scroll.js' export * from './components/index.js' -export type * from './events' -export type * from './context' +export type * from './events.js' +export type * from './context.js' diff --git a/packages/uikit/src/order.ts b/packages/uikit/src/order.ts index 882d92ca..b8f4cf62 100644 --- a/packages/uikit/src/order.ts +++ b/packages/uikit/src/order.ts @@ -8,7 +8,7 @@ export type WithCameraDistance = { cameraDistance: number } export const cameraDistanceKey = Symbol('camera-distance-key') export const orderInfoKey = Symbol('order-info-key') -function reversePainterSortStable(a: RenderItem, b: RenderItem) { +export function reversePainterSortStable(a: RenderItem, b: RenderItem) { if (a.groupOrder !== b.groupOrder) { return a.groupOrder - b.groupOrder } @@ -27,10 +27,6 @@ function reversePainterSortStable(a: RenderItem, b: RenderItem) { return bDistanceRef.cameraDistance - aDistanceRef.cameraDistance } -export function patchRenderOrder(renderer: WebGLRenderer): void { - renderer.setTransparentSort(reversePainterSortStable) -} - //the following order tries to represent the most common element order of the respective element types (e.g. panels are most likely the background element) export const ElementType = { Panel: 0, //render first @@ -47,7 +43,7 @@ export type OrderInfo = { majorIndex: number elementType: ElementType minorIndex: number - instancedGroupDependencies?: Record | undefined + instancedGroupDependencies?: Signal> } function compareOrderInfo(o1: OrderInfo, o2: OrderInfo): number { @@ -73,7 +69,7 @@ const propertyKeys = ['zIndexOffset'] as const export function computedOrderInfo( propertiesSignal: Signal | undefined, type: ElementType, - instancedGroupDependencies: Record | undefined, + instancedGroupDependencies: Signal> | undefined, parentOrderInfoSignal: Signal | undefined, ): Signal { const get = @@ -97,7 +93,7 @@ export function computedOrderInfo( minorIndex = 0 } else if ( type != parentOrderInfo.elementType || - !shallowEqualRecord(instancedGroupDependencies, parentOrderInfo.instancedGroupDependencies) + !shallowEqualRecord(instancedGroupDependencies?.value, parentOrderInfo.instancedGroupDependencies?.value) ) { majorIndex = parentOrderInfo.majorIndex + 1 minorIndex = 0 diff --git a/packages/uikit/src/text/font.ts b/packages/uikit/src/text/font.ts index f51de6bb..bd23fba7 100644 --- a/packages/uikit/src/text/font.ts +++ b/packages/uikit/src/text/font.ts @@ -28,7 +28,7 @@ const fontKeys = ['fontFamily', 'fontWeight'] as const export type FontFamilyProperties = { fontFamily?: string; fontWeight?: FontWeight } -const defaultFontFamilyUrls = { +const defaultFontFamilyUrls: FontFamilies = { inter: { light: 'https://pmndrs.github.io/uikit/fonts/inter-light.json', normal: 'https://pmndrs.github.io/uikit/fonts/inter-normal.json', @@ -36,11 +36,11 @@ const defaultFontFamilyUrls = { 'semi-bold': 'https://pmndrs.github.io/uikit/fonts/inter-semi-bold.json', bold: 'https://pmndrs.github.io/uikit/fonts/inter-bold.json', }, -} satisfies FontFamilies +} export function computedFont( properties: Signal, - fontFamilies: FontFamilies = defaultFontFamilyUrls, + fontFamiliesSignal: Signal | undefined, renderer: WebGLRenderer, subscriptions: Subscriptions, ): Signal { @@ -48,6 +48,7 @@ export function computedFont( const get = createGetBatchedProperties(properties, fontKeys) subscriptions.push( effect(() => { + const fontFamilies = fontFamiliesSignal?.value ?? defaultFontFamilyUrls let fontWeight = get('fontWeight') ?? 'normal' if (typeof fontWeight === 'string') { fontWeight = fontWeightNames[fontWeight] diff --git a/packages/uikit/src/text/render/instanced-glyph.ts b/packages/uikit/src/text/render/instanced-glyph.ts index 651e7606..77012d5b 100644 --- a/packages/uikit/src/text/render/instanced-glyph.ts +++ b/packages/uikit/src/text/render/instanced-glyph.ts @@ -2,8 +2,8 @@ import { Matrix4, Vector2Tuple, Vector3Tuple, WebGLRenderer } from 'three' import { GlyphGroupManager, InstancedGlyphGroup } from './instanced-glyph-group.js' import { ColorRepresentation, Subscriptions } from '../../utils.js' import { ClippingRect, defaultClippingData } from '../../clipping.js' -import { FontFamilies, FontFamilyProperties, GlyphInfo, computedFont, glyphIntoToUV } from '../font.js' -import { Signal, ReadonlySignal, signal, effect } from '@preact/signals-core' +import { Font, FontFamilies, FontFamilyProperties, GlyphInfo, computedFont, glyphIntoToUV } from '../font.js' +import { Signal, ReadonlySignal, signal, effect, computed } from '@preact/signals-core' import { FlexNode } from '../../flex/node.js' import { OrderInfo } from '../../order.js' import { createGetBatchedProperties } from '../../properties/batched.js' @@ -24,25 +24,25 @@ export type InstancedTextProperties = TextAlignProperties & Omit & FontFamilyProperties +export function computedGylphGroupDependencies(fontSignal: Signal) { + return computed(() => ({ font: fontSignal.value })) +} + export function createInstancedText( properties: Signal, - text: string | ReadonlySignal | Array>, + textSignal: Signal | Array>>, matrix: Signal, node: FlexNode, isHidden: Signal | undefined, parentClippingRect: Signal | undefined, - orderInfo: OrderInfo, - fontFamilies: FontFamilies | undefined, - renderer: WebGLRenderer, + orderInfo: Signal, + fontSignal: Signal, glyphGroupManager: GlyphGroupManager, selectionRange: Signal | undefined, selectionBoxes: Signal | undefined, caretPosition: Signal | undefined, subscriptions: Subscriptions, ) { - const fontSignal = computedFont(properties, fontFamilies, renderer, subscriptions) - // eslint-disable-next-line react-hooks/exhaustive-deps - const textSignal = signal | Array>>(text) let layoutPropertiesRef: { current: GlyphLayoutProperties | undefined } = { current: undefined } const measureFunc = computedMeasureFunc(properties, fontSignal, textSignal, layoutPropertiesRef) @@ -74,7 +74,7 @@ export function createInstancedText( return } const instancedText = new InstancedText( - glyphGroupManager.getGroup(orderInfo.majorIndex, font), + glyphGroupManager.getGroup(orderInfo.value.majorIndex, font), getAlign, getAppearance, layoutSignal, diff --git a/packages/uikit/src/vanilla/index.ts b/packages/uikit/src/vanilla/index.ts index b3647a44..ed776419 100644 --- a/packages/uikit/src/vanilla/index.ts +++ b/packages/uikit/src/vanilla/index.ts @@ -1,9 +1,11 @@ -import type { Container } from './container' -import type { Root } from './root' -import type { Image } from './image' +import type { Container } from './container.js' +import type { Root } from './root.js' +import type { Image } from './image.js' +import type { Text } from './text.js' -export type Component = Container | Root | Image +export type Component = Container | Root | Image | Text export * from './container.js' export * from './root.js' export * from './image.js' +export * from './text.js' diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index 86983a56..3804e0b2 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -1,4 +1,4 @@ -import { Camera, Object3D } from 'three' +import { Camera, Object3D, WebGLRenderer } from 'three' import { Signal, batch, signal } from '@preact/signals-core' import { AllOptionalProperties } from '../properties/default.js' import { createRoot, destroyRoot, RootProperties } from '../components/root.js' @@ -14,6 +14,7 @@ export class Root extends Object3D { constructor( public readonly eventConfig: EventConfig, camera: Camera | (() => Camera), + renderer: WebGLRenderer, parent: Object3D, properties: RootProperties, defaultProperties?: AllOptionalProperties, @@ -33,6 +34,7 @@ export class Root extends Object3D { { current: this }, { current: this }, typeof camera === 'function' ? camera : () => camera, + renderer, ) //setup scrolling & events diff --git a/packages/uikit/src/vanilla/text.ts b/packages/uikit/src/vanilla/text.ts new file mode 100644 index 00000000..8937c4c7 --- /dev/null +++ b/packages/uikit/src/vanilla/text.ts @@ -0,0 +1,76 @@ +import { Object3D } from 'three' +import { ContainerProperties, createContainer, destroyContainer } from '../components/container.js' +import { AllOptionalProperties, Properties } from '../properties/default.js' +import { Component } from './index.js' +import { EventConfig, bindHandlers } from './utils.js' +import { Signal, batch, signal } from '@preact/signals-core' +import { TextProperties, createText, destroyText } from '../components/text.js' +import { FontFamilies } from '../internals.js' + +export class Text extends Object3D { + private object: Object3D + public readonly internals: ReturnType + public readonly eventConfig: EventConfig + + private readonly propertiesSignal: Signal + private readonly defaultPropertiesSignal: Signal + private readonly textSignal: Signal | Array>> + private readonly fontFamiliesSignal: Signal + + constructor( + parent: Component, + text: string | Signal | Array>, + fontFamilies: FontFamilies | undefined, + properties: TextProperties, + defaultProperties?: AllOptionalProperties, + ) { + super() + this.propertiesSignal = signal(properties) + this.defaultPropertiesSignal = signal(defaultProperties) + this.textSignal = signal(text) + this.fontFamiliesSignal = signal(fontFamilies) + this.eventConfig = parent.eventConfig + //setting up the threejs elements + this.object = new Object3D() + this.object.matrixAutoUpdate = false + this.object.add(this) + this.matrixAutoUpdate = false + parent.add(this.object) + + //setting up the container + this.internals = createText( + parent.internals, + this.textSignal, + this.fontFamiliesSignal, + this.propertiesSignal, + this.defaultPropertiesSignal, + { current: this.object }, + { current: this }, + ) + + //setup scrolling & events + const { handlers, interactionPanel, subscriptions } = this.internals + this.add(interactionPanel) + bindHandlers(handlers, interactionPanel, this.eventConfig, subscriptions) + } + + setFontFamilies(fontFamilies: FontFamilies) { + this.fontFamiliesSignal.value = fontFamilies + } + + setText(text: string | Signal | Array>) { + this.textSignal.value = text + } + + setProperties(properties: Properties, defaultProperties?: AllOptionalProperties) { + batch(() => { + this.propertiesSignal.value = properties + this.defaultPropertiesSignal.value = defaultProperties + }) + } + + destroy() { + this.object.parent?.remove(this.object) + destroyText(this.internals) + } +} From 6b241d4a518f51d190e7838b2404c7b884af814a Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Wed, 3 Apr 2024 17:43:38 +0200 Subject: [PATCH 12/20] port svg --- examples/uikit/src/App.tsx | 11 +- examples/vanilla/index.ts | 9 +- examples/vanilla/public/example.svg | 6 + packages/react/src/container.tsx | 4 +- packages/react/src/image.tsx | 7 +- packages/react/src/index.ts | 2 + packages/react/src/ref.ts | 4 +- packages/react/src/root.tsx | 9 +- packages/react/src/svg.tsx | 298 ++-------------- packages/react/src/text.tsx | 14 +- packages/uikit/src/clipping.ts | 43 +-- packages/uikit/src/components/container.ts | 4 - packages/uikit/src/components/image.ts | 22 +- packages/uikit/src/components/index.ts | 1 + packages/uikit/src/components/root.ts | 4 - packages/uikit/src/components/svg.ts | 337 ++++++++++++++++++ packages/uikit/src/components/text.ts | 4 - packages/uikit/src/order.ts | 10 +- .../uikit/src/panel/instanced-panel-group.ts | 9 +- .../src/text/render/instanced-glyph-group.ts | 2 +- packages/uikit/src/vanilla/container.ts | 7 +- packages/uikit/src/vanilla/image.ts | 9 +- packages/uikit/src/vanilla/index.ts | 4 +- packages/uikit/src/vanilla/root.ts | 7 +- packages/uikit/src/vanilla/svg.ts | 53 +++ packages/uikit/src/vanilla/text.ts | 10 +- 26 files changed, 523 insertions(+), 367 deletions(-) create mode 100644 examples/vanilla/public/example.svg create mode 100644 packages/uikit/src/components/svg.ts create mode 100644 packages/uikit/src/vanilla/svg.ts diff --git a/examples/uikit/src/App.tsx b/examples/uikit/src/App.tsx index 7d9e06f2..87f7876d 100644 --- a/examples/uikit/src/App.tsx +++ b/examples/uikit/src/App.tsx @@ -1,8 +1,10 @@ import { Canvas } from '@react-three/fiber' import { Gltf } from '@react-three/drei' -import { Container, Root, Image } from '@react-three/uikit' +import { Container, Root, Image, Text, SVG } from '@react-three/uikit' +import { useState } from 'react' export default function App() { + const [text, setText] = useState('abc') return ( - + + + setText('Hello World')}> + {text} + + + + + + diff --git a/packages/react/src/container.tsx b/packages/react/src/container.tsx index 42bff8f7..ce42ae66 100644 --- a/packages/react/src/container.tsx +++ b/packages/react/src/container.tsx @@ -3,7 +3,7 @@ import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from import { Object3D } from 'three' import { ParentProvider, useParent } from './context.js' import { AddHandlers, usePropertySignals } from './utilts.js' -import { ContainerProperties, createContainer, destroyContainer } from '@vanilla-three/uikit/internals' +import { ContainerProperties, createContainer, unsubscribeSubscriptions } from '@vanilla-three/uikit/internals' import { ComponentInternals, useComponentInternals } from './ref.js' export const Container: ( @@ -21,7 +21,7 @@ export const Container: ( () => createContainer(parent, propertySignals.properties, propertySignals.default, outerRef, innerRef), [parent, propertySignals], ) - useEffect(() => () => destroyContainer(internals), [internals]) + useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) useComponentInternals(ref, propertySignals.style, internals) diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx index b90c504a..bd8a9a59 100644 --- a/packages/react/src/image.tsx +++ b/packages/react/src/image.tsx @@ -1,12 +1,13 @@ -import { createImage, ImageProperties, destroyImage } from '@vanilla-three/uikit/internals' +import { createImage, ImageProperties, unsubscribeSubscriptions } from '@vanilla-three/uikit/internals' import { ReactNode, RefAttributes, forwardRef, useEffect, useMemo, useRef } from 'react' import { Object3D } from 'three' import { AddHandlers, usePropertySignals } from './utilts.js' import { ParentProvider, useParent } from './context.js' import { ComponentInternals, useComponentInternals } from './ref.js' +import type { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' export const Image: ( - props: ImageProperties & RefAttributes & { children?: ReactNode }, + props: ImageProperties & EventHandlers & RefAttributes & { children?: ReactNode }, ) => ReactNode = forwardRef((properties, ref) => { const parent = useParent() const outerRef = useRef(null) @@ -16,7 +17,7 @@ export const Image: ( () => createImage(parent, propertySignals.properties, propertySignals.default, outerRef, innerRef), [parent, propertySignals], ) - useEffect(() => () => destroyImage(internals), [internals]) + useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) useComponentInternals(ref, propertySignals.style, internals) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 18e721c1..b4bd93a8 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -15,3 +15,5 @@ export { DefaultProperties } from './default.js' export * from './container.js' export * from './root.js' export * from './image.js' +export * from './text.js' +export * from './svg.js' diff --git a/packages/react/src/ref.ts b/packages/react/src/ref.ts index 2db8c044..b057e885 100644 --- a/packages/react/src/ref.ts +++ b/packages/react/src/ref.ts @@ -1,5 +1,5 @@ import { ReadonlySignal, Signal } from '@preact/signals-core' -import { Inset, createContainer, createImage, createRoot } from '@vanilla-three/uikit/internals' +import { Inset, createContainer, createImage, createRoot, createSVG } from '@vanilla-three/uikit/internals' import { ForwardedRef, useImperativeHandle } from 'react' import { Vector2Tuple, Mesh } from 'three' @@ -17,7 +17,7 @@ export type ComponentInternals = { export function useComponentInternals( ref: ForwardedRef, styleSignal: Signal, - internals: ReturnType & { + internals: ReturnType & { scrollPosition?: Signal }, ): void { diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx index dc1926af..e6c273d5 100644 --- a/packages/react/src/root.tsx +++ b/packages/react/src/root.tsx @@ -3,7 +3,12 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/eve import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' import { ParentProvider } from './context.js' import { AddHandlers, usePropertySignals } from './utilts.js' -import { RootProperties, createRoot, destroyRoot, reversePainterSortStable } from '@vanilla-three/uikit/internals' +import { + RootProperties, + createRoot, + reversePainterSortStable, + unsubscribeSubscriptions, +} from '@vanilla-three/uikit/internals' import { Object3D } from 'three' import { ComponentInternals, useComponentInternals } from './ref.js' @@ -32,7 +37,7 @@ export const Root: ( ), [store, propertySignals, renderer], ) - useEffect(() => () => destroyRoot(internals), [internals]) + useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) useFrame((_, delta) => { for (const onFrame of internals.onFrameSet) { diff --git a/packages/react/src/svg.tsx b/packages/react/src/svg.tsx index dc66002c..6e531770 100644 --- a/packages/react/src/svg.tsx +++ b/packages/react/src/svg.tsx @@ -1,277 +1,33 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ReactNode, RefObject, forwardRef, useMemo, useRef } from 'react' -import { YogaProperties } from '../flex/node.js' -import { useFlexNode } from '../flex/react.js' -import { - InteractionGroup, - MaterialClass, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { - WithReactive, - createCollection, - finalizeCollection, - useGetBatchedProperties, - writeCollection, -} from '../properties/utils.js' -import { useResourceWithParams, useSignalEffect, fitNormalizedContentInside, useRootGroupRef } from '../utils.js' -import { Box3, Color, Group, Mesh, MeshBasicMaterial, Plane, ShapeGeometry, Vector3 } from 'three' -import { computed, ReadonlySignal, Signal } from '@preact/signals-core' -import { useApplyHoverProperties } from '../hover.js' -import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js' -import { Color as ColorRepresentation } from '@react-three/fiber' -import { - ComponentInternals, - LayoutListeners, - ChildrenProvider, - ViewportListeners, - WithConditionals, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { ClippingRect, useGlobalClippingPlanes, useIsClipped, useParentClippingRect } from '../clipping.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' -import { PanelProperties } from '../panel/instanced-panel.js' -import { - WithAllAliases, - flexAliasPropertyTransformation, - panelAliasPropertyTransformation, -} from '../properties/alias.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { WithClasses, useApplyProperties } from '../properties/default.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { CameraDistanceRef, ElementType, OrderInfo, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' -import { ScrollHandler, ScrollListeners, ScrollbarProperties, useScrollPosition, useScrollbars } from '../scroll.js' - -export type SvgProperties = WithConditionals< - WithClasses< - WithAllAliases< - WithReactive & ScrollbarProperties - > - > -> - -export type AppearanceProperties = { - opacity?: number - color?: ColorRepresentation -} - -const loader = new SVGLoader() - -const box3Helper = new Box3() -const vectorHelper = new Vector3() - -async function loadSvg( - url: string, - cameraDistance: CameraDistanceRef, - MaterialClass: MaterialClass = MeshBasicMaterial, - clippingPlanes: Array, - clippedRect: Signal | undefined, - rootGroupRef: RefObject, - orderInfo: OrderInfo, -) { - const object = new Group() - object.matrixAutoUpdate = false - const result = await loader.loadAsync(url) - box3Helper.makeEmpty() - for (const path of result.paths) { - const shapes = SVGLoader.createShapes(path) - const material = new MaterialClass() - material.transparent = true - material.depthWrite = false - material.toneMapped = false - material.clippingPlanes = clippingPlanes - - for (const shape of shapes) { - const geometry = new ShapeGeometry(shape) - geometry.computeBoundingBox() - box3Helper.union(geometry.boundingBox!) - const mesh = new Mesh(geometry, material) - mesh.matrixAutoUpdate = false - mesh.raycast = makeClippedRaycast(mesh, mesh.raycast, rootGroupRef, clippedRect, orderInfo) - setupRenderOrder(mesh, cameraDistance, orderInfo) - mesh.userData.color = path.color - mesh.scale.y = -1 - mesh.updateMatrix() - object.add(mesh) - } - } - box3Helper.getSize(vectorHelper) - const aspectRatio = vectorHelper.x / vectorHelper.y - const scale = 1 / vectorHelper.y - object.scale.set(1, 1, 1).multiplyScalar(scale) - box3Helper.getCenter(vectorHelper) - vectorHelper.y *= -1 - object.position.copy(vectorHelper).negate().multiplyScalar(scale) - object.updateMatrix() - - return Object.assign(object, { aspectRatio }) -} - -const colorHelper = new Color() - -const propertyKeys = ['color', 'opacity'] as const - -export const Svg = forwardRef< - ComponentInternals, - { - zIndexOffset?: ZIndexOffset - children?: ReactNode - src: string | ReadonlySignal - materialClass?: MaterialClass - panelMaterialClass?: MaterialClass - } & SvgProperties & - EventHandlers & - LayoutListeners & - ViewportListeners & - ShadowProperties & - ScrollListeners ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - - const rootGroupRef = useRootGroupRef() - const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) - const orderInfo = useOrderInfo(ElementType.Svg, undefined, undefined, backgroundOrderInfo) - const svgObject = useResourceWithParams( - loadSvg, - properties.src, - node.cameraDistance, - properties.materialClass, - clippingPlanes, - parentClippingRect, - rootGroupRef, - orderInfo, - ) - - const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) - useSignalEffect(() => { - 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) - } else if (colorRepresentation != null) { - color = colorHelper.set(colorRepresentation) - } - svgObject.value?.traverse((object) => { - if (!(object instanceof Mesh)) { - return - } - object.receiveShadow = properties.receiveShadow ?? false - object.castShadow = properties.castShadow ?? false - const material: MeshBasicMaterial = object.material - material.color.copy(color ?? object.userData.color) - material.opacity = opacity ?? 1 - }) - }, [svgObject, properties.color, properties.receiveShadow, properties.castShadow]) - const aspectRatio = useMemo(() => computed(() => svgObject.value?.aspectRatio), [svgObject]) - - const scrollPosition = useScrollPosition() - useScrollbars( - collection, - scrollPosition, - node, - globalMatrix, - isClipped, - properties.scrollbarPanelMaterialClass, - parentClippingRect, - orderInfo, +import { unsubscribeSubscriptions, SVGProperties, createSVG } from '@vanilla-three/uikit/internals' +import { ReactNode, RefAttributes, forwardRef, useEffect, useMemo, useRef } from 'react' +import { Object3D } from 'three' +import { AddHandlers, usePropertySignals } from './utilts.js' +import { ParentProvider, useParent } from './context.js' +import { ComponentInternals, useComponentInternals } from './ref.js' +import type { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' + +export const SVG: ( + props: SVGProperties & EventHandlers & RefAttributes & { children?: ReactNode }, +) => ReactNode = forwardRef((properties, ref) => { + const parent = useParent() + const outerRef = useRef(null) + const innerRef = useRef(null) + const propertySignals = usePropertySignals(properties) + const internals = useMemo( + () => createSVG(parent, propertySignals.properties, propertySignals.default, outerRef, innerRef), + [parent, propertySignals], ) + useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) - //apply all properties - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) - writeCollection(collection, 'aspectRatio', aspectRatio) - finalizeCollection(collection) - - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - - const centerGroup = useMemo(() => { - const group = new Group() - group.matrixAutoUpdate = false - return group - }, []) - - useSignalEffect(() => { - const [offsetX, offsetY, scale] = fitNormalizedContentInside( - node.size, - node.paddingInset, - node.borderInset, - node.pixelSize, - svgObject.value?.aspectRatio ?? 1, - ) - centerGroup.position.set(offsetX, offsetY, 0) - centerGroup.scale.setScalar(scale) - centerGroup.updateMatrix() - }, [node, svgObject]) - - useSignalEffect(() => { - const object = svgObject.value - if (object == null) { - return - } - centerGroup.add(object) - return () => centerGroup.remove(object) - }, [svgObject, centerGroup]) - - useSignalEffect(() => void (centerGroup.visible = !isClipped.value), []) - - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel, scrollPosition) + useComponentInternals(ref, propertySignals.style, internals) return ( - - - - - - {properties.children} - - - + + + + + {properties.children} + + ) }) diff --git a/packages/react/src/text.tsx b/packages/react/src/text.tsx index 3d0099d6..21b0bf7c 100644 --- a/packages/react/src/text.tsx +++ b/packages/react/src/text.tsx @@ -1,15 +1,9 @@ import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' import { Object3D } from 'three' -import { ParentProvider, useParent } from './context.js' +import { useParent } from './context.js' import { AddHandlers, usePropertySignals } from './utilts.js' -import { - ContainerProperties, - createContainer, - createText, - destroyContainer, - FontFamilies, -} from '@vanilla-three/uikit/internals' +import { createText, FontFamilies, TextProperties, unsubscribeSubscriptions } from '@vanilla-three/uikit/internals' import { ComponentInternals, useComponentInternals } from './ref.js' import { Signal, signal } from '@preact/signals-core' import { useFontFamilies } from './font.js' @@ -17,7 +11,7 @@ import { useFontFamilies } from './font.js' export const Text: ( props: { children: string | Array> | Signal - } & ContainerProperties & + } & TextProperties & EventHandlers & RefAttributes, ) => ReactNode = forwardRef((properties, ref) => { @@ -45,7 +39,7 @@ export const Text: ( ), [fontFamilies, parent, propertySignals, textSignal], ) - useEffect(() => () => destroyContainer(internals), [internals]) + useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) useComponentInternals(ref, propertySignals.style, internals) diff --git a/packages/uikit/src/clipping.ts b/packages/uikit/src/clipping.ts index e440fc91..dffc1cca 100644 --- a/packages/uikit/src/clipping.ts +++ b/packages/uikit/src/clipping.ts @@ -3,7 +3,8 @@ import { Group, Matrix4, Plane, Vector3 } from 'three' import type { Vector2Tuple } from 'three' import { Inset } from './flex/node.js' import { Overflow } from 'yoga-layout' -import { Object3DRef } from './context.js' +import { Object3DRef, RootContext } from './context.js' +import { Subscriptions } from './utils.js' const dotLt45deg = Math.cos((45 / 180) * Math.PI) @@ -168,26 +169,28 @@ for (let i = 0; i < 4; i++) { defaultClippingData[i * 4 + 3] = NoClippingPlane.constant } -export function createGlobalClippingPlanes() { - return [new Plane(), new Plane(), new Plane(), new Plane()] -} - -export function updateGlobalClippingPlanes( - clippingRect: Signal | undefined, - rootObject: Object3DRef, - clippingPlanes: Array, -): void { - if (rootObject.current == null) { - return - } - const localPlanes = clippingRect?.value?.planes - if (localPlanes == null) { +export function createGlobalClippingPlanes( + root: RootContext, + clippingRect: Signal, + subscriptions: Subscriptions, +) { + const planes = [new Plane(), new Plane(), new Plane(), new Plane()] + const updateClippingPlanes = () => { + if (root.object.current == null) { + return + } + const localPlanes = clippingRect?.value?.planes + if (localPlanes == null) { + for (let i = 0; i < 4; i++) { + planes[i].copy(NoClippingPlane) + } + return + } for (let i = 0; i < 4; i++) { - clippingPlanes[i].copy(NoClippingPlane) + planes[i].copy(localPlanes[i]).applyMatrix4(root.object.current.matrixWorld) } - return - } - for (let i = 0; i < 4; i++) { - clippingPlanes[i].copy(localPlanes[i]).applyMatrix4(rootObject.current.matrixWorld) } + root.onFrameSet.add(updateClippingPlanes) + subscriptions.push(() => root.onFrameSet.delete(updateClippingPlanes)) + return planes } diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 44675d8c..38e62265 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -165,7 +165,3 @@ export function createContainer( subscriptions, } } - -export function destroyContainer(container: ReturnType) { - unsubscribeSubscriptions(container.subscriptions) -} diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index d01c5134..26bf6a5e 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -32,12 +32,7 @@ import { Subscriptions, readReactive, unsubscribeSubscriptions } from '../utils. import { panelGeometry } from '../panel/utils.js' import { setupImmediateProperties } from '../properties/immediate.js' import { makeClippedRaycast, makePanelRaycast } from '../panel/interaction-panel-mesh.js' -import { - computedIsClipped, - computedClippingRect, - createGlobalClippingPlanes, - updateGlobalClippingPlanes, -} from '../clipping.js' +import { computedIsClipped, computedClippingRect, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupViewportListeners } from '../listeners.js' import { createGetBatchedProperties } from '../properties/batched.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' @@ -73,7 +68,9 @@ export type InheritableImageProperties = WithClasses< > > -export type ImageProperties = InheritableImageProperties & Listeners & EventHandlers & { src: Signal | string } +export type ImageProperties = InheritableImageProperties & + Listeners & + EventHandlers & { src: Signal | string | Texture | Signal } export function createImage( parentContext: WithContext, @@ -204,10 +201,6 @@ export function createImage( }) } -export function destroyImage(internals: ReturnType) { - unsubscribeSubscriptions(internals.subscriptions) -} - let imageMaterialConfig: PanelMaterialConfig | undefined function getImageMaterialConfig() { imageMaterialConfig ??= createPanelMaterialConfig( @@ -238,14 +231,11 @@ function createImageMesh( ) { const mesh = new Mesh(panelGeometry) mesh.matrixAutoUpdate = false - const clippingPlanes = createGlobalClippingPlanes() - const updateClippingPlanes = () => updateGlobalClippingPlanes(clippingRect, root.object, clippingPlanes) - root.onFrameSet.add(updateClippingPlanes) - subscriptions.push(() => root.onFrameSet.delete(updateClippingPlanes)) + const clippingPlanes = createGlobalClippingPlanes(root, clippingRect, subscriptions) const isVisible = getImageMaterialConfig().computedIsVisibile(propertiesSignal, node.borderInset, node.size, isHidden) setupImageMaterials(propertiesSignal, mesh, node.size, node.borderInset, isVisible, clippingPlanes, subscriptions) mesh.raycast = makeClippedRaycast(mesh, makePanelRaycast(mesh), root.object, parent.clippingRect, orderInfo) - subscriptions.push(effect(() => setupRenderOrder(mesh, root, orderInfo.value))) + setupRenderOrder(mesh, root, orderInfo) setupTextureFit(propertiesSignal, texture, node.borderInset, node.size, subscriptions) diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/components/index.ts index 416764a3..09c93e2c 100644 --- a/packages/uikit/src/components/index.ts +++ b/packages/uikit/src/components/index.ts @@ -3,3 +3,4 @@ export * from './root.js' export * from './container.js' export * from './image.js' export * from './text.js' +export * from './svg.js' diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index 4cedc3df..6dd88afb 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -216,10 +216,6 @@ export function createRoot( }) } -export function destroyRoot(internals: ReturnType) { - unsubscribeSubscriptions(internals.subscriptions) -} - function createDeferredRequestLayoutCalculation( onFrameSet: Set<(delta: number) => void>, subscriptions: Subscriptions, diff --git a/packages/uikit/src/components/svg.ts b/packages/uikit/src/components/svg.ts new file mode 100644 index 00000000..5b7de044 --- /dev/null +++ b/packages/uikit/src/components/svg.ts @@ -0,0 +1,337 @@ +import { Signal, computed, effect, signal } from '@preact/signals-core' +import { Box3, Color, Group, Mesh, MeshBasicMaterial, Object3D, Plane, ShapeGeometry, Vector3 } from 'three' +import { Listeners } from '../index.js' +import { Object3DRef, WithContext } from '../context.js' +import { FlexNode, YogaProperties } from '../flex/index.js' +import { ElementType, OrderInfo, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' +import { PanelProperties } from '../panel/instanced-panel.js' +import { WithAllAliases } from '../properties/alias.js' +import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' +import { + ScrollbarProperties, + applyScrollPosition, + computedGlobalScrollMatrix, + createScrollPosition, + createScrollbars, + setupScrollHandler, +} from '../scroll.js' +import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' +import { WithConditionals, computedGlobalMatrix, loadResourceWithParams } from './utils.js' +import { MergedProperties, PropertyTransformers } from '../properties/merged.js' +import { ColorRepresentation, Subscriptions, fitNormalizedContentInside, readReactive } from '../utils.js' +import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' +import { computedIsClipped, computedClippingRect, ClippingRect, createGlobalClippingPlanes } from '../clipping.js' +import { setupLayoutListeners, setupViewportListeners } from '../listeners.js' +import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' +import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { addHandlers, cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { createResponsivePropertyTransformers } from '../responsive.js' +import { EventHandlers } from '../events.js' +import { + PanelGroupProperties, + RootContext, + ShadowProperties, + createGetBatchedProperties, + darkPropertyTransformers, +} from '../internals.js' +import { SVGLoader } from 'three/examples/jsm/Addons.js' + +export type InheritableSVGProperties = WithClasses< + WithConditionals< + WithAllAliases< + WithReactive< + YogaProperties & + ZIndexProperties & + Omit & + AppearanceProperties & { + keepAspectRatio?: boolean + } & TransformProperties & + PanelGroupProperties & + ScrollbarProperties + > + > + > +> +export type AppearanceProperties = { + opacity?: number + color?: ColorRepresentation +} + +export type SVGProperties = InheritableSVGProperties & Listeners & EventHandlers & { src: Signal | string } + +export function createSVG( + parentContext: WithContext, + properties: Signal, + defaultProperties: Signal, + object: Object3DRef, + childrenContainer: Object3DRef, +) { + const subscriptions: Subscriptions = [] + const hoveredSignal = signal>([]) + const activeSignal = signal>([]) + setupCursorCleanup(hoveredSignal, subscriptions) + + const aspectRatio = signal(undefined) + + const signalMap = new Map>() + + const prePropertyTransformers: PropertyTransformers = { + keepAspectRatio: (value, target) => { + let signal = signalMap.get(value) + if (signal == null) { + //if keep aspect ratio is "false" => we write "null" => which overrides the previous properties and returns null + signalMap.set(value, (signal = computed(() => (readReactive(value) === false ? null : undefined)))) + } + target.add('aspectRatio', signal) + }, + } + + const postTransformers = { + ...darkPropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + } + + const mergedProperties = computed(() => { + const merged = new MergedProperties(prePropertyTransformers) + merged.add('aspectRatio', aspectRatio) + merged.addAll(defaultProperties.value, properties.value, postTransformers) + return merged + }) + + const node = parentContext.node.createChild(mergedProperties, object, subscriptions) + parentContext.node.addChild(node) + + const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) + applyTransform(object, transformMatrix, subscriptions) + + const globalMatrix = computedGlobalMatrix(parentContext.matrix, transformMatrix) + + const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) + + const orderInfo = computedOrderInfo(mergedProperties, ElementType.Image, undefined, parentContext.orderInfo) + + const src = computed(() => readReactive(properties.value.src)) + const svgObject = signal(undefined) + const clippingPlanes = createGlobalClippingPlanes(parentContext.root, parentContext.clippingRect, subscriptions) + loadResourceWithParams( + svgObject, + loadSVG, + subscriptions, + src, + parentContext.root, + clippingPlanes, + parentContext.clippingRect, + orderInfo, + aspectRatio, + ) + applySVGProperties(mergedProperties, parentContext.root, orderInfo, svgObject, subscriptions) + const centerGroup = createCenterGroup( + node, + parentContext.root.pixelSize, + svgObject, + aspectRatio, + isClipped, + subscriptions, + ) + + const scrollPosition = createScrollPosition() + applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) + const matrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) + createScrollbars( + mergedProperties, + scrollPosition, + node, + globalMatrix, + isClipped, + parentContext.clippingRect, + orderInfo, + parentContext.root.panelGroupManager, + subscriptions, + ) + + const clippingRect = computedClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parentContext.root.pixelSize, + parentContext.clippingRect, + ) + + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) + + const scrollHandlers = setupScrollHandler( + node, + scrollPosition, + object, + properties, + parentContext.root.pixelSize, + parentContext.root.onFrameSet, + subscriptions, + ) + + subscriptions.push(() => { + parentContext.node.removeChild(node) + node.destroy() + }) + + const ctx: WithContext = { + clippingRect, + matrix, + node, + object, + orderInfo, + root: parentContext.root, + } + return Object.assign(ctx, { + subscriptions, + scrollHandlers, + centerGroup, + handlers: computed(() => { + const handlers = cloneHandlers(properties.value) + addHandlers(handlers, scrollHandlers.value) + addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) + addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) + return handlers + }), + interactionPanel: createInteractionPanel( + node, + orderInfo, + parentContext.root, + parentContext.clippingRect, + subscriptions, + ), + }) +} + +function createCenterGroup( + node: FlexNode, + pixelSize: number, + svgObject: Signal, + aspectRatio: Signal, + isClipped: Signal, + subscriptions: Subscriptions, +): Group { + const centerGroup = new Group() + centerGroup.matrixAutoUpdate = false + //TODO: add and remove + + subscriptions.push( + effect(() => { + const [offsetX, offsetY, scale] = fitNormalizedContentInside( + node.size, + node.paddingInset, + node.borderInset, + pixelSize, + aspectRatio.value ?? 1, + ) + centerGroup.position.set(offsetX, offsetY, 0) + centerGroup.scale.setScalar(scale) + centerGroup.updateMatrix() + }), + effect(() => { + const object = svgObject.value + if (object == null) { + return + } + centerGroup.add(object) + return () => centerGroup.remove(object) + }), + effect(() => void (centerGroup.visible = svgObject.value != null && !isClipped.value)), + ) + return centerGroup +} + +const loader = new SVGLoader() + +const box3Helper = new Box3() +const vectorHelper = new Vector3() +const colorHelper = new Color() + +const propertyKeys = ['opacity', 'color', 'receiveShadow', 'castShadow'] as const + +function applySVGProperties( + propertiesSignal: Signal, + root: RootContext, + orderInfo: Signal, + svgObject: Signal, + subscriptions: Subscriptions, +) { + const getPropertySignal = createGetBatchedProperties( + propertiesSignal, + propertyKeys, + ) + subscriptions.push( + effect(() => { + const colorRepresentation = getPropertySignal('color') + const opacity = getPropertySignal('opacity') + const receiveShadow = getPropertySignal('receiveShadow') + const castShadow = getPropertySignal('castShadow') + let color: Color | undefined + if (Array.isArray(colorRepresentation)) { + color = colorHelper.setRGB(...colorRepresentation) + } else if (colorRepresentation != null) { + color = colorHelper.set(colorRepresentation) + } + svgObject.value?.traverse((object) => { + if (!(object instanceof Mesh)) { + return + } + object.receiveShadow = receiveShadow ?? false + object.castShadow = castShadow ?? false + setupRenderOrder(object, root, orderInfo) + const material: MeshBasicMaterial = object.material + material.color.copy(color ?? object.userData.color) + material.opacity = opacity ?? 1 + }) + }), + ) +} + +async function loadSVG( + url: string, + root: RootContext, + clippingPlanes: Array, + clippedRect: Signal | undefined, + orderInfo: Signal, + aspectRatio: Signal, +) { + const object = new Group() + object.matrixAutoUpdate = false + const result = await loader.loadAsync(url) + box3Helper.makeEmpty() + for (const path of result.paths) { + const shapes = SVGLoader.createShapes(path) + const material = new MeshBasicMaterial() + material.transparent = true + material.depthWrite = false + material.toneMapped = false + material.clippingPlanes = clippingPlanes + + for (const shape of shapes) { + const geometry = new ShapeGeometry(shape) + geometry.computeBoundingBox() + box3Helper.union(geometry.boundingBox!) + const mesh = new Mesh(geometry, material) + mesh.matrixAutoUpdate = false + mesh.raycast = makeClippedRaycast(mesh, mesh.raycast, root.object, clippedRect, orderInfo) + mesh.userData.color = path.color + mesh.scale.y = -1 + mesh.updateMatrix() + object.add(mesh) + } + } + box3Helper.getSize(vectorHelper) + aspectRatio.value = vectorHelper.x / vectorHelper.y + const scale = 1 / vectorHelper.y + object.scale.set(1, 1, 1).multiplyScalar(scale) + box3Helper.getCenter(vectorHelper) + vectorHelper.y *= -1 + object.position.copy(vectorHelper).negate().multiplyScalar(scale) + object.updateMatrix() + + return object +} diff --git a/packages/uikit/src/components/text.ts b/packages/uikit/src/components/text.ts index 4b91c7e1..f6a739ba 100644 --- a/packages/uikit/src/components/text.ts +++ b/packages/uikit/src/components/text.ts @@ -200,7 +200,3 @@ export function createText( subscriptions, } } - -export function destroyText(text: ReturnType) { - unsubscribeSubscriptions(text.subscriptions) -} diff --git a/packages/uikit/src/order.ts b/packages/uikit/src/order.ts index b8f4cf62..f6bf5f69 100644 --- a/packages/uikit/src/order.ts +++ b/packages/uikit/src/order.ts @@ -1,5 +1,5 @@ import { Signal, computed } from '@preact/signals-core' -import { RenderItem, WebGLRenderer } from 'three' +import { RenderItem } from 'three' import { MergedProperties } from './properties/merged.js' import { createGetBatchedProperties } from './properties/batched.js' @@ -22,7 +22,7 @@ export function reversePainterSortStable(a: RenderItem, b: RenderItem) { return a.z !== b.z ? b.z - a.z : a.id - b.id } if (aDistanceRef === bDistanceRef) { - return compareOrderInfo((a.object as any)[orderInfoKey], (b.object as any)[orderInfoKey]) + return compareOrderInfo((a.object as any)[orderInfoKey].value, (b.object as any)[orderInfoKey].value) } return bDistanceRef.cameraDistance - aDistanceRef.cameraDistance } @@ -136,7 +136,11 @@ function shallowEqualRecord(r1: Record | undefined, r2: Record(result: T, rootCameraDistance: WithCameraDistance, orderInfo: OrderInfo): T { +export function setupRenderOrder( + result: T, + rootCameraDistance: WithCameraDistance, + orderInfo: { value: OrderInfo }, +): T { ;(result as any)[cameraDistanceKey] = rootCameraDistance ;(result as any)[orderInfoKey] = orderInfo return result diff --git a/packages/uikit/src/panel/instanced-panel-group.ts b/packages/uikit/src/panel/instanced-panel-group.ts index ce031920..486d5ea8 100644 --- a/packages/uikit/src/panel/instanced-panel-group.ts +++ b/packages/uikit/src/panel/instanced-panel-group.ts @@ -15,12 +15,15 @@ import { createGetBatchedProperties } from '../properties/batched.js' import { MergedProperties } from '../properties/merged.js' import { Object3DRef } from '../context.js' -export type PanelGroupProperties = { - panelMaterialClass?: MaterialClass +export type ShadowProperties = { receiveShadow?: boolean castShadow?: boolean } +export type PanelGroupProperties = { + panelMaterialClass?: MaterialClass +} & ShadowProperties + const propertyKeys = ['panelMaterialClass', 'castShadow', 'receiveShadow'] as const export function computedPanelGroupDependencies(propertiesSignal: Signal) { @@ -239,7 +242,7 @@ export class InstancedPanelGroup { this.instanceClipping = new InstancedBufferAttribute(clippingArray, 16, false) this.instanceClipping.setUsage(DynamicDrawUsage) this.mesh = new InstancedPanelMesh(this.instanceMatrix, this.instanceData, this.instanceClipping) - setupRenderOrder(this.mesh, this.root, this.orderInfo) + setupRenderOrder(this.mesh, this.root, { value: this.orderInfo }) this.mesh.material = this.instanceMaterial this.mesh.receiveShadow = this.meshReceiveShadow ?? false this.mesh.castShadow = this.meshCastShadow ?? false diff --git a/packages/uikit/src/text/render/instanced-glyph-group.ts b/packages/uikit/src/text/render/instanced-glyph-group.ts index c7d8bf2f..23b03240 100644 --- a/packages/uikit/src/text/render/instanced-glyph-group.ts +++ b/packages/uikit/src/text/render/instanced-glyph-group.ts @@ -219,7 +219,7 @@ export class InstancedGlyphGroup { } //finalizing the new mesh - setupRenderOrder(this.mesh, this.rootCameraDistance, this.orderInfo) + setupRenderOrder(this.mesh, this.rootCameraDistance, { value: this.orderInfo }) this.mesh.count = this.glyphs.length this.object.current?.add(this.mesh) } diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts index cf19ddf5..39435fc6 100644 --- a/packages/uikit/src/vanilla/container.ts +++ b/packages/uikit/src/vanilla/container.ts @@ -1,9 +1,10 @@ import { Object3D } from 'three' -import { ContainerProperties, createContainer, destroyContainer } from '../components/container.js' +import { ContainerProperties, createContainer } from '../components/container.js' import { AllOptionalProperties, Properties } from '../properties/default.js' import { Component } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' +import { unsubscribeSubscriptions } from '../utils.js' export class Container extends Object3D { private object: Object3D @@ -37,7 +38,7 @@ export class Container extends Object3D { //setup scrolling & events const { handlers, interactionPanel, subscriptions } = this.internals this.add(interactionPanel) - bindHandlers(handlers, interactionPanel, this.eventConfig, subscriptions) + bindHandlers(handlers, this, this.eventConfig, subscriptions) } setProperties(properties: Properties, defaultProperties?: AllOptionalProperties) { @@ -49,6 +50,6 @@ export class Container extends Object3D { destroy() { this.object.parent?.remove(this.object) - destroyContainer(this.internals) + unsubscribeSubscriptions(this.internals.subscriptions) } } diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index b81ee56f..23eb7482 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -1,9 +1,10 @@ import { Object3D } from 'three' -import { ImageProperties, createImage, destroyImage } from '../components/image.js' +import { ImageProperties, createImage } from '../components/image.js' import { AllOptionalProperties } from '../properties/default.js' import { Component } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' +import { unsubscribeSubscriptions } from '../utils.js' export class Image extends Object3D { public readonly internals: ReturnType @@ -27,14 +28,14 @@ export class Image extends Object3D { parent.internals, this.propertiesSignal, this.defaultPropertiesSignal, - { current: this }, { current: this.container }, + { current: this }, ) this.setProperties(properties, defaultProperties) const { handlers, interactionPanel, subscriptions } = this.internals this.container.add(interactionPanel) - bindHandlers(handlers, interactionPanel, this.eventConfig, subscriptions) + bindHandlers(handlers, this, this.eventConfig, subscriptions) } setProperties(properties: ImageProperties, defaultProperties?: AllOptionalProperties) { @@ -46,6 +47,6 @@ export class Image extends Object3D { destroy() { this.container.parent?.remove(this.container) - destroyImage(this.internals) + unsubscribeSubscriptions(this.internals.subscriptions) } } diff --git a/packages/uikit/src/vanilla/index.ts b/packages/uikit/src/vanilla/index.ts index ed776419..3c3e1743 100644 --- a/packages/uikit/src/vanilla/index.ts +++ b/packages/uikit/src/vanilla/index.ts @@ -2,10 +2,12 @@ import type { Container } from './container.js' import type { Root } from './root.js' import type { Image } from './image.js' import type { Text } from './text.js' +import type { SVG } from './svg.js' -export type Component = Container | Root | Image | Text +export type Component = Container | Root | Image | Text | SVG export * from './container.js' export * from './root.js' export * from './image.js' export * from './text.js' +export * from './svg.js' diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index 3804e0b2..180f3d9a 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -1,8 +1,9 @@ import { Camera, Object3D, WebGLRenderer } from 'three' import { Signal, batch, signal } from '@preact/signals-core' import { AllOptionalProperties } from '../properties/default.js' -import { createRoot, destroyRoot, RootProperties } from '../components/root.js' +import { createRoot, RootProperties } from '../components/root.js' import { EventConfig, bindHandlers } from './utils.js' +import { unsubscribeSubscriptions } from '../utils.js' export class Root extends Object3D { public readonly internals: ReturnType @@ -40,7 +41,7 @@ export class Root extends Object3D { //setup scrolling & events const { handlers, interactionPanel, subscriptions } = this.internals this.add(interactionPanel) - bindHandlers(handlers, interactionPanel, this.eventConfig, subscriptions) + bindHandlers(handlers, this, this.eventConfig, subscriptions) } update(delta: number) { @@ -58,6 +59,6 @@ export class Root extends Object3D { destroy() { this.object.parent?.remove(this.object) - destroyRoot(this.internals) + unsubscribeSubscriptions(this.internals.subscriptions) } } diff --git a/packages/uikit/src/vanilla/svg.ts b/packages/uikit/src/vanilla/svg.ts new file mode 100644 index 00000000..7bdae11e --- /dev/null +++ b/packages/uikit/src/vanilla/svg.ts @@ -0,0 +1,53 @@ +import { Object3D } from 'three' +import { AllOptionalProperties } from '../properties/default.js' +import { Component } from './index.js' +import { EventConfig, bindHandlers } from './utils.js' +import { Signal, batch, signal } from '@preact/signals-core' +import { unsubscribeSubscriptions } from '../utils.js' +import { SVGProperties, createSVG } from '../components/svg.js' + +export class SVG extends Object3D { + public readonly internals: ReturnType + public readonly eventConfig: EventConfig + + private container: Object3D + private readonly propertiesSignal: Signal + private readonly defaultPropertiesSignal: Signal + + constructor(parent: Component, properties: SVGProperties, defaultProperties?: AllOptionalProperties) { + super() + this.propertiesSignal = signal(properties) + this.defaultPropertiesSignal = signal(defaultProperties) + this.eventConfig = parent.eventConfig + this.container = new Object3D() + this.container.matrixAutoUpdate = false + this.container.add(this) + this.matrixAutoUpdate = false + parent.add(this.container) + this.internals = createSVG( + parent.internals, + this.propertiesSignal, + this.defaultPropertiesSignal, + { current: this.container }, + { current: this }, + ) + this.setProperties(properties, defaultProperties) + + const { handlers, centerGroup, interactionPanel, subscriptions } = this.internals + this.container.add(interactionPanel) + this.container.add(centerGroup) + bindHandlers(handlers, this, this.eventConfig, subscriptions) + } + + setProperties(properties: SVGProperties, defaultProperties?: AllOptionalProperties) { + batch(() => { + this.propertiesSignal.value = properties + this.defaultPropertiesSignal.value = defaultProperties + }) + } + + destroy() { + this.container.parent?.remove(this.container) + unsubscribeSubscriptions(this.internals.subscriptions) + } +} diff --git a/packages/uikit/src/vanilla/text.ts b/packages/uikit/src/vanilla/text.ts index 8937c4c7..ca3d17d0 100644 --- a/packages/uikit/src/vanilla/text.ts +++ b/packages/uikit/src/vanilla/text.ts @@ -1,11 +1,11 @@ import { Object3D } from 'three' -import { ContainerProperties, createContainer, destroyContainer } from '../components/container.js' +import { createContainer } from '../components/container.js' import { AllOptionalProperties, Properties } from '../properties/default.js' import { Component } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' -import { TextProperties, createText, destroyText } from '../components/text.js' -import { FontFamilies } from '../internals.js' +import { TextProperties, createText } from '../components/text.js' +import { FontFamilies, unsubscribeSubscriptions } from '../internals.js' export class Text extends Object3D { private object: Object3D @@ -51,7 +51,7 @@ export class Text extends Object3D { //setup scrolling & events const { handlers, interactionPanel, subscriptions } = this.internals this.add(interactionPanel) - bindHandlers(handlers, interactionPanel, this.eventConfig, subscriptions) + bindHandlers(handlers, this, this.eventConfig, subscriptions) } setFontFamilies(fontFamilies: FontFamilies) { @@ -71,6 +71,6 @@ export class Text extends Object3D { destroy() { this.object.parent?.remove(this.object) - destroyText(this.internals) + unsubscribeSubscriptions(this.internals.subscriptions) } } From c7a4282596a26b3d2efdbfd9d3d18afa5a62673d Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Wed, 3 Apr 2024 20:08:09 +0200 Subject: [PATCH 13/20] add icons --- examples/vanilla/index.ts | 11 +- examples/vanilla/package.json | 1 + examples/vanilla/vite.config.ts | 2 + packages/icons/lucide/.gitignore | 2 - packages/icons/lucide/package.json | 24 +- packages/icons/lucide/react/.gitignore | 2 + packages/icons/lucide/{ => react}/LICENSE | 0 packages/icons/lucide/{ => react}/README.md | 0 packages/icons/lucide/{ => react}/generate.ts | 8 +- packages/icons/lucide/react/package.json | 29 +++ .../icons/lucide/{ => react}/src/.gitkeep | 0 .../lucide/{ => react}/tsconfig.build.json | 0 .../icons/lucide/{ => react}/tsconfig.json | 2 +- packages/icons/lucide/vanilla/.gitignore | 2 + packages/icons/lucide/vanilla/LICENSE | 15 ++ packages/icons/lucide/vanilla/README.md | 5 + packages/icons/lucide/vanilla/generate.ts | 41 +++ packages/icons/lucide/vanilla/package.json | 29 +++ packages/icons/lucide/vanilla/src/.gitkeep | 0 .../icons/lucide/vanilla/tsconfig.build.json | 4 + packages/icons/lucide/vanilla/tsconfig.json | 8 + packages/react/src/icon.tsx | 236 +++--------------- packages/react/src/image.tsx | 16 +- packages/react/src/index.ts | 2 + packages/react/src/ref.ts | 19 +- packages/react/src/svg.tsx | 11 +- packages/react/src/text.tsx | 12 +- packages/uikit/src/components/container.ts | 71 +++--- packages/uikit/src/components/custom.ts | 0 packages/uikit/src/components/icon.ts | 202 +++++++++++++++ packages/uikit/src/components/image.ts | 106 +++----- packages/uikit/src/components/index.ts | 1 + packages/uikit/src/components/input.ts | 0 packages/uikit/src/components/root.ts | 64 ++--- packages/uikit/src/components/svg.ts | 109 ++++---- packages/uikit/src/components/text.ts | 103 ++------ packages/uikit/src/components/utils.tsx | 127 +++++++++- packages/uikit/src/context.ts | 2 +- packages/uikit/src/index.ts | 1 + .../uikit/src/panel/instanced-panel-mesh.ts | 2 +- packages/uikit/src/scroll.ts | 8 +- packages/uikit/src/vanilla/container.ts | 6 +- packages/uikit/src/vanilla/icon.ts | 62 +++++ packages/uikit/src/vanilla/image.ts | 21 +- packages/uikit/src/vanilla/index.ts | 4 +- packages/uikit/src/vanilla/input.ts | 0 packages/uikit/src/vanilla/svg.ts | 16 +- packages/uikit/src/vanilla/text.ts | 11 +- 48 files changed, 818 insertions(+), 579 deletions(-) create mode 100644 packages/icons/lucide/react/.gitignore rename packages/icons/lucide/{ => react}/LICENSE (100%) rename packages/icons/lucide/{ => react}/README.md (100%) rename packages/icons/lucide/{ => react}/generate.ts (80%) create mode 100644 packages/icons/lucide/react/package.json rename packages/icons/lucide/{ => react}/src/.gitkeep (100%) rename packages/icons/lucide/{ => react}/tsconfig.build.json (100%) rename packages/icons/lucide/{ => react}/tsconfig.json (72%) create mode 100644 packages/icons/lucide/vanilla/.gitignore create mode 100644 packages/icons/lucide/vanilla/LICENSE create mode 100644 packages/icons/lucide/vanilla/README.md create mode 100644 packages/icons/lucide/vanilla/generate.ts create mode 100644 packages/icons/lucide/vanilla/package.json create mode 100644 packages/icons/lucide/vanilla/src/.gitkeep create mode 100644 packages/icons/lucide/vanilla/tsconfig.build.json create mode 100644 packages/icons/lucide/vanilla/tsconfig.json create mode 100644 packages/uikit/src/components/custom.ts create mode 100644 packages/uikit/src/components/icon.ts create mode 100644 packages/uikit/src/components/input.ts create mode 100644 packages/uikit/src/vanilla/icon.ts create mode 100644 packages/uikit/src/vanilla/input.ts diff --git a/examples/vanilla/index.ts b/examples/vanilla/index.ts index 64ba76cc..d3dd8e83 100644 --- a/examples/vanilla/index.ts +++ b/examples/vanilla/index.ts @@ -1,7 +1,7 @@ import { PerspectiveCamera, Scene, WebGLRenderer } from 'three' -import { Container, Root, Image, Text, SVG } from '@vanilla-three/uikit' +import { EventHandlers, reversePainterSortStable, Container, Root, Image, Text, SVG } from '@vanilla-three/uikit' +import { Delete } from '@vanilla-three/uikit-lucide' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' -import { EventHandlers, reversePainterSortStable } from '@vanilla-three/uikit/internals' // init @@ -54,8 +54,8 @@ const root = new Root( backgroundColor: 'red', }, ) - -new SVG(root, { src: 'example.svg', height: '50%' }) +new Delete(root, { width: 100 }) +new SVG(root, 'example.svg', { height: '50%' }) new Text(root, 'Hello World', undefined, { fontSize: 50 }) new Container(root, { alignSelf: 'stretch', flexGrow: 1, backgroundColor: 'blue' }) const x = new Container(root, { @@ -65,12 +65,11 @@ const x = new Container(root, { backgroundColor: 'green', }) x.dispatchEvent({ type: 'pointerOver', target: x, nativeEvent: { pointerId: 1 } } as any) -new Image(x, { +new Image(x, 'https://picsum.photos/300/300', { borderRadius: 1000, height: '100%', flexBasis: 0, flexGrow: 1, - src: 'https://picsum.photos/300/300', }) renderer.setAnimationLoop(animation) diff --git a/examples/vanilla/package.json b/examples/vanilla/package.json index e2885454..e981b8a9 100644 --- a/examples/vanilla/package.json +++ b/examples/vanilla/package.json @@ -2,6 +2,7 @@ "type": "module", "dependencies": { "@vanilla-three/uikit": "workspace:^", + "@vanilla-three/uikit-lucide": "workspace:^", "react-dom": "^18.2.0", "three": "^0.161.0" }, diff --git a/examples/vanilla/vite.config.ts b/examples/vanilla/vite.config.ts index 1b1f0245..cb4b8aef 100644 --- a/examples/vanilla/vite.config.ts +++ b/examples/vanilla/vite.config.ts @@ -13,7 +13,9 @@ export default defineConfig({ { find: '@vanilla-three/uikit', replacement: path.resolve(__dirname, '../../packages/uikit/src/index.ts') }, ], }, + base: '/uikit/examples/vanilla/', optimizeDeps: { + include: ['@vanilla-three/uikit-lucide', '@vanilla-three/uikit'], esbuildOptions: { target: 'esnext', }, diff --git a/packages/icons/lucide/.gitignore b/packages/icons/lucide/.gitignore index 0eb42ddf..1ad4838c 100644 --- a/packages/icons/lucide/.gitignore +++ b/packages/icons/lucide/.gitignore @@ -1,4 +1,2 @@ icons/* -!.gitkeep -src/* !.gitkeep \ No newline at end of file diff --git a/packages/icons/lucide/package.json b/packages/icons/lucide/package.json index d71b946a..a0cb8ff2 100644 --- a/packages/icons/lucide/package.json +++ b/packages/icons/lucide/package.json @@ -1,32 +1,10 @@ { - "name": "@react-three/uikit-lucide", - "version": "0.0.0", - "description": "lucide icons for r3/uikit", - "files": [ - "dist" - ], - "keywords": [ - "lucide", - "uikit", - "icons", - "threejs", - "r3f" - ], - "author": "Bela Bohlender", "scripts": { - "convert": "node --loader ts-node/esm ./convert.ts", - "generate": "node --loader ts-node/esm ./generate.ts", - "build": "tsc -p ./tsconfig.build.json" - }, - "type": "module", - "main": "dist/index.js", - "dependencies": { - "@react-three/uikit": "workspace:^" + "convert": "node --loader ts-node/esm ./convert.ts" }, "devDependencies": { "oslllo-svg-fixer": "^3.0.0", "@types/node": "^20.11.0", - "@types/react": "^18.2.47", "lucide-static": "^0.331.0" } } diff --git a/packages/icons/lucide/react/.gitignore b/packages/icons/lucide/react/.gitignore new file mode 100644 index 00000000..ed5665aa --- /dev/null +++ b/packages/icons/lucide/react/.gitignore @@ -0,0 +1,2 @@ +src/* +!.gitkeep \ No newline at end of file diff --git a/packages/icons/lucide/LICENSE b/packages/icons/lucide/react/LICENSE similarity index 100% rename from packages/icons/lucide/LICENSE rename to packages/icons/lucide/react/LICENSE diff --git a/packages/icons/lucide/README.md b/packages/icons/lucide/react/README.md similarity index 100% rename from packages/icons/lucide/README.md rename to packages/icons/lucide/react/README.md diff --git a/packages/icons/lucide/generate.ts b/packages/icons/lucide/react/generate.ts similarity index 80% rename from packages/icons/lucide/generate.ts rename to packages/icons/lucide/react/generate.ts index 227b55d5..105aff19 100644 --- a/packages/icons/lucide/generate.ts +++ b/packages/icons/lucide/react/generate.ts @@ -1,6 +1,6 @@ import { readdir, readFile, writeFile } from 'fs/promises' -const baseDir = 'icons/' +const baseDir = '../icons/' async function main() { const icons = await readdir(baseDir) @@ -13,12 +13,12 @@ async function main() { const svg = raw.toString() const code = ` /* eslint-disable no-shadow-restricted-names */ - import { SvgIconFromText, ComponentInternals } from "@react-three/uikit"; + import { Icon, ComponentInternals } from "@react-three/uikit"; import { ComponentPropsWithoutRef, forwardRef } from "react"; - export type ${name}Props = Omit, "text" | "svgWidth" | "svgHeight">; + export type ${name}Props = Omit, "text" | "svgWidth" | "svgHeight">; const text = \`${svg}\`; export const ${name} = /*@__PURE__*/ forwardRef((props, ref) => { - return + return }) ` writeFile(`src/${name}.tsx`, code) diff --git a/packages/icons/lucide/react/package.json b/packages/icons/lucide/react/package.json new file mode 100644 index 00000000..3bbdb496 --- /dev/null +++ b/packages/icons/lucide/react/package.json @@ -0,0 +1,29 @@ +{ + "name": "@react-three/uikit-lucide", + "version": "0.0.0", + "description": "lucide icons for r3/uikit", + "files": [ + "dist" + ], + "keywords": [ + "lucide", + "uikit", + "icons", + "threejs", + "r3f" + ], + "author": "Bela Bohlender", + "scripts": { + "generate": "node --loader ts-node/esm ./generate.ts", + "build": "tsc -p ./tsconfig.build.json" + }, + "type": "module", + "main": "dist/index.js", + "dependencies": { + "@react-three/uikit": "workspace:^" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/react": "^18.2.47" + } +} diff --git a/packages/icons/lucide/src/.gitkeep b/packages/icons/lucide/react/src/.gitkeep similarity index 100% rename from packages/icons/lucide/src/.gitkeep rename to packages/icons/lucide/react/src/.gitkeep diff --git a/packages/icons/lucide/tsconfig.build.json b/packages/icons/lucide/react/tsconfig.build.json similarity index 100% rename from packages/icons/lucide/tsconfig.build.json rename to packages/icons/lucide/react/tsconfig.build.json diff --git a/packages/icons/lucide/tsconfig.json b/packages/icons/lucide/react/tsconfig.json similarity index 72% rename from packages/icons/lucide/tsconfig.json rename to packages/icons/lucide/react/tsconfig.json index 44864705..81d4fe1e 100644 --- a/packages/icons/lucide/tsconfig.json +++ b/packages/icons/lucide/react/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../../../tsconfig.json", "compilerOptions": { "outDir": "dist", "declaration": true, diff --git a/packages/icons/lucide/vanilla/.gitignore b/packages/icons/lucide/vanilla/.gitignore new file mode 100644 index 00000000..ed5665aa --- /dev/null +++ b/packages/icons/lucide/vanilla/.gitignore @@ -0,0 +1,2 @@ +src/* +!.gitkeep \ No newline at end of file diff --git a/packages/icons/lucide/vanilla/LICENSE b/packages/icons/lucide/vanilla/LICENSE new file mode 100644 index 00000000..8662f42e --- /dev/null +++ b/packages/icons/lucide/vanilla/LICENSE @@ -0,0 +1,15 @@ +Copyright 2024 Bela Bohlender + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Copyright 2023 Coconut Capital + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/icons/lucide/vanilla/README.md b/packages/icons/lucide/vanilla/README.md new file mode 100644 index 00000000..418514b6 --- /dev/null +++ b/packages/icons/lucide/vanilla/README.md @@ -0,0 +1,5 @@ +# @vanilla-three/uikit-lucide + +lucide icons for v3/uikit + +`npm i @vanilla-three/uikit-lucide` \ No newline at end of file diff --git a/packages/icons/lucide/vanilla/generate.ts b/packages/icons/lucide/vanilla/generate.ts new file mode 100644 index 00000000..3d265b56 --- /dev/null +++ b/packages/icons/lucide/vanilla/generate.ts @@ -0,0 +1,41 @@ +import { readdir, readFile, writeFile } from 'fs/promises' + +const baseDir = '../icons/' + +async function main() { + const icons = await readdir(baseDir) + for (const icon of icons) { + if (icon === '.gitkeep') { + continue + } + const name = getName(icon) + const raw = await readFile(`${baseDir}${icon}`) + const svg = raw.toString() + const code = ` + /* eslint-disable no-shadow-restricted-names */ + import { AllOptionalProperties, Icon, Parent } from '@vanilla-three/uikit' + import { IconProperties } from '@vanilla-three/uikit/internals' + const text = \`${svg}\`; + export class ${name} extends Icon { + constructor(parent: Parent, properties: IconProperties, defaultProperties?: AllOptionalProperties,) { + super(parent, text, 24, 24, properties, defaultProperties) + } + } + ` + writeFile(`src/${name}.ts`, code) + } + writeFile( + 'src/index.ts', + icons + .filter((icon) => icon != '.gitkeep') + .map((icon) => `export * from "./${getName(icon)}.js";`) + .join('\n'), + ) +} + +function getName(file: string): string { + const name = file.slice(0, -4) + return name[0].toUpperCase() + name.slice(1).replace(/-./g, (x) => x[1].toUpperCase()) +} + +main() diff --git a/packages/icons/lucide/vanilla/package.json b/packages/icons/lucide/vanilla/package.json new file mode 100644 index 00000000..0eaa57f6 --- /dev/null +++ b/packages/icons/lucide/vanilla/package.json @@ -0,0 +1,29 @@ +{ + "name": "@vanilla-three/uikit-lucide", + "version": "0.0.0", + "description": "lucide icons for v3/uikit", + "files": [ + "dist" + ], + "keywords": [ + "lucide", + "uikit", + "icons", + "threejs", + "r3f" + ], + "author": "Bela Bohlender", + "scripts": { + "generate": "node --loader ts-node/esm ./generate.ts", + "build": "tsc -p ./tsconfig.build.json" + }, + "type": "module", + "main": "dist/index.js", + "dependencies": { + "@vanilla-three/uikit": "workspace:^" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/react": "^18.2.47" + } +} diff --git a/packages/icons/lucide/vanilla/src/.gitkeep b/packages/icons/lucide/vanilla/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/icons/lucide/vanilla/tsconfig.build.json b/packages/icons/lucide/vanilla/tsconfig.build.json new file mode 100644 index 00000000..7d480333 --- /dev/null +++ b/packages/icons/lucide/vanilla/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"] +} \ No newline at end of file diff --git a/packages/icons/lucide/vanilla/tsconfig.json b/packages/icons/lucide/vanilla/tsconfig.json new file mode 100644 index 00000000..81d4fe1e --- /dev/null +++ b/packages/icons/lucide/vanilla/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "skipLibCheck": true + } +} \ No newline at end of file diff --git a/packages/react/src/icon.tsx b/packages/react/src/icon.tsx index 5f49153d..1e5b2c73 100644 --- a/packages/react/src/icon.tsx +++ b/packages/react/src/icon.tsx @@ -1,210 +1,40 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { ReactNode, forwardRef, useMemo, useRef } from 'react' -import { useFlexNode } from '../flex/react.js' -import { - InteractionGroup, - MaterialClass, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { createCollection, finalizeCollection, useGetBatchedProperties, writeCollection } from '../properties/utils.js' -import { useSignalEffect, fitNormalizedContentInside, useRootGroupRef } from '../utils.js' -import { Color, Group, Mesh, MeshBasicMaterial, ShapeGeometry } from 'three' -import { useApplyHoverProperties } from '../hover.js' -import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js' -import { - ComponentInternals, - LayoutListeners, - ChildrenProvider, - ViewportListeners, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { useGlobalClippingPlanes, useIsClipped, useParentClippingRect } from '../clipping.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' -import { flexAliasPropertyTransformation, panelAliasPropertyTransformation } from '../properties/alias.js' -import { useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { useApplyProperties } from '../properties/default.js' -import { SvgProperties, AppearanceProperties } from './svg.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { ElementType, ZIndexOffset, setupRenderOrder, useOrderInfo } from '../order.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' -import { ScrollHandler, ScrollListeners, useScrollPosition, useScrollbars } from '../scroll.js' - -const colorHelper = new Color() - -const propertyKeys = ['color', 'opacity'] as const - -const loader = new SVGLoader() - -export const SvgIconFromText = forwardRef< - ComponentInternals, - { - children?: ReactNode - text: string - svgWidth: number - svgHeight: number - zIndexOffset?: ZIndexOffset - materialClass?: MaterialClass - panelMaterialClass?: MaterialClass - } & SvgProperties & +import { unsubscribeSubscriptions, IconProperties, createIcon } from '@vanilla-three/uikit/internals' +import { ReactNode, RefAttributes, forwardRef, useEffect, useMemo, useRef } from 'react' +import { Object3D } from 'three' +import { AddHandlers, usePropertySignals } from './utilts.js' +import { useParent } from './context.js' +import { ComponentInternals, useComponentInternals } from './ref.js' +import type { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' + +export const Icon: ( + props: IconProperties & EventHandlers & - LayoutListeners & - ViewportListeners & - ShadowProperties & - ScrollListeners ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - - const rootGroupRef = useRootGroupRef() - const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) - - const orderInfo = useOrderInfo(ElementType.Svg, undefined, undefined, backgroundOrderInfo) - const svgGroup = useMemo(() => { - const group = new Group() - group.matrixAutoUpdate = false - const result = loader.parse(properties.text) - - for (const path of result.paths) { - const shapes = SVGLoader.createShapes(path) - const material = new (properties.materialClass ?? MeshBasicMaterial)() - material.transparent = true - material.depthWrite = false - material.toneMapped = false - material.clippingPlanes = clippingPlanes - - for (const shape of shapes) { - const geometry = new ShapeGeometry(shape) - geometry.computeBoundingBox() - const mesh = new Mesh(geometry, material) - mesh.matrixAutoUpdate = false - mesh.raycast = makeClippedRaycast(mesh, mesh.raycast, rootGroupRef, parentClippingRect, orderInfo) - setupRenderOrder(mesh, node.cameraDistance, orderInfo) - mesh.userData.color = path.color - mesh.scale.y = -1 - mesh.updateMatrix() - group.add(mesh) - } - } - - return group - }, [properties.text, properties.materialClass, clippingPlanes, rootGroupRef, parentClippingRect, node, orderInfo]) - - const getPropertySignal = useGetBatchedProperties(collection, propertyKeys) - useSignalEffect(() => { - 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) - } else if (colorRepresentation != null) { - color = colorHelper.set(colorRepresentation) - } - svgGroup.traverse((object) => { - if (!(object instanceof Mesh)) { - return - } - object.receiveShadow = properties.receiveShadow ?? false - object.castShadow = properties.castShadow ?? false - const material: MeshBasicMaterial = object.material - material.color.copy(color ?? object.userData.color) - material.opacity = opacity ?? 1 - }) - }, [svgGroup, properties.color, properties.receiveShadow, properties.castShadow]) - - const scrollPosition = useScrollPosition() - useScrollbars( - collection, - scrollPosition, - node, - globalMatrix, - isClipped, - properties.scrollbarPanelMaterialClass, - parentClippingRect, - orderInfo, + RefAttributes & { text: string; svgWidth: number; svgHeight: number; children?: ReactNode }, +) => ReactNode = forwardRef((properties, ref) => { + const parent = useParent() + const outerRef = useRef(null) + const propertySignals = usePropertySignals(properties) + const internals = useMemo( + () => + createIcon( + parent, + properties.text, + properties.svgWidth, + properties.svgHeight, + propertySignals.properties, + propertySignals.default, + outerRef, + ), + [parent, properties.svgHeight, properties.svgWidth, properties.text, propertySignals], ) + useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) - //apply all properties - writeCollection(collection, 'width', properties.svgWidth) - writeCollection(collection, 'height', properties.svgHeight) - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) - writeCollection(collection, 'aspectRatio', properties.svgWidth / properties.svgHeight) - finalizeCollection(collection) - - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - - useSignalEffect(() => { - const aspectRatio = properties.svgWidth / properties.svgHeight - const [offsetX, offsetY, scale] = fitNormalizedContentInside( - node.size, - node.paddingInset, - node.borderInset, - node.pixelSize, - properties.svgWidth / properties.svgHeight, - ) - svgGroup.position.set(offsetX - (scale * aspectRatio) / 2, offsetY + scale / 2, 0) - svgGroup.scale.setScalar(scale / properties.svgHeight) - svgGroup.updateMatrix() - }, [node, svgGroup, properties.svgWidth, properties.svgHeight]) - - useSignalEffect(() => void (svgGroup.visible = !isClipped.value), []) - - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - - useComponentInternals(ref, node, interactionPanel, scrollPosition) + useComponentInternals(ref, propertySignals.style, internals) return ( - - - - - - {properties.children} - - - + + + + ) }) diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx index bd8a9a59..7655df90 100644 --- a/packages/react/src/image.tsx +++ b/packages/react/src/image.tsx @@ -1,21 +1,29 @@ import { createImage, ImageProperties, unsubscribeSubscriptions } from '@vanilla-three/uikit/internals' import { ReactNode, RefAttributes, forwardRef, useEffect, useMemo, useRef } from 'react' -import { Object3D } from 'three' +import { Object3D, Texture } from 'three' import { AddHandlers, usePropertySignals } from './utilts.js' import { ParentProvider, useParent } from './context.js' import { ComponentInternals, useComponentInternals } from './ref.js' import type { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' +import { Signal, signal } from '@preact/signals-core' export const Image: ( - props: ImageProperties & EventHandlers & RefAttributes & { children?: ReactNode }, + props: ImageProperties & + EventHandlers & + RefAttributes & { + src: string | Signal | Texture | Signal + children?: ReactNode + }, ) => ReactNode = forwardRef((properties, ref) => { const parent = useParent() const outerRef = useRef(null) const innerRef = useRef(null) const propertySignals = usePropertySignals(properties) + const srcSignal = useMemo(() => signal | Texture | Signal>(''), []) + srcSignal.value = properties.src const internals = useMemo( - () => createImage(parent, propertySignals.properties, propertySignals.default, outerRef, innerRef), - [parent, propertySignals], + () => createImage(parent, srcSignal, propertySignals.properties, propertySignals.default, outerRef, innerRef), + [parent, propertySignals, srcSignal], ) useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index b4bd93a8..93af3d56 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -12,8 +12,10 @@ export { type AllOptionalProperties, } from '@vanilla-three/uikit' export { DefaultProperties } from './default.js' +export { ComponentInternals } from './ref.js' export * from './container.js' export * from './root.js' export * from './image.js' export * from './text.js' export * from './svg.js' +export * from './icon.js' diff --git a/packages/react/src/ref.ts b/packages/react/src/ref.ts index b057e885..8382529e 100644 --- a/packages/react/src/ref.ts +++ b/packages/react/src/ref.ts @@ -1,5 +1,13 @@ import { ReadonlySignal, Signal } from '@preact/signals-core' -import { Inset, createContainer, createImage, createRoot, createSVG } from '@vanilla-three/uikit/internals' +import { + Inset, + createContainer, + createImage, + createRoot, + createSVG, + createText, + createIcon, +} from '@vanilla-three/uikit/internals' import { ForwardedRef, useImperativeHandle } from 'react' import { Vector2Tuple, Mesh } from 'three' @@ -17,7 +25,14 @@ export type ComponentInternals = { export function useComponentInternals( ref: ForwardedRef, styleSignal: Signal, - internals: ReturnType & { + internals: ReturnType< + | typeof createContainer + | typeof createImage + | typeof createRoot + | typeof createSVG + | typeof createText + | typeof createIcon + > & { scrollPosition?: Signal }, ): void { diff --git a/packages/react/src/svg.tsx b/packages/react/src/svg.tsx index 6e531770..387ac97f 100644 --- a/packages/react/src/svg.tsx +++ b/packages/react/src/svg.tsx @@ -5,17 +5,22 @@ import { AddHandlers, usePropertySignals } from './utilts.js' import { ParentProvider, useParent } from './context.js' import { ComponentInternals, useComponentInternals } from './ref.js' import type { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' +import { Signal, signal } from '@preact/signals-core' export const SVG: ( - props: SVGProperties & EventHandlers & RefAttributes & { children?: ReactNode }, + props: SVGProperties & + EventHandlers & + RefAttributes & { src: string | Signal; children?: ReactNode }, ) => ReactNode = forwardRef((properties, ref) => { const parent = useParent() const outerRef = useRef(null) const innerRef = useRef(null) const propertySignals = usePropertySignals(properties) + const srcSignal = useMemo(() => signal>(''), []) + srcSignal.value = properties.src const internals = useMemo( - () => createSVG(parent, propertySignals.properties, propertySignals.default, outerRef, innerRef), - [parent, propertySignals], + () => createSVG(parent, srcSignal, propertySignals.properties, propertySignals.default, outerRef, innerRef), + [parent, propertySignals, srcSignal], ) useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) diff --git a/packages/react/src/text.tsx b/packages/react/src/text.tsx index 21b0bf7c..c7947de0 100644 --- a/packages/react/src/text.tsx +++ b/packages/react/src/text.tsx @@ -17,7 +17,6 @@ export const Text: ( ) => ReactNode = forwardRef((properties, ref) => { const parent = useParent() const outerRef = useRef(null) - const innerRef = useRef(null) const propertySignals = usePropertySignals(properties) const textSignal = useMemo( () => signal> | Signal>(undefined as any), @@ -27,16 +26,7 @@ export const Text: ( const fontFamilies = useMemo(() => signal(undefined as any), []) fontFamilies.value = useFontFamilies() const internals = useMemo( - () => - createText( - parent, - textSignal, - fontFamilies, - propertySignals.properties, - propertySignals.default, - outerRef, - innerRef, - ), + () => createText(parent, textSignal, fontFamilies, propertySignals.properties, propertySignals.default, outerRef), [fontFamilies, parent, propertySignals, textSignal], ) useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 38e62265..221a4c72 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -17,7 +17,13 @@ import { createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js' import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' import { Signal, computed, signal } from '@preact/signals-core' -import { WithConditionals, computedGlobalMatrix } from './utils.js' +import { + WithConditionals, + computedGlobalMatrix, + computedHandlers, + computedMergedProperties, + createNode, +} from './utils.js' import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' import { MergedProperties } from '../properties/merged.js' import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' @@ -54,34 +60,31 @@ export function createContainer( const hoveredSignal = signal>([]) const activeSignal = signal>([]) const subscriptions = [] as Subscriptions + setupCursorCleanup(hoveredSignal, subscriptions) - const postTranslators = { + //properties + const mergedProperties = computedMergedProperties(properties, defaultProperties, { ...darkPropertyTransformers, ...createResponsivePropertyTransformers(parentContext.root.node.size), ...createHoverPropertyTransformers(hoveredSignal), ...createActivePropertyTransfomers(activeSignal), - } - - const mergedProperties = computed(() => { - const merged = new MergedProperties() - merged.addAll(defaultProperties.value, properties.value, postTranslators) - return merged }) - const node = parentContext.node.createChild(mergedProperties, object, subscriptions) - parentContext.node.addChild(node) + //create node + const node = createNode(parentContext, mergedProperties, object, subscriptions) + //transform const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) applyTransform(object, transformMatrix, subscriptions) - const globalMatrix = computedGlobalMatrix(parentContext.matrix, transformMatrix) + const globalMatrix = computedGlobalMatrix(parentContext.childrenMatrix, transformMatrix) const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) - const groupDeps = computedPanelGroupDependencies(mergedProperties) + //instanced panel + const groupDeps = computedPanelGroupDependencies(mergedProperties) const orderInfo = computedOrderInfo(mergedProperties, ElementType.Panel, groupDeps, parentContext.orderInfo) - createInstancedPanel( mergedProperties, orderInfo, @@ -97,9 +100,10 @@ export function createContainer( subscriptions, ) + //scrolling: const scrollPosition = createScrollPosition() applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) - const matrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) + const childrenMatrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) createScrollbars( mergedProperties, scrollPosition, @@ -111,19 +115,6 @@ export function createContainer( parentContext.root.panelGroupManager, subscriptions, ) - - const clippingRect = computedClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - parentContext.root.pixelSize, - parentContext.clippingRect, - ) - - setupLayoutListeners(properties, node.size, subscriptions) - setupViewportListeners(properties, isClipped, subscriptions) - const scrollHandlers = setupScrollHandler( node, scrollPosition, @@ -134,15 +125,19 @@ export function createContainer( subscriptions, ) - subscriptions.push(() => { - parentContext.node.removeChild(node) - node.destroy() - }) + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) return { - isClipped, - clippingRect, - matrix, + clippingRect: computedClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parentContext.root.pixelSize, + parentContext.clippingRect, + ), + childrenMatrix, node, object, orderInfo, @@ -155,13 +150,7 @@ export function createContainer( parentContext.clippingRect, subscriptions, ), - handlers: computed(() => { - const handlers = cloneHandlers(properties.value) - addHandlers(handlers, scrollHandlers.value) - addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) - addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) - return handlers - }), + handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal, scrollHandlers), subscriptions, } } diff --git a/packages/uikit/src/components/custom.ts b/packages/uikit/src/components/custom.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/uikit/src/components/icon.ts b/packages/uikit/src/components/icon.ts new file mode 100644 index 00000000..d31d0ed7 --- /dev/null +++ b/packages/uikit/src/components/icon.ts @@ -0,0 +1,202 @@ +import { Signal, effect, signal } from '@preact/signals-core' +import { Group, Mesh, MeshBasicMaterial, Plane, ShapeGeometry } from 'three' +import { Listeners } from '../index.js' +import { Object3DRef, WithContext } from '../context.js' +import { FlexNode, YogaProperties } from '../flex/index.js' +import { ElementType, OrderInfo, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' +import { PanelProperties } from '../panel/instanced-panel.js' +import { WithAllAliases } from '../properties/alias.js' +import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' +import { + ScrollbarProperties, + applyScrollPosition, + computedGlobalScrollMatrix, + createScrollPosition, + createScrollbars, + setupScrollHandler, +} from '../scroll.js' +import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' +import { + WithConditionals, + computedGlobalMatrix, + computedHandlers, + computedMergedProperties, + createNode, + keepAspectRatioPropertyTransformer, +} from './utils.js' +import { ColorRepresentation, Subscriptions, fitNormalizedContentInside } from '../utils.js' +import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' +import { computedIsClipped, computedClippingRect, createGlobalClippingPlanes } from '../clipping.js' +import { setupLayoutListeners, setupViewportListeners } from '../listeners.js' +import { createActivePropertyTransfomers } from '../active.js' +import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { createResponsivePropertyTransformers } from '../responsive.js' +import { EventHandlers } from '../events.js' +import { AppearanceProperties, PanelGroupProperties, darkPropertyTransformers } from '../internals.js' +import { SVGLoader } from 'three/examples/jsm/Addons.js' + +export type InheritableIconProperties = WithClasses< + WithConditionals< + WithAllAliases< + WithReactive< + YogaProperties & + ZIndexProperties & + Omit & + AppearanceProperties & + TransformProperties & + PanelGroupProperties & + ScrollbarProperties + > + > + > +> + +export type IconProperties = InheritableIconProperties & Listeners & EventHandlers + +export function createIcon( + parentContext: WithContext, + text: string, + svgWidth: number, + svgHeight: number, + properties: Signal, + defaultProperties: Signal, + object: Object3DRef, +) { + const subscriptions: Subscriptions = [] + const hoveredSignal = signal>([]) + const activeSignal = signal>([]) + setupCursorCleanup(hoveredSignal, subscriptions) + + const mergedProperties = computedMergedProperties( + properties, + defaultProperties, + { + ...darkPropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + }, + keepAspectRatioPropertyTransformer, + (m) => { + m.add('aspectRatio', svgWidth / svgHeight) + m.add('width', svgWidth) + m.add('height', svgHeight) + }, + ) + + const node = createNode(parentContext, mergedProperties, object, subscriptions) + + const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) + applyTransform(object, transformMatrix, subscriptions) + + const globalMatrix = computedGlobalMatrix(parentContext.childrenMatrix, transformMatrix) + + const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) + + const orderInfo = computedOrderInfo(mergedProperties, ElementType.Svg, undefined, parentContext.orderInfo) + + const clippingPlanes = createGlobalClippingPlanes(parentContext.root, parentContext.clippingRect, subscriptions) + const iconGroup = createIconGroup( + text, + svgWidth, + svgHeight, + parentContext, + orderInfo, + node, + isClipped, + clippingPlanes, + subscriptions, + ) + + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) + + return { + clippingRect: computedClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parentContext.root.pixelSize, + parentContext.clippingRect, + ), + node, + object, + orderInfo, + root: parentContext.root, + subscriptions, + iconGroup, + handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal), + interactionPanel: createInteractionPanel( + node, + orderInfo, + parentContext.root, + parentContext.clippingRect, + subscriptions, + ), + } +} + +const loader = new SVGLoader() + +function createIconGroup( + text: string, + svgWidth: number, + svgHeight: number, + parentContext: WithContext, + orderInfo: Signal, + node: FlexNode, + isClipped: Signal, + clippingPlanes: Array, + subscriptions: Subscriptions, +): Group { + const group = new Group() + group.matrixAutoUpdate = false + const result = loader.parse(text) + + for (const path of result.paths) { + const shapes = SVGLoader.createShapes(path) + const material = new MeshBasicMaterial() + material.transparent = true + material.depthWrite = false + material.toneMapped = false + material.clippingPlanes = clippingPlanes + + for (const shape of shapes) { + const geometry = new ShapeGeometry(shape) + geometry.computeBoundingBox() + const mesh = new Mesh(geometry, material) + mesh.matrixAutoUpdate = false + mesh.raycast = makeClippedRaycast( + mesh, + mesh.raycast, + parentContext.root.object, + parentContext.clippingRect, + orderInfo, + ) + setupRenderOrder(mesh, parentContext.root, orderInfo) + mesh.userData.color = path.color + mesh.scale.y = -1 + mesh.updateMatrix() + group.add(mesh) + } + } + const aspectRatio = svgWidth / svgHeight + subscriptions.push( + effect(() => { + const [offsetX, offsetY, scale] = fitNormalizedContentInside( + node.size, + node.paddingInset, + node.borderInset, + parentContext.root.pixelSize, + aspectRatio, + ) + group.position.set(offsetX - (scale * aspectRatio) / 2, offsetY + scale / 2, 0) + group.scale.setScalar(scale / svgHeight) + group.updateMatrix() + }), + effect(() => void (group.visible = !isClipped.value)), + ) + return group +} diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index 26bf6a5e..45617c27 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -26,8 +26,16 @@ import { setupScrollHandler, } from '../scroll.js' import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' -import { WithConditionals, computedGlobalMatrix, loadResourceWithParams } from './utils.js' -import { MergedProperties, PropertyTransformers } from '../properties/merged.js' +import { + WithConditionals, + computedGlobalMatrix, + computedHandlers, + computedMergedProperties, + createNode, + keepAspectRatioPropertyTransformer, + loadResourceWithParams, +} from './utils.js' +import { MergedProperties } from '../properties/merged.js' import { Subscriptions, readReactive, unsubscribeSubscriptions } from '../utils.js' import { panelGeometry } from '../panel/utils.js' import { setupImmediateProperties } from '../properties/immediate.js' @@ -35,9 +43,8 @@ import { makeClippedRaycast, makePanelRaycast } from '../panel/interaction-panel import { computedIsClipped, computedClippingRect, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupViewportListeners } from '../listeners.js' import { createGetBatchedProperties } from '../properties/batched.js' -import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' -import { addHandlers, cloneHandlers } from '../panel/instanced-panel-mesh.js' +import { createActivePropertyTransfomers } from '../active.js' +import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { EventHandlers } from '../events.js' import { @@ -68,12 +75,11 @@ export type InheritableImageProperties = WithClasses< > > -export type ImageProperties = InheritableImageProperties & - Listeners & - EventHandlers & { src: Signal | string | Texture | Signal } +export type ImageProperties = InheritableImageProperties & Listeners & EventHandlers export function createImage( parentContext: WithContext, + srcSignal: Signal | string | Texture | Signal>, properties: Signal, defaultProperties: Signal, object: Object3DRef, @@ -85,7 +91,7 @@ export function createImage( const activeSignal = signal>([]) setupCursorCleanup(hoveredSignal, subscriptions) - const src = computed(() => readReactive(properties.value.src)) + const src = computed(() => readReactive(srcSignal.value)) loadResourceWithParams(texture, loadTextureImpl, subscriptions, src) const textureAspectRatio = computed(() => { @@ -97,40 +103,25 @@ export function createImage( return image.width / image.height }) - const signalMap = new Map>() - - const prePropertyTransformers: PropertyTransformers = { - keepAspectRatio: (value, target) => { - let signal = signalMap.get(value) - if (signal == null) { - //if keep aspect ratio is "false" => we write "null" => which overrides the previous properties and returns null - signalMap.set(value, (signal = computed(() => (readReactive(value) === false ? null : undefined)))) - } - target.add('aspectRatio', signal) + const mergedProperties = computedMergedProperties( + properties, + defaultProperties, + { + ...darkPropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), }, - } - - const postTransformers = { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentContext.root.node.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - } - - const mergedProperties = computed(() => { - const merged = new MergedProperties(prePropertyTransformers) - merged.add('aspectRatio', textureAspectRatio) - merged.addAll(defaultProperties.value, properties.value, postTransformers) - return merged - }) + keepAspectRatioPropertyTransformer, + (m) => m.add('aspectRatio', textureAspectRatio), + ) - const node = parentContext.node.createChild(mergedProperties, object, subscriptions) - parentContext.node.addChild(node) + const node = createNode(parentContext, mergedProperties, object, subscriptions) const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) applyTransform(object, transformMatrix, subscriptions) - const globalMatrix = computedGlobalMatrix(parentContext.matrix, transformMatrix) + const globalMatrix = computedGlobalMatrix(parentContext.childrenMatrix, transformMatrix) const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) const isHidden = computed(() => isClipped.value || texture.value == null) @@ -139,7 +130,7 @@ export function createImage( const scrollPosition = createScrollPosition() applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) - const matrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) + const childrenMatrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) createScrollbars( mergedProperties, scrollPosition, @@ -151,19 +142,6 @@ export function createImage( parentContext.root.panelGroupManager, subscriptions, ) - - const clippingRect = computedClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - parentContext.root.pixelSize, - parentContext.clippingRect, - ) - - setupLayoutListeners(properties, node.size, subscriptions) - setupViewportListeners(properties, isClipped, subscriptions) - const scrollHandlers = setupScrollHandler( node, scrollPosition, @@ -174,14 +152,19 @@ export function createImage( subscriptions, ) - subscriptions.push(() => { - parentContext.node.removeChild(node) - node.destroy() - }) + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) const ctx: WithContext = { - clippingRect, - matrix, + clippingRect: computedClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parentContext.root.pixelSize, + parentContext.clippingRect, + ), + childrenMatrix, node, object, orderInfo, @@ -189,14 +172,7 @@ export function createImage( } return Object.assign(ctx, { subscriptions, - scrollHandlers, - handlers: computed(() => { - const handlers = cloneHandlers(properties.value) - addHandlers(handlers, scrollHandlers.value) - addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) - addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) - return handlers - }), + handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal, scrollHandlers), interactionPanel: createImageMesh(mergedProperties, texture, parentContext, ctx, isHidden, subscriptions), }) } diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/components/index.ts index 09c93e2c..22576fa0 100644 --- a/packages/uikit/src/components/index.ts +++ b/packages/uikit/src/components/index.ts @@ -4,3 +4,4 @@ export * from './container.js' export * from './image.js' export * from './text.js' export * from './svg.js' +export * from './icon.js' diff --git a/packages/uikit/src/components/input.ts b/packages/uikit/src/components/input.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index 6dd88afb..5a8c5a34 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -21,7 +21,7 @@ import { } from '../scroll.js' import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' import { Subscriptions, alignmentXMap, alignmentYMap, readReactive, unsubscribeSubscriptions } from '../utils.js' -import { WithConditionals } from './utils.js' +import { WithConditionals, computedHandlers, computedMergedProperties } from './utils.js' import { computedClippingRect } from '../clipping.js' import { computedOrderInfo, ElementType, WithCameraDistance } from '../order.js' import { Camera, Matrix4, Plane, Vector2Tuple, Vector3, WebGLRenderer } from 'three' @@ -64,8 +64,6 @@ const DEFAULT_PIXEL_SIZE = 0.01 const vectorHelper = new Vector3() const planeHelper = new Plane() -const notClipped = signal(false) - export function createRoot( properties: Signal, defaultProperties: Signal, @@ -81,25 +79,22 @@ export function createRoot( setupCursorCleanup(hoveredSignal, subscriptions) const pixelSize = untracked(() => properties.value.pixelSize ?? DEFAULT_PIXEL_SIZE) - const preTransformers: PropertyTransformers = { - ...createSizeTranslator(pixelSize, 'sizeX', 'width'), - ...createSizeTranslator(pixelSize, 'sizeY', 'height'), - } - - const postTransformers = { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(rootSize), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - } - const onFrameSet = new Set<(delta: number) => void>() - const mergedProperties = computed(() => { - const merged = new MergedProperties(preTransformers) - merged.addAll(defaultProperties.value, properties.value, postTransformers) - return merged - }) + const mergedProperties = computedMergedProperties( + properties, + defaultProperties, + { + ...darkPropertyTransformers, + ...createResponsivePropertyTransformers(rootSize), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + }, + { + ...createSizeTranslator(pixelSize, 'sizeX', 'width'), + ...createSizeTranslator(pixelSize, 'sizeY', 'height'), + }, + ) const requestCalculateLayout = createDeferredRequestLayoutCalculation(onFrameSet, subscriptions) const node = new FlexNode(mergedProperties, rootSize, object, requestCalculateLayout, undefined, subscriptions) @@ -150,7 +145,7 @@ export function createRoot( const scrollPosition = createScrollPosition() applyScrollPosition(childrenContainer, scrollPosition, pixelSize) - const matrix = computedGlobalScrollMatrix(scrollPosition, rootMatrix, pixelSize) + const childrenMatrix = computedGlobalScrollMatrix(scrollPosition, rootMatrix, pixelSize) createScrollbars( mergedProperties, scrollPosition, @@ -163,17 +158,6 @@ export function createRoot( subscriptions, ) - const clippingRect = computedClippingRect( - rootMatrix, - node.size, - node.borderInset, - node.overflow, - pixelSize, - undefined, - ) - - setupLayoutListeners(properties, node.size, subscriptions) - const scrollHandlers = setupScrollHandler( node, scrollPosition, @@ -183,17 +167,19 @@ export function createRoot( onFrameSet, subscriptions, ) + + setupLayoutListeners(properties, node.size, subscriptions) + const gylphGroupManager = new GlyphGroupManager(pixelSize, ctx, object) onFrameSet.add(gylphGroupManager.onFrame) subscriptions.push(() => onFrameSet.delete(gylphGroupManager.onFrame)) const rootCtx: RootContext = Object.assign(ctx, { - isClipped: notClipped, onFrameSet, cameraDistance: 0, - clippingRect, + clippingRect: computedClippingRect(rootMatrix, node.size, node.borderInset, node.overflow, pixelSize, undefined), gylphGroupManager, - matrix, + childrenMatrix, node, object, orderInfo, @@ -205,13 +191,7 @@ export function createRoot( return Object.assign(rootCtx, { subscriptions, interactionPanel: createInteractionPanel(node, orderInfo, rootCtx, undefined, subscriptions), - handlers: computed(() => { - const handlers = cloneHandlers(properties.value) - addHandlers(handlers, scrollHandlers.value) - addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) - addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) - return handlers - }), + handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal, scrollHandlers), root: rootCtx, }) } diff --git a/packages/uikit/src/components/svg.ts b/packages/uikit/src/components/svg.ts index 5b7de044..a4fe045a 100644 --- a/packages/uikit/src/components/svg.ts +++ b/packages/uikit/src/components/svg.ts @@ -16,8 +16,16 @@ import { setupScrollHandler, } from '../scroll.js' import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' -import { WithConditionals, computedGlobalMatrix, loadResourceWithParams } from './utils.js' -import { MergedProperties, PropertyTransformers } from '../properties/merged.js' +import { + WithConditionals, + computedGlobalMatrix, + computedHandlers, + computedMergedProperties, + createNode, + keepAspectRatioPropertyTransformer, + loadResourceWithParams, +} from './utils.js' +import { MergedProperties } from '../properties/merged.js' import { ColorRepresentation, Subscriptions, fitNormalizedContentInside, readReactive } from '../utils.js' import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' import { computedIsClipped, computedClippingRect, ClippingRect, createGlobalClippingPlanes } from '../clipping.js' @@ -57,10 +65,11 @@ export type AppearanceProperties = { color?: ColorRepresentation } -export type SVGProperties = InheritableSVGProperties & Listeners & EventHandlers & { src: Signal | string } +export type SVGProperties = InheritableSVGProperties & Listeners & EventHandlers export function createSVG( parentContext: WithContext, + srcSignal: Signal | string>, properties: Signal, defaultProperties: Signal, object: Object3DRef, @@ -73,46 +82,31 @@ export function createSVG( const aspectRatio = signal(undefined) - const signalMap = new Map>() - - const prePropertyTransformers: PropertyTransformers = { - keepAspectRatio: (value, target) => { - let signal = signalMap.get(value) - if (signal == null) { - //if keep aspect ratio is "false" => we write "null" => which overrides the previous properties and returns null - signalMap.set(value, (signal = computed(() => (readReactive(value) === false ? null : undefined)))) - } - target.add('aspectRatio', signal) + const mergedProperties = computedMergedProperties( + properties, + defaultProperties, + { + ...darkPropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), }, - } - - const postTransformers = { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentContext.root.node.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - } - - const mergedProperties = computed(() => { - const merged = new MergedProperties(prePropertyTransformers) - merged.add('aspectRatio', aspectRatio) - merged.addAll(defaultProperties.value, properties.value, postTransformers) - return merged - }) + keepAspectRatioPropertyTransformer, + (m) => m.add('aspectRatio', aspectRatio), + ) - const node = parentContext.node.createChild(mergedProperties, object, subscriptions) - parentContext.node.addChild(node) + const node = createNode(parentContext, mergedProperties, object, subscriptions) const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) applyTransform(object, transformMatrix, subscriptions) - const globalMatrix = computedGlobalMatrix(parentContext.matrix, transformMatrix) + const globalMatrix = computedGlobalMatrix(parentContext.childrenMatrix, transformMatrix) const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) - const orderInfo = computedOrderInfo(mergedProperties, ElementType.Image, undefined, parentContext.orderInfo) + const orderInfo = computedOrderInfo(mergedProperties, ElementType.Svg, undefined, parentContext.orderInfo) - const src = computed(() => readReactive(properties.value.src)) + const src = computed(() => readReactive(srcSignal.value)) const svgObject = signal(undefined) const clippingPlanes = createGlobalClippingPlanes(parentContext.root, parentContext.clippingRect, subscriptions) loadResourceWithParams( @@ -138,7 +132,7 @@ export function createSVG( const scrollPosition = createScrollPosition() applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) - const matrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) + const childrenMatrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) createScrollbars( mergedProperties, scrollPosition, @@ -150,19 +144,6 @@ export function createSVG( parentContext.root.panelGroupManager, subscriptions, ) - - const clippingRect = computedClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - parentContext.root.pixelSize, - parentContext.clippingRect, - ) - - setupLayoutListeners(properties, node.size, subscriptions) - setupViewportListeners(properties, isClipped, subscriptions) - const scrollHandlers = setupScrollHandler( node, scrollPosition, @@ -173,30 +154,26 @@ export function createSVG( subscriptions, ) - subscriptions.push(() => { - parentContext.node.removeChild(node) - node.destroy() - }) + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) - const ctx: WithContext = { - clippingRect, - matrix, + return { + clippingRect: computedClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parentContext.root.pixelSize, + parentContext.clippingRect, + ), + childrenMatrix, node, object, orderInfo, root: parentContext.root, - } - return Object.assign(ctx, { subscriptions, - scrollHandlers, centerGroup, - handlers: computed(() => { - const handlers = cloneHandlers(properties.value) - addHandlers(handlers, scrollHandlers.value) - addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) - addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) - return handlers - }), + handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal, scrollHandlers), interactionPanel: createInteractionPanel( node, orderInfo, @@ -204,7 +181,7 @@ export function createSVG( parentContext.clippingRect, subscriptions, ), - }) + } } function createCenterGroup( @@ -217,8 +194,6 @@ function createCenterGroup( ): Group { const centerGroup = new Group() centerGroup.matrixAutoUpdate = false - //TODO: add and remove - subscriptions.push( effect(() => { const [offsetX, offsetY, scale] = fitNormalizedContentInside( diff --git a/packages/uikit/src/components/text.ts b/packages/uikit/src/components/text.ts index f6a739ba..813df915 100644 --- a/packages/uikit/src/components/text.ts +++ b/packages/uikit/src/components/text.ts @@ -1,29 +1,27 @@ import { YogaProperties } from '../flex/node.js' -import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { computedIsClipped, computedClippingRect } from '../clipping.js' -import { - ScrollbarProperties, - applyScrollPosition, - computedGlobalScrollMatrix, - createScrollPosition, - createScrollbars, - setupScrollHandler, -} from '../scroll.js' +import { ScrollbarProperties } from '../scroll.js' import { WithAllAliases } from '../properties/alias.js' import { PanelProperties, createInstancedPanel } from '../panel/instanced-panel.js' import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js' -import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { Signal, computed, signal } from '@preact/signals-core' -import { WithConditionals, computedGlobalMatrix } from './utils.js' -import { Subscriptions, readReactive, unsubscribeSubscriptions } from '../utils.js' -import { MergedProperties } from '../properties/merged.js' +import { createActivePropertyTransfomers } from '../active.js' +import { Signal, signal } from '@preact/signals-core' +import { + WithConditionals, + computedGlobalMatrix, + computedHandlers, + computedMergedProperties, + createNode, +} from './utils.js' +import { Subscriptions } from '../utils.js' import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' import { Object3DRef, WithContext } from '../context.js' import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' -import { addHandlers, cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { EventHandlers } from '../events.js' import { FontFamilies, @@ -60,43 +58,30 @@ export function createText( properties: Signal, defaultProperties: Signal, object: Object3DRef, - childrenContainer: Object3DRef, ) { const hoveredSignal = signal>([]) const activeSignal = signal>([]) const subscriptions = [] as Subscriptions setupCursorCleanup(hoveredSignal, subscriptions) - const postTranslators = { + const mergedProperties = computedMergedProperties(properties, defaultProperties, { ...darkPropertyTransformers, ...createResponsivePropertyTransformers(parentContext.root.node.size), ...createHoverPropertyTransformers(hoveredSignal), ...createActivePropertyTransfomers(activeSignal), - } - - const mergedProperties = computed(() => { - const merged = new MergedProperties() - merged.addAll(defaultProperties.value, properties.value, postTranslators) - return merged }) - const node = parentContext.node.createChild(mergedProperties, object, subscriptions) - parentContext.node.addChild(node) - subscriptions.push(() => { - parentContext.node.removeChild(node) - node.destroy() - }) + const node = createNode(parentContext, mergedProperties, object, subscriptions) const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) applyTransform(object, transformMatrix, subscriptions) - const globalMatrix = computedGlobalMatrix(parentContext.matrix, transformMatrix) + const globalMatrix = computedGlobalMatrix(parentContext.childrenMatrix, transformMatrix) const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) - const groupDeps = computedPanelGroupDependencies(mergedProperties) + const groupDeps = computedPanelGroupDependencies(mergedProperties) const backgroundOrderInfo = computedOrderInfo(mergedProperties, ElementType.Panel, groupDeps, parentContext.orderInfo) - createInstancedPanel( mergedProperties, backgroundOrderInfo, @@ -137,52 +122,22 @@ export function createText( ) subscriptions.push(node.setMeasureFunc(measureFunc)) - const scrollPosition = createScrollPosition() - applyScrollPosition(childrenContainer, scrollPosition, parentContext.root.pixelSize) - const matrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentContext.root.pixelSize) - createScrollbars( - mergedProperties, - scrollPosition, - node, - globalMatrix, - isClipped, - parentContext.clippingRect, - backgroundOrderInfo, - parentContext.root.panelGroupManager, - subscriptions, - ) - - const clippingRect = computedClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - parentContext.root.pixelSize, - parentContext.clippingRect, - ) - setupLayoutListeners(properties, node.size, subscriptions) setupViewportListeners(properties, isClipped, subscriptions) - const scrollHandlers = setupScrollHandler( - node, - scrollPosition, - object, - properties, - parentContext.root.pixelSize, - parentContext.root.onFrameSet, - subscriptions, - ) - return { - isClipped, - clippingRect, - matrix, + clippingRect: computedClippingRect( + globalMatrix, + node.size, + node.borderInset, + node.overflow, + parentContext.root.pixelSize, + parentContext.clippingRect, + ), node, object, orderInfo: backgroundOrderInfo, root: parentContext.root, - scrollPosition, interactionPanel: createInteractionPanel( node, backgroundOrderInfo, @@ -190,13 +145,7 @@ export function createText( parentContext.clippingRect, subscriptions, ), - handlers: computed(() => { - const handlers = cloneHandlers(properties.value) - addHandlers(handlers, scrollHandlers.value) - addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) - addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) - return handlers - }), + handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal), subscriptions, } } diff --git a/packages/uikit/src/components/utils.tsx b/packages/uikit/src/components/utils.tsx index bb5bb8ba..e9b6c972 100644 --- a/packages/uikit/src/components/utils.tsx +++ b/packages/uikit/src/components/utils.tsx @@ -1,10 +1,27 @@ import { Signal, computed, effect, signal } from '@preact/signals-core' -import { Matrix4 } from 'three' -import { WithActive } from '../active.js' +import { Color, Matrix4, Mesh, MeshBasicMaterial, Object3D } from 'three' +import { WithActive, addActiveHandlers } from '../active.js' import { WithPreferredColorScheme } from '../dark.js' -import { WithHover } from '../hover.js' +import { WithHover, addHoverHandlers } from '../hover.js' import { WithResponsive } from '../responsive.js' -import { Subscriptions } from '../utils.js' +import { Subscriptions, readReactive } from '../utils.js' +import { + AllOptionalProperties, + AppearanceProperties, + EventHandlers, + MergedProperties, + Object3DRef, + OrderInfo, + Properties, + PropertyTransformers, + RootContext, + ShadowProperties, + WithContext, + addHandlers, + cloneHandlers, + createGetBatchedProperties, + setupRenderOrder, +} from '../internals.js' export function computedGlobalMatrix( parentMatrix: Signal, @@ -45,3 +62,105 @@ export function loadResourceWithParams>( }), ) } + +export function createNode( + parentContext: WithContext, + mergedProperties: Signal, + object: Object3DRef, + subscriptions: Subscriptions, +) { + const node = parentContext.node.createChild(mergedProperties, object, subscriptions) + parentContext.node.addChild(node) + subscriptions.push(() => { + parentContext.node.removeChild(node) + node.destroy() + }) + return node +} + +const signalMap = new Map>() +export const keepAspectRatioPropertyTransformer: PropertyTransformers = { + keepAspectRatio: (value, target) => { + let signal = signalMap.get(value) + if (signal == null) { + //if keep aspect ratio is "false" => we write "null" => which overrides the previous properties and returns null + signalMap.set(value, (signal = computed(() => (readReactive(value) === false ? null : undefined)))) + } + target.add('aspectRatio', signal) + }, +} + +export function computedHandlers( + properties: Signal, + defaultProperties: Signal, + hoveredSignal: Signal>, + activeSignal: Signal>, + scrollHandlers?: Signal, +) { + return computed(() => { + const handlers = cloneHandlers(properties.value) + addHandlers(handlers, scrollHandlers?.value) + addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) + addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) + return handlers + }) +} + +export function computedMergedProperties( + properties: Signal, + defaultProperties: Signal, + postTransformers: PropertyTransformers, + preTransformers?: PropertyTransformers, + onInit?: (merged: MergedProperties) => void, +) { + return computed(() => { + const merged = new MergedProperties(preTransformers) + onInit?.(merged) + merged.addAll(defaultProperties.value, properties.value, postTransformers) + return merged + }) +} + +const colorHelper = new Color() +const propertyKeys = ['opacity', 'color', 'receiveShadow', 'castShadow'] as const + +/** + * @requires that each mesh inside the group has its default color stored inside object.userData.color + */ +export function applyAppearancePropertiesToGroup( + propertiesSignal: Signal, + root: RootContext, + orderInfo: Signal, + group: Signal | Object3D, + subscriptions: Subscriptions, +) { + const getPropertySignal = createGetBatchedProperties( + propertiesSignal, + propertyKeys, + ) + subscriptions.push( + effect(() => { + const colorRepresentation = getPropertySignal('color') + const opacity = getPropertySignal('opacity') + const receiveShadow = getPropertySignal('receiveShadow') + const castShadow = getPropertySignal('castShadow') + let color: Color | undefined + if (Array.isArray(colorRepresentation)) { + color = colorHelper.setRGB(...colorRepresentation) + } else if (colorRepresentation != null) { + color = colorHelper.set(colorRepresentation) + } + readReactive(group)?.traverse((object) => { + if (!(object instanceof Mesh)) { + return + } + object.receiveShadow = receiveShadow ?? false + object.castShadow = castShadow ?? false + setupRenderOrder(object, root, orderInfo) + const material: MeshBasicMaterial = object.material + material.color.copy(color ?? object.userData.color) + material.opacity = opacity ?? 1 + }) + }), + ) +} diff --git a/packages/uikit/src/context.ts b/packages/uikit/src/context.ts index 50573a00..6fe0300c 100644 --- a/packages/uikit/src/context.ts +++ b/packages/uikit/src/context.ts @@ -23,7 +23,7 @@ export type RootContext = WithCameraDistance & export type ElementContext = Readonly<{ node: FlexNode clippingRect: Signal - matrix: Signal + childrenMatrix: Signal orderInfo: Signal object: Object3DRef }> diff --git a/packages/uikit/src/index.ts b/packages/uikit/src/index.ts index 236d725d..adf26ade 100644 --- a/packages/uikit/src/index.ts +++ b/packages/uikit/src/index.ts @@ -1,3 +1,4 @@ +export type { EventHandlers, ThreeEvent } from './events.js' export { reversePainterSortStable } from './order.js' export { basedOnPreferredColorScheme, diff --git a/packages/uikit/src/panel/instanced-panel-mesh.ts b/packages/uikit/src/panel/instanced-panel-mesh.ts index 28f8a7c4..48ed6359 100644 --- a/packages/uikit/src/panel/instanced-panel-mesh.ts +++ b/packages/uikit/src/panel/instanced-panel-mesh.ts @@ -4,7 +4,7 @@ import { instancedPanelDepthMaterial, instancedPanelDistanceMaterial } from './p import { Signal, effect } from '@preact/signals-core' import { Subscriptions } from '../utils.js' import { makeClippedRaycast, makePanelRaycast } from './interaction-panel-mesh.js' -import { EventHandlers, KeyToEvent, ThreeEvent } from '../events.js' +import { EventHandlers, KeyToEvent } from '../events.js' import { OrderInfo } from '../order.js' import { ClippingRect, FlexNode, RootContext } from '../internals.js' diff --git a/packages/uikit/src/scroll.ts b/packages/uikit/src/scroll.ts index a1ae3df9..ed32ddc3 100644 --- a/packages/uikit/src/scroll.ts +++ b/packages/uikit/src/scroll.ts @@ -284,8 +284,7 @@ export function createScrollbars( panelGroupManager: PanelGroupManager, subscriptions: Subscriptions, ): void { - const groupDeps = computedPanelGroupDependencies(propertiesSignal) - const scrollbarOrderInfo = computedOrderInfo(undefined, ElementType.Panel, groupDeps, orderInfo) + const scrollbarOrderInfo = computedOrderInfo(undefined, ElementType.Panel, undefined, orderInfo) const getScrollbarWidth = createGetBatchedProperties( propertiesSignal, @@ -303,7 +302,6 @@ export function createScrollbars( scrollPosition, node, globalMatrix, - groupDeps, isClipped, parentClippingRect, scrollbarOrderInfo, @@ -318,7 +316,6 @@ export function createScrollbars( scrollPosition, node, globalMatrix, - groupDeps, isClipped, parentClippingRect, scrollbarOrderInfo, @@ -357,7 +354,6 @@ function createScrollbar( scrollPosition: Signal, node: FlexNode, globalMatrix: Signal, - panelGroupDependencies: Signal, isClipped: Signal | undefined, parentClippingRect: Signal | undefined, orderInfo: Signal, @@ -382,7 +378,7 @@ function createScrollbar( createInstancedPanel( propertiesSignal, orderInfo, - panelGroupDependencies, + undefined, panelGroupManager, globalMatrix, scrollbarSize, diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts index 39435fc6..d5a63ad4 100644 --- a/packages/uikit/src/vanilla/container.ts +++ b/packages/uikit/src/vanilla/container.ts @@ -1,7 +1,7 @@ import { Object3D } from 'three' import { ContainerProperties, createContainer } from '../components/container.js' import { AllOptionalProperties, Properties } from '../properties/default.js' -import { Component } from './index.js' +import { Parent } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' import { unsubscribeSubscriptions } from '../utils.js' @@ -14,7 +14,7 @@ export class Container extends Object3D { private readonly propertiesSignal: Signal private readonly defaultPropertiesSignal: Signal - constructor(parent: Component, properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { + constructor(parent: Parent, properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { super() this.propertiesSignal = signal(properties) this.defaultPropertiesSignal = signal(defaultProperties) @@ -35,7 +35,7 @@ export class Container extends Object3D { { current: this }, ) - //setup scrolling & events + //setup events const { handlers, interactionPanel, subscriptions } = this.internals this.add(interactionPanel) bindHandlers(handlers, this, this.eventConfig, subscriptions) diff --git a/packages/uikit/src/vanilla/icon.ts b/packages/uikit/src/vanilla/icon.ts new file mode 100644 index 00000000..e9391a44 --- /dev/null +++ b/packages/uikit/src/vanilla/icon.ts @@ -0,0 +1,62 @@ +import { Object3D } from 'three' +import { AllOptionalProperties } from '../properties/default.js' +import { Parent } from './index.js' +import { EventConfig, bindHandlers } from './utils.js' +import { Signal, batch, signal } from '@preact/signals-core' +import { unsubscribeSubscriptions } from '../utils.js' +import { IconProperties, createIcon } from '../components/icon.js' + +export class Icon extends Object3D { + public readonly internals: ReturnType + public readonly eventConfig: EventConfig + + private container: Object3D + private readonly propertiesSignal: Signal + private readonly defaultPropertiesSignal: Signal + + constructor( + parent: Parent, + text: string, + svgWidth: number, + svgHeight: number, + properties: IconProperties, + defaultProperties?: AllOptionalProperties, + ) { + super() + this.propertiesSignal = signal(properties) + this.defaultPropertiesSignal = signal(defaultProperties) + this.eventConfig = parent.eventConfig + this.container = new Object3D() + this.container.matrixAutoUpdate = false + this.container.add(this) + this.matrixAutoUpdate = false + parent.add(this.container) + this.internals = createIcon( + parent.internals, + text, + svgWidth, + svgHeight, + this.propertiesSignal, + this.defaultPropertiesSignal, + { current: this.container }, + ) + this.setProperties(properties, defaultProperties) + + const { handlers, iconGroup, interactionPanel, subscriptions } = this.internals + this.container.add(interactionPanel) + this.container.add(iconGroup) + bindHandlers(handlers, this, this.eventConfig, subscriptions) + } + + setProperties(properties: IconProperties, defaultProperties?: AllOptionalProperties) { + batch(() => { + this.propertiesSignal.value = properties + this.defaultPropertiesSignal.value = defaultProperties + }) + } + + destroy() { + this.container.parent?.remove(this.container) + unsubscribeSubscriptions(this.internals.subscriptions) + } +} diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index 23eb7482..773abf38 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -1,7 +1,7 @@ -import { Object3D } from 'three' +import { Object3D, Texture } from 'three' import { ImageProperties, createImage } from '../components/image.js' import { AllOptionalProperties } from '../properties/default.js' -import { Component } from './index.js' +import { Parent } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' import { unsubscribeSubscriptions } from '../utils.js' @@ -13,9 +13,16 @@ export class Image extends Object3D { private container: Object3D private readonly propertiesSignal: Signal private readonly defaultPropertiesSignal: Signal + private readonly srcSignal: Signal | Texture | Signal> - constructor(parent: Component, properties: ImageProperties, defaultProperties?: AllOptionalProperties) { + constructor( + parent: Parent, + src: string | Signal, + properties: ImageProperties, + defaultProperties?: AllOptionalProperties, + ) { super() + this.srcSignal = signal(src) this.propertiesSignal = signal(properties) this.defaultPropertiesSignal = signal(defaultProperties) this.eventConfig = parent.eventConfig @@ -24,8 +31,11 @@ export class Image extends Object3D { this.container.add(this) this.matrixAutoUpdate = false parent.add(this.container) + + //creating the image this.internals = createImage( parent.internals, + this.srcSignal, this.propertiesSignal, this.defaultPropertiesSignal, { current: this.container }, @@ -33,11 +43,16 @@ export class Image extends Object3D { ) this.setProperties(properties, defaultProperties) + //setting up events const { handlers, interactionPanel, subscriptions } = this.internals this.container.add(interactionPanel) bindHandlers(handlers, this, this.eventConfig, subscriptions) } + setSrc(src: string | Signal | Texture | Signal) { + this.srcSignal.value = src + } + setProperties(properties: ImageProperties, defaultProperties?: AllOptionalProperties) { batch(() => { this.propertiesSignal.value = properties diff --git a/packages/uikit/src/vanilla/index.ts b/packages/uikit/src/vanilla/index.ts index 3c3e1743..00370e9f 100644 --- a/packages/uikit/src/vanilla/index.ts +++ b/packages/uikit/src/vanilla/index.ts @@ -1,13 +1,13 @@ import type { Container } from './container.js' import type { Root } from './root.js' import type { Image } from './image.js' -import type { Text } from './text.js' import type { SVG } from './svg.js' -export type Component = Container | Root | Image | Text | SVG +export type Parent = Container | Root | Image | SVG export * from './container.js' export * from './root.js' export * from './image.js' export * from './text.js' export * from './svg.js' +export * from './icon.js' diff --git a/packages/uikit/src/vanilla/input.ts b/packages/uikit/src/vanilla/input.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/uikit/src/vanilla/svg.ts b/packages/uikit/src/vanilla/svg.ts index 7bdae11e..e04e2418 100644 --- a/packages/uikit/src/vanilla/svg.ts +++ b/packages/uikit/src/vanilla/svg.ts @@ -1,6 +1,6 @@ import { Object3D } from 'three' import { AllOptionalProperties } from '../properties/default.js' -import { Component } from './index.js' +import { Parent } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' import { unsubscribeSubscriptions } from '../utils.js' @@ -13,9 +13,16 @@ export class SVG extends Object3D { private container: Object3D private readonly propertiesSignal: Signal private readonly defaultPropertiesSignal: Signal + private srcSignal: Signal> - constructor(parent: Component, properties: SVGProperties, defaultProperties?: AllOptionalProperties) { + constructor( + parent: Parent, + src: string | Signal, + properties: SVGProperties, + defaultProperties?: AllOptionalProperties, + ) { super() + this.srcSignal = signal(src) this.propertiesSignal = signal(properties) this.defaultPropertiesSignal = signal(defaultProperties) this.eventConfig = parent.eventConfig @@ -26,6 +33,7 @@ export class SVG extends Object3D { parent.add(this.container) this.internals = createSVG( parent.internals, + this.srcSignal, this.propertiesSignal, this.defaultPropertiesSignal, { current: this.container }, @@ -39,6 +47,10 @@ export class SVG extends Object3D { bindHandlers(handlers, this, this.eventConfig, subscriptions) } + setSrc(src: string | Signal) { + this.srcSignal.value = src + } + setProperties(properties: SVGProperties, defaultProperties?: AllOptionalProperties) { batch(() => { this.propertiesSignal.value = properties diff --git a/packages/uikit/src/vanilla/text.ts b/packages/uikit/src/vanilla/text.ts index ca3d17d0..a986d7cd 100644 --- a/packages/uikit/src/vanilla/text.ts +++ b/packages/uikit/src/vanilla/text.ts @@ -1,7 +1,7 @@ import { Object3D } from 'three' import { createContainer } from '../components/container.js' import { AllOptionalProperties, Properties } from '../properties/default.js' -import { Component } from './index.js' +import { Parent } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' import { TextProperties, createText } from '../components/text.js' @@ -9,7 +9,7 @@ import { FontFamilies, unsubscribeSubscriptions } from '../internals.js' export class Text extends Object3D { private object: Object3D - public readonly internals: ReturnType + public readonly internals: ReturnType public readonly eventConfig: EventConfig private readonly propertiesSignal: Signal @@ -18,7 +18,7 @@ export class Text extends Object3D { private readonly fontFamiliesSignal: Signal constructor( - parent: Component, + parent: Parent, text: string | Signal | Array>, fontFamilies: FontFamilies | undefined, properties: TextProperties, @@ -37,7 +37,7 @@ export class Text extends Object3D { this.matrixAutoUpdate = false parent.add(this.object) - //setting up the container + //setting up the text this.internals = createText( parent.internals, this.textSignal, @@ -45,10 +45,9 @@ export class Text extends Object3D { this.propertiesSignal, this.defaultPropertiesSignal, { current: this.object }, - { current: this }, ) - //setup scrolling & events + //setup events const { handlers, interactionPanel, subscriptions } = this.internals this.add(interactionPanel) bindHandlers(handlers, this, this.eventConfig, subscriptions) From 53312756f26c50e7373c62662e8708b4dbec0779 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Thu, 4 Apr 2024 01:59:57 +0200 Subject: [PATCH 14/20] add input and custom-container --- packages/icons/lucide/vanilla/generate.ts | 2 +- packages/react/src/custom.tsx | 152 ++----- packages/react/src/index.ts | 1 + packages/react/src/input.tsx | 358 ++--------------- packages/react/src/ref.ts | 2 + packages/uikit/src/caret.ts | 2 +- packages/uikit/src/components/container.ts | 16 +- packages/uikit/src/components/content.ts | 0 packages/uikit/src/components/custom.ts | 134 +++++++ packages/uikit/src/components/icon.ts | 17 +- packages/uikit/src/components/image.ts | 11 +- packages/uikit/src/components/index.ts | 2 + packages/uikit/src/components/input.ts | 374 ++++++++++++++++++ packages/uikit/src/components/svg.ts | 11 +- packages/uikit/src/components/text.ts | 18 +- packages/uikit/src/components/utils.tsx | 8 +- packages/uikit/src/context.ts | 19 +- packages/uikit/src/events.ts | 1 + packages/uikit/src/selection.ts | 19 +- .../uikit/src/text/render/instanced-glyph.ts | 4 + packages/uikit/src/vanilla/container.ts | 5 +- packages/uikit/src/vanilla/custom.ts | 57 +++ packages/uikit/src/vanilla/icon.ts | 2 +- packages/uikit/src/vanilla/image.ts | 3 + packages/uikit/src/vanilla/index.ts | 2 + packages/uikit/src/vanilla/input.ts | 82 ++++ packages/uikit/src/vanilla/root.ts | 10 +- packages/uikit/src/vanilla/svg.ts | 5 +- packages/uikit/src/vanilla/text.ts | 16 +- 29 files changed, 811 insertions(+), 522 deletions(-) create mode 100644 packages/uikit/src/components/content.ts create mode 100644 packages/uikit/src/vanilla/custom.ts diff --git a/packages/icons/lucide/vanilla/generate.ts b/packages/icons/lucide/vanilla/generate.ts index 3d265b56..9a740584 100644 --- a/packages/icons/lucide/vanilla/generate.ts +++ b/packages/icons/lucide/vanilla/generate.ts @@ -17,7 +17,7 @@ async function main() { import { IconProperties } from '@vanilla-three/uikit/internals' const text = \`${svg}\`; export class ${name} extends Icon { - constructor(parent: Parent, properties: IconProperties, defaultProperties?: AllOptionalProperties,) { + constructor(parent: Parent, properties: IconProperties = {}, defaultProperties?: AllOptionalProperties,) { super(parent, text, 24, 24, properties, defaultProperties) } } diff --git a/packages/react/src/custom.tsx b/packages/react/src/custom.tsx index 79d21018..adc9fef6 100644 --- a/packages/react/src/custom.tsx +++ b/packages/react/src/custom.tsx @@ -1,132 +1,58 @@ -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { forwardRef, ReactNode, useEffect, useRef } from 'react' -import { YogaProperties } from '../flex/node.js' -import { useFlexNode, FlexProvider } from '../flex/react.js' -import { useApplyHoverProperties } from '../hover.js' -import { InteractionGroup, ShadowProperties } from '../panel/react.js' -import { createCollection, finalizeCollection, WithReactive } from '../properties/utils.js' -import { useRootGroupRef } from '../utils.js' -import { FrontSide, Group, Material, Mesh } from 'three' +import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' +import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' +import { Material, Mesh, Object3D } from 'three' +import { ParentProvider, useParent } from './context.js' +import { AddHandlers, usePropertySignals } from './utilts.js' import { - ComponentInternals, - LayoutListeners, - useComponentInternals, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, - ViewportListeners, - WithConditionals, -} from './utils.js' -import { panelGeometry } from '../panel/utils.js' -import { useGlobalClippingPlanes, useIsClipped, useParentClippingRect } from '../clipping.js' -import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' -import { flexAliasPropertyTransformation, WithAllAliases } from '../properties/alias.js' -import { TransformProperties, useTransformMatrix } from '../transform.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { useApplyProperties, WithClasses } from '../properties/default.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { ElementType, setupRenderOrder, useOrderInfo, ZIndexOffset } from '../order.js' -import { effect } from '@preact/signals-core' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useApplyActiveProperties } from '../active.js' - -export type CustomContainerProperties = WithConditionals< - WithClasses>> -> - -export const CustomContainer = forwardRef< - ComponentInternals, - { + createCustomContainer, + CustomContainerProperties, + panelGeometry, + unsubscribeSubscriptions, +} from '@vanilla-three/uikit/internals' +import { ComponentInternals, useComponentInternals } from './ref.js' + +export const CustomContainer: ( + props: { children?: ReactNode - zIndexOffset?: ZIndexOffset customDepthMaterial?: Material customDistanceMaterial?: Material } & CustomContainerProperties & EventHandlers & - LayoutListeners & - ViewportListeners & - ShadowProperties ->((properties, ref) => { - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - - const parentClippingRect = useParentClippingRect() - const rootGroupRef = useRootGroupRef() - const clippingPlanes = useGlobalClippingPlanes(parentClippingRect, rootGroupRef) - - const orderInfo = useOrderInfo(ElementType.Custom, properties.zIndexOffset, undefined) - - const meshRef = useRef(null) - - const globalMatrix = useGlobalMatrix(transformMatrix) - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - + RefAttributes, +) => ReactNode = forwardRef((properties, ref) => { + const parent = useParent() + const outerRef = useRef(null) + const innerRef = useRef(null) + const propertySignals = usePropertySignals(properties) + const internals = useMemo( + () => createCustomContainer(parent, propertySignals.properties, propertySignals.default, outerRef), + [parent, propertySignals], + ) useEffect(() => { - const mesh = meshRef.current - if (mesh == null) { - return - } - - mesh.raycast = makeClippedRaycast(mesh, mesh.raycast, rootGroupRef, parentClippingRect, orderInfo) - setupRenderOrder(mesh, node.cameraDistance, orderInfo) - - if (mesh.material instanceof Material) { - mesh.material.clippingPlanes = clippingPlanes - mesh.material.needsUpdate = true - mesh.material.shadowSide = FrontSide + if (innerRef.current != null) { + internals.setupMesh(innerRef.current) + if (innerRef.current.material instanceof Material) { + internals.setupMaterial(innerRef.current.material) + } } + return () => unsubscribeSubscriptions(internals.subscriptions) + }, [internals]) - const unsubscribeScale = effect(() => { - const [width, height] = node.size.value - mesh.scale.set(width * node.pixelSize, height * node.pixelSize, 1) - mesh.updateMatrix() - }) - - const unsubscribeVisibile = effect(() => void (mesh.visible = !isClipped.value)) - - return () => { - unsubscribeScale() - unsubscribeVisibile() - } - }, [clippingPlanes, node, isClipped, parentClippingRect, orderInfo, rootGroupRef]) - - //apply all properties - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties) - const activeHandlers = useApplyActiveProperties(collection, properties) - finalizeCollection(collection) - - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - - useComponentInternals(ref, node, meshRef) + useComponentInternals(ref, propertySignals.style, internals) return ( - - + + {properties.children} - - + + ) }) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 93af3d56..507468b4 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -19,3 +19,4 @@ export * from './image.js' export * from './text.js' export * from './svg.js' export * from './icon.js' +export * from './input.js' diff --git a/packages/react/src/input.tsx b/packages/react/src/input.tsx index 23ec59e4..7ea7265f 100644 --- a/packages/react/src/input.tsx +++ b/packages/react/src/input.tsx @@ -1,328 +1,52 @@ -import { - ComponentInternals, - LayoutListeners, - ViewportListeners, - useGlobalMatrix, - useLayoutListeners, - useViewportListeners, -} from './utils.js' -import { - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - PointerEvent as ReactPointerEvent, -} from 'react' -import { ReadonlySignal, Signal, signal } from '@preact/signals-core' -import { readReactive, useRootGroupRef, useSignalEffect } from '../utils.js' -import { TextProperties } from './text.js' -import { Group, Vector2, Vector2Tuple, Vector3Tuple } from 'three' -import { InstancedText } from '../text/render/instanced-text.js' -import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events.js' -import { MaterialClass } from '../index.js' -import { ElementType, ZIndexOffset, useOrderInfo } from '../order.js' -import { - InteractionGroup, - ShadowProperties, - useInstancedPanel, - useInteractionPanel, - usePanelGroupDependencies, -} from '../panel/react.js' -import { ScrollListeners } from '../scroll.js' -import { WithFocus, useApplyFocusProperties } from '../focus.js' -import { useApplyActiveProperties } from '../active.js' -import { useCaret } from '../caret.js' -import { useParentClippingRect, useIsClipped } from '../clipping.js' -import { useApplyPreferredColorSchemeProperties } from '../dark.js' -import { useFlexNode } from '../flex/react.js' -import { useApplyHoverProperties } from '../hover.js' -import { flexAliasPropertyTransformation, panelAliasPropertyTransformation } from '../properties/alias.js' -import { useApplyProperties } from '../properties/default.js' -import { useImmediateProperties } from '../properties/immediate.js' -import { createCollection, writeCollection, finalizeCollection } from '../properties/utils.js' -import { useApplyResponsiveProperties } from '../responsive.js' -import { SelectionBoxes, useSelection } from '../selection.js' -import { useInstancedText } from '../text/react.js' -import { useTransformMatrix } from '../transform.js' -import { FlexNode } from '../flex/node.js' - -export type InputProperties = WithFocus - -export type InputInternals = ComponentInternals & { readonly value: string | ReadonlySignal; focus: () => void } - -const cancelSet = new Set() - -function cancelBlur(event: PointerEvent) { - cancelSet.add(event) -} - -export const canvasInputProps = { - onPointerDown: (e: ReactPointerEvent) => { - if (!(document.activeElement instanceof HTMLElement)) { - return - } - if (!cancelSet.has(e.nativeEvent)) { - return - } - cancelSet.delete(e.nativeEvent) - e.preventDefault() - }, -} - -export const Input = forwardRef< - InputInternals, - { - panelMaterialClass?: MaterialClass - zIndexOffset?: ZIndexOffset +import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events' +import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react' +import { Object3D } from 'three' +import { useParent } from './context.js' +import { AddHandlers, usePropertySignals } from './utilts.js' +import { FontFamilies, unsubscribeSubscriptions, InputProperties, createInput } from '@vanilla-three/uikit/internals' +import { ComponentInternals, useComponentInternals } from './ref.js' +import { Signal, signal } from '@preact/signals-core' +import { useFontFamilies } from './font.js' + +export const Input: ( + props: { + children: string | Array> | Signal multiline?: boolean value?: string | Signal defaultValue?: string - onValueChange?: (value: string) => void - tabIndex?: number - disabled?: boolean } & InputProperties & EventHandlers & - LayoutListeners & - ViewportListeners & - ScrollListeners & - ShadowProperties ->((properties, ref) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - const defaultValue = useMemo(() => signal(properties.defaultValue ?? ''), []) - const value = useMemo(() => properties.value ?? defaultValue, [properties.value, defaultValue]) - - const onValueChangeRef = useRef(properties.onValueChange) - onValueChangeRef.current = properties.onValueChange - - const startCharIndex = useRef(undefined) - - const isControlled = properties.value != null - const onChange = useCallback( - (value: string) => { - if (!isControlled) { - defaultValue.value = value - } - onValueChangeRef.current?.(value) - }, - [defaultValue, isControlled], + RefAttributes, +) => ReactNode = forwardRef((properties, ref) => { + const parent = useParent() + const outerRef = useRef(null) + const propertySignals = usePropertySignals(properties) + const valueSignal = useMemo(() => signal | string>(''), []) + valueSignal.value = properties.value ?? '' + const fontFamilies = useMemo(() => signal(undefined as any), []) + fontFamilies.value = useFontFamilies() + //allows to not get a eslint error because of dependencies (we deliberatly never update this ref) + const defaultValue = useRef(properties.defaultValue) + const internals = useMemo( + () => + createInput( + parent, + defaultValue.current == null ? valueSignal : defaultValue.current, + properties.multiline ?? false, + fontFamilies, + propertySignals.properties, + propertySignals.default, + outerRef, + ), + [parent, valueSignal, properties.multiline, fontFamilies, propertySignals], ) - const selectionRange = useMemo(() => signal(undefined), []) - const element = useHtmlInputElement(value, selectionRange, onChange, properties.multiline) - element.tabIndex = properties.tabIndex ?? 0 - element.disabled = properties.disabled ?? false + useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals]) - // eslint-disable-next-line react-hooks/exhaustive-deps - const hasFocusSignal = useMemo(() => signal(document.activeElement === element), []) - useEffect(() => { - const updateFocus = () => (hasFocusSignal.value = document.activeElement === element) - element.addEventListener('focus', updateFocus) - element.addEventListener('blur', updateFocus) - return () => { - element.removeEventListener('focus', updateFocus) - element.removeEventListener('blur', updateFocus) - } - }, [element, hasFocusSignal]) - const setFocus = useCallback( - (focus: boolean) => { - if (hasFocusSignal.peek() === focus) { - return - } - if (focus) { - element.focus() - } else { - element.blur() - } - }, - [hasFocusSignal, element], - ) - - const collection = createCollection() - const groupRef = useRef(null) - const node = useFlexNode(groupRef) - useImmediateProperties(collection, node, flexAliasPropertyTransformation) - const transformMatrix = useTransformMatrix(collection, node) - const rootGroupRef = useRootGroupRef() - const globalMatrix = useGlobalMatrix(transformMatrix) - const parentClippingRect = useParentClippingRect() - const isClipped = useIsClipped(parentClippingRect, globalMatrix, node.size, node) - useLayoutListeners(properties, node.size) - useViewportListeners(properties, isClipped) - const groupDeps = usePanelGroupDependencies(properties.panelMaterialClass, properties) - const backgroundOrderInfo = useOrderInfo(ElementType.Panel, properties.zIndexOffset, groupDeps) - useInstancedPanel( - collection, - globalMatrix, - node.size, - undefined, - node.borderInset, - isClipped, - backgroundOrderInfo, - parentClippingRect, - groupDeps, - panelAliasPropertyTransformation, - ) - const selectionBoxes = useMemo(() => signal([]), []) - const caretPosition = useMemo(() => signal(undefined), []) - const selectionOrderInfo = useSelection( - globalMatrix, - selectionBoxes, - isClipped, - backgroundOrderInfo, - parentClippingRect, - ) - useCaret(collection, globalMatrix, caretPosition, isClipped, backgroundOrderInfo, parentClippingRect) - const interactionPanel = useInteractionPanel(node.size, node, backgroundOrderInfo, rootGroupRef) - const instancedTextRef = useRef() - const measureFunc = useInstancedText( - collection, - value, - globalMatrix, - node, - isClipped, - parentClippingRect, - selectionOrderInfo, - selectionRange, - selectionBoxes, - caretPosition, - instancedTextRef, - ) - - const disabled = properties.disabled ?? false - - useApplyProperties(collection, properties) - useApplyPreferredColorSchemeProperties(collection, properties) - useApplyResponsiveProperties(collection, properties) - const hoverHandlers = useApplyHoverProperties(collection, properties, disabled ? undefined : 'text') - const activeHandlers = useApplyActiveProperties(collection, properties) - useApplyFocusProperties(collection, properties, hasFocusSignal) - writeCollection(collection, 'measureFunc', measureFunc) - finalizeCollection(collection) - - useImperativeHandle( - ref, - () => ({ - focus: () => setFocus(true), - value, - borderInset: node.borderInset, - paddingInset: node.paddingInset, - pixelSize: node.pixelSize, - center: node.relativeCenter, - size: node.size, - interactionPanel, - }), - [interactionPanel, node, value, setFocus], - ) + useComponentInternals(ref, propertySignals.style, internals) return ( - { - if (e.defaultPrevented || e.uv == null || instancedTextRef.current == null) { - return - } - cancelBlur(e.nativeEvent) - e.stopPropagation() - const charIndex = uvToCharIndex(node, e.uv, instancedTextRef.current) - startCharIndex.current = charIndex - - setTimeout(() => { - setFocus(true) - selectionRange.value = [charIndex, charIndex] - element.setSelectionRange(charIndex, charIndex) - }) - }, - onPointerUp: (e) => { - startCharIndex.current = undefined - }, - onPointerLeave: (e) => { - startCharIndex.current = undefined - }, - onPointerMove: (e) => { - if (startCharIndex.current == null || e.uv == null || instancedTextRef.current == null) { - return - } - e.stopPropagation() - const charIndex = uvToCharIndex(node, e.uv, instancedTextRef.current) - - const start = Math.min(startCharIndex.current, charIndex) - const end = Math.max(startCharIndex.current, charIndex) - const direction = startCharIndex.current < charIndex ? 'forward' : 'backward' - - setTimeout(() => { - setFocus(true) - selectionRange.value = [start, end] - element.setSelectionRange(start, end, direction) - }) - }, - } - } - hoverHandlers={hoverHandlers} - activeHandlers={activeHandlers} - > - - + + + ) }) - -export function useHtmlInputElement( - value: string | Signal, - selectionRange: Signal, - onChange?: (value: string) => void, - multiline: boolean = false, -): HTMLInputElement | HTMLTextAreaElement { - const element = useMemo(() => { - const result = document.createElement(multiline ? 'textarea' : 'input') - const style = result.style - style.setProperty('position', 'absolute') - style.setProperty('left', '-1000vw') - style.setProperty('pointerEvents', 'none') - style.setProperty('opacity', '0') - result.addEventListener('input', () => { - onChange?.(result.value) - updateSelection() - }) - const updateSelection = () => { - const { selectionStart, selectionEnd } = result - if (selectionStart == null || selectionEnd == null) { - selectionRange.value = undefined - return - } - const current = selectionRange.peek() - if (current != null && current[0] === selectionStart && current[1] === selectionEnd) { - return - } - selectionRange.value = [selectionStart, selectionEnd] - } - result.addEventListener('keydown', updateSelection) - result.addEventListener('keyup', updateSelection) - result.addEventListener('blur', () => (selectionRange.value = undefined)) - document.body.appendChild(result) - return result - }, [onChange, selectionRange, multiline]) - useSignalEffect(() => { - element.value = readReactive(value) - }, [value]) - useEffect(() => () => element.remove(), [element]) - return element -} - -function uvToCharIndex( - { size, borderInset, paddingInset }: FlexNode, - uv: Vector2, - instancedText: InstancedText, -): number { - const [width, height] = size.peek() - const [bTop, , , bLeft] = borderInset.peek() - const [pTop, , , pLeft] = paddingInset.peek() - const x = uv.x * width - bLeft - pLeft - const y = -uv.y * height + bTop + pTop - return instancedText.getCharIndex(x, y) -} diff --git a/packages/react/src/ref.ts b/packages/react/src/ref.ts index 8382529e..015214b4 100644 --- a/packages/react/src/ref.ts +++ b/packages/react/src/ref.ts @@ -7,6 +7,7 @@ import { createSVG, createText, createIcon, + createCustomContainer, } from '@vanilla-three/uikit/internals' import { ForwardedRef, useImperativeHandle } from 'react' import { Vector2Tuple, Mesh } from 'three' @@ -32,6 +33,7 @@ export function useComponentInternals( | typeof createSVG | typeof createText | typeof createIcon + | typeof createCustomContainer > & { scrollPosition?: Signal }, diff --git a/packages/uikit/src/caret.ts b/packages/uikit/src/caret.ts index 273120c1..309aad44 100644 --- a/packages/uikit/src/caret.ts +++ b/packages/uikit/src/caret.ts @@ -30,7 +30,7 @@ function getCaretMaterialConfig() { return caretMaterialConfig } -export function useCaret( +export function createCaret( propertiesSignal: Signal, matrix: Signal, caretPosition: Signal, diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index 221a4c72..bc1ad80e 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -1,5 +1,5 @@ import { YogaProperties } from '../flex/node.js' -import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { computedIsClipped, computedClippingRect } from '../clipping.js' import { ScrollbarProperties, @@ -15,8 +15,8 @@ import { TransformProperties, applyTransform, computedTransformMatrix } from '.. import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js' -import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { Signal, computed, signal } from '@preact/signals-core' +import { createActivePropertyTransfomers } from '../active.js' +import { Signal, signal } from '@preact/signals-core' import { WithConditionals, computedGlobalMatrix, @@ -24,12 +24,11 @@ import { computedMergedProperties, createNode, } from './utils.js' -import { Subscriptions, unsubscribeSubscriptions } from '../utils.js' -import { MergedProperties } from '../properties/merged.js' +import { Subscriptions } from '../utils.js' import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' -import { Object3DRef, WithContext } from '../context.js' +import { Object3DRef, ParentContext } from '../context.js' import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' -import { addHandlers, cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { EventHandlers } from '../events.js' import { darkPropertyTransformers, getDefaultPanelMaterialConfig } from '../internals.js' @@ -51,7 +50,7 @@ export type InheritableContainerProperties = WithClasses< export type ContainerProperties = InheritableContainerProperties & Listeners & EventHandlers export function createContainer( - parentContext: WithContext, + parentContext: ParentContext, properties: Signal, defaultProperties: Signal, object: Object3DRef, @@ -139,7 +138,6 @@ export function createContainer( ), childrenMatrix, node, - object, orderInfo, root: parentContext.root, scrollPosition, diff --git a/packages/uikit/src/components/content.ts b/packages/uikit/src/components/content.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/uikit/src/components/custom.ts b/packages/uikit/src/components/custom.ts index e69de29b..43aa95cd 100644 --- a/packages/uikit/src/components/custom.ts +++ b/packages/uikit/src/components/custom.ts @@ -0,0 +1,134 @@ +import { YogaProperties } from '../flex/node.js' +import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { computedIsClipped, createGlobalClippingPlanes } from '../clipping.js' +import { ScrollbarProperties } from '../scroll.js' +import { WithAllAliases } from '../properties/alias.js' +import { PanelProperties } from '../panel/instanced-panel.js' +import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' +import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' +import { createResponsivePropertyTransformers } from '../responsive.js' +import { ElementType, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' +import { createActivePropertyTransfomers } from '../active.js' +import { Signal, effect, signal } from '@preact/signals-core' +import { + WithConditionals, + computedGlobalMatrix, + computedHandlers, + computedMergedProperties, + createNode, +} from './utils.js' +import { Subscriptions } from '../utils.js' +import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' +import { Object3DRef, ParentContext } from '../context.js' +import { createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { EventHandlers } from '../events.js' +import { + ShadowProperties, + createGetBatchedProperties, + darkPropertyTransformers, + makeClippedRaycast, +} from '../internals.js' +import { FrontSide, Material, Mesh } from 'three' + +export type InheritableCustomContainerProperties = WithClasses< + WithConditionals< + WithAllAliases< + WithReactive< + YogaProperties & + PanelProperties & + ZIndexProperties & + TransformProperties & + ScrollbarProperties & + ShadowProperties + > + > + > +> + +export type CustomContainerProperties = InheritableCustomContainerProperties & Listeners & EventHandlers + +const shadowPropertyKeys = ['castShadow', 'receiveShadow'] as const + +export function createCustomContainer( + parentContext: ParentContext, + properties: Signal, + defaultProperties: Signal, + object: Object3DRef, +) { + const hoveredSignal = signal>([]) + const activeSignal = signal>([]) + const subscriptions = [] as Subscriptions + + setupCursorCleanup(hoveredSignal, subscriptions) + + //properties + const mergedProperties = computedMergedProperties(properties, defaultProperties, { + ...darkPropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + }) + + //create node + const node = createNode(parentContext, mergedProperties, object, subscriptions) + + //transform + const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) + applyTransform(object, transformMatrix, subscriptions) + + const globalMatrix = computedGlobalMatrix(parentContext.childrenMatrix, transformMatrix) + + const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) + + //instanced panel + const orderInfo = computedOrderInfo(mergedProperties, ElementType.Custom, undefined, parentContext.orderInfo) + + const getShadowProperties = createGetBatchedProperties(mergedProperties, shadowPropertyKeys) + + const setupMesh = (mesh: Mesh): (() => void) => { + mesh.matrixAutoUpdate = false + mesh.raycast = makeClippedRaycast( + mesh, + mesh.raycast, + parentContext.root.object, + parentContext.clippingRect, + orderInfo, + ) + setupRenderOrder(mesh, parentContext.root, orderInfo) + const unsubscribeShadows = effect(() => { + mesh.receiveShadow = getShadowProperties('receiveShadow') ?? false + mesh.castShadow = getShadowProperties('castShadow') ?? false + }) + const unsubscribeScale = effect(() => { + const [width, height] = node.size.value + const pixelSize = parentContext.root.pixelSize + mesh.scale.set(width * pixelSize, height * pixelSize, 1) + mesh.updateMatrix() + }) + const unsubscribeVisible = effect(() => void (mesh.visible = !isClipped.value)) + + return () => { + unsubscribeShadows() + unsubscribeScale() + unsubscribeVisible() + } + } + + const clippingPlanes = createGlobalClippingPlanes(parentContext.root, parentContext.clippingRect, subscriptions) + const setupMaterial = (material: Material) => { + material.clippingPlanes = clippingPlanes + material.needsUpdate = true + material.shadowSide = FrontSide + } + + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) + + return { + setupMesh, + setupMaterial, + node, + handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal), + subscriptions, + } +} diff --git a/packages/uikit/src/components/icon.ts b/packages/uikit/src/components/icon.ts index d31d0ed7..f4460bf0 100644 --- a/packages/uikit/src/components/icon.ts +++ b/packages/uikit/src/components/icon.ts @@ -1,7 +1,7 @@ import { Signal, effect, signal } from '@preact/signals-core' import { Group, Mesh, MeshBasicMaterial, Plane, ShapeGeometry } from 'three' import { Listeners } from '../index.js' -import { Object3DRef, WithContext } from '../context.js' +import { Object3DRef, ParentContext } from '../context.js' import { FlexNode, YogaProperties } from '../flex/index.js' import { ElementType, OrderInfo, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' import { PanelProperties } from '../panel/instanced-panel.js' @@ -55,7 +55,7 @@ export type InheritableIconProperties = WithClasses< export type IconProperties = InheritableIconProperties & Listeners & EventHandlers export function createIcon( - parentContext: WithContext, + parentContext: ParentContext, text: string, svgWidth: number, svgHeight: number, @@ -113,18 +113,7 @@ export function createIcon( setupViewportListeners(properties, isClipped, subscriptions) return { - clippingRect: computedClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - parentContext.root.pixelSize, - parentContext.clippingRect, - ), node, - object, - orderInfo, - root: parentContext.root, subscriptions, iconGroup, handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal), @@ -144,7 +133,7 @@ function createIconGroup( text: string, svgWidth: number, svgHeight: number, - parentContext: WithContext, + parentContext: ParentContext, orderInfo: Signal, node: FlexNode, isClipped: Signal, diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index 45617c27..388a88df 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -10,7 +10,7 @@ import { Vector2Tuple, } from 'three' import { Listeners } from '../index.js' -import { Object3DRef, WithContext } from '../context.js' +import { Object3DRef, ParentContext } from '../context.js' import { Inset, YogaProperties } from '../flex/index.js' import { ElementType, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' import { PanelProperties } from '../panel/instanced-panel.js' @@ -78,7 +78,7 @@ export type InheritableImageProperties = WithClasses< export type ImageProperties = InheritableImageProperties & Listeners & EventHandlers export function createImage( - parentContext: WithContext, + parentContext: ParentContext, srcSignal: Signal | string | Texture | Signal>, properties: Signal, defaultProperties: Signal, @@ -155,7 +155,7 @@ export function createImage( setupLayoutListeners(properties, node.size, subscriptions) setupViewportListeners(properties, isClipped, subscriptions) - const ctx: WithContext = { + const ctx: ParentContext = { clippingRect: computedClippingRect( globalMatrix, node.size, @@ -166,7 +166,6 @@ export function createImage( ), childrenMatrix, node, - object, orderInfo, root: parentContext.root, } @@ -200,8 +199,8 @@ function getImageMaterialConfig() { function createImageMesh( propertiesSignal: Signal, texture: Signal, - parent: WithContext, - { node, orderInfo, root, clippingRect }: WithContext, + parent: ParentContext, + { node, orderInfo, root, clippingRect }: ParentContext, isHidden: Signal, subscriptions: Subscriptions, ) { diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/components/index.ts index 22576fa0..a05936be 100644 --- a/packages/uikit/src/components/index.ts +++ b/packages/uikit/src/components/index.ts @@ -5,3 +5,5 @@ export * from './image.js' export * from './text.js' export * from './svg.js' export * from './icon.js' +export * from './input.js' +export * from './custom.js' diff --git a/packages/uikit/src/components/input.ts b/packages/uikit/src/components/input.ts index e69de29b..b83836f7 100644 --- a/packages/uikit/src/components/input.ts +++ b/packages/uikit/src/components/input.ts @@ -0,0 +1,374 @@ +import { YogaProperties } from '../flex/node.js' +import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { computedIsClipped, computedClippingRect } from '../clipping.js' +import { ScrollbarProperties } from '../scroll.js' +import { WithAllAliases } from '../properties/alias.js' +import { PanelProperties, createInstancedPanel } from '../panel/instanced-panel.js' +import { TransformProperties, applyTransform, computedTransformMatrix } from '../transform.js' +import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' +import { createResponsivePropertyTransformers } from '../responsive.js' +import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js' +import { createActivePropertyTransfomers } from '../active.js' +import { Signal, computed, effect, signal } from '@preact/signals-core' +import { + WithConditionals, + computedGlobalMatrix, + computedHandlers, + computedMergedProperties, + createNode, +} from './utils.js' +import { Subscriptions, readReactive } from '../utils.js' +import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' +import { Object3DRef, ParentContext } from '../context.js' +import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' +import { createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { EventHandlers } from '../events.js' +import { + FlexNode, + FontFamilies, + GetBatchedProperties, + InstancedText, + InstancedTextProperties, + computedFont, + computedGylphGroupDependencies, + createGetBatchedProperties, + createInstancedText, + darkPropertyTransformers, + getDefaultPanelMaterialConfig, +} from '../internals.js' +import { Vector2Tuple, Vector2, Vector3Tuple } from 'three' +import { createCaret } from '../caret.js' +import { SelectionBoxes, createSelection } from '../selection.js' + +export type InheritableInputProperties = WithClasses< + WithConditionals< + WithAllAliases< + WithReactive< + YogaProperties & + PanelProperties & + ZIndexProperties & + TransformProperties & + ScrollbarProperties & + PanelGroupProperties & + InstancedTextProperties & + DisabledProperties + > + > + > +> + +export type DisabledProperties = { + disabled?: boolean +} +const disabledKeys = ['disabled'] as const + +const cancelSet = new Set() + +function cancelBlur(event: PointerEvent) { + cancelSet.add(event) +} + +export const canvasInputProps = { + onPointerDown: (e: { nativeEvent: any; preventDefault: () => void }) => { + if (!(document.activeElement instanceof HTMLElement)) { + return + } + if (!cancelSet.has(e.nativeEvent)) { + return + } + cancelSet.delete(e.nativeEvent) + e.preventDefault() + }, +} + +export type InputProperties = InheritableInputProperties & + Listeners & + EventHandlers & { + onValueChange?: (value: string) => void + } + +export function createInput( + parentContext: ParentContext, + proviedValue: string | Signal>, + multiline: boolean, + fontFamilies: Signal | undefined, + properties: Signal, + defaultProperties: Signal, + object: Object3DRef, +) { + const hoveredSignal = signal>([]) + const activeSignal = signal>([]) + const subscriptions = [] as Subscriptions + setupCursorCleanup(hoveredSignal, subscriptions) + + const mergedProperties = computedMergedProperties(properties, defaultProperties, { + ...darkPropertyTransformers, + ...createResponsivePropertyTransformers(parentContext.root.node.size), + ...createHoverPropertyTransformers(hoveredSignal), + ...createActivePropertyTransfomers(activeSignal), + }) + + const node = createNode(parentContext, mergedProperties, object, subscriptions) + + const transformMatrix = computedTransformMatrix(mergedProperties, node, parentContext.root.pixelSize) + applyTransform(object, transformMatrix, subscriptions) + + const globalMatrix = computedGlobalMatrix(parentContext.childrenMatrix, transformMatrix) + + const isClipped = computedIsClipped(parentContext.clippingRect, globalMatrix, node.size, parentContext.root.pixelSize) + + const groupDeps = computedPanelGroupDependencies(mergedProperties) + const backgroundOrderInfo = computedOrderInfo(mergedProperties, ElementType.Panel, groupDeps, parentContext.orderInfo) + createInstancedPanel( + mergedProperties, + backgroundOrderInfo, + groupDeps, + parentContext.root.panelGroupManager, + globalMatrix, + node.size, + undefined, + node.borderInset, + parentContext.clippingRect, + isClipped, + getDefaultPanelMaterialConfig(), + subscriptions, + ) + + let valueSignal: Signal + let controlled: boolean + if (proviedValue instanceof Signal) { + valueSignal = computed(() => readReactive(valueSignal.value)) + controlled = true + } else { + valueSignal = signal(proviedValue) + controlled = false + } + + const instancedTextRef: { current?: InstancedText } = {} + const selectionBoxes = signal([]) + const caretPosition = signal(undefined) + createCaret( + mergedProperties, + globalMatrix, + caretPosition, + isClipped, + backgroundOrderInfo, + parentContext.clippingRect, + parentContext.root.panelGroupManager, + subscriptions, + ) + const selectionOrderInfo = createSelection( + mergedProperties, + globalMatrix, + selectionBoxes, + isClipped, + backgroundOrderInfo, + parentContext.clippingRect, + parentContext.root.panelGroupManager, + subscriptions, + ) + + const fontSignal = computedFont(mergedProperties, fontFamilies, parentContext.root.renderer, subscriptions) + const orderInfo = computedOrderInfo( + undefined, + ElementType.Text, + computedGylphGroupDependencies(fontSignal), + selectionOrderInfo, + ) + const measureFunc = createInstancedText( + mergedProperties, + valueSignal, + globalMatrix, + node, + isClipped, + parentContext.clippingRect, + orderInfo, + fontSignal, + parentContext.root.gylphGroupManager, + undefined, + undefined, + undefined, + instancedTextRef, + subscriptions, + ) + subscriptions.push(node.setMeasureFunc(measureFunc)) + + setupLayoutListeners(properties, node.size, subscriptions) + setupViewportListeners(properties, isClipped, subscriptions) + + const getDisabled = createGetBatchedProperties(mergedProperties, disabledKeys) + + const selectionRange = signal(undefined) + const element = createHtmlInputElement( + valueSignal, + selectionRange, + (newValue) => { + if (!controlled) { + valueSignal.value = newValue + } + properties.peek().onValueChange?.(newValue) + }, + multiline, + getDisabled, + subscriptions, + ) + const hasFocusSignal = computedHasFocus(element, subscriptions) + const selectionHandlers = computedSelectionHandlers( + node, + element, + instancedTextRef, + selectionRange, + (focus) => { + if (hasFocusSignal.peek() === focus) { + return + } + if (focus) { + element.focus() + } else { + element.blur() + } + }, + getDisabled, + ) + + return { + element, + node, + interactionPanel: createInteractionPanel( + node, + backgroundOrderInfo, + parentContext.root, + parentContext.clippingRect, + subscriptions, + ), + handlers: computedHandlers(properties, defaultProperties, hoveredSignal, activeSignal, selectionHandlers), + subscriptions, + } +} + +export function computedSelectionHandlers( + node: FlexNode, + element: HTMLInputElement | HTMLTextAreaElement, + instancedTextRef: { current?: InstancedText }, + selectionRange: Signal, + setFocus: (focus: boolean) => void, + getDisabled: GetBatchedProperties, +) { + return computed(() => { + if (getDisabled('disabled') === true) { + return undefined + } + let startCharIndex: number | undefined + return { + onPointerDown: (e) => { + if (e.defaultPrevented || e.uv == null || instancedTextRef.current == null) { + return + } + cancelBlur(e.nativeEvent) + e.stopPropagation?.() + const charIndex = uvToCharIndex(node, e.uv, instancedTextRef.current) + startCharIndex = charIndex + + setTimeout(() => { + setFocus(true) + selectionRange.value = [charIndex, charIndex] + element.setSelectionRange(charIndex, charIndex) + }) + }, + onPointerUp: (e) => { + startCharIndex = undefined + }, + onPointerLeave: (e) => { + startCharIndex = undefined + }, + onPointerMove: (e) => { + if (startCharIndex == null || e.uv == null || instancedTextRef.current == null) { + return + } + e.stopPropagation?.() + const charIndex = uvToCharIndex(node, e.uv, instancedTextRef.current) + + const start = Math.min(startCharIndex, charIndex) + const end = Math.max(startCharIndex, charIndex) + const direction = startCharIndex < charIndex ? 'forward' : 'backward' + + setTimeout(() => { + setFocus(true) + selectionRange.value = [start, end] + element.setSelectionRange(start, end, direction) + }) + }, + } + }) +} + +export function createHtmlInputElement( + value: string | Signal, + selectionRange: Signal, + onChange: (value: string) => void, + multiline: boolean, + getDisabled: GetBatchedProperties, + subscriptions: Subscriptions, +): HTMLInputElement | HTMLTextAreaElement { + const element = document.createElement(multiline ? 'textarea' : 'input') + const style = element.style + style.setProperty('position', 'absolute') + style.setProperty('left', '-1000vw') + style.setProperty('pointerEvents', 'none') + style.setProperty('opacity', '0') + element.addEventListener('input', () => { + onChange?.(element.value) + updateSelection() + }) + const updateSelection = () => { + const { selectionStart, selectionEnd } = element + if (selectionStart == null || selectionEnd == null) { + selectionRange.value = undefined + return + } + const current = selectionRange.peek() + if (current != null && current[0] === selectionStart && current[1] === selectionEnd) { + return + } + selectionRange.value = [selectionStart, selectionEnd] + } + element.addEventListener('keydown', updateSelection) + element.addEventListener('keyup', updateSelection) + element.addEventListener('blur', () => (selectionRange.value = undefined)) + document.body.appendChild(element) + subscriptions.push( + effect(() => (element.value = readReactive(value))), + effect(() => (element.disabled = getDisabled('disabled') ?? false)), + () => element.remove(), + ) + return element +} + +function computedHasFocus(element: HTMLElement, subscriptions: Subscriptions) { + const hasFocusSignal = signal(document.activeElement === element) + subscriptions.push( + effect(() => { + const updateFocus = () => (hasFocusSignal.value = document.activeElement === element) + element.addEventListener('focus', updateFocus) + element.addEventListener('blur', updateFocus) + return () => { + element.removeEventListener('focus', updateFocus) + element.removeEventListener('blur', updateFocus) + } + }), + ) + return hasFocusSignal +} + +function uvToCharIndex( + { size, borderInset, paddingInset }: FlexNode, + uv: Vector2, + instancedText: InstancedText, +): number { + const [width, height] = size.peek() + const [bTop, , , bLeft] = borderInset.peek() + const [pTop, , , pLeft] = paddingInset.peek() + const x = uv.x * width - bLeft - pLeft + const y = -uv.y * height + bTop + pTop + return instancedText.getCharIndex(x, y) +} diff --git a/packages/uikit/src/components/svg.ts b/packages/uikit/src/components/svg.ts index a4fe045a..0e384b8c 100644 --- a/packages/uikit/src/components/svg.ts +++ b/packages/uikit/src/components/svg.ts @@ -1,7 +1,7 @@ import { Signal, computed, effect, signal } from '@preact/signals-core' import { Box3, Color, Group, Mesh, MeshBasicMaterial, Object3D, Plane, ShapeGeometry, Vector3 } from 'three' import { Listeners } from '../index.js' -import { Object3DRef, WithContext } from '../context.js' +import { Object3DRef, ParentContext } from '../context.js' import { FlexNode, YogaProperties } from '../flex/index.js' import { ElementType, OrderInfo, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' import { PanelProperties } from '../panel/instanced-panel.js' @@ -30,9 +30,9 @@ import { ColorRepresentation, Subscriptions, fitNormalizedContentInside, readRea import { makeClippedRaycast } from '../panel/interaction-panel-mesh.js' import { computedIsClipped, computedClippingRect, ClippingRect, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupViewportListeners } from '../listeners.js' -import { addActiveHandlers, createActivePropertyTransfomers } from '../active.js' -import { addHoverHandlers, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' -import { addHandlers, cloneHandlers, createInteractionPanel } from '../panel/instanced-panel-mesh.js' +import { createActivePropertyTransfomers } from '../active.js' +import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { createResponsivePropertyTransformers } from '../responsive.js' import { EventHandlers } from '../events.js' import { @@ -68,7 +68,7 @@ export type AppearanceProperties = { export type SVGProperties = InheritableSVGProperties & Listeners & EventHandlers export function createSVG( - parentContext: WithContext, + parentContext: ParentContext, srcSignal: Signal | string>, properties: Signal, defaultProperties: Signal, @@ -168,7 +168,6 @@ export function createSVG( ), childrenMatrix, node, - object, orderInfo, root: parentContext.root, subscriptions, diff --git a/packages/uikit/src/components/text.ts b/packages/uikit/src/components/text.ts index 813df915..9ed3fed5 100644 --- a/packages/uikit/src/components/text.ts +++ b/packages/uikit/src/components/text.ts @@ -19,7 +19,7 @@ import { } from './utils.js' import { Subscriptions } from '../utils.js' import { Listeners, setupLayoutListeners, setupViewportListeners } from '../listeners.js' -import { Object3DRef, WithContext } from '../context.js' +import { Object3DRef, ParentContext } from '../context.js' import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' import { createInteractionPanel } from '../panel/instanced-panel-mesh.js' import { EventHandlers } from '../events.js' @@ -52,7 +52,7 @@ export type InheritableTextProperties = WithClasses< export type TextProperties = InheritableTextProperties & Listeners & EventHandlers export function createText( - parentContext: WithContext, + parentContext: ParentContext, textSignal: Signal | Array>>, fontFamilies: Signal | undefined, properties: Signal, @@ -102,7 +102,7 @@ export function createText( undefined, ElementType.Text, computedGylphGroupDependencies(fontSignal), - parentContext.orderInfo, + backgroundOrderInfo, ) const measureFunc = createInstancedText( @@ -118,6 +118,7 @@ export function createText( undefined, undefined, undefined, + undefined, subscriptions, ) subscriptions.push(node.setMeasureFunc(measureFunc)) @@ -126,18 +127,7 @@ export function createText( setupViewportListeners(properties, isClipped, subscriptions) return { - clippingRect: computedClippingRect( - globalMatrix, - node.size, - node.borderInset, - node.overflow, - parentContext.root.pixelSize, - parentContext.clippingRect, - ), node, - object, - orderInfo: backgroundOrderInfo, - root: parentContext.root, interactionPanel: createInteractionPanel( node, backgroundOrderInfo, diff --git a/packages/uikit/src/components/utils.tsx b/packages/uikit/src/components/utils.tsx index e9b6c972..a86f8ec1 100644 --- a/packages/uikit/src/components/utils.tsx +++ b/packages/uikit/src/components/utils.tsx @@ -16,7 +16,7 @@ import { PropertyTransformers, RootContext, ShadowProperties, - WithContext, + ParentContext, addHandlers, cloneHandlers, createGetBatchedProperties, @@ -64,7 +64,7 @@ export function loadResourceWithParams>( } export function createNode( - parentContext: WithContext, + parentContext: ParentContext, mergedProperties: Signal, object: Object3DRef, subscriptions: Subscriptions, @@ -95,11 +95,11 @@ export function computedHandlers( defaultProperties: Signal, hoveredSignal: Signal>, activeSignal: Signal>, - scrollHandlers?: Signal, + dynamicHandlers?: Signal, ) { return computed(() => { const handlers = cloneHandlers(properties.value) - addHandlers(handlers, scrollHandlers?.value) + addHandlers(handlers, dynamicHandlers?.value) addHoverHandlers(handlers, properties.value, defaultProperties.value, hoveredSignal) addActiveHandlers(handlers, properties.value, defaultProperties.value, activeSignal) return handlers diff --git a/packages/uikit/src/context.ts b/packages/uikit/src/context.ts index 6fe0300c..b4a0d6e3 100644 --- a/packages/uikit/src/context.ts +++ b/packages/uikit/src/context.ts @@ -6,24 +6,23 @@ import { OrderInfo, WithCameraDistance } from './order.js' import { GlyphGroupManager } from './text/render/instanced-glyph-group.js' import { PanelGroupManager } from './panel/instanced-panel-group.js' -export type WithContext = ElementContext & Readonly<{ root: RootContext }> +export type ParentContext = Readonly<{ + node: FlexNode + clippingRect: Signal + childrenMatrix: Signal + orderInfo: Signal + root: RootContext +}> export type Object3DRef = { current: Object3D | null } export type RootContext = WithCameraDistance & Readonly<{ + object: Object3DRef gylphGroupManager: GlyphGroupManager panelGroupManager: PanelGroupManager pixelSize: number onFrameSet: Set<(delta: number) => void> renderer: WebGLRenderer }> & - ElementContext - -export type ElementContext = Readonly<{ - node: FlexNode - clippingRect: Signal - childrenMatrix: Signal - orderInfo: Signal - object: Object3DRef -}> + Omit diff --git a/packages/uikit/src/events.ts b/packages/uikit/src/events.ts index 3c374d3f..6d6a32a3 100644 --- a/packages/uikit/src/events.ts +++ b/packages/uikit/src/events.ts @@ -4,6 +4,7 @@ export type ThreeEvent = Intersection & { nativeEvent: TSourceEvent defaultPrevented?: boolean stopped?: boolean + stopPropagation?: () => void } export type KeyToEvent = Parameters[K]>[0] diff --git a/packages/uikit/src/selection.ts b/packages/uikit/src/selection.ts index d5e54ec1..73fe9313 100644 --- a/packages/uikit/src/selection.ts +++ b/packages/uikit/src/selection.ts @@ -1,5 +1,4 @@ import { Signal, effect, signal } from '@preact/signals-core' -import { useMemo } from 'react' import { createInstancedPanel } from './panel/instanced-panel.js' import { Matrix4, Vector2Tuple } from 'three' import { ClippingRect } from './clipping.js' @@ -33,24 +32,22 @@ function getSelectionMaterialConfig() { return selectionMaterialConfig } -export function useSelection( +export function createSelection( propertiesSignal: Signal, matrix: Signal, selectionBoxes: Signal, isHidden: Signal | undefined, - parentOrderInfo: Signal, + prevOrderInfo: Signal, parentClippingRect: Signal | undefined, panelGroupManager: PanelGroupManager, subscriptions: Subscriptions, ): Signal { - const panels = useMemo< - Array<{ - size: Signal - offset: Signal - panelSubscriptions: Subscriptions - }> - >(() => [], []) - const orderInfo = computedOrderInfo(undefined, ElementType.Panel, undefined, parentOrderInfo) + const panels: Array<{ + size: Signal + offset: Signal + panelSubscriptions: Subscriptions + }> = [] + const orderInfo = computedOrderInfo(undefined, ElementType.Panel, undefined, prevOrderInfo) subscriptions.push( effect(() => { diff --git a/packages/uikit/src/text/render/instanced-glyph.ts b/packages/uikit/src/text/render/instanced-glyph.ts index 77012d5b..cad82b49 100644 --- a/packages/uikit/src/text/render/instanced-glyph.ts +++ b/packages/uikit/src/text/render/instanced-glyph.ts @@ -41,6 +41,7 @@ export function createInstancedText( selectionRange: Signal | undefined, selectionBoxes: Signal | undefined, caretPosition: Signal | undefined, + instancedTextRef: { current?: InstancedText } | undefined, subscriptions: Subscriptions, ) { let layoutPropertiesRef: { current: GlyphLayoutProperties | undefined } = { current: undefined } @@ -85,6 +86,9 @@ export function createInstancedText( selectionBoxes, caretPosition, ) + if (instancedTextRef != null) { + instancedTextRef.current = instancedText + } return () => instancedText.destroy() }), ) diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts index d5a63ad4..2f4e3114 100644 --- a/packages/uikit/src/vanilla/container.ts +++ b/packages/uikit/src/vanilla/container.ts @@ -5,17 +5,20 @@ import { Parent } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' import { unsubscribeSubscriptions } from '../utils.js' +import { FontFamilies } from '../internals.js' export class Container extends Object3D { private object: Object3D public readonly internals: ReturnType public readonly eventConfig: EventConfig + public readonly fontFamiliesSignal: Signal private readonly propertiesSignal: Signal private readonly defaultPropertiesSignal: Signal - constructor(parent: Parent, properties: ContainerProperties, defaultProperties?: AllOptionalProperties) { + constructor(parent: Parent, properties: ContainerProperties = {}, defaultProperties?: AllOptionalProperties) { super() + this.fontFamiliesSignal = parent.fontFamiliesSignal this.propertiesSignal = signal(properties) this.defaultPropertiesSignal = signal(defaultProperties) this.eventConfig = parent.eventConfig diff --git a/packages/uikit/src/vanilla/custom.ts b/packages/uikit/src/vanilla/custom.ts new file mode 100644 index 00000000..5d48b39d --- /dev/null +++ b/packages/uikit/src/vanilla/custom.ts @@ -0,0 +1,57 @@ +import { Mesh, MeshBasicMaterial, Object3D } from 'three' +import { AllOptionalProperties, Properties } from '../properties/default.js' +import { Parent } from './index.js' +import { EventConfig, bindHandlers } from './utils.js' +import { Signal, batch, signal } from '@preact/signals-core' +import { unsubscribeSubscriptions } from '../utils.js' +import { CustomContainerProperties, FontFamilies, createCustomContainer, panelGeometry } from '../internals.js' + +export class CustomContainer extends Object3D { + private object: Object3D + public readonly internals: ReturnType + public readonly eventConfig: EventConfig + public readonly fontFamiliesSignal: Signal + + private readonly propertiesSignal: Signal + private readonly defaultPropertiesSignal: Signal + + constructor(parent: Parent, properties: CustomContainerProperties = {}, defaultProperties?: AllOptionalProperties) { + super() + this.fontFamiliesSignal = parent.fontFamiliesSignal + this.propertiesSignal = signal(properties) + this.defaultPropertiesSignal = signal(defaultProperties) + this.eventConfig = parent.eventConfig + //setting up the threejs elements + this.object = new Object3D() + this.object.matrixAutoUpdate = false + this.object.add(this) + this.matrixAutoUpdate = false + parent.add(this.object) + + //setting up the container + this.internals = createCustomContainer(parent.internals, this.propertiesSignal, this.defaultPropertiesSignal, { + current: this.object, + }) + + //setup events + const { handlers, subscriptions, setupMesh, setupMaterial } = this.internals + //TODO: make the custom container the mesh + const mesh = new Mesh(panelGeometry, new MeshBasicMaterial()) + setupMesh(mesh) + setupMaterial(mesh.material) + this.add(mesh) + bindHandlers(handlers, this, this.eventConfig, subscriptions) + } + + setProperties(properties: Properties, defaultProperties?: AllOptionalProperties) { + batch(() => { + this.propertiesSignal.value = properties + this.defaultPropertiesSignal.value = defaultProperties + }) + } + + destroy() { + this.object.parent?.remove(this.object) + unsubscribeSubscriptions(this.internals.subscriptions) + } +} diff --git a/packages/uikit/src/vanilla/icon.ts b/packages/uikit/src/vanilla/icon.ts index e9391a44..0cb7456e 100644 --- a/packages/uikit/src/vanilla/icon.ts +++ b/packages/uikit/src/vanilla/icon.ts @@ -19,7 +19,7 @@ export class Icon extends Object3D { text: string, svgWidth: number, svgHeight: number, - properties: IconProperties, + properties: IconProperties = {}, defaultProperties?: AllOptionalProperties, ) { super() diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index 773abf38..60df6470 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -5,10 +5,12 @@ import { Parent } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' import { unsubscribeSubscriptions } from '../utils.js' +import { FontFamilies } from '../internals.js' export class Image extends Object3D { public readonly internals: ReturnType public readonly eventConfig: EventConfig + public readonly fontFamiliesSignal: Signal private container: Object3D private readonly propertiesSignal: Signal @@ -22,6 +24,7 @@ export class Image extends Object3D { defaultProperties?: AllOptionalProperties, ) { super() + this.fontFamiliesSignal = parent.fontFamiliesSignal this.srcSignal = signal(src) this.propertiesSignal = signal(properties) this.defaultPropertiesSignal = signal(defaultProperties) diff --git a/packages/uikit/src/vanilla/index.ts b/packages/uikit/src/vanilla/index.ts index 00370e9f..12be23eb 100644 --- a/packages/uikit/src/vanilla/index.ts +++ b/packages/uikit/src/vanilla/index.ts @@ -11,3 +11,5 @@ export * from './image.js' export * from './text.js' export * from './svg.js' export * from './icon.js' +export * from './input.js' +export * from './custom.js' diff --git a/packages/uikit/src/vanilla/input.ts b/packages/uikit/src/vanilla/input.ts index e69de29b..1d13d571 100644 --- a/packages/uikit/src/vanilla/input.ts +++ b/packages/uikit/src/vanilla/input.ts @@ -0,0 +1,82 @@ +import { Object3D } from 'three' +import { AllOptionalProperties, Properties } from '../properties/default.js' +import { Parent } from './index.js' +import { EventConfig, bindHandlers } from './utils.js' +import { Signal, batch, signal } from '@preact/signals-core' +import { unsubscribeSubscriptions } from '../internals.js' +import { InputProperties, createInput } from '../components/input.js' + +export class Input extends Object3D { + private object: Object3D + public readonly internals: ReturnType + public readonly eventConfig: EventConfig + + private readonly propertiesSignal: Signal + private readonly defaultPropertiesSignal: Signal + + private textSignal?: Signal | string> + + constructor( + parent: Parent, + value: string | Signal = '', + controlled: boolean = false, + multiline: boolean = false, + properties: InputProperties = {}, + defaultProperties?: AllOptionalProperties, + ) { + super() + this.propertiesSignal = signal(properties) + this.defaultPropertiesSignal = signal(defaultProperties) + this.textSignal = signal(value) + this.eventConfig = parent.eventConfig + //setting up the threejs elements + this.object = new Object3D() + this.object.matrixAutoUpdate = false + this.object.add(this) + this.matrixAutoUpdate = false + parent.add(this.object) + + if (!controlled && value instanceof Signal) { + throw new Error(`uncontrolled inputs can only receive string values`) + } + + //setting up the text + this.internals = createInput( + parent.internals, + controlled ? (this.textSignal = signal(value)) : value, + multiline, + parent.fontFamiliesSignal, + this.propertiesSignal, + this.defaultPropertiesSignal, + { current: this.object }, + ) + + //setup events + const { handlers, interactionPanel, subscriptions } = this.internals + this.add(interactionPanel) + bindHandlers(handlers, this, this.eventConfig, subscriptions) + } + + setTabIndex(tabIndex: number) { + this.internals.element.tabIndex = tabIndex + } + + setValue(text: string | Signal) { + if (this.textSignal == null) { + throw new Error(`cannot setValue on an uncontrolled input`) + } + this.textSignal.value = text + } + + setProperties(properties: Properties, defaultProperties?: AllOptionalProperties) { + batch(() => { + this.propertiesSignal.value = properties + this.defaultPropertiesSignal.value = defaultProperties + }) + } + + destroy() { + this.object.parent?.remove(this.object) + unsubscribeSubscriptions(this.internals.subscriptions) + } +} diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index 180f3d9a..12d6b59b 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -4,9 +4,11 @@ import { AllOptionalProperties } from '../properties/default.js' import { createRoot, RootProperties } from '../components/root.js' import { EventConfig, bindHandlers } from './utils.js' import { unsubscribeSubscriptions } from '../utils.js' +import { FontFamilies } from '../internals.js' export class Root extends Object3D { public readonly internals: ReturnType + public readonly fontFamiliesSignal: Signal private object: Object3D private readonly propertiesSignal: Signal @@ -17,10 +19,12 @@ export class Root extends Object3D { camera: Camera | (() => Camera), renderer: WebGLRenderer, parent: Object3D, - properties: RootProperties, + fontFamilies?: FontFamilies, + properties: RootProperties = {}, defaultProperties?: AllOptionalProperties, ) { super() + this.fontFamiliesSignal = signal(fontFamilies) this.propertiesSignal = signal(properties) this.defaultPropertiesSignal = signal(defaultProperties) this.object = new Object3D() @@ -50,6 +54,10 @@ export class Root extends Object3D { } } + setFontFamilies(fontFamilies: FontFamilies | undefined) { + this.fontFamiliesSignal.value = fontFamilies + } + setProperties(properties: RootProperties, defaultProperties?: AllOptionalProperties) { batch(() => { this.propertiesSignal.value = properties diff --git a/packages/uikit/src/vanilla/svg.ts b/packages/uikit/src/vanilla/svg.ts index e04e2418..5801185d 100644 --- a/packages/uikit/src/vanilla/svg.ts +++ b/packages/uikit/src/vanilla/svg.ts @@ -5,10 +5,12 @@ import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' import { unsubscribeSubscriptions } from '../utils.js' import { SVGProperties, createSVG } from '../components/svg.js' +import { FontFamilies } from '../internals.js' export class SVG extends Object3D { public readonly internals: ReturnType public readonly eventConfig: EventConfig + public readonly fontFamiliesSignal: Signal private container: Object3D private readonly propertiesSignal: Signal @@ -18,10 +20,11 @@ export class SVG extends Object3D { constructor( parent: Parent, src: string | Signal, - properties: SVGProperties, + properties: SVGProperties = {}, defaultProperties?: AllOptionalProperties, ) { super() + this.fontFamiliesSignal = parent.fontFamiliesSignal this.srcSignal = signal(src) this.propertiesSignal = signal(properties) this.defaultPropertiesSignal = signal(defaultProperties) diff --git a/packages/uikit/src/vanilla/text.ts b/packages/uikit/src/vanilla/text.ts index a986d7cd..1dbef765 100644 --- a/packages/uikit/src/vanilla/text.ts +++ b/packages/uikit/src/vanilla/text.ts @@ -1,11 +1,10 @@ import { Object3D } from 'three' -import { createContainer } from '../components/container.js' import { AllOptionalProperties, Properties } from '../properties/default.js' import { Parent } from './index.js' import { EventConfig, bindHandlers } from './utils.js' import { Signal, batch, signal } from '@preact/signals-core' import { TextProperties, createText } from '../components/text.js' -import { FontFamilies, unsubscribeSubscriptions } from '../internals.js' +import { unsubscribeSubscriptions } from '../internals.js' export class Text extends Object3D { private object: Object3D @@ -15,20 +14,17 @@ export class Text extends Object3D { private readonly propertiesSignal: Signal private readonly defaultPropertiesSignal: Signal private readonly textSignal: Signal | Array>> - private readonly fontFamiliesSignal: Signal constructor( parent: Parent, - text: string | Signal | Array>, - fontFamilies: FontFamilies | undefined, - properties: TextProperties, + text: string | Signal | Array> = '', + properties: TextProperties = {}, defaultProperties?: AllOptionalProperties, ) { super() this.propertiesSignal = signal(properties) this.defaultPropertiesSignal = signal(defaultProperties) this.textSignal = signal(text) - this.fontFamiliesSignal = signal(fontFamilies) this.eventConfig = parent.eventConfig //setting up the threejs elements this.object = new Object3D() @@ -41,7 +37,7 @@ export class Text extends Object3D { this.internals = createText( parent.internals, this.textSignal, - this.fontFamiliesSignal, + parent.fontFamiliesSignal, this.propertiesSignal, this.defaultPropertiesSignal, { current: this.object }, @@ -53,10 +49,6 @@ export class Text extends Object3D { bindHandlers(handlers, this, this.eventConfig, subscriptions) } - setFontFamilies(fontFamilies: FontFamilies) { - this.fontFamiliesSignal.value = fontFamilies - } - setText(text: string | Signal | Array>) { this.textSignal.value = text } From 9f5da7e5ddf0e45405dfce9f36574eac24e5fed9 Mon Sep 17 00:00:00 2001 From: Bela Bohlender Date: Thu, 4 Apr 2024 20:05:09 +0200 Subject: [PATCH 15/20] all react examples running again --- examples/auth/src/App.tsx | 4 +- .../auth/src/components/user-auth-form.tsx | 10 +- examples/market/src/components/menu.tsx | 4 +- examples/uikit/src/App.tsx | 12 +- examples/uikit/vite.config.ts | 6 +- examples/vanilla/index.ts | 65 ++--- examples/vanilla/public/example.glb | Bin 0 -> 8328156 bytes packages/kits/apfel/input.tsx | 8 +- packages/kits/default/input.tsx | 8 +- packages/react/src/container.tsx | 4 +- packages/react/src/content.tsx | 42 ++++ packages/react/src/context.tsx | 6 +- packages/react/src/custom.tsx | 6 +- packages/react/src/fullscreen.tsx | 21 +- packages/react/src/icon.tsx | 4 +- packages/react/src/image.tsx | 4 +- packages/react/src/index.ts | 6 +- packages/react/src/input.tsx | 42 +++- packages/react/src/ref.ts | 22 +- packages/react/src/responsive.ts | 6 +- packages/react/src/root.tsx | 4 +- packages/react/src/svg.tsx | 5 +- packages/react/src/text.tsx | 4 +- packages/react/src/utilts.tsx | 56 ++++- packages/uikit/src/active.ts | 2 +- packages/uikit/src/components/container.ts | 3 +- packages/uikit/src/components/content.ts | 233 ++++++++++++++++++ packages/uikit/src/components/custom.ts | 37 ++- packages/uikit/src/components/icon.ts | 75 ++++-- packages/uikit/src/components/image.ts | 28 ++- packages/uikit/src/components/index.ts | 1 + packages/uikit/src/components/input.ts | 107 ++++---- packages/uikit/src/components/root.ts | 10 +- packages/uikit/src/components/svg.ts | 53 ++-- packages/uikit/src/components/text.ts | 6 +- .../src/components/{utils.tsx => utils.ts} | 36 ++- packages/uikit/src/focus.ts | 4 +- packages/uikit/src/hover.ts | 2 +- packages/uikit/src/index.ts | 1 + .../uikit/src/panel/instanced-panel-mesh.ts | 44 ---- packages/uikit/src/properties/alias.ts | 6 +- packages/uikit/src/properties/default.ts | 26 +- packages/uikit/src/properties/immediate.ts | 2 +- packages/uikit/src/scroll.ts | 27 +- .../uikit/src/text/render/instanced-text.ts | 4 +- packages/uikit/src/vanilla/container.ts | 12 +- packages/uikit/src/vanilla/content.ts | 60 +++++ packages/uikit/src/vanilla/custom.ts | 8 +- packages/uikit/src/vanilla/icon.ts | 6 +- packages/uikit/src/vanilla/image.ts | 6 +- packages/uikit/src/vanilla/index.ts | 1 + packages/uikit/src/vanilla/input.ts | 28 ++- packages/uikit/src/vanilla/root.ts | 13 +- packages/uikit/src/vanilla/svg.ts | 12 +- packages/uikit/src/vanilla/text.ts | 12 +- packages/uikit/src/vanilla/utils.ts | 33 ++- pnpm-lock.yaml | 54 ++-- 57 files changed, 865 insertions(+), 436 deletions(-) create mode 100644 examples/vanilla/public/example.glb create mode 100644 packages/react/src/content.tsx rename packages/uikit/src/components/{utils.tsx => utils.ts} (87%) create mode 100644 packages/uikit/src/vanilla/content.ts diff --git a/examples/auth/src/App.tsx b/examples/auth/src/App.tsx index 5778728f..bdc449ca 100644 --- a/examples/auth/src/App.tsx +++ b/examples/auth/src/App.tsx @@ -4,7 +4,7 @@ import { Fullscreen, DefaultProperties, Text, - SvgIconFromText, + Icon, setPreferredColorScheme, canvasInputProps, } from '@react-three/uikit' @@ -75,7 +75,7 @@ function AuthenticationPage() { > - ) { return ( @@ -33,7 +33,7 @@ export function UserAuthForm(props: React.ComponentPropsWithoutRef