From 9439447c7fc3e20b587c32c97cbe2621849c5d68 Mon Sep 17 00:00:00 2001
From: Sean Hickey <hisean@media.mit.edu>
Date: Fri, 10 Aug 2018 14:44:51 -0400
Subject: [PATCH] Updated pot module. Added profile module.

---
 js/three.js/three.min.js | 1051 ++++++++++++++++++++++++++++++++++++++
 modules/hardware/pot     |  213 +++++---
 modules/hardware/profile |  217 ++++++++
 3 files changed, 1399 insertions(+), 82 deletions(-)
 create mode 100644 modules/hardware/profile

diff --git a/js/three.js/three.min.js b/js/three.js/three.min.js
index a955ba1..473282c 100644
--- a/js/three.js/three.min.js
+++ b/js/three.js/three.min.js
@@ -991,3 +991,1054 @@ THREE.MorphBlendMesh.prototype.setAnimationDuration=function(a,b){var c=this.ani
 THREE.MorphBlendMesh.prototype.getAnimationDuration=function(a){var b=-1;if(a=this.animationsMap[a])b=a.duration;return b};THREE.MorphBlendMesh.prototype.playAnimation=function(a){var b=this.animationsMap[a];b?(b.time=0,b.active=!0):console.warn("THREE.MorphBlendMesh: animation["+a+"] undefined in .playAnimation()")};THREE.MorphBlendMesh.prototype.stopAnimation=function(a){if(a=this.animationsMap[a])a.active=!1};
 THREE.MorphBlendMesh.prototype.update=function(a){for(var b=0,c=this.animationsList.length;b<c;b++){var d=this.animationsList[b];if(d.active){var e=d.duration/d.length;d.time+=d.direction*a;if(d.mirroredLoop){if(d.time>d.duration||0>d.time)d.direction*=-1,d.time>d.duration&&(d.time=d.duration,d.directionBackwards=!0),0>d.time&&(d.time=0,d.directionBackwards=!1)}else d.time%=d.duration,0>d.time&&(d.time+=d.duration);var f=d.start+THREE.Math.clamp(Math.floor(d.time/e),0,d.length-1),g=d.weight;f!==d.currentFrame&&
 (this.morphTargetInfluences[d.lastFrame]=0,this.morphTargetInfluences[d.currentFrame]=1*g,this.morphTargetInfluences[f]=0,d.lastFrame=d.currentFrame,d.currentFrame=f);e=d.time%e/e;d.directionBackwards&&(e=1-e);d.currentFrame!==d.lastFrame?(this.morphTargetInfluences[d.currentFrame]=e*g,this.morphTargetInfluences[d.lastFrame]=(1-e)*g):this.morphTargetInfluences[d.currentFrame]=g}}};
+/**
+ * @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 left mouse + ctrl/metaKey, or arrow keys / touch: two-finger move
+
+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.panSpeed = 1.0;
+  this.screenSpacePanning = false; // if true, pan in screen-space
+  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 = { LEFT: THREE.MOUSE.LEFT, MIDDLE: THREE.MOUSE.MIDDLE, RIGHT: 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 );
+
+        panOffset.multiplyScalar( 1 - scope.dampingFactor );
+
+      } else {
+
+        sphericalDelta.set( 0, 0, 0 );
+
+        panOffset.set( 0, 0, 0 );
+
+      }
+
+      scale = 1;
+
+      // update condition is:
+      // min(camera displacement, camera rotation in radians)^2 > EPS
+      // using small-angle approximation cos(x/2) = 1 - x^2 / 8
+
+      if ( zoomChanged ||
+        lastPosition.distanceToSquared( scope.object.position ) > EPS ||
+        8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
+
+        scope.dispatchEvent( changeEvent );
+
+        lastPosition.copy( scope.object.position );
+        lastQuaternion.copy( scope.object.quaternion );
+        zoomChanged = false;
+
+        return true;
+
+      }
+
+      return false;
+
+    };
+
+  }();
+
+  this.dispose = function () {
+
+    scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
+    scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
+    scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );
+
+    scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
+    scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
+    scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
+
+    document.removeEventListener( 'mousemove', onMouseMove, false );
+    document.removeEventListener( 'mouseup', onMouseUp, false );
+
+    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_PAN: 4 };
+
+  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 ) {
+
+      if ( scope.screenSpacePanning === true ) {
+
+        v.setFromMatrixColumn( objectMatrix, 1 );
+
+      } else {
+
+        v.setFromMatrixColumn( objectMatrix, 0 );
+        v.crossVectors( scope.object.up, v );
+
+      }
+
+      v.multiplyScalar( distance );
+
+      panOffset.add( v );
+
+    };
+
+  }();
+
+  // deltaX and deltaY are in pixels; right and down are positive
+  var pan = function () {
+
+    var offset = new 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 use only clientHeight here so aspect ratio does not distort speed
+        panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
+        panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
+
+      } else if ( scope.object.isOrthographicCamera ) {
+
+        // orthographic
+        panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
+        panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
+
+      } else {
+
+        // camera neither orthographic nor perspective
+        console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+        scope.enablePan = false;
+
+      }
+
+    };
+
+  }();
+
+  function dollyIn( dollyScale ) {
+
+    if ( scope.object.isPerspectiveCamera ) {
+
+      scale /= dollyScale;
+
+    } else if ( scope.object.isOrthographicCamera ) {
+
+      scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
+      scope.object.updateProjectionMatrix();
+      zoomChanged = true;
+
+    } else {
+
+      console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+      scope.enableZoom = false;
+
+    }
+
+  }
+
+  function dollyOut( dollyScale ) {
+
+    if ( scope.object.isPerspectiveCamera ) {
+
+      scale *= dollyScale;
+
+    } else if ( scope.object.isOrthographicCamera ) {
+
+      scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
+      scope.object.updateProjectionMatrix();
+      zoomChanged = true;
+
+    } else {
+
+      console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+      scope.enableZoom = false;
+
+    }
+
+  }
+
+  //
+  // event callbacks - update the object state
+  //
+
+  function handleMouseDownRotate( event ) {
+
+    //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 ).multiplyScalar( scope.rotateSpeed );
+
+    var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+    rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+    rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+
+    rotateStart.copy( rotateEnd );
+
+    scope.update();
+
+  }
+
+  function handleMouseMoveDolly( event ) {
+
+    //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 ).multiplyScalar( scope.panSpeed );
+
+    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 handleTouchStartDollyPan( event ) {
+
+    //console.log( 'handleTouchStartDollyPan' );
+
+    if ( scope.enableZoom ) {
+
+      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 );
+
+    }
+
+    if ( scope.enablePan ) {
+
+      var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+      var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+
+      panStart.set( x, y );
+
+    }
+
+  }
+
+  function handleTouchMoveRotate( event ) {
+
+    //console.log( 'handleTouchMoveRotate' );
+
+    rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+    rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+
+    var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+    rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+    rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+
+    rotateStart.copy( rotateEnd );
+
+    scope.update();
+
+  }
+
+  function handleTouchMoveDollyPan( event ) {
+
+    //console.log( 'handleTouchMoveDollyPan' );
+
+    if ( scope.enableZoom ) {
+
+      var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+      var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+      var distance = Math.sqrt( dx * dx + dy * dy );
+
+      dollyEnd.set( 0, distance );
+
+      dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
+
+      dollyIn( dollyDelta.y );
+
+      dollyStart.copy( dollyEnd );
+
+    }
+
+    if ( scope.enablePan ) {
+
+      var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+      var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+
+      panEnd.set( x, y );
+
+      panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+
+      pan( panDelta.x, panDelta.y );
+
+      panStart.copy( panEnd );
+
+    }
+
+    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.LEFT:
+
+        if ( event.ctrlKey || event.metaKey ) {
+
+          if ( scope.enablePan === false ) return;
+
+          handleMouseDownPan( event );
+
+          state = STATE.PAN;
+
+        } else {
+
+          if ( scope.enableRotate === false ) return;
+
+          handleMouseDownRotate( event );
+
+          state = STATE.ROTATE;
+
+        }
+
+        break;
+
+      case scope.mouseButtons.MIDDLE:
+
+        if ( scope.enableZoom === false ) return;
+
+        handleMouseDownDolly( event );
+
+        state = STATE.DOLLY;
+
+        break;
+
+      case scope.mouseButtons.RIGHT:
+
+        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();
+
+    scope.dispatchEvent( startEvent );
+
+    handleMouseWheel( event );
+
+    scope.dispatchEvent( endEvent );
+
+  }
+
+  function onKeyDown( event ) {
+
+    if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
+
+    handleKeyDown( event );
+
+  }
+
+  function onTouchStart( event ) {
+
+    if ( scope.enabled === false ) return;
+
+    event.preventDefault();
+
+    switch ( event.touches.length ) {
+
+      case 1: // one-fingered touch: rotate
+
+        if ( scope.enableRotate === false ) return;
+
+        handleTouchStartRotate( event );
+
+        state = STATE.TOUCH_ROTATE;
+
+        break;
+
+      case 2: // two-fingered touch: dolly-pan
+
+        if ( scope.enableZoom === false && scope.enablePan === false ) return;
+
+        handleTouchStartDollyPan( event );
+
+        state = STATE.TOUCH_DOLLY_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-pan
+
+        if ( scope.enableZoom === false && scope.enablePan === false ) return;
+        if ( state !== STATE.TOUCH_DOLLY_PAN ) return; // is this needed?
+
+        handleTouchMoveDollyPan( 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/modules/hardware/pot b/modules/hardware/pot
index 5386b80..3e6ffe9 100644
--- a/modules/hardware/pot
+++ b/modules/hardware/pot
@@ -39,13 +39,14 @@
     // inputs
     //
     var inputs = {
-        // gcode: {
-        //     type: 'string',
-        //     label: 'gcode',
-        //     event: function(evt) {
-        //         parseGCode(evt.detail)
-        //     }
-        // }
+        points: {
+            type: 'object',
+            label: 'points',
+            event: function(e) {
+                mod.points = e.detail;
+                updateGeometry();
+            }
+        }
     }
     //
     // outputs
@@ -67,18 +68,37 @@
 
         // Three.js viewer div
         var viewer = document.createElement('div');
-        viewer.style.width = '300px';
-        viewer.style.height = '200px';
+        viewer.style.width = '400px';
+        viewer.style.height = '400px';
         div.appendChild(viewer);
-
         mod.viewer = viewer;
 
-        //
-        // inputs
-        //
+        // Parameters
+        mod.widthInput = make_text_input(div, "width scale", 24);
+        mod.widthInput.value = 20;
+        mod.widthInput.addEventListener('input', function(e) {
+            updateGeometry();
+        });
+
+        mod.heightInput = make_text_input(div, "height scale", 24);
+        mod.heightInput.value = 20;
+        mod.heightInput.addEventListener('input', function(e) {
+            updateGeometry();
+        });
+
+        mod.extrusionInput = make_text_input(div, "extrusion rate", 24);
+        mod.extrusionInput.value = 20;
+
+        mod.layersInput = make_text_input(div, "number of layers", 24);
+        mod.layersInput.value = 50;
+        mod.layersInput.addEventListener('input', function(e) {
+            updateGeometry();
+        });
+
+        // Output event button
         mod.sendButton = make_button_input(div, 'send')
         mod.sendButton.addEventListener('click', function() {
-            outputs.instructions.event(mod.instructionArray);
+            generateInstructions();
         })
     }
 
@@ -86,6 +106,13 @@
         return Math.sqrt( ((v1.x - v0.x) * (v1.x - v0.x)) + ((v1.y - v0.y) * (v1.y - v0.y)) );
     }
 
+    function lerp(pt1, pt2, pct) {
+        return {
+            x: (1 - pct) * pt1.x + pct * pt2.x,
+            y: (1 - pct) * pt1.y + pct * pt2.y
+        }
+    }
+
     function threeJsLoaded() {
         var renderer = new THREE.WebGLRenderer();
         renderer.setSize( mod.viewer.clientWidth, mod.viewer.clientHeight );
@@ -93,92 +120,114 @@
 
         var scene = new THREE.Scene();
         var camera = new THREE.PerspectiveCamera( 75, mod.viewer.clientWidth / mod.viewer.clientHeight, 0.1, 1000 );
-        // var controls = new THREE.OrbitControls( camera );
+        var controls = new THREE.OrbitControls( camera, mod.viewer );
+
+        mod.scene = scene;
 
         camera.position.z = 50;
 
-        var points = [];
-        for ( var i = 0; i < 20; i ++ ) {
-            points.push( new THREE.Vector2( Math.sin( i * 0.1 ) * 10 + 5, i) );
+        // Begin rendering
+        var animate = function () {
+            requestAnimationFrame( animate );
+            controls.update();
+            renderer.render( scene, camera );
+        };
+        animate();
+    }
+
+    function updateGeometry() {
+        if (!mod.points || mod.points.length == 0) { return; }
+
+        // Construct an updated profile given the number of layers specified
+        // using linear interpolation
+        var geomPoints = [];
+        var layers = mod.layersInput.value;
+        for ( var i = 0; i < layers; ++i ) {
+            var scaledLayerIndex = (i / layers) * (mod.points.length - 1);
+            var pt1 = mod.points[Math.floor(scaledLayerIndex)];
+            var pt2 = mod.points[Math.ceil(scaledLayerIndex)];
+            var pct = scaledLayerIndex - Math.floor(scaledLayerIndex);
+            var lerpedPt = lerp(pt1, pt2, pct);
+            if (lerpedPt.x == 0) {
+                lerpedPt.x += 0.001;
+            }
+            geomPoints.push( new THREE.Vector2( lerpedPt.x * +(mod.widthInput.value), lerpedPt.y * +(mod.heightInput.value)) );
         }
+        mod.geomPoints = geomPoints;
 
         // Create revolved geometry
-        var geometry = new THREE.LatheGeometry( points );
-        var material = new THREE.MeshBasicMaterial( { color: 0x0000ff } );
-        var pot = new THREE.Mesh( geometry, material );
+        var geometry = new THREE.LatheGeometry( geomPoints );
 
-        // Rotate the vertex order so that we spiral up from the bottom
-        var potVerts = pot.geometry.vertices;
-        var clayGeometry = new THREE.Geometry();
+        // Construct the extrusion path vertices
+        //
+        // NOTE: We rotate the vertex order so that we spiral up from the bottom
+        var potVerts = geometry.vertices;
+        mod.clayGeometry = new THREE.Geometry();
         var segments = geometry.parameters.segments || 12;
-        for ( var pt = 0; pt < points.length; ++pt ) {
+        for ( var pt = 0; pt < geomPoints.length; ++pt ) {
             for ( var seg = 0; seg < segments; ++seg ) {
-                var vert = potVerts[(points.length * seg) + pt];
-                clayGeometry.vertices.push(vert);
+                var vert = potVerts[(geomPoints.length * seg) + pt];
+                mod.clayGeometry.vertices.push(vert);
             }
         }
         var pathMat = new THREE.LineBasicMaterial( { color: 0x00ff00 } );
-        var path = new THREE.Line(clayGeometry, pathMat);
-        // scene.add(pot);
-        scene.add(path);
-
-        /************************************************************
-       *
-       * Generate move instructions
-       *
-       * NOTE: We rotate the coordinate system from Y-up to Z-up
-       *
-       ************************************************************/
-      var verts = clayGeometry.vertices;
-      var totalExtrusion = 0;
-      const EXTRUSION_RATE = 20;
-      var instructions = [];
-
-      // Add the first instruction manually
-      instructions.push({
-        type: "move",
-        speed: EXTRUSION_RATE,
-        position: {
-          X: verts[0].x,
-          Y: verts[0].z,
-          Z: verts[0].y,
-          E: 0
+        var path = new THREE.Line(mod.clayGeometry, pathMat);
+        if (mod.path) {
+            mod.scene.remove(mod.path);
         }
-      });
+        mod.scene.add(path);
+        mod.path = path;
+    }
 
-      for (var i = 1; i < verts.length; ++i) {
-        var currentVert = verts[i];
-        var previousVert = verts[i - 1];
-        totalExtrusion += distance(currentVert, previousVert);
+    function generateInstructions() {
+        /************************************************************
+         *
+         * Generate move instructions
+         *
+         * NOTE: We rotate the coordinate system from Y-up to Z-up
+         *
+         ************************************************************/
+        if (!mod.points || !mod.clayGeometry || mod.clayGeometry.vertices.length == 0) { return; }
+
+        var verts = mod.clayGeometry.vertices;
+        var totalExtrusion = 0;
+        const rate = +(mod.extrusionInput.value);
+        var instructions = [];
+
+        // Add the first instruction manually
         instructions.push({
-          type: "move",
-          speed: EXTRUSION_RATE,
-          position: {
-            X: verts[i].x,
-            Y: verts[i].z,
-            Z: verts[i].y,
-            E: totalExtrusion
-          }
-        })
-      }
-
-      mod.instructionArray = instructions;
-
-
-      // Render
-
-        var animate = function () {
-            requestAnimationFrame( animate );
-
-            renderer.render( scene, camera );
-        };
+            type: "move",
+            speed: rate,
+            position: {
+                X: verts[0].x,
+                Y: verts[0].z,
+                Z: verts[0].y,
+                E: 0
+            }
+        });
+
+        for (var i = 1; i < verts.length; ++i) {
+            var currentVert = verts[i];
+            var previousVert = verts[i - 1];
+            totalExtrusion += distance(currentVert, previousVert);
+            instructions.push({
+                type: "move",
+                speed: rate,
+                position: {
+                    X: verts[i].x,
+                    Y: verts[i].z,
+                    Z: verts[i].y,
+                    E: totalExtrusion
+                }
+            });
+        }
 
-        animate();
+        outputs.instructions.event(instructions);
     }
 
-    /*
-    UI helpers
-    */
+    // /*
+    // UI helpers
+    // */
     function make_text_input(div, name, size) {
         div.appendChild(document.createElement('br'))
         div.appendChild(document.createTextNode(name + ': '))
diff --git a/modules/hardware/profile b/modules/hardware/profile
new file mode 100644
index 0000000..e72da16
--- /dev/null
+++ b/modules/hardware/profile
@@ -0,0 +1,217 @@
+//
+// print a pot
+//
+// Sean Hickey
+// (c) Massachusetts Institute of Technology 2018
+// 
+// This work may be reproduced, modified, distributed, performed, and 
+// displayed for any purpose, but must acknowledge the mods
+// project. Copyright is retained and must be preserved. The work is 
+// provided as is; no warranty is provided, and users accept all 
+// liability.
+
+
+//
+// closure
+//
+(function() {
+    //
+    // module globals
+    //
+    var mod = {}
+    //
+    // name
+    //
+    var name = 'profile'
+    //
+    // initialization
+    //
+    var init = function() {
+        mod.isDrawing = false;
+    }
+    //
+    // inputs
+    //
+    var inputs = {
+
+    }
+    //
+    // outputs
+    //
+    var outputs = {
+        points: {
+            type: 'object',
+            label: 'points',
+            event: function(obj) {
+                mods.output(mod, 'points', obj)
+            }
+        }
+    }
+    //
+    // interface
+    //
+    var interface = function(div) {
+        mod.div = div
+
+        // Drawing canvas
+        var canvas = div.appendChild(document.createElement('canvas'));
+        canvas.setAttribute('width', 400);
+        canvas.setAttribute('height', 600);
+        canvas.style.width = '400px';
+        canvas.style.height = '600px';
+        div.appendChild(canvas);
+
+        canvas.addEventListener('mousedown', mouseDown, false);
+        canvas.addEventListener('mouseup', mouseUp, false);
+        canvas.addEventListener('mousemove', mouseMove, false);
+
+        mod.canvas = canvas;
+
+        var points = [];
+
+        const TOTAL_POINTS = 25;
+
+        // Prepopulate points with a basic profile
+        points.push({
+                x: 0,
+                y: 0
+        })
+        for (var i = 1; i < TOTAL_POINTS; ++i) {
+            points.push({
+                x: (400 * (i / TOTAL_POINTS)),
+                y: (600 * (i / TOTAL_POINTS) * (i / TOTAL_POINTS))
+            })
+        }
+
+        mod.points = points;
+
+        redraw();
+    }
+
+    function mouseDown() {
+        mod.isDrawing = true;
+    }
+
+    function mouseUp() {
+        if (mod.isDrawing) {
+            mod.isDrawing = false;
+            redraw();
+            var normalizedPoints = [];
+            for (var i = 0; i < mod.points.length; ++i) {
+                var pt = mod.points[i];
+                normalizedPoints.push({
+                    x: pt.x / 400,
+                    y: pt.y / 600
+                })
+            }
+            outputs.points.event(normalizedPoints);
+        }
+    }
+
+    function mouseMove(e) {
+        if (mod.isDrawing) {
+            var mousePoint = {
+                x: e.offsetX,
+                y: 600 - e.offsetY // Flip Y
+            }
+            for (var i = 0; i < mod.points.length; ++i) {
+                var pt = mod.points[i]
+                var dist = distance(mousePoint, pt);
+                if (dist > 50) { continue; }
+                
+                // Falloff function
+                var move = Math.sqrt(dist / 20) * ((Math.PI / 2) - Math.atan(dist / 20));
+                
+                if (pt.x > mousePoint.x) {
+                    mod.points[i].x -= move;
+                }
+                else {
+                    mod.points[i].x += move;
+                }
+            }
+        }
+        redraw();
+    }
+
+    function distance(v0, v1) {
+        return Math.sqrt( ((v1.x - v0.x) * (v1.x - v0.x)) + ((v1.y - v0.y) * (v1.y - v0.y)) );
+    }
+
+    function redraw() {
+        // NOTE: Flip Y here
+        if (mod.canvas.getContext) {
+            var ctx = mod.canvas.getContext('2d');
+            ctx.clearRect(0, 0, mod.canvas.width, mod.canvas.height);
+
+            ctx.beginPath();
+
+            var first = mod.points[0];
+            ctx.moveTo(first.x, 600 - first.y);
+
+            for (var i = 1; i < mod.points.length; ++i) {
+                var pt = mod.points[i];
+                ctx.lineTo(pt.x, 600 - pt.y);
+            }
+            
+            ctx.stroke();
+        }
+    }
+
+    /*
+    UI helpers
+    */
+    function make_text_input(div, name, size) {
+        div.appendChild(document.createElement('br'))
+        div.appendChild(document.createTextNode(name + ': '))
+        var input = document.createElement('input')
+        input.type = 'text'
+        input.size = size
+        div.appendChild(input)
+
+        return input
+    }
+
+    function make_button_input(div, text) {
+        div.appendChild(document.createElement('br'))
+        var button = document.createElement('button')
+        button.style.padding = mods.ui.padding
+        button.style.margin = 1
+        button.appendChild(document.createTextNode(text))
+        div.appendChild(button)
+
+        return button
+    }
+
+    function make_checkbox_input(div, prefix) {
+        div.appendChild(document.createElement('br'))
+        div.appendChild(document.createTextNode(prefix + ': '))
+        var checkbox = document.createElement('input')
+        checkbox.type = 'checkbox'
+        div.appendChild(checkbox)
+
+        return checkbox
+    }
+
+    function make_text_display(div, prefix) {
+        div.appendChild(document.createElement('br'))
+        div.appendChild(document.createTextNode(prefix + ': '))
+        var span = document.createElement('span')
+        span.innerHTML = ''
+        div.appendChild(span)
+
+        return span
+    }
+
+
+    //
+    // return values
+    //
+    return ({
+        mod: mod,
+        name: name,
+        init: init,
+        inputs: inputs,
+        outputs: outputs,
+        interface: interface
+    })
+}())
\ No newline at end of file
-- 
GitLab