main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

605
node_modules/three/examples/jsm/interactive/HTMLMesh.js generated vendored Normal file
View File

@@ -0,0 +1,605 @@
import {
CanvasTexture,
LinearFilter,
Mesh,
MeshBasicMaterial,
PlaneGeometry,
SRGBColorSpace,
Color
} from 'three';
/**
* This class can be used to render a DOM element onto a canvas and use it as a texture
* for a plane mesh.
*
* A typical use case for this class is to render the GUI of `lil-gui` as a texture so it
* is compatible for VR.
*
* ```js
* const gui = new GUI( { width: 300 } ); // create lil-gui instance
*
* const mesh = new HTMLMesh( gui.domElement );
* scene.add( mesh );
* ```
*
* @augments Mesh
* @three_import import { HTMLMesh } from 'three/addons/interactive/HTMLMesh.js';
*/
class HTMLMesh extends Mesh {
/**
* Constructs a new HTML mesh.
*
* @param {HTMLElement} dom - The DOM element to display as a plane mesh.
*/
constructor( dom ) {
const texture = new HTMLTexture( dom );
const geometry = new PlaneGeometry( texture.image.width * 0.001, texture.image.height * 0.001 );
const material = new MeshBasicMaterial( { map: texture, toneMapped: false, transparent: true } );
super( geometry, material );
function onEvent( event ) {
material.map.dispatchDOMEvent( event );
}
this.addEventListener( 'mousedown', onEvent );
this.addEventListener( 'mousemove', onEvent );
this.addEventListener( 'mouseup', onEvent );
this.addEventListener( 'click', onEvent );
/**
* Frees the GPU-related resources allocated by this instance and removes all event listeners.
* Call this method whenever this instance is no longer used in your app.
*/
this.dispose = function () {
geometry.dispose();
material.dispose();
material.map.dispose();
canvases.delete( dom );
this.removeEventListener( 'mousedown', onEvent );
this.removeEventListener( 'mousemove', onEvent );
this.removeEventListener( 'mouseup', onEvent );
this.removeEventListener( 'click', onEvent );
};
}
}
class HTMLTexture extends CanvasTexture {
constructor( dom ) {
super( html2canvas( dom ) );
this.dom = dom;
this.anisotropy = 16;
this.colorSpace = SRGBColorSpace;
this.minFilter = LinearFilter;
this.magFilter = LinearFilter;
this.generateMipmaps = false;
// Create an observer on the DOM, and run html2canvas update in the next loop
const observer = new MutationObserver( () => {
if ( ! this.scheduleUpdate ) {
// ideally should use xr.requestAnimationFrame, here setTimeout to avoid passing the renderer
this.scheduleUpdate = setTimeout( () => this.update(), 16 );
}
} );
const config = { attributes: true, childList: true, subtree: true, characterData: true };
observer.observe( dom, config );
this.observer = observer;
}
dispatchDOMEvent( event ) {
if ( event.data ) {
htmlevent( this.dom, event.type, event.data.x, event.data.y );
}
}
update() {
this.image = html2canvas( this.dom );
this.needsUpdate = true;
this.scheduleUpdate = null;
}
dispose() {
if ( this.observer ) {
this.observer.disconnect();
}
this.scheduleUpdate = clearTimeout( this.scheduleUpdate );
super.dispose();
}
}
//
const canvases = new WeakMap();
function html2canvas( element ) {
const range = document.createRange();
const color = new Color();
function Clipper( context ) {
const clips = [];
let isClipping = false;
function doClip() {
if ( isClipping ) {
isClipping = false;
context.restore();
}
if ( clips.length === 0 ) return;
let minX = - Infinity, minY = - Infinity;
let maxX = Infinity, maxY = Infinity;
for ( let i = 0; i < clips.length; i ++ ) {
const clip = clips[ i ];
minX = Math.max( minX, clip.x );
minY = Math.max( minY, clip.y );
maxX = Math.min( maxX, clip.x + clip.width );
maxY = Math.min( maxY, clip.y + clip.height );
}
context.save();
context.beginPath();
context.rect( minX, minY, maxX - minX, maxY - minY );
context.clip();
isClipping = true;
}
return {
add: function ( clip ) {
clips.push( clip );
doClip();
},
remove: function () {
clips.pop();
doClip();
}
};
}
function drawText( style, x, y, string ) {
if ( string !== '' ) {
if ( style.textTransform === 'uppercase' ) {
string = string.toUpperCase();
}
context.font = style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
context.textBaseline = 'top';
context.fillStyle = style.color;
context.fillText( string, x, y + parseFloat( style.fontSize ) * 0.1 );
}
}
function buildRectPath( x, y, w, h, r ) {
if ( w < 2 * r ) r = w / 2;
if ( h < 2 * r ) r = h / 2;
context.beginPath();
context.moveTo( x + r, y );
context.arcTo( x + w, y, x + w, y + h, r );
context.arcTo( x + w, y + h, x, y + h, r );
context.arcTo( x, y + h, x, y, r );
context.arcTo( x, y, x + w, y, r );
context.closePath();
}
function drawBorder( style, which, x, y, width, height ) {
const borderWidth = style[ which + 'Width' ];
const borderStyle = style[ which + 'Style' ];
const borderColor = style[ which + 'Color' ];
if ( borderWidth !== '0px' && borderStyle !== 'none' && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' ) {
context.strokeStyle = borderColor;
context.lineWidth = parseFloat( borderWidth );
context.beginPath();
context.moveTo( x, y );
context.lineTo( x + width, y + height );
context.stroke();
}
}
function drawElement( element, style ) {
// Do not render invisible elements, comments and scripts.
if ( element.nodeType === Node.COMMENT_NODE || element.nodeName === 'SCRIPT' || ( element.style && element.style.display === 'none' ) ) {
return;
}
let x = 0, y = 0, width = 0, height = 0;
if ( element.nodeType === Node.TEXT_NODE ) {
// text
range.selectNode( element );
const rect = range.getBoundingClientRect();
x = rect.left - offset.left - 0.5;
y = rect.top - offset.top - 0.5;
width = rect.width;
height = rect.height;
drawText( style, x, y, element.nodeValue.trim() );
} else if ( element instanceof HTMLCanvasElement ) {
// Canvas element
const rect = element.getBoundingClientRect();
x = rect.left - offset.left - 0.5;
y = rect.top - offset.top - 0.5;
context.save();
const dpr = window.devicePixelRatio;
context.scale( 1 / dpr, 1 / dpr );
context.drawImage( element, x, y );
context.restore();
} else if ( element instanceof HTMLImageElement ) {
const rect = element.getBoundingClientRect();
x = rect.left - offset.left - 0.5;
y = rect.top - offset.top - 0.5;
width = rect.width;
height = rect.height;
context.drawImage( element, x, y, width, height );
} else {
const rect = element.getBoundingClientRect();
x = rect.left - offset.left - 0.5;
y = rect.top - offset.top - 0.5;
width = rect.width;
height = rect.height;
style = window.getComputedStyle( element );
// Get the border of the element used for fill and border
buildRectPath( x, y, width, height, parseFloat( style.borderRadius ) );
const backgroundColor = style.backgroundColor;
if ( backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' ) {
context.fillStyle = backgroundColor;
context.fill();
}
// If all the borders match then stroke the round rectangle
const borders = [ 'borderTop', 'borderLeft', 'borderBottom', 'borderRight' ];
let match = true;
let prevBorder = null;
for ( const border of borders ) {
if ( prevBorder !== null ) {
match = ( style[ border + 'Width' ] === style[ prevBorder + 'Width' ] ) &&
( style[ border + 'Color' ] === style[ prevBorder + 'Color' ] ) &&
( style[ border + 'Style' ] === style[ prevBorder + 'Style' ] );
}
if ( match === false ) break;
prevBorder = border;
}
if ( match === true ) {
// They all match so stroke the rectangle from before allows for border-radius
const width = parseFloat( style.borderTopWidth );
if ( style.borderTopWidth !== '0px' && style.borderTopStyle !== 'none' && style.borderTopColor !== 'transparent' && style.borderTopColor !== 'rgba(0, 0, 0, 0)' ) {
context.strokeStyle = style.borderTopColor;
context.lineWidth = width;
context.stroke();
}
} else {
// Otherwise draw individual borders
drawBorder( style, 'borderTop', x, y, width, 0 );
drawBorder( style, 'borderLeft', x, y, 0, height );
drawBorder( style, 'borderBottom', x, y + height, width, 0 );
drawBorder( style, 'borderRight', x + width, y, 0, height );
}
if ( element instanceof HTMLInputElement ) {
let accentColor = style.accentColor;
if ( accentColor === undefined || accentColor === 'auto' ) accentColor = style.color;
color.set( accentColor );
const luminance = Math.sqrt( 0.299 * ( color.r ** 2 ) + 0.587 * ( color.g ** 2 ) + 0.114 * ( color.b ** 2 ) );
const accentTextColor = luminance < 0.5 ? 'white' : '#111111';
if ( element.type === 'radio' ) {
buildRectPath( x, y, width, height, height );
context.fillStyle = 'white';
context.strokeStyle = accentColor;
context.lineWidth = 1;
context.fill();
context.stroke();
if ( element.checked ) {
buildRectPath( x + 2, y + 2, width - 4, height - 4, height );
context.fillStyle = accentColor;
context.strokeStyle = accentTextColor;
context.lineWidth = 2;
context.fill();
context.stroke();
}
}
if ( element.type === 'checkbox' ) {
buildRectPath( x, y, width, height, 2 );
context.fillStyle = element.checked ? accentColor : 'white';
context.strokeStyle = element.checked ? accentTextColor : accentColor;
context.lineWidth = 1;
context.stroke();
context.fill();
if ( element.checked ) {
const currentTextAlign = context.textAlign;
context.textAlign = 'center';
const properties = {
color: accentTextColor,
fontFamily: style.fontFamily,
fontSize: height + 'px',
fontWeight: 'bold'
};
drawText( properties, x + ( width / 2 ), y, '✔' );
context.textAlign = currentTextAlign;
}
}
if ( element.type === 'range' ) {
const [ min, max, value ] = [ 'min', 'max', 'value' ].map( property => parseFloat( element[ property ] ) );
const position = ( ( value - min ) / ( max - min ) ) * ( width - height );
buildRectPath( x, y + ( height / 4 ), width, height / 2, height / 4 );
context.fillStyle = accentTextColor;
context.strokeStyle = accentColor;
context.lineWidth = 1;
context.fill();
context.stroke();
buildRectPath( x, y + ( height / 4 ), position + ( height / 2 ), height / 2, height / 4 );
context.fillStyle = accentColor;
context.fill();
buildRectPath( x + position, y, height, height, height / 2 );
context.fillStyle = accentColor;
context.fill();
}
if ( element.type === 'color' || element.type === 'text' || element.type === 'number' || element.type === 'email' || element.type === 'password' ) {
clipper.add( { x: x, y: y, width: width, height: height } );
const displayValue = element.type === 'password' ? '*'.repeat( element.value.length ) : element.value;
drawText( style, x + parseInt( style.paddingLeft ), y + parseInt( style.paddingTop ), displayValue );
clipper.remove();
}
}
}
/*
// debug
context.strokeStyle = '#' + Math.random().toString( 16 ).slice( - 3 );
context.strokeRect( x - 0.5, y - 0.5, width + 1, height + 1 );
*/
const isClipping = style.overflow === 'auto' || style.overflow === 'hidden';
if ( isClipping ) clipper.add( { x: x, y: y, width: width, height: height } );
for ( let i = 0; i < element.childNodes.length; i ++ ) {
drawElement( element.childNodes[ i ], style );
}
if ( isClipping ) clipper.remove();
}
const offset = element.getBoundingClientRect();
let canvas = canvases.get( element );
if ( canvas === undefined ) {
canvas = document.createElement( 'canvas' );
canvas.width = offset.width;
canvas.height = offset.height;
canvases.set( element, canvas );
}
const context = canvas.getContext( '2d'/*, { alpha: false }*/ );
const clipper = new Clipper( context );
// console.time( 'drawElement' );
context.clearRect( 0, 0, canvas.width, canvas.height );
drawElement( element );
// console.timeEnd( 'drawElement' );
return canvas;
}
function htmlevent( element, event, x, y ) {
const mouseEventInit = {
clientX: ( x * element.offsetWidth ) + element.offsetLeft,
clientY: ( y * element.offsetHeight ) + element.offsetTop,
view: element.ownerDocument.defaultView
};
window.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
const rect = element.getBoundingClientRect();
x = x * rect.width + rect.left;
y = y * rect.height + rect.top;
function traverse( element ) {
if ( element.nodeType !== Node.TEXT_NODE && element.nodeType !== Node.COMMENT_NODE ) {
const rect = element.getBoundingClientRect();
if ( x > rect.left && x < rect.right && y > rect.top && y < rect.bottom ) {
element.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
if ( element instanceof HTMLInputElement && element.type === 'range' && ( event === 'mousedown' || event === 'click' ) ) {
const [ min, max ] = [ 'min', 'max' ].map( property => parseFloat( element[ property ] ) );
const width = rect.width;
const offsetX = x - rect.x;
const proportion = offsetX / width;
element.value = min + ( max - min ) * proportion;
element.dispatchEvent( new InputEvent( 'input', { bubbles: true } ) );
}
if ( element instanceof HTMLInputElement && ( element.type === 'text' || element.type === 'number' || element.type === 'email' || element.type === 'password' ) && ( event === 'mousedown' || event === 'click' ) ) {
element.focus();
}
}
for ( let i = 0; i < element.childNodes.length; i ++ ) {
traverse( element.childNodes[ i ] );
}
}
}
traverse( element );
}
export { HTMLMesh };

View File

@@ -0,0 +1,224 @@
import {
Group,
Raycaster,
Vector2
} from 'three';
const _pointer = new Vector2();
const _event = { type: '', data: _pointer };
// The XR events that are mapped to "standard" pointer events.
const _events = {
'move': 'mousemove',
'select': 'click',
'selectstart': 'mousedown',
'selectend': 'mouseup'
};
const _raycaster = new Raycaster();
/**
* This class can be used to group 3D objects in an interactive group.
* The group itself can listen to Pointer, Mouse or XR controller events to
* detect selections of descendant 3D objects. If a 3D object is selected,
* the respective event is going to dispatched to it.
*
* ```js
* const group = new InteractiveGroup();
* group.listenToPointerEvents( renderer, camera );
* group.listenToXRControllerEvents( controller1 );
* group.listenToXRControllerEvents( controller2 );
* scene.add( group );
*
* // now add objects that should be interactive
* group.add( mesh1, mesh2, mesh3 );
* ```
* @augments Group
* @three_import import { InteractiveGroup } from 'three/addons/interactive/InteractiveGroup.js';
*/
class InteractiveGroup extends Group {
constructor() {
super();
/**
* The internal raycaster.
*
* @type {Raycaster}
*/
this.raycaster = new Raycaster();
/**
* The internal raycaster.
*
* @type {?HTMLDOMElement}
* @default null
*/
this.element = null;
/**
* The camera used for raycasting.
*
* @type {?Camera}
* @default null
*/
this.camera = null;
/**
* An array of XR controllers.
*
* @type {Array<Group>}
*/
this.controllers = [];
this._onPointerEvent = this.onPointerEvent.bind( this );
this._onXRControllerEvent = this.onXRControllerEvent.bind( this );
}
onPointerEvent( event ) {
event.stopPropagation();
const rect = this.element.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
this.raycaster.setFromCamera( _pointer, this.camera );
const intersects = this.raycaster.intersectObjects( this.children, false );
if ( intersects.length > 0 ) {
const intersection = intersects[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = event.type;
_event.data.set( uv.x, 1 - uv.y );
object.dispatchEvent( _event );
}
}
onXRControllerEvent( event ) {
const controller = event.target;
_raycaster.setFromXRController( controller );
const intersections = _raycaster.intersectObjects( this.children, false );
if ( intersections.length > 0 ) {
const intersection = intersections[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = _events[ event.type ];
_event.data.set( uv.x, 1 - uv.y );
object.dispatchEvent( _event );
}
}
/**
* Calling this method makes sure the interactive group listens to Pointer and Mouse events.
* The target is the `domElement` of the given renderer. The camera is required for the internal
* raycasting so 3D objects can be detected based on the events.
*
* @param {(WebGPURenderer|WebGLRenderer)} renderer - The renderer.
* @param {Camera} camera - The camera.
*/
listenToPointerEvents( renderer, camera ) {
this.camera = camera;
this.element = renderer.domElement;
this.element.addEventListener( 'pointerdown', this._onPointerEvent );
this.element.addEventListener( 'pointerup', this._onPointerEvent );
this.element.addEventListener( 'pointermove', this._onPointerEvent );
this.element.addEventListener( 'mousedown', this._onPointerEvent );
this.element.addEventListener( 'mouseup', this._onPointerEvent );
this.element.addEventListener( 'mousemove', this._onPointerEvent );
this.element.addEventListener( 'click', this._onPointerEvent );
}
/**
* Disconnects this interactive group from all Pointer and Mouse Events.
*/
disconnectionPointerEvents() {
if ( this.element !== null ) {
this.element.removeEventListener( 'pointerdown', this._onPointerEvent );
this.element.removeEventListener( 'pointerup', this._onPointerEvent );
this.element.removeEventListener( 'pointermove', this._onPointerEvent );
this.element.removeEventListener( 'mousedown', this._onPointerEvent );
this.element.removeEventListener( 'mouseup', this._onPointerEvent );
this.element.removeEventListener( 'mousemove', this._onPointerEvent );
this.element.removeEventListener( 'click', this._onPointerEvent );
}
}
/**
* Calling this method makes sure the interactive group listens to events of
* the given XR controller.
*
* @param {Group} controller - The XR controller.
*/
listenToXRControllerEvents( controller ) {
this.controllers.push( controller );
controller.addEventListener( 'move', this._onXRControllerEvent );
controller.addEventListener( 'select', this._onXRControllerEvent );
controller.addEventListener( 'selectstart', this._onXRControllerEvent );
controller.addEventListener( 'selectend', this._onXRControllerEvent );
}
/**
* Disconnects this interactive group from all XR controllers.
*/
disconnectXrControllerEvents() {
for ( const controller of this.controllers ) {
controller.removeEventListener( 'move', this._onXRControllerEvent );
controller.removeEventListener( 'select', this._onXRControllerEvent );
controller.removeEventListener( 'selectstart', this._onXRControllerEvent );
controller.removeEventListener( 'selectend', this._onXRControllerEvent );
}
}
/**
* Disconnects this interactive group from the DOM and all XR controllers.
*/
disconnect() {
this.disconnectionPointerEvents();
this.disconnectXrControllerEvents();
this.camera = null;
this.element = null;
this.controllers = [];
}
}
export { InteractiveGroup };

View File

@@ -0,0 +1,294 @@
import {
Frustum,
Vector3,
Matrix4,
Quaternion,
} from 'three';
const _frustum = new Frustum();
const _center = new Vector3();
const _tmpPoint = new Vector3();
const _vecNear = new Vector3();
const _vecTopLeft = new Vector3();
const _vecTopRight = new Vector3();
const _vecDownRight = new Vector3();
const _vecDownLeft = new Vector3();
const _vecFarTopLeft = new Vector3();
const _vecFarTopRight = new Vector3();
const _vecFarDownRight = new Vector3();
const _vecFarDownLeft = new Vector3();
const _vectemp1 = new Vector3();
const _vectemp2 = new Vector3();
const _vectemp3 = new Vector3();
const _matrix = new Matrix4();
const _quaternion = new Quaternion();
const _scale = new Vector3();
/**
* This class can be used to select 3D objects in a scene with a selection box.
* It is recommended to visualize the selected area with the help of {@link SelectionHelper}.
*
* ```js
* const selectionBox = new SelectionBox( camera, scene );
* const selectedObjects = selectionBox.select( startPoint, endPoint );
* ```
*
* @three_import import { SelectionBox } from 'three/addons/interactive/SelectionBox.js';
*/
class SelectionBox {
/**
* Constructs a new selection box.
*
* @param {Camera} camera - The camera the scene is rendered with.
* @param {Scene} scene - The scene.
* @param {number} [deep=Number.MAX_VALUE] - How deep the selection frustum of perspective cameras should extend.
*/
constructor( camera, scene, deep = Number.MAX_VALUE ) {
/**
* The camera the scene is rendered with.
*
* @type {Camera}
*/
this.camera = camera;
/**
* The camera the scene is rendered with.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The start point of the selection.
*
* @type {Vector3}
*/
this.startPoint = new Vector3();
/**
* The end point of the selection.
*
* @type {Vector3}
*/
this.endPoint = new Vector3();
/**
* The selected 3D objects.
*
* @type {Array<Object3D>}
*/
this.collection = [];
/**
* The selected instance IDs of instanced meshes.
*
* @type {Object}
*/
this.instances = {};
/**
* How deep the selection frustum of perspective cameras should extend.
*
* @type {number}
* @default Number.MAX_VALUE
*/
this.deep = deep;
}
/**
* This method selects 3D objects in the scene based on the given start
* and end point. If no parameters are provided, the method uses the start
* and end values of the respective members.
*
* @param {Vector3} [startPoint] - The start point.
* @param {Vector3} [endPoint] - The end point.
* @return {Array<Object3D>} The selected 3D objects.
*/
select( startPoint, endPoint ) {
this.startPoint = startPoint || this.startPoint;
this.endPoint = endPoint || this.endPoint;
this.collection = [];
this._updateFrustum( this.startPoint, this.endPoint );
this._searchChildInFrustum( _frustum, this.scene );
return this.collection;
}
// private
_updateFrustum( startPoint, endPoint ) {
startPoint = startPoint || this.startPoint;
endPoint = endPoint || this.endPoint;
// Avoid invalid frustum
if ( startPoint.x === endPoint.x ) {
endPoint.x += Number.EPSILON;
}
if ( startPoint.y === endPoint.y ) {
endPoint.y += Number.EPSILON;
}
this.camera.updateProjectionMatrix();
this.camera.updateMatrixWorld();
if ( this.camera.isPerspectiveCamera ) {
_tmpPoint.copy( startPoint );
_tmpPoint.x = Math.min( startPoint.x, endPoint.x );
_tmpPoint.y = Math.max( startPoint.y, endPoint.y );
endPoint.x = Math.max( startPoint.x, endPoint.x );
endPoint.y = Math.min( startPoint.y, endPoint.y );
_vecNear.setFromMatrixPosition( this.camera.matrixWorld );
_vecTopLeft.copy( _tmpPoint );
_vecTopRight.set( endPoint.x, _tmpPoint.y, 0 );
_vecDownRight.copy( endPoint );
_vecDownLeft.set( _tmpPoint.x, endPoint.y, 0 );
_vecTopLeft.unproject( this.camera );
_vecTopRight.unproject( this.camera );
_vecDownRight.unproject( this.camera );
_vecDownLeft.unproject( this.camera );
_vectemp1.copy( _vecTopLeft ).sub( _vecNear );
_vectemp2.copy( _vecTopRight ).sub( _vecNear );
_vectemp3.copy( _vecDownRight ).sub( _vecNear );
_vectemp1.normalize();
_vectemp2.normalize();
_vectemp3.normalize();
_vectemp1.multiplyScalar( this.deep );
_vectemp2.multiplyScalar( this.deep );
_vectemp3.multiplyScalar( this.deep );
_vectemp1.add( _vecNear );
_vectemp2.add( _vecNear );
_vectemp3.add( _vecNear );
const planes = _frustum.planes;
planes[ 0 ].setFromCoplanarPoints( _vecNear, _vecTopLeft, _vecTopRight );
planes[ 1 ].setFromCoplanarPoints( _vecNear, _vecTopRight, _vecDownRight );
planes[ 2 ].setFromCoplanarPoints( _vecDownRight, _vecDownLeft, _vecNear );
planes[ 3 ].setFromCoplanarPoints( _vecDownLeft, _vecTopLeft, _vecNear );
planes[ 4 ].setFromCoplanarPoints( _vecTopRight, _vecDownRight, _vecDownLeft );
planes[ 5 ].setFromCoplanarPoints( _vectemp3, _vectemp2, _vectemp1 );
planes[ 5 ].normal.multiplyScalar( - 1 );
} else if ( this.camera.isOrthographicCamera ) {
const left = Math.min( startPoint.x, endPoint.x );
const top = Math.max( startPoint.y, endPoint.y );
const right = Math.max( startPoint.x, endPoint.x );
const down = Math.min( startPoint.y, endPoint.y );
_vecTopLeft.set( left, top, - 1 );
_vecTopRight.set( right, top, - 1 );
_vecDownRight.set( right, down, - 1 );
_vecDownLeft.set( left, down, - 1 );
_vecFarTopLeft.set( left, top, 1 );
_vecFarTopRight.set( right, top, 1 );
_vecFarDownRight.set( right, down, 1 );
_vecFarDownLeft.set( left, down, 1 );
_vecTopLeft.unproject( this.camera );
_vecTopRight.unproject( this.camera );
_vecDownRight.unproject( this.camera );
_vecDownLeft.unproject( this.camera );
_vecFarTopLeft.unproject( this.camera );
_vecFarTopRight.unproject( this.camera );
_vecFarDownRight.unproject( this.camera );
_vecFarDownLeft.unproject( this.camera );
const planes = _frustum.planes;
planes[ 0 ].setFromCoplanarPoints( _vecTopLeft, _vecFarTopLeft, _vecFarTopRight );
planes[ 1 ].setFromCoplanarPoints( _vecTopRight, _vecFarTopRight, _vecFarDownRight );
planes[ 2 ].setFromCoplanarPoints( _vecFarDownRight, _vecFarDownLeft, _vecDownLeft );
planes[ 3 ].setFromCoplanarPoints( _vecFarDownLeft, _vecFarTopLeft, _vecTopLeft );
planes[ 4 ].setFromCoplanarPoints( _vecTopRight, _vecDownRight, _vecDownLeft );
planes[ 5 ].setFromCoplanarPoints( _vecFarDownRight, _vecFarTopRight, _vecFarTopLeft );
planes[ 5 ].normal.multiplyScalar( - 1 );
} else {
console.error( 'THREE.SelectionBox: Unsupported camera type.' );
}
}
_searchChildInFrustum( frustum, object ) {
if ( object.isMesh || object.isLine || object.isPoints ) {
if ( object.isInstancedMesh ) {
this.instances[ object.uuid ] = [];
for ( let instanceId = 0; instanceId < object.count; instanceId ++ ) {
object.getMatrixAt( instanceId, _matrix );
_matrix.decompose( _center, _quaternion, _scale );
_center.applyMatrix4( object.matrixWorld );
if ( frustum.containsPoint( _center ) ) {
this.instances[ object.uuid ].push( instanceId );
}
}
} else {
if ( object.geometry.boundingSphere === null ) object.geometry.computeBoundingSphere();
_center.copy( object.geometry.boundingSphere.center );
_center.applyMatrix4( object.matrixWorld );
if ( frustum.containsPoint( _center ) ) {
this.collection.push( object );
}
}
}
if ( object.children.length > 0 ) {
for ( let x = 0; x < object.children.length; x ++ ) {
this._searchChildInFrustum( frustum, object.children[ x ] );
}
}
}
}
export { SelectionBox };

View File

@@ -0,0 +1,150 @@
import { Vector2 } from 'three';
/**
* A helper for {@link SelectionBox}.
*
* It visualizes the current selection box with a `div` container element.
*
* @three_import import { SelectionHelper } from 'three/addons/interactive/SelectionHelper.js';
*/
class SelectionHelper {
/**
* Constructs a new selection helper.
*
* @param {(WebGPURenderer|WebGLRenderer)} renderer - The renderer.
* @param {string} cssClassName - The CSS class name of the `div`.
*/
constructor( renderer, cssClassName ) {
/**
* The visualization of the selection box.
*
* @type {HTMLDivElement}
*/
this.element = document.createElement( 'div' );
this.element.classList.add( cssClassName );
this.element.style.pointerEvents = 'none';
/**
* A reference to the renderer.
*
* @type {(WebGPURenderer|WebGLRenderer)}
*/
this.renderer = renderer;
/**
* Whether the mouse or pointer is pressed down.
*
* @type {boolean}
* @default false
*/
this.isDown = false;
/**
* Whether helper is enabled or not.
*
* @type {boolean}
* @default true
*/
this.enabled = true;
// private
this._startPoint = new Vector2();
this._pointTopLeft = new Vector2();
this._pointBottomRight = new Vector2();
this._onPointerDown = function ( event ) {
if ( this.enabled === false ) return;
this.isDown = true;
this._onSelectStart( event );
}.bind( this );
this._onPointerMove = function ( event ) {
if ( this.enabled === false ) return;
if ( this.isDown ) {
this._onSelectMove( event );
}
}.bind( this );
this._onPointerUp = function ( ) {
if ( this.enabled === false ) return;
this.isDown = false;
this._onSelectOver();
}.bind( this );
this.renderer.domElement.addEventListener( 'pointerdown', this._onPointerDown );
this.renderer.domElement.addEventListener( 'pointermove', this._onPointerMove );
this.renderer.domElement.addEventListener( 'pointerup', this._onPointerUp );
}
/**
* Call this method if you no longer want use to the controls. It frees all internal
* resources and removes all event listeners.
*/
dispose() {
this.renderer.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
this.renderer.domElement.removeEventListener( 'pointermove', this._onPointerMove );
this.renderer.domElement.removeEventListener( 'pointerup', this._onPointerUp );
this.element.remove(); // in case disposal happens while dragging
}
// private
_onSelectStart( event ) {
this.element.style.display = 'none';
this.renderer.domElement.parentElement.appendChild( this.element );
this.element.style.left = event.clientX + 'px';
this.element.style.top = event.clientY + 'px';
this.element.style.width = '0px';
this.element.style.height = '0px';
this._startPoint.x = event.clientX;
this._startPoint.y = event.clientY;
}
_onSelectMove( event ) {
this.element.style.display = 'block';
this._pointBottomRight.x = Math.max( this._startPoint.x, event.clientX );
this._pointBottomRight.y = Math.max( this._startPoint.y, event.clientY );
this._pointTopLeft.x = Math.min( this._startPoint.x, event.clientX );
this._pointTopLeft.y = Math.min( this._startPoint.y, event.clientY );
this.element.style.left = this._pointTopLeft.x + 'px';
this.element.style.top = this._pointTopLeft.y + 'px';
this.element.style.width = ( this._pointBottomRight.x - this._pointTopLeft.x ) + 'px';
this.element.style.height = ( this._pointBottomRight.y - this._pointTopLeft.y ) + 'px';
}
_onSelectOver() {
this.element.remove();
}
}
export { SelectionHelper };