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