UNPKG

@dcl/react-ecs

Version:
117 lines (116 loc) 5.07 kB
import { EntityState } from '@dcl/ecs'; import * as ecsComponents from '@dcl/ecs/dist/components'; import React from 'react'; import { createReconciler } from './reconciler'; import { getUiScaleFactor, resetInteractableArea, resetScreenInsetArea, resetUiScaleFactor, setInteractableArea, setScreenInsetArea, setUiScaleFactor } from './components/utils'; /** * @public */ export function createReactBasedUiSystem(engine, pointerSystem) { const renderer = createReconciler(engine, pointerSystem); let uiComponent = undefined; let virtualSize = undefined; const additionalRenderers = new Map(); const UiCanvasInformation = ecsComponents.UiCanvasInformation(engine); // Unique owner to prevent other UI systems resetting this scale factor. const uiScaleFactorOwner = Symbol('react-ecs-ui-scale'); // Unique owner for the screen inset module variable. const screenInsetAreaOwner = Symbol('react-ecs-screen-inset-area'); // Unique owner for the interactable area module variable. const interactableAreaOwner = Symbol('react-ecs-interactable-area'); function getActiveVirtualSize() { // Main renderer options win; otherwise use the first additional renderer option. if (virtualSize) return virtualSize; for (const entry of additionalRenderers.values()) { if (entry.options) return entry.options; } return undefined; } function ReactBasedUiSystem() { const components = []; // Add main UI component if (uiComponent) { components.push(React.createElement(uiComponent, { key: '__main__' })); } const entitiesToRemove = []; for (const [entity, entry] of additionalRenderers) { // Check for entity-based cleanup if (engine.getEntityState(entity) === EntityState.Removed) { entitiesToRemove.push(entity); } else { components.push(React.createElement(entry.ui, { key: `__entity_${entity}__` })); } } // Entity-based cleanup for (const entity of entitiesToRemove) { additionalRenderers.delete(entity); } // Always update the renderer - pass null when empty to clear the UI if (components.length > 0) { renderer.update(React.createElement(React.Fragment, null, ...components)); } else { renderer.update(null); } } function UiScaleSystem() { const canvasInfo = UiCanvasInformation.getOrNull(engine.RootEntity); // Update the screen inset module variable unconditionally — it is // independent of the virtual size and useful even when the renderer has no // virtual canvas. if (canvasInfo?.screenInsetArea) { setScreenInsetArea(canvasInfo.screenInsetArea, screenInsetAreaOwner); } // Update the interactable area module variable unconditionally. if (canvasInfo?.interactableArea) { setInteractableArea(canvasInfo.interactableArea, interactableAreaOwner); } const activeVirtualSize = getActiveVirtualSize(); if (!activeVirtualSize) { // Reset only if this system owns the scale factor. resetUiScaleFactor(uiScaleFactorOwner); return; } if (!canvasInfo) return; const { width, height, devicePixelRatio } = canvasInfo; const { virtualWidth, virtualHeight } = activeVirtualSize; if (!virtualWidth || !virtualHeight) return; // Normalize by devicePixelRatio so virtual px map to logical px (matching the // vw/vh path); without it the scale was inflated on high-dpr mobile screens. const ratio = devicePixelRatio || 1; const nextScale = Math.min(width / virtualWidth, height / virtualHeight) / ratio; if (Number.isFinite(nextScale) && nextScale !== getUiScaleFactor()) { // Track ownership when updating to avoid cross-system conflicts. setUiScaleFactor(nextScale, uiScaleFactorOwner); } } engine.addSystem(UiScaleSystem, 100e3 + 1, '@dcl/react-ecs-ui-scale'); engine.addSystem(ReactBasedUiSystem, 100e3, '@dcl/react-ecs'); return { destroy() { engine.removeSystem(UiScaleSystem); engine.removeSystem(ReactBasedUiSystem); resetUiScaleFactor(uiScaleFactorOwner); resetScreenInsetArea(screenInsetAreaOwner); resetInteractableArea(interactableAreaOwner); for (const entity of renderer.getEntities()) { engine.removeEntity(entity); } }, setUiRenderer(ui, options) { uiComponent = ui; virtualSize = options; }, addUiRenderer(entity, ui, options) { additionalRenderers.set(entity, { ui, options }); }, removeUiRenderer(entity) { additionalRenderers.delete(entity); } }; }