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