UNPKG

@playcanvas/react

Version:

A React renderer for PlayCanvas – build interactive 3D applications using React's declarative paradigm.

140 lines 6.58 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { FILLMODE_NONE, FILLMODE_FILL_WINDOW, FILLMODE_KEEP_ASPECT, RESOLUTION_AUTO, Application as PlayCanvasApplication, Mouse, TouchDevice, RESOLUTION_FIXED, NullGraphicsDevice } from 'playcanvas'; import { AppContext, ParentContext } from './hooks'; import { PointerEventsContext } from './contexts/pointer-events-context'; import { usePicker } from './utils/picker'; import { PhysicsProvider } from './contexts/physics-context'; import { validatePropsWithDefaults, createComponentDefinition, getNullApplication, applyProps } from './utils/validation'; import { defaultGraphicsDeviceOptions } from './types/graphics-device-options'; /** * The **Application** component is the root node of the PlayCanvas React API. It creates a canvas element * and initializes a PlayCanvas application instance. * * @param {ApplicationProps} props - The props to pass to the application component. * @returns {React.ReactNode} - The application component. * * @example * <Application> * <Entity /> * </Application> */ export const Application = ({ children, className = 'pc-app', style = { width: '100%', height: '100%' }, ...props }) => { const canvasRef = useRef(null); return (_jsxs(_Fragment, { children: [_jsx(Canvas, { className: className, style: style, ref: canvasRef }), _jsx(ApplicationWithoutCanvas, { canvasRef: canvasRef, ...props, children: children })] })); }; const Canvas = React.memo(React.forwardRef((props, ref) => { const { className, style } = props; return _jsx("canvas", { ref: ref, className: className, style: style, "aria-label": "Interactive 3D Scene" }); })); /** * An alternative Application component that does not create a canvas element. * This allows you to create a canvas independently from PlayCanvas and pass it in as a ref. * * @param {ApplicationWithoutCanvasProps} props - The props to pass to the application component. * @returns {React.ReactNode} The application component. * * @example * const canvasRef = useRef<HTMLCanvasElement>(null); * * return ( * <> * <canvas ref={canvasRef} /> * <ApplicationWithoutCanvas canvasRef={canvasRef}> * <Entity /> * </ApplicationWithoutCanvas> * </> * ); */ export const ApplicationWithoutCanvas = (props) => { const { children, ...propsToValidate } = props; const validatedProps = validatePropsWithDefaults(propsToValidate, componentDefinition); const { canvasRef, fillMode = FILLMODE_NONE, resolutionMode = RESOLUTION_AUTO, usePhysics = false, graphicsDeviceOptions, ...otherProps } = validatedProps; const localGraphicsDeviceOptions = { ...defaultGraphicsDeviceOptions, ...graphicsDeviceOptions }; const [app, setApp] = useState(null); const appRef = useRef(null); const pointerEvents = useMemo(() => new Set(), []); usePicker(appRef.current, canvasRef.current, pointerEvents); useLayoutEffect(() => { const canvas = canvasRef.current; if (canvas && !appRef.current) { const localApp = new PlayCanvasApplication(canvas, { mouse: new Mouse(canvas), touch: new TouchDevice(canvas), graphicsDevice: process.env.NODE_ENV === 'test' ? new NullGraphicsDevice(canvas) : undefined, graphicsDeviceOptions: localGraphicsDeviceOptions }); localApp.start(); appRef.current = localApp; setApp(localApp); } return () => { if (!appRef.current) return; appRef.current.destroy(); appRef.current = null; setApp(null); }; }, [canvasRef, ...Object.values(localGraphicsDeviceOptions)]); // Separate useEffect for these props to avoid re-rendering useEffect(() => { if (!app) return; app.setCanvasFillMode(fillMode); app.setCanvasResolution(resolutionMode); }, [app, fillMode, resolutionMode]); // These app properties can be updated without re-rendering useLayoutEffect(() => { if (!app) return; applyProps(app, componentDefinition.schema, otherProps); }); if (!app) return null; return (_jsx(PhysicsProvider, { enabled: usePhysics, app: app, children: _jsx(AppContext.Provider, { value: appRef.current, children: _jsx(PointerEventsContext.Provider, { value: pointerEvents, children: _jsx(ParentContext.Provider, { value: appRef.current?.root, children: children }) }) }) })); }; const componentDefinition = createComponentDefinition("Application", () => getNullApplication(), (app) => app.destroy()); componentDefinition.schema = { ...componentDefinition.schema, className: { validate: (value) => typeof value === 'string', errorMsg: (value) => `className must be a string. Received: ${value}`, default: 'pc-app' }, style: { validate: (value) => typeof value === 'object' && value !== null, errorMsg: (value) => `style must be an object. Received: ${value}`, default: { width: '100%', height: '100%' } }, canvasRef: { validate: (value) => value !== null && typeof value === 'object' && 'current' in value, errorMsg: (value) => `canvasRef must be a React ref object. Received: ${value}`, default: null }, usePhysics: { validate: (value) => typeof value === 'boolean', errorMsg: (value) => `usePhysics must be a boolean. Received: ${value}`, default: false }, fillMode: { validate: (value) => typeof value === 'string' && [FILLMODE_NONE, FILLMODE_FILL_WINDOW, FILLMODE_KEEP_ASPECT].includes(value), errorMsg: () => `"fillMode" must be one of: ${FILLMODE_NONE}, ${FILLMODE_FILL_WINDOW}, ${FILLMODE_KEEP_ASPECT}`, default: FILLMODE_NONE }, resolutionMode: { validate: (value) => typeof value === 'string' && [RESOLUTION_AUTO, RESOLUTION_FIXED].includes(value), errorMsg: () => `"resolutionMode" must be one of: ${RESOLUTION_AUTO}, ${RESOLUTION_FIXED}`, default: RESOLUTION_AUTO }, graphicsDeviceOptions: { validate: (value) => value === undefined || (typeof value === 'object' && value !== null), errorMsg: (value) => `graphicsDeviceOptions must be an object. Received: ${value}`, default: undefined } }; //# sourceMappingURL=Application.js.map