three
Version:
JavaScript 3D library
227 lines (163 loc) • 4.84 kB
JavaScript
import {
Matrix4,
Vector3
} from 'three';
const _pixelToLocal = new Matrix4();
const _mvp = new Matrix4();
const _viewport = new Matrix4();
const _size = new Vector3();
/**
* Manages interaction for 3D objects independently of the scene graph.
*
* For objects with an {@link HTMLTexture}, the manager computes CSS `matrix3d`
* transforms each frame so the underlying HTML elements stay aligned with
* their meshes. Because the elements are children of the canvas, the browser
* dispatches pointer events to them natively.
*
* ```js
* const interactions = new InteractionManager();
* interactions.connect( renderer, camera );
*
* // Objects live anywhere in the scene graph
* scene.add( mesh );
*
* // Register for interaction separately
* interactions.add( mesh );
*
* // In the animation loop
* interactions.update();
* ```
* @three_import import { InteractionManager } from 'three/addons/interaction/InteractionManager.js';
*/
class InteractionManager {
constructor() {
/**
* The registered interactive objects.
*
* @type {Array<Object3D>}
*/
this.objects = [];
/**
* The canvas element.
*
* @type {?HTMLCanvasElement}
* @default null
*/
this.element = null;
/**
* The camera used for computing the element transforms.
*
* @type {?Camera}
* @default null
*/
this.camera = null;
this._cachedCssW = - 1;
this._cachedCssH = - 1;
}
/**
* Adds one or more objects to the manager.
*
* @param {...Object3D} objects - The objects to add.
* @return {this}
*/
add( ...objects ) {
for ( const object of objects ) {
if ( this.objects.indexOf( object ) === - 1 ) {
this.objects.push( object );
}
}
return this;
}
/**
* Removes one or more objects from the manager.
*
* @param {...Object3D} objects - The objects to remove.
* @return {this}
*/
remove( ...objects ) {
for ( const object of objects ) {
const index = this.objects.indexOf( object );
if ( index !== - 1 ) {
this.objects.splice( index, 1 );
}
}
return this;
}
/**
* Stores the renderer and camera needed for computing element transforms.
*
* @param {(WebGPURenderer|WebGLRenderer)} renderer - The renderer.
* @param {Camera} camera - The camera.
*/
connect( renderer, camera ) {
this.camera = camera;
this.element = renderer.domElement;
}
/**
* Updates the element transforms for all registered objects.
* Call this once per frame in the animation loop.
*/
update() {
const canvas = this.element;
const camera = this.camera;
if ( canvas === null || camera === null ) return;
// Viewport: NDC (-1,1) to canvas CSS pixels, Y flipped.
// Using CSS pixels (clientWidth/clientHeight) so the resulting matrix
// can be applied directly as a CSS transform without DPR conversion.
const cssW = canvas.clientWidth;
const cssH = canvas.clientHeight;
if ( cssW !== this._cachedCssW || cssH !== this._cachedCssH ) {
_viewport.set(
cssW / 2, 0, 0, cssW / 2,
0, - cssH / 2, 0, cssH / 2,
0, 0, 1, 0,
0, 0, 0, 1
);
this._cachedCssW = cssW;
this._cachedCssH = cssH;
}
for ( const object of this.objects ) {
const texture = object.material.map;
if ( ! texture || ! texture.isHTMLTexture ) continue;
const element = texture.image;
if ( ! element ) continue;
// Position at canvas origin so the CSS matrix3d maps correctly.
element.style.position = 'absolute';
element.style.left = '0';
element.style.top = '0';
element.style.transformOrigin = '0 0';
const elemW = element.offsetWidth;
const elemH = element.offsetHeight;
// Get mesh dimensions from geometry bounding box
const geometry = object.geometry;
if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
geometry.boundingBox.getSize( _size );
// Map element pixel coords (0,0)-(elemW,elemH) to mesh local coords.
// Front face: top-left at (-sizeX/2, sizeY/2, maxZ), bottom-right at (sizeX/2, -sizeY/2, maxZ).
_pixelToLocal.set(
_size.x / elemW, 0, 0, - _size.x / 2,
0, - _size.y / elemH, 0, _size.y / 2,
0, 0, 1, geometry.boundingBox.max.z,
0, 0, 0, 1
);
// Model-View-Projection
_mvp.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
_mvp.multiply( object.matrixWorld );
_mvp.multiply( _pixelToLocal );
// Apply viewport
_mvp.premultiply( _viewport );
// The browser performs the perspective divide (by w) when applying the matrix3d.
element.style.transform = 'matrix3d(' + _mvp.elements.join( ',' ) + ')';
}
}
/**
* Disconnects this manager, clearing the renderer and camera references.
*/
disconnect() {
this.camera = null;
this.element = null;
this._cachedCssW = - 1;
this._cachedCssH = - 1;
}
}
export { InteractionManager };