Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/docs/src/components/MotionEntity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ export const MotionEntity: FC<MotionEntityProps> = ({ children, animate: animate
return (
<Entity ref={entityRef}
{...props}
position={position.array.get() as number[]}
rotation={rotation.array.get() as number[]}
scale={scale.array.get() as number[]}
position={position.array.get() as [number, number, number]}
rotation={rotation.array.get() as [number, number, number, number?]}
scale={scale.array.get() as [number, number, number]}
>
{children}
</Entity>
Expand Down
177 changes: 114 additions & 63 deletions packages/lib/src/Application.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, PropsWithChildren, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { FC, useLayoutEffect, useMemo, useRef, useState } from 'react';
import {
FILLMODE_NONE,
FILLMODE_FILL_WINDOW,
Expand All @@ -9,62 +9,26 @@
TouchDevice,
Entity as PcEntity,
RESOLUTION_FIXED,
type GraphicsDevice,
} 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';

interface GraphicsOptions {
/** Boolean that indicates if the canvas contains an alpha buffer. */
alpha?: boolean, //true,
/** Boolean that indicates that the drawing buffer is requested to have a depth buffer of at least 16 bits. */
depth?: boolean, //true
/** Boolean that indicates that the drawing buffer is requested to have a stencil buffer of at least 8 bits. */
stencil?: boolean, //true
/** Boolean that indicates whether or not to perform anti-aliasing if possible. */
antialias?: boolean, //true
/** Boolean that indicates that the page compositor will assume the drawing buffer contains colors with pre-multiplied alpha. */
premultipliedAlpha?: boolean, //true
/** If the value is true the buffers will not be cleared and will preserve their values until cleared or overwritten by the author. */
preserveDrawingBuffer?: boolean, //false
/** A hint to the user agent indicating what configuration of GPU is suitable for the WebGL context. */
powerPreference?: 'default' | 'high-performance' | 'low-power' // 'default'
/** Boolean that indicates if a context will be created if the system performance is low or if no hardware GPU is available. */
failIfMajorPerformanceCaveat?: boolean,//false
/** Boolean that hints the user agent to reduce the latency by desynchronizing the canvas paint cycle from the event loop. */
desynchronized?: boolean,//false
/** BBoolean that hints to the user agent to use a compatible graphics adapter for an immersive XR device. */
xrCompatible?: boolean,//false
}

interface ApplicationProps extends PropsWithChildren<unknown> {
/** The class name to attach to the canvas component */
className?: string,
/** A style object added to the canvas component */
style?: Record<string, unknown>
/** Controls how the canvas fills the window and resizes when the window changes. */
fillMode?: typeof FILLMODE_NONE | typeof FILLMODE_FILL_WINDOW | typeof FILLMODE_KEEP_ASPECT
/** Change the resolution of the canvas, and set the way it behaves when the window is resized. */
resolutionMode?: typeof RESOLUTION_AUTO | typeof RESOLUTION_FIXED
/** Clamps per-frame delta time to an upper bound. Useful since returning from a tab deactivation can generate huge values for dt, which can adversely affect game state. */
maxDeltaTime?: number
/** Scales the global time delta. */
timeScale?: number,
/** Whether to use the PlayCanvas Physics system. */
usePhysics?: boolean,
/** Graphics Settings */
graphicsDeviceOptions?: GraphicsOptions
}


interface ApplicationWithoutCanvasProps extends ApplicationProps {
/** A ref to a html canvas element */
canvasRef: React.RefObject<HTMLCanvasElement>;
}
import { validateAndSanitizeProps, createComponentDefinition, Schema, getNullApplication } from './utils/validation';
import { PublicProps } from './utils/types-utils';

/**
* The **Application** component is the root node of the PlayCanvas React api. It creates a canvas element
* 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: React.FC<ApplicationProps> = ({
children,
Expand All @@ -90,18 +54,42 @@

/**
* An alternative Application component that does not create a canvas element.
* This allows you to create a canvas independently from Playcanvas and pass this in as a ref.
* 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: FC<ApplicationWithoutCanvasProps> = ({
children,
canvasRef,
fillMode = FILLMODE_NONE,
resolutionMode = RESOLUTION_AUTO,
maxDeltaTime = 0.1,
timeScale = 1,
usePhysics = false,
...otherProps
}) => {
export const ApplicationWithoutCanvas: FC<ApplicationWithoutCanvasProps> = (props) => {

const { children, ...propsToValidate } = props;

const validatedProps = validateAndSanitizeProps<ApplicationWithoutCanvasProps>(
propsToValidate,
componentDefinition
);

const {
canvasRef,
fillMode = FILLMODE_NONE,
resolutionMode = RESOLUTION_AUTO,
maxDeltaTime = 0.1,
timeScale = 1,
usePhysics = false,
...otherProps
} = validatedProps;

const graphicsDeviceOptions = {
alpha: true,
depth: true,
Expand Down Expand Up @@ -150,9 +138,9 @@
// These app properties can be updated without re-rendering
useLayoutEffect(() => {
if (!app) return;
app.maxDeltaTime = maxDeltaTime;

Check warning on line 141 in packages/lib/src/Application.tsx

View workflow job for this annotation

GitHub Actions / lint

This mutates a variable that React considers immutable
app.timeScale = timeScale;
}, [app])
}, [app, maxDeltaTime, timeScale])

if (!app) return null;

Expand All @@ -167,4 +155,67 @@
</AppContext.Provider>
</PhysicsProvider>
);
};
};

type GraphicsOptions = Partial<PublicProps<GraphicsDevice>>

interface ApplicationProps extends Partial<PublicProps<PlayCanvasApplication>> {
/** The class name to attach to the canvas component */
className?: string,
/** A style object added to the canvas component */
style?: Record<string, unknown>
/** Controls how the canvas fills the window and resizes when the window changes. */
fillMode?: typeof FILLMODE_NONE | typeof FILLMODE_FILL_WINDOW | typeof FILLMODE_KEEP_ASPECT
/** Change the resolution of the canvas, and set the way it behaves when the window is resized. */
resolutionMode?: typeof RESOLUTION_AUTO | typeof RESOLUTION_FIXED
// /** Clamps per-frame delta time to an upper bound. Useful since returning from a tab deactivation can generate huge values for dt, which can adversely affect game state. */
maxDeltaTime?: number
// /** Scales the global time delta. */
timeScale?: number,
/** Whether to use the PlayCanvas Physics system. */
usePhysics?: boolean,
/** Graphics Settings */
graphicsDeviceOptions?: GraphicsOptions,
/** The children of the application */
children?: React.ReactNode,
}


interface ApplicationWithoutCanvasProps extends ApplicationProps {
/** A ref to a html canvas element */
canvasRef: React.RefObject<HTMLCanvasElement>;
}

const componentDefinition = createComponentDefinition(
"Application",
() => getNullApplication(),
(app) => app.destroy()
)

componentDefinition.schema = {
...componentDefinition.schema,
canvasRef: {
validate: (value: unknown) => {
return value !== null &&
typeof value === 'object' &&
'current' in value;
},
errorMsg: (value: unknown) => `canvasRef must be a React ref object. Received: ${value}`,
default: null
},
usePhysics: {
validate: (value: unknown) => typeof value === 'boolean',
errorMsg: (value: unknown) => `usePhysics must be a boolean. Received: ${value}`,
default: false
},
fillMode: {
validate: (value: unknown) => 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: unknown) => typeof value === 'string' && [RESOLUTION_AUTO, RESOLUTION_FIXED].includes(value),
errorMsg: () => `"resolutionMode" must be one of: ${RESOLUTION_AUTO}, ${RESOLUTION_FIXED}`,
default: RESOLUTION_AUTO
}
} as Schema<ApplicationProps>
2 changes: 1 addition & 1 deletion packages/lib/src/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const Container: FC<ContainerProps> = ({ asset, children, ...props }) =>

if(!asset?.resource) return null;

return <Entity ref={entityRef} {...props}>
return <Entity ref={entityRef} {...props} >
{ children }
</Entity>;
};
123 changes: 99 additions & 24 deletions packages/lib/src/Entity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,63 @@
import { useParent, ParentContext, useApp } from './hooks';
import { SyntheticMouseEvent, SyntheticPointerEvent } from './utils/synthetic-event';
import { usePointerEventsContext } from './contexts/pointer-events-context';
import { PublicProps } from './utils/types-utils';
import { validateAndSanitizeProps, createComponentDefinition, ComponentDefinition, Schema } from './utils/validation';

/**
* The Entity component is the fundamental building block of a PlayCanvas scene.
* It represents a node in the scene graph and can have components attached to it.
*
* @example
* // Basic usage
* <Entity name="myEntity" position={[0, 1, 0]}>
* <Render type="box" />
* </Entity>
*
* @example
* // With pointer events
* <Entity
* position={[0, 1, 0]}
* onPointerDown={(e) => console.log('Clicked!')}
* onClick={(e) => console.log('Mouse clicked!')}
* >
* <Render type="sphere" />
* </Entity>
*
* @param {EntityProps} props - Component props
*/
export const Entity = forwardRef<PcEntity, EntityProps> (function Entity(
props,
ref
) : React.ReactElement | null {

type PointerEventCallback = (event: SyntheticPointerEvent) => void;
type MouseEventCallback = (event: SyntheticMouseEvent) => void;

interface EntityProps {
name?: string;
position?: number[];
scale?: number[];
rotation?: number[];
onPointerUp?: PointerEventCallback;
onPointerDown?: PointerEventCallback;
onPointerOver?: PointerEventCallback;
onPointerOut?: PointerEventCallback;
onClick?: MouseEventCallback;
children?: ReactNode
}

const { children, ...propsToValidate } = props;
const safeProps = validateAndSanitizeProps(
propsToValidate,
componentDefinition as ComponentDefinition<EntityProps>
);

export const Entity = forwardRef<PcEntity, EntityProps> (function Entity(
{
const {
/** The name of the entity */
name = 'Untitled',
children,
/** The local position of the entity */
position = [0, 0, 0],
/** The local scale of the entity */
scale = [1, 1, 1],
/** The local rotation of the entity */
rotation = [0, 0, 0],
/** The callback for the pointer down event */
onPointerDown,
/** The callback for the pointer up event */
onPointerUp,
/** The callback for the pointer over event */
onPointerOver,
/** The callback for the pointer out event */
onPointerOut,
/** The callback for the click event */
onClick,
},
ref
) : React.ReactElement | null {
} : EntityProps = safeProps;

const parent = useParent();
const app = useApp();
const pointerEvents = usePointerEventsContext();
Expand All @@ -46,7 +70,7 @@
const hasPointerEvents = !!(onPointerDown || onPointerUp || onPointerOver || onPointerOut || onClick);

// Create the entity only when 'app' changes
const entity = useMemo(() => new PcEntity(name, app), [app]) as PcEntity
const entity = useMemo(() => new PcEntity(undefined, app), [app]) as PcEntity

useImperativeHandle(ref, () => entity);

Expand Down Expand Up @@ -88,7 +112,7 @@
}, [app, parent, entity, onPointerDown, onPointerUp, onPointerOver, onPointerOut, onClick]);

useLayoutEffect(() => {
entity.name = name;

Check warning on line 115 in packages/lib/src/Entity.tsx

View workflow job for this annotation

GitHub Actions / lint

This mutates a variable that React considers immutable
entity.setLocalPosition(...position as [number, number, number]);
entity.setLocalScale(...scale as [number, number, number]);
entity.setLocalEulerAngles(...rotation as [number, number, number]);
Expand All @@ -99,4 +123,55 @@
{children || null}
</ParentContext.Provider>
</> );
});
});

type PointerEventCallback = (event: SyntheticPointerEvent) => void;
type MouseEventCallback = (event: SyntheticMouseEvent) => void;

export interface EntityProps extends Partial<PublicProps<PcEntity>> {
name?: string;
position?: [number, number, number];
scale?: [number, number, number];
rotation?: [number, number, number, number?];
onPointerUp?: PointerEventCallback;
onPointerDown?: PointerEventCallback;
onPointerOver?: PointerEventCallback;
onPointerOut?: PointerEventCallback;
onClick?: MouseEventCallback;
children?: ReactNode;
}

const componentDefinition = createComponentDefinition(
"Entity",
() => new PcEntity(),
(entity) => entity.destroy()
)

componentDefinition.schema = {
...componentDefinition.schema,
onPointerDown: {
validate: (val: unknown) => typeof val === 'function',
errorMsg: (val: unknown) => `Invalid value for prop "onPointerDown": "${val}". Expected a function.`,
default: undefined
},
onPointerUp: {
validate: (val: unknown) => typeof val === 'function',
errorMsg: (val: unknown) => `Invalid value for prop "onPointerUp": "${val}". Expected a function.`,
default: undefined
},
onPointerOver: {
validate: (val: unknown) => typeof val === 'function',
errorMsg: (val: unknown) => `Invalid value for prop "onPointerOver": "${val}". Expected a function.`,
default: undefined
},
onPointerOut: {
validate: (val: unknown) => typeof val === 'function',
errorMsg: (val: unknown) => `Invalid value for prop "onPointerOut": "${val}". Expected a function.`,
default: undefined
},
onClick: {
validate: (val: unknown) => typeof val === 'function',
errorMsg: (val: unknown) => `Invalid value for prop "onClick": "${val}". Expected a function.`,
default: undefined
}
} as Schema<EntityProps>
Loading
Loading