@pmndrs/uikit
Version:
Build performant 3D user interfaces with Three.js and yoga.
175 lines (174 loc) • 8.95 kB
JavaScript
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));
});
}