UNPKG

@playcanvas/react

Version:

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

151 lines 6.31 kB
import { Picker } from "playcanvas"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react"; import { SyntheticMouseEvent, SyntheticPointerEvent } from "./synthetic-event"; // Utility to propagate events up the entity hierarchy const propagateEvent = (entity, event, stopAt = null) => { while (entity) { if (entity === stopAt) return false; entity.fire(event.type, event); if (event.hasStoppedPropagation) return true; entity = entity.parent; } return false; }; const getNearestCommonAncestor = (a, b) => { const ancestors = new Set(); // Traverse up the parent chain of entity 'a' and add each ancestor to the set let current = a; while (current) { ancestors.add(current); current = current.parent; } // Traverse up the parent chain of entity 'b' and check against the set current = b; while (current) { if (ancestors.has(current)) { return current; // Found the nearest common ancestor } current = current.parent; } return null; // No common ancestor found }; const getEntityAtPointerEvent = async (app, picker, rect, e) => { // Find the highest priority camera const [activeCamera] = app.root.findComponents('camera') .filter((camera) => !camera.renderTarget) .sort((a, b) => a.priority - b.priority); if (!activeCamera) return null; // Get canvas bounds const canvas = app.graphicsDevice.canvas; if (!canvas || canvas.width === 0 || canvas.height === 0) return null; // Calculate position relative to canvas const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Account for canvas scaling const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; // prepare the picker and perform picking try { picker.prepare(activeCamera, app.scene); const [meshInstance] = await picker.getSelectionAsync(x * scaleX, y * scaleY); if (!meshInstance) return null; return meshInstance?.node; return null; } catch { // The picker can fail if the camera is not active or the canvas is not visible return null; } }; export const usePicker = (app, el, pointerEvents) => { const activeEntity = useRef(null); const pointerDetails = useRef(null); const canvasRectRef = useRef(app ? app.graphicsDevice.canvas.getBoundingClientRect() : null); // Construct a Global Picker const picker = useMemo(() => { if (!app || !app.graphicsDevice) return null; return new Picker(app, app.graphicsDevice.width, app.graphicsDevice.height); }, [app]); // Watch for the canvas to resize. Neccesary for correct picking useEffect(() => { const resizeObserver = new ResizeObserver(() => { canvasRectRef.current = app ? app.graphicsDevice.canvas.getBoundingClientRect() : null; if (canvasRectRef.current) { picker?.resize(canvasRectRef.current.width, canvasRectRef.current.height); } }); if (app) resizeObserver.observe(app.graphicsDevice.canvas); return () => resizeObserver.disconnect(); }, [app]); // Store pointer position const onPointerMove = useCallback((e) => { pointerDetails.current = e; }, [picker]); const onFrameUpdate = useCallback(async () => { if (pointerEvents.size === 0) return; const e = pointerDetails.current; if (!picker || !app || !e) return null; if (!canvasRectRef.current) return null; const entity = await getEntityAtPointerEvent(app, picker, canvasRectRef.current, e); // if (!entity) return null; const prevEntity = activeEntity.current; // Find the common ancestor of the current target and last event. We do not need to bubble past this const stopBubblingAt = getNearestCommonAncestor(prevEntity, entity); // If the pointer moves out of the current hovered entity (and its children) if (prevEntity && prevEntity !== entity) { const pointerOutEvent = new SyntheticPointerEvent(e); pointerOutEvent.type = 'pointerout'; propagateEvent(prevEntity, pointerOutEvent, stopBubblingAt); } // If the pointer moves over a new entity if (entity && entity !== prevEntity) { const pointerOverEvent = new SyntheticPointerEvent(e); pointerOverEvent.type = 'pointerover'; propagateEvent(entity, pointerOverEvent, stopBubblingAt); } // Update our reference activeEntity.current = entity; return null; }, [picker, pointerEvents]); // Construct a generic handler for pointer events const onInteractionEvent = useCallback(async (e) => { if (!picker || !app || !canvasRectRef.current) return; const entity = await getEntityAtPointerEvent(app, picker, canvasRectRef.current, e); if (!entity) return; // Handle other pointer events (down, up, move) const syntheticEvent = e instanceof PointerEvent ? new SyntheticPointerEvent(e) : new SyntheticMouseEvent(e); propagateEvent(entity, syntheticEvent); }, [picker, pointerEvents]); useLayoutEffect(() => { if (!picker || !el || !app) return; el.addEventListener('pointerup', onInteractionEvent); el.addEventListener('pointerdown', onInteractionEvent); el.addEventListener('mouseup', onInteractionEvent); el.addEventListener('click', onInteractionEvent); el.addEventListener('pointermove', onPointerMove); app.on('update', onFrameUpdate); return () => { el.removeEventListener('pointerup', onInteractionEvent); el.removeEventListener('pointerdown', onInteractionEvent); el.removeEventListener('click', onInteractionEvent); el.removeEventListener('pointermove', onPointerMove); app.off('update', onFrameUpdate); }; }, [app, el, onInteractionEvent, pointerEvents]); }; //# sourceMappingURL=picker.js.map