UNPKG

@pmndrs/uikit

Version:

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

255 lines (254 loc) 9.93 kB
import { Signal, computed } from '@preact/signals-core'; import { BufferGeometry, Color, Material, Mesh } from 'three'; import { addActiveHandlers } from '../active.js'; import { addHoverHandlers } from '../hover.js'; import { abortableEffect, readReactive } from '../utils.js'; import { FlexNode } from '../flex/index.js'; import { MergedProperties, computedInheritableProperty, } from '../properties/index.js'; export function disposeGroup(object) { object?.traverse((mesh) => { if (!(mesh instanceof Mesh)) { return; } if (mesh.material instanceof Material) { mesh.material.dispose(); } if (mesh.geometry instanceof BufferGeometry) { mesh.geometry.dispose(); } }); } export function computedGlobalMatrix(parentMatrix, localMatrix) { return computed(() => { const local = localMatrix.value; const parent = parentMatrix.value; if (local == null || parent == null) { return undefined; } return parent.clone().multiply(local); }); } export function computedIsVisible(flexState, isClipped, mergedProperties) { return computed(() => flexState.displayed.value && (isClipped == null || !isClipped?.value) && mergedProperties.value.read('visibility', 'visible') === 'visible'); } export function loadResourceWithParams(target, fn, cleanup, abortSignal, param, ...additionals) { if (!(param instanceof Signal)) { fn(param, ...additionals).then((value) => (abortSignal.aborted ? undefined : (target.value = value))); return; } abortableEffect(() => { let canceled = false; fn(param.value, ...additionals) .then((value) => (canceled ? undefined : (target.value = value))) .catch(console.error); return () => (canceled = true); }, abortSignal); if (cleanup != null) { abortSignal.addEventListener('abort', () => { const { value } = target; if (value == null) { return; } cleanup(value); }); } } export function setupNode(state, parentContext, object, objectVisibleDefault, abortSignal) { const node = new FlexNode(state, state.mergedProperties, object, objectVisibleDefault, abortSignal); if (parentContext != null) { abortableEffect(() => { const { value: parentNode } = parentContext.node; if (parentNode == null) { return; } parentNode.addChild(node); return () => parentNode.removeChild(node); }, abortSignal); } return (state.node.value = node); } const signalMap = new Map(); export const keepAspectRatioPropertyTransformer = { keepAspectRatio: (value, target) => { let signal = signalMap.get(value); if (signal == null) { //if keep aspect ratio is "false" => we write "null" => which overrides the previous properties and returns null signalMap.set(value, (signal = computed(() => (readReactive(value) === false ? null : undefined)))); } target.add('aspectRatio', signal); }, }; const eventHandlerKeys = [ 'onClick', 'onContextMenu', 'onDoubleClick', 'onPointerCancel', 'onPointerDown', 'onPointerEnter', 'onPointerLeave', 'onPointerMove', 'onPointerOut', 'onPointerOver', 'onPointerUp', 'onWheel', ]; export function computedHandlers(style, propertiesSignal, defaultProperties, hoveredSignal, activeSignal, dynamicHandlers, defaultCursor) { return computed(() => { const handlers = {}; const properties = propertiesSignal.value; if (properties != null) { for (const key of eventHandlerKeys) { const handler = properties[key]; if (handler != null) { handlers[key] = handler; } } } addHandlers(handlers, dynamicHandlers?.value); addHoverHandlers(handlers, style.value, propertiesSignal.value, defaultProperties.value, hoveredSignal, defaultCursor); addActiveHandlers(handlers, style.value, propertiesSignal.value, defaultProperties.value, activeSignal); return handlers; }); } export function computedAncestorsHaveListeners(parentContext, handlers) { return computed(() => (parentContext?.ancestorsHaveListeners.value ?? false) || Object.keys(handlers.value).length > 0); } export function addHandlers(target, handlers) { for (const key in handlers) { addHandler(key, target, handlers[key]); } } export function addHandler(key, target, handler) { if (handler == null) { return; } const existingHandler = target[key]; if (existingHandler == null) { target[key] = handler; return; } target[key] = ((e) => { existingHandler(e); if ('stopped' in e && e.stopped) { return; } handler(e); }); } export function computedMergedProperties(style, properties, defaultProperties, postTransformers, preTransformers, onInit) { return computed(() => { const merged = new MergedProperties(preTransformers); onInit?.(merged); merged.addAll(style.value, properties.value, defaultProperties.value, postTransformers); return merged; }); } const colorHelper = new Color(); /** * @requires that each mesh inside the group has its default color stored inside object.userData.color */ export function applyAppearancePropertiesToGroup(propertiesSignal, group, abortSignal) { abortableEffect(() => { const properties = propertiesSignal.value; const color = properties.read('color', undefined); let c; if (Array.isArray(color)) { 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); readReactive(group)?.traverse((mesh) => { if (!(mesh instanceof Mesh)) { return; } mesh.renderOrder = renderOrder; const material = mesh.material; material.color.copy(c ?? mesh.userData.color); material.opacity = opacity; material.depthTest = depthTest; material.depthWrite = depthWrite; }); }, abortSignal); } export function computeMatrixWorld(target, localMatrix, rootObjectMatrixWorld, globalMatrixSignal) { const globalMatrix = globalMatrixSignal.peek(); if (globalMatrix == null) { return false; } target.multiplyMatrices(rootObjectMatrixWorld, globalMatrix); if (localMatrix != null) { target.multiply(localMatrix); } return true; } export function setupMatrixWorldUpdate(updateMatrixWorld, updateChildrenMatrixWorld, object, rootContext, globalMatrixSignal, useOwnMatrix, abortSignal) { abortableEffect(() => { if (updateMatrixWorld != true && !updateMatrixWorld.value) { return; } const onFrame = () => { const rootObject = rootContext.objectRef; if (object == null || rootObject.current == null) { return; } computeMatrixWorld(object.matrixWorld, useOwnMatrix ? object.matrix : undefined, rootObject.current.matrixWorld, globalMatrixSignal); if (!updateChildrenMatrixWorld) { return; } const length = object.children.length; for (let i = 0; i < length; i++) { object.children[i].updateMatrixWorld(true); } }; rootContext.onUpdateMatrixWorldSet.add(onFrame); return () => rootContext.onUpdateMatrixWorldSet.delete(onFrame); }, abortSignal); } export function computeDefaultProperties(propertiesSignal) { return { 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), }; } export function setupPointerEvents(propertiesSignal, ancestorsHaveListeners, rootContext, target, canHaveNonUikitChildren, abortSignal) { 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); }, abortSignal); abortableEffect(() => { if (!canHaveNonUikitChildren && propertiesSignal.value.read('pointerEvents', undefined) === 'none') { return; } const descendants = rootContext.interactableDescendants; if (descendants == null || target == null) { return; } descendants.push(target); return () => { const index = descendants.indexOf(target); if (index === -1) { return; } descendants.splice(index, 1); }; }, abortSignal); }