From: Lanius Trolling Date: Mon, 4 Dec 2023 00:19:50 +0000 (-0500) Subject: Remove crossorigin attributes X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=6d863135552a92072c7db9bb0b47cc0ab109947c;p=factbooks Remove crossorigin attributes --- diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b4..ae3f30a 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 2e9b997..1b9246d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/build.gradle.kts b/build.gradle.kts index dd2ac1a..7b26e4e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,8 +24,8 @@ buildscript { plugins { java - kotlin("jvm") version "1.9.10" - kotlin("plugin.serialization") version "1.9.10" + kotlin("jvm") version "1.9.21" + kotlin("plugin.serialization") version "1.9.21" id("com.github.johnrengelman.shadow") version "7.1.2" application } diff --git a/src/main/kotlin/info/mechyrdia/lore/view_tpl.kt b/src/main/kotlin/info/mechyrdia/lore/view_tpl.kt index 29d1a4d..e79b50a 100644 --- a/src/main/kotlin/info/mechyrdia/lore/view_tpl.kt +++ b/src/main/kotlin/info/mechyrdia/lore/view_tpl.kt @@ -58,7 +58,6 @@ fun ApplicationCall.page(pageTitle: String, navBar: List? = null, sideb script(src = "/static/obj-viewer/three.js") {} script(src = "/static/obj-viewer/three-examples.js") {} - script(src = "/static/obj-viewer/OrbitControls.js") {} link(rel = "icon", type = "image/svg+xml", href = "/static/images/icon.png") @@ -69,7 +68,6 @@ fun ApplicationCall.page(pageTitle: String, navBar: List? = null, sideb type = "font/woff" ) { attributes["as"] = "font" - attributes["crossorigin"] = "anonymous" } for (image in images) @@ -79,7 +77,6 @@ fun ApplicationCall.page(pageTitle: String, navBar: List? = null, sideb type = "image/png" ) { attributes["as"] = "image" - attributes["crossorigin"] = "anonymous" } link(rel = "stylesheet", href = "/static/style.css") diff --git a/src/main/resources/static/obj-viewer/OrbitControls.js b/src/main/resources/static/obj-viewer/OrbitControls.js deleted file mode 100644 index 151f0c8..0000000 --- a/src/main/resources/static/obj-viewer/OrbitControls.js +++ /dev/null @@ -1,1042 +0,0 @@ -/** - * @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 - */ - -// 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 arrow keys / touch: three finger swipe - -THREE.OrbitControls = function ( object, domElement ) { - - this.object = object; - - this.domElement = ( domElement !== undefined ) ? domElement : document; - - // Set to false to disable this control - this.enabled = true; - - // "target" sets the location of focus, where the object orbits around - this.target = new THREE.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.25; - - // 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.keyPanSpeed = 7.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: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; - - // Mouse buttons - this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; - - // 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 THREE.Vector3(); - - // so camera.up is the orbit axis - var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); - var quatInverse = quat.clone().inverse(); - - var lastPosition = new THREE.Vector3(); - var lastQuaternion = new THREE.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() ); - - } - - 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 - 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 ); - - } else { - - sphericalDelta.set( 0, 0, 0 ); - - } - - scale = 1; - panOffset.set( 0, 0, 0 ); - - // 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 ); - - window.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_DOLLY: 4, TOUCH_PAN: 5 }; - - var state = STATE.NONE; - - var EPS = 0.000001; - - // current position in spherical coordinates - var spherical = new THREE.Spherical(); - var sphericalDelta = new THREE.Spherical(); - - var scale = 1; - var panOffset = new THREE.Vector3(); - var zoomChanged = false; - - var rotateStart = new THREE.Vector2(); - var rotateEnd = new THREE.Vector2(); - var rotateDelta = new THREE.Vector2(); - - var panStart = new THREE.Vector2(); - var panEnd = new THREE.Vector2(); - var panDelta = new THREE.Vector2(); - - var dollyStart = new THREE.Vector2(); - var dollyEnd = new THREE.Vector2(); - var dollyDelta = new THREE.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 THREE.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 THREE.Vector3(); - - return function panUp( distance, objectMatrix ) { - - v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix - v.multiplyScalar( distance ); - - panOffset.add( v ); - - }; - - }(); - - // deltaX and deltaY are in pixels; right and down are positive - var pan = function () { - - var offset = new THREE.Vector3(); - - return function pan( deltaX, deltaY ) { - - var element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - if ( scope.object instanceof THREE.PerspectiveCamera ) { - - // 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 actually don't use screenWidth, since perspective camera is fixed to screen height - panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); - panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); - - } else if ( scope.object instanceof THREE.OrthographicCamera ) { - - // 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 instanceof THREE.PerspectiveCamera ) { - - scale /= dollyScale; - - } else if ( scope.object instanceof THREE.OrthographicCamera ) { - - 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 instanceof THREE.PerspectiveCamera ) { - - scale *= dollyScale; - - } else if ( scope.object instanceof THREE.OrthographicCamera ) { - - 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 ) { - - //console.log( 'handleMouseDownRotate' ); - - rotateStart.set( event.clientX, event.clientY ); - - } - - function handleMouseDownDolly( event ) { - - //console.log( 'handleMouseDownDolly' ); - - dollyStart.set( event.clientX, event.clientY ); - - } - - function handleMouseDownPan( event ) { - - //console.log( 'handleMouseDownPan' ); - - panStart.set( event.clientX, event.clientY ); - - } - - function handleMouseMoveRotate( event ) { - - //console.log( 'handleMouseMoveRotate' ); - - rotateEnd.set( event.clientX, event.clientY ); - rotateDelta.subVectors( rotateEnd, rotateStart ); - - var element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - // rotating across whole screen goes 360 degrees around - rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); - - // rotating up and down along whole screen attempts to go 360, but limited to 180 - rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); - - rotateStart.copy( rotateEnd ); - - scope.update(); - - } - - function handleMouseMoveDolly( event ) { - - //console.log( 'handleMouseMoveDolly' ); - - 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 ) { - - //console.log( 'handleMouseMovePan' ); - - panEnd.set( event.clientX, event.clientY ); - - panDelta.subVectors( panEnd, panStart ); - - pan( panDelta.x, panDelta.y ); - - panStart.copy( panEnd ); - - scope.update(); - - } - - function handleMouseUp( event ) { - - // console.log( 'handleMouseUp' ); - - } - - function handleMouseWheel( event ) { - - // console.log( 'handleMouseWheel' ); - - if ( event.deltaY < 0 ) { - - dollyOut( getZoomScale() ); - - } else if ( event.deltaY > 0 ) { - - dollyIn( getZoomScale() ); - - } - - scope.update(); - - } - - function handleKeyDown( event ) { - - //console.log( 'handleKeyDown' ); - - switch ( event.keyCode ) { - - case scope.keys.UP: - pan( 0, scope.keyPanSpeed ); - scope.update(); - break; - - case scope.keys.BOTTOM: - pan( 0, - scope.keyPanSpeed ); - scope.update(); - break; - - case scope.keys.LEFT: - pan( scope.keyPanSpeed, 0 ); - scope.update(); - break; - - case scope.keys.RIGHT: - pan( - scope.keyPanSpeed, 0 ); - scope.update(); - break; - - } - - } - - function handleTouchStartRotate( event ) { - - //console.log( 'handleTouchStartRotate' ); - - rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); - - } - - function handleTouchStartDolly( event ) { - - //console.log( 'handleTouchStartDolly' ); - - 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 handleTouchStartPan( event ) { - - //console.log( 'handleTouchStartPan' ); - - panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); - - } - - function handleTouchMoveRotate( event ) { - - //console.log( 'handleTouchMoveRotate' ); - - rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); - rotateDelta.subVectors( rotateEnd, rotateStart ); - - var element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - // rotating across whole screen goes 360 degrees around - rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); - - // rotating up and down along whole screen attempts to go 360, but limited to 180 - rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); - - rotateStart.copy( rotateEnd ); - - scope.update(); - - } - - function handleTouchMoveDolly( event ) { - - //console.log( 'handleTouchMoveDolly' ); - - 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.subVectors( dollyEnd, dollyStart ); - - if ( dollyDelta.y > 0 ) { - - dollyOut( getZoomScale() ); - - } else if ( dollyDelta.y < 0 ) { - - dollyIn( getZoomScale() ); - - } - - dollyStart.copy( dollyEnd ); - - scope.update(); - - } - - function handleTouchMovePan( event ) { - - //console.log( 'handleTouchMovePan' ); - - panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); - - panDelta.subVectors( panEnd, panStart ); - - pan( panDelta.x, panDelta.y ); - - panStart.copy( panEnd ); - - scope.update(); - - } - - function handleTouchEnd( event ) { - - //console.log( 'handleTouchEnd' ); - - } - - // - // event handlers - FSM: listen for events and reset state - // - - function onMouseDown( event ) { - - if ( scope.enabled === false ) return; - - event.preventDefault(); - - switch ( event.button ) { - - case scope.mouseButtons.ORBIT: - - if ( scope.enableRotate === false ) return; - - handleMouseDownRotate( event ); - - state = STATE.ROTATE; - - break; - - case scope.mouseButtons.ZOOM: - - if ( scope.enableZoom === false ) return; - - handleMouseDownDolly( event ); - - state = STATE.DOLLY; - - break; - - case scope.mouseButtons.PAN: - - if ( scope.enablePan === false ) return; - - handleMouseDownPan( event ); - - state = STATE.PAN; - - break; - - } - - 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(); - - handleMouseWheel( event ); - - scope.dispatchEvent( startEvent ); // not sure why these are here... - 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; - - switch ( event.touches.length ) { - - case 1: // one-fingered touch: rotate - - if ( scope.enableRotate === false ) return; - - handleTouchStartRotate( event ); - - state = STATE.TOUCH_ROTATE; - - break; - - case 2: // two-fingered touch: dolly - - if ( scope.enableZoom === false ) return; - - handleTouchStartDolly( event ); - - state = STATE.TOUCH_DOLLY; - - break; - - case 3: // three-fingered touch: pan - - if ( scope.enablePan === false ) return; - - handleTouchStartPan( event ); - - state = STATE.TOUCH_PAN; - - 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 ( event.touches.length ) { - - case 1: // one-fingered touch: rotate - - if ( scope.enableRotate === false ) return; - if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?... - - handleTouchMoveRotate( event ); - - break; - - case 2: // two-fingered touch: dolly - - if ( scope.enableZoom === false ) return; - if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?... - - handleTouchMoveDolly( event ); - - break; - - case 3: // three-fingered touch: pan - - if ( scope.enablePan === false ) return; - if ( state !== STATE.TOUCH_PAN ) return; // is this needed?... - - handleTouchMovePan( event ); - - 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 ); - - window.addEventListener( 'keydown', onKeyDown, false ); - - // force an update at start - - this.update(); - -}; - -THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); -THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; - -Object.defineProperties( THREE.OrbitControls.prototype, { - - center: { - - get: function () { - - console.warn( 'THREE.OrbitControls: .center has been renamed to .target' ); - return this.target; - - } - - }, - - // backward compatibility - - noZoom: { - - get: function () { - - console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); - return ! this.enableZoom; - - }, - - set: function ( value ) { - - console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); - this.enableZoom = ! value; - - } - - }, - - noRotate: { - - get: function () { - - console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); - return ! this.enableRotate; - - }, - - set: function ( value ) { - - console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); - this.enableRotate = ! value; - - } - - }, - - noPan: { - - get: function () { - - console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); - return ! this.enablePan; - - }, - - set: function ( value ) { - - console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); - this.enablePan = ! value; - - } - - }, - - noKeys: { - - get: function () { - - console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); - return ! this.enableKeys; - - }, - - set: function ( value ) { - - console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); - this.enableKeys = ! value; - - } - - }, - - staticMoving: { - - get: function () { - - console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); - return ! this.enableDamping; - - }, - - set: function ( value ) { - - console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); - this.enableDamping = ! value; - - } - - }, - - dynamicDampingFactor: { - - get: function () { - - console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); - return this.dampingFactor; - - }, - - set: function ( value ) { - - console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); - this.dampingFactor = value; - - } - - } - -} ); diff --git a/src/main/resources/static/obj-viewer/three-examples.js b/src/main/resources/static/obj-viewer/three-examples.js index 7892ef1..cd3d962 100644 --- a/src/main/resources/static/obj-viewer/three-examples.js +++ b/src/main/resources/static/obj-viewer/three-examples.js @@ -1,1452 +1,2639 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i= 0 ? line.substring( 0, pos ) : line; + key = key.toLowerCase(); + let value = pos >= 0 ? line.substring( pos + 1 ) : ''; + value = value.trim(); + + if ( key === 'newmtl' ) { + + // New material + info = { + name: value + }; + materialsInfo[ value ] = info; + + } else { + + if ( key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke' ) { + + const ss = value.split( delimiter_pattern, 3 ); + info[ key ] = [ parseFloat( ss[ 0 ] ), parseFloat( ss[ 1 ] ), parseFloat( ss[ 2 ] ) ]; + + } else { + + info[ key ] = value; + + } + + } + + } + + const materialCreator = new MaterialCreator( this.resourcePath || path, this.materialOptions ); + materialCreator.setCrossOrigin( this.crossOrigin ); + materialCreator.setManager( this.manager ); + materialCreator.setMaterials( materialsInfo ); + return materialCreator; + + } + + } /** - * Loads and parses a MTL asset from a URL. - * - * @param {String} url - URL to the MTL file. - * @param {Function} [onLoad] - Callback invoked with the loaded object. - * @param {Function} [onProgress] - Callback for download progress. - * @param {Function} [onError] - Callback for download errors. - * - * @see setPath setResourcePath - * - * @note In order for relative texture references to resolve correctly - * you must call setResourcePath() explicitly prior to load. + * Create a new MTLLoader.MaterialCreator + * @param baseUrl - Url relative to which textures are loaded + * @param options - Set of options on how to construct the materials + * side: Which side to apply the material + * THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide + * wrap: What type of wrapping to apply for textures + * THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping + * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255 + * Default: false, assumed to be already normalized + * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's + * Default: false + * @constructor */ - load(url, onLoad, onProgress, onError) { - const scope = this; - const path = (this.path === '') ? THREE.LoaderUtils.extractUrlBase(url) : this.path; + class MaterialCreator { - const loader = new THREE.FileLoader(this.manager); - loader.setPath(this.path); - loader.setRequestHeader(this.requestHeader); - loader.setWithCredentials(this.withCredentials); - loader.load(url, function (text) { + constructor( baseUrl = '', options = {} ) { - try { + this.baseUrl = baseUrl; + this.options = options; + this.materialsInfo = {}; + this.materials = {}; + this.materialsArray = []; + this.nameLookup = {}; + this.crossOrigin = 'anonymous'; + this.side = this.options.side !== undefined ? this.options.side : THREE.FrontSide; + this.wrap = this.options.wrap !== undefined ? this.options.wrap : THREE.RepeatWrapping; - onLoad(scope.parse(text, path)); + } - } catch (e) { + setCrossOrigin( value ) { - if (onError) { + this.crossOrigin = value; + return this; - onError(e); + } - } else { + setManager( value ) { - console.error(e); + this.manager = value; + + } + + setMaterials( materialsInfo ) { + + this.materialsInfo = this.convert( materialsInfo ); + this.materials = {}; + this.materialsArray = []; + this.nameLookup = {}; + + } + + convert( materialsInfo ) { + + if ( ! this.options ) return materialsInfo; + const converted = {}; + + for ( const mn in materialsInfo ) { + + // Convert materials info into normalized form based on options + const mat = materialsInfo[ mn ]; + const covmat = {}; + converted[ mn ] = covmat; + + for ( const prop in mat ) { + + let save = true; + let value = mat[ prop ]; + const lprop = prop.toLowerCase(); + + switch ( lprop ) { + + case 'kd': + case 'ka': + case 'ks': + // Diffuse color (color under white light) using RGB values + if ( this.options && this.options.normalizeRGB ) { + + value = [ value[ 0 ] / 255, value[ 1 ] / 255, value[ 2 ] / 255 ]; + + } + + if ( this.options && this.options.ignoreZeroRGBs ) { + + if ( value[ 0 ] === 0 && value[ 1 ] === 0 && value[ 2 ] === 0 ) { + + // ignore + save = false; + + } + + } + + break; + + default: + break; + + } + + if ( save ) { + + covmat[ lprop ] = value; + + } } - scope.manager.itemError(url); + } + + return converted; + + } + + preload() { + + for ( const mn in this.materialsInfo ) { + + this.create( mn ); } - }, onProgress, onError); + } - } + getIndex( materialName ) { - setMaterialOptions(value) { + return this.nameLookup[ materialName ]; - this.materialOptions = value; - return this; + } - } + getAsArray() { - /** - * Parses a MTL file. - * - * @param {String} text - Content of MTL file - * @return {MaterialCreator} - * - * @see setPath setResourcePath - * - * @note In order for relative texture references to resolve correctly - * you must call setResourcePath() explicitly prior to parse. - */ - parse(text, path) { + let index = 0; - const lines = text.split('\n'); - let info = {}; - const delimiter_pattern = /\s+/; - const materialsInfo = {}; + for ( const mn in this.materialsInfo ) { - for (let i = 0; i < lines.length; i++) { + this.materialsArray[ index ] = this.create( mn ); + this.nameLookup[ mn ] = index; + index ++; - let line = lines[i]; - line = line.trim(); + } - if (line.length === 0 || line.charAt(0) === '#') { + return this.materialsArray; - // Blank line or comment ignore - continue; + } + + create( materialName ) { + + if ( this.materials[ materialName ] === undefined ) { + + this.createMaterial_( materialName ); } - const pos = line.indexOf(' '); + return this.materials[ materialName ]; - let key = (pos >= 0) ? line.substring(0, pos) : line; - key = key.toLowerCase(); + } - let value = (pos >= 0) ? line.substring(pos + 1) : ''; - value = value.trim(); + createMaterial_( materialName ) { - if (key === 'newmtl') { + // Create material + const scope = this; + const mat = this.materialsInfo[ materialName ]; + const params = { + name: materialName, + side: this.side + }; - // New material + function resolveURL( baseUrl, url ) { - info = {name: value}; - materialsInfo[value] = info; + if ( typeof url !== 'string' || url === '' ) return ''; // Absolute URL - } else { + if ( /^https?:\/\//i.test( url ) ) return url; + return baseUrl + url; - if (key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke') { + } - const ss = value.split(delimiter_pattern, 3); - info[key] = [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])]; + function setMapForType( mapType, value ) { - } else { + if ( params[ mapType ] ) return; // Keep the first encountered texture + + const texParams = scope.getTextureParams( value, params ); + const map = scope.loadTexture( resolveURL( scope.baseUrl, texParams.url ) ); + map.repeat.copy( texParams.scale ); + map.offset.copy( texParams.offset ); + map.wrapS = scope.wrap; + map.wrapT = scope.wrap; + + params[ mapType ] = map; + + } + + for ( const prop in mat ) { - info[key] = value; + const value = mat[ prop ]; + let n; + if ( value === '' ) continue; + + switch ( prop.toLowerCase() ) { + + // Ns is material specular exponent + case 'kd': + // Diffuse color (color under white light) using RGB values + params.color = new THREE.Color().fromArray( value ); + break; + + case 'ks': + // Specular color (color when light is reflected from shiny surface) using RGB values + params.specular = new THREE.Color().fromArray( value ); + break; + + case 'ke': + // Emissive using RGB values + params.emissive = new THREE.Color().fromArray( value ); + break; + + case 'map_kd': + // Diffuse texture map + setMapForType( 'map', value ); + break; + + case 'map_ks': + // Specular map + setMapForType( 'specularMap', value ); + break; + + case 'map_ke': + // Emissive map + setMapForType( 'emissiveMap', value ); + break; + + case 'norm': + setMapForType( 'normalMap', value ); + break; + + case 'map_bump': + case 'bump': + // Bump texture map + setMapForType( 'bumpMap', value ); + break; + + case 'map_d': + // Alpha map + setMapForType( 'alphaMap', value ); + params.transparent = true; + break; + + case 'ns': + // The specular exponent (defines the focus of the specular highlight) + // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000. + params.shininess = parseFloat( value ); + break; + + case 'd': + n = parseFloat( value ); + + if ( n < 1 ) { + + params.opacity = n; + params.transparent = true; + + } + + break; + + case 'tr': + n = parseFloat( value ); + if ( this.options && this.options.invertTrProperty ) n = 1 - n; + + if ( n > 0 ) { + + params.opacity = 1 - n; + params.transparent = true; + + } + + break; + + default: + break; } } + this.materials[ materialName ] = new THREE.MeshPhongMaterial( params ); + return this.materials[ materialName ]; + } - const materialCreator = new MaterialCreator(this.resourcePath || path, this.materialOptions); - materialCreator.setCrossOrigin(this.crossOrigin); - materialCreator.setManager(this.manager); - materialCreator.setMaterials(materialsInfo); - return materialCreator; + getTextureParams( value, matParams ) { - } + const texParams = { + scale: new THREE.Vector2( 1, 1 ), + offset: new THREE.Vector2( 0, 0 ) + }; + const items = value.split( /\s+/ ); + let pos; + pos = items.indexOf( '-bm' ); -} + if ( pos >= 0 ) { -/** - * Create a new MTLLoader.MaterialCreator - * @param baseUrl - Url relative to which textures are loaded - * @param options - Set of options on how to construct the materials - * side: Which side to apply the material - * THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide - * wrap: What type of wrapping to apply for textures - * THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping - * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255 - * Default: false, assumed to be already normalized - * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's - * Default: false - * @constructor - */ + matParams.bumpScale = parseFloat( items[ pos + 1 ] ); + items.splice( pos, 2 ); -class MaterialCreator { + } + + pos = items.indexOf( '-s' ); - constructor(baseUrl = '', options = {}) { + if ( pos >= 0 ) { - this.baseUrl = baseUrl; - this.options = options; - this.materialsInfo = {}; - this.materials = {}; - this.materialsArray = []; - this.nameLookup = {}; + texParams.scale.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) ); + items.splice( pos, 4 ); // we expect 3 parameters here! - this.crossOrigin = 'anonymous'; + } - this.side = (this.options.side !== undefined) ? this.options.side : THREE.FrontSide; - this.wrap = (this.options.wrap !== undefined) ? this.options.wrap : THREE.RepeatWrapping; + pos = items.indexOf( '-o' ); - } + if ( pos >= 0 ) { + + texParams.offset.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) ); + items.splice( pos, 4 ); // we expect 3 parameters here! + + } + + texParams.url = items.join( ' ' ).trim(); + return texParams; - setCrossOrigin(value) { + } + + loadTexture( url, mapping, onLoad, onProgress, onError ) { + + const manager = this.manager !== undefined ? this.manager : THREE.DefaultLoadingManager; + let loader = manager.getHandler( url ); + + if ( loader === null ) { + + loader = new THREE.TextureLoader( manager ); + + } - this.crossOrigin = value; - return this; + if ( loader.setCrossOrigin ) loader.setCrossOrigin( this.crossOrigin ); + const texture = loader.load( url, onLoad, onProgress, onError ); + if ( mapping !== undefined ) texture.mapping = mapping; + return texture; + + } } - setManager(value) { + THREE.MTLLoader = MTLLoader; + +} )(); +( function () { + + const _object_pattern = /^[og]\s*(.+)?/; // mtllib file_reference + + const _material_library_pattern = /^mtllib /; // usemtl material_name + + const _material_use_pattern = /^usemtl /; // usemap map_name + + const _map_use_pattern = /^usemap /; + + const _vA = new THREE.Vector3(); + + const _vB = new THREE.Vector3(); + + const _vC = new THREE.Vector3(); + + const _ab = new THREE.Vector3(); + + const _cb = new THREE.Vector3(); + + function ParserState() { + + const state = { + objects: [], + object: {}, + vertices: [], + normals: [], + colors: [], + uvs: [], + materials: {}, + materialLibraries: [], + startObject: function ( name, fromDeclaration ) { + + // If the current object (initial from reset) is not from a g/o declaration in the parsed + // file. We need to use it for the first parsed g/o to keep things in sync. + if ( this.object && this.object.fromDeclaration === false ) { + + this.object.name = name; + this.object.fromDeclaration = fromDeclaration !== false; + return; + + } + + const previousMaterial = this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined; + + if ( this.object && typeof this.object._finalize === 'function' ) { + + this.object._finalize( true ); + + } + + this.object = { + name: name || '', + fromDeclaration: fromDeclaration !== false, + geometry: { + vertices: [], + normals: [], + colors: [], + uvs: [], + hasUVIndices: false + }, + materials: [], + smooth: true, + startMaterial: function ( name, libraries ) { + + const previous = this._finalize( false ); // New usemtl declaration overwrites an inherited material, except if faces were declared + // after the material, then it must be preserved for proper MultiMaterial continuation. + + + if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) { + + this.materials.splice( previous.index, 1 ); + + } + + const material = { + index: this.materials.length, + name: name || '', + mtllib: Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '', + smooth: previous !== undefined ? previous.smooth : this.smooth, + groupStart: previous !== undefined ? previous.groupEnd : 0, + groupEnd: - 1, + groupCount: - 1, + inherited: false, + clone: function ( index ) { + + const cloned = { + index: typeof index === 'number' ? index : this.index, + name: this.name, + mtllib: this.mtllib, + smooth: this.smooth, + groupStart: 0, + groupEnd: - 1, + groupCount: - 1, + inherited: false + }; + cloned.clone = this.clone.bind( cloned ); + return cloned; + + } + }; + this.materials.push( material ); + return material; + + }, + currentMaterial: function () { + + if ( this.materials.length > 0 ) { + + return this.materials[ this.materials.length - 1 ]; + + } + + return undefined; + + }, + _finalize: function ( end ) { + + const lastMultiMaterial = this.currentMaterial(); + + if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) { + + lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; + lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; + lastMultiMaterial.inherited = false; + + } // Ignore objects tail materials if no face declarations followed them before a new o/g started. + + + if ( end && this.materials.length > 1 ) { + + for ( let mi = this.materials.length - 1; mi >= 0; mi -- ) { + + if ( this.materials[ mi ].groupCount <= 0 ) { + + this.materials.splice( mi, 1 ); + + } + + } + + } // Guarantee at least one empty material, this makes the creation later more straight forward. + + + if ( end && this.materials.length === 0 ) { + + this.materials.push( { + name: '', + smooth: this.smooth + } ); + + } + + return lastMultiMaterial; + + } + }; // Inherit previous objects material. + // Spec tells us that a declared material must be set to all objects until a new material is declared. + // If a usemtl declaration is encountered while this new object is being parsed, it will + // overwrite the inherited material. Exception being that there was already face declarations + // to the inherited material, then it will be preserved for proper MultiMaterial continuation. + + if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) { + + const declared = previousMaterial.clone( 0 ); + declared.inherited = true; + this.object.materials.push( declared ); + + } + + this.objects.push( this.object ); + + }, + finalize: function () { + + if ( this.object && typeof this.object._finalize === 'function' ) { + + this.object._finalize( true ); + + } + + }, + parseVertexIndex: function ( value, len ) { + + const index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; + + }, + parseNormalIndex: function ( value, len ) { + + const index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; + + }, + parseUVIndex: function ( value, len ) { + + const index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; + + }, + addVertex: function ( a, b, c ) { + + const src = this.vertices; + const dst = this.object.geometry.vertices; + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + addVertexPoint: function ( a ) { + + const src = this.vertices; + const dst = this.object.geometry.vertices; + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + + }, + addVertexLine: function ( a ) { + + const src = this.vertices; + const dst = this.object.geometry.vertices; + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + + }, + addNormal: function ( a, b, c ) { + + const src = this.normals; + const dst = this.object.geometry.normals; + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + addFaceNormal: function ( a, b, c ) { + + const src = this.vertices; + const dst = this.object.geometry.normals; + + _vA.fromArray( src, a ); + + _vB.fromArray( src, b ); + + _vC.fromArray( src, c ); + + _cb.subVectors( _vC, _vB ); + + _ab.subVectors( _vA, _vB ); + + _cb.cross( _ab ); + + _cb.normalize(); + + dst.push( _cb.x, _cb.y, _cb.z ); + dst.push( _cb.x, _cb.y, _cb.z ); + dst.push( _cb.x, _cb.y, _cb.z ); + + }, + addColor: function ( a, b, c ) { + + const src = this.colors; + const dst = this.object.geometry.colors; + if ( src[ a ] !== undefined ) dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + if ( src[ b ] !== undefined ) dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + if ( src[ c ] !== undefined ) dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + addUV: function ( a, b, c ) { + + const src = this.uvs; + const dst = this.object.geometry.uvs; + dst.push( src[ a + 0 ], src[ a + 1 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ] ); + + }, + addDefaultUV: function () { + + const dst = this.object.geometry.uvs; + dst.push( 0, 0 ); + dst.push( 0, 0 ); + dst.push( 0, 0 ); + + }, + addUVLine: function ( a ) { + + const src = this.uvs; + const dst = this.object.geometry.uvs; + dst.push( src[ a + 0 ], src[ a + 1 ] ); + + }, + addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) { + + const vLen = this.vertices.length; + let ia = this.parseVertexIndex( a, vLen ); + let ib = this.parseVertexIndex( b, vLen ); + let ic = this.parseVertexIndex( c, vLen ); + this.addVertex( ia, ib, ic ); + this.addColor( ia, ib, ic ); // normals + + if ( na !== undefined && na !== '' ) { + + const nLen = this.normals.length; + ia = this.parseNormalIndex( na, nLen ); + ib = this.parseNormalIndex( nb, nLen ); + ic = this.parseNormalIndex( nc, nLen ); + this.addNormal( ia, ib, ic ); + + } else { + + this.addFaceNormal( ia, ib, ic ); + + } // uvs + + + if ( ua !== undefined && ua !== '' ) { + + const uvLen = this.uvs.length; + ia = this.parseUVIndex( ua, uvLen ); + ib = this.parseUVIndex( ub, uvLen ); + ic = this.parseUVIndex( uc, uvLen ); + this.addUV( ia, ib, ic ); + this.object.geometry.hasUVIndices = true; + + } else { + + // add placeholder values (for inconsistent face definitions) + this.addDefaultUV(); + + } + + }, + addPointGeometry: function ( vertices ) { + + this.object.geometry.type = 'Points'; + const vLen = this.vertices.length; + + for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) { + + const index = this.parseVertexIndex( vertices[ vi ], vLen ); + this.addVertexPoint( index ); + this.addColor( index ); + + } + + }, + addLineGeometry: function ( vertices, uvs ) { + + this.object.geometry.type = 'Line'; + const vLen = this.vertices.length; + const uvLen = this.uvs.length; + + for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) { + + this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); + + } + + for ( let uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { + + this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); + + } + + } + }; + state.startObject( '', false ); + return state; + + } // + + + class OBJLoader extends THREE.Loader { + + constructor( manager ) { + + super( manager ); + this.materials = null; + + } + + load( url, onLoad, onProgress, onError ) { + + const scope = this; + const loader = new THREE.FileLoader( this.manager ); + loader.setPath( this.path ); + loader.setRequestHeader( this.requestHeader ); + loader.setWithCredentials( this.withCredentials ); + loader.load( url, function ( text ) { + + try { + + onLoad( scope.parse( text ) ); + + } catch ( e ) { + + if ( onError ) { + + onError( e ); + + } else { + + console.error( e ); + + } + + scope.manager.itemError( url ); + + } + + }, onProgress, onError ); + + } + + setMaterials( materials ) { + + this.materials = materials; + return this; + + } + + parse( text ) { + + const state = new ParserState(); + + if ( text.indexOf( '\r\n' ) !== - 1 ) { + + // This is faster than String.split with regex that splits on both + text = text.replace( /\r\n/g, '\n' ); + + } + + if ( text.indexOf( '\\\n' ) !== - 1 ) { + + // join lines separated by a line continuation character (\) + text = text.replace( /\\\n/g, '' ); + + } + + const lines = text.split( '\n' ); + let line = '', + lineFirstChar = ''; + let lineLength = 0; + let result = []; // Faster to just trim left side of the line. Use if available. + + const trimLeft = typeof ''.trimLeft === 'function'; + + for ( let i = 0, l = lines.length; i < l; i ++ ) { + + line = lines[ i ]; + line = trimLeft ? line.trimLeft() : line.trim(); + lineLength = line.length; + if ( lineLength === 0 ) continue; + lineFirstChar = line.charAt( 0 ); // @todo invoke passed in handler if any + + if ( lineFirstChar === '#' ) continue; + + if ( lineFirstChar === 'v' ) { + + const data = line.split( /\s+/ ); + + switch ( data[ 0 ] ) { + + case 'v': + state.vertices.push( parseFloat( data[ 1 ] ), parseFloat( data[ 2 ] ), parseFloat( data[ 3 ] ) ); + + if ( data.length >= 7 ) { + + state.colors.push( parseFloat( data[ 4 ] ), parseFloat( data[ 5 ] ), parseFloat( data[ 6 ] ) ); + + } else { + + // if no colors are defined, add placeholders so color and vertex indices match + state.colors.push( undefined, undefined, undefined ); + + } + + break; + + case 'vn': + state.normals.push( parseFloat( data[ 1 ] ), parseFloat( data[ 2 ] ), parseFloat( data[ 3 ] ) ); + break; + + case 'vt': + state.uvs.push( parseFloat( data[ 1 ] ), parseFloat( data[ 2 ] ) ); + break; + + } + + } else if ( lineFirstChar === 'f' ) { + + const lineData = line.substr( 1 ).trim(); + const vertexData = lineData.split( /\s+/ ); + const faceVertices = []; // Parse the face vertex data into an easy to work with format + + for ( let j = 0, jl = vertexData.length; j < jl; j ++ ) { + + const vertex = vertexData[ j ]; + + if ( vertex.length > 0 ) { + + const vertexParts = vertex.split( '/' ); + faceVertices.push( vertexParts ); + + } + + } // Draw an edge between the first vertex and all subsequent vertices to form an n-gon + + + const v1 = faceVertices[ 0 ]; + + for ( let j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) { + + const v2 = faceVertices[ j ]; + const v3 = faceVertices[ j + 1 ]; + state.addFace( v1[ 0 ], v2[ 0 ], v3[ 0 ], v1[ 1 ], v2[ 1 ], v3[ 1 ], v1[ 2 ], v2[ 2 ], v3[ 2 ] ); + + } + + } else if ( lineFirstChar === 'l' ) { + + const lineParts = line.substring( 1 ).trim().split( ' ' ); + let lineVertices = []; + const lineUVs = []; + + if ( line.indexOf( '/' ) === - 1 ) { + + lineVertices = lineParts; + + } else { + + for ( let li = 0, llen = lineParts.length; li < llen; li ++ ) { + + const parts = lineParts[ li ].split( '/' ); + if ( parts[ 0 ] !== '' ) lineVertices.push( parts[ 0 ] ); + if ( parts[ 1 ] !== '' ) lineUVs.push( parts[ 1 ] ); + + } + + } + + state.addLineGeometry( lineVertices, lineUVs ); + + } else if ( lineFirstChar === 'p' ) { + + const lineData = line.substr( 1 ).trim(); + const pointData = lineData.split( ' ' ); + state.addPointGeometry( pointData ); + + } else if ( ( result = _object_pattern.exec( line ) ) !== null ) { + + // o object_name + // or + // g group_name + // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 + // let name = result[ 0 ].substr( 1 ).trim(); + const name = ( ' ' + result[ 0 ].substr( 1 ).trim() ).substr( 1 ); + state.startObject( name ); + + } else if ( _material_use_pattern.test( line ) ) { + + // material + state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); + + } else if ( _material_library_pattern.test( line ) ) { + + // mtl file + state.materialLibraries.push( line.substring( 7 ).trim() ); + + } else if ( _map_use_pattern.test( line ) ) { + + // the line is parsed but ignored since the loader assumes textures are defined MTL files + // (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method) + console.warn( 'THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.' ); + + } else if ( lineFirstChar === 's' ) { + + result = line.split( ' ' ); // smooth shading + // @todo Handle files that have varying smooth values for a set of faces inside one geometry, + // but does not define a usemtl for each face set. + // This should be detected and a dummy material created (later MultiMaterial and geometry groups). + // This requires some care to not create extra material on each smooth value for "normal" obj files. + // where explicit usemtl defines geometry groups. + // Example asset: examples/models/obj/cerberus/Cerberus.obj + + /* + * http://paulbourke.net/dataformats/obj/ + * + * From chapter "Grouping" Syntax explanation "s group_number": + * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off. + * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form + * surfaces, smoothing groups are either turned on or off; there is no difference between values greater + * than 0." + */ + + if ( result.length > 1 ) { + + const value = result[ 1 ].trim().toLowerCase(); + state.object.smooth = value !== '0' && value !== 'off'; + + } else { + + // ZBrush can produce "s" lines #11707 + state.object.smooth = true; + + } + + const material = state.object.currentMaterial(); + if ( material ) material.smooth = state.object.smooth; + + } else { + + // Handle null terminated files without exception + if ( line === '\0' ) continue; + console.warn( 'THREE.OBJLoader: Unexpected line: "' + line + '"' ); + + } + + } + + state.finalize(); + const container = new THREE.Group(); + container.materialLibraries = [].concat( state.materialLibraries ); + const hasPrimitives = ! ( state.objects.length === 1 && state.objects[ 0 ].geometry.vertices.length === 0 ); + + if ( hasPrimitives === true ) { + + for ( let i = 0, l = state.objects.length; i < l; i ++ ) { + + const object = state.objects[ i ]; + const geometry = object.geometry; + const materials = object.materials; + const isLine = geometry.type === 'Line'; + const isPoints = geometry.type === 'Points'; + let hasVertexColors = false; // Skip o/g line declarations that did not follow with any faces + + if ( geometry.vertices.length === 0 ) continue; + const buffergeometry = new THREE.BufferGeometry(); + buffergeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( geometry.vertices, 3 ) ); + + if ( geometry.normals.length > 0 ) { + + buffergeometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( geometry.normals, 3 ) ); + + } + + if ( geometry.colors.length > 0 ) { + + hasVertexColors = true; + buffergeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( geometry.colors, 3 ) ); + + } + + if ( geometry.hasUVIndices === true ) { + + buffergeometry.setAttribute( 'uv', new THREE.Float32BufferAttribute( geometry.uvs, 2 ) ); + + } // Create materials + + + const createdMaterials = []; + + for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) { + + const sourceMaterial = materials[ mi ]; + const materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors; + let material = state.materials[ materialHash ]; + + if ( this.materials !== null ) { + + material = this.materials.create( sourceMaterial.name ); // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. + + if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) { + + const materialLine = new THREE.LineBasicMaterial(); + THREE.Material.prototype.copy.call( materialLine, material ); + materialLine.color.copy( material.color ); + material = materialLine; + + } else if ( isPoints && material && ! ( material instanceof THREE.PointsMaterial ) ) { + + const materialPoints = new THREE.PointsMaterial( { + size: 10, + sizeAttenuation: false + } ); + THREE.Material.prototype.copy.call( materialPoints, material ); + materialPoints.color.copy( material.color ); + materialPoints.map = material.map; + material = materialPoints; + + } + + } + + if ( material === undefined ) { + + if ( isLine ) { + + material = new THREE.LineBasicMaterial(); + + } else if ( isPoints ) { + + material = new THREE.PointsMaterial( { + size: 1, + sizeAttenuation: false + } ); + + } else { + + material = new THREE.MeshPhongMaterial(); + + } + + material.name = sourceMaterial.name; + material.flatShading = sourceMaterial.smooth ? false : true; + material.vertexColors = hasVertexColors; + state.materials[ materialHash ] = material; + + } - this.manager = value; + createdMaterials.push( material ); - } + } // Create mesh - setMaterials(materialsInfo) { - this.materialsInfo = this.convert(materialsInfo); - this.materials = {}; - this.materialsArray = []; - this.nameLookup = {}; + let mesh; - } + if ( createdMaterials.length > 1 ) { - convert(materialsInfo) { + for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) { - if (!this.options) return materialsInfo; + const sourceMaterial = materials[ mi ]; + buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); - const converted = {}; + } - for (const mn in materialsInfo) { + if ( isLine ) { - // Convert materials info into normalized form based on options + mesh = new THREE.LineSegments( buffergeometry, createdMaterials ); - const mat = materialsInfo[mn]; + } else if ( isPoints ) { - const covmat = {}; + mesh = new THREE.Points( buffergeometry, createdMaterials ); - converted[mn] = covmat; + } else { - for (const prop in mat) { + mesh = new THREE.Mesh( buffergeometry, createdMaterials ); - let save = true; - let value = mat[prop]; - const lprop = prop.toLowerCase(); + } - switch (lprop) { + } else { - case 'kd': - case 'ka': - case 'ks': + if ( isLine ) { - // Diffuse color (color under white light) using RGB values + mesh = new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ); - if (this.options && this.options.normalizeRGB) { + } else if ( isPoints ) { - value = [value[0] / 255, value[1] / 255, value[2] / 255]; + mesh = new THREE.Points( buffergeometry, createdMaterials[ 0 ] ); - } + } else { - if (this.options && this.options.ignoreZeroRGBs) { + mesh = new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ); - if (value[0] === 0 && value[1] === 0 && value[2] === 0) { + } - // ignore + } - save = false; + mesh.name = object.name; + container.add( mesh ); - } + } - } + } else { - break; + // if there is only the default parser state object with no geometry data, interpret data as point cloud + if ( state.vertices.length > 0 ) { - default: + const material = new THREE.PointsMaterial( { + size: 1, + sizeAttenuation: false + } ); + const buffergeometry = new THREE.BufferGeometry(); + buffergeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( state.vertices, 3 ) ); - break; + if ( state.colors.length > 0 && state.colors[ 0 ] !== undefined ) { - } + buffergeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( state.colors, 3 ) ); + material.vertexColors = true; - if (save) { + } - covmat[lprop] = value; + const points = new THREE.Points( buffergeometry, material ); + container.add( points ); } } - } + return container; - return converted; + } } - preload() { + THREE.OBJLoader = OBJLoader; - for (const mn in this.materialsInfo) { +} )(); +(function () { - this.create(mn); + /** + * Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs + */ - } + const _position = new THREE.Vector3(); - } + const _quaternion = new THREE.Quaternion(); - getIndex(materialName) { + const _scale = new THREE.Vector3(); - return this.nameLookup[materialName]; + class CSS3DObject extends THREE.Object3D { - } + constructor(element = document.createElement('div')) { - getAsArray() { + super(); + this.element = element; + this.element.style.position = 'absolute'; + this.element.style.pointerEvents = 'auto'; + this.element.style.userSelect = 'none'; + this.element.setAttribute('draggable', false); + this.addEventListener('removed', function () { - let index = 0; + this.traverse(function (object) { - for (const mn in this.materialsInfo) { + if (object.element instanceof Element && object.element.parentNode !== null) { - this.materialsArray[index] = this.create(mn); - this.nameLookup[mn] = index; - index++; + object.element.parentNode.removeChild(object.element); - } + } - return this.materialsArray; + }); - } + }); - create(materialName) { + } - if (this.materials[materialName] === undefined) { + copy(source, recursive) { - this.createMaterial_(materialName); + super.copy(source, recursive); + this.element = source.element.cloneNode(true); + return this; } - return this.materials[materialName]; - } - createMaterial_(materialName) { + CSS3DObject.prototype.isCSS3DObject = true; - // Create material + class CSS3DSprite extends CSS3DObject { - const scope = this; - const mat = this.materialsInfo[materialName]; - const params = { + constructor(element) { - name: materialName, - side: this.side + super(element); + this.rotation2D = 0; - }; + } - function resolveURL(baseUrl, url) { + copy(source, recursive) { - if (typeof url !== 'string' || url === '') - return ''; + super.copy(source, recursive); + this.rotation2D = source.rotation2D; + return this; - // Absolute URL - if (/^https?:\/\//i.test(url)) return url; + } - return baseUrl + url; + } - } + CSS3DSprite.prototype.isCSS3DSprite = true; // - function setMapForType(mapType, value) { + const _matrix = new THREE.Matrix4(); - if (params[mapType]) return; // Keep the first encountered texture + const _matrix2 = new THREE.Matrix4(); - const texParams = scope.getTextureParams(value, params); - const map = scope.loadTexture(resolveURL(scope.baseUrl, texParams.url)); + class CSS3DRenderer { - map.repeat.copy(texParams.scale); - map.offset.copy(texParams.offset); + constructor(parameters = {}) { - map.wrapS = scope.wrap; - map.wrapT = scope.wrap; + const _this = this; - params[mapType] = map; + let _width, _height; - } + let _widthHalf, _heightHalf; - for (const prop in mat) { + const cache = { + camera: { + fov: 0, + style: '' + }, + objects: new WeakMap() + }; + const domElement = parameters.element !== undefined ? parameters.element : document.createElement('div'); + domElement.style.overflow = 'hidden'; + this.domElement = domElement; + const cameraElement = document.createElement('div'); + cameraElement.style.transformStyle = 'preserve-3d'; + cameraElement.style.pointerEvents = 'none'; + domElement.appendChild(cameraElement); + + this.getSize = function () { + + return { + width: _width, + height: _height + }; - const value = mat[prop]; - let n; + }; + + this.render = function (scene, camera) { - if (value === '') continue; + const fov = camera.projectionMatrix.elements[5] * _heightHalf; - switch (prop.toLowerCase()) { + if (cache.camera.fov !== fov) { - // Ns is material specular exponent + domElement.style.perspective = camera.isPerspectiveCamera ? fov + 'px' : ''; + cache.camera.fov = fov; - case 'kd': + } - // Diffuse color (color under white light) using RGB values + if (scene.autoUpdate === true) scene.updateMatrixWorld(); + if (camera.parent === null) camera.updateMatrixWorld(); + let tx, ty; - params.color = new THREE.Color().fromArray(value); + if (camera.isOrthographicCamera) { - break; + tx = -(camera.right + camera.left) / 2; + ty = (camera.top + camera.bottom) / 2; - case 'ks': + } - // Specular color (color when light is reflected from shiny surface) using RGB values - params.specular = new THREE.Color().fromArray(value); + const cameraCSSMatrix = camera.isOrthographicCamera ? 'scale(' + fov + ')' + 'translate(' + epsilon(tx) + 'px,' + epsilon(ty) + 'px)' + getCameraCSSMatrix(camera.matrixWorldInverse) : 'translateZ(' + fov + 'px)' + getCameraCSSMatrix(camera.matrixWorldInverse); + const style = cameraCSSMatrix + 'translate(' + _widthHalf + 'px,' + _heightHalf + 'px)'; - break; + if (cache.camera.style !== style) { - case 'ke': + cameraElement.style.transform = style; + cache.camera.style = style; - // Emissive using RGB values - params.emissive = new THREE.Color().fromArray(value); + } - break; + renderObject(scene, scene, camera, cameraCSSMatrix); - case 'map_kd': + }; - // Diffuse texture map + this.setSize = function (width, height) { - setMapForType('map', value); + _width = width; + _height = height; + _widthHalf = _width / 2; + _heightHalf = _height / 2; + domElement.style.width = width + 'px'; + domElement.style.height = height + 'px'; + cameraElement.style.width = width + 'px'; + cameraElement.style.height = height + 'px'; - break; + }; - case 'map_ks': + function epsilon(value) { - // Specular map + return Math.abs(value) < 1e-10 ? 0 : value; - setMapForType('specularMap', value); + } - break; + function getCameraCSSMatrix(matrix) { - case 'map_ke': + const elements = matrix.elements; + return 'matrix3d(' + epsilon(elements[0]) + ',' + epsilon(-elements[1]) + ',' + epsilon(elements[2]) + ',' + epsilon(elements[3]) + ',' + epsilon(elements[4]) + ',' + epsilon(-elements[5]) + ',' + epsilon(elements[6]) + ',' + epsilon(elements[7]) + ',' + epsilon(elements[8]) + ',' + epsilon(-elements[9]) + ',' + epsilon(elements[10]) + ',' + epsilon(elements[11]) + ',' + epsilon(elements[12]) + ',' + epsilon(-elements[13]) + ',' + epsilon(elements[14]) + ',' + epsilon(elements[15]) + ')'; - // Emissive map + } - setMapForType('emissiveMap', value); + function getObjectCSSMatrix(matrix) { - break; + const elements = matrix.elements; + const matrix3d = 'matrix3d(' + epsilon(elements[0]) + ',' + epsilon(elements[1]) + ',' + epsilon(elements[2]) + ',' + epsilon(elements[3]) + ',' + epsilon(-elements[4]) + ',' + epsilon(-elements[5]) + ',' + epsilon(-elements[6]) + ',' + epsilon(-elements[7]) + ',' + epsilon(elements[8]) + ',' + epsilon(elements[9]) + ',' + epsilon(elements[10]) + ',' + epsilon(elements[11]) + ',' + epsilon(elements[12]) + ',' + epsilon(elements[13]) + ',' + epsilon(elements[14]) + ',' + epsilon(elements[15]) + ')'; + return 'translate(-50%,-50%)' + matrix3d; - case 'norm': + } - setMapForType('normalMap', value); + function renderObject(object, scene, camera, cameraCSSMatrix) { - break; + if (object.isCSS3DObject) { - case 'map_bump': - case 'bump': + const visible = object.visible && object.layers.test(camera.layers); + object.element.style.display = visible ? '' : 'none'; // only getObjectCSSMatrix when object.visible - // Bump texture map + if (visible) { - setMapForType('bumpMap', value); + object.onBeforeRender(_this, scene, camera); + let style; - break; + if (object.isCSS3DSprite) { - case 'map_d': + // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/ + _matrix.copy(camera.matrixWorldInverse); - // Alpha map + _matrix.transpose(); - setMapForType('alphaMap', value); - params.transparent = true; + if (object.rotation2D !== 0) _matrix.multiply(_matrix2.makeRotationZ(object.rotation2D)); + object.matrixWorld.decompose(_position, _quaternion, _scale); - break; + _matrix.setPosition(_position); - case 'ns': + _matrix.scale(_scale); - // The specular exponent (defines the focus of the specular highlight) - // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000. + _matrix.elements[3] = 0; + _matrix.elements[7] = 0; + _matrix.elements[11] = 0; + _matrix.elements[15] = 1; + style = getObjectCSSMatrix(_matrix); - params.shininess = parseFloat(value); + } else { - break; + style = getObjectCSSMatrix(object.matrixWorld); - case 'd': - n = parseFloat(value); + } - if (n < 1) { + const element = object.element; + const cachedObject = cache.objects.get(object); - params.opacity = n; - params.transparent = true; + if (cachedObject === undefined || cachedObject.style !== style) { - } + element.style.transform = style; + const objectData = { + style: style + }; + cache.objects.set(object, objectData); - break; + } - case 'tr': - n = parseFloat(value); + if (element.parentNode !== cameraElement) { - if (this.options && this.options.invertTrProperty) n = 1 - n; + cameraElement.appendChild(element); - if (n > 0) { + } - params.opacity = 1 - n; - params.transparent = true; + object.onAfterRender(_this, scene, camera); } - break; + } + + for (let i = 0, l = object.children.length; i < l; i++) { - default: - break; + renderObject(object.children[i], scene, camera, cameraCSSMatrix); + + } } } - this.materials[materialName] = new THREE.MeshPhongMaterial(params); - return this.materials[materialName]; - } - getTextureParams(value, matParams) { + THREE.CSS3DObject = CSS3DObject; + THREE.CSS3DRenderer = CSS3DRenderer; + THREE.CSS3DSprite = CSS3DSprite; - const texParams = { +})(); +( function () { - scale: new THREE.Vector2(1, 1), - offset: new THREE.Vector2(0, 0) + // 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 - }; + const _changeEvent = { + type: 'change' + }; + const _startEvent = { + type: 'start' + }; + const _endEvent = { + type: 'end' + }; - const items = value.split(/\s+/); - let pos; + class OrbitControls extends THREE.EventDispatcher { - pos = items.indexOf('-bm'); + constructor( object, domElement ) { - if (pos >= 0) { + super(); + 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; + this.domElement.style.touchAction = 'none'; // disable touch scroll + // Set to false to disable this control - matParams.bumpScale = parseFloat(items[pos + 1]); - items.splice(pos, 2); + this.enabled = true; // "target" sets the location of focus, where the object orbits around - } + this.target = new THREE.Vector3(); // How far you can dolly in and out ( PerspectiveCamera only ) - pos = items.indexOf('-s'); + this.minDistance = 0; + this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only ) - if (pos >= 0) { + this.minZoom = 0; + this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. - texParams.scale.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); - items.splice(pos, 4); // we expect 3 parameters here! + this.minPolarAngle = 0; // radians - } + this.maxPolarAngle = Math.PI; // radians + // How far you can orbit horizontally, upper and lower limits. + // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) - pos = items.indexOf('-o'); + this.minAzimuthAngle = - Infinity; // radians - if (pos >= 0) { + this.maxAzimuthAngle = Infinity; // radians + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop - texParams.offset.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); - items.splice(pos, 4); // we expect 3 parameters here! + 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 - texParams.url = items.join(' ').trim(); - return texParams; + this.enableRotate = true; + this.rotateSpeed = 1.0; // Set to false to disable panning - } + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up - loadTexture(url, mapping, onLoad, onProgress, onError) { + this.keyPanSpeed = 7.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 - const manager = (this.manager !== undefined) ? this.manager : THREE.DefaultLoadingManager; - let loader = manager.getHandler(url); + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 + // The four arrow keys - if (loader === null) { + this.keys = { + LEFT: 'ArrowLeft', + UP: 'ArrowUp', + RIGHT: 'ArrowRight', + BOTTOM: 'ArrowDown' + }; // Mouse buttons - loader = new THREE.TextureLoader(manager); + this.mouseButtons = { + LEFT: THREE.MOUSE.ROTATE, + MIDDLE: THREE.MOUSE.DOLLY, + RIGHT: THREE.MOUSE.PAN + }; // Touch fingers - } + this.touches = { + ONE: THREE.TOUCH.ROTATE, + TWO: THREE.TOUCH.DOLLY_PAN + }; // for reset - if (loader.setCrossOrigin) loader.setCrossOrigin(this.crossOrigin); + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; // the target DOM element for key events - const texture = loader.load(url, onLoad, onProgress, onError); + this._domElementKeyEvents = null; // + // public methods + // - if (mapping !== undefined) texture.mapping = mapping; + this.getPolarAngle = function () { - return texture; + return spherical.phi; - } + }; -} + this.getAzimuthalAngle = function () { -module.exports = {MTLLoader: MTLLoader}; + return spherical.theta; -},{}],3:[function(require,module,exports){ -window.THREE.OBJLoader = require("./OBJLoader").OBJLoader; + }; -},{"./OBJLoader":4}],4:[function(require,module,exports){ -// o object_name | g group_name -const _object_pattern = /^[og]\s*(.+)?/; -// mtllib file_reference -const _material_library_pattern = /^mtllib /; -// usemtl material_name -const _material_use_pattern = /^usemtl /; -// usemap map_name -const _map_use_pattern = /^usemap /; + this.getDistance = function () { -const _vA = new THREE.Vector3(); -const _vB = new THREE.Vector3(); -const _vC = new THREE.Vector3(); + return this.object.position.distanceTo( this.target ); -const _ab = new THREE.Vector3(); -const _cb = new THREE.Vector3(); + }; -function ParserState() { + this.listenToKeyEvents = function ( domElement ) { - const state = { - objects: [], - object: {}, + domElement.addEventListener( 'keydown', onKeyDown ); + this._domElementKeyEvents = domElement; - vertices: [], - normals: [], - colors: [], - uvs: [], + }; - materials: {}, - materialLibraries: [], + this.saveState = function () { - startObject: function (name, fromDeclaration) { + scope.target0.copy( scope.target ); + scope.position0.copy( scope.object.position ); + scope.zoom0 = scope.object.zoom; - // If the current object (initial from reset) is not from a g/o declaration in the parsed - // file. We need to use it for the first parsed g/o to keep things in sync. - if (this.object && this.object.fromDeclaration === false) { + }; - this.object.name = name; - this.object.fromDeclaration = (fromDeclaration !== false); - return; + 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; - const previousMaterial = (this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined); + }; // this method is exposed, but perhaps it would be better if we can make it private... - if (this.object && typeof this.object._finalize === 'function') { - this.object._finalize(true); + this.update = function () { - } + const offset = new THREE.Vector3(); // so camera.up is the orbit axis - this.object = { - name: name || '', - fromDeclaration: (fromDeclaration !== false), + const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); + const quatInverse = quat.clone().invert(); + const lastPosition = new THREE.Vector3(); + const lastQuaternion = new THREE.Quaternion(); + const twoPI = 2 * Math.PI; + return function update() { - geometry: { - vertices: [], - normals: [], - colors: [], - uvs: [], - hasUVIndices: false - }, - materials: [], - smooth: true, + const position = scope.object.position; + offset.copy( position ).sub( scope.target ); // rotate offset to "y-axis-is-up" space - startMaterial: function (name, libraries) { + offset.applyQuaternion( quat ); // angle from z-axis around y-axis - const previous = this._finalize(false); + spherical.setFromVector3( offset ); - // New usemtl declaration overwrites an inherited material, except if faces were declared - // after the material, then it must be preserved for proper MultiMaterial continuation. - if (previous && (previous.inherited || previous.groupCount <= 0)) { + if ( scope.autoRotate && state === STATE.NONE ) { - this.materials.splice(previous.index, 1); + rotateLeft( getAutoRotationAngle() ); } - const material = { - index: this.materials.length, - name: name || '', - mtllib: (Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : ''), - smooth: (previous !== undefined ? previous.smooth : this.smooth), - groupStart: (previous !== undefined ? previous.groupEnd : 0), - groupEnd: -1, - groupCount: -1, - inherited: false, - - clone: function (index) { - - const cloned = { - index: (typeof index === 'number' ? index : this.index), - name: this.name, - mtllib: this.mtllib, - smooth: this.smooth, - groupStart: 0, - groupEnd: -1, - groupCount: -1, - inherited: false - }; - cloned.clone = this.clone.bind(cloned); - return cloned; + if ( scope.enableDamping ) { - } - }; + spherical.theta += sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; - this.materials.push(material); + } else { - return material; + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; - }, + } // restrict theta to be between desired limits - currentMaterial: function () { - if (this.materials.length > 0) { + let min = scope.minAzimuthAngle; + let max = scope.maxAzimuthAngle; - return this.materials[this.materials.length - 1]; + 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; - return undefined; + if ( min <= max ) { - }, + spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); + + } else { + + spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, 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 - _finalize: function (end) { + spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); // move target to panned location - const lastMultiMaterial = this.currentMaterial(); - if (lastMultiMaterial && lastMultiMaterial.groupEnd === -1) { + if ( scope.enableDamping === true ) { - lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; - lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; - lastMultiMaterial.inherited = false; + scope.target.addScaledVector( panOffset, scope.dampingFactor ); + + } else { + + scope.target.add( panOffset ); } - // Ignore objects tail materials if no face declarations followed them before a new o/g started. - if (end && this.materials.length > 1) { + offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space - for (let mi = this.materials.length - 1; mi >= 0; mi--) { + offset.applyQuaternion( quatInverse ); + position.copy( scope.target ).add( offset ); + scope.object.lookAt( scope.target ); - if (this.materials[mi].groupCount <= 0) { + if ( scope.enableDamping === true ) { - this.materials.splice(mi, 1); + 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 ); } - // Guarantee at least one empty material, this makes the creation later more straight forward. - if (end && this.materials.length === 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 - this.materials.push({ - name: '', - smooth: this.smooth - }); + 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 lastMultiMaterial; + return false; - } - }; + }; - // Inherit previous objects material. - // Spec tells us that a declared material must be set to all objects until a new material is declared. - // If a usemtl declaration is encountered while this new object is being parsed, it will - // overwrite the inherited material. Exception being that there was already face declarations - // to the inherited material, then it will be preserved for proper MultiMaterial continuation. + }(); - if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function') { + this.dispose = function () { - const declared = previousMaterial.clone(0); - declared.inherited = true; - this.object.materials.push(declared); + scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); + scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); + scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); + scope.domElement.removeEventListener( 'wheel', onMouseWheel ); + scope.domElement.removeEventListener( 'pointermove', onPointerMove ); + scope.domElement.removeEventListener( 'pointerup', onPointerUp ); - } + if ( scope._domElementKeyEvents !== null ) { - this.objects.push(this.object); + scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); - }, + } //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? - finalize: function () { + }; // + // internals + // - if (this.object && typeof this.object._finalize === 'function') { - this.object._finalize(true); + const scope = this; + const STATE = { + NONE: - 1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6 + }; + let state = STATE.NONE; + const EPS = 0.000001; // current position in spherical coordinates + + const spherical = new THREE.Spherical(); + const sphericalDelta = new THREE.Spherical(); + let scale = 1; + const panOffset = new THREE.Vector3(); + let zoomChanged = false; + const rotateStart = new THREE.Vector2(); + const rotateEnd = new THREE.Vector2(); + const rotateDelta = new THREE.Vector2(); + const panStart = new THREE.Vector2(); + const panEnd = new THREE.Vector2(); + const panDelta = new THREE.Vector2(); + const dollyStart = new THREE.Vector2(); + const dollyEnd = new THREE.Vector2(); + const dollyDelta = new THREE.Vector2(); + const pointers = []; + const pointerPositions = {}; + + function getAutoRotationAngle() { + + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; } - }, + function getZoomScale() { + + return Math.pow( 0.95, scope.zoomSpeed ); - parseVertexIndex: function (value, len) { + } - const index = parseInt(value, 10); - return (index >= 0 ? index - 1 : index + len / 3) * 3; + function rotateLeft( angle ) { - }, + sphericalDelta.theta -= angle; - parseNormalIndex: function (value, len) { + } - const index = parseInt(value, 10); - return (index >= 0 ? index - 1 : index + len / 3) * 3; + function rotateUp( angle ) { - }, + sphericalDelta.phi -= angle; - parseUVIndex: function (value, len) { + } - const index = parseInt(value, 10); - return (index >= 0 ? index - 1 : index + len / 2) * 2; + const panLeft = function () { - }, + const v = new THREE.Vector3(); + return function panLeft( distance, objectMatrix ) { - addVertex: function (a, b, c) { + v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix - const src = this.vertices; - const dst = this.object.geometry.vertices; + v.multiplyScalar( - distance ); + panOffset.add( v ); - dst.push(src[a + 0], src[a + 1], src[a + 2]); - dst.push(src[b + 0], src[b + 1], src[b + 2]); - dst.push(src[c + 0], src[c + 1], src[c + 2]); + }; - }, + }(); - addVertexPoint: function (a) { + const panUp = function () { - const src = this.vertices; - const dst = this.object.geometry.vertices; + const v = new THREE.Vector3(); + return function panUp( distance, objectMatrix ) { - dst.push(src[a + 0], src[a + 1], src[a + 2]); + if ( scope.screenSpacePanning === true ) { - }, + v.setFromMatrixColumn( objectMatrix, 1 ); - addVertexLine: function (a) { + } else { - const src = this.vertices; - const dst = this.object.geometry.vertices; + v.setFromMatrixColumn( objectMatrix, 0 ); + v.crossVectors( scope.object.up, v ); - dst.push(src[a + 0], src[a + 1], src[a + 2]); + } - }, + v.multiplyScalar( distance ); + panOffset.add( v ); - addNormal: function (a, b, c) { + }; - const src = this.normals; - const dst = this.object.geometry.normals; + }(); // deltaX and deltaY are in pixels; right and down are positive - dst.push(src[a + 0], src[a + 1], src[a + 2]); - dst.push(src[b + 0], src[b + 1], src[b + 2]); - dst.push(src[c + 0], src[c + 1], src[c + 2]); - }, + const pan = function () { - addFaceNormal: function (a, b, c) { + const offset = new THREE.Vector3(); + return function pan( deltaX, deltaY ) { - const src = this.vertices; - const dst = this.object.geometry.normals; + const element = scope.domElement; - _vA.fromArray(src, a); - _vB.fromArray(src, b); - _vC.fromArray(src, c); + if ( scope.object.isPerspectiveCamera ) { - _cb.subVectors(_vC, _vB); - _ab.subVectors(_vA, _vB); - _cb.cross(_ab); + // perspective + const position = scope.object.position; + offset.copy( position ).sub( scope.target ); + let targetDistance = offset.length(); // half of the fov is center to top of screen - _cb.normalize(); + targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed - dst.push(_cb.x, _cb.y, _cb.z); - dst.push(_cb.x, _cb.y, _cb.z); - dst.push(_cb.x, _cb.y, _cb.z); + panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); + panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); - }, + } else if ( scope.object.isOrthographicCamera ) { - addColor: function (a, b, c) { + // 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 ); - const src = this.colors; - const dst = this.object.geometry.colors; + } else { - if (src[a] !== undefined) dst.push(src[a + 0], src[a + 1], src[a + 2]); - if (src[b] !== undefined) dst.push(src[b + 0], src[b + 1], src[b + 2]); - if (src[c] !== undefined) dst.push(src[c + 0], src[c + 1], src[c + 2]); + // camera neither orthographic nor perspective + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); + scope.enablePan = false; - }, + } - addUV: function (a, b, c) { + }; - const src = this.uvs; - const dst = this.object.geometry.uvs; + }(); - dst.push(src[a + 0], src[a + 1]); - dst.push(src[b + 0], src[b + 1]); - dst.push(src[c + 0], src[c + 1]); + function dollyOut( dollyScale ) { - }, + if ( scope.object.isPerspectiveCamera ) { - addDefaultUV: function () { + scale /= dollyScale; - const dst = this.object.geometry.uvs; + } else if ( scope.object.isOrthographicCamera ) { - dst.push(0, 0); - dst.push(0, 0); - dst.push(0, 0); + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; - }, + } else { - addUVLine: function (a) { + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; - const src = this.uvs; - const dst = this.object.geometry.uvs; + } - dst.push(src[a + 0], src[a + 1]); + } - }, + function dollyIn( dollyScale ) { - addFace: function (a, b, c, ua, ub, uc, na, nb, nc) { + if ( scope.object.isPerspectiveCamera ) { - const vLen = this.vertices.length; + scale *= dollyScale; - let ia = this.parseVertexIndex(a, vLen); - let ib = this.parseVertexIndex(b, vLen); - let ic = this.parseVertexIndex(c, vLen); + } else if ( scope.object.isOrthographicCamera ) { - this.addVertex(ia, ib, ic); - this.addColor(ia, ib, ic); + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; - // normals + } else { - if (na !== undefined && na !== '') { + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; - const nLen = this.normals.length; + } - ia = this.parseNormalIndex(na, nLen); - ib = this.parseNormalIndex(nb, nLen); - ic = this.parseNormalIndex(nc, nLen); + } // + // event callbacks - update the object state + // - this.addNormal(ia, ib, ic); - } else { + function handleMouseDownRotate( event ) { - this.addFaceNormal(ia, ib, ic); + rotateStart.set( event.clientX, event.clientY ); } - // uvs + function handleMouseDownDolly( event ) { - if (ua !== undefined && ua !== '') { + dollyStart.set( event.clientX, event.clientY ); - const uvLen = this.uvs.length; + } - ia = this.parseUVIndex(ua, uvLen); - ib = this.parseUVIndex(ub, uvLen); - ic = this.parseUVIndex(uc, uvLen); + function handleMouseDownPan( event ) { - this.addUV(ia, ib, ic); + panStart.set( event.clientX, event.clientY ); - this.object.geometry.hasUVIndices = true; + } - } else { + function handleMouseMoveRotate( event ) { - // add placeholder values (for inconsistent face definitions) + rotateEnd.set( event.clientX, event.clientY ); + rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); + const element = scope.domElement; + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height - this.addDefaultUV(); + 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 ); - addPointGeometry: function (vertices) { + if ( dollyDelta.y > 0 ) { - this.object.geometry.type = 'THREE.Points'; + dollyOut( getZoomScale() ); - const vLen = this.vertices.length; + } else if ( dollyDelta.y < 0 ) { - for (let vi = 0, l = vertices.length; vi < l; vi++) { + dollyIn( getZoomScale() ); - const index = this.parseVertexIndex(vertices[vi], vLen); + } - this.addVertexPoint(index); - this.addColor(index); + 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(); - addLineGeometry: function (vertices, uvs) { + } - this.object.geometry.type = 'Line'; + function handleMouseWheel( event ) { - const vLen = this.vertices.length; - const uvLen = this.uvs.length; + if ( event.deltaY < 0 ) { - for (let vi = 0, l = vertices.length; vi < l; vi++) { + dollyIn( getZoomScale() ); - this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen)); + } else if ( event.deltaY > 0 ) { - } + dollyOut( getZoomScale() ); - for (let uvi = 0, l = uvs.length; uvi < l; uvi++) { + } - this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen)); + scope.update(); } - } + function handleKeyDown( event ) { - }; + let needsUpdate = false; - state.startObject('', false); + switch ( event.code ) { - return state; + case scope.keys.UP: + pan( 0, scope.keyPanSpeed ); + needsUpdate = true; + break; -} + case scope.keys.BOTTOM: + pan( 0, - scope.keyPanSpeed ); + needsUpdate = true; + break; -// + case scope.keys.LEFT: + pan( scope.keyPanSpeed, 0 ); + needsUpdate = true; + break; -class OBJLoader extends THREE.Loader { + case scope.keys.RIGHT: + pan( - scope.keyPanSpeed, 0 ); + needsUpdate = true; + break; - constructor(manager) { + } - super(manager); + if ( needsUpdate ) { - this.materials = null; + // prevent the browser from scrolling on cursor keys + event.preventDefault(); + scope.update(); - } + } - load(url, onLoad, onProgress, onError) { + } + + function handleTouchStartRotate() { + + if ( pointers.length === 1 ) { + + rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); - const scope = this; + } else { - const loader = new THREE.FileLoader(this.manager); - loader.setPath(this.path); - loader.setRequestHeader(this.requestHeader); - loader.setWithCredentials(this.withCredentials); - loader.load(url, function (text) { + const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); + const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); + rotateStart.set( x, y ); - try { + } - onLoad(scope.parse(text)); + } - } catch (e) { + function handleTouchStartPan() { - if (onError) { + if ( pointers.length === 1 ) { - onError(e); + panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); } else { - console.error(e); + const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); + const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); + panStart.set( x, y ); } - scope.manager.itemError(url); - } - }, onProgress, onError); + function handleTouchStartDolly() { - } - - setMaterials(materials) { + const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; + const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; + const distance = Math.sqrt( dx * dx + dy * dy ); + dollyStart.set( 0, distance ); - this.materials = materials; + } - return this; + function handleTouchStartDollyPan() { - } + if ( scope.enableZoom ) handleTouchStartDolly(); + if ( scope.enablePan ) handleTouchStartPan(); - parse(text) { + } - const state = new ParserState(); + function handleTouchStartDollyRotate() { - if (text.indexOf('\r\n') !== -1) { + if ( scope.enableZoom ) handleTouchStartDolly(); + if ( scope.enableRotate ) handleTouchStartRotate(); - // This is faster than String.split with regex that splits on both - text = text.replace(/\r\n/g, '\n'); + } - } + function handleTouchMoveRotate( event ) { - if (text.indexOf('\\\n') !== -1) { + if ( pointers.length == 1 ) { - // join lines separated by a line continuation character (\) - text = text.replace(/\\\n/g, ''); + rotateEnd.set( event.pageX, event.pageY ); - } + } else { - const lines = text.split('\n'); - let line = '', lineFirstChar = ''; - let lineLength = 0; - let result = []; + const position = getSecondPointerPosition( event ); + const x = 0.5 * ( event.pageX + position.x ); + const y = 0.5 * ( event.pageY + position.y ); + rotateEnd.set( x, y ); - // Faster to just trim left side of the line. Use if available. - const trimLeft = (typeof ''.trimLeft === 'function'); + } - for (let i = 0, l = lines.length; i < l; i++) { + rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); + const element = scope.domElement; + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height - line = lines[i]; + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); + rotateStart.copy( rotateEnd ); - line = trimLeft ? line.trimLeft() : line.trim(); + } - lineLength = line.length; + function handleTouchMovePan( event ) { - if (lineLength === 0) continue; + if ( pointers.length === 1 ) { - lineFirstChar = line.charAt(0); + panEnd.set( event.pageX, event.pageY ); - // @todo invoke passed in handler if any - if (lineFirstChar === '#') continue; + } else { - if (lineFirstChar === 'v') { + const position = getSecondPointerPosition( event ); + const x = 0.5 * ( event.pageX + position.x ); + const y = 0.5 * ( event.pageY + position.y ); + panEnd.set( x, y ); - const data = line.split(/\s+/); + } - switch (data[0]) { + panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); + pan( panDelta.x, panDelta.y ); + panStart.copy( panEnd ); - case 'v': - state.vertices.push( - parseFloat(data[1]), - parseFloat(data[2]), - parseFloat(data[3]) - ); - if (data.length >= 7) { + } - state.colors.push( - parseFloat(data[4]), - parseFloat(data[5]), - parseFloat(data[6]) - ); + function handleTouchMoveDolly( event ) { - } else { + const position = getSecondPointerPosition( event ); + const dx = event.pageX - position.x; + const dy = event.pageY - position.y; + const distance = Math.sqrt( dx * dx + dy * dy ); + dollyEnd.set( 0, distance ); + dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); + dollyOut( dollyDelta.y ); + dollyStart.copy( dollyEnd ); - // if no colors are defined, add placeholders so color and vertex indices match + } - state.colors.push(undefined, undefined, undefined); + function handleTouchMoveDollyPan( event ) { - } + if ( scope.enableZoom ) handleTouchMoveDolly( event ); + if ( scope.enablePan ) handleTouchMovePan( event ); - break; - case 'vn': - state.normals.push( - parseFloat(data[1]), - parseFloat(data[2]), - parseFloat(data[3]) - ); - break; - case 'vt': - state.uvs.push( - parseFloat(data[1]), - parseFloat(data[2]) - ); - break; + } - } + function handleTouchMoveDollyRotate( event ) { - } else if (lineFirstChar === 'f') { + if ( scope.enableZoom ) handleTouchMoveDolly( event ); + if ( scope.enableRotate ) handleTouchMoveRotate( event ); - const lineData = line.substr(1).trim(); - const vertexData = lineData.split(/\s+/); - const faceVertices = []; + } // + // event handlers - FSM: listen for events and reset state + // - // Parse the face vertex data into an easy to work with format - for (let j = 0, jl = vertexData.length; j < jl; j++) { + function onPointerDown( event ) { - const vertex = vertexData[j]; + if ( scope.enabled === false ) return; - if (vertex.length > 0) { + if ( pointers.length === 0 ) { - const vertexParts = vertex.split('/'); - faceVertices.push(vertexParts); + scope.domElement.setPointerCapture( event.pointerId ); + scope.domElement.addEventListener( 'pointermove', onPointerMove ); + scope.domElement.addEventListener( 'pointerup', onPointerUp ); - } + } // - } - // Draw an edge between the first vertex and all subsequent vertices to form an n-gon + addPointer( event ); - const v1 = faceVertices[0]; + if ( event.pointerType === 'touch' ) { - for (let j = 1, jl = faceVertices.length - 1; j < jl; j++) { + onTouchStart( event ); - const v2 = faceVertices[j]; - const v3 = faceVertices[j + 1]; + } else { - state.addFace( - v1[0], v2[0], v3[0], - v1[1], v2[1], v3[1], - v1[2], v2[2], v3[2] - ); + onMouseDown( event ); } - } else if (lineFirstChar === 'l') { + } + + function onPointerMove( event ) { - const lineParts = line.substring(1).trim().split(' '); - let lineVertices = []; - const lineUVs = []; + if ( scope.enabled === false ) return; - if (line.indexOf('/') === -1) { + if ( event.pointerType === 'touch' ) { - lineVertices = lineParts; + onTouchMove( event ); } else { - for (let li = 0, llen = lineParts.length; li < llen; li++) { + onMouseMove( event ); + + } + + } - const parts = lineParts[li].split('/'); + function onPointerUp( event ) { - if (parts[0] !== '') lineVertices.push(parts[0]); - if (parts[1] !== '') lineUVs.push(parts[1]); + removePointer( event ); - } + if ( pointers.length === 0 ) { + + scope.domElement.releasePointerCapture( event.pointerId ); + scope.domElement.removeEventListener( 'pointermove', onPointerMove ); + scope.domElement.removeEventListener( 'pointerup', onPointerUp ); } - state.addLineGeometry(lineVertices, lineUVs); + scope.dispatchEvent( _endEvent ); + state = STATE.NONE; + + } + + function onPointerCancel( event ) { - } else if (lineFirstChar === 'p') { + removePointer( event ); - const lineData = line.substr(1).trim(); - const pointData = lineData.split(' '); + } + + function onMouseDown( event ) { - state.addPointGeometry(pointData); + let mouseAction; - } else if ((result = _object_pattern.exec(line)) !== null) { + switch ( event.button ) { - // o object_name - // or - // g group_name + case 0: + mouseAction = scope.mouseButtons.LEFT; + break; - // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 - // let name = result[ 0 ].substr( 1 ).trim(); - const name = (' ' + result[0].substr(1).trim()).substr(1); + case 1: + mouseAction = scope.mouseButtons.MIDDLE; + break; - state.startObject(name); + case 2: + mouseAction = scope.mouseButtons.RIGHT; + break; - } else if (_material_use_pattern.test(line)) { + default: + mouseAction = - 1; - // material + } - state.object.startMaterial(line.substring(7).trim(), state.materialLibraries); + switch ( mouseAction ) { - } else if (_material_library_pattern.test(line)) { + case THREE.MOUSE.DOLLY: + if ( scope.enableZoom === false ) return; + handleMouseDownDolly( event ); + state = STATE.DOLLY; + break; - // mtl file + case THREE.MOUSE.ROTATE: + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - state.materialLibraries.push(line.substring(7).trim()); + if ( scope.enablePan === false ) return; + handleMouseDownPan( event ); + state = STATE.PAN; - } else if (_map_use_pattern.test(line)) { + } else { - // the line is parsed but ignored since the loader assumes textures are defined MTL files - // (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method) + if ( scope.enableRotate === false ) return; + handleMouseDownRotate( event ); + state = STATE.ROTATE; - console.warn('THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.'); + } - } else if (lineFirstChar === 's') { + break; - result = line.split(' '); + case THREE.MOUSE.PAN: + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - // smooth shading + if ( scope.enableRotate === false ) return; + handleMouseDownRotate( event ); + state = STATE.ROTATE; - // @todo Handle files that have varying smooth values for a set of faces inside one geometry, - // but does not define a usemtl for each face set. - // This should be detected and a dummy material created (later MultiMaterial and geometry groups). - // This requires some care to not create extra material on each smooth value for "normal" obj files. - // where explicit usemtl defines geometry groups. - // Example asset: examples/models/obj/cerberus/Cerberus.obj + } else { - /* - * http://paulbourke.net/dataformats/obj/ - * or - * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf - * - * From chapter "Grouping" Syntax explanation "s group_number": - * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off. - * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form - * surfaces, smoothing groups are either turned on or off; there is no difference between values greater - * than 0." - */ - if (result.length > 1) { + if ( scope.enablePan === false ) return; + handleMouseDownPan( event ); + state = STATE.PAN; - const value = result[1].trim().toLowerCase(); - state.object.smooth = (value !== '0' && value !== 'off'); + } - } else { + break; - // ZBrush can produce "s" lines #11707 - state.object.smooth = true; + default: + state = STATE.NONE; } - const material = state.object.currentMaterial(); - if (material) material.smooth = state.object.smooth; + if ( state !== STATE.NONE ) { - } else { - - // Handle null terminated files without exception - if (line === '\0') continue; + scope.dispatchEvent( _startEvent ); - console.warn('THREE.OBJLoader: Unexpected line: "' + line + '"'); + } } - } + function onMouseMove( event ) { - state.finalize(); + if ( scope.enabled === false ) return; - const container = new THREE.Group(); - container.materialLibraries = [].concat(state.materialLibraries); + switch ( state ) { - const hasPrimitives = !(state.objects.length === 1 && state.objects[0].geometry.vertices.length === 0); + case STATE.ROTATE: + if ( scope.enableRotate === false ) return; + handleMouseMoveRotate( event ); + break; - if (hasPrimitives === true) { + case STATE.DOLLY: + if ( scope.enableZoom === false ) return; + handleMouseMoveDolly( event ); + break; - for (let i = 0, l = state.objects.length; i < l; i++) { + case STATE.PAN: + if ( scope.enablePan === false ) return; + handleMouseMovePan( event ); + break; - const object = state.objects[i]; - const geometry = object.geometry; - const materials = object.materials; - const isLine = (geometry.type === 'Line'); - const isPoints = (geometry.type === 'THREE.Points'); - let hasVertexColors = false; + } - // Skip o/g line declarations that did not follow with any faces - if (geometry.vertices.length === 0) continue; + } - const buffergeometry = new THREE.BufferGeometry(); + function onMouseWheel( event ) { - buffergeometry.setAttribute('position', new THREE.Float32BufferAttribute(geometry.vertices, 3)); + if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; + event.preventDefault(); + scope.dispatchEvent( _startEvent ); + handleMouseWheel( event ); + scope.dispatchEvent( _endEvent ); - if (geometry.normals.length > 0) { + } - buffergeometry.setAttribute('normal', new THREE.Float32BufferAttribute(geometry.normals, 3)); + function onKeyDown( event ) { - } + if ( scope.enabled === false || scope.enablePan === false ) return; + handleKeyDown( event ); - if (geometry.colors.length > 0) { + } - hasVertexColors = true; - buffergeometry.setAttribute('color', new THREE.Float32BufferAttribute(geometry.colors, 3)); + function onTouchStart( event ) { - } + trackPointer( event ); - if (geometry.hasUVIndices === true) { + switch ( pointers.length ) { - buffergeometry.setAttribute('uv', new THREE.Float32BufferAttribute(geometry.uvs, 2)); + case 1: + switch ( scope.touches.ONE ) { - } + case THREE.TOUCH.ROTATE: + if ( scope.enableRotate === false ) return; + handleTouchStartRotate(); + state = STATE.TOUCH_ROTATE; + break; - // Create materials + case THREE.TOUCH.PAN: + if ( scope.enablePan === false ) return; + handleTouchStartPan(); + state = STATE.TOUCH_PAN; + break; - const createdMaterials = []; + default: + state = STATE.NONE; - for (let mi = 0, miLen = materials.length; mi < miLen; mi++) { + } - const sourceMaterial = materials[mi]; - const materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors; - let material = state.materials[materialHash]; + break; - if (this.materials !== null) { + case 2: + switch ( scope.touches.TWO ) { - material = this.materials.create(sourceMaterial.name); + case THREE.TOUCH.DOLLY_PAN: + if ( scope.enableZoom === false && scope.enablePan === false ) return; + handleTouchStartDollyPan(); + state = STATE.TOUCH_DOLLY_PAN; + break; - // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. - if (isLine && material && !(material instanceof THREE.LineBasicMaterial)) { + case THREE.TOUCH.DOLLY_ROTATE: + if ( scope.enableZoom === false && scope.enableRotate === false ) return; + handleTouchStartDollyRotate(); + state = STATE.TOUCH_DOLLY_ROTATE; + break; - const materialLine = new THREE.LineBasicMaterial(); - THREE.Material.prototype.copy.call(materialLine, material); - materialLine.color.copy(material.color); - material = materialLine; + default: + state = STATE.NONE; - } else if (isPoints && material && !(material instanceof THREE.PointsMaterial)) { + } - const materialPoints = new THREE.PointsMaterial({size: 10, sizeAttenuation: false}); - THREE.Material.prototype.copy.call(materialPoints, material); - materialPoints.color.copy(material.color); - materialPoints.map = material.map; - material = materialPoints; + break; - } + default: + state = STATE.NONE; - } + } - if (material === undefined) { + if ( state !== STATE.NONE ) { - if (isLine) { + scope.dispatchEvent( _startEvent ); - material = new THREE.LineBasicMaterial(); + } - } else if (isPoints) { + } - material = new THREE.PointsMaterial({size: 1, sizeAttenuation: false}); + function onTouchMove( event ) { - } else { + trackPointer( event ); - material = new THREE.MeshPhongMaterial(); + switch ( state ) { - } + case STATE.TOUCH_ROTATE: + if ( scope.enableRotate === false ) return; + handleTouchMoveRotate( event ); + scope.update(); + break; - material.name = sourceMaterial.name; - material.flatShading = sourceMaterial.smooth ? false : true; - material.vertexColors = hasVertexColors; + case STATE.TOUCH_PAN: + if ( scope.enablePan === false ) return; + handleTouchMovePan( event ); + scope.update(); + break; - state.materials[materialHash] = material; + 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; - createdMaterials.push(material); + default: + state = STATE.NONE; } - // Create mesh + } - let mesh; + function onContextMenu( event ) { - if (createdMaterials.length > 1) { + if ( scope.enabled === false ) return; + event.preventDefault(); - for (let mi = 0, miLen = materials.length; mi < miLen; mi++) { + } - const sourceMaterial = materials[mi]; - buffergeometry.addGroup(sourceMaterial.groupStart, sourceMaterial.groupCount, mi); + function addPointer( event ) { - } + pointers.push( event ); - if (isLine) { + } - mesh = new THREE.LineSegments(buffergeometry, createdMaterials); + function removePointer( event ) { - } else if (isPoints) { + delete pointerPositions[ event.pointerId ]; - mesh = new THREE.Points(buffergeometry, createdMaterials); + for ( let i = 0; i < pointers.length; i ++ ) { - } else { + if ( pointers[ i ].pointerId == event.pointerId ) { - mesh = new THREE.Mesh(buffergeometry, createdMaterials); + pointers.splice( i, 1 ); + return; } - } else { - - if (isLine) { - - mesh = new THREE.LineSegments(buffergeometry, createdMaterials[0]); + } - } else if (isPoints) { + } - mesh = new THREE.Points(buffergeometry, createdMaterials[0]); + function trackPointer( event ) { - } else { + let position = pointerPositions[ event.pointerId ]; - mesh = new THREE.Mesh(buffergeometry, createdMaterials[0]); + if ( position === undefined ) { - } + position = new THREE.Vector2(); + pointerPositions[ event.pointerId ] = position; } - mesh.name = object.name; - - container.add(mesh); + position.set( event.pageX, event.pageY ); } - } else { + function getSecondPointerPosition( event ) { - // if there is only the default parser state object with no geometry data, interpret data as point cloud + const pointer = event.pointerId === pointers[ 0 ].pointerId ? pointers[ 1 ] : pointers[ 0 ]; + return pointerPositions[ pointer.pointerId ]; - if (state.vertices.length > 0) { + } // - const material = new THREE.PointsMaterial({size: 1, sizeAttenuation: false}); - const buffergeometry = new THREE.BufferGeometry(); + scope.domElement.addEventListener( 'contextmenu', onContextMenu ); + scope.domElement.addEventListener( 'pointerdown', onPointerDown ); + scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); + scope.domElement.addEventListener( 'wheel', onMouseWheel, { + passive: false + } ); // force an update at start - buffergeometry.setAttribute('position', new THREE.Float32BufferAttribute(state.vertices, 3)); + this.update(); - if (state.colors.length > 0 && state.colors[0] !== undefined) { + } - buffergeometry.setAttribute('color', new THREE.Float32BufferAttribute(state.colors, 3)); - material.vertexColors = true; + } // 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 - } - const points = new THREE.Points(buffergeometry, material); - container.add(points); + class MapControls extends OrbitControls { - } + constructor( object, domElement ) { - } + super( object, domElement ); + this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up - return container; + this.mouseButtons.LEFT = THREE.MOUSE.PAN; + this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; + this.touches.ONE = THREE.TOUCH.PAN; + this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; - } + } -} + } -module.exports = {OBJLoader: OBJLoader}; + THREE.MapControls = MapControls; + THREE.OrbitControls = OrbitControls; -},{}]},{},[1,3]); +} )();