diff --git a/public/voxeltest.html b/public/voxeltest.html new file mode 100644 index 00000000..e98022e9 --- /dev/null +++ b/public/voxeltest.html @@ -0,0 +1,21 @@ + + + + three.js webgl - interactive - voxel painter + + + + + + + + + diff --git a/src/controls/OrbitControls.js b/src/controls/OrbitControls.js new file mode 100644 index 00000000..0892af1c --- /dev/null +++ b/src/controls/OrbitControls.js @@ -0,0 +1,1164 @@ +/** + * @author qiao / https://github.com/qiao + * @author mrdoob / http://mrdoob.com + * @author alteredq / http://alteredqualia.com/ + * @author WestLangley / http://github.com/WestLangley + * @author erich666 / http://erichaines.com + * @author ScieCode / http://github.com/sciecode + */ + +import { + EventDispatcher, + MOUSE, + Quaternion, + Spherical, + TOUCH, + Vector2, + Vector3 +} from "three"; + +// This set of controls performs orbiting, dollying (zooming), and panning. +// Unlike 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 + +var OrbitControls = function ( object, domElement ) { + + if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); + if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); + + this.object = object; + this.domElement = domElement; + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the object orbits around + this.target = new Vector3(); + + // How far you can dolly in and out ( PerspectiveCamera only ) + this.minDistance = 0; + this.maxDistance = Infinity; + + // How far you can zoom in and out ( OrthographicCamera only ) + this.minZoom = 0; + this.maxZoom = Infinity; + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = - Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + this.enableDamping = false; + this.dampingFactor = 0.05; + + // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + this.enableZoom = true; + this.zoomSpeed = 1.0; + + // Set to false to disable rotating + this.enableRotate = true; + this.rotateSpeed = 1.0; + + // Set to false to disable panning + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; // if true, pan in screen-space + this.keyPanSpeed = 20.0; // pixels moved per arrow key push + + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 + + // Set to false to disable use of the keys + this.enableKeys = true; + + // The four arrow keys + this.keys = { LEFT: 65, FORWARD: 87, RIGHT: 68, BACKWARD: 83 }; + + // Mouse buttons + this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; + + // Touch fingers + this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + + // + // public methods + // + + this.getPolarAngle = function () { + + return spherical.phi; + + }; + + this.getAzimuthalAngle = function () { + + return spherical.theta; + + }; + + this.saveState = function () { + + scope.target0.copy( scope.target ); + scope.position0.copy( scope.object.position ); + scope.zoom0 = scope.object.zoom; + + }; + + this.reset = function () { + + scope.target.copy( scope.target0 ); + scope.object.position.copy( scope.position0 ); + scope.object.zoom = scope.zoom0; + + scope.object.updateProjectionMatrix(); + scope.dispatchEvent( changeEvent ); + + scope.update(); + + state = STATE.NONE; + + }; + + // this method is exposed, but perhaps it would be better if we can make it private... + this.update = function () { + + var offset = new Vector3(); + + // so camera.up is the orbit axis + var quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); + var quatInverse = quat.clone().inverse(); + + var lastPosition = new Vector3(); + var lastQuaternion = new Quaternion(); + + return function update() { + + var position = scope.object.position; + + offset.copy( position ).sub( scope.target ); + + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion( quat ); + + // angle from z-axis around y-axis + spherical.setFromVector3( offset ); + + if ( scope.autoRotate && state === STATE.NONE ) { + + rotateLeft( getAutoRotationAngle() ); + + } + + if ( scope.enableDamping ) { + + spherical.theta += sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; + + } else { + + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + + } + + // restrict theta to be between desired limits + spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); + + // restrict phi to be between desired limits + spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); + + spherical.makeSafe(); + + + spherical.radius *= scale; + + // restrict radius to be between desired limits + spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); + + // move target to panned location + + if ( scope.enableDamping === true ) { + + scope.target.addScaledVector( panOffset, scope.dampingFactor ); + + } else { + + scope.target.add( panOffset ); + + } + + offset.setFromSpherical( spherical ); + + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion( quatInverse ); + + position.copy( scope.target ).add( offset ); + + scope.object.lookAt( scope.target ); + + if ( scope.enableDamping === true ) { + + sphericalDelta.theta *= ( 1 - scope.dampingFactor ); + sphericalDelta.phi *= ( 1 - scope.dampingFactor ); + + panOffset.multiplyScalar( 1 - scope.dampingFactor ); + + } else { + + sphericalDelta.set( 0, 0, 0 ); + + panOffset.set( 0, 0, 0 ); + + } + + scale = 1; + + // 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 || + lastPosition.distanceToSquared( scope.object.position ) > EPS || + 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { + + scope.dispatchEvent( changeEvent ); + + lastPosition.copy( scope.object.position ); + lastQuaternion.copy( scope.object.quaternion ); + zoomChanged = false; + + return true; + + } + + return false; + + }; + + }(); + + this.dispose = function () { + + scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); + scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); + scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); + + scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); + scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); + scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); + + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + + scope.domElement.removeEventListener( 'keydown', onKeyDown, false ); + + //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? + + }; + + // + // internals + // + + var scope = this; + + var changeEvent = { type: 'change' }; + var startEvent = { type: 'start' }; + var endEvent = { type: 'end' }; + + var STATE = { + NONE: - 1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6 + }; + + var state = STATE.NONE; + + var EPS = 0.000001; + + // current position in spherical coordinates + var spherical = new Spherical(); + var sphericalDelta = new Spherical(); + + var scale = 1; + var panOffset = new Vector3(); + var zoomChanged = false; + + var rotateStart = new Vector2(); + var rotateEnd = new Vector2(); + var rotateDelta = new Vector2(); + + var panStart = new Vector2(); + var panEnd = new Vector2(); + var panDelta = new Vector2(); + + var dollyStart = new Vector2(); + var dollyEnd = new Vector2(); + var dollyDelta = new Vector2(); + + function getAutoRotationAngle() { + + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + + } + + function getZoomScale() { + + return Math.pow( 0.95, scope.zoomSpeed ); + + } + + function rotateLeft( angle ) { + + sphericalDelta.theta -= angle; + + } + + function rotateUp( angle ) { + + sphericalDelta.phi -= angle; + + } + + var panLeft = function () { + + var v = new Vector3(); + + return function panLeft( distance, objectMatrix ) { + + v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix + v.multiplyScalar( - distance ); + + panOffset.add( v ); + + }; + + }(); + + var panUp = function () { + + var v = new Vector3(); + + return function panUp( distance, objectMatrix ) { + + if ( scope.screenSpacePanning === true ) { + + v.setFromMatrixColumn( objectMatrix, 1 ); + + } else { + + v.setFromMatrixColumn( objectMatrix, 0 ); + v.crossVectors( scope.object.up, v ); + + } + + v.multiplyScalar( distance ); + + //panOffset.add( v ); + scope.object.z += 50; + + }; + + }(); + + // deltaX and deltaY are in pixels; right and down are positive + var pan = function () { + + var offset = new Vector3(); + + return function pan( deltaX, deltaY ) { + + var element = scope.domElement; + + if ( scope.object.isPerspectiveCamera ) { + + // perspective + var position = scope.object.position; + offset.copy( position ).sub( scope.target ); + var targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); + + // we use only clientHeight here so aspect ratio does not distort speed + panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); + panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); + + } else if ( scope.object.isOrthographicCamera ) { + + // orthographic + panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); + panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); + + } else { + + // camera neither orthographic nor perspective + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); + scope.enablePan = false; + + } + + }; + + }(); + + function dollyIn( dollyScale ) { + + if ( scope.object.isPerspectiveCamera ) { + + scale /= dollyScale; + + } else if ( scope.object.isOrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + function dollyOut( dollyScale ) { + + if ( scope.object.isPerspectiveCamera ) { + + scale *= dollyScale; + + } else if ( scope.object.isOrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + // + // event callbacks - update the object state + // + + function handleMouseDownRotate( event ) { + + rotateStart.set( event.clientX, event.clientY ); + + } + + function handleMouseDownDolly( event ) { + + dollyStart.set( event.clientX, event.clientY ); + + } + + function handleMouseDownPan( event ) { + + panStart.set( event.clientX, event.clientY ); + + } + + function handleMouseMoveRotate( event ) { + + rotateEnd.set( event.clientX, event.clientY ); + + rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); + + var element = scope.domElement; + + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height + + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); + + rotateStart.copy( rotateEnd ); + + scope.update(); + + } + + function handleMouseMoveDolly( event ) { + + dollyEnd.set( event.clientX, event.clientY ); + + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + dollyIn( getZoomScale() ); + + } else if ( dollyDelta.y < 0 ) { + + dollyOut( getZoomScale() ); + + } + + dollyStart.copy( dollyEnd ); + + scope.update(); + + } + + function handleMouseMovePan( event ) { + + panEnd.set( event.clientX, event.clientY ); + + panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); + + pan( panDelta.x, panDelta.y ); + + panStart.copy( panEnd ); + + scope.update(); + + } + + function handleMouseUp( /*event*/ ) { + + // no-op + + } + + function handleMouseWheel( event ) { + + if ( event.deltaY < 0 ) { + + dollyOut( getZoomScale() ); + + } else if ( event.deltaY > 0 ) { + + dollyIn( getZoomScale() ); + + } + + scope.update(); + + } + + function handleKeyDown( event ) { + + var needsUpdate = false; + console.log(event.keyCode); + + switch ( event.keyCode ) { + + case scope.keys.FORWARD: + pan( 0, scope.keyPanSpeed ); + needsUpdate = true; + break; + + case scope.keys.BACKWARD: + pan( 0, - scope.keyPanSpeed ); + needsUpdate = true; + break; + + case scope.keys.LEFT: + pan( scope.keyPanSpeed, 0 ); + needsUpdate = true; + break; + + case scope.keys.RIGHT: + pan( - scope.keyPanSpeed, 0 ); + needsUpdate = true; + break; + + } + + if ( needsUpdate ) { + + // prevent the browser from scrolling on cursor keys + event.preventDefault(); + + scope.update(); + + } + + + } + + function handleTouchStartRotate( event ) { + + if ( event.touches.length == 1 ) { + + rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + + rotateStart.set( x, y ); + + } + + } + + function handleTouchStartPan( event ) { + + if ( event.touches.length == 1 ) { + + panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + + panStart.set( x, y ); + + } + + } + + function handleTouchStartDolly( event ) { + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + + var distance = Math.sqrt( dx * dx + dy * dy ); + + dollyStart.set( 0, distance ); + + } + + function handleTouchStartDollyPan( event ) { + + if ( scope.enableZoom ) handleTouchStartDolly( event ); + + if ( scope.enablePan ) handleTouchStartPan( event ); + + } + + function handleTouchStartDollyRotate( event ) { + + if ( scope.enableZoom ) handleTouchStartDolly( event ); + + if ( scope.enableRotate ) handleTouchStartRotate( event ); + + } + + function handleTouchMoveRotate( event ) { + + if ( event.touches.length == 1 ) { + + rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + + rotateEnd.set( x, y ); + + } + + rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); + + var element = scope.domElement; + + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height + + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); + + rotateStart.copy( rotateEnd ); + + } + + function handleTouchMovePan( event ) { + + if ( event.touches.length == 1 ) { + + panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + + panEnd.set( x, y ); + + } + + panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); + + pan( panDelta.x, panDelta.y ); + + panStart.copy( panEnd ); + + } + + function handleTouchMoveDolly( event ) { + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + + var distance = Math.sqrt( dx * dx + dy * dy ); + + dollyEnd.set( 0, distance ); + + dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); + + dollyIn( dollyDelta.y ); + + dollyStart.copy( dollyEnd ); + + } + + function handleTouchMoveDollyPan( event ) { + + if ( scope.enableZoom ) handleTouchMoveDolly( event ); + + if ( scope.enablePan ) handleTouchMovePan( event ); + + } + + function handleTouchMoveDollyRotate( event ) { + + if ( scope.enableZoom ) handleTouchMoveDolly( event ); + + if ( scope.enableRotate ) handleTouchMoveRotate( event ); + + } + + function handleTouchEnd( /*event*/ ) { + + // no-op + + } + + // + // event handlers - FSM: listen for events and reset state + // + + function onMouseDown( event ) { + + if ( scope.enabled === false ) return; + + // Prevent the browser from scrolling. + event.preventDefault(); + + // Manually set the focus since calling preventDefault above + // prevents the browser from setting it automatically. + + scope.domElement.focus ? scope.domElement.focus() : window.focus(); + + var mouseAction; + + switch ( event.button ) { + + case 0: + + mouseAction = scope.mouseButtons.LEFT; + break; + + case 1: + + mouseAction = scope.mouseButtons.MIDDLE; + break; + + case 2: + + mouseAction = scope.mouseButtons.RIGHT; + break; + + default: + + mouseAction = - 1; + + } + + switch ( mouseAction ) { + + case MOUSE.DOLLY: + + if ( scope.enableZoom === false ) return; + + handleMouseDownDolly( event ); + + state = STATE.DOLLY; + + break; + + case MOUSE.ROTATE: + + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( scope.enablePan === false ) return; + + handleMouseDownPan( event ); + + state = STATE.PAN; + + } else { + + if ( scope.enableRotate === false ) return; + + handleMouseDownRotate( event ); + + state = STATE.ROTATE; + + } + + break; + + case MOUSE.PAN: + + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( scope.enableRotate === false ) return; + + handleMouseDownRotate( event ); + + state = STATE.ROTATE; + + } else { + + if ( scope.enablePan === false ) return; + + handleMouseDownPan( event ); + + state = STATE.PAN; + + } + + break; + + default: + + state = STATE.NONE; + + } + + if ( state !== STATE.NONE ) { + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + + scope.dispatchEvent( startEvent ); + + } + + } + + function onMouseMove( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + switch ( state ) { + + case STATE.ROTATE: + + if ( scope.enableRotate === false ) return; + + handleMouseMoveRotate( event ); + + break; + + case STATE.DOLLY: + + if ( scope.enableZoom === false ) return; + + handleMouseMoveDolly( event ); + + break; + + case STATE.PAN: + + if ( scope.enablePan === false ) return; + + handleMouseMovePan( event ); + + break; + + } + + } + + function onMouseUp( event ) { + + if ( scope.enabled === false ) return; + + handleMouseUp( event ); + + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + + scope.dispatchEvent( endEvent ); + + state = STATE.NONE; + + } + + function onMouseWheel( event ) { + + if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; + + event.preventDefault(); + event.stopPropagation(); + + scope.dispatchEvent( startEvent ); + + handleMouseWheel( event ); + + scope.dispatchEvent( endEvent ); + + } + + function onKeyDown( event ) { + + if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; + + handleKeyDown( event ); + + } + + function onTouchStart( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + switch ( event.touches.length ) { + + case 1: + + switch ( scope.touches.ONE ) { + + case TOUCH.ROTATE: + + if ( scope.enableRotate === false ) return; + + handleTouchStartRotate( event ); + + state = STATE.TOUCH_ROTATE; + + break; + + case TOUCH.PAN: + + if ( scope.enablePan === false ) return; + + handleTouchStartPan( event ); + + state = STATE.TOUCH_PAN; + + break; + + default: + + state = STATE.NONE; + + } + + break; + + case 2: + + switch ( scope.touches.TWO ) { + + case TOUCH.DOLLY_PAN: + + if ( scope.enableZoom === false && scope.enablePan === false ) return; + + handleTouchStartDollyPan( event ); + + state = STATE.TOUCH_DOLLY_PAN; + + break; + + case TOUCH.DOLLY_ROTATE: + + if ( scope.enableZoom === false && scope.enableRotate === false ) return; + + handleTouchStartDollyRotate( event ); + + state = STATE.TOUCH_DOLLY_ROTATE; + + break; + + default: + + state = STATE.NONE; + + } + + break; + + default: + + state = STATE.NONE; + + } + + if ( state !== STATE.NONE ) { + + scope.dispatchEvent( startEvent ); + + } + + } + + function onTouchMove( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + switch ( state ) { + + case STATE.TOUCH_ROTATE: + + if ( scope.enableRotate === false ) return; + + handleTouchMoveRotate( event ); + + scope.update(); + + break; + + case STATE.TOUCH_PAN: + + if ( scope.enablePan === false ) return; + + handleTouchMovePan( event ); + + scope.update(); + + break; + + case STATE.TOUCH_DOLLY_PAN: + + if ( scope.enableZoom === false && scope.enablePan === false ) return; + + handleTouchMoveDollyPan( event ); + + scope.update(); + + break; + + case STATE.TOUCH_DOLLY_ROTATE: + + if ( scope.enableZoom === false && scope.enableRotate === false ) return; + + handleTouchMoveDollyRotate( event ); + + scope.update(); + + break; + + default: + + state = STATE.NONE; + + } + + } + + function onTouchEnd( event ) { + + if ( scope.enabled === false ) return; + + handleTouchEnd( event ); + + scope.dispatchEvent( endEvent ); + + state = STATE.NONE; + + } + + function onContextMenu( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + } + + // + + scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); + + scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); + scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); + + scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); + scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); + scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); + + scope.domElement.addEventListener( 'keydown', onKeyDown, false ); + + // make sure element can receive keys. + + if ( scope.domElement.tabIndex === - 1 ) { + + scope.domElement.tabIndex = 0; + + } + + // force an update at start + + this.update(); + +}; + +OrbitControls.prototype = Object.create( EventDispatcher.prototype ); +OrbitControls.prototype.constructor = OrbitControls; + + +// This set of controls performs orbiting, dollying (zooming), and panning. +// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). +// This is very similar to OrbitControls, another set of touch behavior +// +// Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate +// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish +// Pan - left mouse, or arrow keys / touch: one-finger move + +var MapControls = function ( object, domElement ) { + + OrbitControls.call( this, object, domElement ); + + this.mouseButtons.LEFT = MOUSE.PAN; + this.mouseButtons.RIGHT = MOUSE.ROTATE; + + this.touches.ONE = TOUCH.PAN; + this.touches.TWO = TOUCH.DOLLY_ROTATE; + +}; + +MapControls.prototype = Object.create( EventDispatcher.prototype ); +MapControls.prototype.constructor = MapControls; + +export { OrbitControls, MapControls }; diff --git a/src/controls/VoxelPainterControls.js b/src/controls/VoxelPainterControls.js new file mode 100644 index 00000000..492bbfd5 --- /dev/null +++ b/src/controls/VoxelPainterControls.js @@ -0,0 +1,1248 @@ +/** + * @author qiao / https://github.com/qiao + * @author mrdoob / http://mrdoob.com + * @author alteredq / http://alteredqualia.com/ + * @author WestLangley / http://github.com/WestLangley + * @author erich666 / http://erichaines.com + * @author ScieCode / http://github.com/sciecode + */ + +import { + EventDispatcher, + MOUSE, + Quaternion, + Spherical, + TOUCH, + Vector2, + Vector3, +} from "three"; + +// This set of controls performs orbiting, dollying (zooming), and panning. +// Unlike 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 + +var VoxelPainterControls = function ( object, domElement ) { + + if ( domElement === undefined ) console.warn( 'THREE.VoxelPainterControls: The second parameter "domElement" is now mandatory.' ); + if ( domElement === document ) console.error( 'THREE.VoxelPainterControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); + + this.object = object; + this.domElement = domElement; + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the object orbits around + this.target = new Vector3(); + + // How far you can dolly in and out ( PerspectiveCamera only ) + this.minDistance = 0; + this.maxDistance = Infinity; + + // How far you can zoom in and out ( OrthographicCamera only ) + this.minZoom = 0; + this.maxZoom = Infinity; + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = - Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + this.enableDamping = false; + this.dampingFactor = 0.05; + + // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + this.enableZoom = true; + this.zoomSpeed = 1.0; + + // Set to false to disable rotating + this.enableRotate = true; + this.rotateSpeed = 1.0; + + // Set to false to disable panning + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; // if true, pan in screen-space + this.keyPanSpeed = 20.0; // pixels moved per arrow key push + + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 + + // Mouse buttons + this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; + + // Touch fingers + this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + + // + // public methods + // + + this.getPolarAngle = function () { + + return spherical.phi; + + }; + + this.getAzimuthalAngle = function () { + + return spherical.theta; + + }; + + this.saveState = function () { + + scope.target0.copy( scope.target ); + scope.position0.copy( scope.object.position ); + scope.zoom0 = scope.object.zoom; + + }; + + this.reset = function () { + + scope.target.copy( scope.target0 ); + scope.object.position.copy( scope.position0 ); + scope.object.zoom = scope.zoom0; + + scope.object.updateProjectionMatrix(); + scope.dispatchEvent( changeEvent ); + + scope.update(); + + state = STATE.NONE; + + }; + + // this method is exposed, but perhaps it would be better if we can make it private... + this.update = function () { + + var offset = new Vector3(); + + // so camera.up is the orbit axis + var quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); + var quatInverse = quat.clone().inverse(); + + var lastPosition = new Vector3(); + var lastQuaternion = new Quaternion(); + + var direction = new Vector3(); + var velocity = new Vector3(); + var prevTime = Date.now(); + var logTime = Date.now(); + var vec = new Vector3(); + + return function update() { + var time = Date.now(); + + var log = false; + if (time - logTime > 2000) { + logTime = time; + log = true; + } + + var delta = ( time - prevTime ) / 1000.0; + velocity.x -= velocity.x * 50.0 * delta; + velocity.y -= velocity.y * 50.0 * delta; + velocity.z -= velocity.z * 50.0 * delta; + if (velocity.length() < 1) { + velocity.set(0, 0, 0); + } + + direction.x = Number( moveRight ) - Number( moveLeft ); + direction.y = Number( moveUp ) - Number( moveDown ); + direction.z = Number( moveForward ) - Number( moveBackward ); + direction.normalize(); + + if ( moveLeft || moveRight ) velocity.x -= direction.x * 5000.0 * delta; + if ( moveUp || moveDown ) velocity.y -= direction.y * 5000.0 * delta; + if ( moveForward || moveBackward ) velocity.z -= direction.z * 2500.0 * delta; + + // controls.moveRight( -velocity.x * delta); + // controls.moveUp( -velocity.y * delta); + // forward + if (log) { + console.log(vec.x, vec.y, vec.z, delta); + } + vec.setFromMatrixColumn( scope.object.matrix, 0 ); + vec.crossVectors( scope.object.up, vec); + vec.multiplyScalar( -velocity.z * delta ); + vec.y += -velocity.y * delta; + panOffset.add( vec ); + vec.setFromMatrixColumn( scope.object.matrix, 0 ); + vec.multiplyScalar( -velocity.x * delta ); + panOffset.add( vec ); + + prevTime = time; + + + var position = scope.object.position; + + offset.copy( position ).sub( scope.target ); + + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion( quat ); + + // angle from z-axis around y-axis + spherical.setFromVector3( offset ); + + if ( scope.autoRotate && state === STATE.NONE ) { + + rotateLeft( getAutoRotationAngle() ); + + } + + if ( scope.enableDamping ) { + + spherical.theta += sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; + + } else { + + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + + } + + // restrict theta to be between desired limits + spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); + + // restrict phi to be between desired limits + spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); + + spherical.makeSafe(); + + + spherical.radius *= scale; + + // restrict radius to be between desired limits + spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); + + // move target to panned location + + if (panOffset.length() > 10000) { + panOffset.set(0, 0, 0); + } + if ( scope.enableDamping === true ) { + scope.target.addScaledVector( panOffset, scope.dampingFactor ); + } else { + scope.target.add( panOffset ); + } + if (scope.target.y < 10.0) { + scope.target.y = 10.0; + } + + + offset.setFromSpherical( spherical ); + + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion( quatInverse ); + + position.copy( scope.target ).add( offset ); + + scope.object.lookAt( scope.target ); + + if ( scope.enableDamping === true ) { + + sphericalDelta.theta *= ( 1 - scope.dampingFactor ); + sphericalDelta.phi *= ( 1 - scope.dampingFactor ); + + panOffset.multiplyScalar( 1 - scope.dampingFactor ); + + } else { + + sphericalDelta.set( 0, 0, 0 ); + + panOffset.set( 0, 0, 0 ); + + } + + scale = 1; + + // 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 || + lastPosition.distanceToSquared( scope.object.position ) > EPS || + 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { + + scope.dispatchEvent( changeEvent ); + + lastPosition.copy( scope.object.position ); + lastQuaternion.copy( scope.object.quaternion ); + zoomChanged = false; + + return true; + + } + + return false; + + }; + + }(); + + this.dispose = function () { + + scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); + scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); + scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); + + scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); + scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); + scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); + + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + document.removeEventListener( 'keydown', onDocumentKeyDown, false ); + document.removeEventListener( 'keyup', onDocumentKeyUp, false ); + + //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? + + }; + + var vec = new Vector3(); + + this.moveForward = ( distance ) => { + console.log("forward"); + return; + vec.setFromMatrixColumn( scope.object.matrix, 0 ); + vec.crossVectors( scope.object.up, vec); + scope.object.position.addScaledVector( vec, distance ); + }; + this.moveRight = ( distance ) => { + console.log("down"); + return; + vec.setFromMatrixColumn( scope.object.matrix, 0 ); + scope.object.position.addScaledVector( vec, distance ); + }; + this.moveUp = ( distance ) => { + console.log("up"); + return; + scope.object.position.y += distance; + }; + + // + // internals + // + + var scope = this; + + var changeEvent = { type: 'change' }; + var startEvent = { type: 'start' }; + var endEvent = { type: 'end' }; + + var STATE = { + NONE: - 1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6 + }; + + var state = STATE.NONE; + + var EPS = 0.000001; + + // current position in spherical coordinates + var spherical = new Spherical(); + var sphericalDelta = new Spherical(); + + var moveLeft = false; + var moveRight = false; + var moveForward = false; + var moveBackward = false; + var moveUp = false; + var moveDown = false; + + var scale = 1; + var panOffset = new Vector3(); + var zoomChanged = false; + + var rotateStart = new Vector2(); + var rotateEnd = new Vector2(); + var rotateDelta = new Vector2(); + + var panStart = new Vector2(); + var panEnd = new Vector2(); + var panDelta = new Vector2(); + + var dollyStart = new Vector2(); + var dollyEnd = new Vector2(); + var dollyDelta = new Vector2(); + + function getAutoRotationAngle() { + + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + + } + + function getZoomScale() { + + return Math.pow( 0.95, scope.zoomSpeed ); + + } + + function rotateLeft( angle ) { + + sphericalDelta.theta -= angle; + + } + + function rotateUp( angle ) { + + sphericalDelta.phi -= angle; + + } + + var panLeft = function () { + + var v = new Vector3(); + + return function panLeft( distance, objectMatrix ) { + + v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix + v.multiplyScalar( - distance ); + + panOffset.add( v ); + + }; + + }(); + + var panUp = function () { + + var v = new Vector3(); + + return function panUp( distance, objectMatrix ) { + + if ( scope.screenSpacePanning === true ) { + v.setFromMatrixColumn( objectMatrix, 1 ); + } else { + v.setFromMatrixColumn( objectMatrix, 0 ); + v.crossVectors( scope.object.up, v ); + } + + v.multiplyScalar( distance ); + + panOffset.add( v ); + }; + + }(); + + // deltaX and deltaY are in pixels; right and down are positive + var pan = function () { + + var offset = new Vector3(); + + return function pan( deltaX, deltaY ) { + + var element = scope.domElement; + + if ( scope.object.isPerspectiveCamera ) { + + // perspective + var position = scope.object.position; + offset.copy( position ).sub( scope.target ); + var targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); + + // we use only clientHeight here so aspect ratio does not distort speed + panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); + panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); + + } else if ( scope.object.isOrthographicCamera ) { + + // orthographic + panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); + panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); + + } else { + + // camera neither orthographic nor perspective + console.warn( 'WARNING: VoxelPainterControls.js encountered an unknown camera type - pan disabled.' ); + scope.enablePan = false; + + } + + }; + + }(); + + function dollyIn( dollyScale ) { + + if ( scope.object.isPerspectiveCamera ) { + + scale /= dollyScale; + + } else if ( scope.object.isOrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: VoxelPainterControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + function dollyOut( dollyScale ) { + + if ( scope.object.isPerspectiveCamera ) { + + scale *= dollyScale; + + } else if ( scope.object.isOrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: VoxelPainterControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + // + // event callbacks - update the object state + // + + function handleMouseDownRotate( event ) { + + rotateStart.set( event.clientX, event.clientY ); + + } + + function handleMouseDownDolly( event ) { + + dollyStart.set( event.clientX, event.clientY ); + + } + + function handleMouseDownPan( event ) { + + panStart.set( event.clientX, event.clientY ); + + } + + function handleMouseMoveRotate( event ) { + + rotateEnd.set( event.clientX, event.clientY ); + + rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); + + var element = scope.domElement; + + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height + + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); + + rotateStart.copy( rotateEnd ); + + scope.update(); + + } + + function handleMouseMoveDolly( event ) { + + dollyEnd.set( event.clientX, event.clientY ); + + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + dollyIn( getZoomScale() ); + + } else if ( dollyDelta.y < 0 ) { + + dollyOut( getZoomScale() ); + + } + + dollyStart.copy( dollyEnd ); + + scope.update(); + + } + + function handleMouseMovePan( event ) { + + panEnd.set( event.clientX, event.clientY ); + + panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); + + pan( panDelta.x, panDelta.y ); + + panStart.copy( panEnd ); + + scope.update(); + + } + + function handleMouseUp( /*event*/ ) { + + // no-op + + } + + function handleMouseWheel( event ) { + + if ( event.deltaY < 0 ) { + + dollyOut( getZoomScale() ); + + } else if ( event.deltaY > 0 ) { + + dollyIn( getZoomScale() ); + + } + + scope.update(); + + } + + function handleTouchStartRotate( event ) { + + if ( event.touches.length == 1 ) { + + rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + + rotateStart.set( x, y ); + + } + + } + + function handleTouchStartPan( event ) { + + if ( event.touches.length == 1 ) { + + panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + + panStart.set( x, y ); + + } + + } + + function handleTouchStartDolly( event ) { + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + + var distance = Math.sqrt( dx * dx + dy * dy ); + + dollyStart.set( 0, distance ); + + } + + function handleTouchStartDollyPan( event ) { + + if ( scope.enableZoom ) handleTouchStartDolly( event ); + + if ( scope.enablePan ) handleTouchStartPan( event ); + + } + + function handleTouchStartDollyRotate( event ) { + + if ( scope.enableZoom ) handleTouchStartDolly( event ); + + if ( scope.enableRotate ) handleTouchStartRotate( event ); + + } + + function handleTouchMoveRotate( event ) { + + if ( event.touches.length == 1 ) { + + rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + + rotateEnd.set( x, y ); + + } + + rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); + + var element = scope.domElement; + + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height + + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); + + rotateStart.copy( rotateEnd ); + + } + + function handleTouchMovePan( event ) { + + if ( event.touches.length == 1 ) { + + panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + + panEnd.set( x, y ); + + } + + panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); + + pan( panDelta.x, panDelta.y ); + + panStart.copy( panEnd ); + + } + + function handleTouchMoveDolly( event ) { + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + + var distance = Math.sqrt( dx * dx + dy * dy ); + + dollyEnd.set( 0, distance ); + + dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); + + dollyIn( dollyDelta.y ); + + dollyStart.copy( dollyEnd ); + + } + + function handleTouchMoveDollyPan( event ) { + + if ( scope.enableZoom ) handleTouchMoveDolly( event ); + + if ( scope.enablePan ) handleTouchMovePan( event ); + + } + + function handleTouchMoveDollyRotate( event ) { + + if ( scope.enableZoom ) handleTouchMoveDolly( event ); + + if ( scope.enableRotate ) handleTouchMoveRotate( event ); + + } + + function handleTouchEnd( /*event*/ ) { + + // no-op + + } + + // + // event handlers - FSM: listen for events and reset state + // + + function onMouseDown( event ) { + + if ( scope.enabled === false ) return; + + // Prevent the browser from scrolling. + event.preventDefault(); + + // Manually set the focus since calling preventDefault above + // prevents the browser from setting it automatically. + + scope.domElement.focus ? scope.domElement.focus() : window.focus(); + + var mouseAction; + + switch ( event.button ) { + + case 0: + + mouseAction = scope.mouseButtons.LEFT; + break; + + case 1: + + mouseAction = scope.mouseButtons.MIDDLE; + break; + + case 2: + + mouseAction = scope.mouseButtons.RIGHT; + break; + + default: + + mouseAction = - 1; + + } + + switch ( mouseAction ) { + + case MOUSE.DOLLY: + + if ( scope.enableZoom === false ) return; + + handleMouseDownDolly( event ); + + state = STATE.DOLLY; + + break; + + case MOUSE.ROTATE: + + if ( event.ctrlKey || event.metaKey ) { + + if ( scope.enablePan === false ) return; + + handleMouseDownPan( event ); + + state = STATE.PAN; + + } else { + + if ( scope.enableRotate === false ) return; + + handleMouseDownRotate( event ); + + state = STATE.ROTATE; + + } + + break; + + case MOUSE.PAN: + + if ( event.ctrlKey || event.metaKey ) { + + if ( scope.enableRotate === false ) return; + + handleMouseDownRotate( event ); + + state = STATE.ROTATE; + + } else { + + if ( scope.enablePan === false ) return; + + handleMouseDownPan( event ); + + state = STATE.PAN; + + } + + break; + + default: + + state = STATE.NONE; + + } + + if ( state !== STATE.NONE ) { + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + + scope.dispatchEvent( startEvent ); + + } + + } + + function onDocumentKeyDown( event ) { + switch ( event.keyCode ) { + case 38: // up + case 87: // w + moveForward = true; + break; + + case 37: // left + case 65: // a + moveLeft = true; + break; + + case 40: // down + case 83: // s + moveBackward = true; + break; + + case 39: // right + case 68: // d + moveRight = true; + break; + + case 32: // space + moveUp = true; + break; + + case 16: //shift + moveDown = true; + break; + } + } + + function onDocumentKeyUp( event ) { + switch ( event.keyCode ) { + case 38: // up + case 87: // w + moveForward = false; + break; + + case 37: // left + case 65: // a + moveLeft = false; + break; + + case 40: // down + case 83: // s + moveBackward = false; + break; + + case 39: // right + case 68: // d + moveRight = false; + break; + + case 32: // space + moveUp = false; + break; + + case 16: //shift + moveDown = false; + break; + } + } + + + function onMouseMove( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + switch ( state ) { + + case STATE.ROTATE: + + if ( scope.enableRotate === false ) return; + + handleMouseMoveRotate( event ); + + break; + + case STATE.DOLLY: + + if ( scope.enableZoom === false ) return; + + handleMouseMoveDolly( event ); + + break; + + case STATE.PAN: + + if ( scope.enablePan === false ) return; + + handleMouseMovePan( event ); + + break; + + } + + } + + function onMouseUp( event ) { + + if ( scope.enabled === false ) return; + + handleMouseUp( event ); + + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + + scope.dispatchEvent( endEvent ); + + state = STATE.NONE; + + } + + function onMouseWheel( event ) { + + if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; + + event.preventDefault(); + event.stopPropagation(); + + scope.dispatchEvent( startEvent ); + + handleMouseWheel( event ); + + scope.dispatchEvent( endEvent ); + + } + + function onTouchStart( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + switch ( event.touches.length ) { + + case 1: + + switch ( scope.touches.ONE ) { + + case TOUCH.ROTATE: + + if ( scope.enableRotate === false ) return; + + handleTouchStartRotate( event ); + + state = STATE.TOUCH_ROTATE; + + break; + + case TOUCH.PAN: + + if ( scope.enablePan === false ) return; + + handleTouchStartPan( event ); + + state = STATE.TOUCH_PAN; + + break; + + default: + + state = STATE.NONE; + + } + + break; + + case 2: + + switch ( scope.touches.TWO ) { + + case TOUCH.DOLLY_PAN: + + if ( scope.enableZoom === false && scope.enablePan === false ) return; + + handleTouchStartDollyPan( event ); + + state = STATE.TOUCH_DOLLY_PAN; + + break; + + case TOUCH.DOLLY_ROTATE: + + if ( scope.enableZoom === false && scope.enableRotate === false ) return; + + handleTouchStartDollyRotate( event ); + + state = STATE.TOUCH_DOLLY_ROTATE; + + break; + + default: + + state = STATE.NONE; + + } + + break; + + default: + + state = STATE.NONE; + + } + + if ( state !== STATE.NONE ) { + + scope.dispatchEvent( startEvent ); + + } + + } + + function onTouchMove( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + switch ( state ) { + + case STATE.TOUCH_ROTATE: + + if ( scope.enableRotate === false ) return; + + handleTouchMoveRotate( event ); + + scope.update(); + + break; + + case STATE.TOUCH_PAN: + + if ( scope.enablePan === false ) return; + + handleTouchMovePan( event ); + + scope.update(); + + break; + + case STATE.TOUCH_DOLLY_PAN: + + if ( scope.enableZoom === false && scope.enablePan === false ) return; + + handleTouchMoveDollyPan( event ); + + scope.update(); + + break; + + case STATE.TOUCH_DOLLY_ROTATE: + + if ( scope.enableZoom === false && scope.enableRotate === false ) return; + + handleTouchMoveDollyRotate( event ); + + scope.update(); + + break; + + default: + + state = STATE.NONE; + + } + + } + + function onTouchEnd( event ) { + + if ( scope.enabled === false ) return; + + handleTouchEnd( event ); + + scope.dispatchEvent( endEvent ); + + state = STATE.NONE; + + } + + function onContextMenu( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + } + + // + + scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); + + scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); + scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); + + scope.domElement.addEventListener( 'keydown', onDocumentKeyDown, false ); + scope.domElement.addEventListener( 'keyup', onDocumentKeyUp, false ); + + scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); + scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); + scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); + + // make sure element can receive keys. + + if ( scope.domElement.tabIndex === - 1 ) { + + scope.domElement.tabIndex = 0; + + } + + // force an update at start + + this.update(); + +}; + +VoxelPainterControls.prototype = Object.create( EventDispatcher.prototype ); +VoxelPainterControls.prototype.constructor = VoxelPainterControls; + + +// This set of controls performs orbiting, dollying (zooming), and panning. +// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). +// This is very similar to VoxelPainterControls, another set of touch behavior +// +// Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate +// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish +// Pan - left mouse, or arrow keys / touch: one-finger move + +var MapControls = function ( object, domElement ) { + + VoxelPainterControls.call( this, object, domElement ); + + this.mouseButtons.LEFT = MOUSE.PAN; + this.mouseButtons.RIGHT = MOUSE.ROTATE; + + this.touches.ONE = TOUCH.PAN; + this.touches.TWO = TOUCH.DOLLY_ROTATE; + +}; + +MapControls.prototype = Object.create( EventDispatcher.prototype ); +MapControls.prototype.constructor = MapControls; + +export { VoxelPainterControls, MapControls }; diff --git a/src/voxel.js b/src/voxel.js new file mode 100644 index 00000000..8e9e8f34 --- /dev/null +++ b/src/voxel.js @@ -0,0 +1,199 @@ +import * as THREE from 'three'; +import { VoxelPainterControls } from './controls/VoxelPainterControls'; +// import { OrbitControls } from './controls/OrbitControls'; + +var camera, scene, renderer; +var plane; +var mouse, raycaster; + +var rollOverMesh, rollOverMaterial; +var cubeGeo, cubeMaterial; + +var controls; + +var objects = []; + +document.addEventListener('DOMContentLoaded', () => { + init(); + render(); +}); + +function init() { + + camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 ); + camera.position.set( 100, 160, 260 ); + camera.lookAt( 0, 0, 0 ); + + scene = new THREE.Scene(); + scene.background = new THREE.Color( 0xf0f0f0 ); + + // roll-over helpers + + var rollOverGeo = new THREE.BoxBufferGeometry( 10, 10, 10 ); + rollOverMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, opacity: 0.5, transparent: true } ); + rollOverMesh = new THREE.Mesh( rollOverGeo, rollOverMaterial ); + scene.add( rollOverMesh ); + + // cubes + cubeGeo = new THREE.BoxBufferGeometry( 10, 10, 10 ); + cubeMaterial = new THREE.MeshLambertMaterial( { color: 0xfeb74c } ); + + // grid + var gridHelper = new THREE.GridHelper( 1000, 100, 0x555555, 0x555555 ); + scene.add( gridHelper ); + + // + raycaster = new THREE.Raycaster(); + mouse = new THREE.Vector2(); + + + // Floor with random cold color triangles + var floorGeometry = new THREE.PlaneBufferGeometry( 1000, 1000, 100, 100 ); + floorGeometry.rotateX( - Math.PI / 2 ); + // vertex displacement + var vertex = new THREE.Vector3(); + var color = new THREE.Color(); + var position = floorGeometry.attributes.position; + for ( var i = 0, l = position.count; i < l; i ++ ) { + vertex.fromBufferAttribute( position, i ); + vertex.x += Math.random() * 20 - 10; + vertex.y += Math.random() * 2; + vertex.z += Math.random() * 20 - 10; + position.setXYZ( i, vertex.x, vertex.y, vertex.z ); + } + floorGeometry = floorGeometry.toNonIndexed(); // ensure each face has unique vertices + position = floorGeometry.attributes.position; + var colors = []; + for ( var i = 0, l = position.count; i < l; i ++ ) { + color.setHSL( Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75 ); + colors.push( color.r, color.g, color.b ); + } + floorGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) ); + var floorMaterial = new THREE.MeshBasicMaterial( { vertexColors: THREE.VertexColors } ); + plane = new THREE.Mesh( floorGeometry, floorMaterial ); + scene.add( plane ); + objects.push( plane ); + + /* + // Plane Floor + var geometry = new THREE.PlaneBufferGeometry( 5000, 5000 ); + geometry.rotateX( - Math.PI / 2 ); + plane = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial({ color: 0x009900 }) ); + scene.add( plane ); + objects.push( plane ); + */ + + + // lights + + var ambientLight = new THREE.AmbientLight( 0x606060 ); + scene.add( ambientLight ); + + var directionalLight = new THREE.DirectionalLight( 0xffffff ); + directionalLight.position.set( 1, 0.75, 0.5 ).normalize(); + scene.add( directionalLight ); + + renderer = new THREE.WebGLRenderer( { antialias: true } ); + renderer.setPixelRatio( window.devicePixelRatio ); + renderer.setSize( window.innerWidth, window.innerHeight ); + document.body.appendChild( renderer.domElement ); + + controls = new VoxelPainterControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.75; + controls.maxPolarAngle = Math.PI / 2; + controls.minDistance = 100.00; + controls.maxDistance = 1000.00; + /*controls.rotateSpeed = 1.5; + controls.zoomSpeed = 10.0; + controls.panSpeed = 0.3; + controls.keys = [65, 83, 68]; // ASD + controls.dynamicDampingFactor = 0.2; + */ + + renderer.domElement.addEventListener( 'mousemove', onDocumentMouseMove, false ); + renderer.domElement.addEventListener( 'mousedown', onDocumentMouseDown, false ); + renderer.domElement.addEventListener( 'mouseup', onDocumentMouseUp, false ); + //document.addEventListener( 'keydown', onDocumentKeyDown, false ); + //document.addEventListener( 'keyup', onDocumentKeyUp, false ); + // + window.addEventListener( 'resize', onWindowResize, false ); + +} + +function render() { + controls.update(); + renderer.render( scene, camera ); + requestAnimationFrame(render); +} + +function onWindowResize() { + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize( window.innerWidth, window.innerHeight ); + +} + +function onDocumentMouseMove( event ) { + + event.preventDefault(); + + mouse.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 ); + + raycaster.setFromCamera( mouse, camera ); + + var intersects = raycaster.intersectObjects( objects ); + + if ( intersects.length > 0 ) { + + var intersect = intersects[ 0 ]; + + rollOverMesh.position.copy( intersect.point ).add( intersect.face.normal ); + rollOverMesh.position.divideScalar( 10 ).floor().multiplyScalar( 10 ).addScalar( 5 ); + + } +} + +var pressTime = 0; +function onDocumentMouseDown( event ) { + pressTime = Date.now(); +} + +function onDocumentMouseUp( event ) { + if (Date.now() - pressTime > 600) { + return; + } + + event.preventDefault(); + mouse.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 ); + + raycaster.setFromCamera( mouse, camera ); + + var intersects = raycaster.intersectObjects( objects ); + + if ( intersects.length > 0 ) { + var intersect = intersects[ 0 ]; + + switch ( event.button ) { + case 0: + // left mouse button + console.log("set voxel"); + var voxel = new THREE.Mesh( cubeGeo, cubeMaterial ); + voxel.position.copy( intersect.point ).add( intersect.face.normal ); + voxel.position.divideScalar( 10 ).floor().multiplyScalar( 10 ).addScalar( 5 ); + scene.add( voxel ); + + objects.push( voxel ); + break; + case 2: + // right mouse button + console.log("remove voxel"); + if ( intersect.object !== plane ) { + scene.remove( intersect.object ); + objects.splice( objects.indexOf( intersect.object ), 1 ); + } + break; + } + } +} diff --git a/tools/webpack.config.js b/tools/webpack.config.js index 20fdb599..84dc09a4 100644 --- a/tools/webpack.config.js +++ b/tools/webpack.config.js @@ -21,16 +21,16 @@ const basePackageValues = { start: 'node --nouse-idle-notification --expose-gc web.js', }, dependencies: { - "mysql2": "^2.1.0", - } -} + mysql2: '^2.1.0', + }, +}; const versionsPackageFilename = path.resolve(__dirname, '../package.json'); const config = { context: path.resolve(__dirname, '..'), - mode: (isDebug) ? "development" : "production", + mode: (isDebug) ? 'development' : 'production', output: { path: path.resolve(__dirname, '../build/public/assets'), @@ -39,13 +39,13 @@ const config = { }, resolve: { - extensions: ['.js', '.jsx', '.json'], + extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], }, module: { rules: [ { - test: /\.(js|jsx)$/, + test: /\.(js|jsx|ts|tsx)$/, loader: 'babel-loader', include: [ path.resolve(__dirname, '../src'), @@ -70,18 +70,20 @@ const config = { }, debug: false, }], + "@babel/typescript", // JSX, Flow // https://github.com/babel/babel/tree/master/packages/babel-preset-react '@babel/react', ], plugins: [ '@babel/transform-flow-strip-types', - ["@babel/plugin-proposal-decorators", { "legacy": true }], - "@babel/plugin-proposal-function-sent", - "@babel/plugin-proposal-export-namespace-from", - "@babel/plugin-proposal-numeric-separator", - "@babel/plugin-proposal-throw-expressions", - ["@babel/plugin-proposal-class-properties", { "loose": true }], + ['@babel/plugin-proposal-decorators', { legacy: true }], + '@babel/plugin-proposal-function-sent', + '@babel/plugin-proposal-export-namespace-from', + '@babel/plugin-proposal-numeric-separator', + '@babel/plugin-proposal-throw-expressions', + ['@babel/plugin-proposal-class-properties', { loose: true }], + '@babel/proposal-object-rest-spread', // Adds component stack to warning messages // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source ...isDebug ? ['@babel/transform-react-jsx-source'] : [], @@ -89,10 +91,10 @@ const config = { // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self ...isDebug ? ['@babel/transform-react-jsx-self'] : [], // react-optimize - "@babel/transform-react-constant-elements", - "@babel/transform-react-inline-elements", - "transform-react-remove-prop-types", - "transform-react-pure-class-to-function", + '@babel/transform-react-constant-elements', + '@babel/transform-react-inline-elements', + 'transform-react-remove-prop-types', + 'transform-react-pure-class-to-function', ], }, }, @@ -100,14 +102,14 @@ const config = { test: /\.svg$/, use: [ { - loader: "babel-loader" + loader: 'babel-loader', }, { - loader: "react-svg-loader", + loader: 'react-svg-loader', options: { svgo: { plugins: [ - { + { removeViewBox: false, }, { @@ -115,10 +117,10 @@ const config = { }, ], }, - jsx: false // true outputs JSX tags - } - } - ] + jsx: false, // true outputs JSX tags + }, + }, + ], }, { test: /\.html/, @@ -126,10 +128,10 @@ const config = { { loader: 'html-loader', options: { - attrs: [':data-src'] + attrs: [':data-src'], }, }, - ], + ], }, { test: /\.tcss/, @@ -148,7 +150,7 @@ const config = { }, { test: /\.scss/, - use: [ + use: [ 'style-loader', { loader: 'css-loader', @@ -165,7 +167,7 @@ const config = { }, { test: /\.css/, - use: [ 'style-loader', + use: ['style-loader', { loader: 'css-loader', options: { @@ -232,6 +234,7 @@ const clientConfig = { entry: { client: ['./src/client.js'], globe: ['./src/globe.js'], + voxel: ['./src/voxel.js'], }, output: { @@ -271,18 +274,18 @@ const clientConfig = { vendors: false, vendor: { - name: "vendor", - chunks: chunk => chunk.name === "client", + name: 'vendor', + chunks: (chunk) => chunk.name === 'client', test: /node_modules/, - } - } - } + }, + }, + }, }, - // Some libraries import Node modules but don't use them in the browser. - // Tell Webpack to provide empty mocks for them so importing them works. - // https://webpack.github.io/docs/configuration.html#node - // https://github.com/webpack/node-libs-browser/tree/master/mock + // Some libraries import Node modules but don't use them in the browser. + // Tell Webpack to provide empty mocks for them so importing them works. + // https://webpack.github.io/docs/configuration.html#node + // https://github.com/webpack/node-libs-browser/tree/master/mock node: { fs: 'empty', net: 'empty', @@ -291,6 +294,20 @@ const clientConfig = { }; +const voxelConfig = { + ...clientConfig, + entry: { + voxel: ['./src/voxel.js'], + }, + output: { + ...config.output, + filename: '[name].js', + }, + optimization: { + }, +}; + + const webConfig = { ...config, @@ -313,11 +330,11 @@ const webConfig = { ...config.module, // Override babel-preset-env configuration for Node.js - rules: config.module.rules.map(rule => (rule.loader !== 'babel-loader' ? rule : { + rules: config.module.rules.map((rule) => (rule.loader !== 'babel-loader' ? rule : { ...rule, query: { ...rule.query, - presets: rule.query.presets.map(preset => (preset[0] !== '@babel/preset-env' ? preset : ['@babel/preset-env', { + presets: rule.query.presets.map((preset) => (preset[0] !== '@babel/preset-env' ? preset : ['@babel/preset-env', { targets: { node: pkg.engines.node.replace(/^\D+/g, ''), }, @@ -333,9 +350,8 @@ const webConfig = { externals: [ /^\.\/assets\.json$/, (context, request, callback) => { - const isExternal = - request.match(/^[@a-z][a-z/.\-0-9]*$/i) && - !request.match(/\.(css|less|scss|sss)$/i); + const isExternal = request.match(/^[@a-z][a-z/.\-0-9]*$/i) + && !request.match(/\.(css|less|scss|sss)$/i); callback(null, Boolean(isExternal)); }, ], @@ -363,4 +379,5 @@ const webConfig = { }; -export default [clientConfig, webConfig]; +// export default [clientConfig, webConfig]; +export default [clientConfig, voxelConfig, webConfig];