UNPKG

@pmndrs/uikit

Version:

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

172 lines (171 loc) 10.2 kB
import { computed, signal } from '@preact/signals-core'; import { Box3, Group, Mesh, MeshBasicMaterial, ShapeGeometry, Vector3 } from 'three'; import { createFlexNodeState } from '../flex/index.js'; import { ElementType, computedOrderInfo, setupRenderOrder } from '../order.js'; import { setupInstancedPanel } from '../panel/instanced-panel.js'; import { createScrollPosition, setupScrollbars, computedScrollHandlers, computedAnyAncestorScrollable, createScrollState, computedGlobalScrollMatrix, setupScroll, } from '../scroll.js'; import { setupObjectTransform, computedTransformMatrix } from '../transform.js'; import { applyAppearancePropertiesToGroup, computedGlobalMatrix, computedHandlers, computedIsVisible, computedMergedProperties, setupNode, disposeGroup, keepAspectRatioPropertyTransformer, loadResourceWithParams, } from './utils.js'; import { abortableEffect, fitNormalizedContentInside, readReactive } 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 { darkPropertyTransformers } from '../dark.js'; import { computedPanelGroupDependencies, getDefaultPanelMaterialConfig } from '../panel/index.js'; import { computedInheritableProperty, computeDefaultProperties, setupMatrixWorldUpdate, setupPointerEvents, computedAncestorsHaveListeners, computedClippingRect, } from '../internals.js'; export function createSvgState(parentCtx, objectRef, style, properties, defaultProperties) { const flexState = createFlexNodeState(); const hoveredSignal = signal([]); const activeSignal = signal([]); const aspectRatio = signal(undefined); const svgObject = signal(undefined); const mergedProperties = computedMergedProperties(style, properties, defaultProperties, { ...darkPropertyTransformers, ...createResponsivePropertyTransformers(parentCtx.root.size), ...createHoverPropertyTransformers(hoveredSignal), ...createActivePropertyTransfomers(activeSignal), }, keepAspectRatioPropertyTransformer, (m) => m.add('aspectRatio', aspectRatio)); 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 src = computed(() => readReactive(style.value?.src) ?? readReactive(properties.value?.src)); const scrollPosition = createScrollPosition(); const childrenMatrix = computedGlobalScrollMatrix(scrollPosition, globalMatrix, parentCtx.root.pixelSize); const scrollbarWidth = computedInheritableProperty(mergedProperties, 'scrollbarWidth', 10); const updateMatrixWorld = computedInheritableProperty(mergedProperties, 'updateMatrixWorld', false); const componentState = Object.assign(flexState, { centerGroup: createCenterGroup(), interactionPanel: createInteractionPanel(orderInfo, parentCtx.root, parentCtx.clippingRect, globalMatrix, flexState), scrollState: createScrollState(), hoveredSignal, activeSignal, aspectRatio, svgObject, mergedProperties, transformMatrix, globalMatrix, isClipped, isVisible, groupDeps, backgroundOrderInfo, orderInfo, src, scrollPosition, scrollbarWidth, childrenMatrix, updateMatrixWorld, clippingRect: computedClippingRect(globalMatrix, flexState, parentCtx.root.pixelSize, parentCtx.clippingRect), defaultProperties: computeDefaultProperties(mergedProperties), anyAncestorScrollable: computedAnyAncestorScrollable(flexState.scrollable, parentCtx.anyAncestorScrollable), root: parentCtx.root, }); const scrollHandlers = computedScrollHandlers(componentState, properties, objectRef); const handlers = computedHandlers(style, properties, defaultProperties, hoveredSignal, activeSignal, scrollHandlers); const ancestorsHaveListeners = computedAncestorsHaveListeners(parentCtx, handlers); return Object.assign(componentState, { handlers, ancestorsHaveListeners, }); } export function setupSvg(state, parentCtx, style, properties, object, childrenContainer, 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); const clippingPlanes = createGlobalClippingPlanes(parentCtx.root, parentCtx.clippingRect); loadResourceWithParams(state.svgObject, loadSvg, disposeGroup, abortSignal, state.src, parentCtx.root, clippingPlanes, parentCtx.clippingRect, state.orderInfo, state.aspectRatio, state); applyAppearancePropertiesToGroup(state.mergedProperties, state.svgObject, abortSignal); setupCenterGroup(state.centerGroup, parentCtx.root, state, state.svgObject, state.aspectRatio, state.isVisible, abortSignal); setupScroll(state, properties, parentCtx.root.pixelSize, childrenContainer, abortSignal); setupScrollbars(state.mergedProperties, state.scrollPosition, state, state.globalMatrix, state.isVisible, parentCtx.clippingRect, state.orderInfo, state.groupDeps, parentCtx.root.panelGroupManager, state.scrollbarWidth, abortSignal); setupInteractionPanel(state.interactionPanel, state.root, state.globalMatrix, state.size, abortSignal); setupMatrixWorldUpdate(state.updateMatrixWorld, true, state.interactionPanel, parentCtx.root, state.globalMatrix, true, abortSignal); setupMatrixWorldUpdate(true, true, state.centerGroup, parentCtx.root, state.globalMatrix, true, abortSignal); setupPointerEvents(state.mergedProperties, state.ancestorsHaveListeners, parentCtx.root, state.centerGroup, false, abortSignal); setupPointerEvents(state.mergedProperties, state.ancestorsHaveListeners, parentCtx.root, state.interactionPanel, false, abortSignal); setupLayoutListeners(style, properties, state.size, abortSignal); setupClippedListeners(style, properties, state.isClipped, abortSignal); } function createCenterGroup() { const centerGroup = new Group(); centerGroup.matrixAutoUpdate = false; return centerGroup; } function setupCenterGroup(centerGroup, root, flexState, svgObject, aspectRatio, isVisible, abortSignal) { abortableEffect(() => { fitNormalizedContentInside(centerGroup.position, centerGroup.scale, flexState.size, flexState.paddingInset, flexState.borderInset, root.pixelSize.value, aspectRatio.value ?? 1); centerGroup.updateMatrix(); root.requestRender(); }, abortSignal); abortableEffect(() => { const object = svgObject.value; if (object == null) { return; } centerGroup.add(object); root.requestRender(); return () => { centerGroup.remove(object); root.requestRender(); }; }, abortSignal); abortableEffect(() => { void (centerGroup.visible = svgObject.value != null && isVisible.value); root.requestRender(); }, abortSignal); } const loader = new SVGLoader(); const box3Helper = new Box3(); const vectorHelper = new Vector3(); const svgCache = new Map(); async function loadSvg(url, root, clippingPlanes, clippedRect, orderInfo, aspectRatio, flexState) { if (url == null) { return undefined; } const object = new Group(); object.matrixAutoUpdate = false; let result = svgCache.get(url); if (result == null) { svgCache.set(url, (result = await loader.loadAsync(url))); } box3Helper.makeEmpty(); 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(); box3Helper.union(geometry.boundingBox); const mesh = new Mesh(geometry, material); mesh.matrixAutoUpdate = false; mesh.raycast = makeClippedCast(mesh, mesh.raycast, root.objectRef, clippedRect, orderInfo, flexState); setupRenderOrder(mesh, root, orderInfo); mesh.userData.color = path.color; mesh.scale.y = -1; mesh.updateMatrix(); object.add(mesh); } } box3Helper.getSize(vectorHelper); aspectRatio.value = vectorHelper.x / vectorHelper.y; const scale = 1 / vectorHelper.y; object.scale.set(1, 1, 1).multiplyScalar(scale); box3Helper.getCenter(vectorHelper); vectorHelper.y *= -1; object.position.copy(vectorHelper).negate().multiplyScalar(scale); object.updateMatrix(); return object; }