Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Pointer Events Context for Enhanced Interaction Handling #28

Merged
merged 6 commits into from
Jan 8, 2025
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
14 changes: 9 additions & 5 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, useRef, useState } from 'react';
import React, { FC, PropsWithChildren, useLayoutEffect, useMemo, useRef, useState } from 'react';
import * as Ammo from 'sync-ammo';
import {
FILLMODE_NONE,
Expand All @@ -12,6 +12,7 @@
RESOLUTION_FIXED,
} from 'playcanvas';
import { AppContext, ParentContext } from './hooks';
import { PointerEventsContext } from './contexts/pointer-events-context';
import { usePicker } from './utils/picker';

interface GraphicsOptions {
Expand Down Expand Up @@ -119,7 +120,8 @@
const [app, setApp] = useState<PlayCanvasApplication | null>(null);
const appRef = useRef<PlayCanvasApplication | null>(null);

usePicker(appRef.current, canvasRef.current);
const pointerEvents = useMemo(() => new Set<string>(), []);
usePicker(appRef.current, canvasRef.current, pointerEvents);

useLayoutEffect(() => {
const canvas = canvasRef.current;
Expand Down Expand Up @@ -158,7 +160,7 @@
// These app properties can be updated without re-rendering
useLayoutEffect(() => {
if (!app) return;
app.maxDeltaTime = maxDeltaTime;

Check warning on line 163 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])

Expand All @@ -166,9 +168,11 @@

return (
<AppContext.Provider value={appRef.current}>
<ParentContext.Provider value={appRef.current?.root as PcEntity}>
{children}
</ParentContext.Provider>
<PointerEventsContext.Provider value={pointerEvents}>
<ParentContext.Provider value={appRef.current?.root as PcEntity}>
{children}
</ParentContext.Provider>
</PointerEventsContext.Provider>
</AppContext.Provider>
);
};
42 changes: 27 additions & 15 deletions packages/lib/src/Entity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { ReactNode, forwardRef, useImperativeHandle, useLayoutEffect, useMemo } from 'react';
import { useParent, ParentContext, useApp } from './hooks';
import { SyntheticMouseEvent, SyntheticPointerEvent } from './utils/synthetic-event';
import { usePointerEventsContext } from './contexts/pointer-events-context';

type PointerEventCallback = (event: SyntheticPointerEvent) => void;
type MouseEventCallback = (event: SyntheticMouseEvent) => void;
Expand All @@ -29,16 +30,20 @@
position = [0, 0, 0],
scale = [1, 1, 1],
rotation = [0, 0, 0],
onPointerDown = () => null,
onPointerUp = () => null,
onPointerOver = () => null,
onPointerOut = () => null,
onClick = () => null
onPointerDown,
onPointerUp,
onPointerOver,
onPointerOut,
onClick,
},
ref
) : React.ReactElement | null {
const parent = useParent();
const app = useApp();
const pointerEvents = usePointerEventsContext();

// Check if the entity has pointer events attached
const hasPointerEvents = !!(onPointerDown || onPointerUp || onPointerOver || onPointerOut || onClick);

// Create the entity only when 'app' changes
const entity = useMemo(() => new PcEntity(name, app), [app]) as PcEntity
Expand All @@ -59,24 +64,31 @@
// PointerEvents
useLayoutEffect(() => {

entity.on('pointerdown', onPointerDown);
entity.on('pointerup', onPointerUp);
entity.on('pointerover', onPointerOver);
entity.on('pointerout', onPointerOut);
entity.on('click', onClick);
if (hasPointerEvents) {
pointerEvents.add(entity.getGuid());
}

if (onPointerDown) entity.on('pointerdown', onPointerDown);
if (onPointerUp) entity.on('pointerup', onPointerUp);
if (onPointerOver) entity.on('pointerover', onPointerOver);
if (onPointerOut) entity.on('pointerout', onPointerOut);
if (onClick) entity.on('click', onClick);

return () => {
entity.off('pointerdown', onPointerDown);
entity.off('pointerup', onPointerUp);
entity.off('pointerover', onPointerOver);
entity.off('pointerout', onPointerOut);
entity.off('click', onClick);
if (hasPointerEvents) {
pointerEvents.delete(entity.getGuid());
}
if (onPointerDown) entity.off('pointerdown', onPointerDown);
if (onPointerUp) entity.off('pointerup', onPointerUp);
if (onPointerOver) entity.off('pointerover', onPointerOver);
if (onPointerOut) entity.off('pointerout', onPointerOut);
if (onClick) entity.off('click', onClick);
}

}, [app, parent, entity, onPointerDown, onPointerUp, onPointerOver, onPointerOut, onClick]);

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

Check warning on line 91 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 Down
11 changes: 11 additions & 0 deletions packages/lib/src/contexts/pointer-events-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext, useContext } from "react";

export const PointerEventsContext = createContext<Set<string> | null>(null);

export const usePointerEventsContext = () => {
const context = useContext(PointerEventsContext);
if (context === null) {
throw new Error('usePointerEventsContext must be used within a PointerEventsContext.Provider');
}
return context;
};
9 changes: 5 additions & 4 deletions packages/lib/src/utils/picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const getEntityAtPointerEvent = async (app : AppBase, picker: Picker, rect: DOMR

}

export const usePicker = (app: AppBase | null, el: HTMLElement | null) => {
export const usePicker = (app: AppBase | null, el: HTMLElement | null, pointerEvents: Set<string>) => {
const activeEntity = useRef<Entity | null>(null);
const pointerDetails = useRef<PointerEvent | null>(null);
const canvasRectRef = useRef<DOMRect | null>(app ? app.graphicsDevice.canvas.getBoundingClientRect() : null);
Expand Down Expand Up @@ -103,6 +103,7 @@ export const usePicker = (app: AppBase | null, el: HTMLElement | null) => {
}, [picker])

const onFrameUpdate = useCallback(async () => {
if (pointerEvents.size === 0) return;

const e : PointerEvent | null = pointerDetails.current;
if (!picker || !app || !e) return null;
Expand Down Expand Up @@ -136,7 +137,7 @@ export const usePicker = (app: AppBase | null, el: HTMLElement | null) => {

return null;

}, [picker] );
}, [picker, pointerEvents] );

// Construct a generic handler for pointer events
const onInteractionEvent = useCallback(async (e: MouseEvent) => {
Expand All @@ -153,7 +154,7 @@ export const usePicker = (app: AppBase | null, el: HTMLElement | null) => {

propagateEvent(entity, syntheticEvent);

}, [picker]);
}, [picker, pointerEvents] );

useLayoutEffect(() => {
if (!picker || !el || !app) return;
Expand All @@ -172,5 +173,5 @@ export const usePicker = (app: AppBase | null, el: HTMLElement | null) => {
el.removeEventListener('pointermove', onPointerMove);
app.off('update', onFrameUpdate);
};
}, [app, el, onInteractionEvent]);
}, [app, el, onInteractionEvent, pointerEvents]);
}
Loading