@pmndrs/uikit
Version:
Build performant 3D user interfaces with Three.js and yoga.
263 lines (262 loc) • 12.2 kB
JavaScript
import { computed, signal } from '@preact/signals-core';
import { Matrix4, Mesh, Sphere, } from 'three';
import { abortableEffect, computedAncestorsHaveListeners, computedGlobalMatrix, computedHandlers, computedIsVisible, computeWorldToGlobalMatrix, setupPointerEvents, } from '../utils.js';
import { computedPanelMatrix, InstancedPanelMesh, makeClippedCast, makePanelSpherecast, panelGeometry, setupBoundingSphere, } from '../panel/index.js';
import { Overflow } from 'yoga-layout/load';
import { computedIsClipped } from '../clipping.js';
import { FlexNode } from '../flex/node.js';
import { allAliases } from '../properties/alias.js';
import { createConditionals } from '../properties/conditional.js';
import { PropertiesImplementation, } from '../properties/index.js';
import { computedTransformMatrix } from '../transform.js';
import { setupCursorCleanup } from '../hover.js';
import { ClassList, getStarProperties, StyleSheet } from './classes.js';
import { InstancedGlyphMesh } from '../text/index.js';
import { buildRootContext, buildRootMatrix } from '../context.js';
import { inheritedPropertyKeys } from '../properties/inheritance.js';
import { componentDefaults } from '../properties/defaults.js';
import { getLayerIndex } from '../properties/layer.js';
const IdentityMatrix = new Matrix4();
const sphereHelper = new Sphere();
const worldToGlobalMatrixHelper = new Matrix4();
const returnFalseFunction = () => false;
let currentGlobalProperties;
const baseLayerIndex = getLayerIndex({ type: 'base', section: 'base' });
export function resetGlobalProperties(properties) {
currentGlobalProperties = properties;
globalProperties.setLayer(baseLayerIndex, currentGlobalProperties);
}
export function setGlobalProperties(properties) {
resetGlobalProperties({
...properties,
...currentGlobalProperties,
});
}
const globalProperties = new PropertiesImplementation(allAliases, new Proxy({}, { get: () => returnFalseFunction }));
globalProperties.setEnabled(true);
export class Component extends Mesh {
inputProperties;
abortController = new AbortController();
handlers;
orderInfo = signal(undefined);
isVisible;
isClipped;
boundingSphere = new Sphere();
/**
* the properties of the this component
* e.g. get the final computed backgroundColor using `component.properties.value.backgroundColor`
*/
properties;
starProperties;
node;
size = signal(undefined);
relativeCenter = signal(undefined);
borderInset = signal(undefined);
overflow = signal(Overflow.Visible);
displayed = signal(false);
scrollable = signal([false, false]);
paddingInset = signal(undefined);
maxScrollPosition = signal([undefined, undefined]);
root;
parentContainer = signal(undefined);
hoveredList = signal([]);
activeList = signal([]);
ancestorsHaveListenersSignal;
globalMatrix;
globalPanelMatrix;
abortSignal = this.abortController.signal;
classList;
constructor(inputProperties, initialClasses, config) {
super(panelGeometry, config?.material);
this.inputProperties = inputProperties;
this.matrixAutoUpdate = false;
//setting up the parent signal
const updateParentState = () => {
this.parentContainer.value = this.parent instanceof Component ? this.parent : undefined;
this.properties.setEnabled(this.parent != null);
this.starProperties.setEnabled(this.parent != null);
};
this.addEventListener('added', updateParentState);
this.addEventListener('removed', updateParentState);
this.root = buildRootContext(this, config?.renderContext);
//properties
const conditionals = createConditionals(this.root, this.hoveredList, this.activeList, config?.hasFocus, config?.isPlaceholder);
this.properties = new PropertiesImplementation(allAliases, conditionals, config?.defaults ?? componentDefaults);
this.properties.setLayersWithConditionals({ type: 'default-overrides' }, {
width: computed(() => {
const sizeX = this.properties.value.sizeX;
if (sizeX == null) {
return undefined;
}
return sizeX / this.properties.value.pixelSize;
}),
height: computed(() => {
const sizeY = this.properties.value.sizeY;
if (sizeY == null) {
return undefined;
}
return sizeY / this.properties.value.pixelSize;
}),
...config?.defaultOverrides,
});
abortableEffect(() => {
const parentProprties = this.parentContainer.value?.properties;
const layerIndex = getLayerIndex({ type: 'inheritance' });
const cleanup = parentProprties?.subscribePropertyKeys((key) => {
if (!inheritedPropertyKeys.includes(key)) {
return;
}
const signal = parentProprties.signal[key];
this.properties.set(layerIndex, key, signal);
});
return () => {
cleanup?.();
this.properties.setLayer(layerIndex, undefined);
};
}, this.abortSignal);
this.starProperties = new PropertiesImplementation(allAliases, conditionals);
this.starProperties.setLayersWithConditionals({ type: 'default-overrides' }, getStarProperties(config?.defaultOverrides));
abortableEffect(() => {
const parentStarProprties = this.parentContainer.value?.starProperties ?? globalProperties;
const layerIndex = getLayerIndex({ type: 'star-inheritance' });
const cleanup = parentStarProprties?.subscribePropertyKeys((key) => {
const signal = parentStarProprties.signal[key];
this.starProperties.set(layerIndex, key, signal);
this.properties.set(layerIndex, key, signal);
});
return () => {
cleanup?.();
this.properties.setLayer(layerIndex, undefined);
this.starProperties.setLayer(layerIndex, undefined);
};
}, this.abortSignal);
this.resetProperties(inputProperties);
this.classList = new ClassList(this.properties, this.starProperties);
if (initialClasses != null) {
this.classList.add(...initialClasses);
}
abortableEffect(() => {
const elementId = this.properties.value.id;
if (elementId == null) {
return;
}
const idClassName = `__id__${elementId}`;
if (!(idClassName in StyleSheet)) {
return;
}
this.classList.add(idClassName);
return () => this.classList.remove(idClassName);
}, this.abortSignal);
this.node = new FlexNode(this);
this.globalMatrix = computedGlobalMatrix(computed(() => this.parentContainer.value?.childrenMatrix.value ?? buildRootMatrix(this.properties, this.size)), computedTransformMatrix(this));
this.isClipped = computedIsClipped(this.parentContainer, this.globalMatrix, this.size, this.properties.signal.pixelSize);
this.isVisible = computedIsVisible(this, this.isClipped, this.properties);
this.handlers = computedHandlers(this.properties, this.starProperties, this.hoveredList, this.activeList, config?.dynamicHandlers);
this.ancestorsHaveListenersSignal = computedAncestorsHaveListeners(this.parentContainer, this.handlers);
this.globalPanelMatrix = computedPanelMatrix(this.properties, this.globalMatrix, this.size, undefined);
this.raycast = makeClippedCast(this, this.raycast.bind(this), this.root, this.parentContainer, this.orderInfo);
this.spherecast = makeClippedCast(this, makePanelSpherecast(this.root, this.boundingSphere, this.globalPanelMatrix, this), this.root, this.parentContainer, this.orderInfo);
setupCursorCleanup(this.hoveredList, this.abortSignal);
setupBoundingSphere(this.boundingSphere, this.properties.signal.pixelSize, this.globalMatrix, this.size, this.abortSignal);
const hasNonUikitChildren = config?.hasNonUikitChildren ?? true;
setupPointerEvents(this, hasNonUikitChildren);
abortableEffect(() => {
const { value } = this.handlers;
for (const key in value) {
this.addEventListener(keyToEventName(key), value[key]);
}
return () => {
for (const key in value) {
this.removeEventListener(keyToEventName(key), value[key]);
}
};
}, this.abortSignal);
if (!hasNonUikitChildren) {
//only uikit children allowed - throw when non uikit child is added
const listener = ({ child }) => {
if (child instanceof Component || child instanceof InstancedPanelMesh || child instanceof InstancedGlyphMesh) {
return;
}
throw new Error(`Only pmndrs/uikit components can be added as children to this component. Got ${child.constructor.name} instead.`);
};
this.addEventListener('childadded', listener);
this.abortSignal.addEventListener('abort', () => this.removeEventListener('childadded', listener));
}
}
raycast(raycaster, intersects) {
this.root.peek().component.updateMatrix();
computeWorldToGlobalMatrix(this.root.peek(), worldToGlobalMatrixHelper);
sphereHelper.copy(this.boundingSphere).applyMatrix4(worldToGlobalMatrixHelper);
if (!raycaster.ray.intersectsSphere(sphereHelper)) {
return false;
}
this.updateWorldMatrix(false, false);
super.raycast(raycaster, intersects);
return false;
}
updateMatrixWorld() {
this.updateWorldMatrix(false, true);
}
updateWorldMatrix(updateParents, updateChildren) {
const root = this.root.peek().component;
const rootParent = root.parent;
if (updateParents) {
rootParent?.updateWorldMatrix(true, false);
}
if (this === root) {
root.updateMatrix();
}
computeWorldToGlobalMatrix(this.root.peek(), worldToGlobalMatrixHelper);
this.matrixWorld.multiplyMatrices(worldToGlobalMatrixHelper, this.globalPanelMatrix.peek() ?? IdentityMatrix);
if (updateChildren && this.root.peek().component === this) {
for (const update of this.root.value.onUpdateMatrixWorldSet) {
update();
}
}
}
/**
* allows to extending the existing properties
*/
setProperties(inputProperties) {
this.resetProperties({
...this.inputProperties,
...inputProperties,
});
}
/**
* allows to overwrite the properties
*/
resetProperties(inputProperties) {
this.inputProperties = inputProperties;
this.properties.setLayersWithConditionals({ type: 'base' }, inputProperties);
this.starProperties.setLayersWithConditionals({ type: 'base' }, getStarProperties(inputProperties));
}
/**
* must only be called for the root component; the component that has a non-uikit component as a parent
*/
update(delta) {
const root = this.root.peek();
if (root.component != this) {
//we only call .update on the root component => if not the root component return
return;
}
root.isUpdateRunning = true;
for (const onFrame of this.root.peek().onFrameSet) {
onFrame(delta);
}
root.isUpdateRunning = false;
}
dispose() {
this.parent?.remove(this);
this.abortController.abort();
}
/**
* only used for internally adding instanced panel group and instanced gylph group in case this component is a root component
*/
addUnsafe(...object) {
return super.add(...object);
}
}
function keyToEventName(key) {
return key.slice(2).toLowerCase();
}