|
|
@@ -0,0 +1,226 @@
|
|
|
+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 };
|