diff --git a/examples/card/src/App.tsx b/examples/card/src/App.tsx index dbde2106..3cc37574 100644 --- a/examples/card/src/App.tsx +++ b/examples/card/src/App.tsx @@ -61,6 +61,7 @@ export function CardPage() { ( + // @ts-expect-error internals, parent, propertySignals.style, @@ -55,12 +56,14 @@ export const Container: (props: ContainerProperties & RefAttributes abortController.abort() }, [parent, propertySignals, internals]) + // @ts-expect-error useComponentInternals(ref, parent.root, propertySignals.style, internals, internals.interactionPanel) return ( + {/* @ts-expect-error */} {properties.children} diff --git a/packages/react/src/content.tsx b/packages/react/src/content.tsx index a042d6f8..5d124e3c 100644 --- a/packages/react/src/content.tsx +++ b/packages/react/src/content.tsx @@ -38,6 +38,7 @@ export const Content: (props: ContentProperties & RefAttributes) => } const abortController = new AbortController() setupContent( + // @ts-expect-error internals, parent, propertySignals.style, @@ -49,6 +50,7 @@ export const Content: (props: ContentProperties & RefAttributes) => return () => abortController.abort() }, [internals, parent, propertySignals]) + // @ts-expect-error useComponentInternals(ref, parent.root, propertySignals.style, internals, internals.interactionPanel) return ( diff --git a/packages/react/src/custom.tsx b/packages/react/src/custom.tsx index 3f16112b..1fecec3e 100644 --- a/packages/react/src/custom.tsx +++ b/packages/react/src/custom.tsx @@ -41,6 +41,7 @@ export const CustomContainer: (props: CustomContainerProperties & RefAttributes< } const abortController = new AbortController() setupCustomContainer( + // @ts-expect-error internals, parent, propertySignals.style, @@ -52,6 +53,7 @@ export const CustomContainer: (props: CustomContainerProperties & RefAttributes< return () => abortController.abort() }, [internals, parent, propertySignals]) + // @ts-expect-error useComponentInternals(ref, parent.root, propertySignals.style, internals, innerRef) useEffect(() => { diff --git a/packages/react/src/default.tsx b/packages/react/src/default.tsx index cddf59d9..a3d0b7b3 100644 --- a/packages/react/src/default.tsx +++ b/packages/react/src/default.tsx @@ -1,5 +1,6 @@ import { AllOptionalProperties } from '@pmndrs/uikit' -import { ReactNode, createContext, useContext } from 'react' +import { Signal } from '@preact/signals-core' +import { ReactNode, createContext, useContext, useMemo } from 'react' const DefaultPropertiesContext = createContext(undefined) @@ -9,19 +10,64 @@ export function useDefaultProperties(): AllOptionalProperties | undefined { export type DefaultPropertiesProperties = { children?: ReactNode } & AllOptionalProperties -export function DefaultProperties(properties: DefaultPropertiesProperties) { +const createDefaultPropertiesHash = (properties: AllOptionalProperties | undefined) => { + if (properties === undefined) { + return '' + } + return Array.from(Object.entries(properties)) + .sort(([ka], [kb]) => (ka > kb ? 1 : ka < kb ? -1 : 0)) + .map(([k, v]) => { + switch (typeof v) { + case 'function': + console.warn('no function in default props please', k) + return '' + case 'object': + if (Array.isArray(v)) { + return v.join(',') + } + if (v instanceof Signal) { + return `${k}=${v.peek()}` + } + for (let nk of Object.keys(v)) { + if (typeof v[nk] === 'object') { + console.warn('no deeply nested objects in default props please', k, v) + return '' + } + if (typeof v[nk] === 'function') { + console.warn('no functions nested in objects in default props please', k, v) + return '' + } + } + return `${k}=${JSON.stringify(v)}` + default: + if (v === undefined) return '' + return `${k}=${v}` + } + }) + .join('&') +} + +export function DefaultProperties({ children, ...properties }: DefaultPropertiesProperties) { const existingDefaultProperties = useContext(DefaultPropertiesContext) - const result: any = { ...existingDefaultProperties } - for (const key in properties) { - if (key === 'children') { - continue - } - //TODO: this is not correctly merged but rather overwritten - const value = properties[key as keyof AllOptionalProperties] - if (value == null) { - continue + const existingDefaultPropertiesHash = createDefaultPropertiesHash(existingDefaultProperties) + const propertiesHash = createDefaultPropertiesHash(properties) + + const result: any = useMemo(() => { + const result: any = { ...existingDefaultProperties } + for (const key in properties) { + if (key === 'children') { + continue + } + //TODO: this is not correctly merged but rather overwritten + const value = properties[key as keyof AllOptionalProperties] + if (value == null) { + continue + } + result[key] = value as any } - result[key] = value as any - } - return {properties.children} + return result + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [existingDefaultPropertiesHash, propertiesHash]) + + return {children} } diff --git a/packages/react/src/font.tsx b/packages/react/src/font.tsx index 7134f3dd..175a8713 100644 --- a/packages/react/src/font.tsx +++ b/packages/react/src/font.tsx @@ -41,6 +41,7 @@ export function useMeasureText(fontFamily?: string, fontWeight?: FontWeight) { const fontFamilies = useMemo(() => signal(undefined as any), []) fontFamilies.value = useFontFamilies() const font = useMemo( + // @ts-expect-error () => computedFont(propertiesSignal, fontFamilies, renderer), [fontFamilies, propertiesSignal, renderer], ) diff --git a/packages/react/src/fullscreen.tsx b/packages/react/src/fullscreen.tsx index 0f7b8634..4b4b822d 100644 --- a/packages/react/src/fullscreen.tsx +++ b/packages/react/src/fullscreen.tsx @@ -52,6 +52,7 @@ export const Fullscreen: (props: FullscreenProperties & RefAttributes + {/* @ts-expect-error */} {properties.children} diff --git a/packages/react/src/icon.tsx b/packages/react/src/icon.tsx index ac7adb22..c3ae1e8e 100644 --- a/packages/react/src/icon.tsx +++ b/packages/react/src/icon.tsx @@ -41,6 +41,7 @@ export const Icon: (props: IconProperties & RefAttributes) => ReactNode } const abortController = new AbortController() setupIcon( + // @ts-expect-error internals, parent, propertySignals.style, @@ -51,6 +52,7 @@ export const Icon: (props: IconProperties & RefAttributes) => ReactNode return () => abortController.abort() }, [parent, propertySignals, internals]) + // @ts-expect-error useComponentInternals(ref, parent.root, propertySignals.style, internals, internals.interactionPanel) return ( diff --git a/packages/react/src/image.tsx b/packages/react/src/image.tsx index abb7df4d..4e78dbcc 100644 --- a/packages/react/src/image.tsx +++ b/packages/react/src/image.tsx @@ -38,6 +38,7 @@ export const Image: (props: ImageProperties & RefAttributes) => ReactN } const abortController = new AbortController() setupImage( + // @ts-expect-error internals, parent, propertySignals.style, @@ -49,12 +50,14 @@ export const Image: (props: ImageProperties & RefAttributes) => ReactN return () => abortController.abort() }, [parent, propertySignals, internals]) + // @ts-expect-error useComponentInternals(ref, parent.root, propertySignals.style, internals, internals.interactionPanel) return ( + {/* @ts-expect-error */} {properties.children} diff --git a/packages/react/src/input.tsx b/packages/react/src/input.tsx index 4d1d3db2..4edee6b8 100644 --- a/packages/react/src/input.tsx +++ b/packages/react/src/input.tsx @@ -2,6 +2,7 @@ import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from import { Object3D, Vector2Tuple } from 'three' import { useParent } from './context.js' import { AddHandlers, R3FEventMap, usePropertySignals } from './utils.js' + import { FontFamilies, InputProperties as BaseInputProperties, @@ -58,6 +59,7 @@ export const Input: (props: InputProperties & RefAttributes) => ReactN } const abortController = new AbortController() setupInput( + // @ts-expect-error internals, parent, propertySignals.style, @@ -73,6 +75,7 @@ export const Input: (props: InputProperties & RefAttributes) => ReactN ref, parent.root, propertySignals.style, + // @ts-expect-error internals, internals.interactionPanel, useMemo( diff --git a/packages/react/src/portal.tsx b/packages/react/src/portal.tsx index d800b93a..92313254 100644 --- a/packages/react/src/portal.tsx +++ b/packages/react/src/portal.tsx @@ -175,6 +175,7 @@ export const Portal: (props: PortalProperties & RefAttributes) => Rea usePortalStore, null, )} + {/* @ts-expect-error */} ) diff --git a/packages/react/src/ref.ts b/packages/react/src/ref.ts index 03eed783..b0e18988 100644 --- a/packages/react/src/ref.ts +++ b/packages/react/src/ref.ts @@ -13,6 +13,7 @@ import { createInputState, createContentState, RootContext, + ReadonlyDeepSignalObject, } from '@pmndrs/uikit/internals' import { ForwardedRef, RefObject, useImperativeHandle } from 'react' import { Vector2Tuple, Mesh, Matrix4 } from 'three' @@ -84,7 +85,7 @@ export type ComponentInternals = { getComputedProperty(key: K): T[K] | undefined } -export function useComponentInternals( +export function useComponentInternals( ref: ForwardedRef & O>, root: RootContext, styleSignal: Signal, @@ -101,7 +102,7 @@ export function useComponentInternals( > & { isClipped?: Signal scrollPosition?: Signal - mergedProperties: Signal + mergedProperties: ReadonlyDeepSignalObject }, interactionPanel: Mesh | RefObject, additional?: O, @@ -111,11 +112,10 @@ export function useComponentInternals( internals return { isVisible: internals.isVisible, - setStyle: (style: T | undefined, replace?: boolean) => - (styleSignal.value = replace ? style : ({ ...styleSignal.value, ...style } as T)), + setStyle: (style, replace) => (styleSignal.value = replace ? style : ({ ...styleSignal.value, ...style } as T)), getStyle: () => styleSignal.peek(), - getComputedProperty: (key: K) => - untracked(() => internals.mergedProperties.value.read(key as string, undefined)), + // @ts-expect-error + getComputedProperty: (key) => internals.mergedProperties['$' + key]!.peek(), pixelSize: root.pixelSize, root, borderInset, diff --git a/packages/react/src/root.tsx b/packages/react/src/root.tsx index cbcd4d5f..e805ca6e 100644 --- a/packages/react/src/root.tsx +++ b/packages/react/src/root.tsx @@ -69,6 +69,7 @@ export const Root: (props: RootProperties & RefAttributes) => ReactNode } const abortController = new AbortController() setupRoot( + // @ts-expect-error internals, propertySignals.style, propertySignals.properties, @@ -88,12 +89,14 @@ export const Root: (props: RootProperties & RefAttributes) => ReactNode whileOnFrameRef.current = false }) + // @ts-expect-error useComponentInternals(ref, internals.root, propertySignals.style, internals, internals.interactionPanel) return ( + {/* @ts-expect-error */} {properties.children} diff --git a/packages/react/src/svg.tsx b/packages/react/src/svg.tsx index f3e5aab0..432209a9 100644 --- a/packages/react/src/svg.tsx +++ b/packages/react/src/svg.tsx @@ -38,6 +38,7 @@ export const Svg: (props: SvgProperties & RefAttributes) => ReactNode = } const abortController = new AbortController() setupSvg( + // @ts-expect-error internals, parent, propertySignals.style, @@ -49,6 +50,7 @@ export const Svg: (props: SvgProperties & RefAttributes) => ReactNode = return () => abortController.abort() }, [parent, propertySignals, internals]) + // @ts-expect-error useComponentInternals(ref, parent.root, propertySignals.style, internals, internals.interactionPanel) return ( @@ -56,6 +58,7 @@ export const Svg: (props: SvgProperties & RefAttributes) => ReactNode = + {/* @ts-expect-error */} {properties.children} diff --git a/packages/react/src/text.tsx b/packages/react/src/text.tsx index 0f093f9f..7ed95eac 100644 --- a/packages/react/src/text.tsx +++ b/packages/react/src/text.tsx @@ -46,6 +46,7 @@ export const Text: (props: TextProperties & RefAttributes) => ReactNode } const abortController = new AbortController() setupText( + // @ts-expect-error internals, parent, propertySignals.style, @@ -56,6 +57,7 @@ export const Text: (props: TextProperties & RefAttributes) => ReactNode return () => abortController.abort() }, [parent, propertySignals, internals]) + // @ts-expect-error useComponentInternals(ref, parent.root, propertySignals.style, internals, internals.interactionPanel) return ( diff --git a/packages/react/src/utils.tsx b/packages/react/src/utils.tsx index c26254c8..a5265b53 100644 --- a/packages/react/src/utils.tsx +++ b/packages/react/src/utils.tsx @@ -4,6 +4,7 @@ import { ReactNode, forwardRef, useEffect, useMemo, useState } from 'react' import { Object3D } from 'three' import { useDefaultProperties } from './default.js' import { AllOptionalProperties } from '@pmndrs/uikit/internals' +import { DeepSignal, deepSignal } from 'deepsignal/core' export type R3FEventMap = { mouse: ThreeEvent @@ -35,16 +36,22 @@ export const AddHandlers = forwardRef< ) }) -export function usePropertySignals(properties: T) { +export function usePropertySignals( + properties: T, +): { + style: Signal + properties: DeepSignal + default: DeepSignal +} { const propertySignals = useMemo( () => ({ style: signal(undefined), - properties: signal(undefined as any), - default: signal(undefined), + properties: deepSignal({} as T), + default: deepSignal({}), }), [], ) - propertySignals.properties.value = properties - propertySignals.default.value = useDefaultProperties() + Object.assign(propertySignals.properties, properties) + Object.assign(propertySignals.default, useDefaultProperties()) return propertySignals } diff --git a/packages/react/src/video.tsx b/packages/react/src/video.tsx index bdf2b8a8..fd7401e4 100644 --- a/packages/react/src/video.tsx +++ b/packages/react/src/video.tsx @@ -93,6 +93,7 @@ export const Video: (props: VideoProperties & RefAttributes) => ReactN return ( + {/* @ts-expect-error */} ) diff --git a/packages/uikit/package.json b/packages/uikit/package.json index 173d1769..1a1cfbb6 100644 --- a/packages/uikit/package.json +++ b/packages/uikit/package.json @@ -39,11 +39,12 @@ "three": ">=0.160" }, "dependencies": { + "@pmndrs/msdfonts": "workspace:^", "@preact/signals-core": "^1.5.1", + "deepsignal": "^1.6.0", "inline-style-parser": "^0.2.3", "node-html-parser": "^6.1.13", "tw-to-css": "^0.0.12", - "yoga-layout": "^3.2.1", - "@pmndrs/msdfonts": "workspace:^" + "yoga-layout": "^3.2.1" } } diff --git a/packages/uikit/src/active.ts b/packages/uikit/src/active.ts index 5c38892d..99c63be1 100644 --- a/packages/uikit/src/active.ts +++ b/packages/uikit/src/active.ts @@ -1,8 +1,9 @@ -import { Signal } from '@preact/signals-core' +import { computed, Signal } from '@preact/signals-core' import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './properties/default.js' import { createConditionalPropertyTranslator } from './utils.js' import { EventHandlers, ThreePointerEvent } from './events.js' import { addHandler } from './components/index.js' +import { DeepSignal } from 'deepsignal/core' export type WithActive = T & { active?: T @@ -14,13 +15,13 @@ export type ActiveEventHandlers = Pick> & EventHandlers) | undefined, - properties: (WithClasses> & EventHandlers) | undefined, - defaultProperties: AllOptionalProperties | undefined, + properties: DeepSignal> & EventHandlers>, + defaultProperties: DeepSignal, activeSignal: Signal>, ): void { let activePropertiesExist = false - traverseProperties(style, defaultProperties, properties, (p) => { + traverseProperties(style, properties, defaultProperties, (p) => { if ('active' in p) { activePropertiesExist = true } @@ -32,16 +33,18 @@ export function addActiveHandlers( return } const onLeave = ({ pointerId }: ThreePointerEvent) => { - activeSignal.value = activeSignal.value.filter((id) => id != pointerId) - if (activeSignal.value.length > 0) { + const newValue = activeSignal.value.filter((id) => id != pointerId) + activeSignal.value = newValue + if (newValue.length > 0) { return } properties?.onActiveChange?.(false) style?.onActiveChange?.(false) } addHandler('onPointerDown', target, ({ pointerId }) => { - activeSignal.value = [pointerId, ...activeSignal.value] - if (activeSignal.value.length != 1) { + const newValue = [pointerId, ...activeSignal.peek()] + activeSignal.value = newValue + if (newValue.length != 1) { return } properties?.onActiveChange?.(true) @@ -55,3 +58,12 @@ export function createActivePropertyTransfomers(activeSignal: Signal activeSignal.value.length > 0), } } + +export const createActiveStuff = + (activeSignal: Signal>) => (properties: WithActive, key: string) => { + return computed(() => { + if (activeSignal.value.length > 0 && 'active' in properties) { + return properties.active?.[key] + } + }) + } diff --git a/packages/uikit/src/caret.ts b/packages/uikit/src/caret.ts index bcf817f8..ae8baf7c 100644 --- a/packages/uikit/src/caret.ts +++ b/packages/uikit/src/caret.ts @@ -11,6 +11,7 @@ import { createPanelMaterialConfig, } from './panel/index.js' import { MergedProperties, computedInheritableProperty } from './properties/index.js' +import { ReadonlyDeepSignalObject } from './internals.js' export type CaretTransformation = { position: Vector2Tuple @@ -43,10 +44,21 @@ export type CaretProperties = { [Key in Exclude< keyof PanelProperties, 'backgroundColor' | 'backgroundOpacity' - > as `caret${Capitalize}`]: PanelProperties[Key] + > as `caret${Capitalize}`]?: PanelProperties[Key] } -let caretMaterialConfig: PanelMaterialConfig | undefined +type CaretMaterialPropKeys = + | 'caretColor' + | 'caretOpacity' + | 'caretBorderBend' + | 'caretBorderBottomLeftRadius' + | 'caretBorderBottomRightRadius' + | 'caretBorderColor' + | 'caretBorderOpacity' + | 'caretBorderTopLeftRadius' + | 'caretBorderTopRightRadius' + +let caretMaterialConfig: PanelMaterialConfig | undefined function getCaretMaterialConfig() { caretMaterialConfig ??= createPanelMaterialConfig( { @@ -69,7 +81,7 @@ function getCaretMaterialConfig() { } export function createCaret( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject, matrix: Signal, caretTransformation: Signal, isVisible: Signal, @@ -94,7 +106,12 @@ export function createCaret( return () => clearInterval(ref) }, abortSignal) const borderInset = computedBorderInset(propertiesSignal, caretBorderKeys) - const caretWidth = computedInheritableProperty(propertiesSignal, 'caretWidth', 1.5) + // @ts-expect-error + const caretWidth = computedInheritableProperty<'caretWidth', number>( + propertiesSignal, + 'caretWidth', + 1.5, + ) as ReadonlySignal setupInstancedPanel( propertiesSignal, diff --git a/packages/uikit/src/components/container.ts b/packages/uikit/src/components/container.ts index c781ebeb..ee996d48 100644 --- a/packages/uikit/src/components/container.ts +++ b/packages/uikit/src/components/container.ts @@ -17,8 +17,8 @@ import { TransformProperties, setupObjectTransform, computedTransformMatrix } fr 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, signal } from '@preact/signals-core' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' +import { computed, Signal, signal } from '@preact/signals-core' import { VisibilityProperties, WithConditionals, @@ -33,7 +33,7 @@ import { Listeners, setupLayoutListeners, setupClippedListeners } from '../liste import { ParentContext } from '../context.js' import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/instanced-panel-group.js' import { createInteractionPanel, setupInteractionPanel } from '../panel/instanced-panel-mesh.js' -import { darkPropertyTransformers } from '../dark.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { getDefaultPanelMaterialConfig, PointerEventsProperties } from '../panel/index.js' import { computedAncestorsHaveListeners, @@ -44,23 +44,25 @@ import { EventHandlers, ThreeEventMap, Properties, + mergeProps, } from '../internals.js' import { Object3D } from 'three' +import { DeepSignal } from 'deepsignal/core' export type InheritableContainerProperties = WithClasses< WithConditionals< WithAllAliases< - WithReactive< - YogaProperties & - PanelProperties & - ZIndexProperties & - TransformProperties & - ScrollbarProperties & - PanelGroupProperties & - VisibilityProperties & - UpdateMatrixWorldProperties & - PointerEventsProperties - > + // WithReactive< + YogaProperties & + PanelProperties & + ZIndexProperties & + TransformProperties & + ScrollbarProperties & + PanelGroupProperties & + VisibilityProperties & + UpdateMatrixWorldProperties & + PointerEventsProperties + // > > > > @@ -73,20 +75,27 @@ export function createContainerState( parentCtx: ParentContext, objectRef: { current?: Object3D | null }, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, ) { const flexState = createFlexNodeState() const hoveredList = signal>([]) const pressedList = signal>([]) - //properties - const mergedProperties = computedMergedProperties(style, properties, defaultProperties, { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentCtx.root.size), - ...createHoverPropertyTransformers(hoveredList), - ...createActivePropertyTransfomers(pressedList), - }) + // have to pass the type explicitly to avoid TS error + const mergedProperties = mergeProps>(properties, defaultProperties, [ + [0, darkStuff], + [10, darkStuff], + [20, darkStuff], + [30, createActiveStuff(pressedList)], + ]) + // const mergedProperties = computedMergedProperties(style, properties, defaultProperties, { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(parentCtx.root.size), + // ...createHoverPropertyTransformers(hoveredList), + // ...createActivePropertyTransfomers(pressedList), + // }) + // //transform const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize) @@ -100,7 +109,7 @@ export function createContainerState( //instanced panel const groupDeps = computedPanelGroupDependencies(mergedProperties) const scrollPosition = createScrollPosition() - const scrollbarWidth = computedInheritableProperty(mergedProperties, 'scrollbarWidth', 10) + const scrollbarWidth = computed(() => mergedProperties.scrollbarWidth ?? 10) const orderInfo = computedOrderInfo( mergedProperties, @@ -149,7 +158,7 @@ export function setupContainer( state: ReturnType, parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, + properties: DeepSignal>, object: Object3D, childrenContainer: Object3D, abortSignal: AbortSignal, @@ -200,7 +209,7 @@ export function setupContainer( abortSignal, ) - const updateMatrixWorld = computedInheritableProperty(state.mergedProperties, 'updateMatrixWorld', false) + const updateMatrixWorld = computed(() => state.mergedProperties.$updateMatrixWorld!.value ?? false) setupMatrixWorldUpdate(updateMatrixWorld, false, object, state.root, state.globalMatrix, false, abortSignal) setupMatrixWorldUpdate( updateMatrixWorld, diff --git a/packages/uikit/src/components/content.ts b/packages/uikit/src/components/content.ts index d7249ecd..5d71123d 100644 --- a/packages/uikit/src/components/content.ts +++ b/packages/uikit/src/components/content.ts @@ -1,15 +1,16 @@ import { FlexNodeState, YogaProperties, createFlexNodeState } from '../flex/node.js' -import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createHoveredStuff, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { computedIsClipped, createGlobalClippingPlanes, ClippingRect } from '../clipping.js' import { ScrollbarProperties } from '../scroll.js' import { WithAllAliases } from '../properties/alias.js' import { PanelProperties, setupInstancedPanel } from '../panel/instanced-panel.js' import { TransformProperties, setupObjectTransform, computedTransformMatrix } from '../transform.js' import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' -import { createResponsivePropertyTransformers } from '../responsive.js' +import { createResponsivePropertyStuff, createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, OrderInfo, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' -import { createActivePropertyTransfomers } from '../active.js' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' import { Signal, computed, signal, untracked } from '@preact/signals-core' +import { DeepSignal } from 'deepsignal/core' import { VisibilityProperties, WithConditionals, @@ -22,6 +23,8 @@ import { setupMatrixWorldUpdate, setupPointerEvents, computedAncestorsHaveListeners, + ReadonlyDeepSignalObject, + mergeProps, } from './utils.js' import { abortableEffect, alignmentZMap } from '../utils.js' import { Listeners, setupLayoutListeners, setupClippedListeners } from '../listeners.js' @@ -33,9 +36,9 @@ import { } from '../panel/instanced-panel-group.js' import { createInteractionPanel, setupInteractionPanel } from '../panel/instanced-panel-mesh.js' import { Box3, Material, Mesh, Object3D, Vector3 } from 'three' -import { darkPropertyTransformers } from '../dark.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { getDefaultPanelMaterialConfig, makeClippedCast, PointerEventsProperties } from '../panel/index.js' -import { MergedProperties, computedInheritableProperty } from '../properties/index.js' +import { computedInheritableProperty } from '../properties/index.js' import { KeepAspectRatioProperties } from './image.js' import { EventHandlers, ThreeEventMap } from '../events.js' @@ -70,8 +73,8 @@ export type ContentProperties = Inheri export function createContentState( parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, contentContainerRef: { current?: Object3D | null }, ) { const flexState = createFlexNodeState() @@ -82,19 +85,26 @@ export function createContentState( const aspectRatio = computed(() => sizeSignal.value.x / sizeSignal.value.y) //properties - const mergedProperties = computedMergedProperties( - style, - properties, - defaultProperties, - { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentCtx.root.size), - ...createHoverPropertyTransformers(hoveredList), - ...createActivePropertyTransfomers(pressedList), - }, - keepAspectRatioPropertyTransformer, - (m) => m.add('aspectRatio', aspectRatio), - ) + // const omergedProperties = computedMergedProperties( + // style, + // properties, + // defaultProperties, + // { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(parentCtx.root.size), + // ...createHoverPropertyTransformers(hoveredList), + // ...createActivePropertyTransfomers(pressedList), + // }, + // keepAspectRatioPropertyTransformer, + // (m) => m.add('aspectRatio', aspectRatio), + // ) + + const mergedProperties = mergeProps>(properties, defaultProperties, [ + [0, darkStuff], + [10, createResponsivePropertyStuff(parentCtx.root.size)], + [20, createHoveredStuff(hoveredList)], + [30, createActiveStuff(hoveredList)], + ]) const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize) @@ -163,7 +173,7 @@ export function setupContent( state: ReturnType, parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, + properties: ReadonlyDeepSignalObject>, object: Object3D, contentContainer: Object3D, abortSignal: AbortSignal, @@ -220,7 +230,7 @@ function setupContentContainer( measureContent: () => void, measuredSize: Vector3, measuredCenter: Vector3, - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject, root: RootContext, flexState: FlexNodeState, isVisible: Signal, @@ -231,13 +241,12 @@ function setupContentContainer( const depthAlign = computedInheritableProperty(propertiesSignal, 'depthAlign', defaultDepthAlign) const keepAspectRatio = computedInheritableProperty(propertiesSignal, 'keepAspectRatio', true) abortableEffect(() => { - const properties = propertiesSignal.value updateRenderProperties( { current: contentContainer }, isVisible.value, - properties.read('renderOrder', 0), - properties.read('depthTest', true), - properties.read('depthWrite', false), + propertiesSignal.renderOrder ?? 0, + propertiesSignal.depthTest ?? true, + propertiesSignal.depthWrite ?? false, ) root.requestRender() }, abortSignal) @@ -294,7 +303,7 @@ function createMeasureContent( flexState: FlexNodeState, measuredSize: Vector3, measuredCenter: Vector3, - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject, root: RootContext, parentClippingRect: Signal, isVisible: Signal, @@ -337,13 +346,12 @@ function createMeasureContent( root.requestRender() } return () => { - const properties = propertiesSignal.peek() updateRenderProperties( contentContainerRef, isVisible.peek(), - untracked(() => properties.read('renderOrder', 0)), - untracked(() => properties.read('depthTest', true)), - untracked(() => properties.read('depthWrite', false)), + untracked(() => propertiesSignal.renderOrder ?? 0), + untracked(() => propertiesSignal.depthTest ?? true), + untracked(() => propertiesSignal.depthWrite ?? false), ) measureContent() } diff --git a/packages/uikit/src/components/custom.ts b/packages/uikit/src/components/custom.ts index fbf3d7cc..8cc2c720 100644 --- a/packages/uikit/src/components/custom.ts +++ b/packages/uikit/src/components/custom.ts @@ -1,14 +1,14 @@ import { YogaProperties, createFlexNodeState } from '../flex/node.js' -import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createHoveredStuff, 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, setupObjectTransform, computedTransformMatrix } from '../transform.js' import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' -import { createResponsivePropertyTransformers } from '../responsive.js' +import { createResponsivePropertyStuff, createResponsivePropertyTransformers } from '../responsive.js' import { ElementType, ZIndexProperties, computedOrderInfo, setupRenderOrder } from '../order.js' -import { createActivePropertyTransfomers } from '../active.js' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' import { Signal, signal } from '@preact/signals-core' import { VisibilityProperties, @@ -21,14 +21,16 @@ import { setupMatrixWorldUpdate, setupPointerEvents, computedAncestorsHaveListeners, + mergeProps, } from './utils.js' import { Listeners, setupLayoutListeners, setupClippedListeners } from '../listeners.js' import { ParentContext } from '../context.js' import { FrontSide, Material, Mesh, Object3D } from 'three' -import { darkPropertyTransformers } from '../dark.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { PointerEventsProperties, RenderProperties, ShadowProperties, makeClippedCast } from '../panel/index.js' import { EventHandlers, ThreeEventMap } from '../events.js' import { abortableEffect } from '../utils.js' +import { DeepSignal } from 'deepsignal/core' export type InheritableCustomContainerProperties = WithClasses< WithConditionals< @@ -55,20 +57,26 @@ export type CustomContainerProperties export function createCustomContainerState( parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, ) { const flexState = createFlexNodeState() const hoveredSignal = signal>([]) const activeSignal = signal>([]) //properties - const mergedProperties = computedMergedProperties(style, properties, defaultProperties, { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentCtx.root.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - }) + // const mergedProperties = computedMergedProperties(style, properties, defaultProperties, { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(parentCtx.root.size), + // ...createHoverPropertyTransformers(hoveredSignal), + // ...createActivePropertyTransfomers(activeSignal), + // }) + const mergedProperties = mergeProps>(properties, defaultProperties, [ + [0, darkStuff], + [10, createResponsivePropertyStuff(parentCtx.root.size)], + [20, createHoveredStuff(hoveredSignal)], + [30, createActiveStuff(activeSignal)], + ]) const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize) const globalMatrix = computedGlobalMatrix(parentCtx.childrenMatrix, transformMatrix) @@ -106,7 +114,7 @@ export function setupCustomContainer( state: ReturnType, parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, + properties: DeepSignal>, object: Object3D, mesh: Mesh, abortSignal: AbortSignal, @@ -129,11 +137,11 @@ export function setupCustomContainer( material.needsUpdate = true material.shadowSide = FrontSide abortableEffect(() => { - material.depthTest = state.mergedProperties.value.read('depthTest', true) + material.depthTest = state.mergedProperties.depthTest ?? true parentCtx.root.requestRender() }, abortSignal) abortableEffect(() => { - material.depthWrite = state.mergedProperties.value.read('depthWrite', false) + material.depthWrite = state.mergedProperties.depthWrite ?? false parentCtx.root.requestRender() }, abortSignal) } @@ -149,15 +157,15 @@ export function setupCustomContainer( setupRenderOrder(mesh, parentCtx.root, state.orderInfo) abortableEffect(() => { - mesh.renderOrder = state.mergedProperties.value.read('renderOrder', 0) + mesh.renderOrder = state.mergedProperties.renderOrder ?? 0 parentCtx.root.requestRender() }, abortSignal) abortableEffect(() => { - mesh.receiveShadow = state.mergedProperties.value.read('receiveShadow', false) + mesh.receiveShadow = state.mergedProperties.receiveShadow ?? false parentCtx.root.requestRender() }, abortSignal) abortableEffect(() => { - mesh.castShadow = state.mergedProperties.value.read('castShadow', false) + mesh.castShadow = state.mergedProperties.castShadow ?? false parentCtx.root.requestRender() }, abortSignal) abortableEffect(() => { diff --git a/packages/uikit/src/components/icon.ts b/packages/uikit/src/components/icon.ts index 5e0f3f9b..85b17bd5 100644 --- a/packages/uikit/src/components/icon.ts +++ b/packages/uikit/src/components/icon.ts @@ -22,21 +22,24 @@ import { setupMatrixWorldUpdate, setupPointerEvents, computedAncestorsHaveListeners, + mergeProps, + ReadonlyDeepSignalObject, } from './utils.js' import { abortableEffect, fitNormalizedContentInside } from '../utils.js' import { makeClippedCast, PointerEventsProperties } from '../panel/interaction-panel-mesh.js' import { computedIsClipped, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupClippedListeners } from '../listeners.js' -import { createActivePropertyTransfomers } from '../active.js' -import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' +import { createHoveredStuff, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { createInteractionPanel, setupInteractionPanel } from '../panel/instanced-panel-mesh.js' -import { createResponsivePropertyTransformers } from '../responsive.js' +import { createResponsivePropertyStuff, createResponsivePropertyTransformers } from '../responsive.js' import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js' import { AppearanceProperties } from './svg.js' import { PanelGroupProperties, computedPanelGroupDependencies, getDefaultPanelMaterialConfig } from '../panel/index.js' -import { darkPropertyTransformers } from '../dark.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { MergedProperties } from '../properties/index.js' import { ThreeEventMap } from '../events.js' +import { DeepSignal } from 'deepsignal/core' export type InheritableIconProperties = WithClasses< WithConditionals< @@ -66,30 +69,37 @@ export function createIconState( svgWidth: number, svgHeight: number, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, ) { const flexState = createFlexNodeState() const hoveredSignal = signal>([]) const activeSignal = signal>([]) - const mergedProperties = computedMergedProperties( - style, - properties, - defaultProperties, - { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentCtx.root.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - }, - keepAspectRatioPropertyTransformer, - (m) => { - m.add('aspectRatio', svgWidth / svgHeight) - m.add('width', svgWidth) - m.add('height', svgHeight) - }, - ) + // const mergedProperties = computedMergedProperties( + // style, + // properties, + // defaultProperties, + // { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(parentCtx.root.size), + // ...createHoverPropertyTransformers(hoveredSignal), + // ...createActivePropertyTransfomers(activeSignal), + // }, + // keepAspectRatioPropertyTransformer, + // (m) => { + // m.add('aspectRatio', svgWidth / svgHeight) + // m.add('width', svgWidth) + // m.add('height', svgHeight) + // }, + // ) + + const mergedProperties = mergeProps>(properties, defaultProperties, [ + [0, darkStuff], + [10, createResponsivePropertyStuff(parentCtx.root.size)], + [20, createHoveredStuff(hoveredSignal)], + [30, createActiveStuff(activeSignal)], + ]) const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize) const globalMatrix = computedGlobalMatrix(parentCtx.childrenMatrix, transformMatrix) @@ -144,7 +154,7 @@ export function setupIcon( state: ReturnType, parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, + properties: DeepSignal>, object: Object3D, abortSignal: AbortSignal, ) { @@ -235,7 +245,7 @@ function createIconGroup( function setupIconGroup( group: Group, - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject, svgWidth: number, svgHeight: number, parentContext: ParentContext, diff --git a/packages/uikit/src/components/image.ts b/packages/uikit/src/components/image.ts index 0fa75990..c7b34998 100644 --- a/packages/uikit/src/components/image.ts +++ b/packages/uikit/src/components/image.ts @@ -27,6 +27,7 @@ import { PanelProperties, PanelMaterialConfig, computedPanelGroupDependencies, + MaterialClass, } from '../panel/index.js' import { WithAllAliases } from '../properties/alias.js' import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' @@ -56,6 +57,8 @@ import { setupMatrixWorldUpdate, setupPointerEvents, computedAncestorsHaveListeners, + mergeProps, + ReadonlyDeepSignalObject, } from './utils.js' import { MergedProperties } from '../properties/merged.js' import { abortableEffect, readReactive } from '../utils.js' @@ -70,12 +73,13 @@ import { import { computedClippingRect, computedIsClipped, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupClippedListeners } from '../listeners.js' import { computedInheritableProperty } from '../properties/utils.js' -import { createActivePropertyTransfomers } from '../active.js' -import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' -import { createResponsivePropertyTransformers } from '../responsive.js' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' +import { createHoveredStuff, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createResponsivePropertyStuff, createResponsivePropertyTransformers } from '../responsive.js' import { AppearanceProperties } from './svg.js' -import { darkPropertyTransformers } from '../dark.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { ThreeEventMap } from '../events.js' +import { DeepSignal } from 'deepsignal/core' export type ImageFit = 'cover' | 'fill' const defaultImageFit: ImageFit = 'fill' @@ -118,15 +122,15 @@ export function createImageState( parentCtx: ParentContext, objectRef: { current?: Object3D | null }, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, ) { const flexState = createFlexNodeState() const texture = signal(undefined) const hoveredSignal = signal>([]) const activeSignal = signal>([]) - const src = computed(() => readReactive(style.value?.src) ?? readReactive(properties.value?.src)) + const src = computed(() => readReactive(style.value?.src) ?? readReactive(properties.src)) const textureAspectRatio = computed(() => { const tex = texture.value @@ -137,19 +141,25 @@ export function createImageState( return image.width / image.height }) - const mergedProperties = computedMergedProperties( - style, - properties, - defaultProperties, - { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentCtx.root.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - }, - keepAspectRatioPropertyTransformer, - (m) => m.add('aspectRatio', textureAspectRatio), - ) + // const mergedProperties = computedMergedProperties( + // style, + // properties, + // defaultProperties, + // { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(parentCtx.root.size), + // ...createHoverPropertyTransformers(hoveredSignal), + // ...createActivePropertyTransfomers(activeSignal), + // }, + // keepAspectRatioPropertyTransformer, + // (m) => m.add('aspectRatio', textureAspectRatio), + // ) + const mergedProperties = mergeProps>(properties, defaultProperties, [ + [0, darkStuff], + [10, createResponsivePropertyStuff(parentCtx.root.size)], + [20, createHoveredStuff(hoveredSignal)], + [30, createActiveStuff(activeSignal)], + ]) const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize) const globalMatrix = computedGlobalMatrix(parentCtx.childrenMatrix, transformMatrix) @@ -209,7 +219,7 @@ export function setupImage( state: ReturnType, parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, + properties: DeepSignal>, object: Object3D, childrenContainer: Object3D, abortSignal: AbortSignal, @@ -266,7 +276,17 @@ export function setupImage( ) } -let imageMaterialConfig: PanelMaterialConfig | undefined +type ImagePanelMaterialConfigPropKeys = + | 'borderBend' + | 'borderBottomLeftRadius' + | 'borderBottomRightRadius' + | 'borderColor' + | 'borderOpacity' + | 'borderTopLeftRadius' + | 'borderTopRightRadius' + | 'opacity' + +let imageMaterialConfig: PanelMaterialConfig | undefined function getImageMaterialConfig() { imageMaterialConfig ??= createPanelMaterialConfig( { @@ -321,7 +341,7 @@ function createImageMesh( function setupImageMesh( mesh: Mesh & { boundingSphere: Sphere }, - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject<{}>, textureSignal: Signal, globalMatrix: Signal, parentContext: ParentContext, @@ -446,7 +466,14 @@ async function loadTextureImpl(src?: string | Texture): Promise<(Texture & { dis } function setupImageMaterials( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject<{ + panelMaterialClass?: MaterialClass + depthTest?: boolean + depthWrite?: boolean + renderOrder?: number + castShadow?: boolean + receiveShadow?: boolean + }>, textureSignal: Signal, target: Mesh, size: Signal, @@ -464,15 +491,15 @@ function setupImageMaterials( target.customDistanceMaterial.clippingPlanes = clippingPlanes abortableEffect(() => { - const material = createPanelMaterial(propertiesSignal.value.read('panelMaterialClass', MeshBasicMaterial), info) + const material = createPanelMaterial(propertiesSignal.panelMaterialClass ?? MeshBasicMaterial, info) material.clippingPlanes = clippingPlanes target.material = material const cleanupDepthTestEffect = effect(() => { - material.depthTest = propertiesSignal.value.read('depthTest', true) + material.depthTest = propertiesSignal.depthTest ?? true root.requestRender() }) const cleanupDepthWriteEffect = effect(() => { - material.depthWrite = propertiesSignal.value.read('depthWrite', false) + material.depthWrite = propertiesSignal.depthWrite ?? false root.requestRender() }) const cleanupTextureEffect = effect(() => { @@ -488,15 +515,15 @@ function setupImageMaterials( } }, abortSignal) abortableEffect(() => { - target.renderOrder = propertiesSignal.value.read('renderOrder', 0) + target.renderOrder = propertiesSignal.renderOrder ?? 0 root.requestRender() }, abortSignal) abortableEffect(() => { - target.castShadow = propertiesSignal.value.read('castShadow', false) + target.castShadow = propertiesSignal.castShadow ?? false root.requestRender() }, abortSignal) abortableEffect(() => { - target.receiveShadow = propertiesSignal.value.read('receiveShadow', false) + target.receiveShadow = propertiesSignal.receiveShadow ?? false root.requestRender() }, abortSignal) @@ -519,13 +546,16 @@ function setupImageMaterials( }, abortSignal) const setters = imageMaterialConfig.setters setupImmediateProperties( + // @ts-expect-error propertiesSignal, isVisible, imageMaterialConfig.hasProperty, (key, value) => { + // @ts-expect-error setters[key](data, 0, value as any, size, undefined) root.requestRender() }, abortSignal, + Object.keys(setters) as unknown as keyof typeof setters, ) } diff --git a/packages/uikit/src/components/input.ts b/packages/uikit/src/components/input.ts index 998c3914..d7392a06 100644 --- a/packages/uikit/src/components/input.ts +++ b/packages/uikit/src/components/input.ts @@ -1,6 +1,7 @@ import { FlexNodeState, YogaProperties, createFlexNodeState } from '../flex/index.js' -import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createHoveredStuff, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { computedIsClipped } from '../clipping.js' +import { DeepSignal } from 'deepsignal/core' import { ScrollbarProperties } from '../scroll.js' import { WithAllAliases } from '../properties/alias.js' import { PanelProperties, setupInstancedPanel } from '../panel/instanced-panel.js' @@ -13,9 +14,9 @@ import { computedNonInheritableProperty, traverseProperties, } from '../properties/index.js' -import { createResponsivePropertyTransformers } from '../responsive.js' +import { createResponsivePropertyStuff, createResponsivePropertyTransformers } from '../responsive.js' import { computedOrderInfo, ElementType, ZIndexProperties } from '../order.js' -import { createActivePropertyTransfomers } from '../active.js' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' import { ReadonlySignal, Signal, computed, effect, signal } from '@preact/signals-core' import { UpdateMatrixWorldProperties, @@ -29,6 +30,8 @@ import { setupMatrixWorldUpdate, setupPointerEvents, computedAncestorsHaveListeners, + mergeProps, + ReadonlyDeepSignalObject, } from './utils.js' import { abortableEffect, readReactive } from '../utils.js' import { Listeners, setupLayoutListeners, setupClippedListeners } from '../listeners.js' @@ -48,7 +51,7 @@ import { computedGylphGroupDependencies, createInstancedText, } from '../text/index.js' -import { darkPropertyTransformers } from '../dark.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { getDefaultPanelMaterialConfig, PointerEventsProperties } from '../panel/index.js' export type InheritableInputProperties = WithClasses< @@ -122,33 +125,40 @@ export function createInputState( parentCtx: ParentContext, fontFamilies: Signal, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, ) { const flexState = createFlexNodeState() const hoveredSignal = signal>([]) const activeSignal = signal>([]) const hasFocusSignal = signal(false) - const mergedProperties = computedMergedProperties( - style, - properties, - defaultProperties, - { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentCtx.root.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - ...createFocusPropertyTransformers(hasFocusSignal), - }, - undefined, - (m) => { - traverseProperties(style.value, properties.value, defaultProperties.value, (p) => { - m.add('caretOpacity', p.opacity) - m.add('caretColor', p.color) - }) - }, - ) + // const mergedProperties = computedMergedProperties( + // style, + // properties, + // defaultProperties, + // { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(parentCtx.root.size), + // ...createHoverPropertyTransformers(hoveredSignal), + // ...createActivePropertyTransfomers(activeSignal), + // ...createFocusPropertyTransformers(hasFocusSignal), + // }, + // undefined, + // (m) => { + // // @ts-expect-error + // traverseProperties(style.value, properties, defaultProperties.value, (p) => { + // m.add('caretOpacity', p.opacity) + // m.add('caretColor', p.color) + // }) + // }, + // ) + const mergedProperties = mergeProps>(properties, defaultProperties, [ + [0, darkStuff], + [10, createResponsivePropertyStuff(parentCtx.root.size)], + [20, createHoveredStuff(hoveredSignal)], + [30, createActiveStuff(activeSignal)], + ]) const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize) const globalMatrix = computedGlobalMatrix(parentCtx.childrenMatrix, transformMatrix) @@ -178,12 +188,12 @@ export function createInputState( backgroundOrderInfo, ) - const defaultValue = style.peek()?.defaultValue ?? properties.peek()?.defaultValue + const defaultValue = style.peek()?.defaultValue ?? properties?.$defaultValue?.peek() const writeValue = - style.peek()?.value == null && properties.peek()?.value == null ? signal(defaultValue ?? '') : undefined + style.peek()?.value == null && properties.$value?.peek() == null ? signal(defaultValue ?? '') : undefined const valueSignal = computed( - () => writeValue?.value ?? readReactive(style.value?.value) ?? readReactive(properties.value?.value) ?? '', + () => writeValue?.value ?? readReactive(style.value?.value) ?? readReactive(properties.value) ?? '', ) const type = computedNonInheritableProperty(style, properties, 'type', 'text') @@ -192,7 +202,11 @@ export function createInputState( ) const disabled = computedNonInheritableProperty(style, properties, 'disabled', false) - const updateMatrixWorld = computedInheritableProperty(mergedProperties, 'updateMatrixWorld', false) + const updateMatrixWorld = computedInheritableProperty( + mergedProperties, + 'updateMatrixWorld', + false, + ) const instancedTextRef: { current?: InstancedText } = {} @@ -208,7 +222,7 @@ export function createInputState( const selectionHandlers = computedSelectionHandlers(type, valueSignal, flexState, instancedTextRef, focus, disabled) - const multiline = style.peek()?.multiline ?? properties.peek()?.multiline ?? false + const multiline = style.peek()?.multiline ?? properties.$multiline?.peek() ?? false const element = createHtmlInputElement( selectionRange, @@ -217,7 +231,7 @@ export function createInputState( writeValue.value = newValue } style.peek()?.onValueChange?.(newValue) - properties.peek()?.onValueChange?.(newValue) + properties.$onValueChange?.peek()?.(newValue) }, multiline, ) @@ -276,8 +290,8 @@ export function setupInput( state: ReturnType, parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: ReadonlyDeepSignalObject>, + defaultProperties: DeepSignal, object: Object3D, abortSignal: AbortSignal, ) { @@ -376,7 +390,7 @@ export function setupInput( state.element, state.hasFocusSignal, (hasFocus) => { - properties.peek()?.onFocusChange?.(hasFocus) + properties.$onFocusChange?.peek()?.(hasFocus) style.peek()?.onFocusChange?.(hasFocus) }, abortSignal, diff --git a/packages/uikit/src/components/root.ts b/packages/uikit/src/components/root.ts index cdcedc2d..09ba6e8f 100644 --- a/packages/uikit/src/components/root.ts +++ b/packages/uikit/src/components/root.ts @@ -34,19 +34,22 @@ import { setupPointerEvents, computedAncestorsHaveListeners, setupNode, + mergeProps, + ReadonlyDeepSignalObject, } from './utils.js' import { computedClippingRect } from '../clipping.js' import { ElementType, WithReversePainterSortStableCache, computedOrderInfo } from '../order.js' import { Camera, Matrix4, Object3D, Plane, Vector2Tuple, Vector3, WebGLRenderer } from 'three' import { GlyphGroupManager } from '../text/render/instanced-glyph-group.js' -import { createActivePropertyTransfomers } from '../active.js' -import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' +import { createHoveredStuff, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { createInteractionPanel, setupInteractionPanel } from '../panel/instanced-panel-mesh.js' -import { createResponsivePropertyTransformers } from '../responsive.js' -import { darkPropertyTransformers } from '../dark.js' +import { createResponsivePropertyStuff, createResponsivePropertyTransformers } from '../responsive.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { computedInheritableProperty } from '../properties/index.js' import { getDefaultPanelMaterialConfig, PointerEventsProperties } from '../panel/index.js' import { EventHandlers, ThreeEventMap } from '../events.js' +import { DeepSignal } from 'deepsignal/core' export type InheritableRootProperties = WithClasses< WithConditionals< @@ -83,8 +86,8 @@ export function createRootState( objectRef: { current?: Object3D | null }, pixelSize: Signal, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, getCamera: () => Camera, renderer: WebGLRenderer, onFrameSet: Set<(delta: number) => void>, @@ -97,21 +100,27 @@ export function createRootState( const flexState = createFlexNodeState() - const mergedProperties = computedMergedProperties( - style, - properties, - defaultProperties, - { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(flexState.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - }, - { - ...createSizeTranslator(pixelSize, 'sizeX', 'width'), - ...createSizeTranslator(pixelSize, 'sizeY', 'height'), - }, - ) + // const mergedProperties = computedMergedProperties( + // style, + // properties, + // defaultProperties, + // { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(flexState.size), + // ...createHoverPropertyTransformers(hoveredSignal), + // ...createActivePropertyTransfomers(activeSignal), + // }, + // { + // ...createSizeTranslator(pixelSize, 'sizeX', 'width'), + // ...createSizeTranslator(pixelSize, 'sizeY', 'height'), + // }, + // ) + const mergedProperties = mergeProps>(properties, defaultProperties, [ + [0, darkStuff], + [10, createResponsivePropertyStuff(flexState.size)], + [20, createHoveredStuff(hoveredSignal)], + [30, createActiveStuff(activeSignal)], + ]) const ctx: WithReversePainterSortStableCache & Pick = { @@ -184,7 +193,7 @@ export function createRootState( export function setupRoot( state: ReturnType, style: Signal | undefined>, - properties: Signal | undefined>, + properties: DeepSignal>, object: Object3D, childrenContainer: Object3D, abortSignal: AbortSignal, @@ -317,13 +326,21 @@ const defaultAnchorX: keyof typeof alignmentXMap = 'center' const defaultAnchorY: keyof typeof alignmentYMap = 'center' function computedRootMatrix( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject, matrix: Signal, size: Signal, pixelSize: Signal, ) { - const anchorX = computedInheritableProperty(propertiesSignal, 'anchorX', defaultAnchorX) - const anchorY = computedInheritableProperty(propertiesSignal, 'anchorY', defaultAnchorY) + const anchorX = computedInheritableProperty( + propertiesSignal, + 'anchorX', + defaultAnchorX, + ) + const anchorY = computedInheritableProperty( + propertiesSignal, + 'anchorY', + defaultAnchorY, + ) return computed(() => { if (size.value == null) { return undefined diff --git a/packages/uikit/src/components/svg.ts b/packages/uikit/src/components/svg.ts index 6b11277b..ccbd4b27 100644 --- a/packages/uikit/src/components/svg.ts +++ b/packages/uikit/src/components/svg.ts @@ -1,4 +1,5 @@ import { Signal, computed, signal } from '@preact/signals-core' +import { DeepSignal } from 'deepsignal/core' import { Box3, Group, Mesh, MeshBasicMaterial, Object3D, Plane, ShapeGeometry, Vector3 } from 'three' import { Listeners } from '../index.js' import { ParentContext, RootContext } from '../context.js' @@ -30,17 +31,18 @@ import { disposeGroup, keepAspectRatioPropertyTransformer, loadResourceWithParams, + mergeProps, } from './utils.js' import { abortableEffect, ColorRepresentation, fitNormalizedContentInside, readReactive } from '../utils.js' import { makeClippedCast, PointerEventsProperties } from '../panel/interaction-panel-mesh.js' import { computedIsClipped, ClippingRect, createGlobalClippingPlanes } from '../clipping.js' import { setupLayoutListeners, setupClippedListeners } from '../listeners.js' -import { createActivePropertyTransfomers } from '../active.js' -import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' +import { createHoveredStuff, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { createInteractionPanel, setupInteractionPanel } from '../panel/instanced-panel-mesh.js' -import { createResponsivePropertyTransformers } from '../responsive.js' +import { createResponsivePropertyStuff, createResponsivePropertyTransformers } from '../responsive.js' import { SVGLoader, SVGResult } from 'three/examples/jsm/loaders/SVGLoader.js' -import { darkPropertyTransformers } from '../dark.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { PanelGroupProperties, computedPanelGroupDependencies, getDefaultPanelMaterialConfig } from '../panel/index.js' import { KeepAspectRatioProperties } from './image.js' import { @@ -53,6 +55,7 @@ import { computedAncestorsHaveListeners, computedClippingRect, } from '../internals.js' +import { SVGProps } from 'react' export type InheritableSvgProperties = WithClasses< WithConditionals< @@ -87,8 +90,8 @@ export function createSvgState( parentCtx: ParentContext, objectRef: { current?: Object3D | null }, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, ) { const flexState = createFlexNodeState() const hoveredSignal = signal>([]) @@ -96,18 +99,29 @@ export function createSvgState( const aspectRatio = signal(undefined) const svgObject = signal(undefined) - const mergedProperties = computedMergedProperties( - style, + // const mergedProperties = computedMergedProperties( + // style, + // properties, + // defaultProperties, + // { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(parentCtx.root.size), + // ...createHoverPropertyTransformers(hoveredSignal), + // ...createActivePropertyTransfomers(activeSignal), + // }, + // keepAspectRatioPropertyTransformer, + // (m) => m.add('aspectRatio', aspectRatio), + // ) + + const mergedProperties = mergeProps & { updateMatrixWorld?: boolean }>( properties, defaultProperties, - { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentCtx.root.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - }, - keepAspectRatioPropertyTransformer, - (m) => m.add('aspectRatio', aspectRatio), + [ + [0, darkStuff], + [10, createResponsivePropertyStuff(flexState.size)], + [20, createHoveredStuff(hoveredSignal)], + [30, createActiveStuff(activeSignal)], + ], ) const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize) @@ -126,7 +140,7 @@ export function createSvgState( ) const orderInfo = computedOrderInfo(undefined, 'zIndexOffset', ElementType.Svg, undefined, backgroundOrderInfo) - const src = computed(() => readReactive(style.value?.src) ?? readReactive(properties.value?.src)) + const src = computed(() => readReactive(style.value?.src) ?? readReactive(properties.$src)) const scrollPosition = createScrollPosition() const childrenMatrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentCtx.root.pixelSize) @@ -183,7 +197,7 @@ export function setupSvg( state: ReturnType, parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, + properties: DeepSignal>, object: Object3D, childrenContainer: Object3D, abortSignal: AbortSignal, @@ -215,6 +229,7 @@ export function setupSvg( loadSvg, disposeGroup, abortSignal, + // @ts-expect-error state.src, parentCtx.root, clippingPlanes, diff --git a/packages/uikit/src/components/text.ts b/packages/uikit/src/components/text.ts index bb228087..31ef23ef 100644 --- a/packages/uikit/src/components/text.ts +++ b/packages/uikit/src/components/text.ts @@ -1,14 +1,15 @@ import { YogaProperties, createFlexNodeState } from '../flex/node.js' -import { createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' +import { createHoveredStuff, createHoverPropertyTransformers, setupCursorCleanup } from '../hover.js' import { computedIsClipped } from '../clipping.js' +import { DeepSignal } from 'deepsignal/core' import { ScrollbarProperties } from '../scroll.js' import { WithAllAliases } from '../properties/alias.js' import { PanelProperties, setupInstancedPanel } from '../panel/instanced-panel.js' import { TransformProperties, setupObjectTransform, computedTransformMatrix } from '../transform.js' import { AllOptionalProperties, WithClasses, WithReactive } from '../properties/default.js' -import { createResponsivePropertyTransformers } from '../responsive.js' +import { createResponsivePropertyStuff, createResponsivePropertyTransformers } from '../responsive.js' import { computedOrderInfo, ElementType, ZIndexProperties } from '../order.js' -import { createActivePropertyTransfomers } from '../active.js' +import { createActivePropertyTransfomers, createActiveStuff } from '../active.js' import { Signal, signal } from '@preact/signals-core' import { VisibilityProperties, @@ -20,6 +21,7 @@ import { setupNode, setupPointerEvents, setupMatrixWorldUpdate, + mergeProps, } from './utils.js' import { Listeners, setupLayoutListeners, setupClippedListeners } from '../listeners.js' import { ParentContext } from '../context.js' @@ -37,7 +39,7 @@ import { computedGylphGroupDependencies, createInstancedText, } from '../text/index.js' -import { darkPropertyTransformers } from '../dark.js' +import { darkPropertyTransformers, darkStuff } from '../dark.js' import { abortableEffect, computedAncestorsHaveListeners, @@ -76,19 +78,25 @@ export function createTextState( textSignal: Signal | Array>>, fontFamilies: Signal | undefined, style: Signal | undefined>, - properties: Signal | undefined>, - defaultProperties: Signal, + properties: DeepSignal>, + defaultProperties: DeepSignal, ) { const flexState = createFlexNodeState() const hoveredSignal = signal>([]) const activeSignal = signal>([]) - const mergedProperties = computedMergedProperties(style, properties, defaultProperties, { - ...darkPropertyTransformers, - ...createResponsivePropertyTransformers(parentCtx.root.size), - ...createHoverPropertyTransformers(hoveredSignal), - ...createActivePropertyTransfomers(activeSignal), - }) + // const mergedProperties = computedMergedProperties(style, properties, defaultProperties, { + // ...darkPropertyTransformers, + // ...createResponsivePropertyTransformers(parentCtx.root.size), + // ...createHoverPropertyTransformers(hoveredSignal), + // ...createActivePropertyTransfomers(activeSignal), + // }) + const mergedProperties = mergeProps>(properties, defaultProperties, [ + [0, darkStuff], + [10, createResponsivePropertyStuff(flexState.size)], + [20, createHoveredStuff(hoveredSignal)], + [30, createActiveStuff(activeSignal)], + ]) const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize) const globalMatrix = computedGlobalMatrix(parentCtx.childrenMatrix, transformMatrix) @@ -149,7 +157,7 @@ export function setupText( state: ReturnType, parentCtx: ParentContext, style: Signal | undefined>, - properties: Signal | undefined>, + properties: DeepSignal>, object: Object3D, abortSignal: AbortSignal, ) { diff --git a/packages/uikit/src/components/utils.ts b/packages/uikit/src/components/utils.ts index a3c6418f..d9a39882 100644 --- a/packages/uikit/src/components/utils.ts +++ b/packages/uikit/src/components/utils.ts @@ -1,4 +1,4 @@ -import { ReadonlySignal, Signal, computed, effect } from '@preact/signals-core' +import { ReadonlySignal, Signal, computed, effect, signal } from '@preact/signals-core' import { BufferGeometry, Color, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D } from 'three' import { WithActive, addActiveHandlers } from '../active.js' import { WithPreferredColorScheme } from '../dark.js' @@ -15,7 +15,8 @@ import { PropertyTransformers, computedInheritableProperty, } from '../properties/index.js' -import { AllowedPointerEventsType, PointerEventsProperties } from '../internals.js' +import { AllowedPointerEventsType, PointerEventsProperties, YogaProperties } from '../internals.js' +import { DeepSignal } from 'deepsignal/core' export function disposeGroup(object: Object3D | undefined) { object?.traverse((mesh) => { @@ -54,13 +55,13 @@ export type VisibilityProperties = { export function computedIsVisible( flexState: FlexNodeState, isClipped: Signal | undefined, - mergedProperties: Signal, + mergedProperties: DeepSignal, ) { return computed( () => flexState.displayed.value && (isClipped == null || !isClipped?.value) && - mergedProperties.value.read('visibility', 'visible') === 'visible', + (mergedProperties.visibility ?? 'visible') === 'visible', ) } @@ -101,7 +102,7 @@ export function setupNode( state: FlexNodeState & { root: RootContext node: Signal - mergedProperties: Signal + mergedProperties: ReadonlyDeepSignalObject }, parentContext: ParentContext | undefined, object: Object3D, @@ -151,8 +152,8 @@ const eventHandlerKeys: Array = [ export function computedHandlers( style: Signal, - propertiesSignal: Signal, - defaultProperties: Signal, + propertiesSignal: DeepSignal, + defaultProperties: DeepSignal, hoveredSignal: Signal>, activeSignal: Signal>, dynamicHandlers?: Signal, @@ -160,25 +161,17 @@ export function computedHandlers( ) { return computed(() => { const handlers: EventHandlers = {} - const properties = propertiesSignal.value - if (properties != null) { + if (propertiesSignal != null) { for (const key of eventHandlerKeys) { - const handler = properties[key] + const handler = propertiesSignal[key] if (handler != null) { handlers[key] = handler as any } } } addHandlers(handlers, dynamicHandlers?.value) - addHoverHandlers( - handlers, - style.value, - propertiesSignal.value, - defaultProperties.value, - hoveredSignal, - defaultCursor, - ) - addActiveHandlers(handlers, style.value, propertiesSignal.value, defaultProperties.value, activeSignal) + addHoverHandlers(handlers, style.value, propertiesSignal, defaultProperties, hoveredSignal, defaultCursor) + addActiveHandlers(handlers, style.value, propertiesSignal, defaultProperties, activeSignal) return handlers }) } @@ -222,8 +215,8 @@ export function addHandler void }, K e export function computedMergedProperties( style: Signal, - properties: Signal, - defaultProperties: Signal, + properties: DeepSignal, + defaultProperties: DeepSignal, postTransformers: PropertyTransformers, preTransformers?: PropertyTransformers, onInit?: (merged: MergedProperties) => void, @@ -231,34 +224,131 @@ export function computedMergedProperties( return computed(() => { const merged = new MergedProperties(preTransformers) onInit?.(merged) - merged.addAll(style.value, properties.value, defaultProperties.value, postTransformers) + // @ts-expect-error + merged.addAll(style.value, properties, defaultProperties.value, postTransformers) return merged }) } +declare type ArrayType = T extends Array ? I : T + +// prettier-ignore +/** @ts-expect-error **/ +interface ReadonlyDeepArray extends ReadonlyArray { + map: (callbackfn: (value: ReadonlyDeepSignal, index: number, array: ReadonlyDeepSignalArray) => U, thisArg?: any) => U[]; + forEach: (callbackfn: (value: ReadonlyDeepSignal, index: number, array: ReadonlyDeepSignalArray) => void, thisArg?: any) => void; + concat(...items: ConcatArray[]): ReadonlyDeepSignalArray; + concat(...items: (T | ConcatArray)[]): ReadonlyDeepSignalArray; + reverse(): ReadonlyDeepSignalArray; + shift(): ReadonlyDeepSignal | undefined; + slice(start?: number, end?: number): ReadonlyDeepSignalArray; + splice(start: number, deleteCount?: number): ReadonlyDeepSignalArray; + splice(start: number, deleteCount: number, ...items: T[]): ReadonlyDeepSignalArray; + filter(predicate: (value: ReadonlyDeepSignal, index: number, array: ReadonlyDeepSignalArray) => value is ReadonlyDeepSignal, thisArg?: any): ReadonlyDeepSignalArray; + filter(predicate: (value: ReadonlyDeepSignal, index: number, array: ReadonlyDeepSignalArray) => unknown, thisArg?: any): ReadonlyDeepSignalArray; + reduce(callbackfn: (previousValue: ReadonlyDeepSignal, currentValue: ReadonlyDeepSignal, currentIndex: number, array: ReadonlyDeepSignalArray) => T): ReadonlyDeepSignal; + reduce(callbackfn: (previousValue: ReadonlyDeepSignal, currentValue: ReadonlyDeepSignal, currentIndex: number, array: ReadonlyDeepSignalArray) => ReadonlyDeepSignal, initialValue: T): ReadonlyDeepSignal; + reduce(callbackfn: (previousValue: U, currentValue: ReadonlyDeepSignal, currentIndex: number, array: ReadonlyDeepSignalArray) => U, initialValue: U): U; + reduceRight(callbackfn: (previousValue: ReadonlyDeepSignal, currentValue: ReadonlyDeepSignal, currentIndex: number, array: ReadonlyDeepSignalArray) => T): ReadonlyDeepSignal; + reduceRight(callbackfn: (previousValue: ReadonlyDeepSignal, currentValue: ReadonlyDeepSignal, currentIndex: number, array: ReadonlyDeepSignalArray) => ReadonlyDeepSignal, initialValue: T): ReadonlyDeepSignal; + reduceRight(callbackfn: (previousValue: U, currentValue: ReadonlyDeepSignal, currentIndex: number, array: ReadonlyDeepSignalArray) => U, initialValue: U): U; +} + +declare type ReadonlyDeepSignalArray = ReadonlyDeepArray> & { + [key: number]: ReadonlyDeepSignal> + $?: { + [key: number]: ReadonlySignal> + } + $length?: ReadonlySignal +} + +declare const isShallow: unique symbol + +export type ReadonlyDeepSignal = T extends Function + ? T + : T extends { + [isShallow]: true + } + ? T + : T extends Array + ? ReadonlyDeepSignalArray + : T extends object + ? ReadonlyDeepSignalObject + : T + +export type ReadonlyDeepSignalObject = { + [P in keyof T & string as `$${P}`]?: T[P] extends Function ? never : ReadonlySignal +} & { + [P in keyof T]: DeepSignal +} + +export function mergeProps( + properties: DeepSignal, + defaultProperties: DeepSignal, + stuff: Array< + [priority: number, lookup: (p: DeepSignal | DeepSignal, k: string) => unknown] + >, +): ReadonlyDeepSignalObject { + const sortedStuff = [...stuff].sort(([prioA], [prioB]) => prioA - prioB) + const proxy = new Proxy(properties, { + get(target, fullKey, receiver) { + const key = fullKey as string + for (const [_prio, lookup] of sortedStuff) { + const value = lookup(properties, key) + if (value !== undefined) { + return value + } + const defaultValue = lookup(defaultProperties, key) + if (defaultValue !== undefined) { + return defaultValue + } + } + if (key in target) { + return Reflect.get(target, key, receiver) + } + if (key in defaultProperties) { + return Reflect.get(defaultProperties, key, receiver) + } + return undefined + }, + set(target, fullKey, value, receiver) { + throw new Error('Cannot set properties on merged properties') + }, + }) + // @ts-expect-error + return proxy + // return properties +} + const colorHelper = new Color() /** * @requires that each mesh inside the group has its default color stored inside object.userData.color */ export function applyAppearancePropertiesToGroup( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject<{ + color?: ColorRepresentation + opacity?: number + depthTest?: boolean + depthWrite?: boolean + renderOrder?: number + }>, group: Signal | Object3D, abortSignal: AbortSignal, ) { abortableEffect(() => { - const properties = propertiesSignal.value - const color = properties.read('color', undefined) + const color = propertiesSignal.color let c: Color | undefined if (Array.isArray(color)) { + // @ts-expect-error c = colorHelper.setRGB(...color) } else if (color != null) { c = colorHelper.set(color) } - const opacity = properties.read('opacity', 1) - const depthTest = properties.read('depthTest', true) - const depthWrite = properties.read('depthWrite', false) - const renderOrder = properties.read('renderOrder', 0) + const opacity = propertiesSignal.opacity ?? 1 + const depthTest = propertiesSignal.depthTest ?? true + const depthWrite = propertiesSignal.depthWrite ?? false + const renderOrder = propertiesSignal.renderOrder ?? 0 readReactive(group)?.traverse((mesh) => { if (!(mesh instanceof Mesh)) { return @@ -331,23 +421,37 @@ export function setupMatrixWorldUpdate( }, abortSignal) } -export function computeDefaultProperties(propertiesSignal: Signal) { +export function computeDefaultProperties( + propertiesSignal: ReadonlyDeepSignalObject< + PointerEventsProperties & { + renderOrder?: number + depthTest?: boolean + depthWrite?: boolean + } + >, +) { return { - pointerEvents: computedInheritableProperty( - propertiesSignal, - 'pointerEvents', - undefined, - ), - pointerEventsOrder: computedInheritableProperty( - propertiesSignal, - 'pointerEventsOrder', - undefined, - ), - pointerEventsType: computedInheritableProperty( - propertiesSignal, - 'pointerEventsType', - undefined, - ), + // pointerEvents: computedInheritableProperty( + // propertiesSignal, + // 'pointerEvents', + // undefined, + // ), + // pointerEventsOrder: computedInheritableProperty( + // propertiesSignal, + // 'pointerEventsOrder', + // undefined, + // ), + // pointerEventsType: computedInheritableProperty( + // propertiesSignal, + // 'pointerEventsType', + // undefined, + // ), + // renderOrder: computedInheritableProperty(propertiesSignal, 'renderOrder', 0), + // depthTest: computedInheritableProperty(propertiesSignal, 'depthTest', true), + // depthWrite: computedInheritableProperty(propertiesSignal, 'depthWrite', false), + pointerEvents: propertiesSignal.$pointerEvents, + pointerEventsOrder: propertiesSignal.$pointerEventsOrder, + pointerEventsType: propertiesSignal.$pointerEventsType, renderOrder: computedInheritableProperty(propertiesSignal, 'renderOrder', 0), depthTest: computedInheritableProperty(propertiesSignal, 'depthTest', true), depthWrite: computedInheritableProperty(propertiesSignal, 'depthWrite', false), @@ -364,7 +468,7 @@ export type OutgoingDefaultProperties = { } export function setupPointerEvents( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject, ancestorsHaveListeners: ReadonlySignal, rootContext: RootContext, target: Object3D, @@ -374,25 +478,15 @@ export function setupPointerEvents( if (target == null) { return } - const properties = propertiesSignal.value target.defaultPointerEvents = 'auto' abortableEffect(() => { target.ancestorsHaveListeners = ancestorsHaveListeners.value - target.pointerEvents = properties.read('pointerEvents', undefined) - target.pointerEventsOrder = properties.read( - 'pointerEventsOrder', - undefined, - ) - target.pointerEventsType = properties.read( - 'pointerEventsType', - undefined, - ) + target.pointerEvents = propertiesSignal.pointerEvents + target.pointerEventsOrder = propertiesSignal.pointerEventsOrder + target.pointerEventsType = propertiesSignal.pointerEventsType }, abortSignal) abortableEffect(() => { - if ( - !canHaveNonUikitChildren && - propertiesSignal.value.read('pointerEvents', undefined) === 'none' - ) { + if (!canHaveNonUikitChildren && propertiesSignal.pointerEvents === 'none') { return } const descendants = rootContext.interactableDescendants diff --git a/packages/uikit/src/dark.ts b/packages/uikit/src/dark.ts index 0d14386b..0c5bcb71 100644 --- a/packages/uikit/src/dark.ts +++ b/packages/uikit/src/dark.ts @@ -1,6 +1,7 @@ import { ReadonlySignal, computed, signal } from '@preact/signals-core' import { ColorRepresentation, createConditionalPropertyTranslator } from './utils.js' import { PropertyTransformers } from './properties/merged.js' +import { DeepSignal } from 'deepsignal/core' export type WithPreferredColorScheme = { dark?: T } & T @@ -37,6 +38,10 @@ export const darkPropertyTransformers: PropertyTransformers = { dark: createConditionalPropertyTranslator(() => isDarkMode.value), } +export const darkStuff = (props: DeepSignal>, key: string) => { + return computed(() => (isDarkMode.value ? (props.$dark?.peek() != null ? props.dark[key] : undefined) : undefined)) +} + export function basedOnPreferredColorScheme({ dark, light, diff --git a/packages/uikit/src/flex/node.ts b/packages/uikit/src/flex/node.ts index dcd33bd9..9cde41a3 100644 --- a/packages/uikit/src/flex/node.ts +++ b/packages/uikit/src/flex/node.ts @@ -3,9 +3,9 @@ import { Signal, batch, effect, signal, untracked } from '@preact/signals-core' import { Display, Edge, FlexDirection, MeasureFunction, Node, Overflow } from 'yoga-layout/load' import { setter } from './setter.js' import { setupImmediateProperties } from '../properties/immediate.js' -import { MergedProperties } from '../properties/merged.js' import { PointScaleFactor, createYogaNode } from './yoga.js' import { abortableEffect } from '../utils.js' +import { ReadonlyDeepSignalObject } from '../internals.js' export type YogaProperties = { [Key in keyof typeof setter]?: Parameters<(typeof setter)[Key]>[1] @@ -54,7 +54,7 @@ export class FlexNode { constructor( private state: FlexNodeState & { root: { requestCalculateLayout(): void } }, - private readonly propertiesSignal: Signal, + private readonly propertiesSignal: ReadonlyDeepSignalObject, private object: Object3D, private objectVisibileDefault: boolean, abortSignal: AbortSignal, @@ -81,6 +81,7 @@ export class FlexNode { this.state.root.requestCalculateLayout() }, abortSignal, + Object.keys(setter), ) } @@ -131,11 +132,10 @@ export class FlexNode { /** ---- START : adaptation of yoga's behavior to align more to the web behavior ---- */ const parentDirectionVertical = parentDirection === FlexDirection.Column || parentDirection === FlexDirection.ColumnReverse - const properties = this.propertiesSignal.peek() if ( this.customLayouting != null && - untracked(() => - properties.read(parentDirectionVertical ? 'minHeight' : 'minWidth', undefined), + untracked( + () => (parentDirectionVertical ? this.propertiesSignal.minHeight : this.propertiesSignal.minWidth, undefined), ) === undefined ) { this.yogaNode[parentDirectionVertical ? 'setMinHeight' : 'setMinWidth']( @@ -145,8 +145,8 @@ export class FlexNode { //see: https://codepen.io/Gettinqdown-Dev/pen/wvZLKBm //-> on the web if the parent has flexdireciton column, elements dont shrink below flexBasis - if (untracked(() => properties.read('flexShrink', undefined)) == null) { - const hasHeight = untracked(() => properties.read('height', undefined)) != null + if (untracked(() => this.propertiesSignal.flexShrink) == null) { + const hasHeight = untracked(() => this.propertiesSignal.height) != null this.yogaNode.setFlexShrink(hasHeight && parentDirectionVertical ? 0 : undefined) } /** ---- END ---- */ diff --git a/packages/uikit/src/hover.ts b/packages/uikit/src/hover.ts index a9b910fb..4266f546 100644 --- a/packages/uikit/src/hover.ts +++ b/packages/uikit/src/hover.ts @@ -1,9 +1,10 @@ -import { Signal } from '@preact/signals-core' +import { computed, Signal } from '@preact/signals-core' import { createConditionalPropertyTranslator } from './utils.js' import { PropertyTransformers } from './properties/merged.js' import { EventHandlers } from './events.js' import { addHandler } from './components/index.js' import { AllOptionalProperties, traverseProperties } from './properties/index.js' +import { DeepSignal } from 'deepsignal/core' export type WithHover = T & { cursor?: string @@ -20,8 +21,8 @@ export function setupCursorCleanup(hoveredSignal: Signal>, abortSi export function addHoverHandlers( target: EventHandlers, style: WithHover<{}> | undefined, - properties: WithHover<{}> | undefined, - defaultProperties: AllOptionalProperties | undefined, + properties: DeepSignal>, + defaultProperties: DeepSignal, hoveredSignal: Signal>, defaultCursor?: string, ): void { @@ -39,8 +40,9 @@ export function addHoverHandlers( return } addHandler('onPointerOver', target, ({ pointerId }) => { - hoveredSignal.value = [pointerId, ...hoveredSignal.value] - if (hoveredSignal.value.length === 1) { + const newValue = [pointerId, ...hoveredSignal.peek()] + hoveredSignal.value = newValue + if (newValue.length === 1) { properties?.onHoverChange?.(true) style?.onHoverChange?.(true) } @@ -49,8 +51,9 @@ export function addHoverHandlers( } }) addHandler('onPointerOut', target, ({ pointerId }) => { - hoveredSignal.value = hoveredSignal.value.filter((id) => id != pointerId) - if (hoveredSignal.value.length === 0) { + const newValue = hoveredSignal.peek().filter((id) => id != pointerId) + hoveredSignal.value = newValue + if (newValue.length === 0) { properties?.onHoverChange?.(false) style?.onHoverChange?.(false) } @@ -82,3 +85,12 @@ export function unsetCursorType(ref: unknown): void { cursorTypeStack.splice(index, 1) document.body.style.cursor = cursorTypeStack[cursorTypeStack.length - 1] ?? 'default' } + +export const createHoveredStuff = + (hoveredSignal: Signal>) => (properties: WithHover, key: string) => { + return computed(() => { + if (hoveredSignal.value.length > 0 && 'hover' in properties) { + return properties.hover?.[key] + } + }) + } diff --git a/packages/uikit/src/listeners.ts b/packages/uikit/src/listeners.ts index f05679ac..cb1443d1 100644 --- a/packages/uikit/src/listeners.ts +++ b/packages/uikit/src/listeners.ts @@ -2,6 +2,8 @@ import { Signal, effect } from '@preact/signals-core' import { Vector2Tuple } from 'three' import { ThreeMouseEvent, ThreePointerEvent } from './events.js' import { abortableEffect } from './utils.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from './internals.js' export type Listeners = ScrollListeners & LayoutListeners & ClippedListeners @@ -32,7 +34,7 @@ export type ClippedListeners = { export function setupLayoutListeners( l1: Signal, - l2: Signal, + l2: ReadonlyDeepSignalObject, size: Signal, abortSignal: AbortSignal, ) { @@ -42,13 +44,13 @@ export function setupLayoutListeners( return } l1.peek()?.onSizeChange?.(...s) - l2.peek()?.onSizeChange?.(...s) + l2.$onSizeChange?.peek()?.(...s) }, abortSignal) } export function setupClippedListeners( l1: Signal, - l2: Signal, + l2: ReadonlyDeepSignalObject, isClippedSignal: Signal, abortSignal: AbortSignal, ) { @@ -60,6 +62,6 @@ export function setupClippedListeners( return } l1.peek()?.onIsClippedChange?.(isClipped) - l2.peek()?.onIsClippedChange?.(isClipped) + l2.$onIsClippedChange?.peek()?.(isClipped) }, abortSignal) } diff --git a/packages/uikit/src/order.ts b/packages/uikit/src/order.ts index 67a77bd7..1979aecd 100644 --- a/packages/uikit/src/order.ts +++ b/packages/uikit/src/order.ts @@ -3,6 +3,8 @@ import { RenderItem } from 'three' import { MergedProperties } from './properties/merged.js' import { computedInheritableProperty } from './properties/index.js' import { readReactive } from './utils.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from './components/utils.js' export type WithReversePainterSortStableCache = { reversePainterSortStableCache?: number } @@ -79,17 +81,13 @@ export type ZIndexProperties = { export type ZIndexOffset = { major?: number; minor?: number } | number -export function computedOrderInfo( - propertiesSignal: Signal | undefined, - zIndexOffsetKey: string, +export function computedOrderInfo( + propertiesSignal: ReadonlyDeepSignalObject<{ [TK in K]?: ZIndexOffset }> | undefined, + zIndexOffsetKey: K, type: ElementType, instancedGroupDependencies: Signal> | Record | undefined, parentOrderInfoSignal: Signal | undefined, ): Signal { - const zIndexOffset = - propertiesSignal == null - ? undefined - : computedInheritableProperty(propertiesSignal, zIndexOffsetKey, undefined) return computed(() => { let parentOrderInfo: OrderInfo | undefined if (parentOrderInfoSignal == null) { @@ -100,7 +98,7 @@ export function computedOrderInfo( parentOrderInfo = parentOrderInfoSignal.value } - const offset = zIndexOffset?.value + const offset = propertiesSignal?.[zIndexOffsetKey] 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/instanced-panel-group.ts b/packages/uikit/src/panel/instanced-panel-group.ts index 438aa6d4..3eeaefcf 100644 --- a/packages/uikit/src/panel/instanced-panel-group.ts +++ b/packages/uikit/src/panel/instanced-panel-group.ts @@ -13,6 +13,7 @@ import { ElementType, OrderInfo, WithReversePainterSortStableCache, setupRenderO import { Signal, computed } from '@preact/signals-core' import { MergedProperties } from '../properties/merged.js' import { RootContext } from '../context.js' +import { DeepSignal } from 'deepsignal/core' export type ShadowProperties = { receiveShadow?: boolean @@ -30,16 +31,15 @@ export type PanelGroupProperties = { } & ShadowProperties & RenderProperties -export function computedPanelGroupDependencies(propertiesSignal: Signal) { +export function computedPanelGroupDependencies(propertiesSignal: DeepSignal) { return computed>(() => { - const properties = propertiesSignal.value return { - panelMaterialClass: properties.read('panelMaterialClass', MeshBasicMaterial), - castShadow: properties.read('castShadow', false), - receiveShadow: properties.read('receiveShadow', false), - depthWrite: properties.read('depthWrite', false), - depthTest: properties.read('depthTest', true), - renderOrder: properties.read('renderOrder', 0), + panelMaterialClass: propertiesSignal.panelMaterialClass ?? MeshBasicMaterial, + castShadow: propertiesSignal.castShadow ?? false, + receiveShadow: propertiesSignal.receiveShadow ?? false, + depthWrite: propertiesSignal.depthWrite ?? false, + depthTest: propertiesSignal.depthTest ?? true, + renderOrder: propertiesSignal.renderOrder ?? 0, } }) } diff --git a/packages/uikit/src/panel/instanced-panel.ts b/packages/uikit/src/panel/instanced-panel.ts index aaf74c1e..caf8fd6a 100644 --- a/packages/uikit/src/panel/instanced-panel.ts +++ b/packages/uikit/src/panel/instanced-panel.ts @@ -9,6 +9,8 @@ import { MergedProperties } from '../properties/merged.js' import { setupImmediateProperties } from '../properties/immediate.js' import { OrderInfo } from '../order.js' import { PanelMaterialConfig } from './panel-material.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from '../internals.js' export type PanelProperties = { borderTopLeftRadius?: number @@ -22,8 +24,8 @@ export type PanelProperties = { borderOpacity?: number } -export function setupInstancedPanel( - propertiesSignal: Signal, +export function setupInstancedPanel( + propertiesSignal: ReadonlyDeepSignalObject>>, orderInfo: Signal, panelGroupDependencies: Signal>, panelGroupManager: PanelGroupManager, @@ -33,7 +35,7 @@ export function setupInstancedPanel( borderInset: Signal, clippingRect: Signal | undefined, isVisible: Signal, - materialConfig: PanelMaterialConfig, + materialConfig: PanelMaterialConfig, abortSignal: AbortSignal, ) { abortableEffect(() => { @@ -62,7 +64,7 @@ export function setupInstancedPanel( const matrixHelper1 = new Matrix4() const matrixHelper2 = new Matrix4() -export class InstancedPanel { +export class InstancedPanel { private indexInBucket?: number private bucket?: Bucket @@ -72,7 +74,7 @@ export class InstancedPanel { private abortController?: AbortController constructor( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject>, private group: InstancedPanelGroup, private readonly minorIndex: number, private readonly matrix: Signal, @@ -81,7 +83,7 @@ export class InstancedPanel { private readonly borderInset: Signal, private readonly clippingRect: Signal | undefined, isVisible: Signal, - public readonly materialConfig: PanelMaterialConfig, + public readonly materialConfig: PanelMaterialConfig, abortSignal: AbortSignal, ) { const setters = materialConfig.setters @@ -99,6 +101,7 @@ export class InstancedPanel { root.requestRender() }, abortSignal, + Object.keys(setters) as PropK[], ) const isPanelVisible = materialConfig.computedIsVisibile(propertiesSignal, borderInset, size, isVisible) abortableEffect(() => { @@ -201,7 +204,7 @@ export class InstancedPanel { return } this.insertedIntoGroup = true - this.group.insert(this.minorIndex, this) + this.group.insert(this.minorIndex, this as InstancedPanel) } private hide(): void { @@ -209,7 +212,7 @@ export class InstancedPanel { return } this.active.value = false - this.group.delete(this.minorIndex, this.indexInBucket, this) + this.group.delete(this.minorIndex, this.indexInBucket, this as InstancedPanel) this.insertedIntoGroup = false this.bucket = undefined this.indexInBucket = undefined diff --git a/packages/uikit/src/panel/panel-material.ts b/packages/uikit/src/panel/panel-material.ts index ca85d737..e086f54b 100644 --- a/packages/uikit/src/panel/panel-material.ts +++ b/packages/uikit/src/panel/panel-material.ts @@ -11,10 +11,12 @@ import { WebGLRenderer, } from 'three' import { Constructor, setBorderRadius } from './utils.js' -import { Signal, computed } from '@preact/signals-core' +import { ReadonlySignal, Signal, computed } from '@preact/signals-core' import { ColorRepresentation } from '../utils.js' import { MergedProperties } from '../properties/index.js' import { Inset } from '../flex/index.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from '../internals.js' export type MaterialClass = { new (...args: Array): Material } @@ -22,6 +24,8 @@ type InstanceOf = T extends { new (): infer K } ? K : never const noColor = new Color(-1, -1, -1) +type MaterialSettersKeys = keyof typeof materialSetters + const defaultDefaults = { backgroundColor: noColor as ColorRepresentation, backgroundOpacity: -1, @@ -32,47 +36,73 @@ const defaultDefaults = { borderTopRightRadius: 0, borderBend: 0, borderOpacity: 1, -} satisfies { [Key in keyof typeof materialSetters]: unknown } +} satisfies { [Key in MaterialSettersKeys]: unknown } -export type PanelMaterialConfig = ReturnType +type MaterialSetterFn = ( + data: TypedArray, + offset: number, + value: unknown, + size: Signal, + onUpdate: ((start: number, count: number) => void) | undefined, +) => void + +export type PanelMaterialConfig = { + hasProperty: (key: string) => boolean + defaultData: TypedArray + setters: Record + computedIsVisibile: ( + propertiesSignal: ReadonlyDeepSignalObject>>, + borderInset: Signal, + size: Signal, + isVisible: Signal, + ) => ReadonlySignal +} -let defaultPanelMaterialConfig: PanelMaterialConfig | undefined +let defaultPanelMaterialConfig: PanelMaterialConfig | undefined export function getDefaultPanelMaterialConfig() { if (defaultPanelMaterialConfig == null) { - const defaultPanelMaterialKeys = {} as { [Key in keyof typeof defaultDefaults]: string } + const defaultPanelMaterialKeys = {} as { [Key in keyof typeof defaultDefaults]: keyof typeof defaultDefaults } for (const key in defaultDefaults) { - defaultPanelMaterialKeys[key as keyof typeof defaultDefaults] = key + defaultPanelMaterialKeys[key as keyof typeof defaultDefaults] = key as keyof typeof defaultDefaults } defaultPanelMaterialConfig = createPanelMaterialConfig(defaultPanelMaterialKeys) } return defaultPanelMaterialConfig } -export function createPanelMaterialConfig( - keys: { [Key in keyof typeof materialSetters]?: string }, +export function createPanelMaterialConfig( + keys: Partial>, overrideDefaults?: { [Key in Exclude< keyof typeof defaultDefaults, 'borderBottomLeftRadius' | 'borderTopLeftRadius' | 'borderBottomRightRadius' | 'borderTopRightRadius' >]?: (typeof defaultDefaults)[Key] }, -) { +): PanelMaterialConfig { const defaults = { ...defaultDefaults, ...overrideDefaults } const setters: { - [Key in string]: ( + [Key in PropK]: ( 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) => + } = {} as any + for (const materialSetterKey in keys) { + const fn = materialSetters[materialSetterKey as MaterialSettersKeys] + const defaultValue = defaults[materialSetterKey as MaterialSettersKeys] + const propertiesKey = keys[materialSetterKey as MaterialSettersKeys]! + setters[propertiesKey] = ( + data: TypedArray, + offset: number, + value: unknown, + size: Signal, + onUpdate: ((start: number, count: number) => void) | undefined, + ): void => { fn(data, offset, (value ?? defaultValue) as any, size, onUpdate) + } } const defaultData = new Float32Array(16) //filled with 0s by default @@ -85,12 +115,7 @@ export function createPanelMaterialConfig( hasProperty: (key: string) => key in setters, defaultData, setters, - computedIsVisibile: ( - propertiesSignal: Signal, - borderInset: Signal, - size: Signal, - isVisible: Signal, - ) => { + computedIsVisibile: (propertiesSignal, borderInset, size, isVisible) => { return computed(() => { if (borderInset.value == null || size.value == null) { return true @@ -98,15 +123,15 @@ export function createPanelMaterialConfig( const borderOpacity = keys.borderOpacity == null ? defaults.borderOpacity - : propertiesSignal.value.read(keys.borderOpacity, defaults.borderOpacity) + : ((propertiesSignal[keys.borderOpacity] as number) ?? defaults.borderOpacity) const backgroundOpacity = keys.backgroundOpacity == null ? defaults.backgroundOpacity - : propertiesSignal.value.read(keys.backgroundOpacity, defaults.backgroundOpacity) + : ((propertiesSignal[keys.backgroundOpacity] as number) ?? defaults.backgroundOpacity) const backgroundColor = keys.backgroundColor == null ? defaults.backgroundColor - : propertiesSignal.value.read(keys.backgroundColor, defaults.backgroundColor) + : ((propertiesSignal[keys.backgroundColor] as ColorRepresentation) ?? defaults.backgroundColor) const borderVisible = borderInset.value.some((s) => s > 0) && borderOpacity > 0 const [width, height] = size.value const backgroundVisible = @@ -357,7 +382,7 @@ function compilePanelClippingMaterial(parameters: WebGLProgramParametersWithUnif distanceToPlane = - dot( -localPosition, plane.xyz ) + plane.w; distanceGradient = fwidth( distanceToPlane ) / 2.0; clipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); - + if ( clipOpacity < 0.01 ) discard; } ` @@ -376,68 +401,68 @@ function compilePanelClippingMaterial(parameters: WebGLProgramParametersWithUnif vec4 borderSize = absoluteBorderSize / relative; vec4 v_outsideDistance = vec4(1.0 - vUv.y, (1.0 - vUv.x) * ratio, vUv.y, vUv.x * ratio); vec4 v_borderDistance = v_outsideDistance - borderSize; - + vec2 distance = vec2(min4(v_outsideDistance), min4(v_borderDistance)); vec4 negateBorderDistance = vec4(1.0) - v_borderDistance; float maxWeight = max4(negateBorderDistance); vec4 borderWeight = step(maxWeight, negateBorderDistance); - + vec4 insideBorder; - + if(all(lessThan(v_outsideDistance.xw, borderRadius.xx))) { distance = radiusDistance(borderRadius.x, v_outsideDistance.xw, v_borderDistance.xw, borderSize.xw); - + float tmp = borderRadius.x - borderSize.w; vec2 xIntersection = vec2(tmp, tmp / ratio); tmp = borderRadius.x - borderSize.x; vec2 yIntersection = vec2(tmp * ratio, tmp); vec2 lineIntersection = min(xIntersection, yIntersection); - + insideBorder.yz = vec2(0.0); insideBorder.xw = max(vec2(0.0), lineIntersection - v_borderDistance.xw); - + } else if(all(lessThan(v_outsideDistance.xy, borderRadius.yy))) { distance = radiusDistance(borderRadius.y, v_outsideDistance.xy, v_borderDistance.xy, borderSize.xy); - + float tmp = borderRadius.y - borderSize.y; vec2 xIntersection = vec2(tmp, tmp / ratio); tmp = borderRadius.y - borderSize.x; vec2 yIntersection = vec2(tmp * ratio, tmp); vec2 lineIntersection = min(xIntersection, yIntersection); - + insideBorder.zw = vec2(0.0); insideBorder.xy = max(vec2(0.0), lineIntersection - v_borderDistance.xy); - + } else if(all(lessThan(v_outsideDistance.zy, borderRadius.zz))) { distance = radiusDistance(borderRadius.z, v_outsideDistance.zy, v_borderDistance.zy, borderSize.zy); - + float tmp = borderRadius.z - borderSize.y; vec2 xIntersection = vec2(tmp, tmp / ratio); tmp = borderRadius.z - borderSize.z; vec2 yIntersection = vec2(tmp * ratio, tmp); vec2 lineIntersection = min(xIntersection, yIntersection); - + insideBorder.xw = vec2(0.0); insideBorder.zy =max(vec2(0.0), lineIntersection - v_borderDistance.zy); - + } else if(all(lessThan(v_outsideDistance.zw, borderRadius.ww))) { distance = radiusDistance(borderRadius.w, v_outsideDistance.zw, v_borderDistance.zw, borderSize.zw); - + float tmp = borderRadius.w - borderSize.w; vec2 xIntersection = vec2(tmp, tmp / ratio); tmp = borderRadius.w - borderSize.z; vec2 yIntersection = vec2(tmp * ratio, tmp); vec2 lineIntersection = min(xIntersection, yIntersection); - + insideBorder.xy = vec2(0.0); insideBorder.zw = max(vec2(0.0), lineIntersection - v_borderDistance.zw); - + } - + if(insideBorder.x + insideBorder.y + insideBorder.z + insideBorder.w > 0.0) { borderWeight = normalize(insideBorder); } - + #include `, ) } @@ -464,7 +489,7 @@ function getFargmentOpacityCode(instanced: boolean, existingOpacity: string | un borderOpacity = min(backgroundOpacity + data[3].x, 1.0); borderColor = mix(backgroundColor, data[2].xyz, data[3].x / borderOpacity); - + float outOpacity = ${ instanced ? 'clipOpacity * ' : '' diff --git a/packages/uikit/src/properties/default.ts b/packages/uikit/src/properties/default.ts index 015c9aed..83458946 100644 --- a/packages/uikit/src/properties/default.ts +++ b/packages/uikit/src/properties/default.ts @@ -10,6 +10,7 @@ import type { InheritableInputProperties, InheritableSvgProperties, } from '../components/index.js' +import { DeepSignal } from 'deepsignal/core' export type AllOptionalProperties = | InheritableContainerProperties @@ -24,25 +25,26 @@ export type AllOptionalProperties = | InheritableInputProperties export type WithReactive = { - [Key in keyof T]?: T[Key] | ReadonlySignal + // [Key in keyof T]?: T[Key] | ReadonlySignal + [Key in keyof T]?: T[Key] } export type Properties = Record -export type WithClasses = T & { classes?: T | Array } +export type WithClasses = T & { classes?: T | Array } -export function traverseProperties( +export function traverseProperties( style: WithClasses | undefined, - properties: WithClasses | undefined, - defaultProperties: AllOptionalProperties | undefined, - fn: (properties: T) => void, + properties: DeepSignal>, + defaultProperties: DeepSignal, + fn: (properties: T | DeepSignal) => void, ): void { if (defaultProperties != null) { traverseClasses(defaultProperties.classes as any, fn) fn(defaultProperties as T) } - if (properties != null) { - traverseClasses(properties.classes as any, fn) + if (typeof properties === 'object') { + if ('classes' in properties) traverseClasses(properties.classes as any, fn) fn(properties) } if (style != null) { @@ -51,7 +53,7 @@ export function traverseProperties( } } -function traverseClasses(classes: WithClasses['classes'], fn: (properties: T) => void) { +function traverseClasses(classes: WithClasses['classes'], fn: (properties: T) => void) { if (classes == null) { return } diff --git a/packages/uikit/src/properties/immediate.ts b/packages/uikit/src/properties/immediate.ts index 68d0c8d9..abb9635f 100644 --- a/packages/uikit/src/properties/immediate.ts +++ b/packages/uikit/src/properties/immediate.ts @@ -1,43 +1,57 @@ import { Signal, effect, untracked } from '@preact/signals-core' import { MergedProperties } from './merged.js' import { abortableEffect } from '../utils.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from '../internals.js' type PropertySubscriptions = Record void> -export function setupImmediateProperties( - propertiesSignal: Signal, +export function setupImmediateProperties( + propertiesSignal: ReadonlyDeepSignalObject>, activeSignal: Signal, hasProperty: (key: string) => boolean, - setProperty: (key: string, value: unknown) => void, + setProperty: (key: PropK, value: unknown) => void, abortSignal: AbortSignal, + keys: PropK[], ): void { let active = false - let currentProperties: MergedProperties | undefined - let propertySubscriptions: PropertySubscriptions = {} + let currentProperties: Partial> = {} + // let propertySubscriptions: PropertySubscriptions = {} //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 abortableEffect(() => { - const newProperties = propertiesSignal.value + const newProperties = propertiesSignal if (active) { - applyProperties(hasProperty, newProperties, currentProperties, propertySubscriptions, setProperty) + // applyProperties(hasProperty, newProperties, currentProperties, propertySubscriptions, setProperty) + for (const key of keys) { + const newValue = newProperties[key] + if (currentProperties[key] !== newValue) { + currentProperties[key] = newValue + setProperty(key, newValue) + } + } } - currentProperties = newProperties + // currentProperties = newProperties }, abortSignal) abortableEffect(() => { active = activeSignal.value if (!active) { return } - if (currentProperties == null) { - return - } + // if (currentProperties == null) { + // return + // } //(re-)write all current properties since the object is (re-)activiated it might not have its values set - applyProperties(hasProperty, currentProperties, undefined, propertySubscriptions, setProperty) - return () => { - unsubscribeProperties(propertySubscriptions) - propertySubscriptions = {} + // applyProperties(hasProperty, currentProperties, undefined, propertySubscriptions, setProperty) + for (const key of keys) { + currentProperties[key] = propertiesSignal[key] + setProperty(key, propertiesSignal[key]) } + // return () => { + // // unsubscribeProperties(propertySubscriptions) + // // propertySubscriptions = {} + // } }, abortSignal) } diff --git a/packages/uikit/src/properties/merged.ts b/packages/uikit/src/properties/merged.ts index f7394c6d..3835cb83 100644 --- a/packages/uikit/src/properties/merged.ts +++ b/packages/uikit/src/properties/merged.ts @@ -1,6 +1,7 @@ import { Signal } from '@preact/signals-core' import { AllOptionalProperties, Properties, WithClasses, traverseProperties } from './default.js' import { AllAliases, allAliases } from './alias.js' +import { DeepSignal } from 'deepsignal/core' export type PropertyTransformers = Record void> @@ -118,15 +119,18 @@ export class MergedProperties { addAll( style: WithClasses | undefined, - properties: WithClasses | undefined, + properties: DeepSignal>, defaultProperties: AllOptionalProperties | undefined, postTransformers: PropertyTransformers, ): void { + // @ts-expect-error traverseProperties(style, properties, defaultProperties, (p) => { for (const key in p) { + // @ts-expect-error this.add(key, p[key]) } for (const key in postTransformers) { + // @ts-expect-error const property = p[key] if (property == null) { continue diff --git a/packages/uikit/src/properties/utils.ts b/packages/uikit/src/properties/utils.ts index 7faf1a75..183d441c 100644 --- a/packages/uikit/src/properties/utils.ts +++ b/packages/uikit/src/properties/utils.ts @@ -1,22 +1,23 @@ -import { Signal, computed } from '@preact/signals-core' +import { Signal, ReadonlySignal, computed } from '@preact/signals-core' import { MergedProperties } from './merged.js' import { readReactive } from '../utils.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from '../internals.js' -export function computedInheritableProperty( - propertiesSignal: Signal, - key: string, +export function computedInheritableProperty( + propertiesSignal: ReadonlyDeepSignalObject>>, + key: K, defaultValue: T, -): Signal { - return computed(() => propertiesSignal.value.read(key, defaultValue)) +): ReadonlySignal { + // @ts-expect-error + return computed(() => propertiesSignal[key] ?? defaultValue) } export function computedNonInheritableProperty( style: Signal | undefined>, - properties: Signal | undefined>, + properties: DeepSignal>, key: string, defaultValue: T, ): Signal { - return computed( - () => readReactive(style.value?.[key] as T) ?? readReactive(properties.value?.[key] as T) ?? defaultValue, - ) + return computed(() => readReactive(style.value?.[key] as T) ?? readReactive(properties[key] as T) ?? defaultValue) } diff --git a/packages/uikit/src/responsive.ts b/packages/uikit/src/responsive.ts index d14ec39c..238e4bc6 100644 --- a/packages/uikit/src/responsive.ts +++ b/packages/uikit/src/responsive.ts @@ -1,4 +1,4 @@ -import { Signal } from '@preact/signals-core' +import { computed, Signal } from '@preact/signals-core' import { createConditionalPropertyTranslator } from './utils.js' import { Vector2Tuple } from 'three' import { PropertyTransformers } from './properties/merged.js' @@ -27,3 +27,19 @@ export function createResponsivePropertyTransformers(rootSize: Signal) => + (properties: Partial>, key: string) => { + return computed(() => { + for (let i = 0; i < breakPointKeysLength; i++) { + const rootSizeVal = rootSize.value?.[0] ?? 0 + if (rootSizeVal > breakPoints[key as keyof typeof breakPoints]) { + const responsiveValue = properties[breakPointKeys[i]] + if (responsiveValue != null) { + return responsiveValue[key] + } + } + } + }) + } diff --git a/packages/uikit/src/scroll.ts b/packages/uikit/src/scroll.ts index 506f3662..f5b42d09 100644 --- a/packages/uikit/src/scroll.ts +++ b/packages/uikit/src/scroll.ts @@ -16,6 +16,8 @@ import { import { ParentContext, RootContext } from './context.js' import { ScrollListeners } from './listeners.js' import { EventHandlers, ThreeMouseEvent, ThreePointerEvent } from './events.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from './components/utils.js' const distanceHelper = new Vector3() const localPointHelper = new Vector3() @@ -83,7 +85,7 @@ export type ScrollableComponentState = { export function computedScrollHandlers( state: ScrollableComponentState, - listeners: Signal, + listeners: DeepSignal, objectRef: { current?: Object3D | null }, ) { const isScrollable = computed(() => state.scrollable.value?.some((scrollable) => scrollable) ?? false) @@ -215,7 +217,7 @@ export function computedScrollHandlers( function scroll( state: ScrollableComponentState, - listeners: Signal, + listeners: DeepSignal, event: ThreePointerEvent | ThreeMouseEvent | undefined, deltaX: number, deltaY: number, @@ -250,7 +252,7 @@ function scroll( wasScrolledY || Math.min(y, (maxY ?? 0) - y) > 5, ) } - const preventScroll = listeners.peek()?.onScroll?.(newX, newY, state.scrollPosition, event) + const preventScroll = listeners?.$onScroll?.peek()?.(newX, newY, state.scrollPosition, event) if (preventScroll === false || (x === newX && y === newY)) { return } @@ -259,7 +261,7 @@ function scroll( export function setupScroll( state: ScrollableComponentState, - listeners: Signal, + listeners: DeepSignal, pixelSizeSignal: Signal, object: Object3D, abortSignal: AbortSignal, @@ -391,7 +393,7 @@ export type ScrollbarProperties = { [Key in Exclude< keyof PanelProperties, 'backgroundColor' | 'backgroundOpacity' - > as `scrollbar${Capitalize}`]: PanelProperties[Key] + > as `scrollbar${Capitalize}`]?: PanelProperties[Key] } const scrollbarBorderPropertyKeys = [ @@ -402,7 +404,11 @@ const scrollbarBorderPropertyKeys = [ ] as const export function setupScrollbars( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject< + ScrollbarProperties & + Partial> & + Partial> + >, scrollPosition: Signal, flexState: FlexNodeState, globalMatrix: Signal, @@ -455,7 +461,18 @@ export function setupScrollbars( ) } -let scrollbarMaterialConfig: PanelMaterialConfig | undefined +type ScrollbarPanelMaterialPropKeys = + | 'scrollbarColor' + | 'scrollbarBorderBottomLeftRadius' + | 'scrollbarBorderBottomRightRadius' + | 'scrollbarBorderTopRightRadius' + | 'scrollbarBorderTopLeftRadius' + | 'scrollbarBorderColor' + | 'scrollbarBorderBend' + | 'scrollbarBorderOpacity' + | 'scrollbarOpacity' + +let scrollbarMaterialConfig: PanelMaterialConfig | undefined function getScrollbarMaterialConfig() { scrollbarMaterialConfig ??= createPanelMaterialConfig( { @@ -478,7 +495,9 @@ function getScrollbarMaterialConfig() { } function setupScrollbar( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject< + ScrollbarProperties & Partial> + >, primaryIndex: number, scrollPosition: Signal, flexState: FlexNodeState, diff --git a/packages/uikit/src/selection.ts b/packages/uikit/src/selection.ts index cc59e100..f27a8ad0 100644 --- a/packages/uikit/src/selection.ts +++ b/packages/uikit/src/selection.ts @@ -11,6 +11,7 @@ import { createPanelMaterialConfig, } from './panel/index.js' import { MergedProperties } from './properties/index.js' +import { ReadonlyDeepSignalObject } from './internals.js' export type SelectionTransformation = { size: Vector2Tuple; position: Vector2Tuple } @@ -35,10 +36,17 @@ export type SelectionProperties = { [Key in Exclude< keyof PanelProperties, 'backgroundColor' | 'backgroundOpacity' - > as `selection${Capitalize}`]: PanelProperties[Key] + > as `selection${Capitalize}`]?: PanelProperties[Key] } -let selectionMaterialConfig: PanelMaterialConfig | undefined +type SelectionMaterialConfigPropKeys = + | (typeof selectionBorderKeys)[number] + | 'selectionColor' + | 'selectionOpacity' + | 'selectionBorderColor' + | 'selectionBorderOpacity' + +let selectionMaterialConfig: PanelMaterialConfig | undefined function getSelectionMaterialConfig() { selectionMaterialConfig ??= createPanelMaterialConfig( { @@ -61,7 +69,7 @@ function getSelectionMaterialConfig() { } export function createSelection( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject, matrix: Signal, selectionTransformations: Signal>, isVisible: Signal, diff --git a/packages/uikit/src/text/font.ts b/packages/uikit/src/text/font.ts index 87883aa3..d443d1ab 100644 --- a/packages/uikit/src/text/font.ts +++ b/packages/uikit/src/text/font.ts @@ -5,6 +5,7 @@ import { loadCachedFont } from './cache.js' import { computedInheritableProperty } from '../properties/index.js' import { inter } from '@pmndrs/msdfonts' import { abortableEffect } from '../utils.js' +import { ReadonlyDeepSignalObject } from '../internals.js' export type FontFamilyWeightMap = Partial> @@ -32,13 +33,13 @@ const defaultFontFamilyUrls: FontFamilies = { } export function computedFont( - properties: Signal, + properties: ReadonlyDeepSignalObject, fontFamiliesSignal: Signal | undefined, renderer: WebGLRenderer, ): Signal { const result = signal(undefined) - const fontFamily = computedInheritableProperty(properties, 'fontFamily', undefined) - const fontWeight = computedInheritableProperty(properties, 'fontWeight', 'normal') + const fontFamily = computedInheritableProperty(properties, 'fontFamily', undefined) + const fontWeight = computedInheritableProperty(properties, 'fontWeight', 'normal') effect(() => { const fontFamilies = fontFamiliesSignal?.value ?? defaultFontFamilyUrls let resolvedFontFamily = fontFamily.value diff --git a/packages/uikit/src/text/layout.ts b/packages/uikit/src/text/layout.ts index d4e6e22a..15baa827 100644 --- a/packages/uikit/src/text/layout.ts +++ b/packages/uikit/src/text/layout.ts @@ -6,7 +6,7 @@ import { MeasureFunction, MeasureMode } from 'yoga-layout/load' import { MergedProperties } from '../properties/merged.js' import { readReactive } from '../utils.js' import { computedInheritableProperty } from '../properties/index.js' -import { CustomLayouting } from '../internals.js' +import { CustomLayouting, ReadonlyDeepSignalObject } from '../internals.js' export type GlyphLayoutLine = { charIndexOffset: number @@ -34,7 +34,7 @@ export type GlyphLayoutProperties = { } export function computedCustomLayouting( - properties: Signal, + properties: ReadonlyDeepSignalObject, fontSignal: Signal, textSignal: Signal | Array | unknown>>, propertiesRef: { current: GlyphLayoutProperties | undefined }, @@ -42,7 +42,7 @@ export function computedCustomLayouting( ) { const fontSize = computedInheritableProperty(properties, 'fontSize', 16) const letterSpacing = computedInheritableProperty(properties, 'letterSpacing', 0) - const lineHeight = computedInheritableProperty(properties, 'lineHeight', '120%') + const lineHeight = computedInheritableProperty(properties, 'lineHeight', '120%') const wordBreak = computedInheritableProperty(properties, 'wordBreak', defaultWordBreak) return computed(() => { const font = fontSignal.value diff --git a/packages/uikit/src/text/render/instanced-glyph.ts b/packages/uikit/src/text/render/instanced-glyph.ts index 1e44ba1f..2c30007e 100644 --- a/packages/uikit/src/text/render/instanced-glyph.ts +++ b/packages/uikit/src/text/render/instanced-glyph.ts @@ -13,7 +13,7 @@ const helperMatrix2 = new Matrix4() export type InstancedTextProperties = TextAlignProperties & TextAppearanceProperties & - Omit & + Partial> & FontFamilyProperties export function computedGylphGroupDependencies(fontSignal: Signal) { diff --git a/packages/uikit/src/text/render/instanced-text.ts b/packages/uikit/src/text/render/instanced-text.ts index de8b3e2c..34fca302 100644 --- a/packages/uikit/src/text/render/instanced-text.ts +++ b/packages/uikit/src/text/render/instanced-text.ts @@ -1,5 +1,5 @@ import { Signal, effect, signal } from '@preact/signals-core' -import { InstancedGlyph } from './instanced-glyph.js' +import { InstancedGlyph, InstancedTextProperties } from './instanced-glyph.js' import { Matrix4, Vector2Tuple } from 'three' import { ClippingRect } from '../../clipping.js' import { ColorRepresentation, abortableEffect, alignmentXMap, alignmentYMap } from '../../utils.js' @@ -18,6 +18,7 @@ import { Font } from '../font.js' import { MergedProperties, computedInheritableProperty } from '../../properties/index.js' import { FlexNode, FlexNodeState } from '../../flex/index.js' import { CaretTransformation } from '../../caret.js' +import { ReadonlyDeepSignalObject } from '../../internals.js' export type TextAlignProperties = { textAlign?: keyof typeof alignmentXMap | 'block' @@ -33,7 +34,13 @@ const defaultVerticalAlign: keyof typeof alignmentYMap = 'middle' const defaulttextAlign: keyof typeof alignmentXMap | 'block' = 'left' export function createInstancedText( - properties: Signal, + properties: ReadonlyDeepSignalObject< + InstancedTextProperties & { + depthTest?: boolean + depthWrite?: boolean + renderOrder?: number + } + >, textSignal: Signal | Array>>, matrix: Signal, node: Signal, @@ -94,9 +101,9 @@ export function createInstancedText( const instancedText = new InstancedText( glyphGroupManager.getGroup( orderInfo.value.majorIndex, - properties.value.read('depthTest', true), - properties.value.read('depthWrite', false), - properties.value.read('renderOrder', 0), + properties.depthTest ?? true, + properties.depthWrite ?? false, + properties.renderOrder ?? 0, font, ), textAlign, diff --git a/packages/uikit/src/transform.ts b/packages/uikit/src/transform.ts index d3bfb863..60b5e507 100644 --- a/packages/uikit/src/transform.ts +++ b/packages/uikit/src/transform.ts @@ -5,6 +5,8 @@ import { abortableEffect, alignmentXMap, alignmentYMap, percentageRegex } from ' import { MergedProperties } from './properties/merged.js' import { RootContext } from './context.js' import { computedInheritableProperty } from './properties/index.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from './components/utils.js' export type Percentage = `${number}%` @@ -41,7 +43,7 @@ const defaultTransformOriginX: keyof typeof alignmentXMap = 'center' const defaultTransformOriginY: keyof typeof alignmentYMap = 'center' export function computedTransformMatrix( - propertiesSignal: Signal, + propertiesSignal: ReadonlyDeepSignalObject, { relativeCenter, size }: FlexNodeState, pixelSizeSignal: Signal, ): Signal { @@ -50,22 +52,35 @@ export function computedTransformMatrix( //O = matrix to transform the origin for matrix T //T = transform matrix (translate, rotate, scale) - const tTX = computedInheritableProperty(propertiesSignal, 'transformTranslateX', 0) - const tTY = computedInheritableProperty(propertiesSignal, 'transformTranslateY', 0) - const tTZ = computedInheritableProperty(propertiesSignal, 'transformTranslateZ', 0) - const tRX = computedInheritableProperty(propertiesSignal, 'transformRotateX', 0) - const tRY = computedInheritableProperty(propertiesSignal, 'transformRotateY', 0) - const tRZ = computedInheritableProperty(propertiesSignal, 'transformRotateZ', 0) - const tSX = computedInheritableProperty(propertiesSignal, 'transformScaleX', 1) - const tSY = computedInheritableProperty(propertiesSignal, 'transformScaleY', 1) - const tSZ = computedInheritableProperty(propertiesSignal, 'transformScaleZ', 1) - const tOX = computedInheritableProperty(propertiesSignal, 'transformOriginX', defaultTransformOriginX) - const tOY = computedInheritableProperty(propertiesSignal, 'transformOriginY', defaultTransformOriginY) + // const tTX = computedInheritableProperty(propertiesSignal, 'transformTranslateX', 0) + // const tTY = computedInheritableProperty(propertiesSignal, 'transformTranslateY', 0) + // const tTZ = computedInheritableProperty(propertiesSignal, 'transformTranslateZ', 0) + // const tRX = computedInheritableProperty(propertiesSignal, 'transformRotateX', 0) + // const tRY = computedInheritableProperty(propertiesSignal, 'transformRotateY', 0) + // const tRZ = computedInheritableProperty(propertiesSignal, 'transformRotateZ', 0) + // const tSX = computedInheritableProperty(propertiesSignal, 'transformScaleX', 1) + // const tSY = computedInheritableProperty(propertiesSignal, 'transformScaleY', 1) + // const tSZ = computedInheritableProperty(propertiesSignal, 'transformScaleZ', 1) + // const tOX = computedInheritableProperty(propertiesSignal, 'transformOriginX', defaultTransformOriginX) + // const tOY = computedInheritableProperty(propertiesSignal, 'transformOriginY', defaultTransformOriginY) + + const tTX = computed(() => propertiesSignal.transformTranslateX ?? 0) + const tTY = computed(() => propertiesSignal.transformTranslateY ?? 0) + const tTZ = computed(() => propertiesSignal.transformTranslateZ ?? 0) + const tRX = computed(() => propertiesSignal.transformRotateX ?? 0) + const tRY = computed(() => propertiesSignal.transformRotateY ?? 0) + const tRZ = computed(() => propertiesSignal.transformRotateZ ?? 0) + const tSX = computed(() => propertiesSignal.transformScaleX ?? 1) + const tSY = computed(() => propertiesSignal.transformScaleY ?? 1) + const tSZ = computed(() => propertiesSignal.transformScaleZ ?? 1) + const tOX = computed(() => propertiesSignal.transformOriginX ?? defaultTransformOriginX) + const tOY = computed(() => propertiesSignal.transformOriginY ?? defaultTransformOriginY) return computed(() => { if (relativeCenter.value == null) { return undefined } + const [x, y] = relativeCenter.value const pixelSize = pixelSizeSignal.value const result = new Matrix4().makeTranslation(x * pixelSize, y * pixelSize, 0) diff --git a/packages/uikit/src/utils.ts b/packages/uikit/src/utils.ts index 3f59bdc8..92026fef 100644 --- a/packages/uikit/src/utils.ts +++ b/packages/uikit/src/utils.ts @@ -3,6 +3,8 @@ import { Vector2Tuple, Color, Vector3Tuple, Vector3 } from 'three' import { Inset } from './flex/node.js' import { MergedProperties } from './properties/merged.js' import { computedInheritableProperty } from './properties/index.js' +import { DeepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject } from './components/utils.js' export const percentageRegex = /(-?\d+(?:\.\d+)?)%/ @@ -78,10 +80,9 @@ export function createConditionalPropertyTranslator(condition: () => boolean) { } } -export function computedBorderInset( - propertiesSignal: Signal, - keys: ReadonlyArray, +export function computedBorderInset( + propertiesSignal: ReadonlyDeepSignalObject>>, + keys: ReadonlyArray, ): Signal { - const sizes = keys.map((key) => computedInheritableProperty(propertiesSignal, key, 0)) - return computed(() => sizes.map((size) => size.value) as Inset) + return computed(() => keys.map((key) => propertiesSignal[key] ?? 0) as Inset) } diff --git a/packages/uikit/src/vanilla/container.ts b/packages/uikit/src/vanilla/container.ts index 18e9d97f..50721205 100644 --- a/packages/uikit/src/vanilla/container.ts +++ b/packages/uikit/src/vanilla/container.ts @@ -3,11 +3,12 @@ import { AllOptionalProperties } from '../properties/default.js' import { Signal, effect, signal, untracked } from '@preact/signals-core' import { Parent, createParentContextSignal, setupParentContextSignal, bindHandlers } from './utils.js' import { ThreeEventMap } from '../events.js' +import { DeepSignal, deepSignal } from 'deepsignal/core' export class Container extends Parent { private readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal private readonly parentContextSignal = createParentContextSignal() private readonly unsubscribe: () => void @@ -17,8 +18,8 @@ export class Container extends super() this.matrixAutoUpdate = false setupParentContextSignal(this.parentContextSignal, this) - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) this.unsubscribe = effect(() => { const parentContext = this.parentContextSignal.value?.value if (parentContext == null) { @@ -57,7 +58,8 @@ export class Container extends } getComputedProperty>(key: K): ContainerProperties[K] | undefined { - return untracked(() => this.internals.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.internals.mergedProperties[key]) } getStyle(): undefined | Readonly> { @@ -69,11 +71,11 @@ export class Container extends } setProperties(properties: ContainerProperties | undefined) { - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/content.ts b/packages/uikit/src/vanilla/content.ts index 3b43288b..1ee2acff 100644 --- a/packages/uikit/src/vanilla/content.ts +++ b/packages/uikit/src/vanilla/content.ts @@ -1,17 +1,17 @@ import { Object3D, Object3DEventMap } from 'three' import { AllOptionalProperties } from '../properties/default.js' import { createParentContextSignal, setupParentContextSignal, bindHandlers, Component } from './utils.js' -import { ReadonlySignal, Signal, effect, signal, untracked } from '@preact/signals-core' -import { ContentProperties, setupContent, createContentState } from '../components/index.js' -import { MergedProperties } from '../properties/index.js' +import { DeepSignal, deepSignal } from 'deepsignal/core' +import { ContentProperties, setupContent, createContentState, ReadonlyDeepSignalObject } from '../components/index.js' import { ThreeEventMap } from '../events.js' +import { effect, signal, Signal, untracked } from '@preact/signals-core' export class Content extends Component { - private mergedProperties?: ReadonlySignal + private mergedProperties?: ReadonlyDeepSignalObject> private readonly contentContainer: Object3D private readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal private readonly parentContextSignal = createParentContextSignal() private readonly unsubscribe: () => void @@ -21,8 +21,8 @@ export class Content extends C super() this.matrixAutoUpdate = false setupParentContextSignal(this.parentContextSignal, this) - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) //setting up the threejs elements this.contentContainer = new Object3D() this.contentContainer.matrixAutoUpdate = false @@ -88,7 +88,8 @@ export class Content extends C } getComputedProperty>(key: K): ContentProperties[K] | undefined { - return untracked(() => this.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.mergedProperties?.[key]) } getStyle(): undefined | Readonly> { @@ -100,11 +101,11 @@ export class Content extends C } setProperties(properties: ContentProperties | undefined) { - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/custom.ts b/packages/uikit/src/vanilla/custom.ts index 70189827..693ab70e 100644 --- a/packages/uikit/src/vanilla/custom.ts +++ b/packages/uikit/src/vanilla/custom.ts @@ -4,13 +4,13 @@ import { createParentContextSignal, setupParentContextSignal, bindHandlers, Comp import { ReadonlySignal, Signal, effect, signal, untracked } from '@preact/signals-core' import { CustomContainerProperties, createCustomContainerState, setupCustomContainer } from '../components/index.js' import { panelGeometry } from '../panel/index.js' -import { MergedProperties } from '../properties/index.js' import { ThreeEventMap } from '../events.js' +import { DeepSignal, deepSignal } from 'deepsignal/core' export class CustomContainer extends Component { private readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal private readonly parentContextSignal = createParentContextSignal() private readonly unsubscribe: () => void private readonly material = new MeshBasicMaterial() @@ -22,8 +22,8 @@ export class CustomContainer e //TODO make the container the mesh this.matrixAutoUpdate = false setupParentContextSignal(this.parentContextSignal, this) - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) const mesh = new Mesh(panelGeometry, this.material) super.add(mesh) @@ -63,7 +63,8 @@ export class CustomContainer e getComputedProperty>( key: K, ): CustomContainerProperties[K] | undefined { - return untracked(() => this.internals.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.internals.mergedProperties[key]) } getStyle(): undefined | Readonly> { @@ -75,11 +76,11 @@ export class CustomContainer e } setProperties(properties: CustomContainerProperties | undefined) { - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/fullscreen.ts b/packages/uikit/src/vanilla/fullscreen.ts index a5c3d4d6..a5c23471 100644 --- a/packages/uikit/src/vanilla/fullscreen.ts +++ b/packages/uikit/src/vanilla/fullscreen.ts @@ -31,6 +31,7 @@ export class Fullscreen extend super( parentCameraSignal, renderer, + // @ts-expect-error { ...properties, sizeX, sizeY, pixelSize, transformTranslateZ }, defaultProperties, fontFamilies, @@ -85,9 +86,13 @@ export class Fullscreen extend setProperties(properties: FullscreenProperties | undefined): void { super.setProperties({ ...properties, + // @ts-expect-error sizeX: this.sizeX, + // @ts-expect-error sizeY: this.sizeY, + // @ts-expect-error pixelSize: this.pixelSize, + // @ts-expect-error transformTranslateZ: this.transformTranslateZ, }) } diff --git a/packages/uikit/src/vanilla/icon.ts b/packages/uikit/src/vanilla/icon.ts index 6be50a5a..1a6e8e87 100644 --- a/packages/uikit/src/vanilla/icon.ts +++ b/packages/uikit/src/vanilla/icon.ts @@ -4,11 +4,12 @@ import { ReadonlySignal, Signal, effect, signal, untracked } from '@preact/signa import { IconProperties, createIconState, setupIcon } from '../components/icon.js' import { MergedProperties } from '../properties/index.js' import { ThreeEventMap } from '../events.js' +import { deepSignal, DeepSignal } from 'deepsignal/core' export class Icon extends Component { private readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal private readonly parentContextSignal = createParentContextSignal() private readonly unsubscribe: () => void @@ -24,8 +25,8 @@ export class Icon extends Comp super() this.matrixAutoUpdate = false setupParentContextSignal(this.parentContextSignal, this) - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) this.unsubscribe = effect(() => { const parentContext = this.parentContextSignal.value?.value if (parentContext == null) { @@ -56,7 +57,8 @@ export class Icon extends Comp } getComputedProperty>(key: K): IconProperties[K] | undefined { - return untracked(() => this.internals.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.internals.mergedPropeties[key]) } getStyle(): undefined | Readonly> { @@ -68,11 +70,11 @@ export class Icon extends Comp } setProperties(properties: IconProperties | undefined) { - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/image.ts b/packages/uikit/src/vanilla/image.ts index 0662507d..fa432b35 100644 --- a/packages/uikit/src/vanilla/image.ts +++ b/packages/uikit/src/vanilla/image.ts @@ -3,11 +3,12 @@ import { AllOptionalProperties } from '../properties/default.js' import { Parent, createParentContextSignal, setupParentContextSignal, bindHandlers } from './utils.js' import { Signal, effect, signal, untracked } from '@preact/signals-core' import { ThreeEventMap } from '../events.js' +import { DeepSignal, deepSignal } from 'deepsignal/core' export class Image extends Parent { private readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal protected readonly parentContextSignal = createParentContextSignal() private readonly unsubscribe: () => void @@ -17,8 +18,8 @@ export class Image extends Par super() setupParentContextSignal(this.parentContextSignal, this) this.matrixAutoUpdate = false - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) this.unsubscribe = effect(() => { const parentContext = this.parentContextSignal.value?.value @@ -53,7 +54,8 @@ export class Image extends Par } getComputedProperty>(key: K): ImageProperties[K] | undefined { - return untracked(() => this.internals.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.internals.mergedPropeties[key]) } getStyle(): undefined | Readonly> { @@ -65,11 +67,11 @@ export class Image extends Par } setProperties(properties: ImageProperties | undefined) { - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/input.ts b/packages/uikit/src/vanilla/input.ts index 9192f991..0bf5f236 100644 --- a/packages/uikit/src/vanilla/input.ts +++ b/packages/uikit/src/vanilla/input.ts @@ -1,15 +1,17 @@ import { AllOptionalProperties } from '../properties/default.js' import { createParentContextSignal, setupParentContextSignal, bindHandlers, Component } from './utils.js' import { ReadonlySignal, Signal, effect, signal, untracked } from '@preact/signals-core' +import { DeepSignal, deepSignal } from 'deepsignal/core' import { InputProperties, createInputState, setupInput } from '../components/input.js' import { MergedProperties } from '../properties/index.js' import { ThreeEventMap } from '../events.js' +import { ReadonlyDeepSignalObject } from '../internals.js' export class Input extends Component { - private mergedProperties?: ReadonlySignal + private mergedProperties?: ReadonlyDeepSignalObject> private readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal private readonly parentContextSignal = createParentContextSignal() private readonly unsubscribe: () => void @@ -19,8 +21,8 @@ export class Input extends Com super() this.matrixAutoUpdate = false setupParentContextSignal(this.parentContextSignal, this) - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) this.unsubscribe = effect(() => { const parentContext = this.parentContextSignal.value?.value @@ -58,7 +60,8 @@ export class Input extends Com } getComputedProperty>(key: K): InputProperties[K] | undefined { - return untracked(() => this.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.internals.mergedProperties[key]) } getStyle(): undefined | Readonly> { @@ -70,11 +73,11 @@ export class Input extends Com } setProperties(properties: InputProperties | undefined) { - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/root.ts b/packages/uikit/src/vanilla/root.ts index d686e1c4..f7f29c97 100644 --- a/packages/uikit/src/vanilla/root.ts +++ b/packages/uikit/src/vanilla/root.ts @@ -6,11 +6,12 @@ import { Parent, bindHandlers } from './utils.js' import { readReactive } from '../utils.js' import { FontFamilies } from '../text/index.js' import { ThreeEventMap } from '../events.js' +import { DeepSignal, deepSignal } from 'deepsignal/core' export class Root extends Parent { protected readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal private readonly unsubscribe: () => void private readonly onFrameSet = new Set<(delta: number) => void>() private readonly fontFamiliesSignal: Signal @@ -30,8 +31,8 @@ export class Root extends Pare this.pixelSizeSignal = signal(properties?.pixelSize ?? DEFAULT_PIXEL_SIZE) this.matrixAutoUpdate = false this.fontFamiliesSignal = signal(fontFamilies) - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) this.unsubscribe = effect(() => { let getCamera: () => Camera if (typeof camera === 'function') { @@ -90,7 +91,8 @@ export class Root extends Pare } getComputedProperty>(key: K): RootProperties[K] | undefined { - return untracked(() => this.internals.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.internals.mergedPropeties[key]) } getStyle(): undefined | Readonly> { @@ -103,11 +105,11 @@ export class Root extends Pare setProperties(properties: (RootProperties & WithReactive<{ pixelSize?: number }>) | undefined) { this.pixelSizeSignal.value = properties?.pixelSize ?? DEFAULT_PIXEL_SIZE - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/svg.ts b/packages/uikit/src/vanilla/svg.ts index ece4eceb..6afddee3 100644 --- a/packages/uikit/src/vanilla/svg.ts +++ b/packages/uikit/src/vanilla/svg.ts @@ -1,15 +1,15 @@ import { AllOptionalProperties } from '../properties/default.js' import { Parent, createParentContextSignal, bindHandlers, setupParentContextSignal } from './utils.js' import { ReadonlySignal, Signal, effect, signal, untracked } from '@preact/signals-core' -import { SvgProperties, createSvgState, setupSvg } from '../components/index.js' -import { MergedProperties } from '../properties/index.js' +import { DeepSignal, deepSignal } from 'deepsignal/core' +import { ReadonlyDeepSignalObject, SvgProperties, createSvgState, setupSvg } from '../components/index.js' import { ThreeEventMap } from '../events.js' export class Svg extends Parent { - private mergedProperties?: ReadonlySignal + private mergedProperties?: ReadonlyDeepSignalObject> private readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal private readonly parentContextSignal = createParentContextSignal() private readonly unsubscribe: () => void @@ -19,8 +19,8 @@ export class Svg extends Paren super() this.matrixAutoUpdate = false setupParentContextSignal(this.parentContextSignal, this) - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) this.unsubscribe = effect(() => { const parentContext = this.parentContextSignal.value?.value @@ -60,7 +60,8 @@ export class Svg extends Paren } getComputedProperty>(key: K): SvgProperties[K] | undefined { - return untracked(() => this.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.internals.mergedProperties[key]) } getStyle(): undefined | Readonly> { @@ -72,11 +73,11 @@ export class Svg extends Paren } setProperties(properties: SvgProperties | undefined) { - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/text.ts b/packages/uikit/src/vanilla/text.ts index f5f3866f..2c050823 100644 --- a/packages/uikit/src/vanilla/text.ts +++ b/packages/uikit/src/vanilla/text.ts @@ -1,15 +1,17 @@ import { AllOptionalProperties } from '../properties/default.js' import { createParentContextSignal, setupParentContextSignal, bindHandlers, Component } from './utils.js' import { ReadonlySignal, Signal, effect, signal, untracked } from '@preact/signals-core' +import { DeepSignal, deepSignal } from 'deepsignal/core' import { TextProperties, createTextState, setupText } from '../components/text.js' import { MergedProperties } from '../properties/index.js' import { ThreeEventMap } from '../events.js' +import { ReadonlyDeepSignalObject } from '../internals.js' export class Text extends Component { - private mergedProperties?: ReadonlySignal + private mergedProperties?: ReadonlyDeepSignalObject> private readonly styleSignal: Signal | undefined> = signal(undefined) - private readonly propertiesSignal: Signal | undefined> - private readonly defaultPropertiesSignal: Signal + private readonly propertiesSignal: DeepSignal> + private readonly defaultPropertiesSignal: DeepSignal private readonly textSignal: Signal | Array>> private readonly parentContextSignal = createParentContextSignal() private readonly unsubscribe: () => void @@ -24,8 +26,8 @@ export class Text extends Comp super() this.matrixAutoUpdate = false setupParentContextSignal(this.parentContextSignal, this) - this.propertiesSignal = signal(properties) - this.defaultPropertiesSignal = signal(defaultProperties) + this.propertiesSignal = deepSignal(properties ?? {}) + this.defaultPropertiesSignal = deepSignal(defaultProperties ?? {}) this.textSignal = signal(text) this.unsubscribe = effect(() => { @@ -61,7 +63,8 @@ export class Text extends Comp } getComputedProperty>(key: K): TextProperties[K] | undefined { - return untracked(() => this.mergedProperties?.value.read(key as string, undefined)) + // @ts-expect-error + return untracked(() => this.mergedProperties?.[key]) } getStyle(): undefined | Readonly> { @@ -73,11 +76,11 @@ export class Text extends Comp } setProperties(properties: TextProperties | undefined) { - this.propertiesSignal.value = properties + Object.assign(this.propertiesSignal, properties) } setDefaultProperties(properties: AllOptionalProperties) { - this.defaultPropertiesSignal.value = properties + Object.assign(this.defaultPropertiesSignal, properties) } destroy() { diff --git a/packages/uikit/src/vanilla/video.ts b/packages/uikit/src/vanilla/video.ts index ef435094..6c130803 100644 --- a/packages/uikit/src/vanilla/video.ts +++ b/packages/uikit/src/vanilla/video.ts @@ -23,6 +23,7 @@ export class Video extends Ima const texture = new VideoTexture(element) texture.needsUpdate = true const aspectRatio = signal(1) + // @ts-expect-error super({ aspectRatio, ...props, src: texture }, defaultProperties) this.unsubscribeInvalidate = effect(() => { @@ -44,6 +45,7 @@ export class Video extends Ima setProperties(props: VideoProperties & ImageProperties): void { updateVideoElement(this.element, props) super.setProperties({ + // @ts-expect-error aspectRatio: this.aspectRatio, ...props, src: this.texture, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5375954..1d7e6c0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -333,6 +333,9 @@ importers: '@react-three/uikit-lucide': specifier: workspace:^ version: link:../../packages/icons/lucide/react + deepsignal: + specifier: ^1.6.0 + version: 1.6.0(@preact/signals-core@1.8.0) vite-plugin-mkcert: specifier: ^1.17.4 version: 1.17.6(vite@5.4.14(@types/node@20.17.16)) @@ -478,6 +481,9 @@ importers: commander: specifier: ^12.0.0 version: 12.1.0 + deepsignal: + specifier: ^1.6.0 + version: 1.6.0(@preact/signals-core@1.8.0) ora: specifier: ^8.0.1 version: 8.1.1 @@ -515,6 +521,9 @@ importers: '@preact/signals-core': specifier: ^1.5.1 version: 1.8.0 + deepsignal: + specifier: ^1.6.0 + version: 1.6.0(@preact/signals-core@1.8.0) inline-style-parser: specifier: ^0.2.3 version: 0.2.4 @@ -3327,6 +3336,23 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepsignal@1.6.0: + resolution: {integrity: sha512-oplhhOSfBRKmx96B0PzbhRaX2W7iDpH6BEqvyGLIZPO2pdHQD+/P3u4Z8wi/sKXjCE7ht8C7ULTEzLSJbgwccA==} + peerDependencies: + '@preact/signals': ^1.1.4 || ^2.0.0 + '@preact/signals-core': ^1.5.1 + '@preact/signals-react': ^1.3.8 || ^2.0.0 || ^3.0.0 + preact: ^10.16.0 + peerDependenciesMeta: + '@preact/signals': + optional: true + '@preact/signals-core': + optional: true + '@preact/signals-react': + optional: true + preact: + optional: true + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -4851,6 +4877,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} @@ -9719,6 +9746,10 @@ snapshots: deep-is@0.1.4: {} + deepsignal@1.6.0(@preact/signals-core@1.8.0): + optionalDependencies: + '@preact/signals-core': 1.8.0 + defaults@1.0.4: dependencies: clone: 1.0.4