UNPKG

@pmndrs/uikit

Version:

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

213 lines (212 loc) 11 kB
import { computed, signal } from '@preact/signals-core'; import { Box3, Color, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from 'three'; import { ElementType, setupOrderInfo, setupRenderOrder } from '../order.js'; import { setupInstancedPanel } from '../panel/instanced-panel.js'; import { getDefaultPanelMaterialConfig, writeColor } from '../panel/panel-material.js'; import { Component } from './component.js'; import { computedPanelGroupDependencies } from '../panel/instanced-panel-group.js'; import { abortableEffect, alignmentZMap, computeWorldToGlobalMatrix, setupMatrixWorldUpdate } from '../utils.js'; import { createGlobalClippingPlanes } from '../clipping.js'; import { makeClippedCast } from '../panel/interaction-panel-mesh.js'; import { InstancedGlyphMesh, toAbsoluteNumber } from '../text/index.js'; import { InstancedPanelMesh } from '../panel/instanced-panel-mesh.js'; import { componentDefaults } from '../properties/defaults.js'; export const contentDefaults = { ...componentDefaults, depthAlign: 'back', keepAspectRatio: true, }; const IdentityQuaternion = new Quaternion(); const IdentityMatrix = new Matrix4(); const box3Helper = new Box3(); const smallValue = new Vector3().setScalar(0.000001); const positionHelper = new Vector3(); const scaleHelper = new Vector3(); const vectorHelper = new Vector3(); const RemeasureOnChildrenChangeDefault = true; const DepthWriteDefaultDefault = true; const SupportFillPropertyDefault = false; export class Content extends Component { config; boundingBox; clippingPlanes; childrenMatrix = new Matrix4(); constructor(inputProperties, initialClasses, config) { const defaultAspectRatio = signal(undefined); super(inputProperties, initialClasses, { defaults: contentDefaults, hasNonUikitChildren: true, ...config, defaultOverrides: { aspectRatio: defaultAspectRatio, ...config?.defaultOverrides }, }); this.config = config; this.boundingBox = config?.boundingBox ?? signal({ size: new Vector3(1, 1.01, 1), center: new Vector3(0, 0, 0) }); abortableEffect(() => { if (!this.properties.value.keepAspectRatio || this.boundingBox.value == null) { defaultAspectRatio.value = undefined; return; } defaultAspectRatio.value = this.boundingBox.value.size.x / this.boundingBox.value.size.y; }, this.abortSignal); this.material.visible = false; const panelGroupDeps = computedPanelGroupDependencies(this.properties); const backgroundOrderInfo = signal(); setupOrderInfo(backgroundOrderInfo, this.properties, 'zIndex', ElementType.Panel, panelGroupDeps, computed(() => (this.parentContainer.value == null ? null : this.parentContainer.value.orderInfo.value)), this.abortSignal); setupInstancedPanel(this.properties, this.root, backgroundOrderInfo, panelGroupDeps, this.globalPanelMatrix, this.size, this.borderInset, computed(() => this.parentContainer.value?.clippingRect.value), this.isVisible, getDefaultPanelMaterialConfig(), this.abortSignal); abortableEffect(() => { if (this.size.value == null || this.paddingInset.value == null || this.borderInset.value == null || this.boundingBox.value == null) { this.childrenMatrix.copy(IdentityMatrix); return; } const [width, height] = this.size.value; const [pTop, pRight, pBottom, pLeft] = this.paddingInset.value; const [bTop, bRight, bBottom, bLeft] = this.borderInset.value; const topInset = pTop + bTop; const rightInset = pRight + bRight; const bottomInset = pBottom + bBottom; const leftInset = pLeft + bLeft; const innerWidth = width - leftInset - rightInset; const innerHeight = height - topInset - bottomInset; const pixelSize = this.properties.value.pixelSize; scaleHelper .set(innerWidth * pixelSize, innerHeight * pixelSize, this.properties.value.keepAspectRatio ? (innerHeight * pixelSize * this.boundingBox.value.size.z) / this.boundingBox.value.size.y : this.boundingBox.value.size.z) .divide(this.boundingBox.value.size); positionHelper.copy(this.boundingBox.value.center).negate(); positionHelper.z -= alignmentZMap[this.properties.value.depthAlign] * this.boundingBox.value.size.z; positionHelper.multiply(scaleHelper); positionHelper.add(vectorHelper.set((leftInset - rightInset) * 0.5 * pixelSize, (bottomInset - topInset) * 0.5 * pixelSize, 0)); this.childrenMatrix.compose(positionHelper, IdentityQuaternion, scaleHelper); }, this.abortSignal); setupMatrixWorldUpdate(this, this.root, undefined, this.abortSignal); setupOrderInfo(this.orderInfo, this.properties, 'zIndex', ElementType.Content, undefined, backgroundOrderInfo, this.abortSignal); this.clippingPlanes = createGlobalClippingPlanes(this); abortableEffect(() => { this.visible = this.isVisible.value; applyAppearancePropertiesToGroup(this.properties, this, this.config?.depthWriteDefault ?? DepthWriteDefaultDefault, this.config?.supportFillProperty ?? SupportFillPropertyDefault); this.root.peek().requestRender?.(); }, this.abortSignal); const remeasureOnChildrenChange = this.config?.remeasureOnChildrenChange ?? RemeasureOnChildrenChangeDefault; if (remeasureOnChildrenChange) { const onChildrenChanged = this.debounceNotifyAncestorsChanged.bind(this); this.addEventListener('childadded', onChildrenChanged); this.addEventListener('childremoved', onChildrenChanged); this.abortSignal.addEventListener('abort', () => { this.removeEventListener('childadded', onChildrenChanged); this.removeEventListener('childremoved', onChildrenChanged); }); } } childUpdateWorldMatrix(child, updateParents, updateChildren) { if (!(child.parent instanceof Content)) { Object3D.prototype.updateWorldMatrix.apply(child, [updateParents, updateChildren]); return; } if (updateParents) { this.updateWorldMatrix(true, false); } computeWorldToGlobalMatrix(this.root.value, child.matrixWorld); child.matrixWorld.multiply(this.globalMatrix.peek() ?? IdentityMatrix).multiply(this.childrenMatrix); child.updateMatrix(); child.matrixWorld.multiply(child.matrix); if (updateChildren) { for (const childChild of child.children) { childChild.updateMatrixWorld(true); } } } timeoutRef; debounceNotifyAncestorsChanged() { if (this.timeoutRef != null) { return; } this.timeoutRef = setTimeout(this.notifyAncestorsChanged.bind(this), 0); } notifyAncestorsChanged() { this.timeoutRef = undefined; applyAppearancePropertiesToGroup(this.properties, this, this.config?.depthWriteDefault ?? DepthWriteDefaultDefault, this.config?.supportFillProperty ?? SupportFillPropertyDefault); this.traverse((descendant) => { if (descendant instanceof InstancedGlyphMesh || descendant instanceof InstancedPanelMesh || !(descendant instanceof Mesh)) { return; } setupRenderOrder(descendant, this.root, this.orderInfo); descendant.material.clippingPlanes = this.clippingPlanes; descendant.material.needsUpdate = true; descendant.material.transparent = true; descendant.raycast = makeClippedCast(this, descendant.raycast.bind(descendant), this.root, this.parentContainer, this.orderInfo); descendant.spherecast = descendant.spherecast != null ? makeClippedCast(this, descendant.spherecast?.bind(descendant), this.root, this.parentContainer, this.orderInfo) : undefined; }); for (const child of this.children) { child.updateMatrixWorld = this.childUpdateWorldMatrix.bind(this, child, false, true); child.updateWorldMatrix = this.childUpdateWorldMatrix.bind(this, child); } if (this.config?.boundingBox == null) { //no need to compute the bounding box ourselves box3Helper.makeEmpty(); for (const child of this.children) { if (child instanceof InstancedGlyphMesh || child instanceof InstancedPanelMesh) { continue; } child.parent = null; box3Helper.expandByObject(child); child.parent = this; } const size = new Vector3(); const center = new Vector3(); box3Helper.getSize(size).max(smallValue); box3Helper.getCenter(center); this.boundingBox.value = { center, size }; } this.root.peek().requestRender?.(); } updateWorldMatrix(updateParents, updateChildren) { super.updateWorldMatrix(updateParents, updateChildren); if (updateChildren) { for (const child of this.children) { child.updateWorldMatrix(false, true); } } } dispose() { if (this.timeoutRef != null) { this.timeoutRef = undefined; clearInterval(this.timeoutRef); } super.dispose(); } } const colorHelper = new Color(); const colorArrayHelper = [0, 0, 0, 0]; function applyAppearancePropertiesToGroup(properties, group, depthWriteDefault, supportFillProperty) { const color = (supportFillProperty ? properties.value.fill : undefined) ?? properties.value.color; const opacity = toAbsoluteNumber(properties.value.opacity, () => 1); if (color != null) { writeColor(colorArrayHelper, 0, color, opacity, undefined); colorHelper.fromArray(colorArrayHelper); } const depthTest = properties.value.depthTest; const depthWrite = properties.value.depthWrite ?? depthWriteDefault; const renderOrder = properties.value.renderOrder; group.traverse((child) => { if (child instanceof InstancedGlyphMesh || child instanceof InstancedPanelMesh || !(child instanceof Mesh)) { return; } child.renderOrder = renderOrder; const material = child.material; child.userData.color ??= material.color.clone(); material.color.copy(color != null ? colorHelper : child.userData.color); material.opacity = color != null ? colorArrayHelper[3] : opacity; material.depthTest = depthTest; material.depthWrite = depthWrite; }); }