UNPKG

@pmndrs/uikit

Version:

Build performant 3D user interfaces with Three.js and yoga.

175 lines (174 loc) 8.95 kB
import { computed, signal } from '@preact/signals-core'; import { createFlexNodeState } from '../flex/index.js'; import { setupLayoutListeners } from '../listeners.js'; import { setupInstancedPanel } from '../panel/instanced-panel.js'; import { PanelGroupManager, computedPanelGroupDependencies, } from '../panel/instanced-panel-group.js'; import { createScrollPosition, setupScrollbars, computedScrollHandlers, createScrollState, setupScroll, computedGlobalScrollMatrix, } from '../scroll.js'; import { setupObjectTransform, computedTransformMatrix } from '../transform.js'; import { alignmentXMap, alignmentYMap, readReactive } from '../utils.js'; import { computeDefaultProperties, computedHandlers, computedIsVisible, computedMergedProperties, setupMatrixWorldUpdate, setupPointerEvents, computedAncestorsHaveListeners, setupNode, } from './utils.js'; import { computedClippingRect } from '../clipping.js'; import { ElementType, computedOrderInfo } from '../order.js'; import { Matrix4, Plane, Vector3 } from 'three'; import { GlyphGroupManager } from '../text/render/instanced-glyph-group.js'; import { createActivePropertyTransfomers } from '../active.js'; import { 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 { computedInheritableProperty } from '../properties/index.js'; import { getDefaultPanelMaterialConfig } from '../panel/index.js'; export const DEFAULT_PIXEL_SIZE = 0.01; const vectorHelper = new Vector3(); const planeHelper = new Plane(); export function createRootState(objectRef, pixelSize, style, properties, defaultProperties, getCamera, renderer, onFrameSet, requestRender, requestFrame) { const hoveredSignal = signal([]); const activeSignal = signal([]); const interactableDescendants = []; 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 ctx = { onFrameSet, requestRender, requestFrame, pixelSize, }; const transformMatrix = computedTransformMatrix(mergedProperties, flexState, pixelSize); const globalMatrix = computedRootMatrix(mergedProperties, transformMatrix, flexState.size, pixelSize); const groupDeps = computedPanelGroupDependencies(mergedProperties); const orderInfo = computedOrderInfo(undefined, 'zIndexOffset', ElementType.Panel, groupDeps, undefined); const isVisible = computedIsVisible(flexState, undefined, mergedProperties); const scrollPosition = createScrollPosition(); const childrenMatrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, pixelSize); const scrollbarWidth = computedInheritableProperty(mergedProperties, 'scrollbarWidth', 10); const updateMatrixWorld = computedInheritableProperty(mergedProperties, 'updateMatrixWorld', false); const root = Object.assign(ctx, { objectInvertedWorldMatrix: new Matrix4(), rayInGlobalSpaceMap: new Map(), interactableDescendants, onUpdateMatrixWorldSet: new Set(), requestCalculateLayout: () => { }, objectRef, gylphGroupManager: new GlyphGroupManager(ctx, objectRef), panelGroupManager: new PanelGroupManager(ctx, objectRef), renderer, size: flexState.size, }); const componentState = Object.assign(flexState, { interactionPanel: createInteractionPanel(orderInfo, root, undefined, globalMatrix, flexState), root, scrollState: createScrollState(), anyAncestorScrollable: signal([false, false]), hoveredSignal, activeSignal, mergedProperties, transformMatrix, globalMatrix, groupDeps, orderInfo, isVisible, scrollPosition, childrenMatrix, scrollbarWidth, updateMatrixWorld, defaultProperties: computeDefaultProperties(mergedProperties), renderer, getCamera, }); const scrollHandlers = computedScrollHandlers(componentState, properties, objectRef); const handlers = computedHandlers(style, properties, defaultProperties, hoveredSignal, activeSignal, scrollHandlers); const ancestorsHaveListeners = computedAncestorsHaveListeners(undefined, handlers); return Object.assign(componentState, { clippingRect: computedClippingRect(globalMatrix, componentState, ctx.pixelSize, undefined), handlers, ancestorsHaveListeners, }); } export function setupRoot(state, style, properties, object, childrenContainer, abortSignal) { state.root.gylphGroupManager.init(abortSignal); state.root.panelGroupManager.init(abortSignal); object.interactableDescendants = state.root.interactableDescendants; setupCursorCleanup(state.hoveredSignal, abortSignal); const node = setupNode(state, undefined, object, true, abortSignal); state.root.requestCalculateLayout = createDeferredRequestLayoutCalculation(state.root, node, abortSignal); setupObjectTransform(state.root, object, state.transformMatrix, abortSignal); const onFrame = () => void (state.root.reversePainterSortStableCache = undefined); state.root.onFrameSet.add(onFrame); abortSignal.addEventListener('abort', () => state.root.onFrameSet.delete(onFrame)); setupInstancedPanel(state.mergedProperties, state.orderInfo, state.groupDeps, state.root.panelGroupManager, state.globalMatrix, state.size, undefined, state.borderInset, undefined, state.isVisible, getDefaultPanelMaterialConfig(), abortSignal); setupScroll(state, properties, state.root.pixelSize, childrenContainer, abortSignal); setupScrollbars(state.mergedProperties, state.scrollPosition, state, state.globalMatrix, state.isVisible, undefined, state.orderInfo, state.groupDeps, state.root.panelGroupManager, state.scrollbarWidth, abortSignal); setupLayoutListeners(style, properties, state.size, abortSignal); setupInteractionPanel(state.interactionPanel, state.root, state.globalMatrix, state.size, abortSignal); childrenContainer.updateMatrixWorld = function () { if (this.parent == null) { this.matrixWorld.copy(this.matrix); } else { this.matrixWorld.multiplyMatrices(this.parent.matrixWorld, this.matrix); } for (const update of state.root.onUpdateMatrixWorldSet) { update(); } }; setupMatrixWorldUpdate(state.updateMatrixWorld, false, state.interactionPanel, state.root, state.globalMatrix, true, abortSignal); setupPointerEvents(state.mergedProperties, state.ancestorsHaveListeners, state.root, state.interactionPanel, false, abortSignal); } function createDeferredRequestLayoutCalculation(root, node, abortSignal) { let requested = true; const onFrame = () => { if (!requested) { return; } requested = false; node.calculateLayout(); }; root.onFrameSet.add(onFrame); abortSignal.addEventListener('abort', () => root.onFrameSet.delete(onFrame)); return () => { requested = true; root.requestFrame(); }; } function createSizeTranslator(pixelSize, key, to) { const map = new Map(); return { [key]: (value, target) => { let entry = map.get(value); if (entry == null) { map.set(value, (entry = computed(() => { const s = readReactive(value); if (s == null) { return undefined; } return s / pixelSize.value; }))); } target.add(to, entry); }, }; } const matrixHelper = new Matrix4(); const defaultAnchorX = 'center'; const defaultAnchorY = 'center'; function computedRootMatrix(propertiesSignal, matrix, size, pixelSize) { const anchorX = computedInheritableProperty(propertiesSignal, 'anchorX', defaultAnchorX); const anchorY = computedInheritableProperty(propertiesSignal, 'anchorY', defaultAnchorY); return computed(() => { if (size.value == null) { return undefined; } const [width, height] = size.value; return matrix.value ?.clone() .premultiply(matrixHelper.makeTranslation(alignmentXMap[anchorX.value] * width * pixelSize.value, alignmentYMap[anchorY.value] * height * pixelSize.value, 0)); }); }