UNPKG

@pmndrs/uikit

Version:

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

127 lines (126 loc) 7.29 kB
import { signal } from '@preact/signals-core'; import { Group, Mesh, MeshBasicMaterial, ShapeGeometry } from 'three'; import { createFlexNodeState } from '../flex/index.js'; import { ElementType, computedOrderInfo, setupRenderOrder } from '../order.js'; import { setupInstancedPanel } from '../panel/instanced-panel.js'; import { setupObjectTransform, computedTransformMatrix } from '../transform.js'; import { applyAppearancePropertiesToGroup, computedGlobalMatrix, computedHandlers, computedIsVisible, computedMergedProperties, setupNode, keepAspectRatioPropertyTransformer, setupMatrixWorldUpdate, setupPointerEvents, computedAncestorsHaveListeners, } from './utils.js'; import { abortableEffect, fitNormalizedContentInside } from '../utils.js'; import { makeClippedCast } from '../panel/interaction-panel-mesh.js'; import { computedIsClipped, createGlobalClippingPlanes } from '../clipping.js'; import { setupLayoutListeners, setupClippedListeners } from '../listeners.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 { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js'; import { computedPanelGroupDependencies, getDefaultPanelMaterialConfig } from '../panel/index.js'; import { darkPropertyTransformers } from '../dark.js'; export function createIconState(parentCtx, text, svgWidth, svgHeight, style, properties, defaultProperties) { const flexState = createFlexNodeState(); const hoveredSignal = signal([]); const activeSignal = signal([]); const mergedProperties = computedMergedProperties(style, properties, defaultProperties, { ...darkPropertyTransformers, ...createResponsivePropertyTransformers(parentCtx.root.size), ...createHoverPropertyTransformers(hoveredSignal), ...createActivePropertyTransfomers(activeSignal), }, keepAspectRatioPropertyTransformer, (m) => { m.add('aspectRatio', svgWidth / svgHeight); m.add('width', svgWidth); m.add('height', svgHeight); }); const transformMatrix = computedTransformMatrix(mergedProperties, flexState, parentCtx.root.pixelSize); const globalMatrix = computedGlobalMatrix(parentCtx.childrenMatrix, transformMatrix); const isClipped = computedIsClipped(parentCtx.clippingRect, globalMatrix, flexState.size, parentCtx.root.pixelSize); const isVisible = computedIsVisible(flexState, isClipped, mergedProperties); const groupDeps = computedPanelGroupDependencies(mergedProperties); const backgroundOrderInfo = computedOrderInfo(mergedProperties, 'zIndexOffset', ElementType.Panel, groupDeps, parentCtx.orderInfo); const orderInfo = computedOrderInfo(undefined, 'zIndexOffset', ElementType.Svg, undefined, backgroundOrderInfo); const handlers = computedHandlers(style, properties, defaultProperties, hoveredSignal, activeSignal); const ancestorsHaveListeners = computedAncestorsHaveListeners(parentCtx, handlers); const clippingPlanes = createGlobalClippingPlanes(parentCtx.root, parentCtx.clippingRect); return Object.assign(flexState, { root: parentCtx.root, hoveredSignal, activeSignal, mergedProperties, transformMatrix, globalMatrix, isClipped, isVisible, groupDeps, backgroundOrderInfo, orderInfo, handlers, ancestorsHaveListeners, text, svgWidth, svgHeight, interactionPanel: createInteractionPanel(orderInfo, parentCtx.root, parentCtx.clippingRect, globalMatrix, flexState), iconGroup: createIconGroup(flexState, text, parentCtx, orderInfo, clippingPlanes), }); } export function setupIcon(state, parentCtx, style, properties, object, abortSignal) { setupCursorCleanup(state.hoveredSignal, abortSignal); setupNode(state, parentCtx, object, true, abortSignal); setupObjectTransform(parentCtx.root, object, state.transformMatrix, abortSignal); setupInstancedPanel(state.mergedProperties, state.backgroundOrderInfo, state.groupDeps, parentCtx.root.panelGroupManager, state.globalMatrix, state.size, undefined, state.borderInset, parentCtx.clippingRect, state.isVisible, getDefaultPanelMaterialConfig(), abortSignal); setupIconGroup(state.iconGroup, state.mergedProperties, state.svgWidth, state.svgHeight, parentCtx, state, state.isVisible, abortSignal); setupMatrixWorldUpdate(true, true, object, parentCtx.root, state.globalMatrix, false, abortSignal); setupPointerEvents(state.mergedProperties, state.ancestorsHaveListeners, parentCtx.root, object, false, abortSignal); setupLayoutListeners(style, properties, state.size, abortSignal); setupClippedListeners(style, properties, state.isClipped, abortSignal); setupInteractionPanel(state.interactionPanel, state.root, state.globalMatrix, state.size, abortSignal); } const loader = new SVGLoader(); function createIconGroup(flexState, text, parentContext, orderInfo, clippingPlanes) { const group = new Group(); group.matrixAutoUpdate = false; const result = loader.parse(text); for (const path of result.paths) { const shapes = SVGLoader.createShapes(path); const material = new MeshBasicMaterial(); material.transparent = true; material.depthWrite = false; material.toneMapped = false; material.clippingPlanes = clippingPlanes; for (const shape of shapes) { const geometry = new ShapeGeometry(shape); geometry.computeBoundingBox(); const mesh = new Mesh(geometry, material); mesh.matrixAutoUpdate = false; mesh.raycast = makeClippedCast(mesh, mesh.raycast, parentContext.root.objectRef, parentContext.clippingRect, orderInfo, flexState); setupRenderOrder(mesh, parentContext.root, orderInfo); mesh.userData.color = path.color; mesh.scale.y = -1; mesh.updateMatrix(); group.add(mesh); } } return group; } function setupIconGroup(group, propertiesSignal, svgWidth, svgHeight, parentContext, flexState, isVisible, abortSignal) { const aspectRatio = svgWidth / svgHeight; abortableEffect(() => { fitNormalizedContentInside(group.position, group.scale, flexState.size, flexState.paddingInset, flexState.borderInset, parentContext.root.pixelSize.value, aspectRatio); group.position.x -= (group.scale.x * aspectRatio) / 2; group.position.y += group.scale.x / 2; group.scale.divideScalar(svgHeight); group.updateMatrix(); parentContext.root.requestRender(); }, abortSignal); abortSignal.addEventListener('abort', () => group.children.forEach((child) => { if (!(child instanceof Mesh)) { return; } ; child.geometry.dispose(); child.material.dispose(); })); abortableEffect(() => { group.visible = isVisible.value; parentContext.root.requestRender(); }, abortSignal); applyAppearancePropertiesToGroup(propertiesSignal, group, abortSignal); }