pmndrs-pointer-events
Skill1 starsChanged 30 days agoMITView on GitHub
Use @pmndrs/pointer-events to give a plain three.js scene DOM-style pointer events (click, hover, drag with pointer capture) from mouse/touch, XR controllers, hands, or custom inputs, and to forward events through render-target screens or portals. Use when wiring events into a vanilla three.js scene or building a custom pointer; in React Three Fiber, use @react-three/xr (`PointerEvents`, `useRayPointer`, ...) instead of calling this package directly.
In this repository58 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- cf-cliSkill
- durable-objectsSkill
- game-gdd-authorSkill
- game-playtestSkill
- pmndrs-handleSkill
- pmndrs-uikitSkill
- prototype-fastSkill
- pwa-offlineSkill
- review-game-trailerSkill
- scenario-blender-expertSkill
- scenario-blender-retopologySkill
- scenario-blender-uv-bakingSkill
- threejs-perf-loadingSkill
- webapp-testingSkill
- workers-best-practicesSkill
- wranglerSkill
- xr-perceptual-constraints-displays-and-trackingSkill
- xr-platforms-audio-and-comfortSkill
- xr-referenceSkill
- xr-rendering-input-and-spatial-understandingSkill
- xr-ux-design-and-performance-budgetingSkill
- horizon-launch-roadmapSkill
- horizon-store-listing-assetsSkill
- horizon-web-store-publishSkill
- horizon-web-vrc-checklistSkill
- iwsdk-asset-pipelineSkill
- iwsdk-dev-workflowSkill
- iwsdk-performanceSkill
- iwsdk-project-setupSkill
- iwsdk-quest-device-testingSkill
- iwsdk-testingSkill
- iwsdk-vr-glassesSkill
- quest-browser-webxrSkill
- iwsdk-build-modelSkill
- iwsdk-compose-sceneSkill
- iwsdk-debugSkill
- iwsdk-depth-occlusionSkill
- iwsdk-devSkill
- iwsdk-grabSkill
- iwsdk-hostingSkill
- iwsdk-migrate-0-5Skill
- iwsdk-native-xr-testSkill
- iwsdk-physicsSkill
- iwsdk-pwa-packagingSkill
- iwsdk-raySkill
- iwsdk-uiSkill
- hz-immersive-designerSkill
- hz-iwsdk-webxrSkill
- hz-new-project-creationSkill
- hz-perfetto-debugSkill
- hz-quest-verify-firstSkill
- hz-simpleperf-debugSkill
- hz-store-pwaSkill
- hz-store-submitSkill
- hz-vr-debugSkill
- metavr-cliSkill
The file
Files
@pmndrs/pointer-events
Objects receive events through three's own addEventListener, with the same names and semantics as DOM pointer events: pointerdown/up/move/over/out/enter/leave/cancel, click, dblclick, contextmenu, wheel. Every event source — mouse, touch, an XR ray, a grabbing hand — is a pointer, so one listener works for all of them.
Choosing an entry point
- Screen input (mouse, touch, pen):
forwardHtmlEvents(canvas, () => camera, scene). - Events hitting a mesh that shows another scene (a render-target screen, a portal, an in-world editor viewport):
forwardObjectEvents(mesh, () => innerCamera, innerScene). The hit'suvbecomes the screen coordinate for the inner camera, so the mesh needs UVs. - A tracked object that points or touches (XR controller, hand, a tool in the scene):
createRayPointerfor a straight ray along the object's -Z,createLinesPointerfor a curved/polyline ray (teleport arcs),createGrabPointerfor a sphere you press withdown/upyourself (squeeze to grab),createTouchPointerfor a fingertip sphere that presses on its own when close enough. - Several pointers on the same hand: register them in one
CombinedPointerso the scene is traversed once per frame instead of once per pointer, and only the nearest pointer stays active when constructed withenableMultiplePointers = false.
Wiring
const { update, destroy } = forwardHtmlEvents(canvas, () => camera, scene)
mesh.addEventListener('click', (e) => console.log(e.point))
renderer.setAnimationLoop(() => {
update() // delivers the DOM events batched since the last frame
renderer.render(scene, camera)
})
// on teardown: destroy()Events are batched by default and only delivered inside update(); nothing fires if update() is never called. Batching moves handlers out of the browser's user-gesture call stack, so actions that require a gesture (playing a video, opening a file picker in Safari) fail from a click handler — pass { batchEvents: false } for those, or when the scene is not rendered every frame. Pass intersectEveryFrame: true when objects move under a resting mouse and hover should follow them.
A pointer you create yourself is driven every frame: pointer.move(scene, { timeStamp }), then down/up({ timeStamp, button }) on input, and exit(...) when the input source disappears so hovered objects receive pointerout/pointerleave. The pointer reads the space object's matrix, not its position/rotation, so update matrices before moving it. Objects moved after the last render need scene.updateMatrixWorld() before move/update.
Which objects get hit
pointerEventsworks like CSS and is inherited:'none'excludes an object (a child can opt back in with'auto'),'auto'always hits. The default is'listener': an object is hit only if it or an ancestor has a pointer listener. Content without listeners — particles, decoration, transparent overlays — neither receives events nor blocks them. A listener on theScenetherefore makes the whole scene hittable.pointerEventsTypefilters by pointer type:{ allow: 'ray' },{ deny: ['grab', 'touch'] }, or a function. Types arescreen-mouse,screen-touch,screen-penfor DOM input,ray,lines,grab,touchfor created pointers, andforward-<outer type>insideforwardObjectEvents. A common use:{ deny: 'grab' }on a button so a hand passing through it does not click it.pointerEventsOrder(higher first, likerenderOrder) beats distance, which lets an x-ray overlay or gizmo win over the object it sits inside. Grab and touch pointers sort by distance only.- These properties are not copied by
object.clone().
Easy to get wrong
- Dragging: call
e.object.setPointerCapture(e.pointerId)inpointerdown; later events go to that object even when the pointer leaves it, ande.pointkeeps following the pointer on the plane of the originally hit face. Capture is released onup,cancel, andexit.e.pointerIdis the 3D pointer id, not the DOM one. - Missed clicks: a pointer that hits nothing hits the scene's void object (
getVoidObject(scene), flaggedisVoidObject). Those events bubble to listeners on the scene, so a scene-levelclicklistener also fires on empty space; checke.object.isVoidObjector listen on the void object to handle "clicked nothing". click,dblclick, andcontextmenuare synthesized frompointerdown/pointerupon the same object, not taken from the DOM.pointerenter/pointerleavedo not bubble; every other event bubbles through.parentand honorsstopPropagation().- With React Three Fiber, using this package next to R3F's own event system fires events twice; @react-three/xr's
noEvents+PointerEventsreplace R3F's system with this one.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

