repo: janusweb
action: commit
revision: 
path_from: 
revision_from: 3412e67b67eef7de47ff881da21eb6da63833d7b:
path_to: 
revision_to: 
git.thebackupbox.net
janusweb
git clone git://git.thebackupbox.net/janusweb
commit 3412e67b67eef7de47ff881da21eb6da63833d7b
Author: James Baicoianu 
Date:   Mon Aug 15 04:42:55 2016 -0700

    Experimental code dump.  Massive refactoring for scripting support

diff --git a/scripts/config.js b/scripts/config.js
index a1f02fea3d7ead1d7fd1470cd9fb7be24e13e4b9..
index ..bf08cb601246b38135c46fe1574c932ffbaf6133 100644
--- a/scripts/config.js
+++ b/scripts/config.js
@@ -1,18 +1,28 @@
+/*
 elation.config.set('dependencies.protocol', 'https:');              // "http:" or "https:"
 elation.config.set('dependencies.host', 'janusweb.metacade.com');   // Hostname this release will live on
 elation.config.set('dependencies.rootdir', '/');                    // Directory this release will live in
 elation.config.set('dependencies.main', 'janusweb.js');             // The main script file for this release
+*/

 elation.config.set('janusweb.network.host', 'wss://janusweb.lnq.to:5567');        // Default presence server
 elation.config.set('engine.assets.corsproxy', 'https://janusweb.lnq.to:8089/'); // CORS proxy URL
+elation.config.set('engine.assets.workers', 2); // CORS proxy URL

-elation.config.set('janusweb.tracking.enabled', false);
-elation.config.set('janusweb.tracking.clientid', '');
+elation.config.set('janusweb.tracking.enabled', true);
+elation.config.set('janusweb.tracking.clientid', 'UA-49582649-2');
+
+elation.config.set('dependencies.protocol', 'http:');              // "http:" or "https:"
+elation.config.set('dependencies.host', 'bai.dev.supcrit.com');   // Hostname this release will live on
+elation.config.set('dependencies.rootdir', '/');                    // Directory this release will live in
+elation.config.set('dependencies.main', 'scripts/utils/elation.js');             // The main script file for this release
+
+elation.config.set('demohack.vive', false);

 // You probably don't want to edit past this line unless you know what you're doing
 // --------------------------------------------------------------------------------
 // These settings can be changed if you want to host your .js and media in non-standard locations

 elation.config.set('dependencies.path', elation.config.get('dependencies.protocol') + '//' + elation.config.get('dependencies.host') + elation.config.get('dependencies.rootdir'));
-elation.config.set('janusweb.datapath', elation.config.get('dependencies.path') + 'media/');
+elation.config.set('janusweb.datapath', elation.config.get('dependencies.path') + 'media/janusweb/');
 elation.config.set('engine.assets.font.path', elation.config.get('janusweb.datapath') + 'fonts/');
diff --git a/scripts/external/JanusClientConnection.js b/scripts/external/JanusClientConnection.js
index e7715664e643c030daad639e0429f5947fedbacc..
index ..5e703b68e5deec72d2234bbc14275ad32e7121d3 100644
--- a/scripts/external/JanusClientConnection.js
+++ b/scripts/external/JanusClientConnection.js
@@ -303,9 +303,19 @@ var JanusClientConnection = function(opts)
   this._userId = opts.userId;
   this._roomUrl = opts.roomUrl;
   this._version = opts.version;
+  this._host = opts.host;
+  this.lastattempt = 0;
+  this.reconnectdelay = 10000;
+  this.connect();
+}
+
+EventDispatcher.prototype.apply(JanusClientConnection.prototype);
+
+JanusClientConnection.prototype.connect = function() {
+  this.lastattempt = new Date().getTime();
   this.status = 0;
   this.error = '';
-  this._websocket = new WebSocket(opts.host, 'binary');
+  this._websocket = new WebSocket(this._host, 'binary');
   this.status = 1;
   this.msgQueue = [];
   this._websocket.onopen = function() {
@@ -318,8 +328,13 @@ var JanusClientConnection = function(opts)
   }.bind(this)
   this._websocket.onmessage = this.onMessage.bind(this)  
 };
-
-EventDispatcher.prototype.apply(JanusClientConnection.prototype);
+JanusClientConnection.prototype.reconnect = function() {
+  var now = new Date().getTime();
+  if (this.lastattempt + this.reconnectdelay <= now) {
+    console.log('Reconnecting...');
+    this.connect();
+  }
+}

 JanusClientConnection.prototype.sendLogon = function() {
   var msgData = {
@@ -342,6 +357,8 @@ JanusClientConnection.prototype.send = function(msg) {
     this.msgQueue.push(msg);
   } else if (this._websocket.readyState == 1) {
     this._websocket.send(JSON.stringify(msg) + '\r\n');
+  } else {
+    this.reconnect();
   }
 };

diff --git a/scripts/image.js b/scripts/image.js
index ec31e76ed33499f6e5e0b77b8171967833767ee9..
index ..d594725ecb549e4a681566828cdd227ef555e75d 100644
--- a/scripts/image.js
+++ b/scripts/image.js
@@ -41,26 +41,26 @@ elation.require(['janusweb.janusbase'], function() {
     this.postinit = function() {
       elation.engine.things.janusimage.extendclass.postinit.call(this);
       this.defineProperties({
-        image_id: { type: 'string' },
-        color: { type: 'color', default: 0xffffff },
-        sbs3d: { type: 'boolean', default: false },
-        ou3d: { type: 'boolean', default: false },
-        reverse3d: { type: 'boolean', default: false },
-        lighting: { type: 'boolean', default: true },
+        image_id: { type: 'string', set: this.updateMaterial },
+        sbs3d: { type: 'boolean', default: false, set: this.updateMaterial },
+        ou3d: { type: 'boolean', default: false, set: this.updateMaterial },
+        reverse3d: { type: 'boolean', default: false, set: this.updateMaterial },
       });
     }
     this.createObject3D = function() {
-      this.texture = elation.engine.assets.find('image', this.properties.image_id);
-      if (this.texture) {
-        elation.events.add(this.texture, 'asset_load', elation.bind(this, this.imageloaded));
-        elation.events.add(this.texture, 'update', elation.bind(this, this.refresh));
+      var geo = this.createGeometry();
+      var mat = this.createMaterial();
+      return new THREE.Mesh(geo, mat);
+

+/*
         var geo = this.createGeometry();
         var mat = this.createMaterial();
         return new THREE.Mesh(geo, mat);
       } else {
         console.log('ERROR - could not find image ' + this.properties.image_id);
       }
+*/
     }
     this.createGeometry = function() {
       var aspect = this.getAspect(),
@@ -70,16 +70,25 @@ elation.require(['janusweb.janusbase'], function() {
       return box;
     }
     this.createMaterial = function() {
+      this.asset = elation.engine.assets.find('image', this.image_id, true);
+      if (this.asset) {
+        this.texture = this.asset.getInstance();
+        if (this.texture) {
+          elation.events.add(this.texture, 'asset_load', elation.bind(this, this.imageloaded));
+          elation.events.add(this.texture, 'update', elation.bind(this, this.refresh));
+        } 
+      }
       var matargs = {
-        map: this.texture,
-        color: this.properties.color,
+        color: this.color,
         transparent: true,
         alphaTest: 0.2
       };

-      var sidemattex = this.texture.clone();
-      this.sidetex = sidemattex;
-      sidemattex.repeat.x = .0001;
+      if (this.texture) {
+        var sidemattex = this.texture.clone();
+        this.sidetex = sidemattex;
+        sidemattex.repeat.x = .0001;
+      }
       var sidematargs = {
         map: sidemattex,
         color: this.properties.color,
@@ -91,17 +100,30 @@ elation.require(['janusweb.janusbase'], function() {
       var sidemat = (this.properties.lighting ? new THREE.MeshPhongMaterial(sidematargs) : new THREE.MeshBasicMaterial(sidematargs));
       var facemat = new THREE.MeshFaceMaterial([sidemat,sidemat,sidemat,sidemat,mat,mat]);
       this.facematerial = mat;
+      this.frontmaterial = mat;
       this.sidematerial = sidemat;
       return facemat;
     }
+    this.updateMaterial = function() {
+      var newtexture = elation.engine.assets.find('image', this.image_id);
+      if (newtexture && newtexture !== this.texture) {
+        this.texture = newtexture;
+        if (newtexture.image) {
+          this.imageloaded();
+        } else {
+          elation.events.add(this.texture, 'asset_load', elation.bind(this, this.imageloaded));
+        }
+        elation.events.add(this.texture, 'update', elation.bind(this, this.refresh));
+      } 
+    }
     this.getAspect = function() {
       var aspect = 1;
       if (this.texture && this.texture.image) {
         var size = this.getSize(this.texture.image);
         aspect = size.height / size.width;
       }
-      if (this.properties.sbs3d || (this.asset && this.asset.sbs3d)) aspect *= 2;
-      if (this.properties.ou3d || (this.asset && this.asset.ou3d)) aspect /= 2;
+      if (this.sbs3d || (this.asset && this.asset.sbs3d)) aspect *= 2;
+      if (this.ou3d || (this.asset && this.asset.ou3d)) aspect /= 2;
       return aspect;
     }
     this.getSize = function(image) {
@@ -113,6 +135,10 @@ elation.require(['janusweb.janusbase'], function() {
       this.objects['3d'].geometry = geo;
     }
     this.imageloaded = function(ev) {
+      if (!this.frontmaterial) return;
+
+      this.frontmaterial.map = this.texture;
+      this.frontmaterial.needsUpdate = true;
       this.adjustAspectRatio();
       this.sidetex.image = this.texture.image;
       this.sidetex.needsUpdate = true;
@@ -123,7 +149,7 @@ elation.require(['janusweb.janusbase'], function() {
         texture.reverse = this.properties.reverse3d;
         texture.needsUpdate = true;
         this.texture = texture;
-        this.facematerial.map = texture;
+        this.frontmaterial.map = texture;
         /*
         this.sidematerial.map = texture.clone();
         this.sidematerial.map.needsUpdate = true;
@@ -133,5 +159,12 @@ elation.require(['janusweb.janusbase'], function() {

       this.refresh();
     }
+    this.getProxyObject = function() {
+      var proxy = elation.engine.things.janusimage.extendclass.getProxyObject.call(this);
+      proxy._proxydefs = {
+        id:  [ 'property', 'image_id'],
+      };
+      return proxy;
+    }
   }, elation.engine.things.janusbase);
 });
diff --git a/scripts/janusbase.js b/scripts/janusbase.js
index b30fa0f6dbec021c7c38c11c5f0045221d341357..
index ..a9d2d952966944ba2b29fbbe382cf7efb239ff64 100644
--- a/scripts/janusbase.js
+++ b/scripts/janusbase.js
@@ -5,10 +5,38 @@ elation.require(['engine.things.generic', 'utils.template'], function() {
   elation.component.add('engine.things.janusbase', function() {
     this.postinit = function() {
       elation.engine.things.janusbase.extendclass.postinit.call(this);
+      this.frameupdates = [];
       this.defineProperties({
-        js_id: { type: 'string' },
-        room: { type: 'object' },
+        room:     { type: 'object' },
+        janus:    { type: 'object' },
+        js_id:    { type: 'string' },
+        color:    { type: 'color', default: new THREE.Color(0xffffff), set: this.updateMaterial },
+        fwd:      { type: 'vector3', default: new THREE.Vector3(0,0,1), set: this.pushFrameUpdate },
+        xdir:     { type: 'vector3', default: new THREE.Vector3(1,0,0), set: this.pushFrameUpdate },
+        ydir:     { type: 'vector3', default: new THREE.Vector3(0,1,0), set: this.pushFrameUpdate },
+        zdir:     { type: 'vector3', default: new THREE.Vector3(0,0,1), set: this.pushFrameUpdate },
+        lighting: { type: 'boolean', default: true },
+        sync:     { type: 'boolean', default: false },
+        rotate_axis: { type: 'string', default: '0 1 0' },
+        rotate_deg_per_sec: { type: 'string' },
       });
+      //if (this.col) this.color = this.col;
+      this.jschildren = [];
+      elation.events.add(this.room, 'janusweb_script_frame_end', elation.bind(this, this.handleFrameUpdates));
+    }
+    this.createForces = function() {
+      elation.events.add(this.objects.dynamics, 'physics_collide', elation.bind(this, this.handleCollision));
+
+      var rotate_axis = this.properties.rotate_axis,
+          rotate_speed = this.properties.rotate_deg_per_sec;
+      if (rotate_axis && rotate_speed) {
+        var speed = (rotate_speed * Math.PI/180);
+        var axisparts = rotate_axis.split(' ');
+        var axis = new THREE.Vector3().set(axisparts[0], axisparts[1], axisparts[2]);
+        axis.multiplyScalar(speed);
+        this.objects.dynamics.setAngularVelocity(axis);
+      }
+
     }
     this.setProperties = function(props) {
       var n = this.properties.room.parseNode(props);
@@ -22,13 +50,13 @@ elation.require(['engine.things.generic', 'utils.template'], function() {
         this.properties.render.model = n.id;
         rebuild = true;
       }
-      var curcol = this.properties.col;
+      var curcol = this.properties.col || [1,1,1];
       if (n.col && (n.col[0] != curcol[0] || n.col[1] != curcol[1] || n.col[2] != curcol[2])) {
         this.properties.col = n.col;
         rebuild = true;
       }
       if (rebuild) {
-        this.set('visible', true, true);
+        //this.set('visible', true, true);
       }
       if (n.accel) this.properties.acceleration.fromArray(n.accel.split(' ').map(parseFloat));
       if (n.vel) this.objects.dynamics.setVelocity(this.properties.velocity.fromArray(n.vel.split(' ').map(parseFloat)));
@@ -53,7 +81,7 @@ elation.require(['engine.things.generic', 'utils.template'], function() {
         xdir: xdir.toArray().join(' '),
         ydir: ydir.toArray().join(' '),
         zdir: zdir.toArray().join(' '),
-        col: this.properties.col,
+        col: this.properties.color,
         lighting: this.properties.lighting,
         visible: this.properties.visible,
       };
@@ -62,18 +90,161 @@ elation.require(['engine.things.generic', 'utils.template'], function() {
       return xml;
     }
     this.getProxyObject = function() {
-      return new elation.proxy(this, {
-        id: ['property', 'properties.id'],
-        js_id: ['property', 'properties.js_id'],
-        pos: ['property', 'properties.position'],
-        vel: ['property', 'properties.velocity'],
-        scale: ['property', 'properties.scale'],
-        col: ['property', 'properties.col'],
-      });
+      if (!this._proxyobject) {
+        this._proxyobject = new elation.proxy(this, {
+          js_id: ['property', 'properties.js_id'],
+          pos:   ['property', 'position'],
+          vel:   ['property', 'velocity'],
+          accel: ['property', 'acceleration'],
+          mass:  ['property', 'mass'],
+          scale: ['property', 'scale'],
+          col:   ['property', 'color'],
+          fwd:   [ 'property', 'zdir'],
+          xdir:  [ 'property', 'xdir'],
+          ydir:  [ 'property', 'ydir'],
+          zdir:  [ 'property', 'zdir'],
+          sync:  [ 'property', 'sync'],
+          children: ['property', 'jschildren'],
+
+          oncollision: ['callback', 'collision'],
+          appendChild: ['function', 'appendChild']
+        });
+      }
+      return this._proxyobject;
     }
     this.start = function() {
     }    
     this.stop = function() {
     }    
+    this.pushFrameUpdate = function(key, value) {
+//console.log('frame update!', key, value);
+      this.frameupdates[key] = value;
+    }
+    this.handleFrameUpdates = function() {
+      var updatenames = Object.keys(this.frameupdates);
+      if (updatenames.length > 0) {
+        var updates = this.frameupdates;
+        if ('fwd' in updates) {
+          this.properties.zdir.copy(this.fwd);
+          updates.zdir = this.properties.zdir;
+        }
+        var xdir = this.properties.xdir,
+            ydir = this.properties.ydir,
+            zdir = this.properties.zdir;
+
+        if ( ('xdir' in updates) && 
+            !('ydir' in updates) && 
+            !('zdir' in updates)) {
+          zdir.crossVectors(xdir, ydir);
+          this.updateVectors(true);
+        } 
+        if (!('xdir' in updates) && 
+            !('ydir' in updates) && 
+             ('zdir' in updates)) {
+          xdir.crossVectors(ydir, zdir);
+          this.updateVectors(true);
+        } 
+        if (!('xdir' in updates) && 
+             ('ydir' in updates) && 
+             ('zdir' in updates)) {
+          xdir.crossVectors(zdir, ydir);
+          this.updateVectors(true);
+        } 
+        if ( ('xdir' in updates) && 
+            !('ydir' in updates) && 
+             ('zdir' in updates)) {
+          ydir.crossVectors(xdir, zdir).multiplyScalar(-1);
+          this.updateVectors(true);
+        } 
+        if ( ('xdir' in updates) && 
+             ('ydir' in updates) && 
+            !('zdir' in updates)) {
+          zdir.crossVectors(xdir, ydir);
+          this.updateVectors(true);
+        } 
+
+        if (!('xdir' in updates) && 
+            !('ydir' in updates) && 
+            !('zdir' in updates)) {
+          // None specified, so update the vectors from the orientation quaternion
+          this.updateVectors(false);
+        }
+        this.frameupdates = {};
+      } else {
+        this.updateVectors(false);
+      }
+    }
+    this.updateVectors = function(updateOrientation) {
+      if (updateOrientation) {
+        //var quat = this.room.getOrientation(this.properties.xdir.toArray().join(' '), this.properties.ydir.toArray().join(' '), this.properties.zdir.toArray().join(' '));
+        this.xdir.normalize();
+        this.ydir.normalize();
+        this.zdir.normalize();
+        var mat4 = new THREE.Matrix4().makeBasis(this.xdir, this.ydir, this.properties.zdir.clone().negate());
+        var quat = new THREE.Quaternion();
+        var pos = new THREE.Vector3();
+        var scale = new THREE.Vector3();
+        quat.setFromRotationMatrix(mat4);
+        //mat4.decompose(pos, this.orientation, scale);
+        //this.orientation.normalize();
+//console.log(mat4.elements);
+        this.properties.orientation.copy(quat);
+//console.log(this.xdir.toArray(), this.ydir.toArray(), this.zdir.toArray(), this.orientation.toArray());
+//console.log(this.properties.orientation, this.properties.orientation.toArray());
+      } else if (this.objects['3d']) {
+        //this.objects['3d'].matrix.extractBasis(this.properties.xdir, this.properties.ydir, this.properties.zdir);
+      }
+    }
+    this.appendChild = function(obj) {
+      var proxyobj = obj
+      if (elation.utils.isString(obj)) {
+        proxyobj = this.room.jsobjects[obj];
+      }
+      if (proxyobj) {
+        //var realobj = this.room.getObjectFromProxy(proxyobj);
+        var realobj = proxyobj._target;
+        if (realobj) {
+          this.add(realobj);
+          this.updateScriptChildren();
+        }
+      }
+    }
+    this.removeChild = function(obj) {
+      var proxyobj = obj
+      if (elation.utils.isString(obj)) {
+        proxyobj = this.room.jsobjects[obj];
+      }
+      if (proxyobj) {
+        //var realobj = this.room.getObjectFromProxy(proxyobj);
+        var realobj = proxyobj._target;
+        if (realobj) {
+          this.remove(realobj);
+          this.updateScriptChildren();
+        }
+      }
+    }
+    this.updateScriptChildren = function() {
+      this.jschildren = [];
+      var keys = Object.keys(this.children);
+      for (var i = 0; i < keys.length; i++) {
+        this.jschildren.push(this.children[keys[i]].getProxyObject());
+      }
+    }
+    this.handleCollision = function(ev) {
+      var obj1 = ev.data.bodies[0],
+          obj2 = ev.data.bodies[1];
+      //var proxy1 = obj1.getProxy(),
+      //    proxy2 = obj2.getProxy();
+      var other = (obj1.object == this ? obj2.object : obj1.object);
+      if (other) {
+        if (other.getProxyObject) {
+          var proxy = other.getProxyObject();
+          //console.log('I collided', proxy, this);
+          elation.events.fire({type: 'collision', element: this, data: proxy});
+        } else {
+console.error('dunno what this is', other);
+        }
+      }
+    }
   }, elation.engine.things.generic);
 });
diff --git a/scripts/janusplayer.js b/scripts/janusplayer.js
index 16289dabcf30545486838d28ce57840c9e0badbc..
index ..61e562b962277e0540cfb35efca517c77e96d14b 100644
--- a/scripts/janusplayer.js
+++ b/scripts/janusplayer.js
@@ -4,6 +4,9 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
   elation.component.add('engine.things.janusplayer', function() {
     this.postinit = function() {
       elation.engine.things.janusplayer.extendclass.postinit.call(this);
+      this.defineProperties({
+        cursor_visible: {type: 'boolean', default: true, set: this.toggleCursorVisibility}
+      });
       this.controlstate2 = this.engine.systems.controls.addContext('janusplayer', {
         'voip_active': ['keyboard_v,keyboard_shift_v', elation.bind(this, this.activateVOIP)],
         'browse_back': ['gamepad_0_button_4', elation.bind(this, this.browseBack)],
@@ -13,17 +16,35 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
         xdir: new THREE.Vector3(1, 0, 0),
         ydir: new THREE.Vector3(0, 1, 0),
         zdir: new THREE.Vector3(0, 0, 1),
+        eye_pos: new THREE.Vector3(0, 1.6, 0),
         view_xdir: new THREE.Vector3(1, 0, 0),
         view_ydir: new THREE.Vector3(0, 1, 0),
         view_zdir: new THREE.Vector3(0, 0, 1),
-        hand0_xdir: new THREE.Vector3(1, 0, 0),
-        hand0_ydir: new THREE.Vector3(0, 1, 0),
-        hand0_zdir: new THREE.Vector3(0, 0, 1),
-        hand1_xdir: new THREE.Vector3(1, 0, 0),
-        hand1_ydir: new THREE.Vector3(0, 1, 0),
-        hand1_zdir: new THREE.Vector3(0, 0, 1),
+        cursor_xdir: new THREE.Vector3(1, 0, 0),
+        cursor_ydir: new THREE.Vector3(0, 1, 0),
+        cursor_zdir: new THREE.Vector3(0, 0, 1),
         cursor_pos: new THREE.Vector3(0, 0, 0),
+        lookat_pos: new THREE.Vector3(0, 0, 0),
+      };
+      this.hands = {
+        left: {
+          active: false,
+          position: new THREE.Vector3(0, 0, 0),
+          xdir: new THREE.Vector3(1, 0, 0),
+          ydir: new THREE.Vector3(0, 1, 0),
+          zdir: new THREE.Vector3(0, 0, 1),
+        },
+        right: {
+          active: false,
+          position: new THREE.Vector3(0, 0, 0),
+          xdir: new THREE.Vector3(1, 0, 0),
+          ydir: new THREE.Vector3(0, 1, 0),
+          zdir: new THREE.Vector3(0, 0, 1),
+        }
       };
+      this.cursor_active = false;
+      this.cursor_object = '';
+      this.lookat_object = '';
       this.voip = new JanusVOIPRecorder({audioScale: 1024});
       this.voipqueue = [];
       this.voipbutton = elation.ui.button({append: document.body, classname: 'janusweb_voip', label: 'VOIP'});
@@ -34,6 +55,27 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
       elation.events.add(this.voip, 'voip_data', elation.bind(this, this.handleVOIPData));
       elation.events.add(this.voip, 'voip_error', elation.bind(this, this.handleVOIPError));
       elation.events.add(this.engine, 'engine_frame', elation.bind(this, this.updateVectors));
+      elation.events.add(null, 'mouseover', elation.bind(this, this.updateFocusObject));
+      elation.events.add(null, 'mousemove', elation.bind(this, this.updateFocusObject));
+      elation.events.add(null, 'mouseout', elation.bind(this, this.updateFocusObject));
+      elation.events.add(this.engine.client.container, 'mousedown', elation.bind(this, this.updateMouseStatus));
+      elation.events.add(this.engine.client.container, 'mouseup', elation.bind(this, this.updateMouseStatus));
+    }
+    this.createChildren = function() {
+      elation.engine.things.janusplayer.extendclass.createChildren.call(this);
+/*
+setTimeout(elation.bind(this, function() {
+      this.cursor = this.spawn('janusobject', 'playercursor', {
+        js_id: 'player_cursor',
+        position: this.vectors.cursor_pos,
+        janusid: 'cursor_crosshair',
+        pickable: false,
+        collidable: false,
+        visible: this.cursor_visible
+      }, true);
+      this.vectors.cursor_pos = this.cursor.position;
+}), 1000);
+*/
     }
     this.enable = function() {
       elation.engine.things.janusplayer.extendclass.enable.call(this);
@@ -77,10 +119,129 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
         history.go(1);
       }
     }
+    this.engine_frame = function(ev) {
+      elation.engine.things.janusplayer.extendclass.engine_frame.call(this, ev);
+      if (this.tracker.hasHands()) {
+        var hands = this.tracker.getHands();
+        if (hands.left) {
+          var pos = hands.left.position,
+              orient = hands.left.orientation;
+          this.hands.left.active = true;
+          this.localToWorld(this.hands.left.position.fromArray(pos));
+        }
+        if (hands.right) {
+          var pos = hands.right.position,
+              orient = hands.right.orientation;
+          this.hands.right.active = true;
+          this.localToWorld(this.hands.right.position.fromArray(pos));
+        }
+      }
+    }
     this.updateVectors = function() {
       var v = this.vectors;
-      this.objects['3d'].matrixWorld.extractBasis(v.xdir, v.ydir, v.zdir)
-      this.head.objects['3d'].matrixWorld.extractBasis(v.view_xdir, v.view_ydir, v.view_zdir)
+      if (this.objects['3d']) {
+        this.objects['3d'].matrixWorld.extractBasis(v.xdir, v.ydir, v.zdir)
+      }
+      if (this.head) {
+        this.head.objects['3d'].matrixWorld.extractBasis(v.view_xdir, v.view_ydir, v.view_zdir)
+        v.view_zdir.negate();
+      }
+    }
+    this.updateMouseStatus = function(ev) {
+      if (ev.type == 'mousedown' && ev.button === 0) {
+        this.cursor_active = true;
+      } else if (ev.type == 'mouseup' && ev.button === 0) {
+        this.cursor_active = false;
+      }
+    }
+    this.updateFocusObject = function(ev) {
+      var obj = ev.element;
+      if ((ev.type == 'mouseover' || ev.type == 'mousemove') && obj && obj.js_id) {
+        this.cursor_object = obj.js_id;
+        this.vectors.cursor_pos.copy(ev.data.point);
+        var face = ev.data.face;
+        if (face) {
+          this.vectors.cursor_zdir.copy(face.normal);
+          var worldpos = this.localToWorld(new THREE.Vector3(0,0,0));
+
+          this.vectors.cursor_xdir.subVectors(worldpos, this.vectors.cursor_pos).normalize();
+          //this.vectors.cursor_ydir.crossVectors(this.vectors.cursor_xdir, this.vectors.cursor_zdir).normalize();
+          this.vectors.cursor_ydir.set(0,1,0);
+          var dot = this.vectors.cursor_ydir.dot(face.normal);
+          if (Math.abs(dot) > 0.9) {
+            this.vectors.cursor_ydir.crossVectors(this.vectors.cursor_xdir, this.vectors.cursor_zdir).normalize();
+            this.vectors.cursor_xdir.crossVectors(this.vectors.cursor_ydir, this.vectors.cursor_zdir).normalize();
+            if (dot / Math.abs(dot) > 0) {
+              this.vectors.cursor_zdir.negate();
+            }
+          } else {
+            //this.vectors.cursor_zdir.negate();
+            this.vectors.cursor_ydir.set(0,1,0);
+            this.vectors.cursor_xdir.crossVectors(this.vectors.cursor_zdir, this.vectors.cursor_ydir).normalize().negate();
+          }
+
+          //console.log(this.vectors.cursor_xdir.toArray(), this.vectors.cursor_ydir.toArray(), this.vectors.cursor_zdir.toArray());
+          if (this.cursor) {
+            var mat = new THREE.Matrix4().makeBasis(this.vectors.cursor_xdir, this.vectors.cursor_ydir, this.vectors.cursor_zdir);
+            mat.decompose(new THREE.Vector3(), this.cursor.properties.orientation, new THREE.Vector3());
+            var invscale = ev.data.distance / 10;
+            this.cursor.scale.set(invscale,invscale,invscale);
+          }
+        }
+//console.log(ev.data);
+
+        this.lookat_object = obj.js_id;
+        this.vectors.lookat_pos.copy(ev.data.point);
+      } else {
+        this.cursor_object = '';
+        this.lookat_object = '';
+      }
+    }
+    this.toggleCursorVisibility = function() {
+      if (this.cursor) {
+        this.cursor.visible = this.cursor_visible;
+      }
+    }
+    this.getProxyObject = function() {
+      var proxy = new elation.proxy(this, {
+        pos:           ['property', 'position'],
+        vel:           ['property', 'velocity'],
+        accel:         ['property', 'acceleration'],
+        eye_pos:       ['property', 'vectors.eye_pos'],
+        head_pos:      ['property', 'head.properties.position'],
+        cursor_pos:    ['property', 'vectors.cursor_pos'],
+        cursor_xdir:   ['property', 'vectors.cursor_xdir'],
+        cursor_ydir:   ['property', 'vectors.cursor_ydir'],
+        cursor_zdir:   ['property', 'vectors.cursor_zdir'],
+        view_dir:      ['property', 'vectors.view_zdir'],
+        dir:           ['property', 'vectors.view_zdir'],
+        up_dir:        ['property', 'vectors.ydir'],
+        userid:        ['property', 'properties.player_id'],
+        flying:        ['property', 'flying'],
+        walking:       ['property', 'walking'],
+        running:       ['property', 'running'],
+        //url:           ['property', 'currenturl'],
+        //hmd_enabled:   ['property', 'hmd_enabled'],
+        cursor_active: ['property', 'cursor_active'],
+        cursor_object: ['property', 'cursor_object'],
+        lookat_object: ['property', 'lookat_object'],
+        lookat_pos:    ['property', 'vectors.lookat_pos'],
+        //lookat_xdir:   ['property', 'properties.lookat_xdir'],
+        //lookat_ydir:   ['property', 'properties.lookat_ydir'],
+        //lookat_zdir:   ['property', 'properties.lookat_zdir'],
+        hand0_active:  ['property', 'hands.left.active'],
+        hand0_pos:     ['property', 'hands.left.position'],
+        hand0_xdir:    ['property', 'hands.left.xdir'],
+        hand0_ydir:    ['property', 'hands.left.ydir'],
+        hand0_zdir:    ['property', 'hands.left.zdir'],
+        hand1_active:  ['property', 'hands.right.active'],
+        hand1_pos:     ['property', 'hands.right.position'],
+        hand1_xdir:    ['property', 'hands.right.xdir'],
+        hand1_ydir:    ['property', 'hands.right.ydir'],
+        hand1_zdir:    ['property', 'hands.right.zdir'],
+        url:           ['property', 'parent.currentroom.url'],
+      });
+      return proxy;
     }
   }, elation.engine.things.player);
 });
diff --git a/scripts/janusweb.js b/scripts/janusweb.js
index 730c8c04d8c15c6c59de8acd93da0912f98bc17c..
index ..c51b4bda593dc24a5442e0d24a4df55470c66376 100644
--- a/scripts/janusweb.js
+++ b/scripts/janusweb.js
@@ -49,6 +49,9 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
         elation.engine.assets.setCORSProxy(this.properties.corsproxy);
       }
       elation.engine.assets.loadAssetPack(this.properties.datapath + 'assets.json');
+setTimeout(function() {
+        //elation.engine.assets.setPlaceholder('model', 'loading');
+}, 200);

       this.engine.systems.controls.addContext('janus', {
         'load_url': [ 'keyboard_tab', elation.bind(this, this.showLoadURL) ],
@@ -59,6 +62,7 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
       this.engine.systems.controls.activateContext('janus');
       this.remotePlayers = {};
       this.remotePlayerCount = 0;
+      this.playerCount = this.remotePlayerCount + 1;
       this.lastUpdate = Date.now();
       this.tmpMat = new THREE.Matrix4();
       this.tmpVecX = new THREE.Vector3();
@@ -66,13 +70,9 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
       this.tmpVecZ = new THREE.Vector3();
       this.sentUpdates = 0;
       this.updateRate = 15;
-      this.changes = {};
-
-      if (this.engine.systems.admin) {
-        elation.events.add(this.engine.systems.admin, 'admin_edit_change', elation.bind(this, this.handleRoomEditSelf));
-      }
     }
     this.initScripting = function() {
+      window.delta_time = 1000/60;
       window.janus = new elation.proxy(this, {
         version:           ['property', 'version',       { readonly: true}],
         versiononline:     ['property', 'versiononline', {readonly: true}],
@@ -81,7 +81,7 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
         networkstatus:     ['property', 'network.status', {readonly: true}],
         networkerror:      ['property', 'network.error', {readonly: true}],
         roomserver:        ['property', 'network.server'],
-        playercount:       ['property', 'remotePlayerCount'],
+        playercount:       ['property', 'playerCount'],
         bookmarkurl:       ['property', 'bookmarks.items'],
         bookmarkthumb:     ['property', 'bookmarks.items'], // FIXME - need to filter?
         playerlist:        ['property', ''],
@@ -118,45 +118,52 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay

       var player = this.engine.client.player;
       player.properties.player_id = this.userId; // FIXME - player spawns without an id, so we fix it up here
-      window.player = new elation.proxy(player, {
-        pos:           ['property', 'properties.position'],
-        //eye_pos:       ['property', 'eyes.properties.position'],
-        head_pos:       ['property', 'head.properties.position'],
-        cursor_pos:    ['property', 'vectors.cursor_pos'],
-        //cursor_xdir:    ['property', 'properties.cursor_xdir'],
-        //cursor_ydir:    ['property', 'properties.cursor_ydir'],
-        //cursor_zdir:    ['property', 'properties.cursor_zdir'],
-        view_dir:      ['property', 'vectors.view_zdir'],
-        dir:      ['property', 'vectors.zdir'],
-        userid:      ['property', 'properties.player_id'],
-        //url:      ['property', 'currenturl'],
-        //hmd_enabled:      ['property', 'hmd_enabled'],
-        //cursor_active:      ['property', 'cursor_active'],
-        //cursor_object:      ['property', 'cursor_object'],
-        //lookat_object:      ['property', 'lookat_object'],
-        //lookat_pos:    ['property', 'properties.lookat_position'],
-        //lookat_xdir:    ['property', 'properties.lookat_xdir'],
-        //lookat_ydir:    ['property', 'properties.lookat_ydir'],
-        //lookat_zdir:    ['property', 'properties.lookat_zdir'],
-        hand0_xdir:    ['property', 'vectors.hand0_xdir'],
-        hand0_ydir:    ['property', 'vectors.hand0_ydir'],
-        hand0_zdir:    ['property', 'vectors.hand0_zdir'],
-        hand1_xdir:    ['property', 'vectors.hand1_xdir'],
-        hand1_ydir:    ['property', 'vectors.hand1_ydir'],
-        hand1_zdir:    ['property', 'vectors.hand1_zdir'],
-      });
+      window.player = player.getProxyObject();
+
       window.Vector = function(x, y, z) {
+        if (y === undefined) y = x;
+        if (z === undefined) z = y;
         return new THREE.Vector3(x, y, z);
       }
+      window.translate = function(v1, v2) {
+        return new THREE.Vector3().addVectors(v1, v2);
+      }
       window.distance = function(v1, v2) {
         return v1.distanceTo(v2);
       }
+      window.scalarMultiply = function(v, s) {
+        var ret = new THREE.Vector3().copy(v);
+        if (s instanceof THREE.Vector3) {
+          ret.x *= s.x;
+          ret.y *= s.y;
+          ret.z *= s.z;
+        } else {
+          ret.multiplyScalar(s);
+        }
+        return ret;
+      }
+      window.cross = function(v1, v2) {
+        return new THREE.Vector3().crossVectors(v1, v2);
+      }
+      window.normalized = function(v) {
+        return new THREE.Vector3().copy(v).normalize();
+      }
+      window.equals = function(v1, v2) {
+        return v1.equals(v2);
+      }
       window.print = function() {
         console.log.apply(console, arguments);
       }
       window.debug = function() {
         console.log.apply(console, arguments);
       }
+      window.removeKey = function(dict, key) {
+        delete dict[key];
+      }
+      var uniqueId = 1;
+      window.uniqueId = function() {
+        return uniqueId++;
+      }
     }
     this.createChildren = function() {
       var hashargs = elation.url();
@@ -260,27 +267,37 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
         }
         this.currentroom.enable();

+        this.scriptframeargs = [
+          1000/60
+        ];
         window.room = new elation.proxy(this.currentroom, {
-          url:           ['property', 'properties.url', { readonly: true}],
+          url:           ['property', 'url', { readonly: true}],
           objects:       ['property', 'jsobjects'],
           cookies:       ['property', 'cookies'],
-          walk_speed:    ['property', 'properties.walk_speed'],
-          run_speed:     ['property', 'properties.run_speed'],
-          jump_velocity: ['property', 'properties.jump_velocity'],
-          gravity:       ['property', 'properties.gravity'],
-
+          walk_speed:    ['property', 'walk_speed'],
+          run_speed:     ['property', 'run_speed'],
+          jump_velocity: ['property', 'jump_velocity'],
+          gravity:       ['property', 'gravity'],
+          fog:           ['property', 'fog'],
+          fog_mode:      ['property', 'fog_mode'], 
+          fog_density:   ['property', 'fog_density'],
+          fog_start:     ['property', 'fog_start'],
+          fog_end:       ['property', 'fog_end'],
+          fog_col:       ['property', 'fog_col'],
+  
           createObject:  ['function', 'createObject'],
-          removeObject:  ['function', 'remove'],
+          removeObject:  ['function', 'removeObject'],
           addCookie:     ['function', 'addCookie'],
           playSound:     ['function', 'playSound'],
+          stopSound:     ['function', 'stopSound'],
           getObjectById: ['function', 'getObjectById'],

           onLoad:        ['callback', 'janus_room_scriptload'],
-          update:        ['callback', 'engine_frame', 'engine'],
-          onCollision:   ['callback', 'physics_collide'],
+          update:        ['callback', 'janusweb_script_frame', null, this.scriptframeargs],
+          onCollision:   ['callback', 'physics_collide', 'objects.dynamics'],
           onClick:       ['callback', 'click', 'engine.client.container'],
-          onMouseDown:   ['callback', 'mousedown', 'engine.client.container'],
-          onMouseUp:     ['callback', 'mouseup', 'engine.client.container'],
+          onMouseDown:   ['callback', 'janus_room_mousedown'],
+          onMouseUp:     ['callback', 'janus_room_mouseup'],
           onKeyDown:     ['callback', 'janus_room_keydown'],
           onKeyUp:       ['callback', 'janus_room_keyup']
         });
@@ -355,6 +372,7 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
         }
         delete this.remotePlayers[msg.data.data.userId];
         this.remotePlayerCount = Object.keys(this.remotePlayers).length;
+        this.playerCount = this.remotePlayerCount + 1;
       } else if (method == 'user_portal') {
         var data = msg.data.data;
         var portalname = 'portal_' + data.userId + '_' + md5(data.url);
@@ -403,7 +421,7 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
     this.spawnRemotePlayer = function(data) {
       var userId = data.position._userId;
       var spawnpos = (data.position.pos ? data.position.pos.split(" ").map(parseFloat) : [0,0,0]);
-      this.remotePlayers[userId] = this.currentroom.spawn('remoteplayer', userId, { position: spawnpos, player_id: userId, player_name: userId});
+      this.remotePlayers[userId] = this.currentroom.spawn('remoteplayer', userId, { position: spawnpos, player_id: userId, player_name: userId, pickable: false, collidable: false});
       var remote = this.remotePlayers[userId];
       remote.janusDirs = {
         tmpVec1: new THREE.Vector3(),
@@ -412,6 +430,7 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
         tmpMat4: new THREE.Matrix4()
       }
       this.remotePlayerCount = Object.keys(this.remotePlayers).length;
+      this.playerCount = this.remotePlayerCount + 1;
       return remote;
     }
     this.moveRemotePlayer = function(data) {
@@ -487,13 +506,52 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
         moveData.audio = window.btoa(binary);
         moveData.anim_id = "speak";
       }
-      var changeids = Object.keys(this.changes);
+      var changeids = Object.keys(this.currentroom.changes);
       var changestr = '';
-      changeids.forEach(elation.bind(this, function(id) {
-        changestr += this.changes[id];
-        delete this.changes[id];
-      }));
+      if (changeids.length > 0) {
+        var xmldoc = document.implementation.createDocument(null, 'edit', null);
+        var editroot = xmldoc.documentElement;
+
+        var typemap = {
+          janustext: 'Text',
+          janusobject: 'Object',
+        };
+
+        changeids.forEach(elation.bind(this, function(id) {
+          //changestr += this.currentroom.changes[id];
+          var change = this.currentroom.changes[id];
+          var real = this.currentroom.getObjectFromProxy(change);
+          if (real) {
+            var xmltype = typemap[real.type] || 'Object';
+            xmlnode = xmldoc.createElement(xmltype); // FIXME - determine object's type
+            
+            var attrs = Object.keys(change);
+            for (var i = 0; i < attrs.length; i++) {
+              var k = attrs[i];
+              var val = change[k];
+              if (val instanceof THREE.Vector2 ||
+                  val instanceof THREE.Vector3) {
+                val = val.toArray().join(',');
+              } else if (val instanceof THREE.Color) {
+                val = val.toArray().join(',');
+              }
+              if (val !== null && val !== undefined && typeof val != 'function') {
+                xmlnode.setAttribute(k, val);
+              }
+            }
+            editroot.appendChild(xmlnode);
+          }
+          delete this.currentroom.changes[id];
+        }));
+        this.currentroom.appliedchanges = {};
+        var serializer = new XMLSerializer();
+        changestr = serializer.serializeToString(xmldoc);
+        changestr = changestr.replace(/"/g, '^');
+        changestr = changestr.replace(/^/, '');
+        changestr = changestr.replace(/<\/edit>\s*$/, '');
+      }
       if (changestr != '') {
+        //console.log('SEND', changestr);
         moveData.room_edit = changestr;
       }
       if (document.activeElement && document.activeElement === this.chat.input.inputelement) {
@@ -552,6 +610,7 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
       if (room) {
         if (edit) {
           var editxml = edit.replace(/\^/g, '"');
+//console.log('RECV', editxml);
           room.applyEditXML(editxml);
         }
         if (del) {
@@ -560,13 +619,6 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
         }
       } 
     }
-    this.handleRoomEditSelf = function(ev) {
-      var thing = ev.data;
-      var change = thing.summarizeXML();
-      if (thing.properties.js_id) {
-        this.changes[thing.properties.js_id] = change;
-      }
-    }
     this.getCurrentURL = function() {
       return this.properties.url;
     }
@@ -617,5 +669,15 @@ elation.require(['janusweb.config', 'engine.things.generic','janusweb.remoteplay
     this.hasFocus = function() {
       return true;
     }
+    this.sendScriptFrame = function(ev) {
+/*
+      this.engine.systems.world.scene['world-3d'].updateMatrixWorld(true);
+      this.scriptframeargs[0] = ev.data.delta * 1000;
+      //elation.events.fire({element: this.currentroom, type: 'janusweb_script_frame'});
+      if (this.currentroom.update) {
+        this.currentroom.update(1000/60);
+      }
+*/
+    }
   }, elation.engine.things.generic);
 });
diff --git a/scripts/object.js b/scripts/object.js
index 2655ed00e25d30d11be30b96154ba3c0d54e2377..
index ..881e790b5e378f6bb07564c9967ca7a6e36374c8 100644
--- a/scripts/object.js
+++ b/scripts/object.js
@@ -8,49 +8,83 @@ elation.require(['janusweb.janusbase'], function() {
     this.postinit = function() {
       elation.engine.things.janusobject.extendclass.postinit.call(this);
       this.defineProperties({
-        room: { type: 'object' },
-        image_id: { type: 'string' },
-        video_id: { type: 'string' },
+        janusid: { type: 'string', refreshGeometry: true },
+        image_id: { type: 'string', set: this.updateMaterial },
+        video_id: { type: 'string', set: this.updateMaterial },
         loop: { type: 'boolean' },
         collision_id: { type: 'string' },
-        websurface_id: { type: 'string' },
-        lighting: { type: 'boolean', default: true },
-        col: { type: 'string' },
-        cull_face: { type: 'string', default: 'back' },
-        blend_src: { type: 'string', default: 'src_alpha' },
-        blend_dest: { type: 'string', default: 'one_minus_src_alpha' },
-        rotate_axis: { type: 'string', default: '0 1 0' },
-        rotate_deg_per_sec: { type: 'string' },
-        props: { type: 'object' },
+        websurface_id: { type: 'string', set: this.updateMaterial },
+        lighting: { type: 'boolean', default: true, set: this.updateMaterial },
+        cull_face: { type: 'string', default: 'back', set: this.updateMaterial },
+        blend_src: { type: 'string', default: 'src_alpha', set: this.updateMaterial },
+        blend_dest: { type: 'string', default: 'one_minus_src_alpha', set: this.updateMaterial },
       });
-      elation.events.add(this, 'thing_init3d', elation.bind(this, this.assignTextures));
+      //elation.events.add(this, 'thing_init3d', elation.bind(this, this.assignTextures));
     }
-    this.createChildren = function() {
-      if (this.objects['3d'].userData.loaded) {
-        //this.assignTextures();
+    this.createObject3D = function() {
+      if (this.properties.exists === false) return;
+
+      var object = null, geometry = null, material = null;
+      if (this.janusid) {
+        object = elation.engine.assets.find('model', this.janusid);
+        if (object.userData.loaded) {
+          this.assignTextures();
+        } else {
+          elation.events.add(object, 'asset_load', elation.bind(this, this.assignTextures));
+        }
       } else {
-        elation.events.add(this.objects['3d'], 'asset_load', elation.bind(this, this.assignTextures));
+        object = new THREE.Object3D();
       }
+
+      return object;
+    }
+    this.createChildren = function() {
+      //this.properties.collidable = false;
+      //this.updateColliderFromGeometry(new THREE.BoxGeometry(1,1,1));
     }
     this.createForces = function() {
-      var rotate_axis = this.properties.rotate_axis,
-          rotate_speed = this.properties.rotate_deg_per_sec;
-      if (rotate_axis && rotate_speed) {
-        var speed = (rotate_speed * Math.PI/180);
-        var axisparts = rotate_axis.split(' ');
-        var axis = new THREE.Vector3().set(axisparts[0], axisparts[1], axisparts[2]);
-        axis.multiplyScalar(speed);
-        this.objects.dynamics.setAngularVelocity(axis);
-      }
+      elation.engine.things.janusobject.extendclass.createForces.call(this);

-      if (this.properties.collision_id) {
-        var collider = elation.engine.assets.find('model', this.properties.collision_id);
-        if (collider.userData.loaded) {
-          this.extractColliders(collider, true);
+      if (this.collision_id) {
+        this.collidable = true;
+        if (this.collision_id == 'sphere') {
+          this.setCollider('sphere', {radius: Math.max(this.scale.x, this.scale.y, this.scale.z)});
+        } else if (this.collision_id == 'cube') {
+          var halfsize = this.properties.scale.clone().multiplyScalar(.5);
+          this.setCollider('box', {min: halfsize.clone().negate(), max: halfsize});
+        } else if (this.collision_id == 'plane') {
+          var halfsize = this.properties.scale.clone().multiplyScalar(.5);
+          this.setCollider('box', {min: halfsize.clone().negate(), max: halfsize});
+        } else if (this.collision_id == 'cylinder') {
+          this.setCollider('cylinder', {height: this.scale.y, radius: Math.max(this.scale.x, this.scale.z) / 2, offset: new THREE.Vector3(0, 0.5 * this.scale.y, 0)});
         } else {
-          elation.events.add(collider, 'asset_load', elation.bind(this, function(ev) { this.extractColliders(collider, true); }) );
+          var collider = elation.engine.assets.find('model', this.collision_id);
+          this.collidermesh = collider;
+          if (collider.userData.loaded) {
+            //this.extractColliders(collider, true);
+            collider.userData.thing = this;
+            this.colliders.add(collider);
+          } else {
+            elation.events.add(collider, 'asset_load', elation.bind(this, function(ev) {  
+              collider.userData.thing = this;
+              this.extractColliders(collider, true);
+/*
+              //collider.bindPosition(this.position);
+              //collider.bindQuaternion(this.orientation);
+              //collider.bindScale(this.properties.scale);
+
+              collider.traverse(elation.bind(this, function(n) {
+                if (n.geometry) {
+                  n.geometry.computeVertexNormals();
+                }
+                if (n.material) n.material = new THREE.MeshLambertMaterial({color: 0x999900, opacity: .2, transparent: true, emissive: 0x444400, alphaTest: .01, depthTest: false, depthWrite: false});
+                n.userData.thing = this;
+              }));
+              this.colliders.add(collider);
+*/
+            }) );
+          }
         }
-        
       }
     }
     this.createObjectDOM = function() {
@@ -79,8 +113,12 @@ elation.require(['janusweb.janusbase'], function() {
         }
       }
     }
+    this.updateMaterial = function() {
+      this.assignTextures();
+    }
     this.assignTextures = function() {
-      //console.log('assign textures', this.name, this);
+      //console.log('assign textures', this.name, this.objects['3d']);
+      if (!this.objects['3d']) return;
       var modelasset = false,
           texture = false,
           color = false,
@@ -88,18 +126,23 @@ elation.require(['janusweb.janusbase'], function() {
           blend_dest = false,
           side = this.sidemap[this.properties.cull_face];

-      if (this.properties.render.model) {
-        modelasset = elation.engine.assets.find('model', this.properties.render.model, true);
+      if (this.janusid) {
+        modelasset = elation.engine.assets.find('model', this.janusid, true);
       }
       if (this.properties.image_id) {
-        texture = elation.engine.assets.find('image', this.properties.image_id);
+        texture = elation.engine.assets.find('image', this.image_id);
+        if (!texture) {
+          var asset = { assettype:'image', name:this.image_id, src: this.image_id, baseurl: this.baseurl }; 
+          elation.engine.assets.loadJSON([asset], this.baseurl); 
+          texture = elation.engine.assets.find('image', this.image_id);
+        }
         elation.events.add(texture, 'asset_load', elation.bind(this, this.assignTextures));
         elation.events.add(texture, 'update', elation.bind(this, this.refresh));
       }
       if (this.properties.video_id) {
         var videoasset = elation.engine.assets.find('video', this.properties.video_id, true);
         if (videoasset) {
-          texture = videoasset.getAsset();
+          texture = videoasset.getInstance();
           if (videoasset.sbs3d) {
             texture.repeat.x = 0.5;
           }
@@ -116,14 +159,16 @@ elation.require(['janusweb.janusbase'], function() {
           this.videotexture = texture;
         }
       }
+      color = this.properties.color;
+/*
       if (this.properties.col) {
-        color = new THREE.Color();
         var col = this.properties.col;
         if (!col && modelasset && modelasset.col) {
           col = modelasset.col;
         }
-        color.setRGB(col[0], col[1], col[2]);  
+        //color.setRGB(col.x, col.y, col.z);
       }
+*/
       var srcfactors = {
         'src_alpha': THREE.SrcAlphaFactor,
         'zero': THREE.ZeroFactor,
@@ -141,36 +186,48 @@ elation.require(['janusweb.janusbase'], function() {
       if (srcfactors[this.properties.blend_dest]) {
         blend_dest = srcfactors[this.properties.blend_dest];
       }
-
-      var hasalpha = {};
+      var scene = this.engine.systems.world.scene['world-3d'];
+      if (!this.hasalpha) this.hasalpha = {};
+      var hasalpha = this.hasalpha;
+      var remove = [];
+      var cloneMaterial = false;
       this.objects['3d'].traverse(elation.bind(this, function(n) { 
         if (n.material) {
           var materials = [];
           if (n.material instanceof THREE.MeshFaceMaterial) {
             //materials = [n.material.materials[1]];
             for (var i = 0; i < n.material.materials.length; i++) {
-              var m = this.copyMaterial(n.material.materials[i]);
-              materials.push(m); 
+              if (cloneMaterial) {
+                var m = this.copyMaterial(n.material.materials[i]);
+                materials.push(m); 
+              } else {
+                materials.push(n.material.materials[i]);
+              }
             }
             n.material.materials = materials;
           } else {
+/*
             var m = this.copyMaterial(n.material);
             materials.push(m); 
             n.material = m;
+*/
+            materials.push(n.material);
           }

           for (var i = 0; i < materials.length; i++) {
             var m = materials[i];
-            if (texture) {
+            //m.envMap = scene.background;
+            if (texture && texture.image) {
               m.map = texture; 
             }
             if (m.map && m.map.image) {
+/*
               if (m.map.image instanceof HTMLCanvasElement) {
                 // FIXME - don't think this works
-                hasalpha[m.map.image.src] = this.canvasHasAlpha(m.map.image);
+                //hasalpha[m.map.image.src] = this.canvasHasAlpha(m.map.image);
                 if (hasalpha[m.map.image.src]) {
                   m.transparent = true;
-                  m.alphaTest = 0.1;
+                  m.alphaTest = 0.01;
                   m.needsUpdate = true;
                 }
               } else if (m.map.image.src && m.map.image.src.match(/\.(png|tga)$/)) {
@@ -184,53 +241,52 @@ elation.require(['janusweb.janusbase'], function() {
                     var ctx = canvas.getContext('2d');
                     ctx.drawImage(m.map.image, 0, 0);

-                    hasalpha[ev.target.src] = this.canvasHasAlpha(canvas);
+                    //hasalpha[ev.target.src] = this.canvasHasAlpha(canvas);
                     m.map.image = canvas;
                   }
                   if (hasalpha[ev.target.src]) {
                     m.transparent = true;
-                    m.alphaTest = 0.1;
+                    m.alphaTest = 0.01;
                     m.needsUpdate = true;
                   }
                 }));
               }
+*/
             }
-            m.roughness = 0.75;
-            if (color && m.color) {
-              m.color.copy(color);
+            //m.roughness = 0.75;
+            if (color) {
+              m.color = color;
             }
             if (side) {
               m.side = side;
             }
             if (blend_src) m.blendSrc = blend_src;
             if (blend_dest) m.blendDst = blend_dest;
-            //m.needsUpdate = true;
+            m.needsUpdate = true;
           }
-        };
+        } else if (n instanceof THREE.Light) {
+          remove.push(n);
+        }
       }));
+      for (var i = 0; i < remove.length; i++) {
+        remove[i].parent.remove(remove[i]);
+      }
       this.refresh();
     }
     this.copyMaterial = function(oldmat) {
-      var m = (this.properties.lighting != false || oldmat.lightMap ? new THREE.MeshPhongMaterial() : new THREE.MeshBasicMaterial());
+      //var m = (this.properties.lighting != false || oldmat.lightMap ? new THREE.MeshPhongMaterial() : new THREE.MeshBasicMaterial());
+      var m = new THREE.MeshPhongMaterial();
+      m.anisotropy = 16;
+      m.name = oldmat.name;
       m.map = oldmat.map;
       m.normalMap = oldmat.normalMap;
       m.lightMap = oldmat.lightMap;
       m.color.copy(oldmat.color);
       m.transparent = oldmat.transparent;
+      m.alphaTest = oldmat.alphaTest;

       return m;
     }
-    this.canvasHasAlpha = function(canvas) {
-      var ctx = canvas.getContext('2d');
-      var pixeldata = ctx.getImageData(0, 0, canvas.width, canvas.height);
-      var hasalpha = false;
-      for (var i = 3; i < pixeldata.data.length; i+=4) {
-        if (pixeldata.data[i] != 255) {
-          return true;
-        }
-      }
-      return false;
-    }
     this.pauseVideo = function() {
       if (this.videotexture) {
         var video = this.videotexture.image;
@@ -241,6 +297,19 @@ elation.require(['janusweb.janusbase'], function() {
         }
       }
     }
+    this.start = function() {
+      if (this.properties.image_id) {
+        var texture = elation.engine.assets.find('image', this.properties.image_id);
+        console.log('start the image!', texture);
+      }
+      if (this.properties.video_id) {
+        var texture = elation.engine.assets.find('video', this.properties.video_id);
+        if (!texture.image.playing) {
+          texture.image.play();
+          console.log('start the video!', texture);
+        }
+      }
+    }
     this.stop = function() {
       if (this.properties.image_id) {
         var texture = elation.engine.assets.find('image', this.properties.image_id);
@@ -248,8 +317,17 @@ elation.require(['janusweb.janusbase'], function() {
       }
       if (this.properties.video_id) {
         var texture = elation.engine.assets.find('video', this.properties.video_id);
+        texture.image.pause();
         console.log('stop the video!', texture);
       }
     }
+    this.getProxyObject = function() {
+      var proxy = elation.engine.things.janusobject.extendclass.getProxyObject.call(this);
+      proxy._proxydefs = {
+        id:  [ 'property', 'janusid'],
+        collision_id:  [ 'property', 'collision_id'],
+      };
+      return proxy;
+    }
   }, elation.engine.things.janusbase);
 });
diff --git a/scripts/portal.js b/scripts/portal.js
index a7296a70ebdaa9780bf184ef83d5ed32ce8682f3..
index ..dcaa3d71d4c68a22ab443cba4df396b16d90633b 100644
--- a/scripts/portal.js
+++ b/scripts/portal.js
@@ -1,11 +1,14 @@
-elation.require(['engine.things.portal'], function() {
+elation.require(['janusweb.janusbase'], function() {
   elation.component.add('engine.things.janusportal', function() {
     this.postinit = function() {
       this.defineProperties({
         'janus': { type: 'object' },
-        'url': { type: 'string' },
-        'title': { type: 'string' },
-        'thumbnail': { type: 'texture' }
+        //'color': { type: 'color', default: new THREE.Color(0xffffff), set: this.updateMaterial },
+        'url': { type: 'string', set: this.updateTitle },
+        'title': { type: 'string', set: this.updateTitle },
+        'draw_text': { type: 'boolean', default: true, set: this.updateTitle },
+        'draw_glow': { type: 'boolean', default: true, refreshGeometry: true},
+        'thumb_id': { type: 'string', set: this.updateMaterial }
       });
       this.addTag('usable');
       elation.engine.things.janusportal.extendclass.postinit.call(this);
@@ -17,75 +20,109 @@ elation.require(['engine.things.portal'], function() {
       var offset = ((thickness / 2) / this.properties.scale.z) * 2;
       var box = new THREE.BoxGeometry(1,1,thickness);
       box.applyMatrix(new THREE.Matrix4().makeTranslation(0,0.5,offset/2));
-      var matargs = { color: 0xdddddd };
-      if (this.properties.thumbnail) matargs.map = this.properties.thumbnail;
-      var mat = new THREE.MeshBasicMaterial(matargs);
+
+      var mat = this.createMaterial();
+
       var mesh = new THREE.Mesh(box, mat);

-      var framewidth = .05 / this.properties.scale.x, 
-          frameheight = .05 / this.properties.scale.y, 
-          framedepth = .01 / this.properties.scale.z;
-      var framegeo = new THREE.Geometry();
-      var framepart = new THREE.BoxGeometry(1,frameheight,framedepth);
-      var framemat4 = new THREE.Matrix4();
+      if (this.draw_glow) {
+        var framewidth = .05 / this.properties.scale.x, 
+            frameheight = .05 / this.properties.scale.y, 
+            framedepth = .01 / this.properties.scale.z;
+        var framegeo = new THREE.Geometry();
+        var framepart = new THREE.BoxGeometry(1,frameheight,framedepth);
+        var framemat4 = new THREE.Matrix4();


-      framemat4.makeTranslation(0,1 - frameheight/2,framedepth/2 + offset);
-      framegeo.merge(framepart, framemat4);
-      framemat4.makeTranslation(0,frameheight/2,framedepth/2 + offset);
-      framegeo.merge(framepart, framemat4);
-      
-      framepart = new THREE.BoxGeometry(framewidth,1,framedepth);
+        framemat4.makeTranslation(0,1 - frameheight/2,framedepth/2 + offset);
+        framegeo.merge(framepart, framemat4);
+        framemat4.makeTranslation(0,frameheight/2,framedepth/2 + offset);
+        framegeo.merge(framepart, framemat4);
+        
+        framepart = new THREE.BoxGeometry(framewidth,1,framedepth);

-      framemat4.makeTranslation(.5 - framewidth/2,.5,framedepth/2 + offset);
-      framegeo.merge(framepart, framemat4);
-      framemat4.makeTranslation(-.5 + framewidth/2,.5,framedepth/2 + offset);
-      framegeo.merge(framepart, framemat4);
+        framemat4.makeTranslation(.5 - framewidth/2,.5,framedepth/2 + offset);
+        framegeo.merge(framepart, framemat4);
+        framemat4.makeTranslation(-.5 + framewidth/2,.5,framedepth/2 + offset);
+        framegeo.merge(framepart, framemat4);

-      var framemat = new THREE.MeshPhongMaterial({color: 0x0000cc, emissive: 0x222222});
-      var frame = new THREE.Mesh(framegeo, framemat);
-      this.frame = frame;
+        var framemat = new THREE.MeshPhongMaterial({color: 0x0000cc, emissive: 0x222222});
+        var frame = new THREE.Mesh(framegeo, framemat);
+        this.frame = frame;
+        mesh.add(frame);
+      }
       this.material = mat;
-      mesh.add(frame);
+
+      this.objects['3d'] = mesh;
+
+      this.updateTitle();
+
       return mesh;
     }
+    this.updateTitle = function() {
+      if (this.draw_text) {
+        var title = this.title || this.url;
+        if (title) {
+          if (this.flatlabel) {
+            this.flatlabel.setText(title);
+            this.flatlabel.scale = [1/this.properties.scale.x, 1/this.properties.scale.y, 1/this.properties.scale.z];
+          } else {
+            this.flatlabel = this.spawn('label2d', this.id + '_label', { 
+              text: title, 
+              position: [0, .75, .15],
+              persist: false,
+              color: 0x0000ee,
+              emissive: 0x222266,
+              scale: [1/this.scale.x, 1/this.scale.y, 1/this.scale.z],
+              thickness: 0.5,
+              collidable: false
+            });
+          }
+        }
+      } else if (this.flatlabel) {
+        this.flatlabel.setText('');
+      }
+    }
     this.createChildren = function() {
       this.updateColliderFromGeometry();
-      if (this.properties.render.collada || this.properties.render.meshname) {
-        this.child = this.spawn('generic', this.id + '_model', {
-          'render.collada': this.properties.render.collada, 
-          'render.meshname': this.properties.render.meshname, 
-          'position': this.properties.childposition,
-          'orientation': this.properties.childorientation.clone(),
-          'scale': this.properties.childscale.clone(),
-          persist: false,
-        });
-        elation.events.add(this.child, 'mouseover,mouseout,click', this);
-      }
-      if (this.properties.title) {
-        this.flatlabel = this.spawn('label2d', this.id + '_label', { 
-          text: this.properties.title, 
-          position: [0, .75, .15],
-          persist: false,
-          color: 0x0000ee,
-          emissive: 0x222266,
-          scale: [1/this.properties.scale.x, 1/this.properties.scale.y, 1/this.properties.scale.z],
-          thickness: 0.5,
-/*
-          'bevel.enabled': true,
-          'bevel.thickness': 0.025,
-          'bevel.size': 0.025,
-*/
-          collidable: false
-        });
         //elation.events.add(this.label, 'mouseover,mousemove,mouseout,click', this);
+    }
+    this.createMaterial = function() {
+      var matargs = { color: this.properties.color };
+      var mat;
+      if (this.thumb_id) {
+        var asset = elation.engine.assets.find('image', this.thumb_id, true);
+        if (!asset) {
+          var asset = elation.engine.assets.get({ assettype:'image', id: this.thumb_id, src: this.thumb_id, baseurl: this.room.baseurl }); 
+        }
+        if (asset) var thumb = asset.getInstance();
+        if (thumb) {
+          matargs.map = thumb;
+          if (asset.loaded) {
+            if (asset.hasalpha) {
+              matargs.transparent = true;
+              matargs.alphaTest = 0.1;
+            }
+          } else {
+            elation.events.add(thumb, 'asset_load', function() {
+              if (mat && asset.hasalpha) {
+                mat.transparent = true;
+                mat.alphaTest = 0.1;
+              }
+            });
+          }
+        }
       }
+      mat = new THREE.MeshBasicMaterial(matargs);
+      mat.color = this.properties.color;
+      return mat;
     }
-    this.hover = function() {
-      if (this.child) {
-        this.child.objects.dynamics.setAngularVelocity(new THREE.Vector3(0,Math.PI/4,0));
-        this.child.refresh();
+    this.updateMaterial = function() {
+      if (this.objects['3d'] && this.objects['3d'].material) {
+        this.material = this.objects['3d'].material = this.createMaterial();
       }
+    }
+    this.hover = function() {
       if (this.label) {
         this.label.setEmissionColor(0x2222aa);
       }
@@ -142,9 +179,11 @@ elation.require(['engine.things.portal'], function() {
     this.click = function(ev) {
     }
     this.activate = function() {
-      this.frame.material.emissive.setHex(0x662222);
+      if (this.frame) {
+        this.frame.material.emissive.setHex(0x662222);
+        setTimeout(elation.bind(this, function() { this.frame.material.emissive.setHex(0x222222); }), 250);
+      }
       this.properties.janus.setActiveRoom(this.properties.url, [0,0,0]);
-      setTimeout(elation.bind(this, function() { this.frame.material.emissive.setHex(0x222222); }), 250);
       elation.events.fire({element: this, type: 'janusweb_portal_click'});
     }
     this.canUse = function(object) {
@@ -157,25 +196,19 @@ elation.require(['engine.things.portal'], function() {
       }
     }
     this.useFocus = function(ev) {
-      console.log('focus:', this.properties.gamename);
       this.hover();
     }
     this.useBlur = function(ev) {
-      console.log('blur:', this.properties.gamename);
       this.unhover();
     }
     this.getProxyObject = function() {
-      return new elation.proxy(this, {
-        id: ['property', 'properties.id'],
-        js_id: ['property', 'properties.js_id'],
-        pos: ['property', 'properties.position'],
-        vel: ['property', 'properties.velocity'],
-        scale: ['property', 'properties.scale'],
-        col: ['property', 'properties.col'],
-        url: ['property', 'properties.url'],
-        title: ['property', 'properties.title'],
-        thumbnail: ['property', 'properties.thumbnail'],
-      });
+      var proxy = elation.engine.things.janusobject.extendclass.getProxyObject.call(this);
+      proxy._proxydefs = {
+        url: ['property', 'url'],
+        title: ['property', 'title'],
+        thumb_id: ['property', 'thumb_id'],
+      };
+      return proxy;
     }
   }, elation.engine.things.janusbase);
 });
diff --git a/scripts/remoteplayer.js b/scripts/remoteplayer.js
index 9e179527ad347ae4eb76b433a2ae900c958768b8..
index ..61b4c1906b9dcaf477dc63490c66340490e5487e 100644
--- a/scripts/remoteplayer.js
+++ b/scripts/remoteplayer.js
@@ -3,6 +3,8 @@ elation.component.add('engine.things.remoteplayer', function() {
   this.postinit = function() {
     this.defineProperties({
       startposition: {type: 'vector3', default: new THREE.Vector3()},
+      pickable: {type: 'boolean', default: false},
+      collidable: {type: 'boolean', default: false},
       player_id: {type: 'string', default: 'UnknownPlayer'},
       player_name: {type: 'string', default: 'UnknownPlayer'},
     });
@@ -32,7 +34,9 @@ elation.component.add('engine.things.remoteplayer', function() {
       'position': [0,0,-0.125],
       collidable: false,
       'tilesize': 0.075,
-      'player_id': this.properties.player_name
+      'player_id': this.properties.player_name,
+      pickable: false,
+      collidable: false
     });
     this.label = this.face.spawn('label', this.properties.player_name + '_label', {
       size: .1,
@@ -40,7 +44,9 @@ elation.component.add('engine.things.remoteplayer', function() {
       collidable: false,
       text: this.properties.player_name,
       position: [0,0.35,0],
-      orientation: [0,1,0,0]
+      orientation: [0,1,0,0],
+      pickable: false,
+      collidable: false
     });
     this.mouth = this.face.spawn('sound', this.properties.player_name + '_voice', {
       //loop: true
@@ -49,7 +55,7 @@ elation.component.add('engine.things.remoteplayer', function() {
     var context = this.mouth.audio.context;
     this.voip = new JanusVOIPPlayer();
     this.voip.start(context);
-    this.audiobuffer = new THREE.AudioBuffer(this.mouth.audio.context);
+    this.audiobuffer = {readyCallbacks: []};//new THREE.AudioBuffer(this.mouth.audio.context);
     this.audiobuffer.buffer = this.voip.rawbuffer;

     //elation.events.add(this.voip, 'voip_player_data', elation.bind(this, this.handleVoipData));
@@ -60,7 +66,7 @@ elation.component.add('engine.things.remoteplayer', function() {

     }

-    this.mouth.audio.setBuffer(this.audiobuffer);
+    //this.mouth.audio.setBuffer(this.audiobuffer);
   };
   this.speak = function(noise) {
     this.voip.speak(noise);
diff --git a/scripts/room.js b/scripts/room.js
index 846b314df26746734f7e672cf9a394f0a8f26dba..
index ..24947fcd76749e1e988874984b199049dda53cb0 100644
--- a/scripts/room.js
+++ b/scripts/room.js
@@ -1,20 +1,29 @@
 elation.require([
     'ui.textarea', 'ui.window', 
      'engine.things.generic', 'engine.things.label', 'engine.things.skybox',
-    'janusweb.object', 'janusweb.portal', 'janusweb.image', 'janusweb.video', 'janusweb.text', 'janusweb.sound',
+    'janusweb.object', 'janusweb.portal', 'janusweb.image', 'janusweb.video', 'janusweb.text', 'janusweb.sound', 'janusweb.januslight',
     'janusweb.translators.bookmarks', 'janusweb.translators.reddit', 'janusweb.translators.error'
   ], function() {
   elation.component.add('engine.things.janusroom', function() {
     this.postinit = function() {
+      elation.engine.things.janusroom.extendclass.postinit.call(this);
       this.defineProperties({
         'janus': { type: 'object' },
         'url': { type: 'string', default: false },
+/*
         'skybox_left': { type: 'string', default: 'skyrender_left' },
         'skybox_right': { type: 'string', default: 'skyrender_right' },
         'skybox_up': { type: 'string', default: 'skyrender_up' },
         'skybox_down': { type: 'string', default: 'skyrender_down' },
         'skybox_front': { type: 'string', default: 'skyrender_front' },
         'skybox_back': { type: 'string', default: 'skyrender_back' },
+*/
+        'skybox_left': { type: 'string' },
+        'skybox_right': { type: 'string' },
+        'skybox_up': { type: 'string' },
+        'skybox_down': { type: 'string' },
+        'skybox_front': { type: 'string' },
+        'skybox_back': { type: 'string' },
         'fog': { type: 'boolean', default: false },
         'fog_mode': { type: 'string', default: 'exp' },
         'fog_density': { type: 'float', default: 1.0 },
@@ -26,6 +35,7 @@ elation.require([
         'jump_velocity': { type: 'float', default: 5.0 },
         'gravity': { type: 'float', default: -9.8 },
         'locked': { type: 'bool', default: false },
+        'cursor_visible': { type: 'bool', default: true },
       });
       this.translators = {
         '^bookmarks$': elation.janusweb.translators.bookmarks({}),
@@ -34,31 +44,44 @@ elation.require([
       };
       this.playerstartposition = [0,0,0];
       this.playerstartorientation = new THREE.Quaternion();
-      this.load();
+      this.load(this.properties.url);
       this.roomsrc = '';
+      this.changes = {};
+      this.appliedchanges = {};
+      if (this.engine.systems.admin) {
+        elation.events.add(this.engine.systems.admin, 'admin_edit_change', elation.bind(this, this.onRoomEdit));
+      }
       //this.showDebug();
-      elation.events.add(this.engine.client.container, 'keydown', elation.bind(this, this.onKeyDown));
-      elation.events.add(this.engine.client.container, 'keyup', elation.bind(this, this.onKeyUp));
+      elation.events.add(window, 'keydown', elation.bind(this, this.onKeyDown));
+      elation.events.add(window, 'keyup', elation.bind(this, this.onKeyUp));
+
+      elation.events.add(this.engine.client.container, 'mousedown', elation.bind(this, this.onMouseDown));
+      elation.events.add(this.engine.client.container, 'mouseup', elation.bind(this, this.onMouseUp));
     }
     this.createChildren = function() {
       this.spawn('light_ambient', this.id + '_ambient', {
-        color: 0x333333
+        color: 0x666666
       });
       this.spawn('light_directional', this.id + '_sun', {
         position: [-20,50,25],
-        intensity: 0.2
+        intensity: 0.1
       });
       this.spawn('light_point', this.id + '_point', {
         position: [22,19,-15],
-        intensity: 0.2
+        intensity: 0.1
       });
+
+      this.lastthink = 0;
+      this.thinktime = 0;
+      elation.events.add(this, 'thing_think', elation.bind(this, this.onScriptTick));
     }
     this.setActive = function() {
+      this.setSkybox();
       this.setFog();
       this.setNearFar();
       this.setPlayerPosition();
       this.active = true;
-      elation.events.fire({type: 'room_active', data: this});
+      elation.events.fire({element: this, type: 'room_active', data: this});
     }
     this.setPlayerPosition = function(pos, orientation) {
       if (!pos) {
@@ -73,8 +96,9 @@ elation.require([
         // HACK - we actually want the angle opposite to this
         this.engine.client.player.properties.orientation.multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(0,Math.PI,0)));
       }
-      player.properties.movespeed = this.properties.walk_speed * 100;
-      player.properties.runspeed = this.properties.run_speed * 100;
+      player.properties.movespeed = this.properties.walk_speed;
+      player.properties.runspeed = this.properties.run_speed;
+      player.cursor_visible = this.cursor_visible;
     }
     this.setSkybox = function() {
       if (!this.skybox) {
@@ -216,11 +240,17 @@ elation.require([
           this.roomsrc = this.parseSource(document.documentElement.outerHTML);
           var roomdata = this.parseFireBox(this.roomsrc.source);
           this.createRoomObjects(roomdata);
+          this.setActive();
+          elation.events.fire({type: 'janus_room_load', element: this});
         }), 0);
       } else if (translator) {
         setTimeout(elation.bind(this, function() {
           translator.exec({url: url, janus: this.properties.janus, room: this})
-                    .then(elation.bind(this, this.createRoomObjects));
+                    .then(elation.bind(this, function(objs) {
+                      this.createRoomObjects(objs);
+                      this.setActive();
+                      elation.events.fire({type: 'janus_room_load', element: this});
+                    }));
         }), 0);
       } else {
         elation.net.get(fullurl, null, { 
@@ -248,6 +278,8 @@ elation.require([
               }
               var roomdata = this.parseFireBox(source.source);
               this.createRoomObjects(roomdata);
+              this.setActive();
+              elation.events.fire({type: 'janus_room_load', element: this});
             } else {
               var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
               var transpath = datapath + 'assets/translator/web/';
@@ -314,168 +346,33 @@ elation.require([
           images = roomdata.images || [],
           image3ds = roomdata.image3ds || [],
           texts = roomdata.texts || [],
+          paragraphs = roomdata.paragraphs || [],
+          lights = roomdata.lights || [],
           videos = roomdata.videos || [];

-      objects.forEach(elation.bind(this, function(n) { 
-        var thingname = n.id + (n.js_id ? '_' + n.js_id : '_' + Math.round(Math.random() * 1000000));
-setTimeout(elation.bind(this, function() {
-        var thing = this.spawn('janusobject', thingname, { 
-          'room': this,
-          'js_id': n.js_id,
-          'render.model': n.id, 
-          'position': n.pos,
-          'orientation': n.orientation,
-          //'scale': n.scale,
-          'image_id': n.image_id,
-          'video_id': n.video_id,
-          'loop': n.loop,
-          'collision_id': n.collision_id,
-          'websurface_id': n.websurface_id,
-          'col': n.col,
-          'cull_face': n.cull_face,
-          'blend_src': n.blend_src,
-          'blend_dest': n.blend_dest,
-          'rotate_axis': n.rotate_axis,
-          'rotate_deg_per_sec': n.rotate_deg_per_sec,
-          'props': n,
-          'visible': n.visible,
-          'lighting': n.lighting
-        }); 
-        thing.setProperties(n);
-        if (n.js_id) {
-          this.jsobjects[n.js_id] = thing.getProxyObject();
-        }
-}), Math.random() * 500);
+      if (lights) lights.forEach(elation.bind(this, function(n) {
+        this.createObject('light',  n);
+      }));
+      if (objects) objects.forEach(elation.bind(this, function(n) { 
+        this.createObject('object', n);
       }));
       if (links) links.forEach(elation.bind(this, function(n) {
-setTimeout(elation.bind(this, function() {
-        var linkurl = (n.url.match(/^https?:/) || n.url.match(/^\/\//) || this.getTranslator(n.url) ? n.url : this.baseurl + n.url);
-        var portalargs = { 
-          'room': this,
-          'js_id': n.js_id,
-          'janus': this.properties.janus,
-          'position': n.pos,
-          'orientation': n.orientation,
-          'scale':n.scale,
-          'url': linkurl,
-          'title': n.title,
-        }; 
-        if (n.thumb_id) {
-          portalargs.thumbnail = elation.engine.assets.find('image', n.thumb_id);
-        }
-        var portal = this.spawn('janusportal', 'portal_' + n.url + '_' + Math.round(Math.random() * 10000), portalargs);
-        if (n.js_id) {
-          this.jsobjects[n.js_id] = portal.getProxyObject();
-        }
-}), Math.random() * 500);
+        this.createObject('link', n);
       }));
       if (images) images.forEach(elation.bind(this, function(n) {
-setTimeout(elation.bind(this, function() {
-        var imageargs = { 
-          'room': this,
-          'janus': this.properties.janus,
-          'js_id': n.js_id,
-          'position': n.pos,
-          'orientation': n.orientation,
-          'scale': n.scale,
-          'image_id': n.id,
-          'color': n.col,
-          'lighting': n.lighting
-        }; 
-        var asset = false;
-        if (assets.image) assets.image.forEach(function(img) {
-          if (img.id == n.id) {
-            asset = img;
-          }
-        });
-
-        if (asset) {
-          imageargs.sbs3d = asset.sbs3d;
-          imageargs.ou3d = asset.ou3d;
-          imageargs.reverse3d = asset.reverse3d;
-        }
-        var image = this.spawn('janusimage', n.id + '_' + Math.round(Math.random() * 10000), imageargs);
-        if (n.js_id) {
-          this.jsobjects[n.js_id] = image.getProxyObject();
-        }
-}), Math.random() * 500);
+        this.createObject('image', n);
       }));
       if (image3ds) image3ds.forEach(elation.bind(this, function(n) {
-        var imageargs = { 
-          'room': this,
-          'janus': this.properties.janus,
-          'js_id': n.js_id,
-          'position': n.pos,
-          'orientation': n.orientation,
-          'scale': n.scale,
-          'image_id': n.left_id,
-          'color': n.col,
-          'lighting': n.lighting
-        }; 
-        var image = this.spawn('janusimage', n.id + '_' + Math.round(Math.random() * 10000), imageargs);
-        if (n.js_id) {
-          this.jsobjects[n.js_id] = image.getProxyObject();
-        }
+        this.createObject('image', n);
       }));
       if (texts) texts.forEach(elation.bind(this, function(n) {
-        var labelargs = { 
-          'room': this,
-          'janus': this.properties.janus,
-          'js_id': n.js_id,
-          'position': n.pos,
-          'orientation': n.orientation,
-          'scale': n.scale,
-          'text': n._content || ' ',
-          'color': (n.col ? new THREE.Color().setRGB(n.col[0], n.col[1], n.col[2]) : new THREE.Color(0xffffff)),
-        }; 
-        var label = this.spawn('janustext', n.id + '_' + Math.round(Math.random() * 10000), labelargs);
-        if (n.js_id) {
-          this.jsobjects[n.js_id] = label.getProxyObject();
-        }
+        this.createObject('text', n);
       }));
-      var soundmap = {};
-      if (assets.sound) assets.sound.forEach(elation.bind(this, function(n) { 
-        soundmap[n.id] = n; 
-        var soundurl = (n.src.match(/^https?:/) || n.src[0] == '/' ? n.src : this.baseurl + n.src);
-
-        var proxyurl = this.properties.janus.properties.corsproxy;
-        soundurl = proxyurl + soundurl;
-        var sound = this.spawn('janussound', n.id + '_' + Math.round(Math.random() * 10000), { 
-          'room': this,
-          'js_id': n.js_id,
-          'position': n.pos,
-          'src': soundurl,
-          'distance': parseFloat(n.dist),
-          //'volume': n.scale[0],
-          'autoplay': false,
-          'loop': n.loop,
-        }); 
-        this.sounds[n.id] = sound;
+      if (paragraphs) lights.forEach(elation.bind(this, function(n) {
+        this.createObject('paragraph',  n);
       }));
       if (sounds) sounds.forEach(elation.bind(this, function(n) {
-        var soundargs = soundmap[n.id];
-        if (soundargs) {
-/*
-          var sound = this.spawn('janussound', n.id + '_' + Math.round(Math.random() * 10000), { 
-            'room': this,
-            'js_id': n.js_id,
-            'position': n.pos,
-            'src': soundurl,
-            'distance': parseFloat(n.dist),
-            'volume': n.scale[0],
-            'autoplay': true,
-            'loop': n.loop,
-          }); 
-*/
-          var sound = this.sounds[n.id];
-          sound.properties.autoplay = true;
-          if (n.js_id) {
-            this.jsobjects[n.js_id] = sound.getProxyObject();
-          }
-          this.sounds[n.id] = sound;
-        } else {
-          console.log("Couldn't find sound: " + n.id);
-        }
+        this.createObject('sound',  n);
       }));

       var videoassetmap = {};
@@ -502,13 +399,16 @@ setTimeout(elation.bind(this, function() {

       if (room) {
         if (room.use_local_asset && room.visible !== false) {
-setTimeout(elation.bind(this, function() {
-          var localasset = this.spawn('janusobject', 'local_asset_' + Math.round(Math.random() * 10000), { 
-            'room': this,
-            'render.model': room.use_local_asset,
-            'col': room.col,
-          }); 
-}), Math.random() * 500);
+//setTimeout(elation.bind(this, function() {
+        this.createObject('object', {
+          id: room.use_local_asset,
+          col: room.col,
+          fwd: room.fwd,
+          xdir: room.xdir,
+          ydir: room.ydir,
+          zdir: room.zdir
+        });
+//}), Math.random() * 500);
         }
         // set player position based on room info
         this.playerstartposition = room.pos;
@@ -521,7 +421,7 @@ setTimeout(elation.bind(this, function() {
         if (room.skybox_front_id) this.properties.skybox_front = room.skybox_front_id;
         if (room.skybox_back_id) this.properties.skybox_back = room.skybox_back_id;

-        this.setSkybox();
+        //this.setSkybox();

         this.properties.near_dist = parseFloat(room.near_dist) || 0.01;
         this.properties.far_dist = parseFloat(room.far_dist) || 1000;
@@ -534,11 +434,14 @@ setTimeout(elation.bind(this, function() {

         this.properties.walk_speed = room.walk_speed || 1.8;
         this.properties.run_speed = room.run_speed || 5.4;
+        this.properties.cursor_visible = room.cursor_visible;
       }

       if (assets.scripts) {
+        this.pendingScripts = 0;
         assets.scripts.forEach(elation.bind(this, function(s) {
           var script = elation.engine.assets.find('script', s.src);
+          this.pendingScripts++;
           elation.events.add(script, 'asset_load', elation.bind(this, function() {
             document.head.appendChild(script);
             script.onload = elation.bind(this, this.doScriptOnload);
@@ -547,10 +450,8 @@ setTimeout(elation.bind(this, function() {
       }

       //if (!this.active) {
-        this.setActive();
+      //  this.setActive();
       //}
-
-      elation.events.fire({type: 'janus_room_load', element: this});
       //this.showDebug();
     }
     this.getRoomData = function(xml, room) {
@@ -561,6 +462,8 @@ setTimeout(elation.bind(this, function() {
       var images = this.getAsArray(elation.utils.arrayget(room, '_children.image', [])); 
       var image3ds = this.getAsArray(elation.utils.arrayget(room, '_children.image3d', [])); 
       var texts = this.getAsArray(elation.utils.arrayget(room, '_children.text', [])); 
+      var paragraphs = this.getAsArray(elation.utils.arrayget(room, '_children.paragraph', [])); 
+      var lights = this.getAsArray(elation.utils.arrayget(room, '_children.light', [])); 
       var videos = this.getAsArray(elation.utils.arrayget(room, '_children.video', [])); 

       var orphanobjects = this.getAsArray(elation.utils.arrayget(xml, 'fireboxroom._children.object')); 
@@ -569,6 +472,8 @@ setTimeout(elation.bind(this, function() {
       var orphanvideos = this.getAsArray(elation.utils.arrayget(xml, 'fireboxroom._children.video')); 
       var orphanimages = this.getAsArray(elation.utils.arrayget(xml, 'fireboxroom._children.image')); 
       var orphantexts = this.getAsArray(elation.utils.arrayget(xml, 'fireboxroom._children.text')); 
+      var orphanparagraphs = this.getAsArray(elation.utils.arrayget(xml, 'fireboxroom._children.paragraph')); 
+      var orphanlights = this.getAsArray(elation.utils.arrayget(xml, 'fireboxroom._children.light')); 

       if (orphanobjects && orphanobjects[0]) objects.push.apply(objects, orphanobjects);
       if (links && orphanlinks[0]) links.push.apply(links, orphanlinks);
@@ -576,6 +481,8 @@ setTimeout(elation.bind(this, function() {
       if (videos && orphanvideos[0]) videos.push.apply(videos, orphanvideos);
       if (sounds && orphansounds[0]) sounds.push.apply(sounds, orphansounds);
       if (texts && orphantexts[0]) texts.push.apply(texts, orphantexts);
+      if (paragraphs && orphanparagraphs[0]) paragraphs.push.apply(paragraphs, orphanparagraphs);
+      if (lights && orphanlights[0]) lights.push.apply(lights, orphanlights);

       return {
         assets: assets,
@@ -586,6 +493,8 @@ setTimeout(elation.bind(this, function() {
         images: images.map(elation.bind(this, this.parseNode)),
         image3ds: image3ds.map(elation.bind(this, this.parseNode)),
         texts: texts.map(elation.bind(this, this.parseNode)),
+        paragraphs: paragraphs.map(elation.bind(this, this.parseNode)),
+        lights: lights.map(elation.bind(this, this.parseNode)),
         videos: videos.map(elation.bind(this, this.parseNode)),
       };
     }
@@ -639,10 +548,11 @@ setTimeout(elation.bind(this, function() {
       var websurfaceassets = this.getAsArray(elation.utils.arrayget(assetxml, "_children.assetwebsurface", [])); 
       var assetlist = [];
       var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
-      imageassets.forEach(function(n) { 
+      imageassets.forEach(elation.bind(this, function(n) { 
         var src = (n.src.match(/^file:/) ? n.src.replace(/^file:/, datapath) : n.src);
-        assetlist.push({ assettype:'image', name:n.id, src: src }); 
-      });
+        //src = (src.match(/^(https?:)?\/\//) ? src : this.baseurl + src);
+        assetlist.push({ assettype:'image', name:n.id, src: src, baseurl: this.baseurl }); 
+      }));
       videoassets.forEach(elation.bind(this, function(n) { 
         var src = (n.src.match(/^file:/) ? n.src.replace(/^file:/, datapath) : n.src);
         assetlist.push({ 
@@ -653,6 +563,16 @@ setTimeout(elation.bind(this, function() {
           sbs3d: n.sbs3d == 'true',  
           ou3d: n.ou3d == 'true',  
           auto_play: n.auto_play == 'true',  
+          baseurl: this.baseurl
+        }); 
+      }));
+      soundassets.forEach(elation.bind(this, function(n) { 
+        var src = (n.src.match(/^file:/) ? n.src.replace(/^file:/, datapath) : n.src);
+        assetlist.push({ 
+          assettype:'sound', 
+          name:n.id, 
+          src: src,
+          baseurl: this.baseurl
         }); 
       }));
       websurfaceassets.forEach(elation.bind(this, function(n) { this.websurfaces[n.id] = n; }));
@@ -661,7 +581,8 @@ setTimeout(elation.bind(this, function() {
         assetlist.push({ 
           assettype:'script', 
           name: src,
-          src: src
+          src: src,
+          baseurl: this.baseurl
         }); 
       }));
       elation.engine.assets.loadJSON(assetlist, this.baseurl); 
@@ -695,9 +616,9 @@ setTimeout(elation.bind(this, function() {
       }));

       nodeinfo.pos = (n.pos ? (elation.utils.isArray(n.pos) ? n.pos : n.pos.split(' ')).map(parseFloat) : [0,0,0]);
-      nodeinfo.scale = (n.scale ? (elation.utils.isArray(n.scale) ? n.scale : n.scale.split(' ')).map(parseFloat) : [1,1,1]);
+      nodeinfo.scale = (n.scale ? (elation.utils.isArray(n.scale) ? n.scale : (n.scale instanceof THREE.Vector3 ? n.scale.toArray() : n.scale.split(' '))).map(parseFloat) : [1,1,1]);
       nodeinfo.orientation = this.getOrientation(n.xdir, n.ydir || n.up, n.zdir || n.fwd);
-      nodeinfo.col = (n.col ? (n.col[0] == '#' ? [parseInt(n.col.substr(1,2), 16)/255, parseInt(n.col.substr(3, 2), 16)/255, parseInt(n.col.substr(5, 2), 16)/255] : (elation.utils.isArray(n.col) ? n.col : n.col.split(' '))) : null);
+      nodeinfo.col = (n.col ? (n.col[0] == '#' ? [parseInt(n.col.substr(1,2), 16)/255, parseInt(n.col.substr(3, 2), 16)/255, parseInt(n.col.substr(5, 2), 16)/255] : n.col) : null);

       var minscale = 1e-6;
 /*
@@ -723,6 +644,7 @@ setTimeout(elation.bind(this, function() {
       return false;
     }
     this.enable = function() {
+      this.engine.systems.ai.add(this);
       var keys = Object.keys(this.children);
       for (var i = 0; i < keys.length; i++) {
         var obj = this.children[keys[i]];
@@ -733,6 +655,7 @@ setTimeout(elation.bind(this, function() {
       elation.events.fire({type: 'room_enable', data: this});
     }
     this.disable = function() {
+      this.engine.systems.ai.remove(this);
       var keys = Object.keys(this.children);
       for (var i = 0; i < keys.length; i++) {
         var obj = this.children[keys[i]];
@@ -753,6 +676,11 @@ setTimeout(elation.bind(this, function() {
       var edit = xml.edit._children;
       var keys = Object.keys(edit);
       var hasNew = false;
+
+      var waslocked = this.locked;
+      //this.locked = true;
+      this.applyingEdits = true;
+      var skip = ['sync'];
       keys.forEach(elation.bind(this, function(k) {
         var newobjs = edit[k];
         if (!elation.utils.isArray(newobjs)) newobjs = [newobjs];
@@ -772,25 +700,39 @@ setTimeout(elation.bind(this, function() {
           sounds: [],
           texts: [],
         };
+//console.log('GOT EDIT', editxml);
         for (var i = 0; i < newobjs.length; i++) {
           var newobj = newobjs[i],
               existing = this.jsobjects[newobj.js_id];
+          this.appliedchanges[newobj.jsid] = true;
           if (existing) {
-            existing.setProperties(newobj);
+            //existing.setProperties(newobj);
+            var objkeys = Object.keys(newobj);
+            for (var j = 0; j < objkeys.length; j++) {
+              if (skip.indexOf(objkeys[j]) == -1) {
+                existing[objkeys[j]] = newobj[objkeys[j]];
+              }
+            }
           } else {
             hasNew = true;
+            newobj.sync = false;
             diff[(k.toLowerCase() + 's')].push(newobj);
-            if (newobj.id.match(/^https?:/)) {
+            if (newobj.id && newobj.id.match(/^https?:/)) {
               diff.assets.objects.push({assettype: 'model', name: newobj.id, src: newobj.id});
             }
-            console.log('create new!', newobj.js_id, newobj);
+            //console.log('create new!', newobj.js_id, newobj);
           }
         }
         if (hasNew) {
-          elation.engine.assets.loadJSON(diff.assets.objects, this.baseurl); 
+          //elation.engine.assets.loadJSON(diff.assets.objects, this.baseurl); 
           this.createRoomObjects(diff);
         }
       }));
+      this.applyingEdits = false;
+      setTimeout(elation.bind(this, function() {
+        //this.locked = waslocked;
+        //this.appliedchanges = {};
+      }), 0);
     }
     this.applyDeleteXML = function(deletexml) {
       var del = elation.utils.parseXML(deletexml);
@@ -806,50 +748,191 @@ setTimeout(elation.bind(this, function() {
     this.createObject = function(type, args) {
       var typemap = {
         'object': 'janusobject',
-        'link': 'januslink',
+        'link': 'janusportal',
         'text': 'janustext',
         'image': 'janusimage',
         'image3d': 'janusimage',
         'video': 'janusvideo',
+        'sound': 'janussound',
+        'light': 'januslight',
       };
       var realtype = typemap[type.toLowerCase()] || type;
-        var nargs = { 
-          'room': this,
-          'janus': this.properties.janus,
-          'js_id': args.js_id,
-          'position': args.pos || '0 0 0',
-          'orientation': args.orientation || '0 0 0 1',
-          'scale': args.scale || '1 1 1',
-          'image_id': args.id,
-          'color': args.col,
-          'lighting': args.lighting
-        }; 
+      //var thingname = args.id + (args.js_id ? '_' + args.js_id : '_' + Math.round(Math.random() * 1000000));
+      var thingname = args.js_id;
+      var objectargs = {
+        'room': this,
+        'janus': this.properties.janus,
+        'position': args.pos,
+        'velocity': args.vel,
+        'pickable': true,
+        'collidable': true
+      };
+/*
+        'js_id': args.js_id,
+        'scale': args.scale,
+        'orientation': args.orientation,
+        'visible': args.visible,
+        'rotate_axis': args.rotate_axis,
+        'rotate_deg_per_sec': args.rotate_deg_per_sec,
+        fwd: args.fwd,
+        xdir: args.xdir,
+        ydir: args.ydir,
+        zdir: args.zdir,
+      };
+*/
+      elation.utils.merge(args, objectargs);
+
+      switch (realtype) {
+        case 'janusobject':
+          elation.utils.merge({ 
+            'janusid': args.id, 
+          }, objectargs); 
+          break;
+        case 'janustext':
+          elation.utils.merge({
+            'text': args.text || args._content || ' ',
+            'col': args.col, //(args.col ? new THREE.Color().setRGB(args.col[0], args.col[1], args.col[2]) : new THREE.Color(0xffffff)),
+          }, objectargs);
+          break;
+        case 'janusportal':
+          // If it's an absolute URL or we have a translator for this URL type, use the url unmodified.  Otherwise, treat it as relative
+          var linkurl = (args.url.match(/^(https?:)?\/\//) || this.getTranslator(args.url) ? args.url : this.baseurl + args.url);
+          objectargs.url = linkurl;
+          break;
+        case 'janusimage':
+          objectargs.image_id = args.id;
+          break;
+        case 'janussound':
+          objectargs.sound_id = args.id;
+          objectargs.distance = parseFloat(args.dist);
+          //objectargs.volume = args.scale[0];
+          break;
+      }
       if (elation.engine.things[realtype]) {
-console.log('spawn it', realtype, args, nargs);
-        var object = this.spawn(realtype, args.js_id, nargs);
-        if (args.js_id) {
-          this.jsobjects[args.js_id] = object.getProxyObject();
+        //console.log('spawn it', realtype, args, objectargs);
+        if (!objectargs.js_id) {
+          objectargs.js_id = realtype + '_' + (objectargs.id ? objectargs.id + '_' : '') + window.uniqueId();
+        }
+        if (this.jsobjects[objectargs.js_id]) {
+          objectargs.js_id = objectargs.js_id + '_' + window.uniqueId();
+        }
+        var object = this.spawn(realtype, objectargs.js_id, objectargs);
+        if (objectargs.js_id) {
+          this.jsobjects[objectargs.js_id] = object.getProxyObject();
+        }
+
+        if (realtype == 'janussound') {
+          this.sounds[objectargs.js_id] = object;
         }
+
+        elation.events.add(object, 'thing_change_queued', elation.bind(this, this.onThingChange));
+
+        return this.jsobjects[objectargs.js_id];
       } else {
         console.log('ERROR - unknown type: ', realtype);
       }
     }
+    this.removeObject = function(obj) {
+      var proxy = obj;
+      if (elation.utils.isString(obj)) {
+        proxy = this.jsobjects[obj];
+      }
+      if (proxy) {
+        var obj = this.getObjectFromProxy(proxy);
+        if (obj && obj.parent) {
+          obj.parent.remove(obj);
+        }
+      }
+    }
     this.addCookie = function(name, value) {
       this.cookies[name] = value;
     }
     this.doScriptOnload = function() {
-      elation.events.fire({type: 'janus_room_scriptload', element: this});
+      if (--this.pendingScripts <= 0) {
+        elation.events.fire({type: 'janus_room_scriptload', element: this});
+      }
     }
     this.playSound = function(name) {
       if (this.sounds[name]) {
         this.sounds[name].play();
       }
     }
+    this.stopSound = function(name) {
+      if (this.sounds[name]) {
+        this.sounds[name].stop();
+      }
+    }
     this.onKeyDown = function(ev) { 
       elation.events.fire({type: 'janus_room_keydown', element: this, extras: { keyCode: ev.key.toUpperCase() }});
     }
     this.onKeyUp = function(ev) { 
       elation.events.fire({type: 'janus_room_keyup', element: this, extras: { keyCode: ev.key.toUpperCase() }});
     }
+    this.onMouseDown = function(ev) { 
+      elation.events.fire({type: 'janus_room_mousedown', element: this});
+    }
+    this.onMouseUp = function(ev) { 
+      elation.events.fire({type: 'janus_room_mouseup', element: this});
+    }
+    this.onThingChange = function(ev) {
+      var thing = ev.target;
+      if (!this.applyingEdits && thing.js_id && this.jsobjects[thing.js_id]) {
+        var proxy = this.jsobjects[thing.js_id];
+        if (proxy.sync) {
+          var k = Object.keys(proxy);
+          if (!this.appliedchanges[thing.js_id]) {
+            this.changes[thing.js_id] = proxy;
+          }
+        }
+      }
+    }
+    this.onRoomEdit = function(ev) {
+      var thing = ev.data;
+      if (thing && !this.applyingEdits && !this.appliedchanges[thing.js_id] && thing.js_id && this.jsobjects[thing.js_id]) {
+        this.changes[thing.js_id] = this.jsobjects[thing.js_id];
+      }
+    }
+    this.onScriptTick = function(ev) {
+      this.engine.systems.world.scene['world-3d'].updateMatrix(true);
+      this.engine.systems.world.scene['world-3d'].updateMatrixWorld(true);
+      for (var k in this.jsobjects) {
+        var realobj = this.getObjectFromProxy(this.jsobjects[k]);
+        if (realobj) {
+          realobj.updateVectors(false);
+        }
+      }
+      this.janus.scriptframeargs[0] = ev.data.delta * 1000;
+      (function(room) {
+        elation.events.fire({element: room, type: 'janusweb_script_frame'});
+        elation.events.fire({element: room, type: 'janusweb_script_frame_end'});
+      })(this);
+    }
+    this.getObjectFromProxy = function(proxy, children) {
+      return proxy._target;
+/*
+        for (var k in this.children) {
+          var realobj = this.children[k];
+
+          if (realobj.js_id == proxy.js_id) {
+            return realobj;
+          }
+        }
+*/
+      if (!children) children = this.children;
+      var obj = children[proxy.js_id];
+      if (obj) {
+        return obj;
+      }
+/*
+      var childids = Object.keys(children);
+      for (var i = 0; i < childids.length; i++) {
+        var childobj = children[childids[i]];
+        var realobj = this.getObjectFromProxy(proxy, childobj.children);
+        if (realobj) {
+          return realobj;
+        }
+      }
+*/
+    }
   }, elation.engine.things.generic);
 });
diff --git a/scripts/sound.js b/scripts/sound.js
index f10da3c15acda240f82189219823300a19dd536d..
index ..70311cef4e00af33aea49b768be17bafa3c38338 100644
--- a/scripts/sound.js
+++ b/scripts/sound.js
@@ -3,11 +3,10 @@ elation.require(['janusweb.janusbase'], function() {
     this.postinit = function() {
       elation.engine.things.janussound.extendclass.postinit.call(this);
       this.defineProperties({
-        id: { type: 'string' },
-        src: { type: 'string' },
+        sound_id: { type: 'string' },
         rect: { type: 'string', default: "0 0 0 0" },
         loop: { type: 'boolean', default: false },
-        autoplay: { type: 'boolean', default: true },
+        auto_play: { type: 'boolean', default: true },
         play_once: { type: 'boolean', default: false },
         dist: { type: 'float', default: 1.0 },
         pitch: { type: 'float', default: 1.0 },
@@ -20,10 +19,13 @@ elation.require(['janusweb.janusbase'], function() {
     }
     this.createChildren = function() {
       if (!this.audio) {
-        this.createAudio(this.properties.src);
+        var sound = elation.engine.assets.find('sound', this.sound_id);
+        if (sound) {
+          this.createAudio(sound.getProxiedURL());
+        }
       }
     }
-    this.createAudio = function() {
+    this.createAudio = function(src) {
       if (this.audio) {
         if (this.audio.isPlaying) {
           this.audio.stop();
@@ -42,18 +44,18 @@ elation.require(['janusweb.janusbase'], function() {
         } else {
           this.audio.panner.distanceModel = 'linear';
         }
-        this.audio.autoplay = this.properties.autoplay;
-        this.audio.setLoop(this.properties.loop);
-        this.audio.setVolume(this.properties.gain);
-        if (this.properties.src) {
-          this.audio.load(this.properties.src);
+        this.audio.autoplay = this.auto_play;
+        this.audio.setLoop(this.loop);
+        this.audio.setVolume(this.gain);
+        if (src) {
+          this.audio.load(src);
+        } else {
         }
         this.objects['3d'].add(this.audio);
-console.log('MADE AUDIO', this.audio);
       }
     }
     this.load = function(url) {
-      this.properties.src = url;
+      this.src = url;
       if (this.audio.isPlaying) {
         this.audio.stop();
       }
@@ -61,7 +63,11 @@ console.log('MADE AUDIO', this.audio);
     }
     this.play = function() {
       if (this.audio && this.audio.source.buffer) {
-        this.audio.play();
+        if (this.audio.isPlaying) {
+          this.audio.source.currentTime = 0;
+        } else {
+          this.audio.play();
+        }
       }
     }
     this.pause = function() {
diff --git a/scripts/text.js b/scripts/text.js
index 6038bea4418c2f9f2ec6909872f5f7f9adbeed61..
index ..e71ae5c6e94baa97fe51340790d64d57e9d5a784 100644
--- a/scripts/text.js
+++ b/scripts/text.js
@@ -2,46 +2,171 @@ elation.require(['engine.things.label'], function() {
   elation.component.add('engine.things.janustext', function() {
     this.postinit = function() {
       elation.engine.things.janustext.extendclass.postinit.call(this);
+      this.frameupdates = [];
+      this.textcache = [];
       this.defineProperties({
-        room: { type: 'object' },
-        js_id: { type: 'string' },
-        color: { type: 'color', default: 0xffffff },
-        lighting: { type: 'boolean', default: true },
+        'text':            { type: 'string', default: '', refreshGeometry: true },
+        'font':            { type: 'string', default: 'helvetiker', refreshGeometry: true },
+        'font_size':       { type: 'float', default: 1.0, refreshGeometry: true },
+        'align':           { type: 'string', default: 'left', refreshGeometry: true },
+        'verticalalign':   { type: 'string', default: 'bottom', refreshGeometry: true },
+        'zalign':          { type: 'string', default: 'back', refreshGeometry: true },
+        'emissive':        { type: 'color', default: 0x000000 },
+        'opacity':         { type: 'float', default: 1.0 },
+        'depthTest':       { type: 'bool', default: true },
+        'thickness':       { type: 'float', refreshGeometry: true },
+        'segments':        { type: 'int', default: 6, refreshGeometry: true },
+        'bevel.enabled':   { type: 'bool', default: false, refreshGeometry: true },
+        'bevel.thickness': { type: 'float', default: 0, refreshGeometry: true },
+        'bevel.size':      { type: 'float', default: 0, refreshGeometry: true },
       });
-      this.properties.size = 1;
       this.properties.thickness = .11;
       this.properties.align = 'center';
+      elation.events.add(this.engine, 'engine_frame', elation.bind(this, this.handleFrameUpdates));
     }
     this.createObject3D = function() {
-      var text = this.properties.text || this.name;
-      var geometry = this.createTextGeometry(text);
+      var text = this.properties.text || '';//this.name;
+      var geometry = this.textcache[text];
+      if (!geometry) {
+        geometry = this.createTextGeometry(text);

-      geometry.computeBoundingBox();
+        geometry.computeBoundingBox();

-      var geosize = new THREE.Vector3().subVectors(geometry.boundingBox.max, geometry.boundingBox.min);
-      var geoscale = 1 / geosize.x;
-      geometry.applyMatrix(new THREE.Matrix4().makeScale(geoscale, geoscale, geoscale));
+        var geosize = new THREE.Vector3().subVectors(geometry.boundingBox.max, geometry.boundingBox.min);
+        var geoscale = 1 / Math.max(1e-6, text.length * this.font_size);
+        geometry.applyMatrix(new THREE.Matrix4().makeScale(geoscale, geoscale, geoscale));

-      this.material = new THREE.MeshPhongMaterial({color: this.properties.color, emissive: this.properties.emissive, shading: THREE.SmoothShading, depthTest: this.properties.depthTest});
+        if (this.properties.opacity < 1.0) {
+          this.material.opacity = this.properties.opacity;
+          this.material.transparent = true;
+        }

-      if (this.properties.opacity < 1.0) {
-        this.material.opacity = this.properties.opacity;
-        this.material.transparent = true;
+        this.textcache[text] = geometry;
       }
+      var mesh;
+      if (this.objects['3d']) {
+        mesh = this.objects['3d'];
+        mesh.geometry = geometry;
+      } else {
+        this.material = this.createTextMaterial(text);
+        mesh = new THREE.Mesh(geometry, this.material);

-      var mesh = new THREE.Mesh(geometry, this.material);
+      }

       return mesh;
     }
+    this.createTextMaterial = function() {
+      var matargs = {
+        color: this.properties.color || new THREE.Color(0xffffff), 
+        emissive: new THREE.Color(0xff0000), //this.properties.emissive, 
+        shading: THREE.SmoothShading, 
+        depthTest: this.properties.depthTest
+      };
+      var material = new THREE.MeshPhongMaterial(matargs);
+
+      if (this.properties.opacity < 1.0) {
+        material.opacity = this.properties.opacity;
+        material.transparent = true;
+      }
+      return material;
+    }
+    this.createTextGeometry = function(text) {
+      var font = elation.engine.assets.find('font', this.properties.font);
+      if (!font) font = elation.engine.assets.find('font', 'helvetiker');
+if (!window.GEOMCACHE) window.GEOMCACHE = {};
+if (GEOMCACHE[text]) return GEOMCACHE[text];
+
+      var geometry = new THREE.TextGeometry( text, {
+        size: this.font_size,
+        height: this.properties.thickness || this.font_size / 2,
+        curveSegments: this.segments,
+
+        font: font,
+        weight: "normal",
+        style: "normal",
+
+        bevelThickness: this.properties.bevel.thickness,
+        bevelSize: this.properties.bevel.size,
+        bevelEnabled: this.properties.bevel.enabled
+      });                                                
+      geometry.computeBoundingBox();
+      var bbox = geometry.boundingBox;
+      var diff = new THREE.Vector3().subVectors(bbox.max, bbox.min);
+      var geomod = new THREE.Matrix4();
+      // horizontal alignment
+      if (this.properties.align == 'center') {
+        geomod.makeTranslation(-.5 * diff.x, 0, 0);
+        geometry.applyMatrix(geomod);
+      } else if (this.properties.align == 'right') {
+        geomod.makeTranslation(-1 * diff.x, 0, 0);
+        geometry.applyMatrix(geomod);
+      }
+
+      // vertical alignment
+      if (this.properties.verticalalign == 'middle') {
+        geomod.makeTranslation(0, -.5 * diff.y, 0);
+        geometry.applyMatrix(geomod);
+      } else if (this.properties.verticalalign == 'top') {
+        geomod.makeTranslation(0, -1 * diff.y, 0);
+        geometry.applyMatrix(geomod);
+      }
+
+      // z-alignment
+      if (this.properties.zalign == 'middle') {
+        geomod.makeTranslation(0, 0, -.5 * diff.z);
+        geometry.applyMatrix(geomod);
+      } else if (this.properties.zalign == 'front') {
+        geomod.makeTranslation(0, 0, -1 * diff.z);
+        geometry.applyMatrix(geomod);
+      }
+      geometry.computeBoundingBox();
+GEOMCACHE[text] = geometry;
+      return geometry;
+    }
+    this.setText = function(text) {
+      this.properties.text = text;
+      if (text.indexOf && text.indexOf('\n') != -1) {
+        this.setMultilineText(text);
+      } else {
+        this.objects['3d'].geometry = this.createTextGeometry(text);
+      }
+      if (!this.material) {
+        this.material = this.createTextMaterial(text);
+      }
+      this.objects['3d'].material = this.material;
+      this.refresh();
+   }
+   this.setMultilineText = function(text) {
+      var lines = text.split('\n');
+      var geometry = new THREE.Geometry();
+      var linematrix = new THREE.Matrix4();
+      var lineoffset = 0;
+      var lineheight = 0;
+      for (var i = 0; i < lines.length; i++) {
+        var linegeometry = this.createTextGeometry(lines[i]);
+        linematrix.makeTranslation(0, lineoffset, 0);
+        geometry.merge(linegeometry, linematrix);
+        if (!lineheight) {
+          var bboxdiff = new THREE.Vector3().subVectors(linegeometry.boundingBox.max, linegeometry.boundingBox.min);
+          lineheight = bboxdiff.y;
+        }
+        lineoffset -= lineheight * 1.2;
+      }
+      this.objects['3d'].geometry = geometry;
+    }
     this.getProxyObject = function() {
-      return new elation.proxy(this, {
-        id: ['property', 'properties.id'],
-        js_id: ['property', 'properties.js_id'],
-        pos: ['property', 'properties.position'],
-        vel: ['property', 'properties.velocity'],
-        col: ['property', 'properties.color'],
-        text: ['property', 'properties.text'],
-      });
+      var proxy = elation.engine.things.janustext.extendclass.getProxyObject.call(this);
+
+      proxy._proxydefs = {
+        text:  [ 'property', 'text'],
+        emissive:  [ 'property', 'emissive'],
+      };
+      return proxy;
+    }
+    this.updateMaterial = function() {
+      if (this.material) {
+        this.material.color = this.color;
+      }
     }
-  }, elation.engine.things.label);
+  }, elation.engine.things.janusbase);
 });
diff --git a/scripts/tracking.js b/scripts/tracking.js
index b2f7c2496d800c93ddbc48a9b28dc17cea4d00ee..
index ..d9f197df4b14e62b89fc84fe9502c715ee6ce93c 100644
--- a/scripts/tracking.js
+++ b/scripts/tracking.js
@@ -45,11 +45,13 @@ elation.require([], function() {
       console.log('[tracking] client connected', ev);
       ga('send', 'event', 'client', 'connected', ev.data);
     });
+/*
     elation.events.add(document, 'pointerlockchange,mozpointerlockchange', function(ev) {
 var el = document.pointerLockElement || document.mozPointerLockElement
       console.log('[tracking] pointer lock!', (el !== null), ev);
       ga('send', 'event', 'player', 'pointerlock', (el !== null));
     });
+*/
     elation.events.add(null, 'janusweb_client_disconnected', function(ev) {
       console.log('[tracking] client disconnected', ev);
       ga('send', 'event', 'client', 'disconnected', ev.data);
@@ -133,6 +135,7 @@ var el = document.pointerLockElement || document.mozPointerLockElement
       }, 1000);

       // report FPS every 15 seconds
+/*
       var stats = document.getElementById('fpsText');
       if (stats) {
         setInterval(function() {
@@ -141,6 +144,7 @@ var el = document.pointerLockElement || document.mozPointerLockElement
           ga('send', 'event', 'engine', 'fps', fps);
         }, 15000);
       }
+*/

     });
     elation.events.add(window, 'error', function(msg) {
diff --git a/scripts/video.js b/scripts/video.js
index c05459e0037a90cfd1581ab7d21ef890426ecf36..
index ..adffb3150630e4c9f55b5112c1bc5d9f4c8140d1 100644
--- a/scripts/video.js
+++ b/scripts/video.js
@@ -26,7 +26,7 @@ elation.require(['janusweb.janusbase'], function() {
     }
     this.createMaterial = function() {
       if (this.asset) {
-        var texture = this.texture = this.asset.getAsset();
+        var texture = this.texture = this.asset.getInstance();
         if (this.asset.sbs3d) {
           texture.repeat.x = 0.5;
         }
@@ -63,7 +63,7 @@ elation.require(['janusweb.janusbase'], function() {
       return {width: image.videoWidth, height: image.videoHeight};
     }
     this.click = function() {
-      var texture = this.asset.getAsset();
+      var texture = this.asset.getInstance();
       var video = texture.image;
       if (video.currentTime > 0 && !video.paused && !video.ended) {
         video.pause();
diff --git a/templates/janusweb.tpl b/templates/janusweb.tpl
index dfabe7a2f3bcfc7e6d2fca989f227377a518eb71..
index ..b8c7e24d4aae458596c9ddee0e020fc7b3818e32 100644
--- a/templates/janusweb.tpl
+++ b/templates/janusweb.tpl
@@ -1,3 +1,10 @@
 {dependency name="janusweb.client"}
-
+
{set var="page.title"}JanusWeb{/set} + diff --git a/tests/janusweb.test.js b/tests/janusweb.test.js
index 023f75cc3bed9ff8d397ea6d8b9a0eb937d07e05..
index ..03587f2aaa88f3eb2e98a28dbbd8996b4dee66da 100644
--- a/tests/janusweb.test.js
+++ b/tests/janusweb.test.js
@@ -1,3 +1,4 @@
+/*
 describe("JanusWeb Init", function() {
   jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
   var client, janusweb, canvas;
@@ -97,3 +98,4 @@ describe("JanusWeb Init", function() {
     client.engine.stop();
   });
 });
+*/
diff --git a/tests/karma.conf.js b/tests/karma.conf.js
index c62d6dd132a1a3730f0565d811da9ebb4e23885f..
index ..e83e8860e6ec232a67e7db2f175babac942df63f 100644
--- a/tests/karma.conf.js
+++ b/tests/karma.conf.js
@@ -15,12 +15,14 @@ module.exports = function(config) {

     // list of files / patterns to load in the browser
     files: [
-      {pattern: 'node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js', watched: false, included: true, served: true},
-      {pattern: 'tests/boot.js', watched: true, included: true, served: true},
+      //{pattern: 'node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js', watched: false, included: true, served: true},
+      //{pattern: 'tests/boot.js', watched: true, included: true, served: true},
       {pattern: 'build/*', watched: true, included: true, served: true},
+      {pattern: 'build/media/*', watched: false, included: false, served: true},
       {pattern: 'build/media/**', watched: false, included: false, served: true},
       'tests/imagediff.js',
-      {pattern: 'tests/*.test.js', watched: true, included: false, served: true},
+      //{pattern: 'tests/*.test.js', watched: true, included: true, served: true},
+      {pattern: 'tests/assets/*.test.js', watched: true, included: true, served: true},
     ],


-----END OF PAGE-----