From e332019acb312ec64893c26cf3d797d5ce472f26 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:51:34 +0900 Subject: bluebey: ぶるべー スタジオのページを公開 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../three/examples/jsm/controls/OrbitControls.js | 1972 +++++++++++++++++++ .../examples/jsm/controls/TransformControls.js | 2003 ++++++++++++++++++++ 2 files changed, 3975 insertions(+) create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/controls/OrbitControls.js create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/controls/TransformControls.js (limited to 'public/bluebey-studio/vendor/three/examples/jsm/controls') diff --git a/public/bluebey-studio/vendor/three/examples/jsm/controls/OrbitControls.js b/public/bluebey-studio/vendor/three/examples/jsm/controls/OrbitControls.js new file mode 100644 index 0000000..3b49ec2 --- /dev/null +++ b/public/bluebey-studio/vendor/three/examples/jsm/controls/OrbitControls.js @@ -0,0 +1,1972 @@ +import { + Controls, + MOUSE, + Quaternion, + Spherical, + TOUCH, + Vector2, + Vector3, + Plane, + Ray, + MathUtils +} from 'three'; + +/** + * Fires when the camera has been transformed by the controls. + * + * @event OrbitControls#change + * @type {Object} + */ +const _changeEvent = { type: 'change' }; + +/** + * Fires when an interaction was initiated. + * + * @event OrbitControls#start + * @type {Object} + */ +const _startEvent = { type: 'start' }; + +/** + * Fires when an interaction has finished. + * + * @event OrbitControls#end + * @type {Object} + */ +const _endEvent = { type: 'end' }; + +const _ray = new Ray(); +const _plane = new Plane(); +const _TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD ); + +const _v = new Vector3(); +const _twoPI = 2 * Math.PI; + +const _STATE = { + NONE: - 1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6 +}; +const _EPS = 0.000001; + + +/** + * Orbit controls allow the camera to orbit around a target. + * + * OrbitControls performs orbiting, dollying (zooming), and panning. Unlike {@link TrackballControls}, + * it maintains the "up" direction `object.up` (+Y by default). + * + * - Orbit: Left mouse / touch: one-finger move. + * - Zoom: Middle mouse, or mousewheel / touch: two-finger spread or squish. + * - Pan: Right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move. + * + * ```js + * const controls = new OrbitControls( camera, renderer.domElement ); + * + * // controls.update() must be called after any manual changes to the camera's transform + * camera.position.set( 0, 20, 100 ); + * controls.update(); + * + * function animate() { + * + * // required if controls.enableDamping or controls.autoRotate are set to true + * controls.update(); + * + * renderer.render( scene, camera ); + * + * } + * ``` + * + * @augments Controls + * @three_import import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + */ +class OrbitControls extends Controls { + + /** + * Constructs a new controls instance. + * + * @param {Object3D} object - The object that is managed by the controls. + * @param {?HTMLElement} domElement - The HTML element used for event listeners. + */ + constructor( object, domElement = null ) { + + super( object, domElement ); + + this.state = _STATE.NONE; + + /** + * The focus point of the controls, the `object` orbits around this. + * It can be updated manually at any point to change the focus of the controls. + * + * @type {Vector3} + */ + this.target = new Vector3(); + + /** + * The focus point of the `minTargetRadius` and `maxTargetRadius` limits. + * It can be updated manually at any point to change the center of interest + * for the `target`. + * + * @type {Vector3} + */ + this.cursor = new Vector3(); + + /** + * How far you can dolly in (perspective camera only). + * + * @type {number} + * @default 0 + */ + this.minDistance = 0; + + /** + * How far you can dolly out (perspective camera only). + * + * @type {number} + * @default Infinity + */ + this.maxDistance = Infinity; + + /** + * How far you can zoom in (orthographic camera only). + * + * @type {number} + * @default 0 + */ + this.minZoom = 0; + + /** + * How far you can zoom out (orthographic camera only). + * + * @type {number} + * @default Infinity + */ + this.maxZoom = Infinity; + + /** + * How close you can get the target to the 3D `cursor`. + * + * @type {number} + * @default 0 + */ + this.minTargetRadius = 0; + + /** + * How far you can move the target from the 3D `cursor`. + * + * @type {number} + * @default Infinity + */ + this.maxTargetRadius = Infinity; + + /** + * How far you can orbit vertically, lower limit. Range is `[0, Math.PI]` radians. + * + * @type {number} + * @default 0 + */ + this.minPolarAngle = 0; + + /** + * How far you can orbit vertically, upper limit. Range is `[0, Math.PI]` radians. + * + * @type {number} + * @default Math.PI + */ + this.maxPolarAngle = Math.PI; + + /** + * How far you can orbit horizontally, lower limit. If set, the interval `[ min, max ]` + * must be a sub-interval of `[ - 2 PI, 2 PI ]`, with `( max - min < 2 PI )`. + * + * @type {number} + * @default -Infinity + */ + this.minAzimuthAngle = - Infinity; + + /** + * How far you can orbit horizontally, upper limit. If set, the interval `[ min, max ]` + * must be a sub-interval of `[ - 2 PI, 2 PI ]`, with `( max - min < 2 PI )`. + * + * @type {number} + * @default -Infinity + */ + this.maxAzimuthAngle = Infinity; + + /** + * Set to `true` to enable damping (inertia), which can be used to give a sense of weight + * to the controls. Note that if this is enabled, you must call `update()` in your animation + * loop. + * + * @type {boolean} + * @default false + */ + this.enableDamping = false; + + /** + * The damping inertia used if `enableDamping` is set to `true`. + * + * Note that for this to work, you must call `update()` in your animation loop. + * + * @type {number} + * @default 0.05 + */ + this.dampingFactor = 0.05; + + /** + * Enable or disable zooming (dollying) of the camera. + * + * @type {boolean} + * @default true + */ + this.enableZoom = true; + + /** + * Speed of zooming / dollying. + * + * @type {number} + * @default 1 + */ + this.zoomSpeed = 1.0; + + /** + * Enable or disable horizontal and vertical rotation of the camera. + * + * Note that it is possible to disable a single axis by setting the min and max of the + * `minPolarAngle` or `minAzimuthAngle` to the same value, which will cause the vertical + * or horizontal rotation to be fixed at that value. + * + * @type {boolean} + * @default true + */ + this.enableRotate = true; + + /** + * Speed of rotation. + * + * @type {number} + * @default 1 + */ + this.rotateSpeed = 1.0; + + /** + * How fast to rotate the camera when the keyboard is used. + * + * @type {number} + * @default 1 + */ + this.keyRotateSpeed = 1.0; + + /** + * Enable or disable camera panning. + * + * @type {boolean} + * @default true + */ + this.enablePan = true; + + /** + * Speed of panning. + * + * @type {number} + * @default 1 + */ + this.panSpeed = 1.0; + + /** + * Defines how the camera's position is translated when panning. If `true`, the camera pans + * in screen space. Otherwise, the camera pans in the plane orthogonal to the camera's up + * direction. + * + * @type {boolean} + * @default true + */ + this.screenSpacePanning = true; + + /** + * How fast to pan the camera when the keyboard is used in + * pixels per keypress. + * + * @type {number} + * @default 7 + */ + this.keyPanSpeed = 7.0; + + /** + * Setting this property to `true` allows to zoom to the cursor's position. + * + * @type {boolean} + * @default false + */ + this.zoomToCursor = false; + + /** + * Set to true to automatically rotate around the target + * + * Note that if this is enabled, you must call `update()` in your animation loop. + * If you want the auto-rotate speed to be independent of the frame rate (the refresh + * rate of the display), you must pass the time `deltaTime`, in seconds, to `update()`. + * + * @type {boolean} + * @default false + */ + this.autoRotate = false; + + /** + * How fast to rotate around the target if `autoRotate` is `true`. The default equates to 30 seconds + * per orbit at 60fps. + * + * Note that if `autoRotate` is enabled, you must call `update()` in your animation loop. + * + * @type {number} + * @default 2 + */ + this.autoRotateSpeed = 2.0; + + /** + * This object contains references to the keycodes for controlling camera panning. + * + * ```js + * controls.keys = { + * LEFT: 'ArrowLeft', //left arrow + * UP: 'ArrowUp', // up arrow + * RIGHT: 'ArrowRight', // right arrow + * BOTTOM: 'ArrowDown' // down arrow + * } + * ``` + * @type {Object} + */ + this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; + + /** + * This object contains references to the mouse actions used by the controls. + * + * ```js + * controls.mouseButtons = { + * LEFT: THREE.MOUSE.ROTATE, + * MIDDLE: THREE.MOUSE.DOLLY, + * RIGHT: THREE.MOUSE.PAN + * } + * ``` + * @type {Object} + */ + this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; + + /** + * This object contains references to the touch actions used by the controls. + * + * ```js + * controls.mouseButtons = { + * ONE: THREE.TOUCH.ROTATE, + * TWO: THREE.TOUCH.DOLLY_PAN + * } + * ``` + * @type {Object} + */ + this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; + + /** + * Used internally by `saveState()` and `reset()`. + * + * @type {Vector3} + */ + this.target0 = this.target.clone(); + + /** + * Used internally by `saveState()` and `reset()`. + * + * @type {Vector3} + */ + this.position0 = this.object.position.clone(); + + /** + * Used internally by `saveState()` and `reset()`. + * + * @type {number} + */ + this.zoom0 = this.object.zoom; + + this._cursorStyle = 'auto'; + + // the target DOM element for key events + this._domElementKeyEvents = null; + + // internals + + this._lastPosition = new Vector3(); + this._lastQuaternion = new Quaternion(); + this._lastTargetPosition = new Vector3(); + + // so camera.up is the orbit axis + this._quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); + this._quatInverse = this._quat.clone().invert(); + + // current position in spherical coordinates + this._spherical = new Spherical(); + this._sphericalDelta = new Spherical(); + + this._scale = 1; + this._panOffset = new Vector3(); + + this._rotateStart = new Vector2(); + this._rotateEnd = new Vector2(); + this._rotateDelta = new Vector2(); + + this._panStart = new Vector2(); + this._panEnd = new Vector2(); + this._panDelta = new Vector2(); + + this._dollyStart = new Vector2(); + this._dollyEnd = new Vector2(); + this._dollyDelta = new Vector2(); + + this._dollyDirection = new Vector3(); + this._mouse = new Vector2(); + this._performCursorZoom = false; + + this._pointers = []; + this._pointerPositions = {}; + + this._controlActive = false; + + // event listeners + + this._onPointerMove = onPointerMove.bind( this ); + this._onPointerDown = onPointerDown.bind( this ); + this._onPointerUp = onPointerUp.bind( this ); + this._onContextMenu = onContextMenu.bind( this ); + this._onMouseWheel = onMouseWheel.bind( this ); + this._onKeyDown = onKeyDown.bind( this ); + + this._onTouchStart = onTouchStart.bind( this ); + this._onTouchMove = onTouchMove.bind( this ); + + this._onMouseDown = onMouseDown.bind( this ); + this._onMouseMove = onMouseMove.bind( this ); + + this._interceptControlDown = interceptControlDown.bind( this ); + this._interceptControlUp = interceptControlUp.bind( this ); + + // + + if ( this.domElement !== null ) { + + this.connect( this.domElement ); + + } + + this.update(); + + } + + /** + * Defines the visual representation of the cursor. + * + * @type {('auto'|'grab')} + * @default 'auto' + */ + set cursorStyle( type ) { + + this._cursorStyle = type; + + if ( type === 'grab' ) { + + this.domElement.style.cursor = 'grab'; + + } else { + + this.domElement.style.cursor = 'auto'; + + } + + } + + get cursorStyle() { + + return this._cursorStyle; + + } + + connect( element ) { + + super.connect( element ); + + this.domElement.addEventListener( 'pointerdown', this._onPointerDown ); + this.domElement.addEventListener( 'pointercancel', this._onPointerUp ); + + this.domElement.addEventListener( 'contextmenu', this._onContextMenu ); + this.domElement.addEventListener( 'wheel', this._onMouseWheel, { passive: false } ); + + const document = this.domElement.getRootNode(); // offscreen canvas compatibility + document.addEventListener( 'keydown', this._interceptControlDown, { passive: true, capture: true } ); + + this.domElement.style.touchAction = 'none'; // Disable touch scroll + + } + + disconnect() { + + this.state = _STATE.NONE; + + this.domElement.removeEventListener( 'pointerdown', this._onPointerDown ); + this.domElement.ownerDocument.removeEventListener( 'pointermove', this._onPointerMove ); + this.domElement.ownerDocument.removeEventListener( 'pointerup', this._onPointerUp ); + this.domElement.removeEventListener( 'pointercancel', this._onPointerUp ); + + this.domElement.removeEventListener( 'wheel', this._onMouseWheel ); + this.domElement.removeEventListener( 'contextmenu', this._onContextMenu ); + + this.stopListenToKeyEvents(); + + const document = this.domElement.getRootNode(); // offscreen canvas compatibility + document.removeEventListener( 'keydown', this._interceptControlDown, { capture: true } ); + document.removeEventListener( 'keyup', this._interceptControlUp, { capture: true } ); + + this._controlActive = false; + + this._pointers.length = 0; + this._pointerPositions = {}; + + this.domElement.style.touchAction = ''; // Restore touch scroll + this.domElement.style.cursor = 'auto'; + + } + + dispose() { + + this.disconnect(); + + } + + /** + * Get the current vertical rotation, in radians. + * + * @return {number} The current vertical rotation, in radians. + */ + getPolarAngle() { + + return this._spherical.phi; + + } + + /** + * Get the current horizontal rotation, in radians. + * + * @return {number} The current horizontal rotation, in radians. + */ + getAzimuthalAngle() { + + return this._spherical.theta; + + } + + /** + * Returns the distance from the camera to the target. + * + * @return {number} The distance from the camera to the target. + */ + getDistance() { + + return this.object.position.distanceTo( this.target ); + + } + + /** + * Adds key event listeners to the given DOM element. + * `window` is a recommended argument for using this method. + * + * @param {HTMLElement} domElement - The DOM element + */ + listenToKeyEvents( domElement ) { + + domElement.addEventListener( 'keydown', this._onKeyDown ); + this._domElementKeyEvents = domElement; + + } + + /** + * Removes the key event listener previously defined with `listenToKeyEvents()`. + */ + stopListenToKeyEvents() { + + if ( this._domElementKeyEvents !== null ) { + + this._domElementKeyEvents.removeEventListener( 'keydown', this._onKeyDown ); + this._domElementKeyEvents = null; + + } + + } + + /** + * Save the current state of the controls. This can later be recovered with `reset()`. + */ + saveState() { + + this.target0.copy( this.target ); + this.position0.copy( this.object.position ); + this.zoom0 = this.object.zoom; + + } + + /** + * Reset the controls to their state from either the last time the `saveState()` + * was called, or the initial state. + */ + reset() { + + this.target.copy( this.target0 ); + this.object.position.copy( this.position0 ); + this.object.zoom = this.zoom0; + + this.object.updateProjectionMatrix(); + this.dispatchEvent( _changeEvent ); + + this.update(); + + this.state = _STATE.NONE; + + } + + /** + * Programmatically pan the camera. + * + * @param {number} deltaX - The horizontal pan amount in pixels. + * @param {number} deltaY - The vertical pan amount in pixels. + */ + pan( deltaX, deltaY ) { + + this._pan( deltaX, deltaY ); + this.update(); + + } + + /** + * Programmatically dolly in (zoom in for perspective camera). + * + * @param {number} dollyScale - The dolly scale factor. + */ + dollyIn( dollyScale ) { + + this._dollyIn( dollyScale ); + this.update(); + + } + + /** + * Programmatically dolly out (zoom out for perspective camera). + * + * @param {number} dollyScale - The dolly scale factor. + */ + dollyOut( dollyScale ) { + + this._dollyOut( dollyScale ); + this.update(); + + } + + /** + * Programmatically rotate the camera left (around the vertical axis). + * + * @param {number} angle - The rotation angle in radians. + */ + rotateLeft( angle ) { + + this._rotateLeft( angle ); + this.update(); + + } + + /** + * Programmatically rotate the camera up (around the horizontal axis). + * + * @param {number} angle - The rotation angle in radians. + */ + rotateUp( angle ) { + + this._rotateUp( angle ); + this.update(); + + } + + update( deltaTime = null ) { + + const position = this.object.position; + + _v.copy( position ).sub( this.target ); + + // rotate offset to "y-axis-is-up" space + _v.applyQuaternion( this._quat ); + + // angle from z-axis around y-axis + this._spherical.setFromVector3( _v ); + + if ( this.autoRotate && this.state === _STATE.NONE ) { + + this._rotateLeft( this._getAutoRotationAngle( deltaTime ) ); + + } + + if ( this.enableDamping ) { + + this._spherical.theta += this._sphericalDelta.theta * this.dampingFactor; + this._spherical.phi += this._sphericalDelta.phi * this.dampingFactor; + + } else { + + this._spherical.theta += this._sphericalDelta.theta; + this._spherical.phi += this._sphericalDelta.phi; + + } + + // restrict theta to be between desired limits + + let min = this.minAzimuthAngle; + let max = this.maxAzimuthAngle; + + if ( isFinite( min ) && isFinite( max ) ) { + + if ( min < - Math.PI ) min += _twoPI; else if ( min > Math.PI ) min -= _twoPI; + + if ( max < - Math.PI ) max += _twoPI; else if ( max > Math.PI ) max -= _twoPI; + + if ( min <= max ) { + + this._spherical.theta = Math.max( min, Math.min( max, this._spherical.theta ) ); + + } else { + + this._spherical.theta = ( this._spherical.theta > ( min + max ) / 2 ) ? + Math.max( min, this._spherical.theta ) : + Math.min( max, this._spherical.theta ); + + } + + } + + // restrict phi to be between desired limits + this._spherical.phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, this._spherical.phi ) ); + + this._spherical.makeSafe(); + + + // move target to panned location + + if ( this.enableDamping === true ) { + + this.target.addScaledVector( this._panOffset, this.dampingFactor ); + + } else { + + this.target.add( this._panOffset ); + + } + + // Limit the target distance from the cursor to create a sphere around the center of interest + this.target.sub( this.cursor ); + this.target.clampLength( this.minTargetRadius, this.maxTargetRadius ); + this.target.add( this.cursor ); + + let zoomChanged = false; + // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera + // we adjust zoom later in these cases + if ( this.zoomToCursor && this._performCursorZoom || this.object.isOrthographicCamera ) { + + this._spherical.radius = this._clampDistance( this._spherical.radius ); + + } else { + + const prevRadius = this._spherical.radius; + this._spherical.radius = this._clampDistance( this._spherical.radius * this._scale ); + zoomChanged = prevRadius != this._spherical.radius; + + } + + _v.setFromSpherical( this._spherical ); + + // rotate offset back to "camera-up-vector-is-up" space + _v.applyQuaternion( this._quatInverse ); + + position.copy( this.target ).add( _v ); + + this.object.lookAt( this.target ); + + if ( this.enableDamping === true ) { + + this._sphericalDelta.theta *= ( 1 - this.dampingFactor ); + this._sphericalDelta.phi *= ( 1 - this.dampingFactor ); + + this._panOffset.multiplyScalar( 1 - this.dampingFactor ); + + } else { + + this._sphericalDelta.set( 0, 0, 0 ); + + this._panOffset.set( 0, 0, 0 ); + + } + + // adjust camera position + if ( this.zoomToCursor && this._performCursorZoom ) { + + let newRadius = null; + if ( this.object.isPerspectiveCamera ) { + + // move the camera down the pointer ray + // this method avoids floating point error + const prevRadius = _v.length(); + newRadius = this._clampDistance( prevRadius * this._scale ); + + const radiusDelta = prevRadius - newRadius; + this.object.position.addScaledVector( this._dollyDirection, radiusDelta ); + this.object.updateMatrixWorld(); + + zoomChanged = !! radiusDelta; + + } else if ( this.object.isOrthographicCamera ) { + + // adjust the ortho camera position based on zoom changes + const mouseBefore = new Vector3( this._mouse.x, this._mouse.y, 0 ); + mouseBefore.unproject( this.object ); + + const prevZoom = this.object.zoom; + this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) ); + this.object.updateProjectionMatrix(); + + zoomChanged = prevZoom !== this.object.zoom; + + const mouseAfter = new Vector3( this._mouse.x, this._mouse.y, 0 ); + mouseAfter.unproject( this.object ); + + this.object.position.sub( mouseAfter ).add( mouseBefore ); + this.object.updateMatrixWorld(); + + newRadius = _v.length(); + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' ); + this.zoomToCursor = false; + + } + + // handle the placement of the target + if ( newRadius !== null ) { + + if ( this.screenSpacePanning ) { + + // position the orbit target in front of the new camera position + this.target.set( 0, 0, - 1 ) + .transformDirection( this.object.matrix ) + .multiplyScalar( newRadius ) + .add( this.object.position ); + + } else { + + // get the ray and translation plane to compute target + _ray.origin.copy( this.object.position ); + _ray.direction.set( 0, 0, - 1 ).transformDirection( this.object.matrix ); + + // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid + // extremely large values + if ( Math.abs( this.object.up.dot( _ray.direction ) ) < _TILT_LIMIT ) { + + this.object.lookAt( this.target ); + + } else { + + _plane.setFromNormalAndCoplanarPoint( this.object.up, this.target ); + _ray.intersectPlane( _plane, this.target ); + + } + + } + + } + + } else if ( this.object.isOrthographicCamera ) { + + const prevZoom = this.object.zoom; + this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) ); + + if ( prevZoom !== this.object.zoom ) { + + this.object.updateProjectionMatrix(); + zoomChanged = true; + + } + + } + + this._scale = 1; + this._performCursorZoom = false; + + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + + if ( zoomChanged || + this._lastPosition.distanceToSquared( this.object.position ) > _EPS || + 8 * ( 1 - this._lastQuaternion.dot( this.object.quaternion ) ) > _EPS || + this._lastTargetPosition.distanceToSquared( this.target ) > _EPS ) { + + this.dispatchEvent( _changeEvent ); + + this._lastPosition.copy( this.object.position ); + this._lastQuaternion.copy( this.object.quaternion ); + this._lastTargetPosition.copy( this.target ); + + return true; + + } + + return false; + + } + + _getAutoRotationAngle( deltaTime ) { + + if ( deltaTime !== null ) { + + return ( _twoPI / 60 * this.autoRotateSpeed ) * deltaTime; + + } else { + + return _twoPI / 60 / 60 * this.autoRotateSpeed; + + } + + } + + _getZoomScale( delta ) { + + const normalizedDelta = Math.abs( delta * 0.01 ); + return Math.pow( 0.95, this.zoomSpeed * normalizedDelta ); + + } + + _rotateLeft( angle ) { + + this._sphericalDelta.theta -= angle; + + } + + _rotateUp( angle ) { + + this._sphericalDelta.phi -= angle; + + } + + _panLeft( distance, objectMatrix ) { + + _v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix + _v.multiplyScalar( - distance ); + + this._panOffset.add( _v ); + + } + + _panUp( distance, objectMatrix ) { + + if ( this.screenSpacePanning === true ) { + + _v.setFromMatrixColumn( objectMatrix, 1 ); + + } else { + + _v.setFromMatrixColumn( objectMatrix, 0 ); + _v.crossVectors( this.object.up, _v ); + + } + + _v.multiplyScalar( distance ); + + this._panOffset.add( _v ); + + } + + // deltaX and deltaY are in pixels; right and down are positive + _pan( deltaX, deltaY ) { + + const element = this.domElement; + + if ( this.object.isPerspectiveCamera ) { + + // perspective + const position = this.object.position; + _v.copy( position ).sub( this.target ); + let targetDistance = _v.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan( ( this.object.fov / 2 ) * Math.PI / 180.0 ); + + // we use only clientHeight here so aspect ratio does not distort speed + this._panLeft( 2 * deltaX * targetDistance / element.clientHeight, this.object.matrix ); + this._panUp( 2 * deltaY * targetDistance / element.clientHeight, this.object.matrix ); + + } else if ( this.object.isOrthographicCamera ) { + + // orthographic + this._panLeft( deltaX * ( this.object.right - this.object.left ) / this.object.zoom / element.clientWidth, this.object.matrix ); + this._panUp( deltaY * ( this.object.top - this.object.bottom ) / this.object.zoom / element.clientHeight, this.object.matrix ); + + } else { + + // camera neither orthographic nor perspective + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); + this.enablePan = false; + + } + + } + + _dollyOut( dollyScale ) { + + if ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) { + + this._scale /= dollyScale; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + this.enableZoom = false; + + } + + } + + _dollyIn( dollyScale ) { + + if ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) { + + this._scale *= dollyScale; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + this.enableZoom = false; + + } + + } + + _updateZoomParameters( x, y ) { + + if ( ! this.zoomToCursor ) { + + return; + + } + + this._performCursorZoom = true; + + const rect = this.domElement.getBoundingClientRect(); + const dx = x - rect.left; + const dy = y - rect.top; + const w = rect.width; + const h = rect.height; + + this._mouse.x = ( dx / w ) * 2 - 1; + this._mouse.y = - ( dy / h ) * 2 + 1; + + this._dollyDirection.set( this._mouse.x, this._mouse.y, 1 ).unproject( this.object ).sub( this.object.position ).normalize(); + + } + + _clampDistance( dist ) { + + return Math.max( this.minDistance, Math.min( this.maxDistance, dist ) ); + + } + + // + // event callbacks - update the object state + // + + _handleMouseDownRotate( event ) { + + this._rotateStart.set( event.clientX, event.clientY ); + + } + + _handleMouseDownDolly( event ) { + + this._updateZoomParameters( event.clientX, event.clientX ); + this._dollyStart.set( event.clientX, event.clientY ); + + } + + _handleMouseDownPan( event ) { + + this._panStart.set( event.clientX, event.clientY ); + + } + + _handleMouseMoveRotate( event ) { + + this._rotateEnd.set( event.clientX, event.clientY ); + + this._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed ); + + const element = this.domElement; + + this._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height + + this._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight ); + + this._rotateStart.copy( this._rotateEnd ); + + this.update(); + + } + + _handleMouseMoveDolly( event ) { + + this._dollyEnd.set( event.clientX, event.clientY ); + + this._dollyDelta.subVectors( this._dollyEnd, this._dollyStart ); + + if ( this._dollyDelta.y > 0 ) { + + this._dollyOut( this._getZoomScale( this._dollyDelta.y ) ); + + } else if ( this._dollyDelta.y < 0 ) { + + this._dollyIn( this._getZoomScale( this._dollyDelta.y ) ); + + } + + this._dollyStart.copy( this._dollyEnd ); + + this.update(); + + } + + _handleMouseMovePan( event ) { + + this._panEnd.set( event.clientX, event.clientY ); + + this._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed ); + + this._pan( this._panDelta.x, this._panDelta.y ); + + this._panStart.copy( this._panEnd ); + + this.update(); + + } + + _handleMouseWheel( event ) { + + this._updateZoomParameters( event.clientX, event.clientY ); + + if ( event.deltaY < 0 ) { + + this._dollyIn( this._getZoomScale( event.deltaY ) ); + + } else if ( event.deltaY > 0 ) { + + this._dollyOut( this._getZoomScale( event.deltaY ) ); + + } + + this.update(); + + } + + _handleKeyDown( event ) { + + let needsUpdate = false; + + switch ( event.code ) { + + case this.keys.UP: + + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( this.enableRotate ) { + + this._rotateUp( _twoPI * this.keyRotateSpeed / this.domElement.clientHeight ); + + } + + } else { + + if ( this.enablePan ) { + + this._pan( 0, this.keyPanSpeed ); + + } + + } + + needsUpdate = true; + break; + + case this.keys.BOTTOM: + + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( this.enableRotate ) { + + this._rotateUp( - _twoPI * this.keyRotateSpeed / this.domElement.clientHeight ); + + } + + } else { + + if ( this.enablePan ) { + + this._pan( 0, - this.keyPanSpeed ); + + } + + } + + needsUpdate = true; + break; + + case this.keys.LEFT: + + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( this.enableRotate ) { + + this._rotateLeft( _twoPI * this.keyRotateSpeed / this.domElement.clientHeight ); + + } + + } else { + + if ( this.enablePan ) { + + this._pan( this.keyPanSpeed, 0 ); + + } + + } + + needsUpdate = true; + break; + + case this.keys.RIGHT: + + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( this.enableRotate ) { + + this._rotateLeft( - _twoPI * this.keyRotateSpeed / this.domElement.clientHeight ); + + } + + } else { + + if ( this.enablePan ) { + + this._pan( - this.keyPanSpeed, 0 ); + + } + + } + + needsUpdate = true; + break; + + } + + if ( needsUpdate ) { + + // prevent the browser from scrolling on cursor keys + event.preventDefault(); + + this.update(); + + } + + + } + + _handleTouchStartRotate( event ) { + + if ( this._pointers.length === 1 ) { + + this._rotateStart.set( event.pageX, event.pageY ); + + } else { + + const position = this._getSecondPointerPosition( event ); + + const x = 0.5 * ( event.pageX + position.x ); + const y = 0.5 * ( event.pageY + position.y ); + + this._rotateStart.set( x, y ); + + } + + } + + _handleTouchStartPan( event ) { + + if ( this._pointers.length === 1 ) { + + this._panStart.set( event.pageX, event.pageY ); + + } else { + + const position = this._getSecondPointerPosition( event ); + + const x = 0.5 * ( event.pageX + position.x ); + const y = 0.5 * ( event.pageY + position.y ); + + this._panStart.set( x, y ); + + } + + } + + _handleTouchStartDolly( event ) { + + const position = this._getSecondPointerPosition( event ); + + const dx = event.pageX - position.x; + const dy = event.pageY - position.y; + + const distance = Math.sqrt( dx * dx + dy * dy ); + + this._dollyStart.set( 0, distance ); + + } + + _handleTouchStartDollyPan( event ) { + + if ( this.enableZoom ) this._handleTouchStartDolly( event ); + + if ( this.enablePan ) this._handleTouchStartPan( event ); + + } + + _handleTouchStartDollyRotate( event ) { + + if ( this.enableZoom ) this._handleTouchStartDolly( event ); + + if ( this.enableRotate ) this._handleTouchStartRotate( event ); + + } + + _handleTouchMoveRotate( event ) { + + if ( this._pointers.length == 1 ) { + + this._rotateEnd.set( event.pageX, event.pageY ); + + } else { + + const position = this._getSecondPointerPosition( event ); + + const x = 0.5 * ( event.pageX + position.x ); + const y = 0.5 * ( event.pageY + position.y ); + + this._rotateEnd.set( x, y ); + + } + + this._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed ); + + const element = this.domElement; + + this._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height + + this._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight ); + + this._rotateStart.copy( this._rotateEnd ); + + } + + _handleTouchMovePan( event ) { + + if ( this._pointers.length === 1 ) { + + this._panEnd.set( event.pageX, event.pageY ); + + } else { + + const position = this._getSecondPointerPosition( event ); + + const x = 0.5 * ( event.pageX + position.x ); + const y = 0.5 * ( event.pageY + position.y ); + + this._panEnd.set( x, y ); + + } + + this._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed ); + + this._pan( this._panDelta.x, this._panDelta.y ); + + this._panStart.copy( this._panEnd ); + + } + + _handleTouchMoveDolly( event ) { + + const position = this._getSecondPointerPosition( event ); + + const dx = event.pageX - position.x; + const dy = event.pageY - position.y; + + const distance = Math.sqrt( dx * dx + dy * dy ); + + this._dollyEnd.set( 0, distance ); + + this._dollyDelta.set( 0, Math.pow( this._dollyEnd.y / this._dollyStart.y, this.zoomSpeed ) ); + + this._dollyOut( this._dollyDelta.y ); + + this._dollyStart.copy( this._dollyEnd ); + + const centerX = ( event.pageX + position.x ) * 0.5; + const centerY = ( event.pageY + position.y ) * 0.5; + + this._updateZoomParameters( centerX, centerY ); + + } + + _handleTouchMoveDollyPan( event ) { + + if ( this.enableZoom ) this._handleTouchMoveDolly( event ); + + if ( this.enablePan ) this._handleTouchMovePan( event ); + + } + + _handleTouchMoveDollyRotate( event ) { + + if ( this.enableZoom ) this._handleTouchMoveDolly( event ); + + if ( this.enableRotate ) this._handleTouchMoveRotate( event ); + + } + + // pointers + + _addPointer( event ) { + + this._pointers.push( event.pointerId ); + + } + + _removePointer( event ) { + + delete this._pointerPositions[ event.pointerId ]; + + for ( let i = 0; i < this._pointers.length; i ++ ) { + + if ( this._pointers[ i ] == event.pointerId ) { + + this._pointers.splice( i, 1 ); + return; + + } + + } + + } + + _isTrackingPointer( event ) { + + for ( let i = 0; i < this._pointers.length; i ++ ) { + + if ( this._pointers[ i ] == event.pointerId ) return true; + + } + + return false; + + } + + _trackPointer( event ) { + + let position = this._pointerPositions[ event.pointerId ]; + + if ( position === undefined ) { + + position = new Vector2(); + this._pointerPositions[ event.pointerId ] = position; + + } + + position.set( event.pageX, event.pageY ); + + } + + _getSecondPointerPosition( event ) { + + const pointerId = ( event.pointerId === this._pointers[ 0 ] ) ? this._pointers[ 1 ] : this._pointers[ 0 ]; + + return this._pointerPositions[ pointerId ]; + + } + + // + + _customWheelEvent( event ) { + + const mode = event.deltaMode; + + // minimal wheel event altered to meet delta-zoom demand + const newEvent = { + clientX: event.clientX, + clientY: event.clientY, + deltaY: event.deltaY, + }; + + switch ( mode ) { + + case 1: // LINE_MODE + newEvent.deltaY *= 16; + break; + + case 2: // PAGE_MODE + newEvent.deltaY *= 100; + break; + + } + + // detect if event was triggered by pinching + if ( event.ctrlKey && ! this._controlActive ) { + + newEvent.deltaY *= 10; + + } + + return newEvent; + + } + +} + +function onPointerDown( event ) { + + if ( this.enabled === false ) return; + + if ( this._pointers.length === 0 ) { + + this.domElement.setPointerCapture( event.pointerId ); + + this.domElement.ownerDocument.addEventListener( 'pointermove', this._onPointerMove ); + this.domElement.ownerDocument.addEventListener( 'pointerup', this._onPointerUp ); + + } + + // + + if ( this._isTrackingPointer( event ) ) return; + + // + + this._addPointer( event ); + + if ( event.pointerType === 'touch' ) { + + this._onTouchStart( event ); + + } else { + + this._onMouseDown( event ); + + } + + if ( this._cursorStyle === 'grab' ) { + + this.domElement.style.cursor = 'grabbing'; + + } + +} + +function onPointerMove( event ) { + + if ( this.enabled === false ) return; + + if ( event.pointerType === 'touch' ) { + + this._onTouchMove( event ); + + } else { + + this._onMouseMove( event ); + + } + +} + +function onPointerUp( event ) { + + this._removePointer( event ); + + switch ( this._pointers.length ) { + + case 0: + + this.domElement.releasePointerCapture( event.pointerId ); + + this.domElement.ownerDocument.removeEventListener( 'pointermove', this._onPointerMove ); + this.domElement.ownerDocument.removeEventListener( 'pointerup', this._onPointerUp ); + + this.dispatchEvent( _endEvent ); + + this.state = _STATE.NONE; + + if ( this._cursorStyle === 'grab' ) { + + this.domElement.style.cursor = 'grab'; + + } + + break; + + case 1: + + const pointerId = this._pointers[ 0 ]; + const position = this._pointerPositions[ pointerId ]; + + // minimal placeholder event - allows state correction on pointer-up + this._onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } ); + + break; + + } + +} + +function onMouseDown( event ) { + + let mouseAction; + + switch ( event.button ) { + + case 0: + + mouseAction = this.mouseButtons.LEFT; + break; + + case 1: + + mouseAction = this.mouseButtons.MIDDLE; + break; + + case 2: + + mouseAction = this.mouseButtons.RIGHT; + break; + + default: + + mouseAction = - 1; + + } + + switch ( mouseAction ) { + + case MOUSE.DOLLY: + + if ( this.enableZoom === false ) return; + + this._handleMouseDownDolly( event ); + + this.state = _STATE.DOLLY; + + break; + + case MOUSE.ROTATE: + + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( this.enablePan === false ) return; + + this._handleMouseDownPan( event ); + + this.state = _STATE.PAN; + + } else { + + if ( this.enableRotate === false ) return; + + this._handleMouseDownRotate( event ); + + this.state = _STATE.ROTATE; + + } + + break; + + case MOUSE.PAN: + + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( this.enableRotate === false ) return; + + this._handleMouseDownRotate( event ); + + this.state = _STATE.ROTATE; + + } else { + + if ( this.enablePan === false ) return; + + this._handleMouseDownPan( event ); + + this.state = _STATE.PAN; + + } + + break; + + default: + + this.state = _STATE.NONE; + + } + + if ( this.state !== _STATE.NONE ) { + + this.dispatchEvent( _startEvent ); + + } + +} + +function onMouseMove( event ) { + + switch ( this.state ) { + + case _STATE.ROTATE: + + if ( this.enableRotate === false ) return; + + this._handleMouseMoveRotate( event ); + + break; + + case _STATE.DOLLY: + + if ( this.enableZoom === false ) return; + + this._handleMouseMoveDolly( event ); + + break; + + case _STATE.PAN: + + if ( this.enablePan === false ) return; + + this._handleMouseMovePan( event ); + + break; + + } + +} + +function onMouseWheel( event ) { + + if ( this.enabled === false || this.enableZoom === false || this.state !== _STATE.NONE ) return; + + event.preventDefault(); + + this.dispatchEvent( _startEvent ); + + this._handleMouseWheel( this._customWheelEvent( event ) ); + + this.dispatchEvent( _endEvent ); + +} + +function onKeyDown( event ) { + + if ( this.enabled === false ) return; + + this._handleKeyDown( event ); + +} + +function onTouchStart( event ) { + + this._trackPointer( event ); + + switch ( this._pointers.length ) { + + case 1: + + switch ( this.touches.ONE ) { + + case TOUCH.ROTATE: + + if ( this.enableRotate === false ) return; + + this._handleTouchStartRotate( event ); + + this.state = _STATE.TOUCH_ROTATE; + + break; + + case TOUCH.PAN: + + if ( this.enablePan === false ) return; + + this._handleTouchStartPan( event ); + + this.state = _STATE.TOUCH_PAN; + + break; + + default: + + this.state = _STATE.NONE; + + } + + break; + + case 2: + + switch ( this.touches.TWO ) { + + case TOUCH.DOLLY_PAN: + + if ( this.enableZoom === false && this.enablePan === false ) return; + + this._handleTouchStartDollyPan( event ); + + this.state = _STATE.TOUCH_DOLLY_PAN; + + break; + + case TOUCH.DOLLY_ROTATE: + + if ( this.enableZoom === false && this.enableRotate === false ) return; + + this._handleTouchStartDollyRotate( event ); + + this.state = _STATE.TOUCH_DOLLY_ROTATE; + + break; + + default: + + this.state = _STATE.NONE; + + } + + break; + + default: + + this.state = _STATE.NONE; + + } + + if ( this.state !== _STATE.NONE ) { + + this.dispatchEvent( _startEvent ); + + } + +} + +function onTouchMove( event ) { + + this._trackPointer( event ); + + switch ( this.state ) { + + case _STATE.TOUCH_ROTATE: + + if ( this.enableRotate === false ) return; + + this._handleTouchMoveRotate( event ); + + this.update(); + + break; + + case _STATE.TOUCH_PAN: + + if ( this.enablePan === false ) return; + + this._handleTouchMovePan( event ); + + this.update(); + + break; + + case _STATE.TOUCH_DOLLY_PAN: + + if ( this.enableZoom === false && this.enablePan === false ) return; + + this._handleTouchMoveDollyPan( event ); + + this.update(); + + break; + + case _STATE.TOUCH_DOLLY_ROTATE: + + if ( this.enableZoom === false && this.enableRotate === false ) return; + + this._handleTouchMoveDollyRotate( event ); + + this.update(); + + break; + + default: + + this.state = _STATE.NONE; + + } + +} + +function onContextMenu( event ) { + + if ( this.enabled === false ) return; + + event.preventDefault(); + +} + +function interceptControlDown( event ) { + + if ( event.key === 'Control' ) { + + this._controlActive = true; + + const document = this.domElement.getRootNode(); // offscreen canvas compatibility + + document.addEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } ); + + } + +} + +function interceptControlUp( event ) { + + if ( event.key === 'Control' ) { + + this._controlActive = false; + + const document = this.domElement.getRootNode(); // offscreen canvas compatibility + + document.removeEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } ); + + } + +} + +export { OrbitControls }; diff --git a/public/bluebey-studio/vendor/three/examples/jsm/controls/TransformControls.js b/public/bluebey-studio/vendor/three/examples/jsm/controls/TransformControls.js new file mode 100644 index 0000000..88fc5e2 --- /dev/null +++ b/public/bluebey-studio/vendor/three/examples/jsm/controls/TransformControls.js @@ -0,0 +1,2003 @@ +import { + BoxGeometry, + BufferGeometry, + Controls, + CylinderGeometry, + DoubleSide, + Euler, + Float32BufferAttribute, + Line, + LineBasicMaterial, + Matrix4, + Mesh, + MeshBasicMaterial, + Object3D, + OctahedronGeometry, + PlaneGeometry, + Quaternion, + Raycaster, + SphereGeometry, + TorusGeometry, + Vector3 +} from 'three'; + +const _raycaster = new Raycaster(); + +const _tempVector = new Vector3(); +const _tempVector2 = new Vector3(); +const _tempQuaternion = new Quaternion(); +const _unit = { + X: new Vector3( 1, 0, 0 ), + Y: new Vector3( 0, 1, 0 ), + Z: new Vector3( 0, 0, 1 ) +}; + +/** + * Fires if any type of change (object or property change) is performed. Property changes + * are separate events you can add event listeners to. The event type is "propertyname-changed". + * + * @event TransformControls#change + * @type {Object} + */ +const _changeEvent = { type: 'change' }; + +/** + * Fires if a pointer (mouse/touch) becomes active. + * + * @event TransformControls#mouseDown + * @type {Object} + */ +const _mouseDownEvent = { type: 'mouseDown', mode: null }; + +/** + * Fires if a pointer (mouse/touch) is no longer active. + * + * @event TransformControls#mouseUp + * @type {Object} + */ +const _mouseUpEvent = { type: 'mouseUp', mode: null }; + +/** + * Fires if the controlled 3D object is changed. + * + * @event TransformControls#objectChange + * @type {Object} + */ +const _objectChangeEvent = { type: 'objectChange' }; + +/** + * This class can be used to transform objects in 3D space by adapting a similar interaction model + * of DCC tools like Blender. Unlike other controls, it is not intended to transform the scene's camera. + * + * `TransformControls` expects that its attached 3D object is part of the scene graph. + * + * @augments Controls + * @three_import import { TransformControls } from 'three/addons/controls/TransformControls.js'; + */ +class TransformControls extends Controls { + + /** + * Constructs a new controls instance. + * + * @param {Camera} camera - The camera of the rendered scene. + * @param {?HTMLElement} domElement - The HTML element used for event listeners. + */ + constructor( camera, domElement = null ) { + + super( undefined, domElement ); + + const root = new TransformControlsRoot( this ); + this._root = root; + + const gizmo = new TransformControlsGizmo(); + this._gizmo = gizmo; + root.add( gizmo ); + + const plane = new TransformControlsPlane(); + this._plane = plane; + root.add( plane ); + + const scope = this; + + // Defined getter, setter and store for a property + function defineProperty( propName, defaultValue ) { + + let propValue = defaultValue; + + Object.defineProperty( scope, propName, { + + get: function () { + + return propValue !== undefined ? propValue : defaultValue; + + }, + + set: function ( value ) { + + if ( propValue !== value ) { + + propValue = value; + plane[ propName ] = value; + gizmo[ propName ] = value; + + scope.dispatchEvent( { type: propName + '-changed', value: value } ); + scope.dispatchEvent( _changeEvent ); + + } + + } + + } ); + + scope[ propName ] = defaultValue; + plane[ propName ] = defaultValue; + gizmo[ propName ] = defaultValue; + + } + + // Define properties with getters/setter + // Setting the defined property will automatically trigger change event + // Defined properties are passed down to gizmo and plane + + /** + * The camera of the rendered scene. + * + * @name TransformControls#camera + * @type {Camera} + */ + defineProperty( 'camera', camera ); + defineProperty( 'object', undefined ); + defineProperty( 'enabled', true ); + + /** + * The current transformation axis. + * + * @name TransformControls#axis + * @type {string} + */ + defineProperty( 'axis', null ); + + /** + * The current transformation axis. + * + * @name TransformControls#mode + * @type {('translate'|'rotate'|'scale')} + * @default 'translate' + */ + defineProperty( 'mode', 'translate' ); + + /** + * By default, 3D objects are continuously translated. If you set this property to a numeric + * value (world units), you can define in which steps the 3D object should be translated. + * + * @name TransformControls#translationSnap + * @type {?number} + * @default null + */ + defineProperty( 'translationSnap', null ); + + /** + * By default, 3D objects are continuously rotated. If you set this property to a numeric + * value (radians), you can define in which steps the 3D object should be rotated. + * + * @name TransformControls#rotationSnap + * @type {?number} + * @default null + */ + defineProperty( 'rotationSnap', null ); + + /** + * By default, 3D objects are continuously scaled. If you set this property to a numeric + * value, you can define in which steps the 3D object should be scaled. + * + * @name TransformControls#scaleSnap + * @type {?number} + * @default null + */ + defineProperty( 'scaleSnap', null ); + + /** + * Defines in which coordinate space transformations should be performed. + * + * @name TransformControls#space + * @type {('world'|'local')} + * @default 'world' + */ + defineProperty( 'space', 'world' ); + + /** + * The size of the helper UI (axes/planes). + * + * @name TransformControls#size + * @type {number} + * @default 1 + */ + defineProperty( 'size', 1 ); + + /** + * The viewport rectangle, in logical (CSS) pixels with the origin at the lower-left + * of the canvas. Set this when the renderer uses a sub-canvas viewport so pointer + * coordinates map to the correct region. If `null`, the full canvas is used. + * + * @name TransformControls#viewport + * @type {?Vector4} + * @default null + */ + this.viewport = null; + + /** + * Whether dragging is currently performed or not. + * + * @name TransformControls#dragging + * @type {boolean} + * @readonly + * @default false + */ + defineProperty( 'dragging', false ); + + /** + * Whether the x-axis helper should be visible or not. + * + * @name TransformControls#showX + * @type {boolean} + * @default true + */ + defineProperty( 'showX', true ); + + /** + * Whether the y-axis helper should be visible or not. + * + * @name TransformControls#showY + * @type {boolean} + * @default true + */ + defineProperty( 'showY', true ); + + /** + * Whether the z-axis helper should be visible or not. + * + * @name TransformControls#showZ + * @type {boolean} + * @default true + */ + defineProperty( 'showZ', true ); + + /** + * Whether the xy-plane helper should be visible or not. + * + * @name TransformControls#showXY + * @type {boolean} + * @default true + */ + defineProperty( 'showXY', true ); + + /** + * Whether the yz-plane helper should be visible or not. + * + * @name TransformControls#showYZ + * @type {boolean} + * @default true + */ + defineProperty( 'showYZ', true ); + + /** + * Whether the xz-axis helper should be visible or not. + * + * @name TransformControls#showXZ + * @type {boolean} + * @default true + */ + defineProperty( 'showXZ', true ); + + /** + * Whether the xyze rotation helper should be visible or not. + * + * @name TransformControls#showXYZE + * @type {boolean} + * @default true + */ + defineProperty( 'showXYZE', true ); + + /** + * Whether the e rotation helper should be visible or not. + * + * @name TransformControls#showE + * @type {boolean} + * @default true + */ + defineProperty( 'showE', true ); + + /** + * The minimum allowed X position during translation. + * + * @name TransformControls#minX + * @type {number} + * @default -Infinity + */ + defineProperty( 'minX', - Infinity ); + + /** + * The maximum allowed X position during translation. + * + * @name TransformControls#maxX + * @type {number} + * @default Infinity + */ + defineProperty( 'maxX', Infinity ); + + /** + * The minimum allowed y position during translation. + * + * @name TransformControls#minY + * @type {number} + * @default -Infinity + */ + defineProperty( 'minY', - Infinity ); + + /** + * The maximum allowed Y position during translation. + * + * @name TransformControls#maxY + * @type {number} + * @default Infinity + */ + defineProperty( 'maxY', Infinity ); + + /** + * The minimum allowed z position during translation. + * + * @name TransformControls#minZ + * @type {number} + * @default -Infinity + */ + defineProperty( 'minZ', - Infinity ); + + /** + * The maximum allowed Z position during translation. + * + * @name TransformControls#maxZ + * @type {number} + * @default Infinity + */ + defineProperty( 'maxZ', Infinity ); + + // Reusable utility variables + + const worldPosition = new Vector3(); + const worldPositionStart = new Vector3(); + const worldQuaternion = new Quaternion(); + const worldQuaternionStart = new Quaternion(); + const cameraPosition = new Vector3(); + const cameraQuaternion = new Quaternion(); + const pointStart = new Vector3(); + const pointEnd = new Vector3(); + const rotationAxis = new Vector3(); + const rotationAngle = 0; + const eye = new Vector3(); + + // TODO: remove properties unused in plane and gizmo + + defineProperty( 'worldPosition', worldPosition ); + defineProperty( 'worldPositionStart', worldPositionStart ); + defineProperty( 'worldQuaternion', worldQuaternion ); + defineProperty( 'worldQuaternionStart', worldQuaternionStart ); + defineProperty( 'cameraPosition', cameraPosition ); + defineProperty( 'cameraQuaternion', cameraQuaternion ); + defineProperty( 'pointStart', pointStart ); + defineProperty( 'pointEnd', pointEnd ); + defineProperty( 'rotationAxis', rotationAxis ); + defineProperty( 'rotationAngle', rotationAngle ); + defineProperty( 'eye', eye ); + + this._offset = new Vector3(); + this._startNorm = new Vector3(); + this._endNorm = new Vector3(); + this._cameraScale = new Vector3(); + + this._parentPosition = new Vector3(); + this._parentQuaternion = new Quaternion(); + this._parentQuaternionInv = new Quaternion(); + this._parentScale = new Vector3(); + + this._worldScaleStart = new Vector3(); + this._worldQuaternionInv = new Quaternion(); + this._worldScale = new Vector3(); + + this._positionStart = new Vector3(); + this._quaternionStart = new Quaternion(); + this._scaleStart = new Vector3(); + + this._getPointer = getPointer.bind( this ); + this._onPointerDown = onPointerDown.bind( this ); + this._onPointerHover = onPointerHover.bind( this ); + this._onPointerMove = onPointerMove.bind( this ); + this._onPointerUp = onPointerUp.bind( this ); + + if ( domElement !== null ) { + + this.connect( domElement ); + + } + + } + + connect( element ) { + + super.connect( element ); + + this.domElement.addEventListener( 'pointerdown', this._onPointerDown ); + this.domElement.addEventListener( 'pointermove', this._onPointerHover ); + this.domElement.addEventListener( 'pointerup', this._onPointerUp ); + + this.domElement.style.touchAction = 'none'; // Disable touch scroll + + } + + disconnect() { + + this.domElement.removeEventListener( 'pointerdown', this._onPointerDown ); + this.domElement.removeEventListener( 'pointermove', this._onPointerHover ); + this.domElement.removeEventListener( 'pointermove', this._onPointerMove ); + this.domElement.removeEventListener( 'pointerup', this._onPointerUp ); + + this.domElement.style.touchAction = ''; // Restore touch scroll + + } + + /** + * Returns the visual representation of the controls. Add the helper to your scene to + * visually transform the attached 3D object. + * + * @return {TransformControlsRoot} The helper. + */ + getHelper() { + + return this._root; + + } + + pointerHover( pointer ) { + + if ( this.object === undefined || this.dragging === true ) return; + + if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera ); + + const intersect = intersectObjectWithRay( this._gizmo.picker[ this.mode ], _raycaster ); + + if ( intersect ) { + + this.axis = intersect.object.name; + + } else { + + this.axis = null; + + } + + } + + pointerDown( pointer ) { + + if ( this.object === undefined || this.dragging === true || ( pointer != null && pointer.button !== 0 ) ) return; + + if ( this.axis !== null ) { + + if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera ); + + const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true ); + + if ( planeIntersect ) { + + this.object.updateMatrixWorld(); + this.object.parent.updateMatrixWorld(); + + this._positionStart.copy( this.object.position ); + this._quaternionStart.copy( this.object.quaternion ); + this._scaleStart.copy( this.object.scale ); + + this.object.matrixWorld.decompose( this.worldPositionStart, this.worldQuaternionStart, this._worldScaleStart ); + + this.pointStart.copy( planeIntersect.point ).sub( this.worldPositionStart ); + + } + + this.dragging = true; + _mouseDownEvent.mode = this.mode; + this.dispatchEvent( _mouseDownEvent ); + + } + + } + + pointerMove( pointer ) { + + const axis = this.axis; + const mode = this.mode; + const object = this.object; + let space = this.space; + + if ( mode === 'scale' ) { + + space = 'local'; + + } else if ( axis === 'E' || axis === 'XYZE' || axis === 'XYZ' ) { + + space = 'world'; + + } + + if ( object === undefined || axis === null || this.dragging === false || ( pointer !== null && pointer.button !== - 1 ) ) return; + + if ( pointer !== null ) _raycaster.setFromCamera( pointer, this.camera ); + + const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true ); + + if ( ! planeIntersect ) return; + + this.pointEnd.copy( planeIntersect.point ).sub( this.worldPositionStart ); + + if ( mode === 'translate' ) { + + // Apply translate + + this._offset.copy( this.pointEnd ).sub( this.pointStart ); + + if ( space === 'local' && axis !== 'XYZ' ) { + + this._offset.applyQuaternion( this._worldQuaternionInv ); + + } + + if ( axis.indexOf( 'X' ) === - 1 ) this._offset.x = 0; + if ( axis.indexOf( 'Y' ) === - 1 ) this._offset.y = 0; + if ( axis.indexOf( 'Z' ) === - 1 ) this._offset.z = 0; + + if ( space === 'local' && axis !== 'XYZ' ) { + + this._offset.applyQuaternion( this._quaternionStart ).divide( this._parentScale ); + + } else { + + this._offset.applyQuaternion( this._parentQuaternionInv ).divide( this._parentScale ); + + } + + object.position.copy( this._offset ).add( this._positionStart ); + + // Apply translation snap + + if ( this.translationSnap ) { + + if ( space === 'local' ) { + + object.position.applyQuaternion( _tempQuaternion.copy( this._quaternionStart ).invert() ); + + if ( axis.search( 'X' ) !== - 1 ) { + + object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap; + + } + + if ( axis.search( 'Y' ) !== - 1 ) { + + object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap; + + } + + if ( axis.search( 'Z' ) !== - 1 ) { + + object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap; + + } + + object.position.applyQuaternion( this._quaternionStart ); + + } + + if ( space === 'world' ) { + + object.getWorldPosition( _tempVector ); + + if ( axis.search( 'X' ) !== - 1 ) { + + _tempVector.x = Math.round( _tempVector.x / this.translationSnap ) * this.translationSnap; + + } + + if ( axis.search( 'Y' ) !== - 1 ) { + + _tempVector.y = Math.round( _tempVector.y / this.translationSnap ) * this.translationSnap; + + } + + if ( axis.search( 'Z' ) !== - 1 ) { + + _tempVector.z = Math.round( _tempVector.z / this.translationSnap ) * this.translationSnap; + + } + + object.position.copy( object.parent.worldToLocal( _tempVector ) ); + + } + + } + + object.position.x = Math.max( this.minX, Math.min( this.maxX, object.position.x ) ); + object.position.y = Math.max( this.minY, Math.min( this.maxY, object.position.y ) ); + object.position.z = Math.max( this.minZ, Math.min( this.maxZ, object.position.z ) ); + + } else if ( mode === 'scale' ) { + + if ( axis.search( 'XYZ' ) !== - 1 ) { + + let d = this.pointEnd.length() / this.pointStart.length(); + + if ( this.pointEnd.dot( this.pointStart ) < 0 ) d *= - 1; + + _tempVector2.set( d, d, d ); + + } else { + + _tempVector.copy( this.pointStart ); + _tempVector2.copy( this.pointEnd ); + + _tempVector.applyQuaternion( this._worldQuaternionInv ); + _tempVector2.applyQuaternion( this._worldQuaternionInv ); + + _tempVector2.divide( _tempVector ); + + if ( axis.search( 'X' ) === - 1 ) { + + _tempVector2.x = 1; + + } + + if ( axis.search( 'Y' ) === - 1 ) { + + _tempVector2.y = 1; + + } + + if ( axis.search( 'Z' ) === - 1 ) { + + _tempVector2.z = 1; + + } + + } + + // Apply scale + + object.scale.copy( this._scaleStart ).multiply( _tempVector2 ); + + if ( this.scaleSnap ) { + + if ( axis.search( 'X' ) !== - 1 ) { + + object.scale.x = Math.round( object.scale.x / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; + + } + + if ( axis.search( 'Y' ) !== - 1 ) { + + object.scale.y = Math.round( object.scale.y / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; + + } + + if ( axis.search( 'Z' ) !== - 1 ) { + + object.scale.z = Math.round( object.scale.z / this.scaleSnap ) * this.scaleSnap || this.scaleSnap; + + } + + } + + } else if ( mode === 'rotate' ) { + + this._offset.copy( this.pointEnd ).sub( this.pointStart ); + + const ROTATION_SPEED = 20 / this.worldPosition.distanceTo( _tempVector.setFromMatrixPosition( this.camera.matrixWorld ) ); + + let _inPlaneRotation = false; + + if ( axis === 'XYZE' ) { + + this.rotationAxis.copy( this._offset ).cross( this.eye ).normalize(); + this.rotationAngle = this._offset.dot( _tempVector.copy( this.rotationAxis ).cross( this.eye ) ) * ROTATION_SPEED; + + } else if ( axis === 'X' || axis === 'Y' || axis === 'Z' ) { + + this.rotationAxis.copy( _unit[ axis ] ); + + _tempVector.copy( _unit[ axis ] ); + + if ( space === 'local' ) { + + _tempVector.applyQuaternion( this.worldQuaternion ); + + } + + _tempVector.cross( this.eye ); + + // When _tempVector is 0 after cross with this.eye the vectors are parallel and should use in-plane rotation logic. + if ( _tempVector.length() === 0 ) { + + _inPlaneRotation = true; + + } else { + + this.rotationAngle = this._offset.dot( _tempVector.normalize() ) * ROTATION_SPEED; + + } + + + } + + if ( axis === 'E' || _inPlaneRotation ) { + + this.rotationAxis.copy( this.eye ); + this.rotationAngle = this.pointEnd.angleTo( this.pointStart ); + + this._startNorm.copy( this.pointStart ).normalize(); + this._endNorm.copy( this.pointEnd ).normalize(); + + this.rotationAngle *= ( this._endNorm.cross( this._startNorm ).dot( this.eye ) < 0 ? 1 : - 1 ); + + } + + // Apply rotation snap + + if ( this.rotationSnap ) this.rotationAngle = Math.round( this.rotationAngle / this.rotationSnap ) * this.rotationSnap; + + // Apply rotate + if ( space === 'local' && axis !== 'E' && axis !== 'XYZE' ) { + + object.quaternion.copy( this._quaternionStart ); + object.quaternion.multiply( _tempQuaternion.setFromAxisAngle( this.rotationAxis, this.rotationAngle ) ).normalize(); + + } else { + + this.rotationAxis.applyQuaternion( this._parentQuaternionInv ); + object.quaternion.copy( _tempQuaternion.setFromAxisAngle( this.rotationAxis, this.rotationAngle ) ); + object.quaternion.multiply( this._quaternionStart ).normalize(); + + } + + } + + this.dispatchEvent( _changeEvent ); + this.dispatchEvent( _objectChangeEvent ); + + } + + pointerUp( pointer ) { + + if ( pointer !== null && pointer.button !== 0 ) return; + + if ( this.dragging && ( this.axis !== null ) ) { + + _mouseUpEvent.mode = this.mode; + this.dispatchEvent( _mouseUpEvent ); + + } + + this.dragging = false; + this.axis = null; + + } + + dispose() { + + this.disconnect(); + + this._root.dispose(); + + } + + /** + * Sets the 3D object that should be transformed and ensures the controls UI is visible. + * + * @param {Object3D} object - The 3D object that should be transformed. + * @return {TransformControls} A reference to this controls. + */ + attach( object ) { + + this.object = object; + this._root.visible = true; + + return this; + + } + + /** + * Removes the current 3D object from the controls and makes the helper UI invisible. + * + * @return {TransformControls} A reference to this controls. + */ + detach() { + + this.object = undefined; + this.axis = null; + + this._root.visible = false; + + return this; + + } + + /** + * Resets the object's position, rotation and scale to when the current transform began. + */ + reset() { + + if ( ! this.enabled ) return; + + if ( this.dragging ) { + + this.object.position.copy( this._positionStart ); + this.object.quaternion.copy( this._quaternionStart ); + this.object.scale.copy( this._scaleStart ); + + this.dispatchEvent( _changeEvent ); + this.dispatchEvent( _objectChangeEvent ); + + this.pointStart.copy( this.pointEnd ); + + } + + } + + /** + * Returns the raycaster that is used for user interaction. This object is shared between all + * instances of `TransformControls`. + * + * @returns {Raycaster} The internal raycaster. + */ + getRaycaster() { + + return _raycaster; + + } + + /** + * Returns the transformation mode. + * + * @returns {'translate'|'rotate'|'scale'} The transformation mode. + */ + getMode() { + + return this.mode; + + } + + /** + * Sets the given transformation mode. + * + * @param {'translate'|'rotate'|'scale'} mode - The transformation mode to set. + */ + setMode( mode ) { + + this.mode = mode; + + } + + /** + * Sets the translation snap. + * + * @param {?number} translationSnap - The translation snap to set. + */ + setTranslationSnap( translationSnap ) { + + this.translationSnap = translationSnap; + + } + + /** + * Sets the rotation snap. + * + * @param {?number} rotationSnap - The rotation snap to set. + */ + setRotationSnap( rotationSnap ) { + + this.rotationSnap = rotationSnap; + + } + + /** + * Sets the scale snap. + * + * @param {?number} scaleSnap - The scale snap to set. + */ + setScaleSnap( scaleSnap ) { + + this.scaleSnap = scaleSnap; + + } + + /** + * Sets the size of the helper UI. + * + * @param {number} size - The size to set. + */ + setSize( size ) { + + this.size = size; + + } + + /** + * Sets the coordinate space in which transformations are applied. + * + * @param {'world'|'local'} space - The space to set. + */ + setSpace( space ) { + + this.space = space; + + } + + /** + * Sets the colors of the control's gizmo. + * + * @param {number|Color|string} xAxis - The x-axis color. + * @param {number|Color|string} yAxis - The y-axis color. + * @param {number|Color|string} zAxis - The z-axis color. + * @param {number|Color|string} active - The color for active elements. + */ + setColors( xAxis, yAxis, zAxis, active ) { + + const materialLib = this._gizmo.materialLib; + + materialLib.xAxis.color.set( xAxis ); + materialLib.yAxis.color.set( yAxis ); + materialLib.zAxis.color.set( zAxis ); + materialLib.active.color.set( active ); + materialLib.xAxisTransparent.color.set( xAxis ); + materialLib.yAxisTransparent.color.set( yAxis ); + materialLib.zAxisTransparent.color.set( zAxis ); + materialLib.activeTransparent.color.set( active ); + + // update color caches + + if ( materialLib.xAxis._color ) materialLib.xAxis._color.set( xAxis ); + if ( materialLib.yAxis._color ) materialLib.yAxis._color.set( yAxis ); + if ( materialLib.zAxis._color ) materialLib.zAxis._color.set( zAxis ); + if ( materialLib.active._color ) materialLib.active._color.set( active ); + if ( materialLib.xAxisTransparent._color ) materialLib.xAxisTransparent._color.set( xAxis ); + if ( materialLib.yAxisTransparent._color ) materialLib.yAxisTransparent._color.set( yAxis ); + if ( materialLib.zAxisTransparent._color ) materialLib.zAxisTransparent._color.set( zAxis ); + if ( materialLib.activeTransparent._color ) materialLib.activeTransparent._color.set( active ); + + } + +} + +// mouse / touch event handlers + +function getPointer( event ) { + + if ( this.domElement.ownerDocument.pointerLockElement ) { + + return { + x: 0, + y: 0, + button: event.button + }; + + } else { + + const rect = this.domElement.getBoundingClientRect(); + const viewport = this.viewport; + + let originX, originY, regionWidth, regionHeight; + + if ( viewport !== null ) { + + originX = viewport.x; + originY = rect.height - viewport.y - viewport.w; + regionWidth = viewport.z; + regionHeight = viewport.w; + + } else { + + originX = 0; + originY = 0; + regionWidth = rect.width; + regionHeight = rect.height; + + } + + return { + x: ( event.clientX - rect.left - originX ) / regionWidth * 2 - 1, + y: - ( event.clientY - rect.top - originY ) / regionHeight * 2 + 1, + button: event.button + }; + + } + +} + +function onPointerHover( event ) { + + if ( ! this.enabled ) return; + + switch ( event.pointerType ) { + + case 'mouse': + case 'pen': + this.pointerHover( this._getPointer( event ) ); + break; + + } + +} + +function onPointerDown( event ) { + + if ( ! this.enabled ) return; + + if ( ! document.pointerLockElement ) { + + this.domElement.setPointerCapture( event.pointerId ); + + } + + this.domElement.addEventListener( 'pointermove', this._onPointerMove ); + + this.pointerHover( this._getPointer( event ) ); + this.pointerDown( this._getPointer( event ) ); + +} + +function onPointerMove( event ) { + + if ( ! this.enabled ) return; + + this.pointerMove( this._getPointer( event ) ); + +} + +function onPointerUp( event ) { + + if ( ! this.enabled ) return; + + this.domElement.releasePointerCapture( event.pointerId ); + + this.domElement.removeEventListener( 'pointermove', this._onPointerMove ); + + this.pointerUp( this._getPointer( event ) ); + +} + +function intersectObjectWithRay( object, raycaster, includeInvisible ) { + + const allIntersections = raycaster.intersectObject( object, true ); + + for ( let i = 0; i < allIntersections.length; i ++ ) { + + if ( allIntersections[ i ].object.visible || includeInvisible ) { + + return allIntersections[ i ]; + + } + + } + + return false; + +} + +// + +// Reusable utility variables + +const _tempEuler = new Euler(); +const _alignVector = new Vector3( 0, 1, 0 ); +const _zeroVector = new Vector3( 0, 0, 0 ); +const _lookAtMatrix = new Matrix4(); +const _tempQuaternion2 = new Quaternion(); +const _identityQuaternion = new Quaternion(); +const _dirVector = new Vector3(); +const _tempMatrix = new Matrix4(); + +const _unitX = new Vector3( 1, 0, 0 ); +const _unitY = new Vector3( 0, 1, 0 ); +const _unitZ = new Vector3( 0, 0, 1 ); + +const _v1 = new Vector3(); +const _v2 = new Vector3(); +const _v3 = new Vector3(); + +class TransformControlsRoot extends Object3D { + + constructor( controls ) { + + super(); + + this.isTransformControlsRoot = true; + + this.controls = controls; + this.visible = false; + + } + + // updateMatrixWorld updates key transformation variables + updateMatrixWorld( force ) { + + const controls = this.controls; + + if ( controls.object !== undefined ) { + + controls.object.updateMatrixWorld(); + + if ( controls.object.parent === null ) { + + console.error( 'TransformControls: The attached 3D object must be a part of the scene graph.' ); + + } else { + + controls.object.parent.matrixWorld.decompose( controls._parentPosition, controls._parentQuaternion, controls._parentScale ); + + } + + controls.object.matrixWorld.decompose( controls.worldPosition, controls.worldQuaternion, controls._worldScale ); + + controls._parentQuaternionInv.copy( controls._parentQuaternion ).invert(); + controls._worldQuaternionInv.copy( controls.worldQuaternion ).invert(); + + } + + controls.camera.updateMatrixWorld(); + controls.camera.matrixWorld.decompose( controls.cameraPosition, controls.cameraQuaternion, controls._cameraScale ); + + if ( controls.camera.isOrthographicCamera ) { + + controls.camera.getWorldDirection( controls.eye ).negate(); + + } else { + + controls.eye.copy( controls.cameraPosition ).sub( controls.worldPosition ).normalize(); + + } + + // Cancel out the parent's transform so the gizmo stays world-aligned. + + if ( this.parent ) { + + _tempMatrix.copy( this.parent.matrixWorld ).invert(); + _tempMatrix.decompose( this.position, this.quaternion, this.scale ); + + } + + super.updateMatrixWorld( force ); + + } + + dispose() { + + this.traverse( function ( child ) { + + if ( child.geometry ) child.geometry.dispose(); + if ( child.material ) child.material.dispose(); + + } ); + + } + +} + +class TransformControlsGizmo extends Object3D { + + constructor() { + + super(); + + this.isTransformControlsGizmo = true; + + this.type = 'TransformControlsGizmo'; + + // shared materials + + const gizmoMaterial = new MeshBasicMaterial( { + depthTest: false, + depthWrite: false, + fog: false, + toneMapped: false, + transparent: true + } ); + + const gizmoLineMaterial = new LineBasicMaterial( { + depthTest: false, + depthWrite: false, + fog: false, + toneMapped: false, + transparent: true + } ); + + // Make unique material for each axis/color + + const matInvisible = gizmoMaterial.clone(); + matInvisible.opacity = 0.15; + + const matHelper = gizmoLineMaterial.clone(); + matHelper.opacity = 0.5; + + const matRed = gizmoMaterial.clone(); + matRed.color.setHex( 0xff0000 ); + + const matGreen = gizmoMaterial.clone(); + matGreen.color.setHex( 0x00ff00 ); + + const matBlue = gizmoMaterial.clone(); + matBlue.color.setHex( 0x0000ff ); + + const matRedTransparent = gizmoMaterial.clone(); + matRedTransparent.color.setHex( 0xff0000 ); + matRedTransparent.opacity = 0.5; + + const matGreenTransparent = gizmoMaterial.clone(); + matGreenTransparent.color.setHex( 0x00ff00 ); + matGreenTransparent.opacity = 0.5; + + const matBlueTransparent = gizmoMaterial.clone(); + matBlueTransparent.color.setHex( 0x0000ff ); + matBlueTransparent.opacity = 0.5; + + const matWhiteTransparent = gizmoMaterial.clone(); + matWhiteTransparent.opacity = 0.25; + + const matYellowTransparent = gizmoMaterial.clone(); + matYellowTransparent.color.setHex( 0xffff00 ); + matYellowTransparent.opacity = 0.25; + + const matYellow = gizmoMaterial.clone(); + matYellow.color.setHex( 0xffff00 ); + + const matGray = gizmoMaterial.clone(); + matGray.color.setHex( 0x787878 ); + + // materials in the below property are configurable via setColors() + + this.materialLib = { + xAxis: matRed, + yAxis: matGreen, + zAxis: matBlue, + active: matYellow, + xAxisTransparent: matRedTransparent, + yAxisTransparent: matGreenTransparent, + zAxisTransparent: matBlueTransparent, + activeTransparent: matYellowTransparent + }; + + // reusable geometry + + const arrowGeometry = new CylinderGeometry( 0, 0.04, 0.1, 12 ); + arrowGeometry.translate( 0, 0.05, 0 ); + + const scaleHandleGeometry = new BoxGeometry( 0.08, 0.08, 0.08 ); + scaleHandleGeometry.translate( 0, 0.04, 0 ); + + const lineGeometry = new BufferGeometry(); + lineGeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) ); + + const lineGeometry2 = new CylinderGeometry( 0.0075, 0.0075, 0.5, 3 ); + lineGeometry2.translate( 0, 0.25, 0 ); + + function CircleGeometry( radius, arc ) { + + const geometry = new TorusGeometry( radius, 0.0075, 3, 64, arc * Math.PI * 2 ); + geometry.rotateY( Math.PI / 2 ); + geometry.rotateX( Math.PI / 2 ); + return geometry; + + } + + // Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position + + function TranslateHelperGeometry() { + + const geometry = new BufferGeometry(); + + geometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 1, 1 ], 3 ) ); + + return geometry; + + } + + // Gizmo definitions - custom hierarchy definitions for setupGizmo() function + + const gizmoTranslate = { + X: [ + [ new Mesh( arrowGeometry, matRed ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], + [ new Mesh( arrowGeometry, matRed ), [ - 0.5, 0, 0 ], [ 0, 0, Math.PI / 2 ]], + [ new Mesh( lineGeometry2, matRed ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]] + ], + Y: [ + [ new Mesh( arrowGeometry, matGreen ), [ 0, 0.5, 0 ]], + [ new Mesh( arrowGeometry, matGreen ), [ 0, - 0.5, 0 ], [ Math.PI, 0, 0 ]], + [ new Mesh( lineGeometry2, matGreen ) ] + ], + Z: [ + [ new Mesh( arrowGeometry, matBlue ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]], + [ new Mesh( arrowGeometry, matBlue ), [ 0, 0, - 0.5 ], [ - Math.PI / 2, 0, 0 ]], + [ new Mesh( lineGeometry2, matBlue ), null, [ Math.PI / 2, 0, 0 ]] + ], + XYZ: [ + [ new Mesh( new OctahedronGeometry( 0.1, 0 ), matWhiteTransparent ), [ 0, 0, 0 ]] + ], + XY: [ + [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent ), [ 0.15, 0.15, 0 ]] + ], + YZ: [ + [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] + ], + XZ: [ + [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] + ] + }; + + const pickerTranslate = { + X: [ + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0.3, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ - 0.3, 0, 0 ], [ 0, 0, Math.PI / 2 ]] + ], + Y: [ + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0.3, 0 ]], + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, - 0.3, 0 ], [ 0, 0, Math.PI ]] + ], + Z: [ + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, 0.3 ], [ Math.PI / 2, 0, 0 ]], + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, - 0.3 ], [ - Math.PI / 2, 0, 0 ]] + ], + XYZ: [ + [ new Mesh( new OctahedronGeometry( 0.2, 0 ), matInvisible ) ] + ], + XY: [ + [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0.15, 0 ]] + ], + YZ: [ + [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] + ], + XZ: [ + [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] + ] + }; + + const helperTranslate = { + START: [ + [ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ] + ], + END: [ + [ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ] + ], + DELTA: [ + [ new Line( TranslateHelperGeometry(), matHelper ), null, null, null, 'helper' ] + ], + X: [ + [ new Line( lineGeometry, matHelper ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] + ], + Y: [ + [ new Line( lineGeometry, matHelper ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ] + ], + Z: [ + [ new Line( lineGeometry, matHelper ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ] + ] + }; + + const gizmoRotate = { + XYZE: [ + [ new Mesh( CircleGeometry( 0.5, 1 ), matGray ), null, [ 0, Math.PI / 2, 0 ]] + ], + X: [ + [ new Mesh( CircleGeometry( 0.5, 0.5 ), matRed ) ] + ], + Y: [ + [ new Mesh( CircleGeometry( 0.5, 0.5 ), matGreen ), null, [ 0, 0, - Math.PI / 2 ]] + ], + Z: [ + [ new Mesh( CircleGeometry( 0.5, 0.5 ), matBlue ), null, [ 0, Math.PI / 2, 0 ]] + ], + E: [ + [ new Mesh( CircleGeometry( 0.75, 1 ), matYellowTransparent ), null, [ 0, Math.PI / 2, 0 ]] + ] + }; + + const helperRotate = { + AXIS: [ + [ new Line( lineGeometry, matHelper ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] + ] + }; + + const pickerRotate = { + XYZE: [ + [ new Mesh( new SphereGeometry( 0.25, 10, 8 ), matInvisible ) ] + ], + X: [ + [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, - Math.PI / 2, - Math.PI / 2 ]], + ], + Y: [ + [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]], + ], + Z: [ + [ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], + ], + E: [ + [ new Mesh( new TorusGeometry( 0.75, 0.1, 2, 24 ), matInvisible ) ] + ] + }; + + const gizmoScale = { + X: [ + [ new Mesh( scaleHandleGeometry, matRed ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], + [ new Mesh( lineGeometry2, matRed ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], + [ new Mesh( scaleHandleGeometry, matRed ), [ - 0.5, 0, 0 ], [ 0, 0, Math.PI / 2 ]], + ], + Y: [ + [ new Mesh( scaleHandleGeometry, matGreen ), [ 0, 0.5, 0 ]], + [ new Mesh( lineGeometry2, matGreen ) ], + [ new Mesh( scaleHandleGeometry, matGreen ), [ 0, - 0.5, 0 ], [ 0, 0, Math.PI ]], + ], + Z: [ + [ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]], + [ new Mesh( lineGeometry2, matBlue ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]], + [ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, - 0.5 ], [ - Math.PI / 2, 0, 0 ]] + ], + XY: [ + [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent ), [ 0.15, 0.15, 0 ]] + ], + YZ: [ + [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]] + ], + XZ: [ + [ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]] + ], + XYZ: [ + [ new Mesh( new BoxGeometry( 0.1, 0.1, 0.1 ), matWhiteTransparent ) ], + ] + }; + + const pickerScale = { + X: [ + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0.3, 0, 0 ], [ 0, 0, - Math.PI / 2 ]], + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ - 0.3, 0, 0 ], [ 0, 0, Math.PI / 2 ]] + ], + Y: [ + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0.3, 0 ]], + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, - 0.3, 0 ], [ 0, 0, Math.PI ]] + ], + Z: [ + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, 0.3 ], [ Math.PI / 2, 0, 0 ]], + [ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, - 0.3 ], [ - Math.PI / 2, 0, 0 ]] + ], + XY: [ + [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0.15, 0 ]], + ], + YZ: [ + [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]], + ], + XZ: [ + [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]], + ], + XYZ: [ + [ new Mesh( new BoxGeometry( 0.2, 0.2, 0.2 ), matInvisible ), [ 0, 0, 0 ]], + ] + }; + + const helperScale = { + X: [ + [ new Line( lineGeometry, matHelper ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ] + ], + Y: [ + [ new Line( lineGeometry, matHelper ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ] + ], + Z: [ + [ new Line( lineGeometry, matHelper ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ] + ] + }; + + // Creates an Object3D with gizmos described in custom hierarchy definition. + + function setupGizmo( gizmoMap ) { + + const gizmo = new Object3D(); + + for ( const name in gizmoMap ) { + + for ( let i = gizmoMap[ name ].length; i --; ) { + + const object = gizmoMap[ name ][ i ][ 0 ].clone(); + const position = gizmoMap[ name ][ i ][ 1 ]; + const rotation = gizmoMap[ name ][ i ][ 2 ]; + const scale = gizmoMap[ name ][ i ][ 3 ]; + const tag = gizmoMap[ name ][ i ][ 4 ]; + + // name and tag properties are essential for picking and updating logic. + object.name = name; + object.tag = tag; + + if ( position ) { + + object.position.set( position[ 0 ], position[ 1 ], position[ 2 ] ); + + } + + if ( rotation ) { + + object.rotation.set( rotation[ 0 ], rotation[ 1 ], rotation[ 2 ] ); + + } + + if ( scale ) { + + object.scale.set( scale[ 0 ], scale[ 1 ], scale[ 2 ] ); + + } + + object.updateMatrix(); + + const tempGeometry = object.geometry.clone(); + tempGeometry.applyMatrix4( object.matrix ); + object.geometry = tempGeometry; + object.renderOrder = Infinity; + + object.position.set( 0, 0, 0 ); + object.rotation.set( 0, 0, 0 ); + object.scale.set( 1, 1, 1 ); + + gizmo.add( object ); + + } + + } + + return gizmo; + + } + + // Gizmo creation + + this.gizmo = {}; + this.picker = {}; + this.helper = {}; + + this.add( this.gizmo[ 'translate' ] = setupGizmo( gizmoTranslate ) ); + this.add( this.gizmo[ 'rotate' ] = setupGizmo( gizmoRotate ) ); + this.add( this.gizmo[ 'scale' ] = setupGizmo( gizmoScale ) ); + this.add( this.picker[ 'translate' ] = setupGizmo( pickerTranslate ) ); + this.add( this.picker[ 'rotate' ] = setupGizmo( pickerRotate ) ); + this.add( this.picker[ 'scale' ] = setupGizmo( pickerScale ) ); + this.add( this.helper[ 'translate' ] = setupGizmo( helperTranslate ) ); + this.add( this.helper[ 'rotate' ] = setupGizmo( helperRotate ) ); + this.add( this.helper[ 'scale' ] = setupGizmo( helperScale ) ); + + // Pickers should be hidden always + + this.picker[ 'translate' ].visible = false; + this.picker[ 'rotate' ].visible = false; + this.picker[ 'scale' ].visible = false; + + } + + // updateMatrixWorld will update transformations and appearance of individual handles + + updateMatrixWorld( force ) { + + const space = ( this.mode === 'scale' ) ? 'local' : this.space; // scale always oriented to local rotation + + const quaternion = ( space === 'local' ) ? this.worldQuaternion : _identityQuaternion; + + // Show only gizmos for current transform mode + + this.gizmo[ 'translate' ].visible = this.mode === 'translate'; + this.gizmo[ 'rotate' ].visible = this.mode === 'rotate'; + this.gizmo[ 'scale' ].visible = this.mode === 'scale'; + + this.helper[ 'translate' ].visible = this.mode === 'translate'; + this.helper[ 'rotate' ].visible = this.mode === 'rotate'; + this.helper[ 'scale' ].visible = this.mode === 'scale'; + + + let handles = []; + handles = handles.concat( this.picker[ this.mode ].children ); + handles = handles.concat( this.gizmo[ this.mode ].children ); + handles = handles.concat( this.helper[ this.mode ].children ); + + for ( let i = 0; i < handles.length; i ++ ) { + + const handle = handles[ i ]; + + // hide aligned to camera + + handle.visible = true; + handle.rotation.set( 0, 0, 0 ); + handle.position.copy( this.worldPosition ); + + let factor; + + if ( this.camera.isOrthographicCamera ) { + + factor = ( this.camera.top - this.camera.bottom ) / this.camera.zoom; + + } else { + + factor = this.worldPosition.distanceTo( this.cameraPosition ) * Math.min( 1.9 * Math.tan( Math.PI * this.camera.fov / 360 ) / this.camera.zoom, 7 ); + + } + + handle.scale.set( 1, 1, 1 ).multiplyScalar( factor * this.size / 4 ); + + // TODO: simplify helpers and consider decoupling from gizmo + + if ( handle.tag === 'helper' ) { + + handle.visible = false; + + if ( handle.name === 'AXIS' ) { + + handle.visible = !! this.axis; + + if ( this.axis === 'X' ) { + + _tempQuaternion.setFromEuler( _tempEuler.set( 0, 0, 0 ) ); + handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); + + if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { + + handle.visible = false; + + } + + } + + if ( this.axis === 'Y' ) { + + _tempQuaternion.setFromEuler( _tempEuler.set( 0, 0, Math.PI / 2 ) ); + handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); + + if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { + + handle.visible = false; + + } + + } + + if ( this.axis === 'Z' ) { + + _tempQuaternion.setFromEuler( _tempEuler.set( 0, Math.PI / 2, 0 ) ); + handle.quaternion.copy( quaternion ).multiply( _tempQuaternion ); + + if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) { + + handle.visible = false; + + } + + } + + if ( this.axis === 'XYZE' ) { + + _tempQuaternion.setFromEuler( _tempEuler.set( 0, Math.PI / 2, 0 ) ); + _alignVector.copy( this.rotationAxis ); + handle.quaternion.setFromRotationMatrix( _lookAtMatrix.lookAt( _zeroVector, _alignVector, _unitY ) ); + handle.quaternion.multiply( _tempQuaternion ); + handle.visible = this.dragging; + + } + + if ( this.axis === 'E' ) { + + handle.visible = false; + + } + + + } else if ( handle.name === 'START' ) { + + handle.position.copy( this.worldPositionStart ); + handle.visible = this.dragging; + + } else if ( handle.name === 'END' ) { + + handle.position.copy( this.worldPosition ); + handle.visible = this.dragging; + + } else if ( handle.name === 'DELTA' ) { + + handle.position.copy( this.worldPositionStart ); + handle.quaternion.copy( this.worldQuaternionStart ); + _tempVector.set( 1e-10, 1e-10, 1e-10 ).add( this.worldPositionStart ).sub( this.worldPosition ).multiplyScalar( - 1 ); + _tempVector.applyQuaternion( this.worldQuaternionStart.clone().invert() ); + handle.scale.copy( _tempVector ); + handle.visible = this.dragging; + + } else { + + handle.quaternion.copy( quaternion ); + + if ( this.dragging ) { + + handle.position.copy( this.worldPositionStart ); + + } else { + + handle.position.copy( this.worldPosition ); + + } + + if ( this.axis ) { + + handle.visible = this.axis.search( handle.name ) !== - 1; + + } + + } + + // If updating helper, skip rest of the loop + continue; + + } + + // Align handles to current local or world rotation + + handle.quaternion.copy( quaternion ); + + if ( this.mode === 'translate' || this.mode === 'scale' ) { + + // Hide translate and scale axis facing the camera + + const AXIS_HIDE_THRESHOLD = 0.99; + const PLANE_HIDE_THRESHOLD = 0.2; + + if ( handle.name === 'X' ) { + + if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) { + + handle.scale.set( 1e-10, 1e-10, 1e-10 ); + handle.visible = false; + + } + + } + + if ( handle.name === 'Y' ) { + + if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) { + + handle.scale.set( 1e-10, 1e-10, 1e-10 ); + handle.visible = false; + + } + + } + + if ( handle.name === 'Z' ) { + + if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) { + + handle.scale.set( 1e-10, 1e-10, 1e-10 ); + handle.visible = false; + + } + + } + + if ( handle.name === 'XY' ) { + + if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) { + + handle.scale.set( 1e-10, 1e-10, 1e-10 ); + handle.visible = false; + + } + + } + + if ( handle.name === 'YZ' ) { + + if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) { + + handle.scale.set( 1e-10, 1e-10, 1e-10 ); + handle.visible = false; + + } + + } + + if ( handle.name === 'XZ' ) { + + if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) { + + handle.scale.set( 1e-10, 1e-10, 1e-10 ); + handle.visible = false; + + } + + } + + } else if ( this.mode === 'rotate' ) { + + // Align handles to current local or world rotation + + _tempQuaternion2.copy( quaternion ); + _alignVector.copy( this.eye ).applyQuaternion( _tempQuaternion.copy( quaternion ).invert() ); + + if ( handle.name.search( 'E' ) !== - 1 ) { + + handle.quaternion.setFromRotationMatrix( _lookAtMatrix.lookAt( this.eye, _zeroVector, _unitY ) ); + + } + + if ( handle.name === 'X' ) { + + _tempQuaternion.setFromAxisAngle( _unitX, Math.atan2( - _alignVector.y, _alignVector.z ) ); + _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); + handle.quaternion.copy( _tempQuaternion ); + + } + + if ( handle.name === 'Y' ) { + + _tempQuaternion.setFromAxisAngle( _unitY, Math.atan2( _alignVector.x, _alignVector.z ) ); + _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); + handle.quaternion.copy( _tempQuaternion ); + + } + + if ( handle.name === 'Z' ) { + + _tempQuaternion.setFromAxisAngle( _unitZ, Math.atan2( _alignVector.y, _alignVector.x ) ); + _tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion ); + handle.quaternion.copy( _tempQuaternion ); + + } + + } + + // Hide disabled axes + handle.visible = handle.visible && ( handle.name.indexOf( 'X' ) === - 1 || this.showX ); + handle.visible = handle.visible && ( handle.name.indexOf( 'Y' ) === - 1 || this.showY ); + handle.visible = handle.visible && ( handle.name.indexOf( 'Z' ) === - 1 || this.showZ ); + handle.visible = handle.visible && ( handle.name.indexOf( 'E' ) === - 1 || ( this.showX && this.showY && this.showZ ) ); + + // Hide disabled plane helpers + handle.visible = handle.visible && ( handle.name.indexOf( 'XY' ) === - 1 || this.showXY ); + handle.visible = handle.visible && ( handle.name.indexOf( 'YZ' ) === - 1 || this.showYZ ); + handle.visible = handle.visible && ( handle.name.indexOf( 'XZ' ) === - 1 || this.showXZ ); + + // Hide disabled rotation helpers + handle.visible = handle.visible && ( handle.name !== 'E' || this.showE ); + handle.visible = handle.visible && ( handle.name !== 'XYZE' || this.showXYZE ); + + // highlight selected axis + + handle.material._color = handle.material._color || handle.material.color.clone(); + handle.material._opacity = handle.material._opacity || handle.material.opacity; + + handle.material.color.copy( handle.material._color ); + handle.material.opacity = handle.material._opacity; + + if ( this.enabled && this.axis ) { + + if ( handle.name === this.axis ) { + + handle.material.color.copy( this.materialLib.active.color ); + handle.material.opacity = 1.0; + + } else if ( this.axis.split( '' ).some( function ( a ) { + + return handle.name === a; + + } ) ) { + + handle.material.color.copy( this.materialLib.active.color ); + handle.material.opacity = 1.0; + + } + + } + + } + + super.updateMatrixWorld( force ); + + } + +} + +// + +class TransformControlsPlane extends Mesh { + + constructor() { + + super( + new PlaneGeometry( 100000, 100000, 2, 2 ), + new MeshBasicMaterial( { visible: false, wireframe: true, side: DoubleSide, transparent: true, opacity: 0.1, toneMapped: false } ) + ); + + this.isTransformControlsPlane = true; + + this.type = 'TransformControlsPlane'; + + } + + updateMatrixWorld( force ) { + + let space = this.space; + + this.position.copy( this.worldPosition ); + + if ( this.mode === 'scale' ) space = 'local'; // scale always oriented to local rotation + + _v1.copy( _unitX ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); + _v2.copy( _unitY ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); + _v3.copy( _unitZ ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion ); + + // Align the plane for current transform mode, axis and space. + + _alignVector.copy( _v2 ); + + switch ( this.mode ) { + + case 'translate': + case 'scale': + switch ( this.axis ) { + + case 'X': + _alignVector.copy( this.eye ).cross( _v1 ); + _dirVector.copy( _v1 ).cross( _alignVector ); + break; + case 'Y': + _alignVector.copy( this.eye ).cross( _v2 ); + _dirVector.copy( _v2 ).cross( _alignVector ); + break; + case 'Z': + _alignVector.copy( this.eye ).cross( _v3 ); + _dirVector.copy( _v3 ).cross( _alignVector ); + break; + case 'XY': + _dirVector.copy( _v3 ); + break; + case 'YZ': + _dirVector.copy( _v1 ); + break; + case 'XZ': + _alignVector.copy( _v3 ); + _dirVector.copy( _v2 ); + break; + case 'XYZ': + case 'E': + _dirVector.set( 0, 0, 0 ); + break; + + } + + break; + case 'rotate': + default: + // special case for rotate + _dirVector.set( 0, 0, 0 ); + + } + + if ( _dirVector.length() === 0 ) { + + // If in rotate mode, make the plane parallel to camera + this.quaternion.copy( this.cameraQuaternion ); + + } else { + + _tempMatrix.lookAt( _tempVector.set( 0, 0, 0 ), _dirVector, _alignVector ); + + this.quaternion.setFromRotationMatrix( _tempMatrix ); + + } + + super.updateMatrixWorld( force ); + + } + +} + +export { TransformControls, TransformControlsGizmo, TransformControlsPlane }; -- cgit v1.3.1