diff options
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | build.gradle | 47 | ||||
| -rw-r--r-- | gradle.properties | 1 | ||||
| -rw-r--r-- | gradle/wrapper/gradle-wrapper.properties | 6 | ||||
| -rwxr-xr-x | gradlew | 172 | ||||
| -rw-r--r-- | gradlew.bat | 84 | ||||
| -rw-r--r-- | settings.gradle | 11 | ||||
| -rw-r--r-- | src/main/kotlin/SimplexNoise.kt | 3 | ||||
| -rw-r--r-- | src/main/kotlin/Terrain.kt | 230 | ||||
| -rw-r--r-- | src/main/resources/deps/OrbitControls.js | 1042 | ||||
| -rw-r--r-- | src/main/resources/deps/dat.gui.min.js | 2 | ||||
| -rw-r--r-- | src/main/resources/deps/simplex-noise.min.js | 1 | ||||
| -rw-r--r-- | src/main/resources/deps/three.min.js | 898 | ||||
| -rw-r--r-- | src/main/resources/index.html | 31 |
14 files changed, 2533 insertions, 0 deletions
@@ -21,3 +21,8 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +web/ +build/ +.idea +.gradle
\ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..462af51 --- /dev/null +++ b/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'kotlin2js' version '1.3.21' +} + +group 'io.jamesbarnett' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-js" + compile "com.github.markaren:three.kt:v0.88-ALPHA-7" + testImplementation "org.jetbrains.kotlin:kotlin-test-js" +} + +task assembleWeb(type: Sync) { + configurations.compile.each { File file -> + from(zipTree(file.absolutePath), { + includeEmptyDirs = false + include { fileTreeElement -> + def path = fileTreeElement.path + path.endsWith(".js") && (path.startsWith("META-INF/resources/") || + !path.startsWith("META-INF/")) + } + }) + } + + from compileKotlin2Js.destinationDir + into "${projectDir}/web/kotlin" + + dependsOn classes +} +assemble.dependsOn assembleWeb + +task copyWebResources(type: Sync) { + from "${projectDir}/src/main/resources" + into "${projectDir}/web" +} + +assembleWeb.dependsOn copyWebResources + +clean.doFirst() { + delete("${projectDir}/web") +}
\ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official
\ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3e88002 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jul 13 19:05:25 BST 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..20189e5 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.id == "kotlin2js") { + useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") + } + } + } +} +rootProject.name = 'terrain' + diff --git a/src/main/kotlin/SimplexNoise.kt b/src/main/kotlin/SimplexNoise.kt new file mode 100644 index 0000000..2a984e5 --- /dev/null +++ b/src/main/kotlin/SimplexNoise.kt @@ -0,0 +1,3 @@ +external class SimplexNoise { + fun noise2D(x: Double, y: Double) : Double +}
\ No newline at end of file diff --git a/src/main/kotlin/Terrain.kt b/src/main/kotlin/Terrain.kt new file mode 100644 index 0000000..4f854ab --- /dev/null +++ b/src/main/kotlin/Terrain.kt @@ -0,0 +1,230 @@ +import info.laht.threekt.THREE +import info.laht.threekt.cameras.PerspectiveCamera +import info.laht.threekt.external.controls.OrbitControls +import info.laht.threekt.external.libs.datgui.GUIParams +import info.laht.threekt.external.libs.datgui.NumberController +import info.laht.threekt.external.libs.datgui.dat +import info.laht.threekt.geometries.BoxGeometry +import info.laht.threekt.geometries.PlaneGeometry +import info.laht.threekt.lights.AmbientLight +import info.laht.threekt.lights.PointLight +import info.laht.threekt.materials.MeshBasicMaterial +import info.laht.threekt.materials.MeshPhongMaterial +import info.laht.threekt.math.ColorConstants +import info.laht.threekt.objects.Mesh +import info.laht.threekt.renderers.WebGLRenderer +import info.laht.threekt.renderers.WebGLRendererParams +import info.laht.threekt.scenes.Scene +import kotlin.browser.document +import kotlin.browser.window + +class Terrain { + + private val renderer: WebGLRenderer + private val scene: Scene = Scene() + private val camera: PerspectiveCamera + private val controls: OrbitControls + private lateinit var simplexNoise: SimplexNoise + private lateinit var terrainMesh: Mesh + private lateinit var waterMesh: Mesh + + private val size: Int = 256 + + val zoomFactor = 80 + val scalingFactor = 11 + val autoRotate = false + + val waterHeight = 5 + val snowHeightThreshold = 15 + + val showWireframe = false + + init { + + scene.add(AmbientLight(0xeeeeee)) + + PointLight(0xffffff) + .apply { + castShadow = true + position.set(0, 90, 200) + }.also(scene::add) + + camera = PerspectiveCamera(75, window.innerWidth.toDouble() / window.innerHeight, 0.1, 1000) + camera.position.setZ(45) + camera.position.setY(-73) + + renderer = WebGLRenderer(WebGLRendererParams(antialias = true)) + .apply { + setClearColor(ColorConstants.black, 1) + setSize(window.innerWidth, window.innerHeight) + } + + controls = OrbitControls(camera, renderer.domElement) + + initGui() + + seedNoise() + + generateTerrain() + + generateWater() + + window.addEventListener("resize", { + camera.aspect = window.innerWidth.toDouble() / window.innerHeight + camera.updateProjectionMatrix() + + renderer.setSize(window.innerWidth, window.innerHeight) + }, false) + + document.getElementById("container") + ?.apply { + appendChild(renderer.domElement) + } + + } + + private fun seedNoise() { + simplexNoise = js("new SimplexNoise()") as SimplexNoise + } + + fun reseedNoise() { + seedNoise() + generateTerrain() + } + + private fun generateTerrain() { + + if (this::terrainMesh.isInitialized) { + scene.remove(terrainMesh) + } + + val terrainGeom = PlaneGeometry(100,100, size-1, size-1) + + for (x in 0 until size) { + for (y in 0 until size) { + + if (x == 0 || x == size-1 || y == 0 || y == size-1) { + terrainGeom.vertices[x + (y*size)].setZ(0) + } else { + + var noise = simplexNoise.noise2D(x / zoomFactor.toDouble(), y / zoomFactor.toDouble()) + noise += (0.01 * simplexNoise.noise2D(x.toDouble(), y.toDouble())) + + noise += 1 + noise *= scalingFactor + terrainGeom.vertices[x + (y * size)].setZ(noise) + } + } + } + + applyHeightMapColour(terrainGeom) + + terrainGeom.computeFaceNormals() + terrainGeom.computeVertexNormals() + terrainGeom.colorsNeedUpdate = true + + val terrainMaterial = MeshPhongMaterial() + .apply { + if (showWireframe) { + wireframe = true + wireframeLinewidth = 1.0 + } + vertexColors = THREE.FaceColors + } + + terrainMesh = Mesh(terrainGeom, terrainMaterial) + .apply { receiveShadows = true } + .also(scene::add) + } + + private fun applyHeightMapColour(planeGeometry: PlaneGeometry) { + planeGeometry.faces.forEach { face -> + val vertexes = listOf(planeGeometry.vertices[face.a].z, planeGeometry.vertices[face.b].z, planeGeometry.vertices[face.c].z) + val min = vertexes.min()!! + + if (min < snowHeightThreshold) { + face.color?.set(ColorConstants.forestgreen) + } else if (min >= snowHeightThreshold) { + face.color?.set(ColorConstants.floralwhite) + } + } + } + + private fun generateWater() { + + if (this::waterMesh.isInitialized) { + scene.remove(waterMesh) + } + + val waterGeom = BoxGeometry(99, 99, waterHeight) + val waterMaterial = MeshBasicMaterial() + .apply { + color.set(ColorConstants.aqua) + transparent = true + opacity = 0.7 + } + + waterMesh = Mesh(waterGeom, waterMaterial) + .also { + scene.add(it) + it.translateZ(waterHeight/2) + } + } + + @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") + fun initGui() { + dat.GUI( + GUIParams( + closed = false + ) + ).also { + + (it.add(this, "zoomFactor") as NumberController).apply { + min(10) + .max(200) + .step(5) + .onChange { generateTerrain() } + } + + (it.add(this, "scalingFactor") as NumberController).apply { + min(0) + .max(50) + .step(1) + .onChange { generateTerrain() } + } + + (it.add(this, "waterHeight") as NumberController).apply { + min(0) + .max(30) + .step(1) + .onChange { generateWater() } + } + + (it.add(this, "snowHeightThreshold") as NumberController).apply { + min(0) + .max(30) + .step(1) + .onChange { generateTerrain() } + } + + it.add(this, "reseedNoise") + it.add(this, "showWireframe").onChange { generateTerrain() } + it.add(this, "autoRotate") + } + } + + fun animate() { + + window.requestAnimationFrame { + if (autoRotate) { + terrainMesh.rotation.z += 0.005 + waterMesh.rotation.z += 0.005 +// println("x:${camera.position.x} y: ${camera.position.y} z:${camera.position.z}") + } + animate() + } + + renderer.render(scene, camera) + } + +} diff --git a/src/main/resources/deps/OrbitControls.js b/src/main/resources/deps/OrbitControls.js new file mode 100644 index 0000000..6669341 --- /dev/null +++ b/src/main/resources/deps/OrbitControls.js @@ -0,0 +1,1042 @@ +/** + * @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.isPerspectiveCamera ) { + + // perspective + var position = scope.object.position; + offset.copy( position ).sub( scope.target ); + var targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); + + // we 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.isOrthographicCamera ) { + + // orthographic + panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); + panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); + + } else { + + // camera neither orthographic nor perspective + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); + scope.enablePan = false; + + } + + }; + + }(); + + function dollyIn( dollyScale ) { + + if ( scope.object.isPerspectiveCamera ) { + + scale /= dollyScale; + + } else if ( scope.object.isOrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + function dollyOut( dollyScale ) { + + if ( scope.object.isPerspectiveCamera ) { + + scale *= dollyScale; + + } else if ( scope.object.isOrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + // + // event callbacks - update the object state + // + + function handleMouseDownRotate( event ) { + + //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() ); + + |